Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

does the ssr support apollo subscription ? #123

Open
Kingwzb opened this issue Mar 28, 2020 · 3 comments
Open

does the ssr support apollo subscription ? #123

Kingwzb opened this issue Mar 28, 2020 · 3 comments

Comments

@Kingwzb
Copy link

Kingwzb commented Mar 28, 2020

Firstly, thanks for you good work to share such useful module and saved me lots of time!

I managed to use isomorphic-ws to create the ws link for apollo client on both side and passed getDataFromTree to withApollo HOC on my page.

For react component using useQuery hook, the ssr works correctly but for component using useSubscription hook, I see 6 subscriptions other than 1 got fired and the page source code is still showing 'loading' instead of sever rendered context with initial data from subscription

is there any working example to do ssr rendering with initial subscription data ?

@positonic
Copy link

I have the same question. It calls my Apollo server and renders the page, but subscriptions don't work. Which kinda makes sense to me since it was server side rendered, and the page is rendered - done job...

I'm new to next.js so not sure if/how subscription will work after render in this case...

@hoangvvo
Copy link
Contributor

hoangvvo commented Apr 4, 2020

I do not think subscription is meant to be used server-side. Subscription is used if you want to notify the user while they use the app, not when they load it the first time.

You should not use isomorphic-ws. Instead, simply excluding WebSocketLink for server-side:

const httpLink = createUploadLink({
  uri: process.env.API_URI,
  fetch,
  credentials: process.browser ? 'same-origin' : 'include',
});

const link = process.browser
  ? split(
      ({ query }) => {
        const definition = getMainDefinition(query);
        return (
          definition.kind === 'OperationDefinition' &&
          definition.operation === 'subscription'
        );
      },
      new WebSocketLink({
        uri: process.env.WEBSOCKET_URI,
        options: {
          reconnect: true,
        },
      }),
      httpLink
    )
  : httpLink;

@Kingwzb
Copy link
Author

Kingwzb commented Apr 5, 2020

I've figured it out using graphql query to get initial data then use subscribeToMore to trigger subscription to get realtime data, the server side rendering works fine with the initial query, hope it helps.

(isormophic-ws used below is not needed, I will remove it from my code)
with-apollo.tsx

import React from 'react';
import withApollo from 'next-with-apollo';
import {ApolloProvider} from '@apollo/react-hooks';
import {ApolloLink, split} from 'apollo-link';
import {HttpLink} from 'apollo-link-http';
import {WebSocketLink} from 'apollo-link-ws';
import {getMainDefinition} from 'apollo-utilities';
import {ApolloClient} from 'apollo-client';
import {InMemoryCache} from "apollo-cache-inmemory";
import fetch from 'isomorphic-unfetch';
import WebSocket from 'isomorphic-ws';

let ssrMode = !process.browser;
let httpURI: string = '/graphql';
let wsURI = '';
if (ssrMode) {
    const port = (process.env.PORT && parseInt(process.env.PORT, 10)) || 3000;
    httpURI = `http://localhost:${port}/graphql`;
    wsURI = `ws://localhost:${port}/graphql`;
} else {
    wsURI = 'ws://' + window.location.hostname + ':' + window.location.port + '/graphql';
}
let httpLink: ApolloLink = new HttpLink({
    uri: httpURI,
    credentials: 'same-origin',
    fetch: fetch
});
let link = httpLink;
if (!ssrMode) {
    let wsLink = new WebSocketLink({
        uri: wsURI,
        options: {
            reconnect: true,
        },
        webSocketImpl: WebSocket
    });

    link = split(
        ({query}) => {
            const def = getMainDefinition(query);
            return def.kind === 'OperationDefinition' && def.operation === 'subscription';
        },
        wsLink,
        httpLink
    );
}

export default withApollo(
    ({initialState}) =>
        new ApolloClient({
            link: link,
            ssrMode: ssrMode,
            connectToDevTools: !ssrMode,
            cache: new InMemoryCache().restore(initialState || {})
        }),
    {
        render: ({Page, props}) => {
            return (
                <ApolloProvider client={props.apollo}>
                    <Page {...props} />
                </ApolloProvider>
            );
        }
    }
);

resolvers.ts

import grpc from 'grpc';
import {MarketClient} from '../grpc/market/market_grpc_pb';
import pbMarket from '../grpc/market/market_pb';
import {Index, IndexResponse, Resolvers} from './types';
import {PubSub} from 'apollo-server-express';
import * as pb from 'google-protobuf/google/protobuf/timestamp_pb';
import dayjs from 'dayjs';

