Open kitten opened 4 years ago
Here's a nice video about the different ways to synchronize data across documents: https://www.youtube.com/watch?v=9UNwHmagedE
Just posting it in case it can help to decide on what API to use. They do speak about the BroadcastChannel
API which looks great but not supported by Safari.
A use case to consider is how this would work with the subscription exchange and with subscriptions that tabs may have open:
I've been wondering if we could handle this a bit more naive, in the sense that we could do something like combining the refocusExchange
and the persisted data
to achieve this goal.
Let's say a user has a list of todos on tab 1, and the same window open on tab2. The user mutates a todo on tab 1, this will indicate loading, mutate against the server and come up with a result. This will be a deferred write to our storageAdapter. We could place the storageAdapter in some "lock-state" while this response is pending.
When the user switched to tab2, the refocusExchange will trigger and our cache should trigger the promise to readFromStorage, this means that the queries on-screen will be refetched. This hits the cache which will in-turn buffer these queries since we have a pending readFromStorage, when the cache sees that it's in lock-state it should poll for the lock to be removed, when it's removed it can rehydrate the cache and respond to the in-flight queries.
The concern I have here is that currently we don't use our optimistic results in storage, so this could mean that we inherit the pending mutations from the storage which could possibly introduce us dispatching them twice. This should be a case to take in account.
I think that'd require us to make the assumption that only one tab is "active" at a time, which isn't necessarily the case with multiple windows, background tasks, timers, etc 😅
What's the progress on this one? Did anybody manage to get something like this to work?
Yeah. This is really needed.
@kitten do you actually need a cross-tab synchronization exchange, considering that "data stored in IndexedDB is available to all tabs from within the same origin" (ref)? Switching to a tab could refresh its state based on a pull from IndexedDB. If a background push to all tabs would be too complex.
This is how I implemented syncExchange with the help of IndexedDB. It's probably not super efficient, but good enough for my needs:
import { Exchange, OperationResult } from 'urql';
import { makeSubject, merge, pipe, tap } from 'wonka';
function pageVisible(callback: (visible: boolean) => void) {
if (typeof window === 'undefined') {
return () => undefined;
}
const focusHandler = () => callback(true);
// Blur handler gives us a few false positives, but we can live with that
const blurHandler = () => callback(false);
const visibilityChangeHandler = () => {
callback(document.visibilityState === 'visible');
};
window.addEventListener('focus', focusHandler, false);
window.addEventListener('blur', blurHandler, false);
window.addEventListener('visibilitychange', visibilityChangeHandler, false);
// Returns unsubscribe
return () => {
window.removeEventListener('focus', focusHandler);
window.removeEventListener('blur', blurHandler);
window.removeEventListener('visibilitychange', visibilityChangeHandler);
};
}
export function syncExchange(): Exchange {
let leader = true;
pageVisible((visible) => (leader = visible));
return ({ forward }) =>
(operations$) => {
if (typeof window === 'undefined') {
return forward(operations$);
}
const { source, next } = makeSubject<OperationResult>();
const channel = new BroadcastChannel('syncExchange');
channel.addEventListener('message', (event) => {
if (leader) {
return;
}
next(event.data as OperationResult);
});
const processOutgoingOperation = (operation: OperationResult) => {
if (!leader) {
return;
}
// Right now we're forwarding everything, but since it's only on already
// handled graphql operations, it shouldn't matter for our use case
channel.postMessage(operation);
};
return pipe(
merge([forward(operations$), source]),
tap(processOutgoingOperation)
);
};
}
They do speak about the BroadcastChannel API which looks great but not supported by Safari. [as of Oct 18, 2020]
BroadcastChannel has full support in Safari now, as of Mar 15 2022.
Last comment here was a couple years back — curious what the latest thinking is. 🙏
Summary
Especially with Graphcache we've discussed cases where a sufficiently complex app may want to synchronize what's happening across apps. As part of this one may have multiple tabs of a Graphcache application opened.
We can make the assumption that it doesn't matter whether the cache itself is 100% in sync, but we hypothesize that two tabs of Graphcache can already coexist (Needs Testing)
A cross-tab synchronization exchange can therefore focus on distributing results across all apps.
Proposed Solution
Create an exchange that deduplicates fetching results and distributes results to other tabs, maybe using the
BroadcastChannel
API.Related Conversations