I'm trying to enhance GraphQL Relay so that it can sync its store across different tabs of the same application. To do so I'm extending the Store and the RecordSource classes.
The RecordSource class becomes this:
import { RecordSource } from 'relay-runtime';
const sourceId = Math.random();
export class CrossTabRecordSource extends RecordSource {
broadcastChannel: BroadcastChannel;
constructor(records?: RecordSource['constructor']['arguments'][0]) {
super(records);
this.broadcastChannel = new BroadcastChannel(
'relay-cross-tab-record-source'
);
this.broadcastChannel.onmessage = (event) => {
if (event.data.sourceId === sourceId) {
return;
}
const { operation, dataID, record } = event.data;
switch (operation) {
case 'set':
super.set(dataID, record);
break;
case 'delete':
super.delete(dataID);
break;
}
};
}
set(
dataID: RecordSource['set']['arguments'][0],
record: RecordSource['set']['arguments'][1]
): ReturnType<RecordSource['set']> {
super.set(dataID, record);
this.broadcastChannel.postMessage({
sourceId,
operation: 'set',
dataID,
record,
});
}
delete(
dataID: RecordSource['delete']['arguments'][0]
): ReturnType<RecordSource['delete']> {
super.delete(dataID);
this.broadcastChannel.postMessage({
sourceId,
operation: 'delete',
dataID,
});
}
}
The Store becomes this:
import { Store } from 'relay-runtime';
export let notifyListenerPaused = false;
export class CrossTabStore extends Store {
broadcastChannel: BroadcastChannel;
constructor(
source: Store['constructor']['arguments'][0],
options?: Store['constructor']['arguments'][1]
) {
super(source, options);
this.broadcastChannel = new BroadcastChannel('relay-cross-tab-store');
}
notifyListenerTimeout: number | undefined;
notify(
sourceOperation?: Store['notify']['arguments'][0],
invalidateStore?: Store['notify']['arguments'][1]
): ReturnType<Store['notify']> {
notifyListenerPaused = true;
clearTimeout(this.notifyListenerTimeout);
this.notifyListenerTimeout = setTimeout(() => {
notifyListenerPaused = false;
}, 2000);
this.broadcastChannel.postMessage({
operation: 'notify',
sourceOperation,
invalidateStore,
});
return super.notify(sourceOperation, invalidateStore);
}
}
My RelayEnvironment.ts is:
import { Environment, Network } from 'relay-runtime';
import type { FetchFunction } from 'relay-runtime';
import { fetchGraphQL } from './fetchGraphQL';
import { CrossTabRecordSource } from './CrossTabRecordSource';
import { RecordMap } from 'relay-runtime/lib/store/RelayStoreTypes';
import { CrossTabStore, notifyListenerPaused } from './CrossTabStore';
// Relay passes a "params" object with the query name and text. So we define a helper function
// to call our fetchGraphQL utility with params.text.
async function fetchRelay(
params: FetchFunction['arguments'][0],
variables: FetchFunction['arguments'][1]
) {
console.log(
`fetching query ${params.name} with ${JSON.stringify(variables)}`
);
return fetchGraphQL(params.text, variables);
}
export function initRelayEnvironment(records: RecordMap) {
const source = new CrossTabRecordSource(records);
const store = new CrossTabStore(source);
const environment = new Environment({
network: Network.create(fetchRelay),
store,
});
store.broadcastChannel.onmessage = async (event) => {
if (notifyListenerPaused) return;
const { operation, sourceOperation, invalidateStore } = event.data;
switch (operation) {
case 'notify': {
console.log(operation, sourceOperation, invalidateStore);
// NOT SURE WHAT TO DO HERE
break;
}
}
};
return environment;
}
With the above code I managed to sync the RecordSource properly, this means that after a query is fetched on tab 1, the same query will not have to go through the network when executed on tab2, since Relay will find in the RecordSource the data the other tab already fetched from network.
The problem I'm having is with subscribed components, when the RecordSource is synchronized, the React components are not re-rendering with the fresh data, this is especially evident if I run a mutation on tab 1, and I observe on tab 2 the component still displays the old data even though the RecordSource is correctly updated with the updated data.
I would like to subscribe to the notify events happening on tab 1 and use the sourceOperation argument to properly trigger the correct subscription updates on tab 2 but I can't figure out how?
I published a repository with the code here: github.com/FezVrasta/graphql-relay-cross-tab-sync
You can run it with yarn json-graphql-server and yarn start