@absolutejs/sync 0.1.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 +60 -36
- package/dist/adapters/drizzle/collection.d.ts +27 -0
- package/dist/adapters/drizzle/index.d.ts +3 -0
- package/dist/adapters/drizzle/index.js +139 -2
- package/dist/adapters/drizzle/index.js.map +5 -3
- package/dist/adapters/drizzle/predicate.d.ts +20 -0
- package/dist/angular/index.js +99 -5
- package/dist/angular/index.js.map +3 -3
- package/dist/client/index.d.ts +6 -2
- package/dist/client/index.js +456 -5
- package/dist/client/index.js.map +6 -4
- package/dist/client/presence.d.ts +37 -0
- package/dist/client/syncClient.d.ts +53 -0
- package/dist/client/syncCollection.d.ts +52 -0
- package/dist/engine/cluster.d.ts +41 -0
- package/dist/engine/connection.d.ts +33 -1
- package/dist/engine/index.d.ts +8 -2
- package/dist/engine/index.js +542 -37
- package/dist/engine/index.js.map +9 -6
- package/dist/engine/mutation.d.ts +39 -3
- package/dist/engine/presence.d.ts +46 -0
- package/dist/engine/reactive.d.ts +67 -0
- package/dist/engine/socket.d.ts +4 -1
- package/dist/engine/syncEngine.d.ts +33 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +175 -9
- package/dist/index.js.map +6 -5
- package/dist/react/index.js +99 -5
- package/dist/react/index.js.map +3 -3
- package/dist/svelte/index.js +99 -5
- package/dist/svelte/index.js.map +3 -3
- package/dist/vue/index.js +99 -5
- package/dist/vue/index.js.map +3 -3
- package/package.json +3 -1
|
@@ -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 {\n\t\t\t// reject — 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};\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,7 +3,11 @@ 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
|
+
export { createPresence } from './presence';
|
|
9
|
+
export type { PresenceClient, PresenceClientOptions, PresenceMember } from './presence';
|
|
10
|
+
export { createSyncClient } from './syncClient';
|
|
11
|
+
export type { SyncClient, SyncClientOptions, SyncCollectionHandle, SyncCollectionHandleOptions } from './syncClient';
|
|
8
12
|
export { syncStore, unwrapEden } from './syncStore';
|
|
9
13
|
export type { MutationMap, SyncStore, SyncStoreOptions, SyncStoreState, SyncStoreStatus } from './syncStore';
|
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);
|
|
@@ -327,7 +394,7 @@ var createSyncCollection = (options) => {
|
|
|
327
394
|
recompute();
|
|
328
395
|
mutation.resolve(frame.result);
|
|
329
396
|
}
|
|
330
|
-
} else {
|
|
397
|
+
} else if (frame.type === "reject") {
|
|
331
398
|
const mutation = settlePending(frame.mutationId);
|
|
332
399
|
if (mutation !== undefined) {
|
|
333
400
|
recompute();
|
|
@@ -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) => {
|
|
@@ -451,6 +545,359 @@ var createSyncCollection = (options) => {
|
|
|
451
545
|
}
|
|
452
546
|
};
|
|
453
547
|
};
|
|
548
|
+
// src/client/presence.ts
|
|
549
|
+
var createPresence = (options) => {
|
|
550
|
+
const reconnectMs = options.reconnectMs ?? 500;
|
|
551
|
+
const maxReconnectMs = options.maxReconnectMs ?? 1e4;
|
|
552
|
+
const Impl = options.webSocketImpl ?? globalThis.WebSocket;
|
|
553
|
+
if (!Impl) {
|
|
554
|
+
throw new Error("createPresence requires WebSocket. Run in a browser or pass webSocketImpl.");
|
|
555
|
+
}
|
|
556
|
+
const id = options.memberId ?? globalThis.crypto?.randomUUID?.() ?? `m${Math.random()}`;
|
|
557
|
+
const members = new Map;
|
|
558
|
+
let state = options.state;
|
|
559
|
+
let snapshot = [];
|
|
560
|
+
const listeners = new Set;
|
|
561
|
+
const emit = () => {
|
|
562
|
+
snapshot = [...members].map(([memberId, memberState]) => ({
|
|
563
|
+
id: memberId,
|
|
564
|
+
state: memberState
|
|
565
|
+
}));
|
|
566
|
+
for (const listener of listeners) {
|
|
567
|
+
listener(snapshot);
|
|
568
|
+
}
|
|
569
|
+
};
|
|
570
|
+
let socket;
|
|
571
|
+
let connected = false;
|
|
572
|
+
let closed = false;
|
|
573
|
+
let attempt = 0;
|
|
574
|
+
let reconnectTimer;
|
|
575
|
+
const send = (frame) => {
|
|
576
|
+
if (connected) {
|
|
577
|
+
socket?.send(JSON.stringify(frame));
|
|
578
|
+
}
|
|
579
|
+
};
|
|
580
|
+
const connect = () => {
|
|
581
|
+
if (closed) {
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
const ws = new Impl(options.url);
|
|
585
|
+
socket = ws;
|
|
586
|
+
ws.onopen = () => {
|
|
587
|
+
attempt = 0;
|
|
588
|
+
connected = true;
|
|
589
|
+
ws.send(JSON.stringify({
|
|
590
|
+
type: "presence-join",
|
|
591
|
+
room: options.room,
|
|
592
|
+
memberId: id,
|
|
593
|
+
state
|
|
594
|
+
}));
|
|
595
|
+
};
|
|
596
|
+
ws.onmessage = (event) => {
|
|
597
|
+
let frame;
|
|
598
|
+
try {
|
|
599
|
+
frame = JSON.parse(event.data);
|
|
600
|
+
} catch {
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
if (frame.type !== "presence" || frame.room !== options.room) {
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
606
|
+
for (const member of frame.joined ?? []) {
|
|
607
|
+
members.set(member.id, member.state);
|
|
608
|
+
}
|
|
609
|
+
for (const member of frame.updated ?? []) {
|
|
610
|
+
members.set(member.id, member.state);
|
|
611
|
+
}
|
|
612
|
+
for (const memberId of frame.left ?? []) {
|
|
613
|
+
members.delete(memberId);
|
|
614
|
+
}
|
|
615
|
+
emit();
|
|
616
|
+
};
|
|
617
|
+
ws.onclose = () => {
|
|
618
|
+
connected = false;
|
|
619
|
+
if (closed || reconnectMs <= 0) {
|
|
620
|
+
return;
|
|
621
|
+
}
|
|
622
|
+
const delay = Math.min(reconnectMs * 2 ** attempt, maxReconnectMs);
|
|
623
|
+
attempt += 1;
|
|
624
|
+
reconnectTimer = setTimeout(connect, delay);
|
|
625
|
+
};
|
|
626
|
+
};
|
|
627
|
+
connect();
|
|
628
|
+
return {
|
|
629
|
+
id,
|
|
630
|
+
get: () => snapshot,
|
|
631
|
+
subscribe: (listener) => {
|
|
632
|
+
listeners.add(listener);
|
|
633
|
+
listener(snapshot);
|
|
634
|
+
return () => {
|
|
635
|
+
listeners.delete(listener);
|
|
636
|
+
};
|
|
637
|
+
},
|
|
638
|
+
set: (next) => {
|
|
639
|
+
state = next;
|
|
640
|
+
send({ type: "presence-set", room: options.room, state: next });
|
|
641
|
+
},
|
|
642
|
+
close: () => {
|
|
643
|
+
closed = true;
|
|
644
|
+
if (reconnectTimer !== undefined) {
|
|
645
|
+
clearTimeout(reconnectTimer);
|
|
646
|
+
}
|
|
647
|
+
send({ type: "presence-leave", room: options.room });
|
|
648
|
+
socket?.close();
|
|
649
|
+
members.clear();
|
|
650
|
+
}
|
|
651
|
+
};
|
|
652
|
+
};
|
|
653
|
+
// src/client/syncClient.ts
|
|
654
|
+
var createSyncClient = (options) => {
|
|
655
|
+
const reconnectMs = options.reconnectMs ?? 500;
|
|
656
|
+
const maxReconnectMs = options.maxReconnectMs ?? 1e4;
|
|
657
|
+
const Impl = options.webSocketImpl ?? globalThis.WebSocket;
|
|
658
|
+
if (!Impl) {
|
|
659
|
+
throw new Error("createSyncClient requires WebSocket. Run in a browser or pass webSocketImpl.");
|
|
660
|
+
}
|
|
661
|
+
const entries = new Map;
|
|
662
|
+
const mutationOwner = new Map;
|
|
663
|
+
let nextEntryId = 0;
|
|
664
|
+
let mutationSeq = 0;
|
|
665
|
+
let socket;
|
|
666
|
+
let connected = false;
|
|
667
|
+
let closed = false;
|
|
668
|
+
let attempt = 0;
|
|
669
|
+
let reconnectTimer;
|
|
670
|
+
const notify = (entry) => {
|
|
671
|
+
for (const listener of entry.listeners) {
|
|
672
|
+
listener(entry.state);
|
|
673
|
+
}
|
|
674
|
+
};
|
|
675
|
+
const rebuild = (entry, patch = {}) => {
|
|
676
|
+
const working = new Map(entry.confirmed);
|
|
677
|
+
const draft = {
|
|
678
|
+
set: (row) => working.set(entry.key(row), row),
|
|
679
|
+
delete: (rowKey) => working.delete(rowKey)
|
|
680
|
+
};
|
|
681
|
+
for (const mutation of entry.pending) {
|
|
682
|
+
mutation.optimistic?.(draft);
|
|
683
|
+
}
|
|
684
|
+
entry.state = {
|
|
685
|
+
...entry.state,
|
|
686
|
+
...patch,
|
|
687
|
+
data: [...working.values()]
|
|
688
|
+
};
|
|
689
|
+
};
|
|
690
|
+
const recompute = (entry, patch = {}) => {
|
|
691
|
+
rebuild(entry, patch);
|
|
692
|
+
notify(entry);
|
|
693
|
+
};
|
|
694
|
+
const applyDiffToConfirmed = (entry, diff) => {
|
|
695
|
+
for (const row of diff.removed) {
|
|
696
|
+
entry.confirmed.delete(entry.key(row));
|
|
697
|
+
}
|
|
698
|
+
for (const row of diff.added) {
|
|
699
|
+
entry.confirmed.set(entry.key(row), row);
|
|
700
|
+
}
|
|
701
|
+
for (const row of diff.changed) {
|
|
702
|
+
entry.confirmed.set(entry.key(row), row);
|
|
703
|
+
}
|
|
704
|
+
};
|
|
705
|
+
const settlePending = (mutationId) => {
|
|
706
|
+
const entry = mutationOwner.get(mutationId);
|
|
707
|
+
mutationOwner.delete(mutationId);
|
|
708
|
+
if (entry === undefined) {
|
|
709
|
+
return;
|
|
710
|
+
}
|
|
711
|
+
const index = entry.pending.findIndex((mutation2) => mutation2.mutationId === mutationId);
|
|
712
|
+
if (index === -1) {
|
|
713
|
+
return;
|
|
714
|
+
}
|
|
715
|
+
const [mutation] = entry.pending.splice(index, 1);
|
|
716
|
+
return { entry, mutation };
|
|
717
|
+
};
|
|
718
|
+
const applyFrame = (frame) => {
|
|
719
|
+
if (frame.type === "snapshot") {
|
|
720
|
+
const entry = entries.get(frame.id);
|
|
721
|
+
if (entry === undefined) {
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
724
|
+
entry.confirmed.clear();
|
|
725
|
+
for (const row of frame.rows) {
|
|
726
|
+
entry.confirmed.set(entry.key(row), row);
|
|
727
|
+
}
|
|
728
|
+
if (frame.version !== undefined) {
|
|
729
|
+
entry.appliedVersion = frame.version;
|
|
730
|
+
}
|
|
731
|
+
recompute(entry, { status: "ready", error: undefined });
|
|
732
|
+
} else if (frame.type === "diff") {
|
|
733
|
+
const entry = entries.get(frame.id);
|
|
734
|
+
if (entry === undefined) {
|
|
735
|
+
return;
|
|
736
|
+
}
|
|
737
|
+
applyDiffToConfirmed(entry, frame);
|
|
738
|
+
if (frame.version !== undefined) {
|
|
739
|
+
entry.appliedVersion = Math.max(entry.appliedVersion, frame.version);
|
|
740
|
+
}
|
|
741
|
+
recompute(entry);
|
|
742
|
+
} else if (frame.type === "frame") {
|
|
743
|
+
const affected = new Set;
|
|
744
|
+
for (const diff of frame.diffs) {
|
|
745
|
+
const entry = entries.get(diff.id);
|
|
746
|
+
if (entry === undefined) {
|
|
747
|
+
continue;
|
|
748
|
+
}
|
|
749
|
+
applyDiffToConfirmed(entry, diff);
|
|
750
|
+
if (frame.version !== undefined) {
|
|
751
|
+
entry.appliedVersion = Math.max(entry.appliedVersion, frame.version);
|
|
752
|
+
}
|
|
753
|
+
rebuild(entry);
|
|
754
|
+
affected.add(entry);
|
|
755
|
+
}
|
|
756
|
+
for (const entry of affected) {
|
|
757
|
+
notify(entry);
|
|
758
|
+
}
|
|
759
|
+
} else if (frame.type === "error") {
|
|
760
|
+
if (frame.id !== undefined) {
|
|
761
|
+
const entry = entries.get(frame.id);
|
|
762
|
+
if (entry !== undefined) {
|
|
763
|
+
recompute(entry, { error: frame.message });
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
options.onError?.(frame.message);
|
|
767
|
+
} else if (frame.type === "ack") {
|
|
768
|
+
const settled = settlePending(frame.mutationId);
|
|
769
|
+
if (settled !== undefined) {
|
|
770
|
+
recompute(settled.entry);
|
|
771
|
+
settled.mutation.resolve(frame.result);
|
|
772
|
+
}
|
|
773
|
+
} else if (frame.type === "reject") {
|
|
774
|
+
const settled = settlePending(frame.mutationId);
|
|
775
|
+
if (settled !== undefined) {
|
|
776
|
+
recompute(settled.entry);
|
|
777
|
+
settled.mutation.reject(new Error(String(frame.message)));
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
};
|
|
781
|
+
const sendSubscribe = (entry) => {
|
|
782
|
+
socket?.send(JSON.stringify({
|
|
783
|
+
type: "subscribe",
|
|
784
|
+
id: entry.id,
|
|
785
|
+
collection: entry.collection,
|
|
786
|
+
params: entry.params,
|
|
787
|
+
since: entry.appliedVersion > 0 ? entry.appliedVersion : undefined
|
|
788
|
+
}));
|
|
789
|
+
};
|
|
790
|
+
const sendMutate = (mutation) => {
|
|
791
|
+
if (connected) {
|
|
792
|
+
socket?.send(JSON.stringify({
|
|
793
|
+
type: "mutate",
|
|
794
|
+
mutationId: mutation.mutationId,
|
|
795
|
+
name: mutation.name,
|
|
796
|
+
args: mutation.args
|
|
797
|
+
}));
|
|
798
|
+
}
|
|
799
|
+
};
|
|
800
|
+
const connect = () => {
|
|
801
|
+
if (closed) {
|
|
802
|
+
return;
|
|
803
|
+
}
|
|
804
|
+
const ws = new Impl(options.url);
|
|
805
|
+
socket = ws;
|
|
806
|
+
ws.onopen = () => {
|
|
807
|
+
attempt = 0;
|
|
808
|
+
connected = true;
|
|
809
|
+
for (const entry of entries.values()) {
|
|
810
|
+
sendSubscribe(entry);
|
|
811
|
+
}
|
|
812
|
+
for (const entry of entries.values()) {
|
|
813
|
+
for (const mutation of entry.pending) {
|
|
814
|
+
sendMutate(mutation);
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
};
|
|
818
|
+
ws.onmessage = (event) => {
|
|
819
|
+
try {
|
|
820
|
+
applyFrame(JSON.parse(event.data));
|
|
821
|
+
} catch {}
|
|
822
|
+
};
|
|
823
|
+
ws.onclose = () => {
|
|
824
|
+
connected = false;
|
|
825
|
+
if (closed || reconnectMs <= 0) {
|
|
826
|
+
return;
|
|
827
|
+
}
|
|
828
|
+
const delay = Math.min(reconnectMs * 2 ** attempt, maxReconnectMs);
|
|
829
|
+
attempt += 1;
|
|
830
|
+
reconnectTimer = setTimeout(connect, delay);
|
|
831
|
+
};
|
|
832
|
+
};
|
|
833
|
+
connect();
|
|
834
|
+
const collection = (handleOptions) => {
|
|
835
|
+
const entryId = `c${nextEntryId}`;
|
|
836
|
+
nextEntryId += 1;
|
|
837
|
+
const entry = {
|
|
838
|
+
id: entryId,
|
|
839
|
+
collection: handleOptions.collection,
|
|
840
|
+
params: handleOptions.params,
|
|
841
|
+
key: handleOptions.key ?? ((row) => row.id),
|
|
842
|
+
confirmed: new Map,
|
|
843
|
+
pending: [],
|
|
844
|
+
state: { data: [], status: "connecting", error: undefined },
|
|
845
|
+
listeners: new Set,
|
|
846
|
+
appliedVersion: 0,
|
|
847
|
+
closed: false
|
|
848
|
+
};
|
|
849
|
+
entries.set(entryId, entry);
|
|
850
|
+
if (connected) {
|
|
851
|
+
sendSubscribe(entry);
|
|
852
|
+
}
|
|
853
|
+
return {
|
|
854
|
+
get: () => entry.state,
|
|
855
|
+
subscribe: (listener) => {
|
|
856
|
+
const typed = listener;
|
|
857
|
+
entry.listeners.add(typed);
|
|
858
|
+
listener(entry.state);
|
|
859
|
+
return () => {
|
|
860
|
+
entry.listeners.delete(typed);
|
|
861
|
+
};
|
|
862
|
+
},
|
|
863
|
+
mutate: (mutateOptions) => new Promise((resolve, reject) => {
|
|
864
|
+
mutationSeq += 1;
|
|
865
|
+
const mutation = {
|
|
866
|
+
mutationId: mutationSeq,
|
|
867
|
+
name: mutateOptions.name,
|
|
868
|
+
args: mutateOptions.args,
|
|
869
|
+
optimistic: mutateOptions.optimistic,
|
|
870
|
+
resolve: (result) => resolve(result),
|
|
871
|
+
reject
|
|
872
|
+
};
|
|
873
|
+
entry.pending.push(mutation);
|
|
874
|
+
mutationOwner.set(mutation.mutationId, entry);
|
|
875
|
+
recompute(entry);
|
|
876
|
+
sendMutate(mutation);
|
|
877
|
+
}),
|
|
878
|
+
close: () => {
|
|
879
|
+
if (entry.closed) {
|
|
880
|
+
return;
|
|
881
|
+
}
|
|
882
|
+
entry.closed = true;
|
|
883
|
+
entries.delete(entryId);
|
|
884
|
+
if (connected) {
|
|
885
|
+
socket?.send(JSON.stringify({ type: "unsubscribe", id: entryId }));
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
};
|
|
889
|
+
};
|
|
890
|
+
const close = () => {
|
|
891
|
+
closed = true;
|
|
892
|
+
if (reconnectTimer !== undefined) {
|
|
893
|
+
clearTimeout(reconnectTimer);
|
|
894
|
+
}
|
|
895
|
+
socket?.close();
|
|
896
|
+
entries.clear();
|
|
897
|
+
mutationOwner.clear();
|
|
898
|
+
};
|
|
899
|
+
return { collection, close };
|
|
900
|
+
};
|
|
454
901
|
// src/client/syncStore.ts
|
|
455
902
|
var SUBSCRIPTION_ID2 = "s";
|
|
456
903
|
var syncStore = (options) => {
|
|
@@ -772,11 +1219,15 @@ export {
|
|
|
772
1219
|
unwrapEden,
|
|
773
1220
|
syncStore,
|
|
774
1221
|
localStorageMutationStorage,
|
|
1222
|
+
localStorageCollectionCache,
|
|
775
1223
|
jsonFetcher,
|
|
1224
|
+
indexedDbCollectionCache,
|
|
776
1225
|
createSyncSubscriber,
|
|
777
1226
|
createSyncCollection,
|
|
1227
|
+
createSyncClient,
|
|
1228
|
+
createPresence,
|
|
778
1229
|
createLiveQuery
|
|
779
1230
|
};
|
|
780
1231
|
|
|
781
|
-
//# debugId=
|
|
1232
|
+
//# debugId=FB434F3CF30BD04964756E2164756E21
|
|
782
1233
|
//# sourceMappingURL=index.js.map
|