@absolutejs/sync 0.1.0 → 0.2.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 +51 -34
- 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 +2 -2
- package/dist/angular/index.js.map +3 -3
- package/dist/client/index.d.ts +4 -0
- package/dist/client/index.js +357 -2
- 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/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 +2 -2
- package/dist/react/index.js.map +3 -3
- package/dist/svelte/index.js +2 -2
- package/dist/svelte/index.js.map +3 -3
- package/dist/vue/index.js +2 -2
- package/dist/vue/index.js.map +3 -3
- package/package.json +3 -1
package/dist/react/index.js
CHANGED
|
@@ -171,7 +171,7 @@ var createSyncCollection = (options) => {
|
|
|
171
171
|
recompute();
|
|
172
172
|
mutation.resolve(frame.result);
|
|
173
173
|
}
|
|
174
|
-
} else {
|
|
174
|
+
} else if (frame.type === "reject") {
|
|
175
175
|
const mutation = settlePending(frame.mutationId);
|
|
176
176
|
if (mutation !== undefined) {
|
|
177
177
|
recompute();
|
|
@@ -328,5 +328,5 @@ export {
|
|
|
328
328
|
useSyncCollection
|
|
329
329
|
};
|
|
330
330
|
|
|
331
|
-
//# debugId=
|
|
331
|
+
//# debugId=24BDB56D7EB329F664756E2164756E21
|
|
332
332
|
//# sourceMappingURL=index.js.map
|
package/dist/react/index.js.map
CHANGED
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
"sources": ["../src/react/useSyncCollection.ts", "../src/client/syncCollection.ts"],
|
|
4
4
|
"sourcesContent": [
|
|
5
5
|
"import { useCallback, useEffect, useRef, useState } from 'react';\nimport { createSyncCollection } from '../client/syncCollection';\nimport type {\n\tMutateOptions,\n\tSyncCollection,\n\tSyncCollectionOptions,\n\tSyncCollectionState\n} from '../client/syncCollection';\n\n/**\n * React binding for a live sync-engine collection (the Tier 3 store). Subscribes\n * to `{ added, removed, changed }` diffs over the WebSocket and re-renders on\n * change; returns the current `data`/`status`/`error` plus an optimistic\n * `mutate`.\n *\n * SSR-safe: the socket opens in an effect (client only), and re-opens if `url`,\n * `collection`, or `params` change. The collection closes on unmount.\n */\nexport const useSyncCollection = <T>(options: SyncCollectionOptions<T>) => {\n\tconst [state, setState] = useState<SyncCollectionState<T>>({\n\t\tdata: [],\n\t\terror: undefined,\n\t\tstatus: 'connecting'\n\t});\n\tconst collectionRef = useRef<SyncCollection<T> | null>(null);\n\tconst paramsKey = JSON.stringify(options.params ?? null);\n\n\tuseEffect(() => {\n\t\tconst collection = createSyncCollection<T>(options);\n\t\tcollectionRef.current = collection;\n\t\tsetState(collection.get());\n\t\tconst unsubscribe = collection.subscribe(setState);\n\n\t\treturn () => {\n\t\t\tunsubscribe();\n\t\t\tcollection.close();\n\t\t\tcollectionRef.current = null;\n\t\t};\n\t\t// Re-open only when the subscription identity changes.\n\t\t// eslint-disable-next-line react-hooks/exhaustive-deps\n\t}, [options.url, options.collection, paramsKey]);\n\n\tconst mutate = useCallback(\n\t\t<R = unknown>(mutateOptions: MutateOptions<T>): Promise<R> =>\n\t\t\tcollectionRef.current\n\t\t\t\t? collectionRef.current.mutate<R>(mutateOptions)\n\t\t\t\t: Promise.reject(new Error('sync collection is not ready')),\n\t\t[]\n\t);\n\n\treturn {\n\t\tdata: state.data,\n\t\terror: state.error,\n\t\tmutate,\n\t\tstatus: state.status\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\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"
|
|
7
7
|
],
|
|
8
|
-
"mappings": "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
|
|
9
|
-
"debugId": "
|
|
8
|
+
"mappings": "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",
|
|
9
|
+
"debugId": "24BDB56D7EB329F664756E2164756E21",
|
|
10
10
|
"names": []
|
|
11
11
|
}
|
package/dist/svelte/index.js
CHANGED
|
@@ -168,7 +168,7 @@ var createSyncCollection = (options) => {
|
|
|
168
168
|
recompute();
|
|
169
169
|
mutation.resolve(frame.result);
|
|
170
170
|
}
|
|
171
|
-
} else {
|
|
171
|
+
} else if (frame.type === "reject") {
|
|
172
172
|
const mutation = settlePending(frame.mutationId);
|
|
173
173
|
if (mutation !== undefined) {
|
|
174
174
|
recompute();
|
|
@@ -334,5 +334,5 @@ export {
|
|
|
334
334
|
createSyncCollectionStore
|
|
335
335
|
};
|
|
336
336
|
|
|
337
|
-
//# debugId=
|
|
337
|
+
//# debugId=C708D38F08FD3F2564756E2164756E21
|
|
338
338
|
//# sourceMappingURL=index.js.map
|
package/dist/svelte/index.js.map
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/client/syncCollection.ts", "../src/svelte/createSyncCollectionStore.ts"],
|
|
4
4
|
"sourcesContent": [
|
|
5
|
-
"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",
|
|
5
|
+
"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
6
|
"import { createSyncCollection } from '../client/syncCollection';\nimport type {\n\tMutateOptions,\n\tSyncCollection,\n\tSyncCollectionOptions,\n\tSyncCollectionState\n} from '../client/syncCollection';\n\n/**\n * Svelte binding for a live sync-engine collection (the Tier 3 store). A proper\n * readable store — `$store` gives the current `{ data, status, error }`,\n * maintained from the WebSocket diff stream — with `mutate` (optimistic) and\n * `destroy` attached.\n *\n * SSR-safe: the socket opens lazily on the first browser subscription, so it is\n * inert during server rendering. Call `destroy()` (e.g. in `onDestroy`) to close.\n */\nexport const createSyncCollectionStore = <T>(\n\toptions: SyncCollectionOptions<T>\n) => {\n\tlet collection: SyncCollection<T> | null = null;\n\tlet current: SyncCollectionState<T> = {\n\t\tdata: [],\n\t\terror: undefined,\n\t\tstatus: 'connecting'\n\t};\n\tconst subscribers = new Set<(state: SyncCollectionState<T>) => void>();\n\n\tconst ensureConnected = () => {\n\t\tif (collection !== null || typeof window === 'undefined') {\n\t\t\treturn;\n\t\t}\n\t\tcollection = createSyncCollection<T>(options);\n\t\tcurrent = collection.get();\n\t\tcollection.subscribe((state) => {\n\t\t\tcurrent = state;\n\t\t\tsubscribers.forEach((run) => run(current));\n\t\t});\n\t};\n\n\treturn {\n\t\tsubscribe(run: (state: SyncCollectionState<T>) => void) {\n\t\t\tsubscribers.add(run);\n\t\t\tensureConnected();\n\t\t\trun(current);\n\n\t\t\treturn () => {\n\t\t\t\tsubscribers.delete(run);\n\t\t\t};\n\t\t},\n\t\tmutate: <R = unknown>(mutateOptions: MutateOptions<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\t\tdestroy() {\n\t\t\tcollection?.close();\n\t\t\tcollection = null;\n\t\t\tsubscribers.clear();\n\t\t}\n\t};\n};\n"
|
|
7
7
|
],
|
|
8
|
-
"mappings": "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
|
|
9
|
-
"debugId": "
|
|
8
|
+
"mappings": "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",
|
|
9
|
+
"debugId": "C708D38F08FD3F2564756E2164756E21",
|
|
10
10
|
"names": []
|
|
11
11
|
}
|
package/dist/vue/index.js
CHANGED
|
@@ -171,7 +171,7 @@ var createSyncCollection = (options) => {
|
|
|
171
171
|
recompute();
|
|
172
172
|
mutation.resolve(frame.result);
|
|
173
173
|
}
|
|
174
|
-
} else {
|
|
174
|
+
} else if (frame.type === "reject") {
|
|
175
175
|
const mutation = settlePending(frame.mutationId);
|
|
176
176
|
if (mutation !== undefined) {
|
|
177
177
|
recompute();
|
|
@@ -327,5 +327,5 @@ export {
|
|
|
327
327
|
useSyncCollection
|
|
328
328
|
};
|
|
329
329
|
|
|
330
|
-
//# debugId=
|
|
330
|
+
//# debugId=F6F06722E6C55B4F64756E2164756E21
|
|
331
331
|
//# sourceMappingURL=index.js.map
|
package/dist/vue/index.js.map
CHANGED
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
"sources": ["../src/vue/useSyncCollection.ts", "../src/client/syncCollection.ts"],
|
|
4
4
|
"sourcesContent": [
|
|
5
5
|
"import { onMounted, onUnmounted, ref, shallowRef } from 'vue';\nimport type { Ref } from 'vue';\nimport { createSyncCollection } from '../client/syncCollection';\nimport type {\n\tMutateOptions,\n\tSyncCollection,\n\tSyncCollectionOptions,\n\tSyncCollectionStatus\n} from '../client/syncCollection';\n\n/**\n * Vue composable for a live sync-engine collection (the Tier 3 store). Returns\n * reactive `data`/`status`/`error` refs maintained from the WebSocket diff\n * stream, plus an optimistic `mutate`.\n *\n * SSR-safe: the socket opens in `onMounted` (client only) and closes in\n * `onUnmounted` (or via the returned `destroy`).\n */\nexport const useSyncCollection = <T>(options: SyncCollectionOptions<T>) => {\n\tconst data = shallowRef<T[]>([]) as Ref<T[]>;\n\tconst status = ref<SyncCollectionStatus>('connecting');\n\tconst error = ref<unknown>(undefined);\n\n\tlet collection: SyncCollection<T> | null = null;\n\tlet unsubscribe: (() => void) | null = null;\n\n\tonMounted(() => {\n\t\tcollection = createSyncCollection<T>(options);\n\t\tconst apply = (state: {\n\t\t\tdata: T[];\n\t\t\tstatus: SyncCollectionStatus;\n\t\t\terror: unknown;\n\t\t}) => {\n\t\t\tdata.value = state.data;\n\t\t\tstatus.value = state.status;\n\t\t\terror.value = state.error;\n\t\t};\n\t\tapply(collection.get());\n\t\tunsubscribe = collection.subscribe(apply);\n\t});\n\n\tconst destroy = () => {\n\t\tunsubscribe?.();\n\t\tcollection?.close();\n\t\tunsubscribe = null;\n\t\tcollection = null;\n\t};\n\n\tonUnmounted(destroy);\n\n\tconst mutate = <R = unknown>(\n\t\tmutateOptions: MutateOptions<T>\n\t): Promise<R> =>\n\t\tcollection\n\t\t\t? collection.mutate<R>(mutateOptions)\n\t\t\t: Promise.reject(new Error('sync collection is not ready'));\n\n\treturn { data, destroy, error, mutate, status };\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\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"
|
|
7
7
|
],
|
|
8
|
-
"mappings": "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
|
|
9
|
-
"debugId": "
|
|
8
|
+
"mappings": "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",
|
|
9
|
+
"debugId": "F6F06722E6C55B4F64756E2164756E21",
|
|
10
10
|
"names": []
|
|
11
11
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@absolutejs/sync",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Lightweight reactive-push and write-behind-cache primitives for Elysia and the AbsoluteJS ecosystem — kill polling and keep a remote store off your hot path, without adopting a whole sync-engine backend.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -83,6 +83,7 @@
|
|
|
83
83
|
},
|
|
84
84
|
"scripts": {
|
|
85
85
|
"build": "bun run scripts/build.ts",
|
|
86
|
+
"config": "absolute config",
|
|
86
87
|
"test": "bun test",
|
|
87
88
|
"typecheck": "tsc --noEmit",
|
|
88
89
|
"format": "prettier --write \"./**/*.{ts,json,md}\"",
|
|
@@ -137,6 +138,7 @@
|
|
|
137
138
|
}
|
|
138
139
|
},
|
|
139
140
|
"devDependencies": {
|
|
141
|
+
"@absolutejs/absolute": "^0.19.0-beta.1051",
|
|
140
142
|
"@angular/core": "^21.0.0",
|
|
141
143
|
"@elysiajs/eden": "^1.4.9",
|
|
142
144
|
"@types/bun": "latest",
|