140 lines
5.2 KiB
JavaScript
140 lines
5.2 KiB
JavaScript
(function (root, factory) {
|
|
const createUnfiledDraftSync = factory();
|
|
if (typeof module === 'object' && module.exports) module.exports = createUnfiledDraftSync;
|
|
if (root) root.createUnfiledDraftSync = createUnfiledDraftSync;
|
|
})(typeof globalThis !== 'undefined' ? globalThis : this, function () {
|
|
function createUnfiledDraftSync(options, mountedFetch) {
|
|
if (mountedFetch) return createUnfiledDraftSync.mount(options, mountedFetch);
|
|
const {captures, fetchJson, storage, getLogin = () => '', onState = () => {}} = options;
|
|
const key = 'stackchain.unfiled-draft-sync.v1';
|
|
let inFlight = null;
|
|
let rerun = false;
|
|
|
|
function read(login) {
|
|
try {
|
|
const all = JSON.parse(storage?.getItem(key) || '{}');
|
|
const state = all?.[login];
|
|
return {
|
|
all:all && typeof all === 'object' ? all : {},
|
|
known:new Set(Array.isArray(state?.known) ? state.known : []),
|
|
deleted:new Set(Array.isArray(state?.deleted) ? state.deleted : []),
|
|
};
|
|
} catch (_error) {
|
|
return {all:{}, known:new Set(), deleted:new Set()};
|
|
}
|
|
}
|
|
|
|
function write(login, state) {
|
|
state.all[login] = {known:[...state.known], deleted:[...state.deleted]};
|
|
storage?.setItem(key, JSON.stringify(state.all));
|
|
}
|
|
|
|
function publish(status, message) {
|
|
onState({status, ...(message ? {message} : {})});
|
|
const visibleStatus = globalThis.document?.querySelector?.('#my-work-action-status');
|
|
if (message && visibleStatus) visibleStatus.textContent = message;
|
|
}
|
|
|
|
function sameDrafts(left, right) {
|
|
return JSON.stringify(left) === JSON.stringify(right);
|
|
}
|
|
|
|
async function runSync(login) {
|
|
const owner = String(login || '').trim();
|
|
if (!owner) return false;
|
|
publish('syncing', 'Syncing Drafts…');
|
|
const state = read(owner);
|
|
try {
|
|
const remote = await fetchJson('api/v1/unfiled-drafts');
|
|
const remoteDrafts = Array.isArray(remote?.drafts) ? remote.drafts : [];
|
|
const remoteIds = new Set(remoteDrafts.map(item => item.id));
|
|
const before = await captures.exportOwned(owner);
|
|
|
|
for (const local of before) {
|
|
if (state.known.has(local.id) && !remoteIds.has(local.id) && !state.deleted.has(local.id)) {
|
|
await captures.discard(local.id);
|
|
}
|
|
}
|
|
await captures.mergeRemote(
|
|
remoteDrafts.filter(item => !state.deleted.has(item.id)), owner
|
|
);
|
|
const local = await captures.exportOwned(owner);
|
|
const merged = [];
|
|
const byId = new Map();
|
|
for (const item of remoteDrafts) if (!state.deleted.has(item.id)) byId.set(item.id, item);
|
|
for (const item of local) if (!state.deleted.has(item.id)) byId.set(item.id, item);
|
|
for (const item of byId.values()) merged.push(item);
|
|
merged.sort((left, right) => Number(right.saved_at) - Number(left.saved_at));
|
|
|
|
let finalSnapshot = remote;
|
|
if (!sameDrafts(merged, remoteDrafts)) {
|
|
finalSnapshot = await fetchJson('api/v1/unfiled-drafts', {
|
|
method:'PUT', headers:{'Content-Type':'application/json'},
|
|
body:JSON.stringify({revision:Number(remote.revision || 0), drafts:merged}),
|
|
});
|
|
}
|
|
state.known = new Set((finalSnapshot.drafts || []).map(item => item.id));
|
|
state.deleted.clear();
|
|
write(owner, state);
|
|
publish('ready', 'Drafts synced across devices.');
|
|
return true;
|
|
} catch (error) {
|
|
if (error?.status === 409) {
|
|
publish('conflict', 'Drafts changed on another device. Sync again to combine them.');
|
|
} else {
|
|
publish('pending', 'Saved on this device · sync pending');
|
|
}
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
function sync(login) {
|
|
const owner = String(login || '').trim();
|
|
if (!owner) return Promise.resolve(false);
|
|
if (inFlight) {
|
|
rerun = true;
|
|
return inFlight;
|
|
}
|
|
inFlight = (async () => {
|
|
let result = false;
|
|
do {
|
|
rerun = false;
|
|
result = await runSync(owner);
|
|
} while (rerun);
|
|
return result;
|
|
})().finally(() => { inFlight = null; });
|
|
return inFlight;
|
|
}
|
|
|
|
async function remove(id, login) {
|
|
const owner = String(login || '').trim();
|
|
if (!owner) return false;
|
|
const state = read(owner);
|
|
state.deleted.add(String(id));
|
|
write(owner, state);
|
|
await captures.discard(String(id));
|
|
return sync(owner);
|
|
}
|
|
|
|
captures.subscribe?.((id, removed) => {
|
|
const login = getLogin();
|
|
if (login) void (removed ? remove(id, login) : sync(login)).catch(() => {});
|
|
});
|
|
|
|
return {sync, remove};
|
|
}
|
|
createUnfiledDraftSync.mount = (captures, fetchJson) => {
|
|
const getLogin = captures.currentLogin;
|
|
const controller = createUnfiledDraftSync({captures, fetchJson, storage:globalThis.localStorage, getLogin});
|
|
const connect = () => {
|
|
const login = getLogin();
|
|
if (login) void controller.sync(login).catch(() => {});
|
|
else globalThis.setTimeout(connect, 250);
|
|
};
|
|
globalThis.setTimeout(connect, 0);
|
|
globalThis.addEventListener?.('online', connect);
|
|
return controller;
|
|
};
|
|
return createUnfiledDraftSync;
|
|
});
|