feat: converge Today sync across active devices (#361)
This commit is contained in:
1 parent
7ec3980713
commit
3c743a2b71
9 files changed
+217
-11
No files matched your search
@@ -156,6 +156,7 @@
|
||||
'Today sync unavailable · changes stay on this device.'));
|
||||
},
|
||||
});
|
||||
todaySync.startLifecycle({ window, document });
|
||||
const laterWork = createLaterWork({
|
||||
storage: localStorage,
|
||||
getLogin: () => planningOwnerLogin,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
const BASE = new URL('./', self.location.href).pathname;
|
||||
importScripts(BASE + 'static/background-issue-sync.js');
|
||||
const CACHE = 'stackchain-dashboard-shell-v46';
|
||||
const CACHE = 'stackchain-dashboard-shell-v47';
|
||||
const OUTAGE_STATUSES = new Set([500, 502, 503, 504]);
|
||||
const NAVIGATION_TIMEOUT_MS = self.__STACKCHAIN_NAVIGATION_TIMEOUT_MS || 4000;
|
||||
const SHELL = [
|
||||
|
||||
+56
-4
@@ -1,13 +1,57 @@
|
||||
function createTodaySync({ storage, getLogin, fetchJson, onRemoteIds, onStatus, createOperationId }) {
|
||||
function createTodaySync({ storage, getLogin, fetchJson, onRemoteIds, onStatus, createOperationId, createChannel }) {
|
||||
const prefix = 'stackchain.today-sync.v1.';
|
||||
const migrationPrefix = 'stackchain.today-sync-migrated.v1.';
|
||||
const snapshotPrefix = 'stackchain.today-sync-snapshot.v1.';
|
||||
let flushing = null;
|
||||
let channel = null;
|
||||
let channelKey = '';
|
||||
|
||||
function key() {
|
||||
const login = String(getLogin?.() || '').trim().toLowerCase();
|
||||
return login ? prefix + encodeURIComponent(login) : '';
|
||||
}
|
||||
|
||||
function snapshotKey() {
|
||||
const storageKey = key();
|
||||
return storageKey ? snapshotPrefix + storageKey.slice(prefix.length) : '';
|
||||
}
|
||||
|
||||
function savedRevision() {
|
||||
try {
|
||||
const snapshot = JSON.parse(storage?.getItem(snapshotKey()) || 'null');
|
||||
return Number.isInteger(snapshot?.revision) ? snapshot.revision : -1;
|
||||
} catch (_error) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
function adopt(plan, broadcast = true) {
|
||||
if (!Number.isInteger(plan?.revision) || !Array.isArray(plan?.ids)) return false;
|
||||
if (plan.revision < savedRevision()) return false;
|
||||
try {
|
||||
storage?.setItem(snapshotKey(), JSON.stringify({ revision: plan.revision, ids: plan.ids }));
|
||||
} catch (_error) {
|
||||
// A storage quota failure must not prevent the current tab from using server truth.
|
||||
}
|
||||
onRemoteIds?.(plan.ids);
|
||||
if (broadcast) channel?.postMessage({ revision: plan.revision, ids: plan.ids });
|
||||
return true;
|
||||
}
|
||||
|
||||
function ensureChannel() {
|
||||
const storageKey = key();
|
||||
if (!storageKey || channelKey === storageKey) return;
|
||||
channel?.close?.();
|
||||
const factory = createChannel || (globalThis.window?.BroadcastChannel
|
||||
? name => new globalThis.window.BroadcastChannel(name)
|
||||
: null);
|
||||
channelKey = storageKey;
|
||||
channel = factory?.('stackchain-today-' + storageKey.slice(prefix.length)) || null;
|
||||
channel?.addEventListener?.('message', event => {
|
||||
if (key() === storageKey) adopt(event.data, false);
|
||||
});
|
||||
}
|
||||
|
||||
function pending() {
|
||||
const storageKey = key();
|
||||
if (!storageKey || !storage) return [];
|
||||
@@ -71,6 +115,7 @@ function createTodaySync({ storage, getLogin, fetchJson, onRemoteIds, onStatus,
|
||||
|
||||
async function run() {
|
||||
if (!key()) return false;
|
||||
ensureChannel();
|
||||
try {
|
||||
let plan = await fetchJson('api/v1/today');
|
||||
const operations = pending();
|
||||
@@ -85,14 +130,14 @@ function createTodaySync({ storage, getLogin, fetchJson, onRemoteIds, onStatus,
|
||||
if (error?.status !== 409) throw error;
|
||||
const rejected = pending();
|
||||
save(rejected.filter(candidate => candidate.operation_id !== operation.operation_id));
|
||||
onRemoteIds?.(Array.isArray(plan.ids) ? plan.ids : []);
|
||||
adopt(plan);
|
||||
onStatus?.('full');
|
||||
return false;
|
||||
}
|
||||
const remaining = pending();
|
||||
if (remaining[0]?.operation_id === operation.operation_id) save(remaining.slice(1));
|
||||
}
|
||||
onRemoteIds?.(Array.isArray(plan.ids) ? plan.ids : []);
|
||||
adopt(plan);
|
||||
onStatus?.(pending().length ? 'pending' : 'saved');
|
||||
return true;
|
||||
} catch (_error) {
|
||||
@@ -106,7 +151,14 @@ function createTodaySync({ storage, getLogin, fetchJson, onRemoteIds, onStatus,
|
||||
return flushing;
|
||||
}
|
||||
|
||||
return { enqueue, migrate, flush, pending };
|
||||
function startLifecycle({ window: windowObject, document: documentObject }) {
|
||||
windowObject?.addEventListener?.('online', flush);
|
||||
documentObject?.addEventListener?.('visibilitychange', () =>
|
||||
documentObject.hidden ? false : flush()
|
||||
);
|
||||
}
|
||||
|
||||
return { enqueue, migrate, flush, pending, startLifecycle };
|
||||
}
|
||||
|
||||
if (typeof module !== 'undefined' && module.exports) module.exports = createTodaySync;
|
||||
Reference in new issue
Block a user