@absolutejs/sync 0.2.0 → 0.3.0
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/README.md +10 -3
- package/dist/angular/index.js +98 -4
- package/dist/angular/index.js.map +3 -3
- package/dist/client/index.d.ts +2 -2
- package/dist/client/index.js +100 -4
- package/dist/client/index.js.map +3 -3
- package/dist/client/syncCollection.d.ts +52 -0
- package/dist/react/index.js +98 -4
- package/dist/react/index.js.map +3 -3
- package/dist/svelte/index.js +98 -4
- package/dist/svelte/index.js.map +3 -3
- package/dist/vue/index.js +98 -4
- package/dist/vue/index.js.map +3 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -221,8 +221,12 @@ await orders.mutate({
|
|
|
221
221
|
`ChangeSource` — e.g. `postgresChangeSource` (`/postgres`) over `LISTEN/NOTIFY`,
|
|
222
222
|
wired with `engine.connectSource(...)` and the trigger SQL from
|
|
223
223
|
`postgresNotifyTrigger`.
|
|
224
|
-
- **Offline.** Pending mutations replay on reconnect; pass `storage`
|
|
225
|
-
(e.g. `localStorageMutationStorage`) to
|
|
224
|
+
- **Offline & local-first.** Pending mutations replay on reconnect; pass `storage`
|
|
225
|
+
(e.g. `localStorageMutationStorage`) to let unconfirmed writes survive a reload.
|
|
226
|
+
Pass `cache` (`localStorageCollectionCache` or `indexedDbCollectionCache`) to
|
|
227
|
+
persist the confirmed rows too — reads are then instant on reload and available
|
|
228
|
+
offline, and the socket resumes from the cached version (a catch-up diff if the
|
|
229
|
+
server's changelog still covers it, a fresh snapshot otherwise).
|
|
226
230
|
- **Access control is mandatory.** Each collection's `authorize` gates subscribe and
|
|
227
231
|
its filter scopes rows, so a change to a row a caller can't see never reaches them.
|
|
228
232
|
|
|
@@ -268,7 +272,10 @@ it, ~3 store round-trips every 20ms ran the voice pipeline far slower than real
|
|
|
268
272
|
| `jsonFetcher(url, init?)` | Default `fetcher`: GET + JSON parse, forwards the abort signal. |
|
|
269
273
|
| `createSyncCollection({ url, collection, ... })` | Live diff-driven collection store with optimistic `mutate`. |
|
|
270
274
|
| `createSyncClient({ url })` | One socket, many collections (`client.collection(...)`). Applies a multi-collection mutation's diffs as one **consistent frame** — no torn cross-collection paint. |
|
|
271
|
-
| `
|
|
275
|
+
| `createPresence({ url, room, state })` | Join a presence room: see who's online / typing (`get` + `subscribe`) and publish your own state (`set`). |
|
|
276
|
+
| `localStorageMutationStorage(key)` | `localStorage`-backed offline write queue for `createSyncCollection`. |
|
|
277
|
+
| `localStorageCollectionCache(key)` | `localStorage`-backed local-first read cache: confirmed rows survive a reload, resume from the cached version. |
|
|
278
|
+
| `indexedDbCollectionCache({ key, ... })` | IndexedDB-backed local-first read cache — durable, large-capacity. Same resume semantics, async storage. |
|
|
272
279
|
|
|
273
280
|
### Framework bindings — `@absolutejs/sync/{react,vue,svelte,angular}`
|
|
274
281
|
|
package/dist/angular/index.js
CHANGED
|
@@ -81,6 +81,57 @@ var localStorageMutationStorage = (key) => ({
|
|
|
81
81
|
globalThis.localStorage?.setItem(key, JSON.stringify(records));
|
|
82
82
|
}
|
|
83
83
|
});
|
|
84
|
+
var localStorageCollectionCache = (key) => ({
|
|
85
|
+
load: () => {
|
|
86
|
+
const raw = globalThis.localStorage?.getItem(key);
|
|
87
|
+
return raw ? JSON.parse(raw) : undefined;
|
|
88
|
+
},
|
|
89
|
+
save: (snapshot) => {
|
|
90
|
+
globalThis.localStorage?.setItem(key, JSON.stringify(snapshot));
|
|
91
|
+
},
|
|
92
|
+
clear: () => {
|
|
93
|
+
globalThis.localStorage?.removeItem(key);
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
var openIndexedDb = (databaseName, storeName) => new Promise((resolve, reject) => {
|
|
97
|
+
const request = globalThis.indexedDB.open(databaseName, 1);
|
|
98
|
+
request.onupgradeneeded = () => {
|
|
99
|
+
request.result.createObjectStore(storeName);
|
|
100
|
+
};
|
|
101
|
+
request.onsuccess = () => resolve(request.result);
|
|
102
|
+
request.onerror = () => reject(request.error);
|
|
103
|
+
});
|
|
104
|
+
var indexedDbCollectionCache = ({
|
|
105
|
+
key,
|
|
106
|
+
databaseName = "absolutejs-sync",
|
|
107
|
+
storeName = "collections"
|
|
108
|
+
}) => {
|
|
109
|
+
let handle;
|
|
110
|
+
const database = () => {
|
|
111
|
+
handle ??= openIndexedDb(databaseName, storeName);
|
|
112
|
+
return handle;
|
|
113
|
+
};
|
|
114
|
+
const withStore = async (mode, run) => {
|
|
115
|
+
if (globalThis.indexedDB === undefined) {
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
const db = await database();
|
|
119
|
+
return new Promise((resolve, reject) => {
|
|
120
|
+
const request = run(db.transaction(storeName, mode).objectStore(storeName));
|
|
121
|
+
request.onsuccess = () => resolve(request.result);
|
|
122
|
+
request.onerror = () => reject(request.error);
|
|
123
|
+
});
|
|
124
|
+
};
|
|
125
|
+
return {
|
|
126
|
+
load: () => withStore("readonly", (store) => store.get(key)),
|
|
127
|
+
save: async (snapshot) => {
|
|
128
|
+
await withStore("readwrite", (store) => store.put(snapshot, key));
|
|
129
|
+
},
|
|
130
|
+
clear: async () => {
|
|
131
|
+
await withStore("readwrite", (store) => store.delete(key));
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
};
|
|
84
135
|
var SUBSCRIPTION_ID = "s";
|
|
85
136
|
var createSyncCollection = (options) => {
|
|
86
137
|
const key = options.key ?? ((row) => row.id);
|
|
@@ -129,6 +180,20 @@ var createSyncCollection = (options) => {
|
|
|
129
180
|
args: mutation.args
|
|
130
181
|
})));
|
|
131
182
|
};
|
|
183
|
+
let cacheScheduled = false;
|
|
184
|
+
const persistCache = () => {
|
|
185
|
+
if (options.cache === undefined || cacheScheduled) {
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
cacheScheduled = true;
|
|
189
|
+
queueMicrotask(() => {
|
|
190
|
+
cacheScheduled = false;
|
|
191
|
+
options.cache?.save({
|
|
192
|
+
rows: [...confirmed.values()],
|
|
193
|
+
version: appliedVersion
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
};
|
|
132
197
|
const settlePending = (mutationId) => {
|
|
133
198
|
const index = pending.findIndex((mutation2) => mutation2.mutationId === mutationId);
|
|
134
199
|
if (index === -1) {
|
|
@@ -147,6 +212,7 @@ var createSyncCollection = (options) => {
|
|
|
147
212
|
if (frame.version !== undefined) {
|
|
148
213
|
appliedVersion = frame.version;
|
|
149
214
|
}
|
|
215
|
+
persistCache();
|
|
150
216
|
recompute({ status: "ready", error: undefined });
|
|
151
217
|
} else if (frame.type === "diff") {
|
|
152
218
|
for (const row of frame.removed) {
|
|
@@ -161,7 +227,8 @@ var createSyncCollection = (options) => {
|
|
|
161
227
|
if (frame.version !== undefined) {
|
|
162
228
|
appliedVersion = Math.max(appliedVersion, frame.version);
|
|
163
229
|
}
|
|
164
|
-
|
|
230
|
+
persistCache();
|
|
231
|
+
recompute({ status: "ready", error: undefined });
|
|
165
232
|
} else if (frame.type === "error") {
|
|
166
233
|
setState({ error: frame.message });
|
|
167
234
|
options.onError?.(frame.message);
|
|
@@ -225,7 +292,6 @@ var createSyncCollection = (options) => {
|
|
|
225
292
|
reconnectTimer = setTimeout(connect, delay);
|
|
226
293
|
};
|
|
227
294
|
};
|
|
228
|
-
connect();
|
|
229
295
|
const hydratePersisted = async () => {
|
|
230
296
|
if (options.storage === undefined) {
|
|
231
297
|
return;
|
|
@@ -250,7 +316,35 @@ var createSyncCollection = (options) => {
|
|
|
250
316
|
}
|
|
251
317
|
}
|
|
252
318
|
};
|
|
253
|
-
|
|
319
|
+
const hydrateCache = async () => {
|
|
320
|
+
if (options.cache === undefined) {
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
let snapshot;
|
|
324
|
+
try {
|
|
325
|
+
snapshot = await options.cache.load();
|
|
326
|
+
} catch {
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
if (snapshot === undefined || appliedVersion > 0) {
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
for (const row of snapshot.rows) {
|
|
333
|
+
confirmed.set(key(row), row);
|
|
334
|
+
}
|
|
335
|
+
appliedVersion = snapshot.version;
|
|
336
|
+
recompute();
|
|
337
|
+
};
|
|
338
|
+
if (options.cache === undefined) {
|
|
339
|
+
connect();
|
|
340
|
+
hydratePersisted();
|
|
341
|
+
} else {
|
|
342
|
+
(async () => {
|
|
343
|
+
await hydrateCache();
|
|
344
|
+
await hydratePersisted();
|
|
345
|
+
connect();
|
|
346
|
+
})();
|
|
347
|
+
}
|
|
254
348
|
return {
|
|
255
349
|
get: () => state,
|
|
256
350
|
subscribe: (listener) => {
|
|
@@ -343,5 +437,5 @@ export {
|
|
|
343
437
|
SyncCollectionService
|
|
344
438
|
};
|
|
345
439
|
|
|
346
|
-
//# debugId=
|
|
440
|
+
//# debugId=C534ED0FEAC9CC6664756E2164756E21
|
|
347
441
|
//# sourceMappingURL=index.js.map
|
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
"sources": ["../src/angular/sync-collection.service.ts", "../src/client/syncCollection.ts"],
|
|
4
4
|
"sourcesContent": [
|
|
5
5
|
"import { computed, Injectable, OnDestroy, signal } from '@angular/core';\nimport { createSyncCollection } from '../client/syncCollection';\nimport type {\n\tMutateOptions,\n\tSyncCollection,\n\tSyncCollectionOptions,\n\tSyncCollectionStatus\n} from '../client/syncCollection';\n\n/**\n * Angular binding for live sync-engine collections (the Tier 3 store). Inject\n * the service and call `connect(options)` to get `data`/`status`/`error` signals\n * maintained from the WebSocket diff stream, plus an optimistic `mutate`. All\n * opened collections close on the service's destroy.\n *\n * SSR-safe: the socket only opens in a browser, so server rendering is inert.\n */\n@Injectable({ providedIn: 'root' })\nexport class SyncCollectionService implements OnDestroy {\n\tprivate readonly collections = new Set<SyncCollection<unknown>>();\n\n\tconnect<T>(options: SyncCollectionOptions<T>) {\n\t\tconst data = signal<T[]>([]);\n\t\tconst status = signal<SyncCollectionStatus>('connecting');\n\t\tconst error = signal<unknown>(undefined);\n\n\t\tlet collection: SyncCollection<T> | null = null;\n\n\t\tif (typeof window !== 'undefined') {\n\t\t\tcollection = createSyncCollection<T>(options);\n\t\t\tthis.collections.add(collection as SyncCollection<unknown>);\n\t\t\tconst apply = (state: {\n\t\t\t\tdata: T[];\n\t\t\t\tstatus: SyncCollectionStatus;\n\t\t\t\terror: unknown;\n\t\t\t}) => {\n\t\t\t\tdata.set(state.data);\n\t\t\t\tstatus.set(state.status);\n\t\t\t\terror.set(state.error);\n\t\t\t};\n\t\t\tapply(collection.get());\n\t\t\tcollection.subscribe(apply);\n\t\t}\n\n\t\tconst mutate = <R = unknown>(\n\t\t\tmutateOptions: MutateOptions<T>\n\t\t): Promise<R> =>\n\t\t\tcollection\n\t\t\t\t? collection.mutate<R>(mutateOptions)\n\t\t\t\t: Promise.reject(new Error('sync collection is not ready'));\n\n\t\treturn {\n\t\t\tdata: computed(() => data()),\n\t\t\terror: computed(() => error()),\n\t\t\tmutate,\n\t\t\tstatus: computed(() => status())\n\t\t};\n\t}\n\n\tngOnDestroy() {\n\t\tfor (const collection of this.collections) {\n\t\t\tcollection.close();\n\t\t}\n\t\tthis.collections.clear();\n\t}\n}\n",
|
|
6
|
-
"import type { ServerFrame } from '../engine/connection';\nimport type { RowKey } from '../engine/types';\n\nexport type { ServerFrame } from '../engine/connection';\n\nexport type SyncCollectionStatus = 'connecting' | 'ready' | 'closed';\n\nexport type SyncCollectionState<T> = {\n\t/** Visible rows: the server state with pending optimistic mutations applied. */\n\tdata: T[];\n\t/** Connection/sync status. */\n\tstatus: SyncCollectionStatus;\n\t/** Last error message from the server, or `undefined`. */\n\terror: unknown;\n};\n\n/** A working set a mutation's optimistic effect edits in place. */\nexport type OptimisticDraft<T> = {\n\t/** Insert or replace a row by key. */\n\tset: (row: T) => void;\n\t/** Remove a row by key. */\n\tdelete: (key: RowKey) => void;\n};\n\nexport type MutateOptions<T> = {\n\t/** Registered server mutation name. */\n\tname: string;\n\t/** Arguments forwarded to the mutation handler. */\n\targs?: unknown;\n\t/**\n\t * Apply this mutation's effect to the local set immediately for instant UI.\n\t * Reverted automatically if the server rejects it. Omit for a non-optimistic\n\t * mutation (UI updates only once the authoritative diff arrives).\n\t */\n\toptimistic?: (draft: OptimisticDraft<T>) => void;\n};\n\n/** A pending mutation persisted for replay across reloads. */\nexport type PendingMutationRecord = {\n\tmutationId: number;\n\tname: string;\n\targs: unknown;\n};\n\n/**\n * Durable storage for the pending-mutation queue, so unconfirmed mutations\n * survive a page reload (offline). The queue is replayed when the socket\n * connects; records are dropped as they're acked.\n */\nexport type MutationStorage = {\n\tload: () => PendingMutationRecord[] | Promise<PendingMutationRecord[]>;\n\tsave: (records: PendingMutationRecord[]) => void | Promise<void>;\n};\n\n/**\n * A {@link MutationStorage} backed by `localStorage` under `key`. No-ops where\n * `localStorage` is unavailable (e.g. SSR).\n */\nexport const localStorageMutationStorage = (key: string): MutationStorage => ({\n\tload: () => {\n\t\tconst raw = globalThis.localStorage?.getItem(key);\n\t\treturn raw ? (JSON.parse(raw) as PendingMutationRecord[]) : [];\n\t},\n\tsave: (records) => {\n\t\tglobalThis.localStorage?.setItem(key, JSON.stringify(records));\n\t}\n});\n\nexport type SyncCollectionOptions<T> = {\n\t/** WebSocket URL of the {@link syncSocket} endpoint (e.g. `ws://host/sync/ws`). */\n\turl: string;\n\t/** Registered collection name to subscribe to. */\n\tcollection: string;\n\t/** Query params forwarded to the server collection's hydrate/match/authorize. */\n\tparams?: unknown;\n\t/** Row identity, used to apply diffs and optimistic edits. Defaults to `row.id`. */\n\tkey?: (row: T) => RowKey;\n\t/** WebSocket implementation; defaults to the global one (pass for tests/SSR). */\n\twebSocketImpl?: typeof WebSocket;\n\t/**\n\t * Base reconnect delay (ms), doubled each attempt up to `maxReconnectMs`.\n\t * Set 0 to disable auto-reconnect. Defaults to 500.\n\t */\n\treconnectMs?: number;\n\t/** Maximum reconnect backoff (ms). Defaults to 10000. */\n\tmaxReconnectMs?: number;\n\t/**\n\t * Persist the pending-mutation queue so it survives a reload (offline) and\n\t * replays on connect. See {@link localStorageMutationStorage}.\n\t */\n\tstorage?: MutationStorage;\n\t/** Called with each server error message. */\n\tonError?: (error: unknown) => void;\n};\n\nexport type SyncCollection<T> = {\n\t/** Current state snapshot (stable reference until the next change). */\n\tget: () => SyncCollectionState<T>;\n\t/** Subscribe to state changes; returns an unsubscribe. */\n\tsubscribe: (\n\t\tlistener: (state: SyncCollectionState<T>) => void\n\t) => () => void;\n\t/**\n\t * Run a server mutation, optionally applying it optimistically. Resolves with\n\t * the server's result on ack, rejects (and rolls back) on reject. Pending\n\t * mutations are replayed when the socket reconnects, so they survive a drop.\n\t */\n\tmutate: <R = unknown>(options: MutateOptions<T>) => Promise<R>;\n\t/** Unsubscribe on the server, close the socket, and stop reconnecting. */\n\tclose: () => void;\n};\n\n// One store subscribes to exactly one collection, so a fixed frame id suffices.\nconst SUBSCRIPTION_ID = 's';\n\ntype PendingMutation<T> = {\n\tmutationId: number;\n\tname: string;\n\targs: unknown;\n\toptimistic?: (draft: OptimisticDraft<T>) => void;\n\tresolve: (result: unknown) => void;\n\treject: (error: unknown) => void;\n};\n\n/**\n * A live collection backed by the WebSocket sync engine. Reads: connect,\n * subscribe, apply the server's snapshot then row-level diffs, re-sync on\n * reconnect. Writes: {@link SyncCollection.mutate} applies an optimistic overlay\n * immediately, sends the mutation, and reconciles on ack (drop the overlay — the\n * authoritative diff already arrived) or reject (roll back). Framework-agnostic\n * (`get` + `subscribe`).\n *\n * Mutations are replayed on reconnect, so make server mutations idempotent —\n * delivery is at-least-once if an ack is lost across a drop.\n */\nexport const createSyncCollection = <T>(\n\toptions: SyncCollectionOptions<T>\n): SyncCollection<T> => {\n\tconst key = options.key ?? ((row: T) => (row as { id: RowKey }).id);\n\tconst reconnectMs = options.reconnectMs ?? 500;\n\tconst maxReconnectMs = options.maxReconnectMs ?? 10_000;\n\tconst Impl = options.webSocketImpl ?? globalThis.WebSocket;\n\tif (!Impl) {\n\t\tthrow new Error(\n\t\t\t'createSyncCollection requires WebSocket. Run in a browser or pass webSocketImpl.'\n\t\t);\n\t}\n\n\t// Server-authoritative rows; `pending` is the optimistic overlay on top.\n\tconst confirmed = new Map<RowKey, T>();\n\tconst pending: PendingMutation<T>[] = [];\n\tlet mutationSeq = 0;\n\n\tlet state: SyncCollectionState<T> = {\n\t\tdata: [],\n\t\tstatus: 'connecting',\n\t\terror: undefined\n\t};\n\tconst listeners = new Set<(state: SyncCollectionState<T>) => void>();\n\tconst setState = (patch: Partial<SyncCollectionState<T>>) => {\n\t\tstate = { ...state, ...patch };\n\t\tfor (const listener of listeners) {\n\t\t\tlistener(state);\n\t\t}\n\t};\n\n\t/** Recompute visible rows = confirmed + pending optimistic effects. */\n\tconst recompute = (patch: Partial<SyncCollectionState<T>> = {}) => {\n\t\tconst working = new Map(confirmed);\n\t\tconst draft: OptimisticDraft<T> = {\n\t\t\tset: (row) => working.set(key(row), row),\n\t\t\tdelete: (rowKey) => working.delete(rowKey)\n\t\t};\n\t\tfor (const mutation of pending) {\n\t\t\tmutation.optimistic?.(draft);\n\t\t}\n\t\tsetState({ ...patch, data: [...working.values()] });\n\t};\n\n\tlet socket: WebSocket | undefined;\n\tlet connected = false;\n\tlet closed = false;\n\tlet attempt = 0;\n\tlet reconnectTimer: ReturnType<typeof setTimeout> | undefined;\n\t// Highest change-feed version applied; sent as `since` to resume on reconnect.\n\tlet appliedVersion = 0;\n\n\tconst persist = () => {\n\t\tvoid options.storage?.save(\n\t\t\tpending.map((mutation) => ({\n\t\t\t\tmutationId: mutation.mutationId,\n\t\t\t\tname: mutation.name,\n\t\t\t\targs: mutation.args\n\t\t\t}))\n\t\t);\n\t};\n\n\tconst settlePending = (mutationId: number) => {\n\t\tconst index = pending.findIndex(\n\t\t\t(mutation) => mutation.mutationId === mutationId\n\t\t);\n\t\tif (index === -1) {\n\t\t\treturn undefined;\n\t\t}\n\t\tconst [mutation] = pending.splice(index, 1);\n\t\tpersist();\n\t\treturn mutation;\n\t};\n\n\tconst applyFrame = (frame: ServerFrame<T>) => {\n\t\tif (frame.type === 'snapshot') {\n\t\t\tconfirmed.clear();\n\t\t\tfor (const row of frame.rows) {\n\t\t\t\tconfirmed.set(key(row), row);\n\t\t\t}\n\t\t\tif (frame.version !== undefined) {\n\t\t\t\tappliedVersion = frame.version;\n\t\t\t}\n\t\t\trecompute({ status: 'ready', error: undefined });\n\t\t} else if (frame.type === 'diff') {\n\t\t\tfor (const row of frame.removed) {\n\t\t\t\tconfirmed.delete(key(row));\n\t\t\t}\n\t\t\tfor (const row of frame.added) {\n\t\t\t\tconfirmed.set(key(row), row);\n\t\t\t}\n\t\t\tfor (const row of frame.changed) {\n\t\t\t\tconfirmed.set(key(row), row);\n\t\t\t}\n\t\t\tif (frame.version !== undefined) {\n\t\t\t\tappliedVersion = Math.max(appliedVersion, frame.version);\n\t\t\t}\n\t\t\trecompute();\n\t\t} else if (frame.type === 'error') {\n\t\t\tsetState({ error: frame.message });\n\t\t\toptions.onError?.(frame.message);\n\t\t} else if (frame.type === 'ack') {\n\t\t\t// The authoritative diff already arrived (ordered before the ack), so\n\t\t\t// dropping the overlay leaves the confirmed row in place — no flicker.\n\t\t\tconst mutation = settlePending(frame.mutationId);\n\t\t\tif (mutation !== undefined) {\n\t\t\t\trecompute();\n\t\t\t\tmutation.resolve(frame.result);\n\t\t\t}\n\t\t} else if (frame.type === 'reject') {\n\t\t\t// roll the optimistic overlay back.\n\t\t\tconst mutation = settlePending(frame.mutationId);\n\t\t\tif (mutation !== undefined) {\n\t\t\t\trecompute();\n\t\t\t\tmutation.reject(new Error(String(frame.message)));\n\t\t\t}\n\t\t}\n\t\t// A `frame` (multi-collection batch) never reaches a single-collection\n\t\t// store — that's the multiplexed createSyncClient's job — so ignore it.\n\t};\n\n\tconst sendMutate = (mutation: PendingMutation<T>) => {\n\t\tif (connected) {\n\t\t\tsocket?.send(\n\t\t\t\tJSON.stringify({\n\t\t\t\t\ttype: 'mutate',\n\t\t\t\t\tmutationId: mutation.mutationId,\n\t\t\t\t\tname: mutation.name,\n\t\t\t\t\targs: mutation.args\n\t\t\t\t})\n\t\t\t);\n\t\t}\n\t};\n\n\tconst connect = () => {\n\t\tif (closed) {\n\t\t\treturn;\n\t\t}\n\t\tsetState({ status: 'connecting' });\n\t\tconst ws = new Impl(options.url);\n\t\tsocket = ws;\n\t\tws.onopen = () => {\n\t\t\tattempt = 0;\n\t\t\tconnected = true;\n\t\t\tws.send(\n\t\t\t\tJSON.stringify({\n\t\t\t\t\ttype: 'subscribe',\n\t\t\t\t\tid: SUBSCRIPTION_ID,\n\t\t\t\t\tcollection: options.collection,\n\t\t\t\t\tparams: options.params,\n\t\t\t\t\t// Resume from what we've applied (catch-up instead of snapshot).\n\t\t\t\t\tsince: appliedVersion > 0 ? appliedVersion : undefined\n\t\t\t\t})\n\t\t\t);\n\t\t\t// Replay anything still pending across the (re)connect.\n\t\t\tfor (const mutation of pending) {\n\t\t\t\tsendMutate(mutation);\n\t\t\t}\n\t\t};\n\t\tws.onmessage = (event) => {\n\t\t\ttry {\n\t\t\t\tapplyFrame(JSON.parse(event.data as string) as ServerFrame<T>);\n\t\t\t} catch {\n\t\t\t\t// ignore non-JSON frames\n\t\t\t}\n\t\t};\n\t\tws.onclose = () => {\n\t\t\tconnected = false;\n\t\t\tif (closed || reconnectMs <= 0) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst delay = Math.min(reconnectMs * 2 ** attempt, maxReconnectMs);\n\t\t\tattempt += 1;\n\t\t\treconnectTimer = setTimeout(connect, delay);\n\t\t};\n\t};\n\n\tconnect();\n\n\t// Reload recovery: re-queue persisted unconfirmed mutations and replay them.\n\t// They carry no optimistic effect or promise (the fresh snapshot is\n\t// authoritative); resending produces the server diffs that bring them in.\n\tconst hydratePersisted = async () => {\n\t\tif (options.storage === undefined) {\n\t\t\treturn;\n\t\t}\n\t\tconst records = await options.storage.load();\n\t\tfor (const record of records) {\n\t\t\tif (pending.some((m) => m.mutationId === record.mutationId)) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tpending.push({\n\t\t\t\tmutationId: record.mutationId,\n\t\t\t\tname: record.name,\n\t\t\t\targs: record.args,\n\t\t\t\tresolve: () => {},\n\t\t\t\treject: () => {}\n\t\t\t});\n\t\t\tmutationSeq = Math.max(mutationSeq, record.mutationId);\n\t\t}\n\t\tif (connected) {\n\t\t\tfor (const mutation of pending) {\n\t\t\t\tsendMutate(mutation);\n\t\t\t}\n\t\t}\n\t};\n\tvoid hydratePersisted();\n\n\treturn {\n\t\tget: () => state,\n\t\tsubscribe: (listener) => {\n\t\t\tlisteners.add(listener);\n\t\t\treturn () => {\n\t\t\t\tlisteners.delete(listener);\n\t\t\t};\n\t\t},\n\t\tmutate: <R = unknown>(mutateOptions: MutateOptions<T>) =>\n\t\t\tnew Promise<R>((resolve, reject) => {\n\t\t\t\tconst mutation: PendingMutation<T> = {\n\t\t\t\t\tmutationId: (mutationSeq += 1),\n\t\t\t\t\tname: mutateOptions.name,\n\t\t\t\t\targs: mutateOptions.args,\n\t\t\t\t\toptimistic: mutateOptions.optimistic,\n\t\t\t\t\tresolve: (result) => resolve(result as R),\n\t\t\t\t\treject\n\t\t\t\t};\n\t\t\t\tpending.push(mutation);\n\t\t\t\tpersist();\n\t\t\t\trecompute(); // apply the optimistic overlay immediately\n\t\t\t\tsendMutate(mutation);\n\t\t\t}),\n\t\tclose: () => {\n\t\t\tif (closed) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tclosed = true;\n\t\t\tconnected = false;\n\t\t\tif (reconnectTimer !== undefined) {\n\t\t\t\tclearTimeout(reconnectTimer);\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tsocket?.send(\n\t\t\t\t\tJSON.stringify({ type: 'unsubscribe', id: SUBSCRIPTION_ID })\n\t\t\t\t);\n\t\t\t\tsocket?.close();\n\t\t\t} catch {\n\t\t\t\t// socket already closing/closed\n\t\t\t}\n\t\t\t// Fail any still-pending mutations so their promises don't hang.\n\t\t\tfor (const mutation of pending.splice(0)) {\n\t\t\t\tmutation.reject(new Error('sync collection closed'));\n\t\t\t}\n\t\t\tpersist();\n\t\t\tsetState({ status: 'closed' });\n\t\t\tlisteners.clear();\n\t\t}\n\t};\n};\n"
|
|
6
|
+
"import type { ServerFrame } from '../engine/connection';\nimport type { RowKey } from '../engine/types';\n\nexport type { ServerFrame } from '../engine/connection';\n\nexport type SyncCollectionStatus = 'connecting' | 'ready' | 'closed';\n\nexport type SyncCollectionState<T> = {\n\t/** Visible rows: the server state with pending optimistic mutations applied. */\n\tdata: T[];\n\t/** Connection/sync status. */\n\tstatus: SyncCollectionStatus;\n\t/** Last error message from the server, or `undefined`. */\n\terror: unknown;\n};\n\n/** A working set a mutation's optimistic effect edits in place. */\nexport type OptimisticDraft<T> = {\n\t/** Insert or replace a row by key. */\n\tset: (row: T) => void;\n\t/** Remove a row by key. */\n\tdelete: (key: RowKey) => void;\n};\n\nexport type MutateOptions<T> = {\n\t/** Registered server mutation name. */\n\tname: string;\n\t/** Arguments forwarded to the mutation handler. */\n\targs?: unknown;\n\t/**\n\t * Apply this mutation's effect to the local set immediately for instant UI.\n\t * Reverted automatically if the server rejects it. Omit for a non-optimistic\n\t * mutation (UI updates only once the authoritative diff arrives).\n\t */\n\toptimistic?: (draft: OptimisticDraft<T>) => void;\n};\n\n/** A pending mutation persisted for replay across reloads. */\nexport type PendingMutationRecord = {\n\tmutationId: number;\n\tname: string;\n\targs: unknown;\n};\n\n/**\n * Durable storage for the pending-mutation queue, so unconfirmed mutations\n * survive a page reload (offline). The queue is replayed when the socket\n * connects; records are dropped as they're acked.\n */\nexport type MutationStorage = {\n\tload: () => PendingMutationRecord[] | Promise<PendingMutationRecord[]>;\n\tsave: (records: PendingMutationRecord[]) => void | Promise<void>;\n};\n\n/**\n * A {@link MutationStorage} backed by `localStorage` under `key`. No-ops where\n * `localStorage` is unavailable (e.g. SSR).\n */\nexport const localStorageMutationStorage = (key: string): MutationStorage => ({\n\tload: () => {\n\t\tconst raw = globalThis.localStorage?.getItem(key);\n\t\treturn raw ? (JSON.parse(raw) as PendingMutationRecord[]) : [];\n\t},\n\tsave: (records) => {\n\t\tglobalThis.localStorage?.setItem(key, JSON.stringify(records));\n\t}\n});\n\n/**\n * A persisted snapshot of a collection's server-authoritative rows plus the\n * change-feed `version` they were current as of — the cursor used to resume on\n * the next connect (catch-up diff if the server's changelog still covers it, a\n * fresh snapshot otherwise).\n */\nexport type CollectionCacheSnapshot<T> = {\n\trows: T[];\n\tversion: number;\n};\n\n/**\n * Durable local cache of a collection's confirmed rows, so reads are instant on\n * reload and available offline (local-first). Distinct from {@link\n * MutationStorage}, which persists *unconfirmed writes*: the cache is the\n * read side, the queue is the write side. On startup the cache hydrates the\n * collection before the socket connects; the engine then resumes from the\n * cached `version`.\n */\nexport type CollectionCache<T> = {\n\tload: () =>\n\t\t| CollectionCacheSnapshot<T>\n\t\t| undefined\n\t\t| Promise<CollectionCacheSnapshot<T> | undefined>;\n\tsave: (snapshot: CollectionCacheSnapshot<T>) => void | Promise<void>;\n\t/** Drop the cached snapshot (optional). */\n\tclear?: () => void | Promise<void>;\n};\n\n/**\n * A {@link CollectionCache} backed by `localStorage` under `key`. Synchronous\n * and capped (~5MB); fine for small collections. No-ops where `localStorage`\n * is unavailable (e.g. SSR). For larger sets use {@link indexedDbCollectionCache}.\n */\nexport const localStorageCollectionCache = <T>(\n\tkey: string\n): CollectionCache<T> => ({\n\tload: () => {\n\t\tconst raw = globalThis.localStorage?.getItem(key);\n\t\treturn raw\n\t\t\t? (JSON.parse(raw) as CollectionCacheSnapshot<T>)\n\t\t\t: undefined;\n\t},\n\tsave: (snapshot) => {\n\t\tglobalThis.localStorage?.setItem(key, JSON.stringify(snapshot));\n\t},\n\tclear: () => {\n\t\tglobalThis.localStorage?.removeItem(key);\n\t}\n});\n\nconst openIndexedDb = (\n\tdatabaseName: string,\n\tstoreName: string\n): Promise<IDBDatabase> =>\n\tnew Promise((resolve, reject) => {\n\t\tconst request = globalThis.indexedDB.open(databaseName, 1);\n\t\trequest.onupgradeneeded = () => {\n\t\t\trequest.result.createObjectStore(storeName);\n\t\t};\n\t\trequest.onsuccess = () => resolve(request.result);\n\t\trequest.onerror = () => reject(request.error);\n\t});\n\n/**\n * A {@link CollectionCache} backed by IndexedDB — the durable, large-capacity\n * local-first store. Asynchronous; one row per collection `key` in a shared\n * object store. No-ops (resolving to `undefined`) where `indexedDB` is\n * unavailable (e.g. SSR), so the collection falls back to the server snapshot.\n */\nexport const indexedDbCollectionCache = <T>({\n\tkey,\n\tdatabaseName = 'absolutejs-sync',\n\tstoreName = 'collections'\n}: {\n\t/** Distinct entry name within the store (e.g. the collection + params). */\n\tkey: string;\n\t/** IndexedDB database name. Defaults to `absolutejs-sync`. */\n\tdatabaseName?: string;\n\t/** Object-store name. Defaults to `collections`. */\n\tstoreName?: string;\n}): CollectionCache<T> => {\n\tlet handle: Promise<IDBDatabase> | undefined;\n\tconst database = () => {\n\t\thandle ??= openIndexedDb(databaseName, storeName);\n\t\treturn handle;\n\t};\n\tconst withStore = async <R>(\n\t\tmode: IDBTransactionMode,\n\t\trun: (store: IDBObjectStore) => IDBRequest\n\t): Promise<R | undefined> => {\n\t\tif (globalThis.indexedDB === undefined) {\n\t\t\treturn undefined;\n\t\t}\n\t\tconst db = await database();\n\t\treturn new Promise<R>((resolve, reject) => {\n\t\t\tconst request = run(\n\t\t\t\tdb.transaction(storeName, mode).objectStore(storeName)\n\t\t\t);\n\t\t\trequest.onsuccess = () => resolve(request.result as R);\n\t\t\trequest.onerror = () => reject(request.error);\n\t\t});\n\t};\n\n\treturn {\n\t\tload: () =>\n\t\t\twithStore<CollectionCacheSnapshot<T>>('readonly', (store) =>\n\t\t\t\tstore.get(key)\n\t\t\t),\n\t\tsave: async (snapshot) => {\n\t\t\tawait withStore('readwrite', (store) => store.put(snapshot, key));\n\t\t},\n\t\tclear: async () => {\n\t\t\tawait withStore('readwrite', (store) => store.delete(key));\n\t\t}\n\t};\n};\n\nexport type SyncCollectionOptions<T> = {\n\t/** WebSocket URL of the {@link syncSocket} endpoint (e.g. `ws://host/sync/ws`). */\n\turl: string;\n\t/** Registered collection name to subscribe to. */\n\tcollection: string;\n\t/** Query params forwarded to the server collection's hydrate/match/authorize. */\n\tparams?: unknown;\n\t/** Row identity, used to apply diffs and optimistic edits. Defaults to `row.id`. */\n\tkey?: (row: T) => RowKey;\n\t/** WebSocket implementation; defaults to the global one (pass for tests/SSR). */\n\twebSocketImpl?: typeof WebSocket;\n\t/**\n\t * Base reconnect delay (ms), doubled each attempt up to `maxReconnectMs`.\n\t * Set 0 to disable auto-reconnect. Defaults to 500.\n\t */\n\treconnectMs?: number;\n\t/** Maximum reconnect backoff (ms). Defaults to 10000. */\n\tmaxReconnectMs?: number;\n\t/**\n\t * Persist the pending-mutation queue so it survives a reload (offline) and\n\t * replays on connect. See {@link localStorageMutationStorage}.\n\t */\n\tstorage?: MutationStorage;\n\t/**\n\t * Persist confirmed rows locally for instant reads on reload and offline\n\t * (local-first). Hydrated before the socket connects; the engine then\n\t * resumes from the cached version (catch-up diff, or a fresh snapshot if the\n\t * server's changelog no longer covers it). See {@link\n\t * localStorageCollectionCache} / {@link indexedDbCollectionCache}.\n\t */\n\tcache?: CollectionCache<T>;\n\t/** Called with each server error message. */\n\tonError?: (error: unknown) => void;\n};\n\nexport type SyncCollection<T> = {\n\t/** Current state snapshot (stable reference until the next change). */\n\tget: () => SyncCollectionState<T>;\n\t/** Subscribe to state changes; returns an unsubscribe. */\n\tsubscribe: (\n\t\tlistener: (state: SyncCollectionState<T>) => void\n\t) => () => void;\n\t/**\n\t * Run a server mutation, optionally applying it optimistically. Resolves with\n\t * the server's result on ack, rejects (and rolls back) on reject. Pending\n\t * mutations are replayed when the socket reconnects, so they survive a drop.\n\t */\n\tmutate: <R = unknown>(options: MutateOptions<T>) => Promise<R>;\n\t/** Unsubscribe on the server, close the socket, and stop reconnecting. */\n\tclose: () => void;\n};\n\n// One store subscribes to exactly one collection, so a fixed frame id suffices.\nconst SUBSCRIPTION_ID = 's';\n\ntype PendingMutation<T> = {\n\tmutationId: number;\n\tname: string;\n\targs: unknown;\n\toptimistic?: (draft: OptimisticDraft<T>) => void;\n\tresolve: (result: unknown) => void;\n\treject: (error: unknown) => void;\n};\n\n/**\n * A live collection backed by the WebSocket sync engine. Reads: connect,\n * subscribe, apply the server's snapshot then row-level diffs, re-sync on\n * reconnect. Writes: {@link SyncCollection.mutate} applies an optimistic overlay\n * immediately, sends the mutation, and reconciles on ack (drop the overlay — the\n * authoritative diff already arrived) or reject (roll back). Framework-agnostic\n * (`get` + `subscribe`).\n *\n * Mutations are replayed on reconnect, so make server mutations idempotent —\n * delivery is at-least-once if an ack is lost across a drop.\n */\nexport const createSyncCollection = <T>(\n\toptions: SyncCollectionOptions<T>\n): SyncCollection<T> => {\n\tconst key = options.key ?? ((row: T) => (row as { id: RowKey }).id);\n\tconst reconnectMs = options.reconnectMs ?? 500;\n\tconst maxReconnectMs = options.maxReconnectMs ?? 10_000;\n\tconst Impl = options.webSocketImpl ?? globalThis.WebSocket;\n\tif (!Impl) {\n\t\tthrow new Error(\n\t\t\t'createSyncCollection requires WebSocket. Run in a browser or pass webSocketImpl.'\n\t\t);\n\t}\n\n\t// Server-authoritative rows; `pending` is the optimistic overlay on top.\n\tconst confirmed = new Map<RowKey, T>();\n\tconst pending: PendingMutation<T>[] = [];\n\tlet mutationSeq = 0;\n\n\tlet state: SyncCollectionState<T> = {\n\t\tdata: [],\n\t\tstatus: 'connecting',\n\t\terror: undefined\n\t};\n\tconst listeners = new Set<(state: SyncCollectionState<T>) => void>();\n\tconst setState = (patch: Partial<SyncCollectionState<T>>) => {\n\t\tstate = { ...state, ...patch };\n\t\tfor (const listener of listeners) {\n\t\t\tlistener(state);\n\t\t}\n\t};\n\n\t/** Recompute visible rows = confirmed + pending optimistic effects. */\n\tconst recompute = (patch: Partial<SyncCollectionState<T>> = {}) => {\n\t\tconst working = new Map(confirmed);\n\t\tconst draft: OptimisticDraft<T> = {\n\t\t\tset: (row) => working.set(key(row), row),\n\t\t\tdelete: (rowKey) => working.delete(rowKey)\n\t\t};\n\t\tfor (const mutation of pending) {\n\t\t\tmutation.optimistic?.(draft);\n\t\t}\n\t\tsetState({ ...patch, data: [...working.values()] });\n\t};\n\n\tlet socket: WebSocket | undefined;\n\tlet connected = false;\n\tlet closed = false;\n\tlet attempt = 0;\n\tlet reconnectTimer: ReturnType<typeof setTimeout> | undefined;\n\t// Highest change-feed version applied; sent as `since` to resume on reconnect.\n\tlet appliedVersion = 0;\n\n\tconst persist = () => {\n\t\tvoid options.storage?.save(\n\t\t\tpending.map((mutation) => ({\n\t\t\t\tmutationId: mutation.mutationId,\n\t\t\t\tname: mutation.name,\n\t\t\t\targs: mutation.args\n\t\t\t}))\n\t\t);\n\t};\n\n\t// Coalesce a burst of confirmed changes (a frame of diffs) into one cache\n\t// write per tick. Persists only the server-authoritative set — never the\n\t// optimistic overlay (those live in the mutation queue instead).\n\tlet cacheScheduled = false;\n\tconst persistCache = () => {\n\t\tif (options.cache === undefined || cacheScheduled) {\n\t\t\treturn;\n\t\t}\n\t\tcacheScheduled = true;\n\t\tqueueMicrotask(() => {\n\t\t\tcacheScheduled = false;\n\t\t\tvoid options.cache?.save({\n\t\t\t\trows: [...confirmed.values()],\n\t\t\t\tversion: appliedVersion\n\t\t\t});\n\t\t});\n\t};\n\n\tconst settlePending = (mutationId: number) => {\n\t\tconst index = pending.findIndex(\n\t\t\t(mutation) => mutation.mutationId === mutationId\n\t\t);\n\t\tif (index === -1) {\n\t\t\treturn undefined;\n\t\t}\n\t\tconst [mutation] = pending.splice(index, 1);\n\t\tpersist();\n\t\treturn mutation;\n\t};\n\n\tconst applyFrame = (frame: ServerFrame<T>) => {\n\t\tif (frame.type === 'snapshot') {\n\t\t\tconfirmed.clear();\n\t\t\tfor (const row of frame.rows) {\n\t\t\t\tconfirmed.set(key(row), row);\n\t\t\t}\n\t\t\tif (frame.version !== undefined) {\n\t\t\t\tappliedVersion = frame.version;\n\t\t\t}\n\t\t\tpersistCache();\n\t\t\trecompute({ status: 'ready', error: undefined });\n\t\t} else if (frame.type === 'diff') {\n\t\t\tfor (const row of frame.removed) {\n\t\t\t\tconfirmed.delete(key(row));\n\t\t\t}\n\t\t\tfor (const row of frame.added) {\n\t\t\t\tconfirmed.set(key(row), row);\n\t\t\t}\n\t\t\tfor (const row of frame.changed) {\n\t\t\t\tconfirmed.set(key(row), row);\n\t\t\t}\n\t\t\tif (frame.version !== undefined) {\n\t\t\t\tappliedVersion = Math.max(appliedVersion, frame.version);\n\t\t\t}\n\t\t\tpersistCache();\n\t\t\t// A diff only arrives once subscribed — including the catch-up diff a\n\t\t\t// resume replies with — so receiving one means we're live.\n\t\t\trecompute({ status: 'ready', error: undefined });\n\t\t} else if (frame.type === 'error') {\n\t\t\tsetState({ error: frame.message });\n\t\t\toptions.onError?.(frame.message);\n\t\t} else if (frame.type === 'ack') {\n\t\t\t// The authoritative diff already arrived (ordered before the ack), so\n\t\t\t// dropping the overlay leaves the confirmed row in place — no flicker.\n\t\t\tconst mutation = settlePending(frame.mutationId);\n\t\t\tif (mutation !== undefined) {\n\t\t\t\trecompute();\n\t\t\t\tmutation.resolve(frame.result);\n\t\t\t}\n\t\t} else if (frame.type === 'reject') {\n\t\t\t// roll the optimistic overlay back.\n\t\t\tconst mutation = settlePending(frame.mutationId);\n\t\t\tif (mutation !== undefined) {\n\t\t\t\trecompute();\n\t\t\t\tmutation.reject(new Error(String(frame.message)));\n\t\t\t}\n\t\t}\n\t\t// A `frame` (multi-collection batch) never reaches a single-collection\n\t\t// store — that's the multiplexed createSyncClient's job — so ignore it.\n\t};\n\n\tconst sendMutate = (mutation: PendingMutation<T>) => {\n\t\tif (connected) {\n\t\t\tsocket?.send(\n\t\t\t\tJSON.stringify({\n\t\t\t\t\ttype: 'mutate',\n\t\t\t\t\tmutationId: mutation.mutationId,\n\t\t\t\t\tname: mutation.name,\n\t\t\t\t\targs: mutation.args\n\t\t\t\t})\n\t\t\t);\n\t\t}\n\t};\n\n\tconst connect = () => {\n\t\tif (closed) {\n\t\t\treturn;\n\t\t}\n\t\tsetState({ status: 'connecting' });\n\t\tconst ws = new Impl(options.url);\n\t\tsocket = ws;\n\t\tws.onopen = () => {\n\t\t\tattempt = 0;\n\t\t\tconnected = true;\n\t\t\tws.send(\n\t\t\t\tJSON.stringify({\n\t\t\t\t\ttype: 'subscribe',\n\t\t\t\t\tid: SUBSCRIPTION_ID,\n\t\t\t\t\tcollection: options.collection,\n\t\t\t\t\tparams: options.params,\n\t\t\t\t\t// Resume from what we've applied (catch-up instead of snapshot).\n\t\t\t\t\tsince: appliedVersion > 0 ? appliedVersion : undefined\n\t\t\t\t})\n\t\t\t);\n\t\t\t// Replay anything still pending across the (re)connect.\n\t\t\tfor (const mutation of pending) {\n\t\t\t\tsendMutate(mutation);\n\t\t\t}\n\t\t};\n\t\tws.onmessage = (event) => {\n\t\t\ttry {\n\t\t\t\tapplyFrame(JSON.parse(event.data as string) as ServerFrame<T>);\n\t\t\t} catch {\n\t\t\t\t// ignore non-JSON frames\n\t\t\t}\n\t\t};\n\t\tws.onclose = () => {\n\t\t\tconnected = false;\n\t\t\tif (closed || reconnectMs <= 0) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst delay = Math.min(reconnectMs * 2 ** attempt, maxReconnectMs);\n\t\t\tattempt += 1;\n\t\t\treconnectTimer = setTimeout(connect, delay);\n\t\t};\n\t};\n\n\t// Reload recovery: re-queue persisted unconfirmed mutations so they replay on\n\t// connect. They carry no optimistic effect or promise (the resumed/snapshot\n\t// state is authoritative); resending produces the diffs that bring them in.\n\tconst hydratePersisted = async () => {\n\t\tif (options.storage === undefined) {\n\t\t\treturn;\n\t\t}\n\t\tconst records = await options.storage.load();\n\t\tfor (const record of records) {\n\t\t\tif (pending.some((m) => m.mutationId === record.mutationId)) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tpending.push({\n\t\t\t\tmutationId: record.mutationId,\n\t\t\t\tname: record.name,\n\t\t\t\targs: record.args,\n\t\t\t\tresolve: () => {},\n\t\t\t\treject: () => {}\n\t\t\t});\n\t\t\tmutationSeq = Math.max(mutationSeq, record.mutationId);\n\t\t}\n\t\tif (connected) {\n\t\t\tfor (const mutation of pending) {\n\t\t\t\tsendMutate(mutation);\n\t\t\t}\n\t\t}\n\t};\n\n\t// Local-first: load cached rows + version before connecting, so reads are\n\t// instant on reload and available offline. The subscribe then resumes from\n\t// the cached version — a catch-up diff if the server's changelog still\n\t// covers it, else a fresh snapshot that replaces the stale cache.\n\tconst hydrateCache = async () => {\n\t\tif (options.cache === undefined) {\n\t\t\treturn;\n\t\t}\n\t\tlet snapshot: CollectionCacheSnapshot<T> | undefined;\n\t\ttry {\n\t\t\tsnapshot = await options.cache.load();\n\t\t} catch {\n\t\t\treturn; // corrupt/unavailable cache: fall back to the server snapshot\n\t\t}\n\t\t// Don't clobber server data if a frame somehow already landed.\n\t\tif (snapshot === undefined || appliedVersion > 0) {\n\t\t\treturn;\n\t\t}\n\t\tfor (const row of snapshot.rows) {\n\t\t\tconfirmed.set(key(row), row);\n\t\t}\n\t\tappliedVersion = snapshot.version;\n\t\trecompute(); // show cached rows immediately (status stays 'connecting')\n\t};\n\n\tif (options.cache === undefined) {\n\t\t// No cache: preserve the original connect-then-hydrate ordering/timing.\n\t\tconnect();\n\t\tvoid hydratePersisted();\n\t} else {\n\t\t// Cache: hydrate reads + queued writes first, then connect so the\n\t\t// subscribe carries the cached resume version.\n\t\tvoid (async () => {\n\t\t\tawait hydrateCache();\n\t\t\tawait hydratePersisted();\n\t\t\tconnect();\n\t\t})();\n\t}\n\n\treturn {\n\t\tget: () => state,\n\t\tsubscribe: (listener) => {\n\t\t\tlisteners.add(listener);\n\t\t\treturn () => {\n\t\t\t\tlisteners.delete(listener);\n\t\t\t};\n\t\t},\n\t\tmutate: <R = unknown>(mutateOptions: MutateOptions<T>) =>\n\t\t\tnew Promise<R>((resolve, reject) => {\n\t\t\t\tconst mutation: PendingMutation<T> = {\n\t\t\t\t\tmutationId: (mutationSeq += 1),\n\t\t\t\t\tname: mutateOptions.name,\n\t\t\t\t\targs: mutateOptions.args,\n\t\t\t\t\toptimistic: mutateOptions.optimistic,\n\t\t\t\t\tresolve: (result) => resolve(result as R),\n\t\t\t\t\treject\n\t\t\t\t};\n\t\t\t\tpending.push(mutation);\n\t\t\t\tpersist();\n\t\t\t\trecompute(); // apply the optimistic overlay immediately\n\t\t\t\tsendMutate(mutation);\n\t\t\t}),\n\t\tclose: () => {\n\t\t\tif (closed) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tclosed = true;\n\t\t\tconnected = false;\n\t\t\tif (reconnectTimer !== undefined) {\n\t\t\t\tclearTimeout(reconnectTimer);\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tsocket?.send(\n\t\t\t\t\tJSON.stringify({ type: 'unsubscribe', id: SUBSCRIPTION_ID })\n\t\t\t\t);\n\t\t\t\tsocket?.close();\n\t\t\t} catch {\n\t\t\t\t// socket already closing/closed\n\t\t\t}\n\t\t\t// Fail any still-pending mutations so their promises don't hang.\n\t\t\tfor (const mutation of pending.splice(0)) {\n\t\t\t\tmutation.reject(new Error('sync collection closed'));\n\t\t\t}\n\t\t\tpersist();\n\t\t\tsetState({ status: 'closed' });\n\t\t\tlisteners.clear();\n\t\t}\n\t};\n};\n"
|
|
7
7
|
],
|
|
8
|
-
"mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;;;AC0DO,IAAM,8BAA8B,CAAC,SAAkC;AAAA,EAC7E,MAAM,MAAM;AAAA,IACX,MAAM,MAAM,WAAW,cAAc,QAAQ,GAAG;AAAA,IAChD,OAAO,MAAO,KAAK,MAAM,GAAG,IAAgC,CAAC;AAAA;AAAA,EAE9D,MAAM,CAAC,YAAY;AAAA,IAClB,WAAW,cAAc,QAAQ,KAAK,KAAK,UAAU,OAAO,CAAC;AAAA;AAE/D;
|
|
9
|
-
"debugId": "
|
|
8
|
+
"mappings": "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",
|
|
9
|
+
"debugId": "C534ED0FEAC9CC6664756E2164756E21",
|
|
10
10
|
"names": []
|
|
11
11
|
}
|
package/dist/client/index.d.ts
CHANGED
|
@@ -3,8 +3,8 @@ export { createSyncSubscriber } from './subscriber';
|
|
|
3
3
|
export type { SyncSubscriber, SyncSubscriberOptions } from './subscriber';
|
|
4
4
|
export { createLiveQuery, jsonFetcher } from './liveQuery';
|
|
5
5
|
export type { LiveQuery, LiveQueryOptions, LiveQueryState } from './liveQuery';
|
|
6
|
-
export { createSyncCollection, localStorageMutationStorage } from './syncCollection';
|
|
7
|
-
export type { MutateOptions, MutationStorage, OptimisticDraft, PendingMutationRecord, SyncCollection, SyncCollectionOptions, SyncCollectionState, SyncCollectionStatus } from './syncCollection';
|
|
6
|
+
export { createSyncCollection, indexedDbCollectionCache, localStorageCollectionCache, localStorageMutationStorage } from './syncCollection';
|
|
7
|
+
export type { CollectionCache, CollectionCacheSnapshot, MutateOptions, MutationStorage, OptimisticDraft, PendingMutationRecord, SyncCollection, SyncCollectionOptions, SyncCollectionState, SyncCollectionStatus } from './syncCollection';
|
|
8
8
|
export { createPresence } from './presence';
|
|
9
9
|
export type { PresenceClient, PresenceClientOptions, PresenceMember } from './presence';
|
|
10
10
|
export { createSyncClient } from './syncClient';
|
package/dist/client/index.js
CHANGED
|
@@ -237,6 +237,57 @@ var localStorageMutationStorage = (key) => ({
|
|
|
237
237
|
globalThis.localStorage?.setItem(key, JSON.stringify(records));
|
|
238
238
|
}
|
|
239
239
|
});
|
|
240
|
+
var localStorageCollectionCache = (key) => ({
|
|
241
|
+
load: () => {
|
|
242
|
+
const raw = globalThis.localStorage?.getItem(key);
|
|
243
|
+
return raw ? JSON.parse(raw) : undefined;
|
|
244
|
+
},
|
|
245
|
+
save: (snapshot) => {
|
|
246
|
+
globalThis.localStorage?.setItem(key, JSON.stringify(snapshot));
|
|
247
|
+
},
|
|
248
|
+
clear: () => {
|
|
249
|
+
globalThis.localStorage?.removeItem(key);
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
var openIndexedDb = (databaseName, storeName) => new Promise((resolve, reject) => {
|
|
253
|
+
const request = globalThis.indexedDB.open(databaseName, 1);
|
|
254
|
+
request.onupgradeneeded = () => {
|
|
255
|
+
request.result.createObjectStore(storeName);
|
|
256
|
+
};
|
|
257
|
+
request.onsuccess = () => resolve(request.result);
|
|
258
|
+
request.onerror = () => reject(request.error);
|
|
259
|
+
});
|
|
260
|
+
var indexedDbCollectionCache = ({
|
|
261
|
+
key,
|
|
262
|
+
databaseName = "absolutejs-sync",
|
|
263
|
+
storeName = "collections"
|
|
264
|
+
}) => {
|
|
265
|
+
let handle;
|
|
266
|
+
const database = () => {
|
|
267
|
+
handle ??= openIndexedDb(databaseName, storeName);
|
|
268
|
+
return handle;
|
|
269
|
+
};
|
|
270
|
+
const withStore = async (mode, run) => {
|
|
271
|
+
if (globalThis.indexedDB === undefined) {
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
const db = await database();
|
|
275
|
+
return new Promise((resolve, reject) => {
|
|
276
|
+
const request = run(db.transaction(storeName, mode).objectStore(storeName));
|
|
277
|
+
request.onsuccess = () => resolve(request.result);
|
|
278
|
+
request.onerror = () => reject(request.error);
|
|
279
|
+
});
|
|
280
|
+
};
|
|
281
|
+
return {
|
|
282
|
+
load: () => withStore("readonly", (store) => store.get(key)),
|
|
283
|
+
save: async (snapshot) => {
|
|
284
|
+
await withStore("readwrite", (store) => store.put(snapshot, key));
|
|
285
|
+
},
|
|
286
|
+
clear: async () => {
|
|
287
|
+
await withStore("readwrite", (store) => store.delete(key));
|
|
288
|
+
}
|
|
289
|
+
};
|
|
290
|
+
};
|
|
240
291
|
var SUBSCRIPTION_ID = "s";
|
|
241
292
|
var createSyncCollection = (options) => {
|
|
242
293
|
const key = options.key ?? ((row) => row.id);
|
|
@@ -285,6 +336,20 @@ var createSyncCollection = (options) => {
|
|
|
285
336
|
args: mutation.args
|
|
286
337
|
})));
|
|
287
338
|
};
|
|
339
|
+
let cacheScheduled = false;
|
|
340
|
+
const persistCache = () => {
|
|
341
|
+
if (options.cache === undefined || cacheScheduled) {
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
cacheScheduled = true;
|
|
345
|
+
queueMicrotask(() => {
|
|
346
|
+
cacheScheduled = false;
|
|
347
|
+
options.cache?.save({
|
|
348
|
+
rows: [...confirmed.values()],
|
|
349
|
+
version: appliedVersion
|
|
350
|
+
});
|
|
351
|
+
});
|
|
352
|
+
};
|
|
288
353
|
const settlePending = (mutationId) => {
|
|
289
354
|
const index = pending.findIndex((mutation2) => mutation2.mutationId === mutationId);
|
|
290
355
|
if (index === -1) {
|
|
@@ -303,6 +368,7 @@ var createSyncCollection = (options) => {
|
|
|
303
368
|
if (frame.version !== undefined) {
|
|
304
369
|
appliedVersion = frame.version;
|
|
305
370
|
}
|
|
371
|
+
persistCache();
|
|
306
372
|
recompute({ status: "ready", error: undefined });
|
|
307
373
|
} else if (frame.type === "diff") {
|
|
308
374
|
for (const row of frame.removed) {
|
|
@@ -317,7 +383,8 @@ var createSyncCollection = (options) => {
|
|
|
317
383
|
if (frame.version !== undefined) {
|
|
318
384
|
appliedVersion = Math.max(appliedVersion, frame.version);
|
|
319
385
|
}
|
|
320
|
-
|
|
386
|
+
persistCache();
|
|
387
|
+
recompute({ status: "ready", error: undefined });
|
|
321
388
|
} else if (frame.type === "error") {
|
|
322
389
|
setState({ error: frame.message });
|
|
323
390
|
options.onError?.(frame.message);
|
|
@@ -381,7 +448,6 @@ var createSyncCollection = (options) => {
|
|
|
381
448
|
reconnectTimer = setTimeout(connect, delay);
|
|
382
449
|
};
|
|
383
450
|
};
|
|
384
|
-
connect();
|
|
385
451
|
const hydratePersisted = async () => {
|
|
386
452
|
if (options.storage === undefined) {
|
|
387
453
|
return;
|
|
@@ -406,7 +472,35 @@ var createSyncCollection = (options) => {
|
|
|
406
472
|
}
|
|
407
473
|
}
|
|
408
474
|
};
|
|
409
|
-
|
|
475
|
+
const hydrateCache = async () => {
|
|
476
|
+
if (options.cache === undefined) {
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
let snapshot;
|
|
480
|
+
try {
|
|
481
|
+
snapshot = await options.cache.load();
|
|
482
|
+
} catch {
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
if (snapshot === undefined || appliedVersion > 0) {
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
for (const row of snapshot.rows) {
|
|
489
|
+
confirmed.set(key(row), row);
|
|
490
|
+
}
|
|
491
|
+
appliedVersion = snapshot.version;
|
|
492
|
+
recompute();
|
|
493
|
+
};
|
|
494
|
+
if (options.cache === undefined) {
|
|
495
|
+
connect();
|
|
496
|
+
hydratePersisted();
|
|
497
|
+
} else {
|
|
498
|
+
(async () => {
|
|
499
|
+
await hydrateCache();
|
|
500
|
+
await hydratePersisted();
|
|
501
|
+
connect();
|
|
502
|
+
})();
|
|
503
|
+
}
|
|
410
504
|
return {
|
|
411
505
|
get: () => state,
|
|
412
506
|
subscribe: (listener) => {
|
|
@@ -1125,7 +1219,9 @@ export {
|
|
|
1125
1219
|
unwrapEden,
|
|
1126
1220
|
syncStore,
|
|
1127
1221
|
localStorageMutationStorage,
|
|
1222
|
+
localStorageCollectionCache,
|
|
1128
1223
|
jsonFetcher,
|
|
1224
|
+
indexedDbCollectionCache,
|
|
1129
1225
|
createSyncSubscriber,
|
|
1130
1226
|
createSyncCollection,
|
|
1131
1227
|
createSyncClient,
|
|
@@ -1133,5 +1229,5 @@ export {
|
|
|
1133
1229
|
createLiveQuery
|
|
1134
1230
|
};
|
|
1135
1231
|
|
|
1136
|
-
//# debugId=
|
|
1232
|
+
//# debugId=FB434F3CF30BD04964756E2164756E21
|
|
1137
1233
|
//# sourceMappingURL=index.js.map
|