feat: converge Today sync across active devices (#361)
CI / lint (pull_request) Successful in 42s
CI / build-release (pull_request) Successful in 5s
CI / release-candidate (pull_request) Has been skipped

This commit is contained in:
timmy committed 2026-08-09 02:01:55 +00:00
1 parent 7ec3980713
commit 3c743a2b71
9 files changed
+217 -11

No files matched your search

+1
View File
@@ -156,6 +156,7 @@
'Today sync unavailable · changes stay on this device.'));
},
});
todaySync.startLifecycle({ window, document });
const laterWork = createLaterWork({
storage: localStorage,
getLogin: () => planningOwnerLogin,
+1 -1
View File
@@ -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
View File
@@ -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;