@gonvex/client 0.5.2-staging.10 → 0.5.2-staging.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/browser-upgrades.d.ts +19 -0
- package/dist/browser-upgrades.js +111 -0
- package/dist/browser-upgrades.js.map +1 -0
- package/dist/client-upgrades.d.ts +26 -0
- package/dist/client-upgrades.js +40 -0
- package/dist/client-upgrades.js.map +1 -0
- package/dist/index.d.ts +45 -1
- package/dist/index.js +309 -5
- package/dist/index.js.map +1 -1
- package/dist/indexeddb-replica.d.ts +3 -0
- package/dist/indexeddb-replica.js +21 -0
- package/dist/indexeddb-replica.js.map +1 -1
- package/dist/local-replica.d.ts +23 -0
- package/dist/local-replica.js +20 -0
- package/dist/local-replica.js.map +1 -1
- package/dist/outbox.d.ts +16 -0
- package/dist/outbox.js +62 -7
- package/dist/outbox.js.map +1 -1
- package/package.json +5 -3
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type ClientContract, type ClientMigration } from "./client-upgrades.js";
|
|
2
|
+
import type { LocalReplicaStorage } from "./local-replica.js";
|
|
3
|
+
import type { OutboxStore } from "./outbox.js";
|
|
4
|
+
export type BrowserUpgradeOptions = {
|
|
5
|
+
replicaName: string;
|
|
6
|
+
outboxName: string;
|
|
7
|
+
contract: ClientContract;
|
|
8
|
+
/** Version assigned to data written before this mechanism was introduced. */
|
|
9
|
+
initialVersion: number;
|
|
10
|
+
migrations: readonly ClientMigration[];
|
|
11
|
+
onRequired?: (error: Error) => void;
|
|
12
|
+
};
|
|
13
|
+
/** All app access goes through version-fenced SDK stores, including old tabs. */
|
|
14
|
+
export declare function browserUpgradeStorage(options: BrowserUpgradeOptions): {
|
|
15
|
+
storage: LocalReplicaStorage;
|
|
16
|
+
store: OutboxStore;
|
|
17
|
+
ready: Promise<void>;
|
|
18
|
+
close(): void;
|
|
19
|
+
};
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { Dexie } from "dexie";
|
|
2
|
+
import { IndexedDBLocalReplicaStorage } from "./indexeddb-replica.js";
|
|
3
|
+
import { migrateClientData, migrationChain } from "./client-upgrades.js";
|
|
4
|
+
/** All app access goes through version-fenced SDK stores, including old tabs. */
|
|
5
|
+
export function browserUpgradeStorage(options) {
|
|
6
|
+
const replica = new IndexedDBLocalReplicaStorage(options.replicaName);
|
|
7
|
+
const queue = new Dexie(options.outboxName);
|
|
8
|
+
queue.version(1).stores({ entries: "++id, state, nextAttemptAt" });
|
|
9
|
+
queue.version(2).stores({ entries: "++id, scope, state, nextAttemptAt, [scope+state], [scope+nextAttemptAt]" });
|
|
10
|
+
const meta = new Dexie(`${options.replicaName}-upgrades`);
|
|
11
|
+
meta.version(1).stores({ state: "&key" });
|
|
12
|
+
const lockName = `gonvex-upgrade:${options.replicaName}:${options.outboxName}`;
|
|
13
|
+
const locks = globalThis.navigator?.locks;
|
|
14
|
+
const channel = typeof BroadcastChannel === "function" ? new BroadcastChannel(lockName) : undefined;
|
|
15
|
+
let closed = false;
|
|
16
|
+
const requireUpgrade = (error) => { options.onRequired?.(error); return error; };
|
|
17
|
+
if (channel)
|
|
18
|
+
channel.onmessage = event => {
|
|
19
|
+
if (event.data?.version !== options.contract.version)
|
|
20
|
+
requireUpgrade(new Error("Another tab upgraded the application. Reload to continue."));
|
|
21
|
+
};
|
|
22
|
+
const ready = (async () => {
|
|
23
|
+
if (!locks)
|
|
24
|
+
throw new Error("Safe offline upgrades require browser Web Locks support");
|
|
25
|
+
await locks.request(lockName, { mode: "exclusive" }, async () => {
|
|
26
|
+
await Promise.all([meta.open(), queue.open()]);
|
|
27
|
+
const saved = await meta.state.get("contract");
|
|
28
|
+
const hasOldData = saved ? true : (await replica.listScopes()).length > 0 || await queue.entries.count() > 0;
|
|
29
|
+
let record = saved ?? { key: "contract", version: hasOldData ? options.initialVersion : options.contract.version };
|
|
30
|
+
// A interrupted upgrade must finish its exact staged payload before another upgrade starts.
|
|
31
|
+
const finish = async () => {
|
|
32
|
+
if (!record.journal)
|
|
33
|
+
return;
|
|
34
|
+
for (const [scope, snapshot] of Object.entries(record.journal.snapshots))
|
|
35
|
+
await replica.replaceSnapshot(snapshot, scope);
|
|
36
|
+
await queue.transaction("rw", queue.entries, async () => {
|
|
37
|
+
await queue.entries.clear();
|
|
38
|
+
await queue.entries.bulkPut(record.journal.entries);
|
|
39
|
+
});
|
|
40
|
+
record = { key: "contract", version: record.version };
|
|
41
|
+
await meta.state.put(record);
|
|
42
|
+
};
|
|
43
|
+
// An older tab must never finish a newer migration and then hydrate it.
|
|
44
|
+
if (record.version > options.contract.version)
|
|
45
|
+
throw new Error("Stored data requires a newer application build");
|
|
46
|
+
await finish();
|
|
47
|
+
const chain = migrationChain(record.version, options.contract.version, options.migrations);
|
|
48
|
+
if (chain.length) {
|
|
49
|
+
const snapshots = {};
|
|
50
|
+
for (const scope of await replica.listScopes()) {
|
|
51
|
+
const snapshot = await replica.load(scope);
|
|
52
|
+
if (snapshot)
|
|
53
|
+
snapshots[scope] = snapshot;
|
|
54
|
+
}
|
|
55
|
+
const entries = await queue.entries.toArray();
|
|
56
|
+
if (entries.some(entry => !entry.scope))
|
|
57
|
+
throw new Error("Unowned offline work needs recovery before upgrading");
|
|
58
|
+
// No writes until every transform succeeds. The journal is temporary upgrade data,
|
|
59
|
+
// never a second source of application state. Applying it is repeatable after a crash.
|
|
60
|
+
const journal = migrateClientData(snapshots, entries, chain);
|
|
61
|
+
record = { key: "contract", version: options.contract.version, journal };
|
|
62
|
+
await meta.state.put(record);
|
|
63
|
+
await finish();
|
|
64
|
+
channel?.postMessage({ version: record.version });
|
|
65
|
+
}
|
|
66
|
+
else
|
|
67
|
+
await meta.state.put(record);
|
|
68
|
+
});
|
|
69
|
+
})().catch(error => { throw requireUpgrade(error instanceof Error ? error : new Error(String(error))); });
|
|
70
|
+
// The SDK may initialize storage before React subscribes; retain the rejected ready promise.
|
|
71
|
+
void ready.catch(() => undefined);
|
|
72
|
+
const guarded = async (run) => {
|
|
73
|
+
await ready;
|
|
74
|
+
if (closed)
|
|
75
|
+
throw new Error("Application storage is closed");
|
|
76
|
+
return locks.request(lockName, { mode: "shared" }, async () => {
|
|
77
|
+
const record = await meta.state.get("contract");
|
|
78
|
+
if (record?.version !== options.contract.version || record.journal) {
|
|
79
|
+
throw requireUpgrade(new Error("Application storage changed. Reload before continuing."));
|
|
80
|
+
}
|
|
81
|
+
return run();
|
|
82
|
+
});
|
|
83
|
+
};
|
|
84
|
+
const storage = new Proxy(replica, {
|
|
85
|
+
get(target, property) {
|
|
86
|
+
const value = Reflect.get(target, property);
|
|
87
|
+
return typeof value === "function" ? (...args) => guarded(() => value.apply(target, args)) : value;
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
return {
|
|
91
|
+
storage, ready,
|
|
92
|
+
store: {
|
|
93
|
+
strictPersistence: true,
|
|
94
|
+
allocateId: () => guarded(async () => {
|
|
95
|
+
const last = await queue.entries.orderBy('id').last();
|
|
96
|
+
return meta.transaction('rw', meta.state, async () => {
|
|
97
|
+
const counter = await meta.state.get('sequence');
|
|
98
|
+
const version = Math.max(counter?.version ?? 0, last?.id ?? 0) + 1;
|
|
99
|
+
await meta.state.put({ key: 'sequence', version });
|
|
100
|
+
return version;
|
|
101
|
+
});
|
|
102
|
+
}),
|
|
103
|
+
load: () => guarded(() => queue.entries.toArray()),
|
|
104
|
+
put: entry => guarded(async () => { await queue.entries.put(entry); }),
|
|
105
|
+
delete: id => guarded(() => queue.entries.delete(id)),
|
|
106
|
+
clear: scope => guarded(() => scope ? queue.entries.where("scope").equals(scope).delete().then(() => undefined) : queue.entries.clear()),
|
|
107
|
+
},
|
|
108
|
+
close() { closed = true; channel?.close(); replica.close(); queue.close(); meta.close(); },
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
//# sourceMappingURL=browser-upgrades.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"browser-upgrades.js","sourceRoot":"","sources":["../src/browser-upgrades.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAc,MAAM,OAAO,CAAC;AAC1C,OAAO,EAAE,4BAA4B,EAAE,MAAM,wBAAwB,CAAC;AACtE,OAAO,EAAE,iBAAiB,EAAE,cAAc,EAA6C,MAAM,sBAAsB,CAAC;AAiBpH,iFAAiF;AACjF,MAAM,UAAU,qBAAqB,CAAC,OAA8B;IAGlE,MAAM,OAAO,GAAG,IAAI,4BAA4B,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC;IACtE,MAAM,KAAK,GAAG,IAAI,KAAK,CAAC,OAAO,CAAC,UAAU,CAA2D,CAAC;IACtG,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,EAAE,OAAO,EAAE,4BAA4B,EAAE,CAAC,CAAC;IACnE,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,EAAE,OAAO,EAAE,yEAAyE,EAAE,CAAC,CAAC;IAChH,MAAM,IAAI,GAAG,IAAI,KAAK,CAAC,GAAG,OAAO,CAAC,WAAW,WAAW,CAAoD,CAAC;IAC7G,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC,CAAC;IAC1C,MAAM,QAAQ,GAAG,kBAAkB,OAAO,CAAC,WAAW,IAAI,OAAO,CAAC,UAAU,EAAE,CAAC;IAC/E,MAAM,KAAK,GAAG,UAAU,CAAC,SAAS,EAAE,KAAK,CAAC;IAC1C,MAAM,OAAO,GAAG,OAAO,gBAAgB,KAAK,UAAU,CAAC,CAAC,CAAC,IAAI,gBAAgB,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;IACpG,IAAI,MAAM,GAAG,KAAK,CAAC;IACnB,MAAM,cAAc,GAAG,CAAC,KAAY,EAAE,EAAE,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC,OAAO,KAAK,CAAC,CAAC,CAAC,CAAC;IACxF,IAAI,OAAO;QAAE,OAAO,CAAC,SAAS,GAAG,KAAK,CAAC,EAAE;YACvC,IAAI,KAAK,CAAC,IAAI,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,OAAO;gBAAE,cAAc,CAAC,IAAI,KAAK,CAAC,2DAA2D,CAAC,CAAC,CAAC;QAC/I,CAAC,CAAC;IACF,MAAM,KAAK,GAAG,CAAC,KAAK,IAAI,EAAE;QACxB,IAAI,CAAC,KAAK;YAAE,MAAM,IAAI,KAAK,CAAC,yDAAyD,CAAC,CAAC;QACvF,MAAM,KAAK,CAAC,OAAO,CAAC,QAAQ,EAAE,EAAE,IAAI,EAAE,WAAW,EAAE,EAAE,KAAK,IAAI,EAAE;YAC9D,MAAM,OAAO,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,IAAI,EAAE,EAAE,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC;YAC/C,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;YAC/C,MAAM,UAAU,GAAG,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,MAAM,OAAO,CAAC,UAAU,EAAE,CAAC,CAAC,MAAM,GAAG,CAAC,IAAI,MAAM,KAAK,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;YAC7G,IAAI,MAAM,GAAG,KAAK,IAAI,EAAE,GAAG,EAAE,UAAU,EAAE,OAAO,EAAE,UAAU,CAAC,CAAC,CAAC,OAAO,CAAC,cAAc,CAAC,CAAC,CAAC,OAAO,CAAC,QAAQ,CAAC,OAAO,EAAE,CAAC;YACnH,4FAA4F;YAC5F,MAAM,MAAM,GAAG,KAAK,IAAI,EAAE;gBACxB,IAAI,CAAC,MAAM,CAAC,OAAO;oBAAE,OAAO;gBAC5B,KAAK,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC;oBAAE,MAAM,OAAO,CAAC,eAAe,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAC;gBACzH,MAAM,KAAK,CAAC,WAAW,CAAC,IAAI,EAAE,KAAK,CAAC,OAAO,EAAE,KAAK,IAAI,EAAE;oBACtD,MAAM,KAAK,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC;oBAC5B,MAAM,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,MAAM,CAAC,OAAQ,CAAC,OAAO,CAAC,CAAC;gBACvD,CAAC,CAAC,CAAC;gBACH,MAAM,GAAG,EAAE,GAAG,EAAE,UAAU,EAAE,OAAO,EAAE,MAAM,CAAC,OAAO,EAAE,CAAC;gBACtD,MAAM,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;YAC/B,CAAC,CAAC;YACF,wEAAwE;YACxE,IAAI,MAAM,CAAC,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC,OAAO;gBAAE,MAAM,IAAI,KAAK,CAAC,gDAAgD,CAAC,CAAC;YACjH,MAAM,MAAM,EAAE,CAAC;YACf,MAAM,KAAK,GAAG,cAAc,CAAC,MAAM,CAAC,OAAO,EAAE,OAAO,CAAC,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC,UAAU,CAAC,CAAC;YAC3F,IAAI,KAAK,CAAC,MAAM,EAAE,CAAC;gBACjB,MAAM,SAAS,GAAoC,EAAE,CAAC;gBACtD,KAAK,MAAM,KAAK,IAAI,MAAM,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC;oBAC/C,MAAM,QAAQ,GAAG,MAAM,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;oBAC3C,IAAI,QAAQ;wBAAE,SAAS,CAAC,KAAK,CAAC,GAAG,QAAQ,CAAC;gBAC5C,CAAC;gBACD,MAAM,OAAO,GAAG,MAAM,KAAK,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC;gBAC9C,IAAI,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC;oBAAE,MAAM,IAAI,KAAK,CAAC,sDAAsD,CAAC,CAAC;gBACjH,mFAAmF;gBACnF,uFAAuF;gBACvF,MAAM,OAAO,GAAG,iBAAiB,CAAC,SAAS,EAAE,OAAO,EAAE,KAAK,CAAC,CAAC;gBAC7D,MAAM,GAAG,EAAE,GAAG,EAAE,UAAU,EAAE,OAAO,EAAE,OAAO,CAAC,QAAQ,CAAC,OAAO,EAAE,OAAO,EAAE,CAAC;gBACzE,MAAM,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;gBAC7B,MAAM,MAAM,EAAE,CAAC;gBACf,OAAO,EAAE,WAAW,CAAC,EAAE,OAAO,EAAE,MAAM,CAAC,OAAO,EAAE,CAAC,CAAC;YACpD,CAAC;;gBAAM,MAAM,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;QACtC,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC,EAAE,GAAG,MAAM,cAAc,CAAC,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAC1G,6FAA6F;IAC7F,KAAK,KAAK,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC;IAClC,MAAM,OAAO,GAAG,KAAK,EAAK,GAAqB,EAAc,EAAE;QAC7D,MAAM,KAAK,CAAC;QACZ,IAAI,MAAM;YAAE,MAAM,IAAI,KAAK,CAAC,+BAA+B,CAAC,CAAC;QAC7D,OAAO,KAAM,CAAC,OAAO,CAAC,QAAQ,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,EAAE,KAAK,IAAI,EAAE;YAC7D,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;YAChD,IAAI,MAAM,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,OAAO,IAAI,MAAM,CAAC,OAAO,EAAE,CAAC;gBACnE,MAAM,cAAc,CAAC,IAAI,KAAK,CAAC,wDAAwD,CAAC,CAAC,CAAC;YAC5F,CAAC;YACD,OAAO,GAAG,EAAE,CAAC;QACf,CAAC,CAAC,CAAC;IACL,CAAC,CAAC;IACF,MAAM,OAAO,GAAG,IAAI,KAAK,CAAC,OAAO,EAAE;QACjC,GAAG,CAAC,MAAM,EAAE,QAAQ;YAClB,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;YAC5C,OAAO,OAAO,KAAK,KAAK,UAAU,CAAC,CAAC,CAAC,CAAC,GAAG,IAAe,EAAE,EAAE,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC;QAChH,CAAC;KACF,CAAC,CAAC;IACH,OAAO;QACL,OAAO,EAAE,KAAK;QACd,KAAK,EAAE;YACL,iBAAiB,EAAE,IAAI;YACvB,UAAU,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,KAAK,IAAI,EAAE;gBACnC,MAAM,IAAI,GAAG,MAAM,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,CAAC;gBACtD,OAAO,IAAI,CAAC,WAAW,CAAC,IAAI,EAAE,IAAI,CAAC,KAAK,EAAE,KAAK,IAAI,EAAE;oBACnD,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;oBACjD,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC,OAAO,EAAE,OAAO,IAAI,CAAC,EAAE,IAAI,EAAE,EAAE,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC;oBACnE,MAAM,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,GAAG,EAAE,UAAU,EAAE,OAAO,EAAE,CAAC,CAAC;oBACnD,OAAO,OAAO,CAAC;gBACjB,CAAC,CAAC,CAAC;YACL,CAAC,CAAC;YACF,IAAI,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,KAAK,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC;YAClD,GAAG,EAAE,KAAK,CAAC,EAAE,CAAC,OAAO,CAAC,KAAK,IAAI,EAAE,GAAG,MAAM,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;YACtE,MAAM,EAAE,EAAE,CAAC,EAAE,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;YACrD,KAAK,EAAE,KAAK,CAAC,EAAE,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,MAAM,EAAE,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC;SACzI;QACD,KAAK,KAAK,MAAM,GAAG,IAAI,CAAC,CAAC,OAAO,EAAE,KAAK,EAAE,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC;KAC3F,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { JsonValue } from "@gonvex/protocol";
|
|
2
|
+
import type { ReplicaSnapshot } from "./local-replica.js";
|
|
3
|
+
import type { ReducerOutboxEntry } from "./outbox.js";
|
|
4
|
+
export type ClientContract = {
|
|
5
|
+
version: number;
|
|
6
|
+
offlineMaxAgeMs: number;
|
|
7
|
+
};
|
|
8
|
+
export type ClientMigration = {
|
|
9
|
+
from: number;
|
|
10
|
+
to: number;
|
|
11
|
+
/** Pure synchronous transformation of confirmed rows. Never optimistic patches. */
|
|
12
|
+
replica?: (snapshot: ReplicaSnapshot) => ReplicaSnapshot;
|
|
13
|
+
/** Throw if the user's meaning cannot be preserved. The original queue survives. */
|
|
14
|
+
intent?: (intent: {
|
|
15
|
+
path: string;
|
|
16
|
+
args: JsonValue;
|
|
17
|
+
}) => {
|
|
18
|
+
path: string;
|
|
19
|
+
args: JsonValue;
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
export declare function migrationChain(from: number, to: number, migrations: readonly ClientMigration[]): ClientMigration[];
|
|
23
|
+
export declare function migrateClientData(snapshots: Record<string, ReplicaSnapshot>, entries: readonly ReducerOutboxEntry[], chain: readonly ClientMigration[]): {
|
|
24
|
+
snapshots: Record<string, ReplicaSnapshot>;
|
|
25
|
+
entries: ReducerOutboxEntry[];
|
|
26
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export function migrationChain(from, to, migrations) {
|
|
2
|
+
if (!Number.isSafeInteger(from) || from < 1 || !Number.isSafeInteger(to) || to < from) {
|
|
3
|
+
throw new Error(`Unsupported client contract transition ${from} -> ${to}`);
|
|
4
|
+
}
|
|
5
|
+
const chain = [];
|
|
6
|
+
for (let version = from; version < to; version++) {
|
|
7
|
+
const candidates = migrations.filter(m => m.from === version);
|
|
8
|
+
if (candidates.length !== 1 || candidates[0].to !== version + 1) {
|
|
9
|
+
throw new Error(`Missing or ambiguous client migration ${version} -> ${version + 1}`);
|
|
10
|
+
}
|
|
11
|
+
chain.push(candidates[0]);
|
|
12
|
+
}
|
|
13
|
+
return chain;
|
|
14
|
+
}
|
|
15
|
+
export function migrateClientData(snapshots, entries, chain) {
|
|
16
|
+
let nextSnapshots = structuredClone(snapshots);
|
|
17
|
+
let nextEntries = structuredClone([...entries]);
|
|
18
|
+
for (const migration of chain) {
|
|
19
|
+
nextSnapshots = Object.fromEntries(Object.entries(nextSnapshots).map(([scope, snapshot]) => {
|
|
20
|
+
const next = migration.replica ? migration.replica(snapshot) : snapshot;
|
|
21
|
+
// Shape migration does not establish server freshness or preserve query membership contracts.
|
|
22
|
+
delete next.cursor;
|
|
23
|
+
next.liveQueries = {};
|
|
24
|
+
return [scope, next];
|
|
25
|
+
}));
|
|
26
|
+
nextEntries = nextEntries.map(entry => {
|
|
27
|
+
const originalPath = entry.receiptPath ?? entry.path;
|
|
28
|
+
const intent = migration.intent?.({ path: entry.path, args: structuredClone(entry.args) })
|
|
29
|
+
?? { path: entry.path, args: entry.args };
|
|
30
|
+
if (!intent.path || intent.args === undefined)
|
|
31
|
+
throw new Error("Client migration returned an invalid intent");
|
|
32
|
+
return { ...entry, path: intent.path, args: intent.args,
|
|
33
|
+
receiptPath: originalPath, patches: [],
|
|
34
|
+
// Committed responses awaiting a watermark must be reconciled by their original receipt too.
|
|
35
|
+
state: "pending", nextAttemptAt: 0 };
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
return { snapshots: nextSnapshots, entries: nextEntries };
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=client-upgrades.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client-upgrades.js","sourceRoot":"","sources":["../src/client-upgrades.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { BrowserTelemetryInfo, ExecutionScope, JsonValue, MessageTrace, ServerCapabilities, ServerMessage } from "@gonvex/protocol";
|
|
2
|
+
import type { LocalRuntimeBinding } from "@gonvex/local-runtime/worker-client";
|
|
2
3
|
import { type ErrorReporterOptions } from "./error-reporter.js";
|
|
3
4
|
export { GonvexErrorReporter } from "./error-reporter.js";
|
|
4
5
|
export type { ErrorReporterOptions, ErrorEventPayload, ErrorContext, ErrorAccount } from "./error-reporter.js";
|
|
@@ -10,9 +11,11 @@ export * from "./error-reporter.js";
|
|
|
10
11
|
export * from "./optimistic.js";
|
|
11
12
|
export * from "./outbox.js";
|
|
12
13
|
export * from "./kv-stores.js";
|
|
14
|
+
export * from "./client-upgrades.js";
|
|
15
|
+
export * from "./browser-upgrades.js";
|
|
13
16
|
export * from "./signals.js";
|
|
14
17
|
export * from "./external-auth.js";
|
|
15
|
-
export { MemoryLocalReplicaStorage, type LocalReplicaStorage, type LocalReplicaView, type ReplicaChange, type ReplicaFreshness, type ReplicaRow, type ReplicaScope, type ReplicaSnapshot, type ReplicaTransaction, type ReplicaWindow, type LiveQueryResult, type ReplicaCollectionState, type ReplicaCollectionSubscriptionState, type ReplicaCollectionPlan, } from "./local-replica.js";
|
|
18
|
+
export { MemoryLocalReplicaStorage, type LocalReplicaStorage, type LocalReplicaSession, type LocalReplicaView, type ReplicaChange, type ReplicaFreshness, type ReplicaRow, type ReplicaScope, type ReplicaSnapshot, type ReplicaTransaction, type ReplicaWindow, type LiveQueryResult, type ReplicaCollectionState, type ReplicaCollectionSubscriptionState, type ReplicaCollectionPlan, } from "./local-replica.js";
|
|
16
19
|
export * from "./query-expression.js";
|
|
17
20
|
export * from "./indexeddb-replica.js";
|
|
18
21
|
export * from "./control.js";
|
|
@@ -49,6 +52,11 @@ export type FunctionReference<Args extends JsonValue = JsonValue, Result extends
|
|
|
49
52
|
conflict?: "reject" | "expectedVersion" | "merge";
|
|
50
53
|
reason?: string;
|
|
51
54
|
};
|
|
55
|
+
localExecution?: 1;
|
|
56
|
+
/** SDK-owned version check for replay; never an application argument. */
|
|
57
|
+
receiptPath?: string;
|
|
58
|
+
artifactHash?: string;
|
|
59
|
+
intentEntropy?: string;
|
|
52
60
|
live?: {
|
|
53
61
|
entity: string;
|
|
54
62
|
key: string;
|
|
@@ -153,6 +161,13 @@ export type GonvexClientAuth = {
|
|
|
153
161
|
};
|
|
154
162
|
};
|
|
155
163
|
export type GonvexClientOptions = GonvexClientAuth & {
|
|
164
|
+
clientContract?: import("./client-upgrades.js").ClientContract;
|
|
165
|
+
onUpdateRequired?: (reason: string) => void;
|
|
166
|
+
/** Supplied by generated client bindings, once for the entire application. */
|
|
167
|
+
localRuntime?: LocalRuntimeBinding & {
|
|
168
|
+
clientContract?: import("./client-upgrades.js").ClientContract;
|
|
169
|
+
collections?: readonly FunctionReference[];
|
|
170
|
+
};
|
|
156
171
|
/**
|
|
157
172
|
* Keep listenerless live queries subscribed for this long so route
|
|
158
173
|
* backtracking can reuse their current result without WebSocket churn.
|
|
@@ -197,6 +212,16 @@ export type GonvexTelemetryEvent = {
|
|
|
197
212
|
};
|
|
198
213
|
export declare class GonvexClient {
|
|
199
214
|
private readonly url;
|
|
215
|
+
private readonly localBinding?;
|
|
216
|
+
private readonly localExecutor?;
|
|
217
|
+
private readonly localStorage?;
|
|
218
|
+
private localIdentity?;
|
|
219
|
+
private localLane;
|
|
220
|
+
private localReplayScheduled;
|
|
221
|
+
private replacingLocal;
|
|
222
|
+
private readonly unsubscribeLocal?;
|
|
223
|
+
private readonly localCollectionClosers;
|
|
224
|
+
private readonly reducerRejectionHandlers;
|
|
200
225
|
private socket;
|
|
201
226
|
private readonly handlers;
|
|
202
227
|
private readonly querySubscriptions;
|
|
@@ -259,7 +284,25 @@ export declare class GonvexClient {
|
|
|
259
284
|
private hasEverConnected;
|
|
260
285
|
private connectionCount;
|
|
261
286
|
private readonly timeouts;
|
|
287
|
+
private readonly clientContract?;
|
|
288
|
+
private readonly updateRequiredHandler?;
|
|
289
|
+
private updateRequired;
|
|
290
|
+
private lastOnlineAtMs;
|
|
262
291
|
constructor(url: string, options?: GonvexClientOptions);
|
|
292
|
+
/** Authoritative sync failures arrive after an offline call returned locally. */
|
|
293
|
+
onReducerRejection(listener: (event: {
|
|
294
|
+
reducerId: string;
|
|
295
|
+
path: string;
|
|
296
|
+
error: string;
|
|
297
|
+
}) => void): () => void;
|
|
298
|
+
private restoreLocalSession;
|
|
299
|
+
private ensureLocalCollections;
|
|
300
|
+
private localSnapshot;
|
|
301
|
+
private inLocalLane;
|
|
302
|
+
private scheduleLocalReplay;
|
|
303
|
+
private reportLocalRejection;
|
|
304
|
+
private rebaseLocalEntries;
|
|
305
|
+
private runLocalReducer;
|
|
263
306
|
/** The single normalized authoritative + optimistic application data store. */
|
|
264
307
|
get localReplica(): LocalReplicaView;
|
|
265
308
|
replicaSignature(ref: FunctionReference, args?: JsonValue): string;
|
|
@@ -372,6 +415,7 @@ export declare class GonvexClient {
|
|
|
372
415
|
}): Promise<T | QueuedReducerOutcome>;
|
|
373
416
|
reducer<T extends JsonValue = JsonValue, Args extends JsonValue = JsonValue>(ref: FunctionReference<Args, T>, args?: Args, options?: CallOptions): Promise<T>;
|
|
374
417
|
private runOptimisticReducer;
|
|
418
|
+
private requireClientUpdate;
|
|
375
419
|
private canSendReducerNow;
|
|
376
420
|
action<T extends JsonValue = JsonValue, Args extends JsonValue = JsonValue>(ref: FunctionReference<Args, T>, args?: Args, options?: CallOptions): Promise<T>;
|
|
377
421
|
query<T extends JsonValue = JsonValue, Args extends JsonValue = JsonValue>(ref: FunctionReference<Args, T>, args?: Args, options?: CallOptions): Promise<T>;
|