@absolutejs/sync 0.0.1 → 0.1.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.
Files changed (68) hide show
  1. package/README.md +264 -24
  2. package/dist/adapters/drizzle/index.d.ts +17 -0
  3. package/dist/adapters/drizzle/index.js +128 -0
  4. package/dist/adapters/drizzle/index.js.map +12 -0
  5. package/dist/adapters/drizzle/read.d.ts +31 -0
  6. package/dist/adapters/drizzle/topics.d.ts +41 -0
  7. package/dist/adapters/drizzle/write.d.ts +69 -0
  8. package/dist/adapters/mysql/index.d.ts +75 -0
  9. package/dist/adapters/mysql/index.js +171 -0
  10. package/dist/adapters/mysql/index.js.map +11 -0
  11. package/dist/adapters/postgres/index.d.ts +53 -0
  12. package/dist/adapters/postgres/index.js +86 -0
  13. package/dist/adapters/postgres/index.js.map +10 -0
  14. package/dist/adapters/prisma/collection.d.ts +39 -0
  15. package/dist/adapters/prisma/index.d.ts +23 -0
  16. package/dist/adapters/prisma/index.js +231 -0
  17. package/dist/adapters/prisma/index.js.map +14 -0
  18. package/dist/adapters/prisma/predicate.d.ts +20 -0
  19. package/dist/adapters/prisma/read.d.ts +28 -0
  20. package/dist/adapters/prisma/topics.d.ts +29 -0
  21. package/dist/adapters/prisma/write.d.ts +65 -0
  22. package/dist/adapters/sqlite/index.d.ts +32 -0
  23. package/dist/adapters/sqlite/index.js +128 -0
  24. package/dist/adapters/sqlite/index.js.map +11 -0
  25. package/dist/angular/index.d.ts +1 -0
  26. package/dist/angular/index.js +347 -0
  27. package/dist/angular/index.js.map +11 -0
  28. package/dist/angular/sync-collection.service.d.ts +20 -0
  29. package/dist/client/index.d.ts +8 -30
  30. package/dist/client/index.js +744 -3
  31. package/dist/client/index.js.map +8 -4
  32. package/dist/client/liveQuery.d.ts +75 -0
  33. package/dist/client/subscriber.d.ts +30 -0
  34. package/dist/client/syncCollection.d.ts +102 -0
  35. package/dist/client/syncStore.d.ts +81 -0
  36. package/dist/engine/aggregate.d.ts +45 -0
  37. package/dist/engine/collection.d.ts +87 -0
  38. package/dist/engine/connection.d.ts +71 -0
  39. package/dist/engine/dataflow.d.ts +109 -0
  40. package/dist/engine/equiJoin.d.ts +51 -0
  41. package/dist/engine/graph.d.ts +85 -0
  42. package/dist/engine/index.d.ts +34 -0
  43. package/dist/engine/index.js +1269 -0
  44. package/dist/engine/index.js.map +20 -0
  45. package/dist/engine/materializedView.d.ts +53 -0
  46. package/dist/engine/mutation.d.ts +30 -0
  47. package/dist/engine/pollingSource.d.ts +42 -0
  48. package/dist/engine/routes.d.ts +40 -0
  49. package/dist/engine/socket.d.ts +64 -0
  50. package/dist/engine/syncEngine.d.ts +100 -0
  51. package/dist/engine/types.d.ts +45 -0
  52. package/dist/index.d.ts +2 -0
  53. package/dist/index.js +160 -2
  54. package/dist/index.js.map +7 -5
  55. package/dist/react/index.d.ts +1 -0
  56. package/dist/react/index.js +332 -0
  57. package/dist/react/index.js.map +11 -0
  58. package/dist/react/useSyncCollection.d.ts +16 -0
  59. package/dist/reactiveHub.d.ts +6 -0
  60. package/dist/svelte/createSyncCollectionStore.d.ts +15 -0
  61. package/dist/svelte/index.d.ts +1 -0
  62. package/dist/svelte/index.js +338 -0
  63. package/dist/svelte/index.js.map +11 -0
  64. package/dist/vue/index.d.ts +1 -0
  65. package/dist/vue/index.js +331 -0
  66. package/dist/vue/index.js.map +11 -0
  67. package/dist/vue/useSyncCollection.d.ts +17 -0
  68. package/package.json +102 -6
@@ -1,10 +1,14 @@
1
1
  {
2
2
  "version": 3,
3
- "sources": ["../../src/client/index.ts"],
3
+ "sources": ["../src/client/subscriber.ts", "../src/reactiveHub.ts", "../src/client/liveQuery.ts", "../src/client/syncCollection.ts", "../src/client/syncStore.ts"],
4
4
  "sourcesContent": [
5
- "import type { ReactiveEvent } from '../reactiveHub';\n\nexport type { ReactiveEvent } from '../reactiveHub';\n\nexport type SyncSubscriberOptions = {\n\t/** Topics to subscribe to. A trailing `*` matches by prefix server-side. */\n\ttopics: string[];\n\t/** Called for every reactive event pushed from the server. */\n\tonEvent: (event: ReactiveEvent) => void;\n\t/** SSE endpoint mounted by the {@link sync} plugin. Defaults to `/sync`. */\n\turl?: string;\n\tonOpen?: () => void;\n\tonError?: (event: Event) => void;\n\t/** Send cookies with the SSE request (cross-origin auth). */\n\twithCredentials?: boolean;\n\t/**\n\t * EventSource implementation to use. Defaults to the global one; pass a polyfill\n\t * for non-browser runtimes.\n\t */\n\teventSourceImpl?: typeof EventSource;\n};\n\nexport type SyncSubscriber = {\n\tclose: () => void;\n\t/** The underlying EventSource, for advanced listeners. */\n\tsource: EventSource;\n};\n\n/**\n * Subscribe a browser to the server's {@link ReactiveHub} over SSE. `onEvent` fires\n * whenever a subscribed topic is published — the cue to refetch (or read the pushed\n * payload) instead of polling. EventSource reconnects automatically on transient\n * network drops.\n */\nexport const createSyncSubscriber = ({\n\ttopics,\n\tonEvent,\n\turl = '/sync',\n\tonOpen,\n\tonError,\n\twithCredentials,\n\teventSourceImpl\n}: SyncSubscriberOptions): SyncSubscriber => {\n\tconst Impl = eventSourceImpl ?? globalThis.EventSource;\n\tif (!Impl) {\n\t\tthrow new Error(\n\t\t\t'createSyncSubscriber requires EventSource. Run in a browser or pass eventSourceImpl.'\n\t\t);\n\t}\n\n\tconst params = new URLSearchParams({ topics: topics.join(',') });\n\tconst separator = url.includes('?') ? '&' : '?';\n\tconst source = new Impl(`${url}${separator}${params.toString()}`, {\n\t\twithCredentials: withCredentials ?? false\n\t});\n\n\tsource.onmessage = (event) => {\n\t\ttry {\n\t\t\tonEvent(JSON.parse(event.data) as ReactiveEvent);\n\t\t} catch {\n\t\t\t// ignore heartbeats / non-JSON frames\n\t\t}\n\t};\n\tif (onOpen) {\n\t\tsource.onopen = () => onOpen();\n\t}\n\tif (onError) {\n\t\tsource.onerror = (event) => onError(event);\n\t}\n\n\treturn {\n\t\tclose: () => source.close(),\n\t\tsource\n\t};\n};\n"
5
+ "import type { ReactiveEvent } from '../reactiveHub';\n\nexport type SyncSubscriberOptions = {\n\t/** Topics to subscribe to. A trailing `*` matches by prefix server-side. */\n\ttopics: string[];\n\t/** Called for every reactive event pushed from the server. */\n\tonEvent: (event: ReactiveEvent) => void;\n\t/** SSE endpoint mounted by the {@link sync} plugin. Defaults to `/sync`. */\n\turl?: string;\n\tonOpen?: () => void;\n\tonError?: (event: Event) => void;\n\t/** Send cookies with the SSE request (cross-origin auth). */\n\twithCredentials?: boolean;\n\t/**\n\t * EventSource implementation to use. Defaults to the global one; pass a polyfill\n\t * for non-browser runtimes.\n\t */\n\teventSourceImpl?: typeof EventSource;\n};\n\nexport type SyncSubscriber = {\n\tclose: () => void;\n\t/** The underlying EventSource, for advanced listeners. */\n\tsource: EventSource;\n};\n\n/**\n * Subscribe a browser to the server's {@link ReactiveHub} over SSE. `onEvent` fires\n * whenever a subscribed topic is published — the cue to refetch (or read the pushed\n * payload) instead of polling. EventSource reconnects automatically on transient\n * network drops.\n */\nexport const createSyncSubscriber = ({\n\ttopics,\n\tonEvent,\n\turl = '/sync',\n\tonOpen,\n\tonError,\n\twithCredentials,\n\teventSourceImpl\n}: SyncSubscriberOptions): SyncSubscriber => {\n\tconst Impl = eventSourceImpl ?? globalThis.EventSource;\n\tif (!Impl) {\n\t\tthrow new Error(\n\t\t\t'createSyncSubscriber requires EventSource. Run in a browser or pass eventSourceImpl.'\n\t\t);\n\t}\n\n\tconst params = new URLSearchParams({ topics: topics.join(',') });\n\tconst separator = url.includes('?') ? '&' : '?';\n\tconst source = new Impl(`${url}${separator}${params.toString()}`, {\n\t\twithCredentials: withCredentials ?? false\n\t});\n\n\tsource.onmessage = (event) => {\n\t\ttry {\n\t\t\tonEvent(JSON.parse(event.data) as ReactiveEvent);\n\t\t} catch {\n\t\t\t// ignore heartbeats / non-JSON frames\n\t\t}\n\t};\n\tif (onOpen) {\n\t\tsource.onopen = () => onOpen();\n\t}\n\tif (onError) {\n\t\tsource.onerror = (event) => onError(event);\n\t}\n\n\treturn {\n\t\tclose: () => source.close(),\n\t\tsource\n\t};\n};\n",
6
+ "/**\n * Topic of the synthetic frame the SSE plugin emits when a stream opens (and\n * re-opens after a reconnect). Clients use it to tell \"the stream connected\"\n * apart from a real data-change event.\n */\nexport const SYNC_OPEN_TOPIC = '@absolutejs/sync:open';\n\nexport type ReactiveEvent<TPayload = unknown> = {\n\ttopic: string;\n\tat: number;\n\tpayload?: TPayload;\n};\n\nexport type ReactiveListener<TPayload = unknown> = (\n\tevent: ReactiveEvent<TPayload>\n) => void;\n\nexport type ReactiveHub = {\n\t/**\n\t * Notify every subscriber of `topic` (and any prefix-wildcard subscriber that\n\t * matches it). Call this from a mutation after the durable write commits.\n\t */\n\tpublish: (topic: string, payload?: unknown) => void;\n\t/**\n\t * Listen on one or more topics. A topic ending in `*` matches every topic that\n\t * starts with the prefix before it (e.g. `voice:session:*`). Returns an\n\t * unsubscribe function.\n\t */\n\tsubscribe: (topics: string[], listener: ReactiveListener) => () => void;\n\t/** Number of active subscribers, optionally for a single exact topic. */\n\tsubscriberCount: (topic?: string) => number;\n};\n\ntype Subscription = {\n\texact: Set<string>;\n\tprefixes: string[];\n\tlistener: ReactiveListener;\n};\n\n/**\n * An in-memory topic pub/sub for reactive, push-on-change updates.\n *\n * The pattern that replaces polling: a query/widget subscribes to the topics its\n * data depends on; a mutation `publish`es those topics after it writes; subscribers\n * are notified immediately and refetch (or receive the pushed payload) — instead of\n * every client hammering the server on a timer.\n *\n * Dependencies are explicit (you name the topics) rather than auto-tracked from a\n * query's read set — deliberately small, with no sandbox or query interception.\n * Pair it with the {@link sync} Elysia plugin to stream events to browsers over SSE.\n */\nexport const createReactiveHub = (): ReactiveHub => {\n\tconst subscriptions = new Set<Subscription>();\n\n\tconst matches = (subscription: Subscription, topic: string) => {\n\t\tif (subscription.exact.has(topic)) {\n\t\t\treturn true;\n\t\t}\n\t\treturn subscription.prefixes.some((prefix) => topic.startsWith(prefix));\n\t};\n\n\treturn {\n\t\tpublish: (topic, payload) => {\n\t\t\tconst event: ReactiveEvent = { topic, at: Date.now(), payload };\n\t\t\tfor (const subscription of subscriptions) {\n\t\t\t\tif (matches(subscription, topic)) {\n\t\t\t\t\tsubscription.listener(event);\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\t\tsubscribe: (topics, listener) => {\n\t\t\tconst exact = new Set<string>();\n\t\t\tconst prefixes: string[] = [];\n\t\t\tfor (const topic of topics) {\n\t\t\t\tif (topic.endsWith('*')) {\n\t\t\t\t\tprefixes.push(topic.slice(0, -1));\n\t\t\t\t} else {\n\t\t\t\t\texact.add(topic);\n\t\t\t\t}\n\t\t\t}\n\t\t\tconst subscription: Subscription = { exact, prefixes, listener };\n\t\t\tsubscriptions.add(subscription);\n\t\t\treturn () => {\n\t\t\t\tsubscriptions.delete(subscription);\n\t\t\t};\n\t\t},\n\t\tsubscriberCount: (topic) => {\n\t\t\tif (topic === undefined) {\n\t\t\t\treturn subscriptions.size;\n\t\t\t}\n\t\t\tlet count = 0;\n\t\t\tfor (const subscription of subscriptions) {\n\t\t\t\tif (matches(subscription, topic)) {\n\t\t\t\t\tcount += 1;\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn count;\n\t\t}\n\t};\n};\n",
7
+ "import { SYNC_OPEN_TOPIC } from '../reactiveHub';\nimport type { ReactiveEvent } from '../reactiveHub';\nimport { createSyncSubscriber } from './subscriber';\n\nexport type LiveQueryState<T> = {\n\t/** Latest query result, or `undefined` before the first successful fetch. */\n\tdata: T | undefined;\n\t/** Error from the most recent fetch, or `undefined` if it succeeded. */\n\terror: unknown;\n\t/** `true` until the first result arrives (no data yet). */\n\tloading: boolean;\n\t/** `true` while a (re)fetch is in flight — data may still be present. */\n\tfetching: boolean;\n};\n\nexport type LiveQueryOptions<T> = {\n\t/**\n\t * Topics this query depends on — typically the server's\n\t * `deriveReadTopics(...).topics`. Any event on one of them triggers a\n\t * refetch. A trailing `*` matches by prefix server-side.\n\t */\n\ttopics: string[];\n\t/**\n\t * Runs the read and resolves the query result. Receives an `AbortSignal`\n\t * that fires when a newer fetch supersedes this one or the query is closed.\n\t */\n\tfetcher: (signal: AbortSignal) => Promise<T>;\n\t/** SSE endpoint mounted by the {@link sync} plugin. Defaults to `/sync`. */\n\turl?: string;\n\t/** Send cookies with the SSE request (cross-origin auth). */\n\twithCredentials?: boolean;\n\t/** EventSource implementation; defaults to the global one. */\n\teventSourceImpl?: typeof EventSource;\n\t/**\n\t * Seed data (e.g. from SSR). When provided, the initial fetch is skipped —\n\t * the query trusts this until an event or a manual {@link LiveQuery.refetch}.\n\t */\n\tinitialData?: T;\n\t/**\n\t * Skip the initial fetch and stay idle until the first event or a manual\n\t * refetch. Reconnects still re-hydrate.\n\t */\n\tmanual?: boolean;\n\t/**\n\t * Coalesce a burst of events into one refetch within this window (ms).\n\t * Defaults to 0 — refetch once per event.\n\t */\n\tdebounceMs?: number;\n\t/** Called when a fetch rejects (stale data is retained). */\n\tonError?: (error: unknown) => void;\n};\n\nexport type LiveQuery<T> = {\n\t/** Current state snapshot (stable reference until the next change). */\n\tget: () => LiveQueryState<T>;\n\t/** Subscribe to state changes; returns an unsubscribe. */\n\tsubscribe: (listener: (state: LiveQueryState<T>) => void) => () => void;\n\t/** Force a refetch now. Resolves when this fetch settles. */\n\trefetch: () => Promise<void>;\n\t/** Stop the SSE subscription, cancel any in-flight fetch, drop listeners. */\n\tclose: () => void;\n};\n\n/**\n * A live, self-refreshing query: hydrate once via `fetcher`, then refetch\n * whenever the server publishes one of `topics` — the read half of Tier 2,\n * built on {@link createSyncSubscriber}. Framework-agnostic: `get` + `subscribe`\n * plug straight into React's `useSyncExternalStore` or any equivalent.\n *\n * Pair it with the Drizzle adapter's `deriveReadTopics` (server) and\n * `publishChange`/`publishWhere` (mutations) so a write invalidates exactly the\n * queries that read the changed rows.\n */\nexport const createLiveQuery = <T>(\n\toptions: LiveQueryOptions<T>\n): LiveQuery<T> => {\n\tconst hasSeed = options.initialData !== undefined;\n\tlet state: LiveQueryState<T> = {\n\t\tdata: options.initialData,\n\t\terror: undefined,\n\t\tloading: !options.manual && !hasSeed,\n\t\tfetching: false\n\t};\n\n\tconst listeners = new Set<(state: LiveQueryState<T>) => void>();\n\tconst setState = (patch: Partial<LiveQueryState<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\tlet requestSeq = 0;\n\tlet inFlight: AbortController | undefined;\n\tlet closed = false;\n\n\tconst refetch = async () => {\n\t\tif (closed) {\n\t\t\treturn;\n\t\t}\n\t\tconst seq = (requestSeq += 1);\n\t\tinFlight?.abort();\n\t\tconst controller = new AbortController();\n\t\tinFlight = controller;\n\t\tsetState({ fetching: true });\n\n\t\ttry {\n\t\t\tconst data = await options.fetcher(controller.signal);\n\t\t\tif (seq !== requestSeq) {\n\t\t\t\treturn; // superseded by a newer fetch\n\t\t\t}\n\t\t\tsetState({\n\t\t\t\tdata,\n\t\t\t\terror: undefined,\n\t\t\t\tloading: false,\n\t\t\t\tfetching: false\n\t\t\t});\n\t\t} catch (error) {\n\t\t\tif (controller.signal.aborted || seq !== requestSeq) {\n\t\t\t\treturn; // aborted or superseded — leave state to the winner\n\t\t\t}\n\t\t\tsetState({ error, loading: false, fetching: false });\n\t\t\toptions.onError?.(error);\n\t\t} finally {\n\t\t\tif (inFlight === controller) {\n\t\t\t\tinFlight = undefined;\n\t\t\t}\n\t\t}\n\t};\n\n\tlet debounceTimer: ReturnType<typeof setTimeout> | undefined;\n\tconst scheduleRefetch = () => {\n\t\tif (closed) {\n\t\t\treturn;\n\t\t}\n\t\tif (!options.debounceMs) {\n\t\t\tvoid refetch();\n\t\t\treturn;\n\t\t}\n\t\tif (debounceTimer !== undefined) {\n\t\t\treturn; // a refetch is already queued for this window\n\t\t}\n\t\tdebounceTimer = setTimeout(() => {\n\t\t\tdebounceTimer = undefined;\n\t\t\tvoid refetch();\n\t\t}, options.debounceMs);\n\t};\n\n\tlet opened = false;\n\tconst onEvent = (event: ReactiveEvent) => {\n\t\tif (event.topic === SYNC_OPEN_TOPIC) {\n\t\t\t// First open is the initial connect (already hydrated); a later open\n\t\t\t// is a reconnect, so re-hydrate to catch events missed while down.\n\t\t\tif (opened) {\n\t\t\t\tscheduleRefetch();\n\t\t\t}\n\t\t\topened = true;\n\t\t\treturn;\n\t\t}\n\t\tscheduleRefetch();\n\t};\n\n\tconst subscriber = createSyncSubscriber({\n\t\ttopics: options.topics,\n\t\tonEvent,\n\t\turl: options.url,\n\t\twithCredentials: options.withCredentials,\n\t\teventSourceImpl: options.eventSourceImpl\n\t});\n\n\tif (!options.manual && !hasSeed) {\n\t\tvoid refetch();\n\t}\n\n\tconst close = () => {\n\t\tif (closed) {\n\t\t\treturn;\n\t\t}\n\t\tclosed = true;\n\t\tsubscriber.close();\n\t\tinFlight?.abort();\n\t\tif (debounceTimer !== undefined) {\n\t\t\tclearTimeout(debounceTimer);\n\t\t\tdebounceTimer = undefined;\n\t\t}\n\t\tlisteners.clear();\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\trefetch,\n\t\tclose\n\t};\n};\n\n/**\n * A small default `fetcher`: GET `url` and parse JSON. Forwards the live query's\n * abort signal and throws on a non-2xx response.\n *\n * @example\n * createLiveQuery({ topics, fetcher: jsonFetcher<User[]>('/api/users') })\n */\nexport const jsonFetcher =\n\t<T>(url: string, init?: RequestInit) =>\n\tasync (signal: AbortSignal): Promise<T> => {\n\t\tconst response = await fetch(url, { ...init, signal });\n\t\tif (!response.ok) {\n\t\t\tthrow new Error(`${response.status} ${response.statusText}`);\n\t\t}\n\t\treturn (await response.json()) as T;\n\t};\n",
8
+ "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",
9
+ "import type { ServerFrame } from '../engine/connection';\nimport type { RowKey } from '../engine/types';\nimport type { MutationStorage, PendingMutationRecord } from './syncCollection';\n\nexport type SyncStoreStatus = 'connecting' | 'ready' | 'closed';\n\nexport type SyncStoreState<Row> = {\n\t/** Visible rows: confirmed server state with pending optimistic edits applied. */\n\tdata: Row[];\n\tstatus: SyncStoreStatus;\n\terror: unknown;\n};\n\n/** A working set a mutation's optimistic effect edits in place. */\nexport type OptimisticDraft<Row> = {\n\tset: (row: Row) => void;\n\tdelete: (key: RowKey) => void;\n};\n\n/** A map of named server mutations — typically Eden calls. */\nexport type MutationMap = Record<string, (args: never) => Promise<unknown>>;\n\nexport type MutateOptions<Row> = {\n\t/** Apply the mutation's effect locally for instant UI (rolled back on reject). */\n\toptimistic?: (draft: OptimisticDraft<Row>) => void;\n};\n\nexport type SyncStoreOptions<Row, M extends MutationMap> = {\n\t/** WebSocket URL of the {@link syncSocket} endpoint. */\n\turl: string;\n\t/** Collection name to subscribe to for diffs. */\n\tcollection: string;\n\t/** Query params forwarded to the server collection. */\n\tparams?: unknown;\n\t/**\n\t * Typed read — typically an Eden call (`() => unwrapEden(api.sync.orders.get(...))`).\n\t * It is the source of the `Row` **type**, gives an eager first paint, and is\n\t * reusable for SSR. Live confirmed state then comes from the WS snapshot.\n\t */\n\thydrate?: () => Promise<Row[]>;\n\t/** Seed rows (e.g. from SSR); shown immediately, refreshed by the WS snapshot. */\n\tinitialData?: Row[];\n\t/** Typed server mutations (Eden calls). Enables `store.mutate(name, args, ...)`. */\n\tmutations?: M;\n\t/** Row identity. Defaults to `row.id`. */\n\tkey?: (row: Row) => RowKey;\n\twebSocketImpl?: typeof WebSocket;\n\treconnectMs?: number;\n\tmaxReconnectMs?: number;\n\t/**\n\t * After the server confirms a mutation, drop its optimistic overlay this long\n\t * after if no diff has reflected it (covers mutations that don't touch this\n\t * collection). Defaults to 3000.\n\t */\n\treconcileGraceMs?: number;\n\t/** Persist the pending-mutation queue across reloads (offline). */\n\tstorage?: MutationStorage;\n\tonError?: (error: unknown) => void;\n};\n\nexport type SyncStore<Row, M extends MutationMap> = {\n\tget: () => SyncStoreState<Row>;\n\tsubscribe: (listener: (state: SyncStoreState<Row>) => void) => () => void;\n\t/**\n\t * Run a named server mutation, optionally applying it optimistically. Resolves\n\t * with the server's result; rolls back and rejects if the server rejects it.\n\t * While offline (socket down) it stays queued and retries on reconnect.\n\t */\n\tmutate: <K extends keyof M>(\n\t\tname: K,\n\t\targs: Parameters<M[K]>[0],\n\t\toptions?: MutateOptions<Row>\n\t) => Promise<Awaited<ReturnType<M[K]>>>;\n\t/** Re-run `hydrate` and refresh confirmed state. */\n\trefetch: () => Promise<void>;\n\tclose: () => void;\n};\n\nconst SUBSCRIPTION_ID = 's';\n\ntype Pending<Row> = {\n\tid: number;\n\tname: string;\n\targs: unknown;\n\t/** Keys this mutation's optimistic effect touched, and how. */\n\ttouched: Map<RowKey, 'set' | 'delete'>;\n\toptimistic?: (draft: OptimisticDraft<Row>) => void;\n\tsettled: boolean;\n\tinFlight: boolean;\n\tresolve: (value: unknown) => void;\n\treject: (error: unknown) => void;\n\tgraceTimer?: ReturnType<typeof setTimeout>;\n};\n\n/**\n * A generic, Eden-fed live collection store (the typed Tier 3 client). Confirmed\n * state is maintained from the WS snapshot + diffs; mutations run over your typed\n * transport (Eden), apply an optimistic overlay, and reconcile against the diffs.\n * Types come entirely from the `hydrate`/`mutations` you pass — no `<T>`.\n */\nexport const syncStore = <Row, M extends MutationMap = MutationMap>(\n\toptions: SyncStoreOptions<Row, M>\n): SyncStore<Row, M> => {\n\tconst key = options.key ?? ((row: Row) => (row as { id: RowKey }).id);\n\tconst reconnectMs = options.reconnectMs ?? 500;\n\tconst maxReconnectMs = options.maxReconnectMs ?? 10_000;\n\tconst reconcileGraceMs = options.reconcileGraceMs ?? 3000;\n\tconst mutations = options.mutations ?? ({} as M);\n\tconst Impl = options.webSocketImpl ?? globalThis.WebSocket;\n\tif (!Impl) {\n\t\tthrow new Error(\n\t\t\t'syncStore requires WebSocket. Run in a browser or pass webSocketImpl.'\n\t\t);\n\t}\n\n\tconst confirmed = new Map<RowKey, Row>();\n\tconst pending: Pending<Row>[] = [];\n\tlet mutationSeq = 0;\n\n\tlet state: SyncStoreState<Row> = {\n\t\tdata: options.initialData ? [...options.initialData] : [],\n\t\tstatus: 'connecting',\n\t\terror: undefined\n\t};\n\tif (options.initialData) {\n\t\tfor (const row of options.initialData) {\n\t\t\tconfirmed.set(key(row), row);\n\t\t}\n\t}\n\n\tconst listeners = new Set<(state: SyncStoreState<Row>) => void>();\n\tconst setState = (patch: Partial<SyncStoreState<Row>>) => {\n\t\tstate = { ...state, ...patch };\n\t\tfor (const listener of listeners) {\n\t\t\tlistener(state);\n\t\t}\n\t};\n\tconst recompute = (patch: Partial<SyncStoreState<Row>> = {}) => {\n\t\tconst working = new Map(confirmed);\n\t\tconst draft: OptimisticDraft<Row> = {\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\tconst persist = () => {\n\t\tvoid options.storage?.save(\n\t\t\tpending.map((mutation) => ({\n\t\t\t\tmutationId: mutation.id,\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 dropPending = (mutation: Pending<Row>) => {\n\t\tconst index = pending.indexOf(mutation);\n\t\tif (index !== -1) {\n\t\t\tpending.splice(index, 1);\n\t\t}\n\t\tif (mutation.graceTimer !== undefined) {\n\t\t\tclearTimeout(mutation.graceTimer);\n\t\t}\n\t};\n\n\t/** Drop settled overlays whose touched keys are now reflected in confirmed. */\n\tconst reconcileSettled = () => {\n\t\tlet changed = false;\n\t\tfor (const mutation of [...pending]) {\n\t\t\tif (!mutation.settled) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tlet reflected = true;\n\t\t\tfor (const [rowKey, kind] of mutation.touched) {\n\t\t\t\tconst present = confirmed.has(rowKey);\n\t\t\t\tif (kind === 'set' ? !present : present) {\n\t\t\t\t\treflected = false;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (reflected) {\n\t\t\t\tdropPending(mutation);\n\t\t\t\tchanged = true;\n\t\t\t}\n\t\t}\n\t\tif (changed) {\n\t\t\trecompute();\n\t\t}\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 applyFrame = (frame: ServerFrame<Row>) => {\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\treconcileSettled();\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\treconcileSettled();\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}\n\t\t// ack/reject frames are unused here — mutations run over the typed transport.\n\t};\n\n\tconst runMutation = async (mutation: Pending<Row>) => {\n\t\tif (mutation.inFlight || mutation.settled) {\n\t\t\treturn;\n\t\t}\n\t\tconst run = mutations[mutation.name];\n\t\tif (run === undefined) {\n\t\t\tdropPending(mutation);\n\t\t\trecompute();\n\t\t\tmutation.reject(new Error(`Unknown mutation \"${mutation.name}\"`));\n\t\t\treturn;\n\t\t}\n\t\tmutation.inFlight = true;\n\t\ttry {\n\t\t\tconst result = await (run as (args: unknown) => Promise<unknown>)(\n\t\t\t\tmutation.args\n\t\t\t);\n\t\t\tmutation.inFlight = false;\n\t\t\tmutation.settled = true;\n\t\t\tmutation.resolve(result);\n\t\t\tpersist();\n\t\t\treconcileSettled();\n\t\t\t// If the diff hasn't reflected it yet, drop the overlay after a grace.\n\t\t\tif (pending.includes(mutation)) {\n\t\t\t\tmutation.graceTimer = setTimeout(() => {\n\t\t\t\t\tdropPending(mutation);\n\t\t\t\t\trecompute();\n\t\t\t\t}, reconcileGraceMs);\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tmutation.inFlight = false;\n\t\t\tif (connected) {\n\t\t\t\t// Server is reachable → a real rejection: roll back.\n\t\t\t\tdropPending(mutation);\n\t\t\t\trecompute();\n\t\t\t\tpersist();\n\t\t\t\tmutation.reject(error);\n\t\t\t} else {\n\t\t\t\t// Offline → keep queued; retry on reconnect.\n\t\t\t\toptions.onError?.(error);\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// Retry mutations that failed/queued while offline.\n\t\t\tfor (const mutation of pending) {\n\t\t\t\tif (!mutation.settled && !mutation.inFlight) {\n\t\t\t\t\tvoid runMutation(mutation);\n\t\t\t\t}\n\t\t\t}\n\t\t};\n\t\tws.onmessage = (event) => {\n\t\t\ttry {\n\t\t\t\tapplyFrame(\n\t\t\t\t\tJSON.parse(event.data as string) as ServerFrame<Row>\n\t\t\t\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\tconst eagerHydrate = async () => {\n\t\tif (\n\t\t\toptions.hydrate === undefined ||\n\t\t\toptions.initialData !== undefined\n\t\t) {\n\t\t\treturn;\n\t\t}\n\t\ttry {\n\t\t\tconst rows = await options.hydrate();\n\t\t\t// Don't clobber a WS snapshot that already arrived.\n\t\t\tif (state.status !== 'ready') {\n\t\t\t\tconfirmed.clear();\n\t\t\t\tfor (const row of rows) {\n\t\t\t\t\tconfirmed.set(key(row), row);\n\t\t\t\t}\n\t\t\t\trecompute({ status: 'ready' });\n\t\t\t}\n\t\t} catch (error) {\n\t\t\toptions.onError?.(error);\n\t\t}\n\t};\n\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 as PendingMutationRecord[]) {\n\t\t\tif (pending.some((m) => m.id === record.mutationId)) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tpending.push({\n\t\t\t\tid: record.mutationId,\n\t\t\t\tname: record.name,\n\t\t\t\targs: record.args,\n\t\t\t\ttouched: new Map(),\n\t\t\t\tsettled: false,\n\t\t\t\tinFlight: false,\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\tvoid runMutation(mutation);\n\t\t\t}\n\t\t}\n\t};\n\n\tconnect();\n\tvoid eagerHydrate();\n\tvoid hydratePersisted();\n\n\tconst collectTouched = (\n\t\toptimistic?: (draft: OptimisticDraft<Row>) => void\n\t) => {\n\t\tconst touched = new Map<RowKey, 'set' | 'delete'>();\n\t\toptimistic?.({\n\t\t\tset: (row) => touched.set(key(row), 'set'),\n\t\t\tdelete: (rowKey) => touched.set(rowKey, 'delete')\n\t\t});\n\t\treturn touched;\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: ((name, args, mutateOptions) =>\n\t\t\tnew Promise((resolve, reject) => {\n\t\t\t\tconst mutation: Pending<Row> = {\n\t\t\t\t\tid: (mutationSeq += 1),\n\t\t\t\t\tname: name as string,\n\t\t\t\t\targs,\n\t\t\t\t\ttouched: collectTouched(mutateOptions?.optimistic),\n\t\t\t\t\toptimistic: mutateOptions?.optimistic,\n\t\t\t\t\tsettled: false,\n\t\t\t\t\tinFlight: false,\n\t\t\t\t\tresolve: resolve as (value: unknown) => void,\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();\n\t\t\t\tvoid runMutation(mutation);\n\t\t\t})) as SyncStore<Row, M>['mutate'],\n\t\trefetch: async () => {\n\t\t\tif (options.hydrate === undefined) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst rows = await options.hydrate();\n\t\t\tconfirmed.clear();\n\t\t\tfor (const row of rows) {\n\t\t\t\tconfirmed.set(key(row), row);\n\t\t\t}\n\t\t\trecompute({ status: 'ready' });\n\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// already closing\n\t\t\t}\n\t\t\tfor (const mutation of pending.splice(0)) {\n\t\t\t\tif (mutation.graceTimer !== undefined) {\n\t\t\t\t\tclearTimeout(mutation.graceTimer);\n\t\t\t\t}\n\t\t\t\tmutation.reject(new Error('sync store 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\n/**\n * Unwrap an Eden treaty response (`{ data, error }`) to its data, throwing the\n * error. Use it to feed Eden calls to {@link syncStore}'s `hydrate`/`mutations`:\n * `hydrate: () => unwrapEden(api.sync.orders.get({ query }))`.\n */\nexport const unwrapEden = async <T>(\n\tresponse: Promise<{ data: T | null; error?: unknown }>\n): Promise<T> => {\n\tconst { data, error } = await response;\n\tif (error !== null && error !== undefined) {\n\t\tthrow error;\n\t}\n\treturn data as T;\n};\n"
6
10
  ],
7
- "mappings": ";AAkCO,IAAM,uBAAuB;AAAA,EACnC;AAAA,EACA;AAAA,EACA,MAAM;AAAA,EACN;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,MAC4C;AAAA,EAC5C,MAAM,OAAO,mBAAmB,WAAW;AAAA,EAC3C,IAAI,CAAC,MAAM;AAAA,IACV,MAAM,IAAI,MACT,sFACD;AAAA,EACD;AAAA,EAEA,MAAM,SAAS,IAAI,gBAAgB,EAAE,QAAQ,OAAO,KAAK,GAAG,EAAE,CAAC;AAAA,EAC/D,MAAM,YAAY,IAAI,SAAS,GAAG,IAAI,MAAM;AAAA,EAC5C,MAAM,SAAS,IAAI,KAAK,GAAG,MAAM,YAAY,OAAO,SAAS,KAAK;AAAA,IACjE,iBAAiB,mBAAmB;AAAA,EACrC,CAAC;AAAA,EAED,OAAO,YAAY,CAAC,UAAU;AAAA,IAC7B,IAAI;AAAA,MACH,QAAQ,KAAK,MAAM,MAAM,IAAI,CAAkB;AAAA,MAC9C,MAAM;AAAA;AAAA,EAIT,IAAI,QAAQ;AAAA,IACX,OAAO,SAAS,MAAM,OAAO;AAAA,EAC9B;AAAA,EACA,IAAI,SAAS;AAAA,IACZ,OAAO,UAAU,CAAC,UAAU,QAAQ,KAAK;AAAA,EAC1C;AAAA,EAEA,OAAO;AAAA,IACN,OAAO,MAAM,OAAO,MAAM;AAAA,IAC1B;AAAA,EACD;AAAA;",
8
- "debugId": "10D2496AED02ED9364756E2164756E21",
11
+ "mappings": "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",
12
+ "debugId": "5E158140B740DFD964756E2164756E21",
9
13
  "names": []
10
14
  }
@@ -0,0 +1,75 @@
1
+ export type LiveQueryState<T> = {
2
+ /** Latest query result, or `undefined` before the first successful fetch. */
3
+ data: T | undefined;
4
+ /** Error from the most recent fetch, or `undefined` if it succeeded. */
5
+ error: unknown;
6
+ /** `true` until the first result arrives (no data yet). */
7
+ loading: boolean;
8
+ /** `true` while a (re)fetch is in flight — data may still be present. */
9
+ fetching: boolean;
10
+ };
11
+ export type LiveQueryOptions<T> = {
12
+ /**
13
+ * Topics this query depends on — typically the server's
14
+ * `deriveReadTopics(...).topics`. Any event on one of them triggers a
15
+ * refetch. A trailing `*` matches by prefix server-side.
16
+ */
17
+ topics: string[];
18
+ /**
19
+ * Runs the read and resolves the query result. Receives an `AbortSignal`
20
+ * that fires when a newer fetch supersedes this one or the query is closed.
21
+ */
22
+ fetcher: (signal: AbortSignal) => Promise<T>;
23
+ /** SSE endpoint mounted by the {@link sync} plugin. Defaults to `/sync`. */
24
+ url?: string;
25
+ /** Send cookies with the SSE request (cross-origin auth). */
26
+ withCredentials?: boolean;
27
+ /** EventSource implementation; defaults to the global one. */
28
+ eventSourceImpl?: typeof EventSource;
29
+ /**
30
+ * Seed data (e.g. from SSR). When provided, the initial fetch is skipped —
31
+ * the query trusts this until an event or a manual {@link LiveQuery.refetch}.
32
+ */
33
+ initialData?: T;
34
+ /**
35
+ * Skip the initial fetch and stay idle until the first event or a manual
36
+ * refetch. Reconnects still re-hydrate.
37
+ */
38
+ manual?: boolean;
39
+ /**
40
+ * Coalesce a burst of events into one refetch within this window (ms).
41
+ * Defaults to 0 — refetch once per event.
42
+ */
43
+ debounceMs?: number;
44
+ /** Called when a fetch rejects (stale data is retained). */
45
+ onError?: (error: unknown) => void;
46
+ };
47
+ export type LiveQuery<T> = {
48
+ /** Current state snapshot (stable reference until the next change). */
49
+ get: () => LiveQueryState<T>;
50
+ /** Subscribe to state changes; returns an unsubscribe. */
51
+ subscribe: (listener: (state: LiveQueryState<T>) => void) => () => void;
52
+ /** Force a refetch now. Resolves when this fetch settles. */
53
+ refetch: () => Promise<void>;
54
+ /** Stop the SSE subscription, cancel any in-flight fetch, drop listeners. */
55
+ close: () => void;
56
+ };
57
+ /**
58
+ * A live, self-refreshing query: hydrate once via `fetcher`, then refetch
59
+ * whenever the server publishes one of `topics` — the read half of Tier 2,
60
+ * built on {@link createSyncSubscriber}. Framework-agnostic: `get` + `subscribe`
61
+ * plug straight into React's `useSyncExternalStore` or any equivalent.
62
+ *
63
+ * Pair it with the Drizzle adapter's `deriveReadTopics` (server) and
64
+ * `publishChange`/`publishWhere` (mutations) so a write invalidates exactly the
65
+ * queries that read the changed rows.
66
+ */
67
+ export declare const createLiveQuery: <T>(options: LiveQueryOptions<T>) => LiveQuery<T>;
68
+ /**
69
+ * A small default `fetcher`: GET `url` and parse JSON. Forwards the live query's
70
+ * abort signal and throws on a non-2xx response.
71
+ *
72
+ * @example
73
+ * createLiveQuery({ topics, fetcher: jsonFetcher<User[]>('/api/users') })
74
+ */
75
+ export declare const jsonFetcher: <T>(url: string, init?: RequestInit) => (signal: AbortSignal) => Promise<T>;
@@ -0,0 +1,30 @@
1
+ import type { ReactiveEvent } from '../reactiveHub';
2
+ export type SyncSubscriberOptions = {
3
+ /** Topics to subscribe to. A trailing `*` matches by prefix server-side. */
4
+ topics: string[];
5
+ /** Called for every reactive event pushed from the server. */
6
+ onEvent: (event: ReactiveEvent) => void;
7
+ /** SSE endpoint mounted by the {@link sync} plugin. Defaults to `/sync`. */
8
+ url?: string;
9
+ onOpen?: () => void;
10
+ onError?: (event: Event) => void;
11
+ /** Send cookies with the SSE request (cross-origin auth). */
12
+ withCredentials?: boolean;
13
+ /**
14
+ * EventSource implementation to use. Defaults to the global one; pass a polyfill
15
+ * for non-browser runtimes.
16
+ */
17
+ eventSourceImpl?: typeof EventSource;
18
+ };
19
+ export type SyncSubscriber = {
20
+ close: () => void;
21
+ /** The underlying EventSource, for advanced listeners. */
22
+ source: EventSource;
23
+ };
24
+ /**
25
+ * Subscribe a browser to the server's {@link ReactiveHub} over SSE. `onEvent` fires
26
+ * whenever a subscribed topic is published — the cue to refetch (or read the pushed
27
+ * payload) instead of polling. EventSource reconnects automatically on transient
28
+ * network drops.
29
+ */
30
+ export declare const createSyncSubscriber: ({ topics, onEvent, url, onOpen, onError, withCredentials, eventSourceImpl }: SyncSubscriberOptions) => SyncSubscriber;
@@ -0,0 +1,102 @@
1
+ import type { RowKey } from '../engine/types';
2
+ export type { ServerFrame } from '../engine/connection';
3
+ export type SyncCollectionStatus = 'connecting' | 'ready' | 'closed';
4
+ export type SyncCollectionState<T> = {
5
+ /** Visible rows: the server state with pending optimistic mutations applied. */
6
+ data: T[];
7
+ /** Connection/sync status. */
8
+ status: SyncCollectionStatus;
9
+ /** Last error message from the server, or `undefined`. */
10
+ error: unknown;
11
+ };
12
+ /** A working set a mutation's optimistic effect edits in place. */
13
+ export type OptimisticDraft<T> = {
14
+ /** Insert or replace a row by key. */
15
+ set: (row: T) => void;
16
+ /** Remove a row by key. */
17
+ delete: (key: RowKey) => void;
18
+ };
19
+ export type MutateOptions<T> = {
20
+ /** Registered server mutation name. */
21
+ name: string;
22
+ /** Arguments forwarded to the mutation handler. */
23
+ args?: unknown;
24
+ /**
25
+ * Apply this mutation's effect to the local set immediately for instant UI.
26
+ * Reverted automatically if the server rejects it. Omit for a non-optimistic
27
+ * mutation (UI updates only once the authoritative diff arrives).
28
+ */
29
+ optimistic?: (draft: OptimisticDraft<T>) => void;
30
+ };
31
+ /** A pending mutation persisted for replay across reloads. */
32
+ export type PendingMutationRecord = {
33
+ mutationId: number;
34
+ name: string;
35
+ args: unknown;
36
+ };
37
+ /**
38
+ * Durable storage for the pending-mutation queue, so unconfirmed mutations
39
+ * survive a page reload (offline). The queue is replayed when the socket
40
+ * connects; records are dropped as they're acked.
41
+ */
42
+ export type MutationStorage = {
43
+ load: () => PendingMutationRecord[] | Promise<PendingMutationRecord[]>;
44
+ save: (records: PendingMutationRecord[]) => void | Promise<void>;
45
+ };
46
+ /**
47
+ * A {@link MutationStorage} backed by `localStorage` under `key`. No-ops where
48
+ * `localStorage` is unavailable (e.g. SSR).
49
+ */
50
+ export declare const localStorageMutationStorage: (key: string) => MutationStorage;
51
+ export type SyncCollectionOptions<T> = {
52
+ /** WebSocket URL of the {@link syncSocket} endpoint (e.g. `ws://host/sync/ws`). */
53
+ url: string;
54
+ /** Registered collection name to subscribe to. */
55
+ collection: string;
56
+ /** Query params forwarded to the server collection's hydrate/match/authorize. */
57
+ params?: unknown;
58
+ /** Row identity, used to apply diffs and optimistic edits. Defaults to `row.id`. */
59
+ key?: (row: T) => RowKey;
60
+ /** WebSocket implementation; defaults to the global one (pass for tests/SSR). */
61
+ webSocketImpl?: typeof WebSocket;
62
+ /**
63
+ * Base reconnect delay (ms), doubled each attempt up to `maxReconnectMs`.
64
+ * Set 0 to disable auto-reconnect. Defaults to 500.
65
+ */
66
+ reconnectMs?: number;
67
+ /** Maximum reconnect backoff (ms). Defaults to 10000. */
68
+ maxReconnectMs?: number;
69
+ /**
70
+ * Persist the pending-mutation queue so it survives a reload (offline) and
71
+ * replays on connect. See {@link localStorageMutationStorage}.
72
+ */
73
+ storage?: MutationStorage;
74
+ /** Called with each server error message. */
75
+ onError?: (error: unknown) => void;
76
+ };
77
+ export type SyncCollection<T> = {
78
+ /** Current state snapshot (stable reference until the next change). */
79
+ get: () => SyncCollectionState<T>;
80
+ /** Subscribe to state changes; returns an unsubscribe. */
81
+ subscribe: (listener: (state: SyncCollectionState<T>) => void) => () => void;
82
+ /**
83
+ * Run a server mutation, optionally applying it optimistically. Resolves with
84
+ * the server's result on ack, rejects (and rolls back) on reject. Pending
85
+ * mutations are replayed when the socket reconnects, so they survive a drop.
86
+ */
87
+ mutate: <R = unknown>(options: MutateOptions<T>) => Promise<R>;
88
+ /** Unsubscribe on the server, close the socket, and stop reconnecting. */
89
+ close: () => void;
90
+ };
91
+ /**
92
+ * A live collection backed by the WebSocket sync engine. Reads: connect,
93
+ * subscribe, apply the server's snapshot then row-level diffs, re-sync on
94
+ * reconnect. Writes: {@link SyncCollection.mutate} applies an optimistic overlay
95
+ * immediately, sends the mutation, and reconciles on ack (drop the overlay — the
96
+ * authoritative diff already arrived) or reject (roll back). Framework-agnostic
97
+ * (`get` + `subscribe`).
98
+ *
99
+ * Mutations are replayed on reconnect, so make server mutations idempotent —
100
+ * delivery is at-least-once if an ack is lost across a drop.
101
+ */
102
+ export declare const createSyncCollection: <T>(options: SyncCollectionOptions<T>) => SyncCollection<T>;
@@ -0,0 +1,81 @@
1
+ import type { RowKey } from '../engine/types';
2
+ import type { MutationStorage } from './syncCollection';
3
+ export type SyncStoreStatus = 'connecting' | 'ready' | 'closed';
4
+ export type SyncStoreState<Row> = {
5
+ /** Visible rows: confirmed server state with pending optimistic edits applied. */
6
+ data: Row[];
7
+ status: SyncStoreStatus;
8
+ error: unknown;
9
+ };
10
+ /** A working set a mutation's optimistic effect edits in place. */
11
+ export type OptimisticDraft<Row> = {
12
+ set: (row: Row) => void;
13
+ delete: (key: RowKey) => void;
14
+ };
15
+ /** A map of named server mutations — typically Eden calls. */
16
+ export type MutationMap = Record<string, (args: never) => Promise<unknown>>;
17
+ export type MutateOptions<Row> = {
18
+ /** Apply the mutation's effect locally for instant UI (rolled back on reject). */
19
+ optimistic?: (draft: OptimisticDraft<Row>) => void;
20
+ };
21
+ export type SyncStoreOptions<Row, M extends MutationMap> = {
22
+ /** WebSocket URL of the {@link syncSocket} endpoint. */
23
+ url: string;
24
+ /** Collection name to subscribe to for diffs. */
25
+ collection: string;
26
+ /** Query params forwarded to the server collection. */
27
+ params?: unknown;
28
+ /**
29
+ * Typed read — typically an Eden call (`() => unwrapEden(api.sync.orders.get(...))`).
30
+ * It is the source of the `Row` **type**, gives an eager first paint, and is
31
+ * reusable for SSR. Live confirmed state then comes from the WS snapshot.
32
+ */
33
+ hydrate?: () => Promise<Row[]>;
34
+ /** Seed rows (e.g. from SSR); shown immediately, refreshed by the WS snapshot. */
35
+ initialData?: Row[];
36
+ /** Typed server mutations (Eden calls). Enables `store.mutate(name, args, ...)`. */
37
+ mutations?: M;
38
+ /** Row identity. Defaults to `row.id`. */
39
+ key?: (row: Row) => RowKey;
40
+ webSocketImpl?: typeof WebSocket;
41
+ reconnectMs?: number;
42
+ maxReconnectMs?: number;
43
+ /**
44
+ * After the server confirms a mutation, drop its optimistic overlay this long
45
+ * after if no diff has reflected it (covers mutations that don't touch this
46
+ * collection). Defaults to 3000.
47
+ */
48
+ reconcileGraceMs?: number;
49
+ /** Persist the pending-mutation queue across reloads (offline). */
50
+ storage?: MutationStorage;
51
+ onError?: (error: unknown) => void;
52
+ };
53
+ export type SyncStore<Row, M extends MutationMap> = {
54
+ get: () => SyncStoreState<Row>;
55
+ subscribe: (listener: (state: SyncStoreState<Row>) => void) => () => void;
56
+ /**
57
+ * Run a named server mutation, optionally applying it optimistically. Resolves
58
+ * with the server's result; rolls back and rejects if the server rejects it.
59
+ * While offline (socket down) it stays queued and retries on reconnect.
60
+ */
61
+ mutate: <K extends keyof M>(name: K, args: Parameters<M[K]>[0], options?: MutateOptions<Row>) => Promise<Awaited<ReturnType<M[K]>>>;
62
+ /** Re-run `hydrate` and refresh confirmed state. */
63
+ refetch: () => Promise<void>;
64
+ close: () => void;
65
+ };
66
+ /**
67
+ * A generic, Eden-fed live collection store (the typed Tier 3 client). Confirmed
68
+ * state is maintained from the WS snapshot + diffs; mutations run over your typed
69
+ * transport (Eden), apply an optimistic overlay, and reconcile against the diffs.
70
+ * Types come entirely from the `hydrate`/`mutations` you pass — no `<T>`.
71
+ */
72
+ export declare const syncStore: <Row, M extends MutationMap = MutationMap>(options: SyncStoreOptions<Row, M>) => SyncStore<Row, M>;
73
+ /**
74
+ * Unwrap an Eden treaty response (`{ data, error }`) to its data, throwing the
75
+ * error. Use it to feed Eden calls to {@link syncStore}'s `hydrate`/`mutations`:
76
+ * `hydrate: () => unwrapEden(api.sync.orders.get({ query }))`.
77
+ */
78
+ export declare const unwrapEden: <T>(response: Promise<{
79
+ data: T | null;
80
+ error?: unknown;
81
+ }>) => Promise<T>;
@@ -0,0 +1,45 @@
1
+ import type { RowChange, RowKey } from './types';
2
+ export type AggregateOptions<T> = {
3
+ /** Row identity — used to track each row's contribution across updates. */
4
+ key: (row: T) => RowKey;
5
+ /** Group rows by this key. Omit to aggregate everything into one group (`''`). */
6
+ groupBy?: (row: T) => RowKey;
7
+ /**
8
+ * Numeric value to aggregate for `sum`/`avg`/`min`/`max`. Omit for a
9
+ * count-only aggregate (sum stays 0, min/max stay undefined).
10
+ */
11
+ value?: (row: T) => number;
12
+ };
13
+ /** Maintained summary for one group. */
14
+ export type AggregateGroup = {
15
+ group: RowKey;
16
+ count: number;
17
+ sum: number;
18
+ /** `sum / count`, or 0 when the group is empty. */
19
+ avg: number;
20
+ min: number | undefined;
21
+ max: number | undefined;
22
+ };
23
+ export type Aggregate<T> = {
24
+ /** Bulk-load the initial rows (replaces current state). */
25
+ hydrate: (rows: Iterable<T>) => void;
26
+ /** Fold one row change into the running aggregates. */
27
+ apply: (change: RowChange<T>) => void;
28
+ /** Current summary for every non-empty group. */
29
+ groups: () => AggregateGroup[];
30
+ /** Current summary for one group, or `undefined` if empty. */
31
+ group: (group: RowKey) => AggregateGroup | undefined;
32
+ };
33
+ /**
34
+ * An incrementally-maintained aggregation — the DD-lite for `count`/`sum`/`avg`/
35
+ * `min`/`max`, optionally grouped. Feed it the change feed (insert/update/delete)
36
+ * and it updates each group's summary in place: count/sum/avg are O(1); min/max
37
+ * use a value multiset so removing the current extremum recomputes correctly
38
+ * (O(distinct values) only when the extremum leaves).
39
+ *
40
+ * Per-row contributions are tracked by `key`, so updates (including a row moving
41
+ * between groups) and deletes adjust the right group without re-scanning. Use it
42
+ * server-side over the engine's change feed, or client-side over collection
43
+ * diffs.
44
+ */
45
+ export declare const createAggregate: <T>(options: AggregateOptions<T>) => Aggregate<T>;
@@ -0,0 +1,87 @@
1
+ import type { RowKey } from './types';
2
+ /**
3
+ * App-provided context for a subscription — typically the authenticated session
4
+ * (user id, roles). Passed to `authorize`, `hydrate`, and `match` so a
5
+ * collection can scope its rows to the caller.
6
+ */
7
+ export type CollectionContext = Record<string, unknown>;
8
+ export type CollectionDefinition<T, P = void, Ctx = CollectionContext> = {
9
+ /** Collection name — its identity for subscribe (e.g. `orders`). */
10
+ name: string;
11
+ /**
12
+ * Source tables this collection reads. A committed change to any of them
13
+ * updates the collection. Defaults to `[name]`. List several for a join /
14
+ * aggregate collection — which uses the refetch fallback, since a single
15
+ * table's row can't be matched into a multi-table result.
16
+ */
17
+ tables?: string[];
18
+ /**
19
+ * Fetch the initial result set from your database (any ORM). Receives the
20
+ * subscription's params and context so it can filter to the caller.
21
+ */
22
+ hydrate: (params: P, ctx: Ctx) => Promise<Iterable<T>> | Iterable<T>;
23
+ /** Row identity. Defaults to `row.id`. */
24
+ key?: (row: T) => RowKey;
25
+ /**
26
+ * The query's filter as a JS predicate, for incremental matching. Omit to use
27
+ * the refetch fallback (re-hydrate on every change to the collection).
28
+ *
29
+ * It MUST encode the same row filter as `hydrate`/`authorize`: a change that
30
+ * the predicate accepts is pushed to the subscriber, so a too-loose predicate
31
+ * leaks rows. (Deriving `match` from the same filter as `hydrate` keeps the
32
+ * two in lockstep — the planned adapter convenience.)
33
+ */
34
+ match?: (row: T, params: P, ctx: Ctx) => boolean;
35
+ /**
36
+ * Access control: return `false` (or throw) to deny the subscription. Runs
37
+ * before `hydrate`. Without it a collection is world-readable, so treat it as
38
+ * mandatory for any non-public data.
39
+ */
40
+ authorize?: (params: P, ctx: Ctx) => boolean | Promise<boolean>;
41
+ };
42
+ /**
43
+ * Define a syncable collection. Identity at runtime — it exists for type
44
+ * inference, so `params`/`ctx`/row types flow through `hydrate`/`match`/
45
+ * `authorize` without restating them. Register it with a {@link SyncEngine}.
46
+ */
47
+ export declare const defineCollection: <T, P = void, Ctx = CollectionContext>(definition: CollectionDefinition<T, P, Ctx>) => CollectionDefinition<T, P, Ctx>;
48
+ /** One input of a join collection. */
49
+ export type JoinSide<Row, P, Ctx> = {
50
+ /** Source table name (the change feed routes its changes to this side). */
51
+ table: string;
52
+ /** Fetch this side's rows for the subscription (scoped to the caller). */
53
+ hydrate: (params: P, ctx: Ctx) => Promise<Iterable<Row>> | Iterable<Row>;
54
+ /** Row identity within this side. */
55
+ key: (row: Row) => RowKey;
56
+ /** Join value — matched for equality against the other side's `on`. */
57
+ on: (row: Row) => RowKey;
58
+ /**
59
+ * Access/predicate filter for incremental changes on this side. A changed row
60
+ * that fails it is treated as a leave (removed from the join), so a row that
61
+ * becomes invisible drops out. Omit only for an unscoped side.
62
+ */
63
+ match?: (row: Row, params: P, ctx: Ctx) => boolean;
64
+ };
65
+ /**
66
+ * A collection that is the incremental inner equi-join of two tables. The engine
67
+ * maintains it with an {@link createEquiJoin} operator — a change to either side
68
+ * moves only the affected pairs, instead of re-hydrating the whole join.
69
+ */
70
+ export type JoinCollectionDefinition<L, R, Out, P = void, Ctx = CollectionContext> = {
71
+ name: string;
72
+ kind: 'join';
73
+ left: JoinSide<L, P, Ctx>;
74
+ right: JoinSide<R, P, Ctx>;
75
+ /** Combine a matched pair into an output row. */
76
+ select: (left: L, right: R) => Out;
77
+ /** Output row identity (must be unique per emitted row). */
78
+ key: (out: Out) => RowKey;
79
+ /** Access control; return false (or throw) to deny the subscription. */
80
+ authorize?: (params: P, ctx: Ctx) => boolean | Promise<boolean>;
81
+ };
82
+ /**
83
+ * Define an incremental equi-join collection (see {@link JoinCollectionDefinition}).
84
+ * For a many-to-one join the output can key by the left id; for many-to-many,
85
+ * include both ids in the output and key on the pair.
86
+ */
87
+ export declare const defineJoinCollection: <L, R, Out, P = void, Ctx = CollectionContext>(definition: Omit<JoinCollectionDefinition<L, R, Out, P, Ctx>, "kind">) => JoinCollectionDefinition<L, R, Out, P, Ctx>;
@@ -0,0 +1,71 @@
1
+ import type { SyncEngine } from './syncEngine';
2
+ /**
3
+ * Wire protocol for the sync-engine WebSocket. One connection multiplexes many
4
+ * collection subscriptions, each tagged with a client-chosen `id`.
5
+ */
6
+ /** Client → server. */
7
+ export type ClientFrame = {
8
+ type: 'subscribe';
9
+ id: string;
10
+ collection: string;
11
+ params?: unknown;
12
+ /** Resume from a version already applied (catch-up instead of snapshot). */
13
+ since?: number;
14
+ } | {
15
+ type: 'unsubscribe';
16
+ id: string;
17
+ } | {
18
+ type: 'mutate';
19
+ mutationId: number;
20
+ name: string;
21
+ args?: unknown;
22
+ };
23
+ /** Server → client. `version` is the change-feed watermark this frame brings. */
24
+ export type ServerFrame<T = unknown> = {
25
+ type: 'snapshot';
26
+ id: string;
27
+ rows: T[];
28
+ version?: number;
29
+ } | {
30
+ type: 'diff';
31
+ id: string;
32
+ added: T[];
33
+ removed: T[];
34
+ changed: T[];
35
+ version?: number;
36
+ } | {
37
+ type: 'error';
38
+ id?: string;
39
+ message: string;
40
+ } | {
41
+ type: 'ack';
42
+ mutationId: number;
43
+ result?: unknown;
44
+ } | {
45
+ type: 'reject';
46
+ mutationId: number;
47
+ message: string;
48
+ };
49
+ export type SyncConnectionOptions = {
50
+ engine: SyncEngine;
51
+ /** Resolved auth context for this connection; passed to every subscribe. */
52
+ ctx: unknown;
53
+ /** Send a frame to the client (the transport serializes it). */
54
+ send: (frame: ServerFrame) => void;
55
+ };
56
+ export type SyncConnection = {
57
+ /** Handle one client frame (a parsed object or a raw JSON string). */
58
+ handle: (raw: unknown) => Promise<void>;
59
+ /** Tear down every subscription on this connection (call on socket close). */
60
+ close: () => void;
61
+ };
62
+ /**
63
+ * The per-connection protocol handler — transport-agnostic glue between a single
64
+ * client socket and the {@link SyncEngine}. It owns that connection's
65
+ * subscriptions: a `subscribe` frame authorizes + hydrates and replies with a
66
+ * `snapshot`, then streams `diff` frames; `unsubscribe`/`close` release views.
67
+ *
68
+ * Pure (no WebSocket import) so it can be unit-tested with a fake `send`; the
69
+ * Elysia `syncSocket` plugin is the thin adapter that feeds it socket events.
70
+ */
71
+ export declare const createSyncConnection: ({ engine, ctx, send }: SyncConnectionOptions) => SyncConnection;