const pub = new PubSub();
let subID = 1;

const grpcPort = (process.env.GRPCPORT && parseInt(process.env.GRPCPORT, 10)) || 3001;
const grpcURI = 'localhost:' + grpcPort;
const marketClient = new MarketClient(grpcURI, grpc.credentials.createInsecure());

function formatTimestamp(timestamp: pb.Timestamp | undefined): string {
    const fmt = "YYYY-MM-DDTHH:mm:ss.SSS"
    if (timestamp) {
        let obj = timestamp.toObject();
        return dayjs(new Date(obj.seconds * 1000 + obj.nanos / 1000000)).format(fmt);
    }
    return dayjs().format(fmt);
}

function withCancel<T>(
    asyncIterator: AsyncIterator<T | undefined>,
    onCancel: () => void
): AsyncIterator<T | undefined> {
    if (!asyncIterator.return) {
        asyncIterator.return = () => Promise.resolve({value: undefined, done: true});
    }

    const savedReturn = asyncIterator.return.bind(asyncIterator);
    asyncIterator.return = () => {
        onCancel();
        return savedReturn();
    };

    return asyncIterator;
}
export const resolvers: Resolvers = {
    Query: {
        indexLevel: async (_parent, args) => {
            return new Promise<IndexResponse>((resolve, reject) => {
                let req = new pbMarket.IndexRequest();
                req.setUnderlying(args.underlying);
                marketClient.getIndexLevel(req, (error, data) => {
                    if (error) {
                        reject(error);
                    } else {
                        let index: Index = {
                            level: data.getLevel()
                        }
                        let response: IndexResponse = {
                            index: index,
                            timestamp: formatTimestamp(data.getTimestamp())
                        };
                        resolve(response);
                    }
                });
            });
        }
    },
    Subscription: {
        indexLevel: {
            subscribe: (_parent, args) => {
                let req = new pbMarket.IndexRequest();
                req.setUnderlying(args.underlying);
                let sub = new pbMarket.IndexSubscription();
                sub.setRequest(req);
                sub.setMinIntervalMs(args.minIntervalMs);
                let responses = marketClient.subscribeIndexLevel(sub);
                let topic = 'IndexLevel_' + (subID++);
                console.info('started graphql subscription', topic);
                responses.on('data', (data: pbMarket.IndexResponse) => {
                    let index: Index = {
                        level: data.getLevel()
                    };
                    let response: IndexResponse = {
                        index: index,
                        timestamp: formatTimestamp(data.getTimestamp())
                    }
                    pub.publish(topic, {indexLevel: response});
                });
                responses.on('error', (err) => {
                    console.warn(topic, err.message);
                });
                return withCancel(pub.asyncIterator(topic), () => {
                    console.info('cancelling graphql subscription', topic);
                    responses.cancel();
                });
            }
        }
    }
};

pages¥index.tsx

import React, {useEffect} from 'react';
import {useQuery} from '@apollo/react-hooks';
import {getDataFromTree} from '@apollo/react-ssr';
import gql from 'graphql-tag';
import withApollo from '../api/graphql/with-apollo';

const INDEX_QUERY = gql`
    query {
        indexLevel(underlying:"BTC-USD") {
            index {
                level
            }
            timestamp
        }
    }
`;

const INDEX_SUBSCRIPTION = gql`
    subscription {
        indexLevel(underlying:"BTC-USD" minIntervalMs:1000) {
            index {
                level
            }
            timestamp
        }
    }
`;
const IndexLevel = () => {
    const {loading, data, subscribeToMore} = useQuery(INDEX_QUERY);
    useEffect(() => {
        const unsub = subscribeToMore({
            document: INDEX_SUBSCRIPTION,
            updateQuery: (prev, {subscriptionData}) => {
                if (!subscriptionData.data) {
                    return prev;
                }
                return subscriptionData.data;
            }
        });
        return () => {
            unsub();
        }
    }, [subscribeToMore]);
    if (loading || !data) {
        return <p>loading</p>;
    }
    return <p>{data.indexLevel.index.level} {data.indexLevel.timestamp}</p>;
};

const Home = () => (
    <div>
        <IndexLevel/>
    </div>
)
export default withApollo(Home,
    {getDataFromTree}
);

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants