@doync/client 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 (52) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +233 -0
  3. package/dist/adapter.cjs +1 -0
  4. package/dist/adapter.d.cts +86 -0
  5. package/dist/adapter.d.cts.map +1 -0
  6. package/dist/adapter.d.ts +86 -0
  7. package/dist/adapter.d.ts.map +1 -0
  8. package/dist/adapter.js +2 -0
  9. package/dist/adapter.js.map +1 -0
  10. package/dist/client-C6jAdhbe.cjs +15 -0
  11. package/dist/client-CNyLMCw0.d.ts +812 -0
  12. package/dist/client-CNyLMCw0.d.ts.map +1 -0
  13. package/dist/client-ClV8ce6X.js +16 -0
  14. package/dist/client-ClV8ce6X.js.map +1 -0
  15. package/dist/client-DHXO0dbf.d.cts +812 -0
  16. package/dist/client-DHXO0dbf.d.cts.map +1 -0
  17. package/dist/index.cjs +0 -0
  18. package/dist/index.d.cts +2 -0
  19. package/dist/index.d.ts +2 -0
  20. package/dist/index.js +0 -0
  21. package/dist/internal.cjs +1 -0
  22. package/dist/internal.d.cts +69 -0
  23. package/dist/internal.d.cts.map +1 -0
  24. package/dist/internal.d.ts +69 -0
  25. package/dist/internal.d.ts.map +1 -0
  26. package/dist/internal.js +2 -0
  27. package/dist/internal.js.map +1 -0
  28. package/package.json +79 -0
  29. package/src/adapter.ts +25 -0
  30. package/src/client-mutation-registry.ts +31 -0
  31. package/src/client.ts +100 -0
  32. package/src/engine.ts +3322 -0
  33. package/src/identity.ts +41 -0
  34. package/src/index.ts +36 -0
  35. package/src/internal.ts +37 -0
  36. package/src/migrate.ts +156 -0
  37. package/src/mutations.ts +96 -0
  38. package/src/port.ts +74 -0
  39. package/src/raw-read.ts +135 -0
  40. package/src/replica/db/0000_replica_engine_v0.sql +20 -0
  41. package/src/replica/db/0001_release_stamps.sql +6 -0
  42. package/src/replica/db/meta/0000_snapshot.json +129 -0
  43. package/src/replica/db/meta/0001_snapshot.json +167 -0
  44. package/src/replica/db/meta/_journal.json +20 -0
  45. package/src/replica/db/schema.ts +67 -0
  46. package/src/replica/index.ts +185 -0
  47. package/src/replica/meta.ts +26 -0
  48. package/src/replica/stamps.ts +102 -0
  49. package/src/replica/track.ts +190 -0
  50. package/src/socket-reconnect.ts +242 -0
  51. package/src/socket.ts +44 -0
  52. package/src/sql-raw.d.ts +4 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Vitor Buzinaro
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,233 @@
1
+ # `@doync/client`
2
+
3
+ **Building an app?** You almost certainly want one of the platform / framework packages, not this one:
4
+
5
+ - [`@doync/web`](../web) — browser client (SharedWorker + DB worker + OPFS)
6
+ - [`@doync/mobile`](../mobile) — single-process React Native client
7
+ - [`@doync/react`](../react) — `DoyncProvider`, `useQuery`, `useMutation`, and friends
8
+
9
+ Those packages own the host seams (workers, sockets, native SQLite) and hand your UI a ready [`DoyncClient`](#the-surface-adapters-wrap). This package is the **client engine** they wrap: View handles, the optimistic Layer 1 (savepoint/rebase), the subscription desire map, mutate/push, schema events, reconnect coordination. App code should not import it directly unless you are writing a custom adapter.
10
+
11
+ The package splits its public surface by **audience** (same lockstep semver — the distinction is not stability):
12
+
13
+ | Entry | Audience | Contents |
14
+ | --- | --- | --- |
15
+ | `@doync/client` (`.`) | Apps via platform/framework packages | App-facing call-surface **types** only — nothing callable |
16
+ | [`@doync/client/adapter`](#writing-a-host--framework-adapter) | Third-party adapter authors | Host seams, reconnect coordinator, `createClient` |
17
+ | [`@doync/client/internal`](#internal) | Sibling `@doync/*` packages | Engine construction; **may break anytime** |
18
+
19
+ ## What this package is
20
+
21
+ `@doync/client` is the engine half of every doync client:
22
+
23
+ - **`DoyncClient` + View handles** — the call surface adapters wrap (`subscribe` / `once` / `local` / `mutate`, connection + schema status, preload); shared reactive reads with `retain` / `release` ownership.
24
+ - **OnceView** — per-call cache-and-network reads.
25
+ - **Mutation types** (`ClientMutation*`) — the sealed registry shapes consumers see; dispatch helpers live under `/internal`.
26
+ - **Host seams + reconnect + `createClient`** — on [`@doync/client/adapter`](#writing-a-host--framework-adapter); environment packages implement `LocalDb` / `SyncSocket` and construct clients there.
27
+
28
+ Schema, queries, and mutations themselves are defined in [`@doync/core`](../core). The concrete engine class and construction helpers live under [`@doync/client/internal`](#internal).
29
+
30
+ ## What's on the main entry
31
+
32
+ The main entry exports **types only** — the symbols apps meet through `@doync/web` / `@doync/mobile` / `@doync/react`. Nothing callable remains here (`createClient` moved to `/adapter`).
33
+
34
+ | Symbol | Role |
35
+ | --- | --- |
36
+ | `DoyncClient` | The call surface adapters wrap |
37
+ | `View` / `OnceView` / `ViewStatus` / `QueryStatus` | Reactive + once handle types |
38
+ | `SubscribeOptions` / `PreloadHandle` / `PreloadOptions` / `FalsyQuery` | Subscribe / preload options |
39
+ | `MutationResult` / `MutationOptions` | `mutate()` return + options |
40
+ | `ConnectionStatus` / `SchemaEvent` / `SchemaEventKind` | Connection + schema status |
41
+ | `LogoutBehavior` | `'keep' \| 'forget'` durable logout policy (here only — `/adapter` references it, no double-listing) |
42
+ | `ClientMutation` / `ClientMutationHandler` / `ClientMutationRegistry` / `ClientMutationTx` | Client mutation registry types |
43
+
44
+ ## Writing a host / framework adapter
45
+
46
+ Host adapters (`@doync/web`, `@doync/mobile`, a community Electron or Node one) implement the engine's ports and construct a client. Framework adapters (`@doync/react`, a community Svelte/Vue/Solid one) are a **thin** layer over the `DoyncClient` call surface.
47
+
48
+ Import seams, reconnect, and construction from **`@doync/client/adapter`**. App-facing types stay on `@doync/client`.
49
+
50
+ ```ts
51
+ import type { DoyncClient, View, OnceView, LogoutBehavior } from '@doync/client'
52
+ import {
53
+ createClient,
54
+ createReconnectingSocket,
55
+ type CreateClientOptions,
56
+ type LocalDb,
57
+ type LocalRow,
58
+ type SyncSocket,
59
+ type SyncSocketHandlers,
60
+ type SeamStatus,
61
+ type ReconnectSocket,
62
+ type ReconnectingSocket,
63
+ type ReconnectingSocketConfig,
64
+ type SocketCloseInfo,
65
+ } from '@doync/client/adapter'
66
+ ```
67
+
68
+ Our own platform packages use `createClientEngine` from [`/internal`](#internal) rather than `createClient` — the latter is the construction entry a **third-party** adapter author reaches for.
69
+
70
+ ### The surface adapters wrap
71
+
72
+ ```ts
73
+ import type { DoyncClient, View, OnceView } from '@doync/client'
74
+
75
+ interface DoyncClient {
76
+ // Bound form only: pass queries.issues.open(args), or falsy to skip.
77
+ subscribe(bound | FalsyQuery, options?): View // reactive; SHARED
78
+ local(sql, ...params): View // reactive; SHARED
79
+ once(bound | FalsyQuery): OnceView // cache-and-network; PER-CALL
80
+ mutate(mutation, args, options?): MutationResult // { client, server }
81
+
82
+ preload(bound | FalsyQuery, options?): PreloadHandle
83
+
84
+ readonly userId: string | null
85
+ readonly connectionStatus: ConnectionStatus
86
+ onConnectionChange(listener): () => void
87
+ readonly schemaStatus: SchemaEvent | null
88
+ onSchemaChange(listener): () => void
89
+ }
90
+ ```
91
+
92
+ Lifecycle for framework adapters lives here: the adapter mints handles, retains for a mount, releases on unmount. Everything else — sharing, the Warm pool, wire frames, once's deferred dispose — is the client's job.
93
+
94
+ The rule: **render may compute, only commit may own.** Creation is pure: a handle you never retain owns nothing and is GC'd like any object. `retain()` is the ownership act; `release()` is its cleanup.
95
+
96
+ Query-taking methods accept a **Bound query** — `queries.issues.open(args)` — or falsy (`false | null | undefined`) to mean "no query". Binding is pure; validation and resolve happen inside the engine. `options.skip` remains supported.
97
+
98
+ ### Host seams on `/adapter`
99
+
100
+ ```ts
101
+ import type {
102
+ LocalDb,
103
+ LocalRow,
104
+ SyncSocket,
105
+ SyncSocketHandlers,
106
+ SeamStatus,
107
+ } from '@doync/client/adapter'
108
+
109
+ // LocalDb — synchronous local-DB port (wa-sqlite / op-sqlite / node:sqlite)
110
+ // SyncSocket — Mirror socket seam the engine drives (send / setHandlers / reconnect)
111
+ // SeamStatus — 'connecting' | 'error' channel for truthful connectionStatus
112
+ ```
113
+
114
+ ### Reconnect coordinator on `/adapter`
115
+
116
+ ```ts
117
+ import {
118
+ createReconnectingSocket,
119
+ type ReconnectSocket,
120
+ type ReconnectingSocket,
121
+ type ReconnectingSocketConfig,
122
+ type SocketCloseInfo,
123
+ } from '@doync/client/adapter'
124
+ ```
125
+
126
+ Shared reconnect machinery (backoff, buffer-until-open outbox, re-handshake hook, keepalive). Wrap a transport-shaped `ReconnectSocket` and hand the resulting transcript to the engine as a `SyncSocket`.
127
+
128
+ ### `createClient` on `/adapter`
129
+
130
+ ```ts
131
+ import { createClient, type CreateClientOptions } from '@doync/client/adapter'
132
+ import type { DoyncClient, LogoutBehavior } from '@doync/client'
133
+
134
+ const client: DoyncClient = createClient({
135
+ db, // LocalDb
136
+ socket, // SyncSocket
137
+ schema,
138
+ mutations,
139
+ token,
140
+ ctx,
141
+ userId,
142
+ logoutBehavior, // optional LogoutBehavior ('keep' | 'forget')
143
+ })
144
+ ```
145
+
146
+ ### View — shared handle
147
+
148
+ ```ts
149
+ interface View<Row> {
150
+ current(): readonly Row[] // last decoded rows; STABLE until they move
151
+ status(): ViewStatus // { status: 'unknown' | 'complete' | 'error', error? }
152
+ onChange(listener): () => void
153
+ readonly one?: boolean // runtime one-ness (set when known)
154
+ retain(): void // commit/mount — the only ownership act
155
+ release(): void // cleanup — idempotent; last release parks Warm pool
156
+ }
157
+ ```
158
+
159
+ Lifecycle:
160
+
161
+ 1. **Create** in your setup/state slot. Pure: resolve, local SQL, decode. A discarded setup result owns nothing.
162
+ 2. **`retain()`** at mount / after the host commits the component. Never during render.
163
+ 3. **`release()`** at unmount / cleanup. The last release of a key fires the wire `unsubscribe` immediately (the Warm pool is a client paint budget, not a server cost) and parks the read for one policy tick.
164
+ 4. **Key change** → create the new handle, retain it, release the old.
165
+
166
+ `current()` stays **stale-but-stable** through any backing teardown (Warm-pool window, remount, server error status): it keeps answering the last snapshot until a new recompute notifies. Server rejection is **status-only** — last rows keep serving with `status: 'error'` alongside; the component decides.
167
+
168
+ Sharing is per statement identity at the engine desire map. Decode and one-ness are per-handle. Don't re-count, don't re-decode for sharing, don't invent a second store above `DoyncClient`.
169
+
170
+ ### OnceView — commit-owned, not shared
171
+
172
+ ```ts
173
+ interface OnceView<Row> {
174
+ current(): readonly Row[]
175
+ onChange(listener): () => void
176
+ dispose(): void // deferred one Warm-pool tick; re-use cancels
177
+ readonly server: Promise<readonly Row[]> // starts the network half
178
+ }
179
+ ```
180
+
181
+ `once()` is per-call: promise identity, no sharing, no Warm pool of rows. Create in setup; the network half starts on first `onChange` / `server` access (so a discarded render never dials). Cleanup calls `dispose()`. Cache-phase rows paint immediately (stale-but-stable).
182
+
183
+ ### `mutate`
184
+
185
+ ```ts
186
+ const { client, server } = doync.mutate(mutations.issues.create, args)
187
+ await client // optimistic body applied on the local replica
188
+ await server // Origin ack (or reject → rebase)
189
+ ```
190
+
191
+ Pass the registered mutation definition object and its args. The engine resolves the body from its own registry; the object is a typed reference only.
192
+
193
+ ### `connectionStatus` — five states
194
+
195
+ | state | meaning |
196
+ | -------------- | ------------------------------------------------------ |
197
+ | `connecting` | a (re)connect attempt is dialing or backing off |
198
+ | `connected` | the socket is open — sync is live |
199
+ | `disconnected` | dropped and backing off (offline) |
200
+ | `error` | a transport-level error |
201
+ | `needs-auth` | the Mirror rejected auth — a token refresh is required |
202
+
203
+ Subscribe with `onConnectionChange` and read `connectionStatus`. Never throw on a status — a degraded answer beats a crash.
204
+
205
+ ### Schema events
206
+
207
+ `schemaStatus` (`SchemaEvent | null`) covers disruption classes. Subscribe with `onSchemaChange` (fires on a new state **and** on a silent clear) and re-read `schemaStatus`. Kinds: `reload` (terminal — reload the app), `server-behind` (transient), `resync` / `forget` (transient recovery notices — surface as dismissible banners so a heal never looks like silent data loss).
208
+
209
+ ## `/adapter` surface
210
+
211
+ | Symbol | Role |
212
+ | --- | --- |
213
+ | `createClient` / `CreateClientOptions` | Construct a `DoyncClient` over a `LocalDb` + `SyncSocket` |
214
+ | `LocalDb` / `LocalRow` | Synchronous local-DB port |
215
+ | `SyncSocket` / `SyncSocketHandlers` / `SeamStatus` | Mirror socket seam |
216
+ | `createReconnectingSocket` (+ reconnect types) | Shared reconnect coordinator |
217
+
218
+ `LogoutBehavior` is **not** re-exported here — import it from `@doync/client`.
219
+
220
+ ## `/internal`
221
+
222
+ ```ts
223
+ import {
224
+ createClientEngine,
225
+ ClientEngine,
226
+ asClientMutation,
227
+ validateMutationArgs,
228
+ } from '@doync/client/internal'
229
+ ```
230
+
231
+ Concrete-engine construction (`createClientEngine`, the `ClientEngine` class, replica DDL, meta-key constants, mutation flattening) and mutation-registry dispatch helpers (`asClientMutation`, `validateMutationArgs`) live on `@doync/client/internal`. **Anything under `/internal` may change in any release, including patches, with no notice**. Platform packages (`@doync/web`, `@doync/mobile`) import it; application code and framework adapters should not.
232
+
233
+ Semver for this package applies to the public entries (`.` and `./adapter`). `/internal` is the carve-out.
@@ -0,0 +1 @@
1
+ Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("./client-C6jAdhbe.cjs");let t=require("@doync/core/internal");function n(e){let n=e.schedule??((e,t)=>{setTimeout(e,t)}),r=e.scheduleInterval??((e,t)=>{let n=setInterval(e,t);return()=>clearInterval(n)}),i=e.backoffBaseMs??250,a=e.backoffMaxMs??1e4,o=e.pingIntervalMs??3e4,s=null,c=[],l=!1,u=0,d=!1,f=null,p=0;function m(){f?.(),f=null}function h(){m(),f=r(()=>{s?.readyState===`open`&&s.send(t.PING_FRAME)},o)}function g(){if(!(!s||s.readyState!==`open`))for(;c.length>0;){let e=c.shift();e!==void 0&&s.send(e)}}function _(){let e=Math.min(i*2**u,a);u++;let t=p;n(()=>{l||t===p&&v()},e)}function v(){if(s&&s.readyState!==`closed`)return;e.onConnecting?.();let t=e.connect();s=t;let n=!1;t.onopen=()=>{s===t&&(u=0,d&&(c.length=0),d=!0,e.onReconnect(),g(),h())},t.onmessage=n=>{s===t&&e.onMessage?.(n)},t.onerror=()=>{s===t&&(n=!0)},t.onclose=r=>{s===t&&(s=null,m(),l||e.onDisconnected?.({code:r,wasError:n}),l||_())}}return{get isConnected(){return s?.readyState===`open`},open(){l=!1,v()},close(){l=!0,m(),p++,s?.close(),s=null},reconnect(){s?.close()},reconnectNow(){s&&s.readyState!==`closed`||(l=!1,p++,v())},send(e){c.push(e),g()}}}exports.createClient=e.t,exports.createReconnectingSocket=n;
@@ -0,0 +1,86 @@
1
+ import { H as LocalRow, M as SeamStatus, N as SyncSocket, P as SyncSocketHandlers, V as LocalDb, n as createClient, t as CreateClientOptions } from "./client-DHXO0dbf.cjs";
2
+
3
+ //#region src/socket-reconnect.d.ts
4
+ /**
5
+ * Minimal socket surface the reconnect coordinator drives — a structural subset
6
+ * of browser `WebSocket` so fakes work under test.
7
+ */
8
+ interface ReconnectSocket {
9
+ readonly readyState: "connecting" | "open" | "closed";
10
+ send(frame: string): void;
11
+ close(): void;
12
+ onopen: (() => void) | null;
13
+ onmessage: ((data: string) => void) | null;
14
+ /**
15
+ * Socket closed. `code` is the WebSocket close code when available (1008 =
16
+ * auth-close).
17
+ */
18
+ onclose: ((code?: number) => void) | null;
19
+ /** Transport-level error; browsers fire it before `onclose`. */
20
+ onerror: (() => void) | null;
21
+ }
22
+ /**
23
+ * How an unexpected drop looked: optional WebSocket close `code`, and whether a
24
+ * transport error preceded the close. Feeds disconnected / error / needs-auth
25
+ * status decisions.
26
+ */
27
+ interface SocketCloseInfo {
28
+ readonly code?: number;
29
+ readonly wasError?: boolean;
30
+ }
31
+ /** Environment the reconnect coordinator needs (all injectable for tests). */
32
+ interface ReconnectingSocketConfig {
33
+ /** Open a fresh server socket. Called on `open()` and each reconnect. */
34
+ connect(): ReconnectSocket;
35
+ /**
36
+ * Fired on every (re)open — host should re-handshake and re-send pending
37
+ * pushes.
38
+ */
39
+ onReconnect(): void;
40
+ /** A (re)connect attempt started (initial open and every backoff retry). */
41
+ onConnecting?(): void;
42
+ /** Unexpected drop (not an explicit `close()`). Drives connection status. */
43
+ onDisconnected?(info: SocketCloseInfo): void;
44
+ /** Server→client frame (JSON text). */
45
+ onMessage?(data: string): void;
46
+ /** Schedule a reconnect after `delayMs` (defaults to `setTimeout`). */
47
+ schedule?(callback: () => void, delayMs: number): void;
48
+ /**
49
+ * Register a repeating keepalive tick; return a canceller (defaults to
50
+ * `setInterval` / `clearInterval`).
51
+ */
52
+ scheduleInterval?(callback: () => void, ms: number): () => void;
53
+ /** Backoff base in ms (first retry). Default 250. */
54
+ backoffBaseMs?: number;
55
+ /** Backoff cap in ms. Default 10_000. */
56
+ backoffMaxMs?: number;
57
+ /** Keepalive ping period in ms while open. Default 30_000. */
58
+ pingIntervalMs?: number;
59
+ }
60
+ /** Lifecycle handle for one reconnecting server socket. */
61
+ interface ReconnectingSocket {
62
+ /** Whether the socket is currently open. */
63
+ readonly isConnected: boolean;
64
+ /** Open (idempotent while already open/connecting). */
65
+ open(): void;
66
+ /** Intentional close — does not reconnect. */
67
+ close(): void;
68
+ /** Drop and re-establish under backoff (engine-requested). */
69
+ reconnect(): void;
70
+ /**
71
+ * Skip pending backoff and dial immediately if not open. No-op while open or
72
+ * dial already in flight. Use for mobile foreground / web `online`.
73
+ */
74
+ reconnectNow(): void;
75
+ /** Enqueue a client→server frame; flushed once open. */
76
+ send(frame: string): void;
77
+ }
78
+ /**
79
+ * Create a reconnecting socket coordinator: buffers sends, exponential backoff
80
+ * on unexpected close, keepalive ping while open, drops the stale outbox on
81
+ * reconnect (the re-handshake resends pending pushes).
82
+ */
83
+ declare function createReconnectingSocket(config: ReconnectingSocketConfig): ReconnectingSocket;
84
+ //#endregion
85
+ export { type CreateClientOptions, type LocalDb, type LocalRow, type ReconnectSocket, type ReconnectingSocket, type ReconnectingSocketConfig, type SeamStatus, type SocketCloseInfo, type SyncSocket, type SyncSocketHandlers, createClient, createReconnectingSocket };
86
+ //# sourceMappingURL=adapter.d.cts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"adapter.d.cts","names":[],"sources":["../src/socket-reconnect.ts"],"mappings":";;;;;;AAUA;UAAiB,eAAA;EAAA,SACN,UAAA;EACT,IAAA,CAAK,KAAA;EACL,KAAA;EACA,MAAA;EACA,SAAA,IAAa,IAAA;;;;;EAKb,OAAA,IAAW,IAAA;;EAEX,OAAA;AAAA;AAAA;AAQF;;;;AARE,UAQe,eAAA;EAAA,SACN,IAAA;EAAA,SACA,QAAA;AAAA;;UAIM,wBAAA;;EAEf,OAAA,IAAW,eAAA;;;;;EAKX,WAAA;;EAEA,YAAA;;EAEA,cAAA,EAAgB,IAAA,EAAM,eAAA;;EAEtB,SAAA,EAAW,IAAA;;EAEX,QAAA,EAAU,QAAA,cAAsB,OAAA;;;;AAWhC;EANA,gBAAA,EAAkB,QAAA,cAAsB,EAAA;EAUzB;EARf,aAAA;EAQe;EANf,YAAA;;EAEA,cAAA;AAAA;;UAIe,kBAAA;;WAEN,WAAA;EAaJ;EAXL,IAAA;EAuBc;EArBd,KAAA;EAuBC;EArBD,SAAA;;;;AAqBC;EAhBD,YAAA;;EAEA,IAAA,CAAK,KAAA;AAAA;;;;;;iBAYS,wBAAA,CACd,MAAA,EAAQ,wBAAA,GACP,kBAAA"}
@@ -0,0 +1,86 @@
1
+ import { H as LocalRow, M as SeamStatus, N as SyncSocket, P as SyncSocketHandlers, V as LocalDb, n as createClient, t as CreateClientOptions } from "./client-CNyLMCw0.js";
2
+
3
+ //#region src/socket-reconnect.d.ts
4
+ /**
5
+ * Minimal socket surface the reconnect coordinator drives — a structural subset
6
+ * of browser `WebSocket` so fakes work under test.
7
+ */
8
+ interface ReconnectSocket {
9
+ readonly readyState: "connecting" | "open" | "closed";
10
+ send(frame: string): void;
11
+ close(): void;
12
+ onopen: (() => void) | null;
13
+ onmessage: ((data: string) => void) | null;
14
+ /**
15
+ * Socket closed. `code` is the WebSocket close code when available (1008 =
16
+ * auth-close).
17
+ */
18
+ onclose: ((code?: number) => void) | null;
19
+ /** Transport-level error; browsers fire it before `onclose`. */
20
+ onerror: (() => void) | null;
21
+ }
22
+ /**
23
+ * How an unexpected drop looked: optional WebSocket close `code`, and whether a
24
+ * transport error preceded the close. Feeds disconnected / error / needs-auth
25
+ * status decisions.
26
+ */
27
+ interface SocketCloseInfo {
28
+ readonly code?: number;
29
+ readonly wasError?: boolean;
30
+ }
31
+ /** Environment the reconnect coordinator needs (all injectable for tests). */
32
+ interface ReconnectingSocketConfig {
33
+ /** Open a fresh server socket. Called on `open()` and each reconnect. */
34
+ connect(): ReconnectSocket;
35
+ /**
36
+ * Fired on every (re)open — host should re-handshake and re-send pending
37
+ * pushes.
38
+ */
39
+ onReconnect(): void;
40
+ /** A (re)connect attempt started (initial open and every backoff retry). */
41
+ onConnecting?(): void;
42
+ /** Unexpected drop (not an explicit `close()`). Drives connection status. */
43
+ onDisconnected?(info: SocketCloseInfo): void;
44
+ /** Server→client frame (JSON text). */
45
+ onMessage?(data: string): void;
46
+ /** Schedule a reconnect after `delayMs` (defaults to `setTimeout`). */
47
+ schedule?(callback: () => void, delayMs: number): void;
48
+ /**
49
+ * Register a repeating keepalive tick; return a canceller (defaults to
50
+ * `setInterval` / `clearInterval`).
51
+ */
52
+ scheduleInterval?(callback: () => void, ms: number): () => void;
53
+ /** Backoff base in ms (first retry). Default 250. */
54
+ backoffBaseMs?: number;
55
+ /** Backoff cap in ms. Default 10_000. */
56
+ backoffMaxMs?: number;
57
+ /** Keepalive ping period in ms while open. Default 30_000. */
58
+ pingIntervalMs?: number;
59
+ }
60
+ /** Lifecycle handle for one reconnecting server socket. */
61
+ interface ReconnectingSocket {
62
+ /** Whether the socket is currently open. */
63
+ readonly isConnected: boolean;
64
+ /** Open (idempotent while already open/connecting). */
65
+ open(): void;
66
+ /** Intentional close — does not reconnect. */
67
+ close(): void;
68
+ /** Drop and re-establish under backoff (engine-requested). */
69
+ reconnect(): void;
70
+ /**
71
+ * Skip pending backoff and dial immediately if not open. No-op while open or
72
+ * dial already in flight. Use for mobile foreground / web `online`.
73
+ */
74
+ reconnectNow(): void;
75
+ /** Enqueue a client→server frame; flushed once open. */
76
+ send(frame: string): void;
77
+ }
78
+ /**
79
+ * Create a reconnecting socket coordinator: buffers sends, exponential backoff
80
+ * on unexpected close, keepalive ping while open, drops the stale outbox on
81
+ * reconnect (the re-handshake resends pending pushes).
82
+ */
83
+ declare function createReconnectingSocket(config: ReconnectingSocketConfig): ReconnectingSocket;
84
+ //#endregion
85
+ export { type CreateClientOptions, type LocalDb, type LocalRow, type ReconnectSocket, type ReconnectingSocket, type ReconnectingSocketConfig, type SeamStatus, type SocketCloseInfo, type SyncSocket, type SyncSocketHandlers, createClient, createReconnectingSocket };
86
+ //# sourceMappingURL=adapter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"adapter.d.ts","names":[],"sources":["../src/socket-reconnect.ts"],"mappings":";;;;;;AAUA;UAAiB,eAAA;EAAA,SACN,UAAA;EACT,IAAA,CAAK,KAAA;EACL,KAAA;EACA,MAAA;EACA,SAAA,IAAa,IAAA;;;;;EAKb,OAAA,IAAW,IAAA;;EAEX,OAAA;AAAA;AAAA;AAQF;;;;AARE,UAQe,eAAA;EAAA,SACN,IAAA;EAAA,SACA,QAAA;AAAA;;UAIM,wBAAA;;EAEf,OAAA,IAAW,eAAA;;;;;EAKX,WAAA;;EAEA,YAAA;;EAEA,cAAA,EAAgB,IAAA,EAAM,eAAA;;EAEtB,SAAA,EAAW,IAAA;;EAEX,QAAA,EAAU,QAAA,cAAsB,OAAA;;;;AAWhC;EANA,gBAAA,EAAkB,QAAA,cAAsB,EAAA;EAUzB;EARf,aAAA;EAQe;EANf,YAAA;;EAEA,cAAA;AAAA;;UAIe,kBAAA;;WAEN,WAAA;EAaJ;EAXL,IAAA;EAuBc;EArBd,KAAA;EAuBC;EArBD,SAAA;;;;AAqBC;EAhBD,YAAA;;EAEA,IAAA,CAAK,KAAA;AAAA;;;;;;iBAYS,wBAAA,CACd,MAAA,EAAQ,wBAAA,GACP,kBAAA"}
@@ -0,0 +1,2 @@
1
+ import{t as e}from"./client-ClV8ce6X.js";import{PING_FRAME as t}from"@doync/core/internal";function n(e){let n=e.schedule??((e,t)=>{setTimeout(e,t)}),r=e.scheduleInterval??((e,t)=>{let n=setInterval(e,t);return()=>clearInterval(n)}),i=e.backoffBaseMs??250,a=e.backoffMaxMs??1e4,o=e.pingIntervalMs??3e4,s=null,c=[],l=!1,u=0,d=!1,f=null,p=0;function m(){f?.(),f=null}function h(){m(),f=r(()=>{s?.readyState===`open`&&s.send(t)},o)}function g(){if(!(!s||s.readyState!==`open`))for(;c.length>0;){let e=c.shift();e!==void 0&&s.send(e)}}function _(){let e=Math.min(i*2**u,a);u++;let t=p;n(()=>{l||t===p&&v()},e)}function v(){if(s&&s.readyState!==`closed`)return;e.onConnecting?.();let t=e.connect();s=t;let n=!1;t.onopen=()=>{s===t&&(u=0,d&&(c.length=0),d=!0,e.onReconnect(),g(),h())},t.onmessage=n=>{s===t&&e.onMessage?.(n)},t.onerror=()=>{s===t&&(n=!0)},t.onclose=r=>{s===t&&(s=null,m(),l||e.onDisconnected?.({code:r,wasError:n}),l||_())}}return{get isConnected(){return s?.readyState===`open`},open(){l=!1,v()},close(){l=!0,m(),p++,s?.close(),s=null},reconnect(){s?.close()},reconnectNow(){s&&s.readyState!==`closed`||(l=!1,p++,v())},send(e){c.push(e),g()}}}export{e as createClient,n as createReconnectingSocket};
2
+ //# sourceMappingURL=adapter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"adapter.js","names":[],"sources":["../src/socket-reconnect.ts"],"sourcesContent":["// ADR-0016; closeio/doync#90/#185 — shared reconnect policy for web and mobile.\n// Buffers sends, exponential backoff on unexpected close, keepalive ping,\n// drops stale outbox on reconnect (re-handshake resends pending).\n\nimport { PING_FRAME } from '@doync/core/internal'\n\n/**\n * Minimal socket surface the reconnect coordinator drives — a structural subset\n * of browser `WebSocket` so fakes work under test.\n */\nexport interface ReconnectSocket {\n readonly readyState: 'connecting' | 'open' | 'closed'\n send(frame: string): void\n close(): void\n onopen: (() => void) | null\n onmessage: ((data: string) => void) | null\n /**\n * Socket closed. `code` is the WebSocket close code when available (1008 =\n * auth-close).\n */\n onclose: ((code?: number) => void) | null\n /** Transport-level error; browsers fire it before `onclose`. */\n onerror: (() => void) | null\n}\n\n/**\n * How an unexpected drop looked: optional WebSocket close `code`, and whether a\n * transport error preceded the close. Feeds disconnected / error / needs-auth\n * status decisions.\n */\nexport interface SocketCloseInfo {\n readonly code?: number\n readonly wasError?: boolean\n}\n\n/** Environment the reconnect coordinator needs (all injectable for tests). */\nexport interface ReconnectingSocketConfig {\n /** Open a fresh server socket. Called on `open()` and each reconnect. */\n connect(): ReconnectSocket\n /**\n * Fired on every (re)open — host should re-handshake and re-send pending\n * pushes.\n */\n onReconnect(): void\n /** A (re)connect attempt started (initial open and every backoff retry). */\n onConnecting?(): void\n /** Unexpected drop (not an explicit `close()`). Drives connection status. */\n onDisconnected?(info: SocketCloseInfo): void\n /** Server→client frame (JSON text). */\n onMessage?(data: string): void\n /** Schedule a reconnect after `delayMs` (defaults to `setTimeout`). */\n schedule?(callback: () => void, delayMs: number): void\n /**\n * Register a repeating keepalive tick; return a canceller (defaults to\n * `setInterval` / `clearInterval`).\n */\n scheduleInterval?(callback: () => void, ms: number): () => void\n /** Backoff base in ms (first retry). Default 250. */\n backoffBaseMs?: number\n /** Backoff cap in ms. Default 10_000. */\n backoffMaxMs?: number\n /** Keepalive ping period in ms while open. Default 30_000. */\n pingIntervalMs?: number\n}\n\n/** Lifecycle handle for one reconnecting server socket. */\nexport interface ReconnectingSocket {\n /** Whether the socket is currently open. */\n readonly isConnected: boolean\n /** Open (idempotent while already open/connecting). */\n open(): void\n /** Intentional close — does not reconnect. */\n close(): void\n /** Drop and re-establish under backoff (engine-requested). */\n reconnect(): void\n /**\n * Skip pending backoff and dial immediately if not open. No-op while open or\n * dial already in flight. Use for mobile foreground / web `online`.\n */\n reconnectNow(): void\n /** Enqueue a client→server frame; flushed once open. */\n send(frame: string): void\n}\n\nconst DEFAULT_BACKOFF_BASE_MS = 250\nconst DEFAULT_BACKOFF_MAX_MS = 10_000\nconst DEFAULT_PING_INTERVAL_MS = 30_000\n\n/**\n * Create a reconnecting socket coordinator: buffers sends, exponential backoff\n * on unexpected close, keepalive ping while open, drops the stale outbox on\n * reconnect (the re-handshake resends pending pushes).\n */\nexport function createReconnectingSocket(\n config: ReconnectingSocketConfig,\n): ReconnectingSocket {\n const schedule =\n config.schedule ??\n ((cb, delay) => {\n setTimeout(cb, delay)\n })\n const scheduleInterval =\n config.scheduleInterval ??\n ((cb, ms) => {\n const handle = setInterval(cb, ms)\n return () => clearInterval(handle)\n })\n const backoffBaseMs = config.backoffBaseMs ?? DEFAULT_BACKOFF_BASE_MS\n const backoffMaxMs = config.backoffMaxMs ?? DEFAULT_BACKOFF_MAX_MS\n const pingIntervalMs = config.pingIntervalMs ?? DEFAULT_PING_INTERVAL_MS\n\n let socket: ReconnectSocket | null = null\n const outbox: string[] = []\n /** Set by `close()` so intentional teardown does not reconnect. */\n let closedByUs = false\n /** Failed/closed attempts since last successful open. */\n let attempt = 0\n /**\n * Next open is a reconnect (socket opened at least once). First open keeps\n * outbox; reconnect drops it (re-handshake resends pending).\n */\n let reopening = false\n /** Cancel keepalive interval; null when not open. */\n let cancelPing: (() => void) | null = null\n /**\n * Bumped to invalidate pending backoff (#185). Injectable `schedule` has no\n * canceller — generation token lets `reconnectNow()` skip a dial.\n */\n let reconnectGeneration = 0\n\n function stopPing(): void {\n cancelPing?.()\n cancelPing = null\n }\n\n function startPing(): void {\n stopPing()\n cancelPing = scheduleInterval(() => {\n // Exact DO keepalive bytes (ADR-0016); skip if socket closed between ticks.\n if (socket?.readyState === 'open') socket.send(PING_FRAME)\n }, pingIntervalMs)\n }\n\n function flushOutbox(): void {\n if (!socket || socket.readyState !== 'open') return\n while (outbox.length > 0) {\n const frame = outbox.shift()\n if (frame !== undefined) socket.send(frame)\n }\n }\n\n function scheduleReconnect(): void {\n const delay = Math.min(backoffBaseMs * 2 ** attempt, backoffMaxMs)\n attempt++\n const gen = reconnectGeneration\n schedule(() => {\n if (closedByUs) return\n // reconnectNow/later schedule stole this slot — no double-dial.\n if (gen !== reconnectGeneration) return\n connect()\n }, delay)\n }\n\n function connect(): void {\n if (socket && socket.readyState !== 'closed') return\n // Surface connecting through the whole backoff, not just first open (#102).\n config.onConnecting?.()\n const next = config.connect()\n socket = next\n // Transport error on THIS socket before close => error status vs disconnected.\n let errored = false\n next.onopen = () => {\n // A superseded socket (close then open before the prior onclose) must not\n // drive handshake / ping of the abandoned dial (closeio/doync#319).\n if (socket !== next) return\n attempt = 0\n // Reconnect drops stale outbox (re-handshake resends); first open keeps it.\n if (reopening) outbox.length = 0\n reopening = true\n config.onReconnect()\n flushOutbox()\n startPing()\n }\n next.onmessage = (data: string) => {\n if (socket !== next) return\n config.onMessage?.(data)\n }\n next.onerror = () => {\n if (socket !== next) return\n errored = true\n }\n next.onclose = (code?: number) => {\n // Browser WebSocket.close() delivers onclose asynchronously. Last-out\n // teardown (ADR-0034 / #280) closes, then a new tab may open() before that\n // onclose lands. The late event must not null the NEW socket or schedule a\n // reconnect that races the intentional redial — otherwise the tab wedges\n // `disconnected` with no usable dial (closeio/doync#319).\n if (socket !== next) return\n socket = null\n stopPing()\n // Report drop unless we closed; 1008 included (#102). Auth stays sticky on hub.\n if (!closedByUs) config.onDisconnected?.({ code, wasError: errored })\n // Unexpected close reconnects; close() stays down.\n if (!closedByUs) scheduleReconnect()\n }\n }\n\n return {\n get isConnected(): boolean {\n return socket?.readyState === 'open'\n },\n open(): void {\n closedByUs = false\n connect()\n },\n close(): void {\n closedByUs = true\n stopPing()\n // Invalidate pending backoff so close after drop stays silent.\n reconnectGeneration++\n socket?.close()\n socket = null\n },\n reconnect(): void {\n // Engine drop-and-reopen (client-ahead skew, ADR-0020): onclose schedules\n // one backoff (closedByUs false). No-op if no socket.\n socket?.close()\n },\n reconnectNow(): void {\n // Foreground/online nudge (#185): if not open and no dial in flight, skip\n // backoff and dial now. Open/connecting is no-op.\n if (socket && socket.readyState !== 'closed') return\n closedByUs = false\n reconnectGeneration++\n connect()\n },\n send(frame: string): void {\n outbox.push(frame)\n flushOutbox()\n },\n }\n}\n"],"mappings":"2FA6FA,SAAgB,EACd,EACoB,CACpB,IAAM,EACJ,EAAO,YACL,EAAI,IAAU,CACd,WAAW,EAAI,CAAK,CACtB,GACI,EACJ,EAAO,oBACL,EAAI,IAAO,CACX,IAAM,EAAS,YAAY,EAAI,CAAE,EACjC,UAAa,cAAc,CAAM,CACnC,GACI,EAAgB,EAAO,eAAiB,IACxC,EAAe,EAAO,cAAgB,IACtC,EAAiB,EAAO,gBAAkB,IAE5C,EAAiC,KAC/B,EAAmB,CAAC,EAEtB,EAAa,GAEb,EAAU,EAKV,EAAY,GAEZ,EAAkC,KAKlC,EAAsB,EAE1B,SAAS,GAAiB,CACxB,IAAa,EACb,EAAa,IACf,CAEA,SAAS,GAAkB,CACzB,EAAS,EACT,EAAa,MAAuB,CAE9B,GAAQ,aAAe,QAAQ,EAAO,KAAK,CAAU,CAC3D,EAAG,CAAc,CACnB,CAEA,SAAS,GAAoB,CACvB,MAAC,GAAU,EAAO,aAAe,QACrC,KAAO,EAAO,OAAS,GAAG,CACxB,IAAM,EAAQ,EAAO,MAAM,EACvB,IAAU,IAAA,IAAW,EAAO,KAAK,CAAK,CAC5C,CACF,CAEA,SAAS,GAA0B,CACjC,IAAM,EAAQ,KAAK,IAAI,EAAgB,GAAK,EAAS,CAAY,EACjE,IACA,IAAM,EAAM,EACZ,MAAe,CACT,GAEA,IAAQ,GACZ,EAAQ,CACV,EAAG,CAAK,CACV,CAEA,SAAS,GAAgB,CACvB,GAAI,GAAU,EAAO,aAAe,SAAU,OAE9C,EAAO,eAAe,EACtB,IAAM,EAAO,EAAO,QAAQ,EAC5B,EAAS,EAET,IAAI,EAAU,GACd,EAAK,WAAe,CAGd,IAAW,IACf,EAAU,EAEN,IAAW,EAAO,OAAS,GAC/B,EAAY,GACZ,EAAO,YAAY,EACnB,EAAY,EACZ,EAAU,EACZ,EACA,EAAK,UAAa,GAAiB,CAC7B,IAAW,GACf,EAAO,YAAY,CAAI,CACzB,EACA,EAAK,YAAgB,CACf,IAAW,IACf,EAAU,GACZ,EACA,EAAK,QAAW,GAAkB,CAM5B,IAAW,IACf,EAAS,KACT,EAAS,EAEJ,GAAY,EAAO,iBAAiB,CAAE,OAAM,SAAU,CAAQ,CAAC,EAE/D,GAAY,EAAkB,EACrC,CACF,CAEA,MAAO,CACL,IAAI,aAAuB,CACzB,OAAO,GAAQ,aAAe,MAChC,EACA,MAAa,CACX,EAAa,GACb,EAAQ,CACV,EACA,OAAc,CACZ,EAAa,GACb,EAAS,EAET,IACA,GAAQ,MAAM,EACd,EAAS,IACX,EACA,WAAkB,CAGhB,GAAQ,MAAM,CAChB,EACA,cAAqB,CAGf,GAAU,EAAO,aAAe,WACpC,EAAa,GACb,IACA,EAAQ,EACV,EACA,KAAK,EAAqB,CACxB,EAAO,KAAK,CAAK,EACjB,EAAY,CACd,CACF,CACF"}
@@ -0,0 +1,15 @@
1
+ let e=require("@doync/core/internal"),t=require("@doync/sqlite-parser");function n(t){let n={};for(let[r,i]of Object.entries((0,e.flattenMutations)(t)))n[r]={args:i.args,handler:(e,t,n)=>i.body(e,t,n)};return n}function r(e){return typeof e==`function`?{handler:e}:e}function i(t,n){(0,e.assertWireRepresentable)(n,`mutation args`,{allowBinary:!0});let r=(0,e.canonicalizeArgsBinary)(n),i=t[`~standard`].validate(r);if(i instanceof Promise)throw Error(`doync: async standard-schema validation is not supported for mutation args — use a synchronous validator`);if(i.issues!==void 0){let e=i.issues.map(e=>{let t=(e.path??[]).map(e=>String(typeof e==`object`?e.key:e)).join(`.`);return t===``?e.message:`${t}: ${e.message}`}).join(`; `);throw Error(`doync: mutation args failed validation — ${e}`)}return i.value}var a=class{host;statement;identity;#e=[];#t=0;#n=new Set;#r;constructor(e,t,n,r){this.host=e,this.statement=t,this.identity=n,this.#r=r}get readSet(){return this.identity===void 0?void 0:this.host.readSet()}status(){return this.#r}updateStatus(e){return this.#r.status===e.status&&this.#r.error===e.error?!1:(this.#r=e,!0)}recompute(){let e=this.host.run(this.statement);return o(e,this.#e)?!1:(this.#e=e,this.#t+=1,!0)}rawRows(){return this.#e}get generation(){return this.#t}onChange(e){return this.#n.add(e),()=>this.#n.delete(e)}notify(){for(let e of this.#n)e()}};function o(e,t){if(e.length!==t.length)return!1;for(let n=0;n<e.length;n++){let r=e[n],i=t[n],a=Object.keys(r);if(a.length!==Object.keys(i).length)return!1;for(let e of a)if(!s(r[e]??null,i[e]??null))return!1}return!0}function s(e,t){if(e===t)return!0;if(e instanceof ArrayBuffer&&t instanceof ArrayBuffer){if(e.byteLength!==t.byteLength)return!1;let n=new Uint8Array(e),r=new Uint8Array(t);for(let e=0;e<n.length;e++)if(n[e]!==r[e])return!1;return!0}return!1}const c=new Set([`STMT_CREATE_TABLE`,`STMT_CREATE_INDEX`,`STMT_CREATE_VIEW`,`STMT_CREATE_VTABLE`,`STMT_DROP`,`STMT_ALTER`]),l=new Set([`STMT_INSERT`,`STMT_UPDATE`,`STMT_DELETE`,`STMT_SELECT`,`COMPOUND_SELECT`]);function u(e){try{return(0,t.parse)(e)}catch(e){if(e instanceof t.SQLiteParserError)return null;throw e}}function d(e){try{return(0,t.parseAll)(e)}catch(e){if(e instanceof t.SQLiteParserError)return null;throw e}}function f(e){return e.kind===`STMT_CREATE_TRIGGER`||e.kind===`STMT_DROP`&&e.target===`TRIGGER`?`trigger`:e.kind===`STMT_PRAGMA`?`pragma`:c.has(e.kind)?`ddl`:l.has(e.kind)?`dml`:`other`}function p(e){let n=d(e);if(n===null)throw Error(`could not parse the SQL — a statement has a syntax error; fix it and retry`);return n.map(e=>(0,t.unparse)(e))}function m(e){let t=u(e);return t===null?`other`:f(t)}function h(e,t,n){let r=[];for(let i=t;i<n;i+=1){let a=e.migrations[i];if(a===void 0)throw Error(`doync: bundled migration track has a hole at version ${i} (requested range ${t}..${n}, but the track holds ${e.migrations.length}) — the schema is corrupt or truncated`);for(let e of p(a.sql))m(e)===`ddl`&&r.push(e)}return r}function g(e,t){let n=e.exec(`SELECT v FROM __doync_meta WHERE k = ?`,t)[0];return n===void 0||n.v===null?null:String(n.v)}function _(e,t,n){e.exec(`INSERT INTO __doync_meta (k, v) VALUES (?, ?)
2
+ ON CONFLICT (k) DO UPDATE SET v = excluded.v`,t,n)}var ee="CREATE TABLE `__doync_membership` (\n `instance` text NOT NULL,\n `level` integer NOT NULL,\n `tbl` text NOT NULL,\n `pk` text NOT NULL,\n PRIMARY KEY(`instance`, `level`, `pk`)\n) WITHOUT ROWID;\n--> statement-breakpoint\nCREATE INDEX `__doync_membership_by_row` ON `__doync_membership` (`tbl`,`pk`);--> statement-breakpoint\nCREATE TABLE `__doync_meta` (\n `k` text PRIMARY KEY NOT NULL,\n `v` blob\n) WITHOUT ROWID;\n--> statement-breakpoint\nCREATE TABLE `__doync_pending` (\n `mutation_id` integer PRIMARY KEY NOT NULL,\n `name` text NOT NULL,\n `args` text NOT NULL,\n `idem_key` text\n);\n",te="CREATE TABLE `__doync_release_stamp` (\n `instance` text PRIMARY KEY NOT NULL,\n `cookie` integer NOT NULL,\n `released_at` integer NOT NULL,\n `ttl_ms` integer NOT NULL\n) WITHOUT ROWID;\n",ne=[{idx:0,version:`6`,when:1784031358638,tag:`0000_replica_engine_v0`,breakpoints:!0},{idx:1,version:`6`,when:1784450022155,tag:`0001_release_stamps`,breakpoints:!0}];const re={"0000_replica_engine_v0":ee,"0001_release_stamps":te},v=ne.slice().sort((e,t)=>e.idx-t.idx).map(e=>{let t=re[e.tag];if(t===void 0)throw Error(`doync: replica engine track missing sql for journal tag ${JSON.stringify(e.tag)}`);return t.split(/-->\s*statement-breakpoint/).map(e=>e.trim()).filter(e=>e.length>0)});function y(e){e.exec(`CREATE TABLE IF NOT EXISTS __doync_engine (
3
+ k TEXT PRIMARY KEY,
4
+ v
5
+ ) WITHOUT ROWID`)}function b(e,t){let n=t;for(;n<v.length;){let t=v[n];if(t===void 0)break;let r=n+1;for(let n of t)e.exec(n);e.exec(`INSERT INTO __doync_engine (k, v) VALUES ('track_version', ?)
6
+ ON CONFLICT (k) DO UPDATE SET v = excluded.v`,r),n=r}}function ie(e){y(e);let t=e.exec(`SELECT v FROM __doync_engine WHERE k = 'track_version'`),n=t.length===0?0:Number(t[0]?.v);if((!Number.isFinite(n)||n<0)&&(n=0),n>v.length)return console.warn(`doync: replica engine track_version ${n} is above bundled track length ${v.length} — wiping engine tables and rebuilding (code rollback against a newer database). Consumer state will resync through wipe-and-resync.`),x(e),{rolledBack:!0};try{b(e,n)}catch(t){return console.warn(`doync: replica engine track failed at version ${n} — wiping engine tables and rebuilding (code rollback against a newer database). Consumer state will resync through wipe-and-resync.`,t),x(e),{rolledBack:!0}}return{rolledBack:!1}}function x(e){let t=null,n=null,r=null,i=[];try{t=g(e,`client_id`),n=g(e,`next_mutation_id`),r=g(e,`logout_behavior`),i=e.exec(`SELECT mutation_id, name, args, idem_key FROM __doync_pending`)}catch{}e.exec(`DROP TABLE IF EXISTS __doync_release_stamp`),e.exec(`DROP TABLE IF EXISTS __doync_membership`),e.exec(`DROP TABLE IF EXISTS __doync_meta`),e.exec(`DROP TABLE IF EXISTS __doync_pending`),e.exec(`DROP TABLE IF EXISTS __doync_engine`),y(e),b(e,0),t!==null&&_(e,`client_id`,t),n!==null&&_(e,`next_mutation_id`,n),r!==null&&_(e,`logout_behavior`,r);for(let t of i)e.exec(`INSERT INTO __doync_pending (mutation_id, name, args, idem_key)
7
+ VALUES (?, ?, ?, ?)`,t.mutation_id,t.name,t.args,t.idem_key)}function ae(e){return e===void 0?18e5:e}function oe(e,t){e.exec(`INSERT INTO __doync_release_stamp (instance, cookie, released_at, ttl_ms)
8
+ VALUES (?, ?, ?, ?)
9
+ ON CONFLICT (instance) DO UPDATE SET
10
+ cookie = excluded.cookie,
11
+ released_at = excluded.released_at,
12
+ ttl_ms = excluded.ttl_ms`,t.instance,t.cookie,t.releasedAt,t.ttlMs)}function S(e,t){e.exec(`DELETE FROM __doync_release_stamp WHERE instance = ?`,t)}function C(e){e.exec(`DELETE FROM __doync_release_stamp`)}function w(e){return{instance:String(e.instance),cookie:Number(e.cookie),releasedAt:Number(e.released_at),ttlMs:Number(e.ttl_ms)}}function T(e,t){let n=e.exec(`SELECT instance, cookie, released_at, ttl_ms
13
+ FROM __doync_release_stamp WHERE instance = ?`,t)[0];return n===void 0?null:w(n)}function E(e){return e.exec(`SELECT instance, cookie, released_at, ttl_ms FROM __doync_release_stamp`).map(w)}function D(t){if(!(0,e.isSqlIdentifier)(t))throw Error(`doync: refusing to quote non-identifier ${JSON.stringify(t)}`);return`"${t}"`}function O(t,n){let r=JSON.parse(t),i=(0,e.tablePrimaryKey)(n);if(!Array.isArray(r)||r.length!==i.length)throw Error(`doync: malformed pk ${t} for table ${n.name} (expected ${i.length} key part(s))`);return r.map(t=>(0,e.decodePkValue)(t))}function k(t){return(0,e.tablePrimaryKey)(t).map(e=>`${D(e)} = ?`).join(` AND `)}function se(t,n){let r=Object.keys(n);for(let n of r)if(!(0,e.isSqlIdentifier)(n))throw Error(`doync: patch image for table ${t.name} has an invalid column name ${JSON.stringify(n)}`);return{columns:r,values:r.map(t=>(0,e.decodeImageValue)(n[t]))}}function A(e,t){let n=e.tables.find(e=>e.name===t);if(n===void 0)throw Error(`doync: no synced table named ${JSON.stringify(t)} in the client schema`);return n}function j(e){return ie(e)}function M(t,n){g(t,`schema_version`)===null&&N(t,n,0,(0,e.declaredSchemaVersion)(n))}function N(e,t,n,r){for(let i of h(t,n,r))e.execBatch(i);_(e,`schema_version`,String(r))}function P(e,t){for(let n of t.tables)e.exec(`DROP TABLE IF EXISTS ${D(n.name)}`)}function ce(t){for(let n of t.tables)if((0,e.isInternalTable)(n.name))throw Error(`doync: synced table ${n.name} uses the reserved __doync_ prefix`)}const F=`logout_behavior`,I=`connected_clock`,L=`doync_optimistic`,R=`doync_body`;var z=class{lifecycle;decode;one;#e=!1;#t=null;#n=new Set;#r=Y;#i=X;#a=!1;#o=null;constructor(e,t,n){this.lifecycle=e,this.decode=t,this.one=n}#s(e){if(this.#o?.read!==e||this.#o.generation!==e.generation){let t=e.rawRows();this.#r=this.decode?this.decode(t):t,this.#o={read:e,generation:e.generation}}return this.#r}#c(){if(this.#a)return;this.#a=!0;let e=this.lifecycle.peek();if(e!==null)this.#s(e),this.#i=e.status();else{let e=this.lifecycle.compute();this.#r=e.rows,this.#i=e.status}}current(){let e=this.lifecycle.peek();return e!==null&&e.generation>0?(this.#a=!0,this.#s(e)):(this.#c(),this.#r)}status(){let e=this.lifecycle.peek();return e===null?(this.#c(),this.#i):(this.#a=!0,this.#i=e.status(),this.#i)}onChange(e){return this.#n.add(e),()=>this.#n.delete(e)}retain(){if(this.#e)return;this.#e=!0,this.#a=!0;let e=this.lifecycle.retain();if(this.#t=e.onChange(()=>{this.#s(e),this.#i=e.status();for(let e of this.#n)e()}),e.generation>0&&(this.#o?.read!==e||this.#o.generation!==e.generation)||e.status()!==this.#i){e.generation>0&&this.#s(e),this.#i=e.status();for(let e of this.#n)e()}}release(){this.#e&&(this.#e=!1,this.#t?.(),this.#t=null,this.lifecycle.release())}},B=class{#e;#t;#n;#r;#i;#a;#o=null;#s;#c;#l=1;#u=null;#d=!1;#f=0;#p=null;#m;#h=!1;#g=null;#_=!1;#v=new Set;#y=0;#b=0;#x=null;#S;#C=new Set;#w=!1;#T=[];#E=new Map;#D=new Map;#O=new Set;#k=new Set;#A=new Map;#j=new Map;#M=new Map;#N=new Map;#P=new Map;#F=1;#I=new Map;#L=new Map;#R=new Set;#z=null;#B=[];#V=!1;#H=[];#U=new Set;constructor(e){this.#e=e.db,this.#t=e.schema,this.#n=e.mutations,this.#r=e.socket,this.#i=e.ctx??{},this.#a=e.token,this.#o=e.userId??null,this.#S=e.onSchemaEvent,this.#c=e.generateId??_e,this.#m=e.now??Date.now,this.#W(e),this.#r.setHandlers({message:e=>this.#Ee(e),open:()=>this.#xe(),close:()=>{this.#p=null,this.#tt(!1)},status:e=>this.#nt(e)})}get connectedClock(){return this.#f}releaseStamp(e){return T(this.#e,e)}releaseStamps(){return E(this.#e)}get clientId(){return this.#s}get userId(){return this.#o}get cookie(){return this.#u}get errors(){return this.#B}get recoveredKeys(){return this.#O}get schemaVersion(){return this.#y}get schemaStatus(){return this.#x}#W(t){let n=this.#e;n.exec(`PRAGMA foreign_keys = OFF`),ce(this.#t);let{rolledBack:r}=j(n);r?(P(n,this.#t),n.exec(`DELETE FROM __doync_membership`),_(n,`schema_version`,`0`),N(n,this.#t,0,(0,e.declaredSchemaVersion)(this.#t)),this.#u=null,n.exec(`DELETE FROM __doync_meta WHERE k = ?`,`cookie`)):M(n,this.#t),this.#y=(0,e.declaredSchemaVersion)(this.#t);let i=ye(n);i!==null&&(console.error(`doync: durable meta "${i}" is corrupt — healing by FORGETTING the local store (fresh identity, full re-hydration). A corrupt mutation counter makes the pending queue untrustworthy, so unsynced writes are discarded rather than risk double-apply.`),this.#Ge(n)),this.#b=this.#G(n,`schema_version`)??0;let a=g(n,`client_id`);this.#s=a??t.clientId??this.#c(),a===null&&_(n,`client_id`,this.#s),this.#l=this.#G(n,`next_mutation_id`)??1,this.#u=this.#G(n,`cookie`);let o=Number(g(n,I));Number.isInteger(o)&&o>=0?this.#f=o:(g(n,`connected_clock`)!==null&&console.warn(`doync: durable connected-clock reading is corrupt — resetting to 0 (clock runs slow; GC defers, server full-hydrates)`),this.#f=0),this.#p=null,i!==null&&this.#Ze(`forget`,`corrupt durable meta "${i}" — forgot the local store and reset to a fresh identity`),this.#T=n.exec(`SELECT mutation_id, name, args, idem_key FROM __doync_pending ORDER BY mutation_id ASC`).map(t=>{let n=JSON.parse(String(t.args));return{mutationId:Number(t.mutation_id),name:String(t.name),wireArgs:n,args:(0,e.decodeArgs)(n,`mutation args`),key:t.idem_key===null?void 0:String(t.idem_key)}});for(let e of this.#T){let t=this.#ct(e.mutationId,!0);e.key!==void 0&&(this.#D.set(e.key,t.pair),this.#O.add(e.key))}this.#ge({includeStampless:!1}),n.drainWrittenTables();let s=this.#Q();V(s)&&(this.#V=!0,s.then(()=>this.#J(),()=>this.#J()))}#G(e,t){let n=g(e,t);if(n===null)return null;let r=Number(n);if(!Number.isInteger(r))throw Error(`doync: durable meta "${t}" is corrupt (${JSON.stringify(n)} → ${r}) — a non-integer must never ride the wire (ADR-0019 next-id trap); the boot-time corrupt-meta heal should have caught this`);return r}#K(e){if(this.#V){this.#H.push(e);return}this.#V=!0,this.#q(e)}#q(e){let t;try{t=e()}catch(e){throw this.#V=!1,this.#Y(),e}V(t)?t.then(()=>this.#J(),e=>{this.#B.push(`doync: exclusive-chain step rejected — ${q(e)}`),this.#J()}):this.#J()}#J(){this.#V=!1,this.#Y()}#Y(){let e=this.#H.shift();if(e!==void 0){this.#V=!0,this.#q(e);return}this.#X()}#X(){if(this.#U.size===0)return;let e=[...this.#U];this.#U.clear();for(let t of e)t.recompute()&&t.notify()}#Z(e){this.#V?this.#U.add(e):e.recompute()}#Q(){return this.#e.drainWrittenTables(),this.#e.exec(`SAVEPOINT ${L}`),this.#_=!0,this.#A.clear(),H(this.#$(0),()=>{this.#R=G(this.#e.drainWrittenTables())})}#$(e){for(let t=e;t<this.#T.length;t++){let e=this.#ne(this.#T[t]);if(V(e))return e.then(()=>this.#$(t+1))}}#ee(){this.#_&&=(this.#e.exec(`ROLLBACK TO ${L}`),this.#e.exec(`RELEASE ${L}`),!1)}#te(e){let t=this.#R;this.#ee(),this.#e.drainWrittenTables(),this.#e.exec(`BEGIN`);try{e(),this.#e.exec(`COMMIT`)}catch(e){return this.#e.exec(`ROLLBACK`),H(this.#Q(),()=>{throw e})}let n=G(this.#e.drainWrittenTables());return H(this.#Q(),()=>new Set([...t,...n,...this.#R]))}#ne(e){this.#e.exec(`SAVEPOINT ${R}`);let t;try{let n=this.#n[e.name];if(n===void 0)throw Error(`unknown mutation "${e.name}"`);t=r(n).handler(e.args,this.#i,this.#ae())}catch(t){this.#ie(e.mutationId,t);return}if(V(t))return t.then(()=>this.#re(e.mutationId),t=>this.#ie(e.mutationId,t));this.#re(e.mutationId)}#re(e){this.#e.exec(`RELEASE ${R}`),this.#A.delete(e)}#ie(e,t){this.#e.exec(`ROLLBACK TO ${R}`),this.#e.exec(`RELEASE ${R}`),this.#A.set(e,t)}#ae(){return{exec:(e,...t)=>this.#e.exec(e,...t)}}#oe(e){this.#r.send({type:`push`,clientId:this.#s,mutationId:e.mutationId,name:e.name,args:e.wireArgs})}mutate(t,n,a){let o=a?.key;if(o!==void 0){let e=this.#D.get(o);if(e!==void 0)return e}let s=t.name;if(s===void 0||s===``)return K(Error(`doync: mutate requires a registered mutation — wrap it in defineMutations(...) so it carries a dotted name`));let c=this.#n[s];if(c===void 0)return K(Error(`doync: unknown mutation "${s}"`));let l=r(c),u,d;try{(0,e.assertWireRepresentable)(n,`mutation args`,{allowBinary:!0});let t=(0,e.canonicalizeArgsBinary)(n);u=l.args===void 0?t:i(l.args,t),d=(0,e.encodeArgs)(u,`mutation args`)}catch(e){return K(e)}let f=this.#lt(!1);return o!==void 0&&this.#D.set(o,f.pair),this.#K(()=>this.#se({name:s,args:u,wireArgs:d,key:o,promise:f})),f.pair}#se(e){let t=this.#e,n=this.#l;this.#E.set(n,e.promise);let r={mutationId:n,name:e.name,args:e.args,wireArgs:e.wireArgs,key:e.key};return H(this.#te(()=>{t.exec(`INSERT INTO __doync_pending (mutation_id, name, args, idem_key) VALUES (?, ?, ?, ?)`,n,r.name,JSON.stringify(r.wireArgs??null),r.key??null),this.#l=n+1,_(t,`next_mutation_id`,String(this.#l)),this.#T.push(r)}),e=>this.#ce(r,e))}#ce(e,t){let n=e.mutationId;if(!this.#A.has(n)){this.#ut(n,null),this.#at(t),this.#d&&this.#oe(e);return}let r=this.#A.get(n),i=this.#E.get(n)?.pair;return e.key!==void 0&&this.#D.delete(e.key),H(this.#le(n,!0),e=>{this.#ut(n,r),this.#dt(n,r),i?.client.catch(()=>{}),i?.server.catch(()=>{}),this.#at(new Set([...t,...e]))})}#le(e,t=!1){let n=this.#e;return this.#te(()=>{n.exec(`DELETE FROM __doync_pending WHERE mutation_id = ?`,e),this.#T=this.#T.filter(t=>t.mutationId!==e),t&&(this.#l=e,_(n,`next_mutation_id`,String(e)))})}updateAuth(e,t,n){this.#a=e??void 0,n!==void 0&&(this.#o=n),t!==void 0&&(this.#i=t),this.#d&&!this.#w&&typeof e==`string`&&e!==``&&this.#r.send({type:`updateAuth`,jwt:e})}subscribe(e,t){if(U(e))return new ue;let{query:n,args:r,options:i}=W(`subscribe`,e,t);if(i?.skip)return new ue;let a=n.name,o=n.resolve({args:r,ctx:this.#i}),s={sql:o.sql,params:o.params},c=o.identity,l={name:a,args:r,ttl:i?.ttl,statement:s},u=o.decode;return new z({peek:()=>this.#M.get(c)?.read??null,retain:()=>this.#de(c,l),release:()=>this.#pe(c),compute:()=>{if(this.#V)return{rows:Y,status:X};let e=this.#e.exec(s.sql,...s.params),t=this.#M.get(c);return{rows:u?u(e):e,status:Q(t?.phase??`pending`,t?.error)}}},u,o.one??!1)}preload(e,t){if(U(e))return he;let{query:n,args:r,options:i}=W(`preload`,e,t),a=n.name,o=n.resolve({args:r,ctx:this.#i}),s=o.identity;this.#fe(s,{name:a,args:r,ttl:i?.ttl,statement:{sql:o.sql,params:o.params}});let c=!1;return{cleanup:()=>{c||(c=!0,this.#pe(s))}}}#ue(e,t){let n=this.#M.get(e);if(n===void 0){let r=this.#Te(e);n={name:t.name,args:t.args,ttl:t.ttl,count:0,read:null,phase:`pending`,error:void 0},this.#M.set(e,n),this.#We(()=>S(this.#e,e)),this.#d&&this.#r.send({type:`subscribe`,queries:[this.#we(e,{heldAt:r})]})}else n.phase===`error`&&this.#d?(this.#st(e,`pending`),this.#r.send({type:`subscribe`,queries:[this.#we(e)]})):t.ttl!==void 0&&(n.ttl===void 0||t.ttl>n.ttl)&&(n.ttl=t.ttl,this.#d&&this.#r.send({type:`subscribe`,queries:[this.#we(e)]}));return n.count+=1,n}#de(e,t){let n=this.#ue(e,t);if(n.read===null){let r=this.#L.get(e);if(r!==void 0)clearTimeout(r.timer),this.#L.delete(e),n.read=r.read,this.#Z(r.read);else{let r=new a({run:e=>this.#e.exec(e.sql,...e.params),readSet:()=>{let t=this.#j.get(e);return t===void 0?void 0:new Set(t.readSet)}},t.statement,e,Q(n.phase,n.error));n.read=r,this.#Z(r)}}return n.read}#fe(e,t){this.#ue(e,t)}#pe(t){let n=this.#M.get(t);n===void 0||--n.count>0||(this.#M.delete(t),this.#d&&this.#r.send({type:`unsubscribe`,queries:[{name:n.name,args:n.args===void 0?void 0:(0,e.encodeArgs)(n.args,`query args`)}]}),this.#me(t,n.ttl),n.read!==null&&this.#ve(t,n.read))}#me(e,t){let n=this.#u;if(n===null)return;let r={instance:e,cookie:n,releasedAt:this.#f,ttlMs:ae(t)};this.#K(()=>H(this.#te(()=>{oe(this.#e,r),this.#ge({includeStampless:!0}),this.#he()}),e=>this.#at(e)))}#he(){_(this.#e,I,String(this.#f))}#ge(e){let t=this.#_e(e.includeStampless);if(t.length!==0){for(let e of t)this.#Le(e);this.#he()}}#_e(e){let t=this.#M,n=E(this.#e),r=new Set(n.map(e=>e.instance)),i=[];if(e){let e=this.#e.exec(`SELECT DISTINCT instance FROM __doync_membership`);for(let n of e){let e=String(n.instance);t.has(e)||r.has(e)||i.push(e)}}let a=this.#f;for(let e of n)t.has(e.instance)||(e.ttlMs<=0||a-e.releasedAt>e.ttlMs)&&i.push(e.instance);return i}#ve(e,t){let n=this.#L.get(e);n!==void 0&&clearTimeout(n.timer),this.#L.set(e,{read:t,timer:setTimeout(()=>{let n=this.#L.get(e);n===void 0||n.read!==t||(this.#L.delete(e),this.#U.delete(t))},J)})}once(t){if(U(t))return new me;let{query:n,args:r}=W(`once`,t),i=n.name,a=n.resolve({args:r,ctx:this.#i}),o;if(this.#V)o=[];else{let e=this.#e.exec(a.sql,...a.params);o=a.decode?a.decode(e):e}let s=`once-${this.#F++}`,c=r===void 0?void 0:(0,e.encodeArgs)(r,`query args`),l=new de(o,a.decode,{start:()=>{this.#N.set(s,{view:l,name:i,args:c}),this.#d&&this.#r.send({type:`once`,id:s,name:i,args:c})},drop:()=>{this.#N.delete(s),this.#P.delete(s)}});return l}local(e,...t){let n=`local:${e}\u0000${JSON.stringify(t)}`,r={sql:e,params:t};return new z({peek:()=>this.#I.get(n)?.read??null,retain:()=>this.#ye(n,r),release:()=>this.#be(n),compute:()=>this.#V?{rows:Y,status:X}:{rows:this.#e.exec(r.sql,...r.params),status:Z}},void 0,void 0)}#ye(e,t){let n=this.#I.get(e);if(n===void 0&&(n={count:0,read:null},this.#I.set(e,n)),n.count+=1,n.read===null){let r=this.#L.get(e);if(r!==void 0)clearTimeout(r.timer),this.#L.delete(e),n.read=r.read,this.#Z(r.read);else{let e=new a({run:e=>this.#e.exec(e.sql,...e.params),readSet:()=>void 0},t,void 0,Z);n.read=e,this.#Z(e)}}return n.read}#be(e){let t=this.#I.get(e);t===void 0||--t.count>0||(this.#I.delete(e),t.read!==null&&this.#ve(e,t.read))}#xe(){this.#w||(this.#p=null,this.#tt(!0),this.#Ce())}#Se(){let e=this.#m();if(this.#p!==null){let t=e-this.#p;t>0&&(this.#f+=t)}this.#p=e}#Ce(){for(let e of this.#M.keys())this.#st(e,`pending`);this.#r.send({type:`connect`,clientId:this.#s,...this.#a===void 0?{}:{jwt:this.#a},cookie:this.#u,desiredQueries:[...this.#M.keys()].map(e=>this.#we(e)),schemaVersion:this.#y});for(let e of this.#T)this.#oe(e);for(let[e,t]of this.#N)this.#r.send({type:`once`,id:e,name:t.name,args:t.args})}#we(t,n){let r=this.#M.get(t),i=n===void 0?this.#Te(t):n.heldAt;return{name:r?.name??t,args:r?.args===void 0?void 0:(0,e.encodeArgs)(r.args,`query args`),...r?.ttl===void 0?{}:{ttl:r.ttl},...i===void 0?{}:{heldAt:i}}}#Te(e){if(!(this.#e.exec(`SELECT 1 FROM __doync_membership WHERE instance = ? LIMIT 1`,e).length>0))return;if(this.#M.has(e))return this.#u??void 0;let t=T(this.#e,e);if(t!==null)return t.cookie}#Ee(e){if(this.#w){e.type===`error`&&this.#B.push(e.message);return}if(e.type===`pong`){this.#Se();return}if(e.type===`unauthorized`){this.#B.push(e.message),this.#rt(!0);return}switch(this.#h&&e.type!==`error`&&this.#rt(!1),e.type){case`schemaSkew`:this.#Be(e);break;case`schema`:this.#Ve(e);break;case`subscribeAck`:this.#Qe();for(let t of e.instances){this.#j.set(t.instance,t);let e=this.#M.get(t.instance);e!==void 0&&e.phase!==`complete`&&this.#st(t.instance,`acked`)}break;case`pokeStart`:this.#Qe(),this.#z=[];break;case`pokePart`:this.#z===null?this.#B.push(`doync: pokePart arrived with no open poke`):this.#z.push(...e.patches);break;case`pokeEnd`:this.#z===null?this.#B.push(`doync: pokeEnd arrived with no open poke`):this.#Ae(e.cookie,e.lastMutationId,e.confirms);break;case`pokeReject`:this.#ze(e.mutationId,e.error);break;case`onceStart`:this.#P.set(e.id,[]);break;case`oncePart`:{let t=this.#P.get(e.id);t===void 0?this.#B.push(`doync: oncePart arrived with no open once`):t.push(...e.rows);break}case`onceEnd`:this.#ke(e);break;case`resyncRequired`:this.#B.push(e.message),this.#K(()=>this.#Ye(`heldAt ahead of vouched baseline (instance ${e.instance}: heldAt=${e.heldAt} baseline=${e.baseline}) — ${e.message}`));break;case`error`:if(this.#B.push(e.message),be(e.message)){this.#K(()=>this.#Ye(`cookie above the Origin head (server reset) — ${e.message}`));break}e.instances===void 0?this.#Oe(e.message):this.#De(e.instances,e.message);break;default:this.#B.push(`doync: unknown ServerMessage type ${String(e.type)}`)}}#De(e,t){let n=Error(t);for(let t of e){let e=this.#M.get(t);e!==void 0&&(e.phase===`pending`||e.phase===`acked`)&&this.#st(t,`error`,n)}}#Oe(e){let t=Error(e);for(let[e,n]of this.#M)(n.phase===`pending`||n.phase===`acked`)&&this.#st(e,`error`,t)}#ke(e){let t=this.#P.get(e.id)??[];this.#P.delete(e.id);let n=this.#N.get(e.id);if(n!==void 0){if(this.#N.delete(e.id),e.error!==void 0){n.view.settleError(Error(e.error));return}n.view.deliver(t.map(ge))}}#Ae(e,t,n){let r=this.#z??[];this.#z=null,this.#K(()=>this.#je(r,e,t,n))}#je(e,t,n,r){let i=this.#e,a=[];return H(this.#te(()=>{for(let t of e)t.op===`put`&&this.#Ne(t);for(let t of e)t.op===`del`&&this.#Pe(t);for(let t of e)t.op===`pks`&&this.#Fe(t);if(n!==null){for(let e of this.#T)e.mutationId<=n&&!this.#k.has(e.mutationId)&&a.push(e.mutationId);for(let e of a)i.exec(`DELETE FROM __doync_pending WHERE mutation_id = ?`,e)}this.#u=t,_(i,`cookie`,String(t)),this.#he(),this.#T=this.#T.filter(e=>!a.includes(e.mutationId))}),e=>{for(let e of a)this.#dt(e,null);for(let e of a)this.#k.delete(e);this.#at(e),this.#Me(r)})}#Me(e){if(e!==void 0)for(let t of e){let e=this.#M.get(t);e!==void 0&&e.phase===`acked`&&this.#st(t,`complete`)}}#Ne(e){let t=this.#e,n=this.#Re(e.instance,e.level);t.exec(`INSERT INTO __doync_membership (instance, level, tbl, pk) VALUES (?, ?, ?, ?)
14
+ ON CONFLICT (instance, level, pk) DO UPDATE SET tbl = excluded.tbl`,e.instance,e.level,n.name,e.pk);let r=O(e.pk,n);t.exec(`DELETE FROM ${D(n.name)} WHERE ${k(n)}`,...r);let{columns:i,values:a}=se(n,e.image);t.exec(`INSERT INTO ${D(n.name)} (${i.map(D).join(`, `)})
15
+ VALUES (${i.map(()=>`?`).join(`, `)})`,...a)}#Pe(e){this.#Ie(e.instance,e.level,e.pk)}#Fe(e){let t=new Set(e.pks),n=this.#e.exec(`SELECT pk FROM __doync_membership WHERE instance = ? AND level = ?`,e.instance,e.level);for(let r of n){let n=String(r.pk);t.has(n)||this.#Ie(e.instance,e.level,n)}}#Ie(e,t,n){let r=this.#e,i=this.#Re(e,t);r.exec(`DELETE FROM __doync_membership WHERE instance = ? AND level = ? AND pk = ?`,e,t,n);let a=r.exec(`SELECT DISTINCT instance FROM __doync_membership WHERE tbl = ? AND pk = ?`,i.name,n);if(!a.some(e=>this.#M.has(String(e.instance)))){r.exec(`DELETE FROM ${D(i.name)} WHERE ${k(i)}`,...O(n,i));for(let e of a)this.#Le(String(e.instance))}}#Le(e){let t=this.#e,n=t.exec(`SELECT DISTINCT tbl, pk FROM __doync_membership WHERE instance = ?`,e);t.exec(`DELETE FROM __doync_membership WHERE instance = ?`,e),S(t,e);for(let e of n){let n=String(e.tbl),r=String(e.pk);if(t.exec(`SELECT 1 FROM __doync_membership WHERE tbl = ? AND pk = ? LIMIT 1`,n,r).length===0){let e=A(this.#t,n);t.exec(`DELETE FROM ${D(e.name)} WHERE ${k(e)}`,...O(r,e))}}}#Re(e,t){let n=this.#j.get(e);if(n===void 0)throw Error(`doync: patch for unknown instance ${e} — a subscribeAck must precede its hydration poke`);let r=n.levels.find(e=>e.level===t);if(r===void 0)throw Error(`doync: instance ${e} has no Level ${t} in its subscribeAck`);return A(this.#t,r.table)}#ze(e,t){this.#k.add(e),this.#K(()=>H(this.#le(e),n=>{this.#dt(e,Error(t)),this.#at(n)}))}#Be(e){let t=e.serverVersion===void 0?``:` (server v${e.serverVersion})`;e.reason===`client-stale`?(this.#Ze(`reload`,`schema client-stale: bundle v${this.#y} is behind the Mirror${t} — reload for the new bundle`),this.#et()):(this.#Ze(`server-behind`,`schema client-ahead: bundle v${this.#y} is ahead of the Mirror${t} — backing off until it deploys`),this.#tt(!1),this.#r.reconnect())}#Ve(e){let t=e.version;if(t>this.#y){this.#Ze(`reload`,`schema directive v${t} is above the bundled track v${this.#y} — reload for the new bundle`),this.#et();return}t<=this.#b||this.#K(()=>this.#He(t))}#He(e){let t=t=>`local migration to schema v${e} failed: ${q(t)}`,n;try{n=this.#te(()=>{N(this.#e,this.#t,this.#b,e)})}catch(e){return this.#Ye(t(e))}return le(n,t=>{this.#b=e,this.#at(new Set([...t,...this.#Xe()]))},e=>this.#Ye(t(e)))}resync(){this.#K(()=>this.#Ye(`resync() requested by the consumer`))}forget(){this.#K(()=>this.#Ke(`forget() requested by the consumer`))}setLogoutBehavior(e){this.#Ue(F,e)}#Ue(e,t){this.#We(()=>_(this.#e,e,t))}#We(e){this.#K(()=>H(this.#te(e),()=>{}))}#Ge(e){P(e,this.#t),e.exec(`DELETE FROM __doync_membership`),C(e),e.exec(`DELETE FROM __doync_pending`),e.exec(`DELETE FROM __doync_meta`),N(e,this.#t,0,this.#y)}#Ke(e){let t=this.#e,n=this.#te(()=>{this.#Je(Error(`doync: ${e} — pending writes were forgotten`)),this.#Ge(t),this.#s=this.#c(),_(t,`client_id`,this.#s),this.#u=null,this.#l=1,this.#f=0,this.#p=null,this.#T=[]});return this.#qe(n,`forget`,`forgot the local store — ${e}`)}#qe(e,t,n){return H(e,e=>{this.#b=this.#y,this.#j.clear(),this.#Ze(t,n),this.#d&&this.#Ce(),this.#at(new Set([...e,...this.#Xe()]))})}#Je(e){for(let t of this.#E.keys())this.#ut(t,e),this.#dt(t,e);this.#D.clear(),this.#O.clear(),this.#k.clear(),this.#A.clear()}#Ye(e){let t=this.#e,n=this.#te(()=>{P(t,this.#t),t.exec(`DELETE FROM __doync_membership`),C(t),_(t,`schema_version`,`0`),N(t,this.#t,0,this.#y),this.#u=null,t.exec(`DELETE FROM __doync_meta WHERE k = ?`,`cookie`)});return this.#qe(n,`resync`,`wiped and resyncing — ${e}`)}#Xe(){return this.#t.tables.map(e=>e.name)}onSchemaChange(e){return this.#C.add(e),()=>this.#C.delete(e)}#Ze(e,t){let n={kind:e,message:t};this.#x=n,this.#S?.(n),this.#$e()}#Qe(){let e=this.#x?.kind;(e===`server-behind`||e===`resync`||e===`forget`)&&(this.#x=null,this.#$e())}#$e(){for(let e of this.#C)e()}#et(){this.#w=!0,this.#tt(!1)}get connectionStatus(){return this.#h?`needs-auth`:this.#d?`connected`:this.#g??`disconnected`}onConnectionChange(e){return this.#v.add(e),()=>this.#v.delete(e)}#tt(e){this.#it(()=>{this.#d=e,e&&(this.#g=null)})}#nt(e){this.#it(()=>{this.#g=e})}#rt(e){this.#it(()=>{this.#h=e})}#it(e){let t=this.connectionStatus;if(e(),this.connectionStatus!==t)for(let e of this.#v)e()}#at(e){let t=G(e),n=new Set(this.#U);this.#U.clear();for(let e of this.#ot()){let r=e.readSet;(n.has(e)||r===void 0||[...r].some(e=>t.has(e)))&&e.recompute()&&e.notify()}}*#ot(){for(let e of this.#M.values())e.read!==null&&(yield e.read);for(let e of this.#I.values())e.read!==null&&(yield e.read)}#st(e,t,n){let r=this.#M.get(e);if(r===void 0)return;r.phase=t,r.error=n;let i=Q(t,n);r.read!==null&&r.read.updateStatus(i)&&r.read.notify()}#ct(e,t){let n=this.#E.get(e);if(n!==void 0)return n;let r=this.#lt(t);return this.#E.set(e,r),r}#lt(e){let t,n,r,i,a=new Promise((e,r)=>{t=e,n=r}),o=new Promise((e,t)=>{r=e,i=t});return e&&(a.catch(()=>{}),o.catch(()=>{})),{pair:{client:a,server:o},resolveClient:t,rejectClient:n,resolveServer:r,rejectServer:i,clientSettled:!1,serverSettled:!1}}#ut(e,t){let n=this.#E.get(e);if(n===void 0){this.#B.push(`doync: settleClient for unknown mutation ${e} — no pending promise (a mutate() awaiting it would hang)`);return}n.clientSettled||(n.clientSettled=!0,t===null?n.resolveClient():n.rejectClient(t))}#dt(e,t){let n=this.#E.get(e);if(n===void 0){this.#B.push(`doync: settleServer for unknown mutation ${e} — no pending promise (a mutate() awaiting it would hang)`);return}n.serverSettled||(n.serverSettled=!0,t===null?n.resolveServer():n.rejectServer(t),n.clientSettled&&this.#E.delete(e))}};function V(e){return e instanceof Promise}function H(e,t){return e instanceof Promise?e.then(t):t(e)}function le(e,t,n){return e instanceof Promise?e.then(t,n):t(e)}var ue=class{current(){return Y}status(){return X}onChange(){return()=>{}}retain(){}release(){}},de=class{#e;#t=new Set;#n=!1;#r=!1;#i=Promise.withResolvers();#a=null;#o;#s;#c;constructor(e,t,n){this.#e=e,this.#o=t,this.#s=n.start,this.#c=n.drop}current(){return this.#l(),this.#e}onChange(e){return this.#l(),this.#t.add(e),()=>this.#t.delete(e)}get server(){return this.#l(),this.#i.promise}deliver(e){if(this.#r)return;let t=this.#o?this.#o(e):e;this.#e=t,this.#i.resolve(t);for(let e of this.#t)e()}settleError(e){this.#r||this.#i.reject(e)}dispose(){this.#r||this.#a===null&&(this.#a=setTimeout(()=>{this.#a=null,!this.#r&&(this.#r=!0,this.#t.clear(),this.#c())},J))}#l(){this.#r||(this.#a!==null&&(clearTimeout(this.#a),this.#a=null),!this.#n&&(this.#n=!0,this.#i=Promise.withResolvers(),this.#i.promise.catch(()=>{}),this.#s()))}};function U(e){return e===!1||e==null}function W(t,n,r){if((0,e.isBoundQuery)(n))return{query:n.query,args:n.args,options:r};throw fe(t,n)}function fe(e,t){return Error(typeof t==`function`?`doync: client.${e} expected a BoundQuery — received a function; did you forget to call it?`:`doync: client.${e} expected a BoundQuery, got ${pe(t)}`)}function pe(e){if(e===null)return`null`;if(Array.isArray(e))return`an array`;let t=typeof e;if(t===`object`){let t=e.kind;return typeof t==`string`?`an object with kind "${t}"`:`an object`}return t}var me=class{current(){return Y}onChange(){return()=>{}}get server(){return new Promise(()=>{})}dispose(){}};const he={cleanup(){}};function ge(t){let n={};for(let[r,i]of Object.entries(t))n[r]=(0,e.decodeImageValue)(i);return n}function G(t){return new Set([...t].filter(t=>!(0,e.isInternalTable)(t)))}function K(e){let t=Promise.reject(e),n=Promise.reject(e);return t.catch(()=>{}),n.catch(()=>{}),{client:t,server:n}}function q(e){return e instanceof Error?e.message:String(e)}const J=0,Y=Object.freeze([]),X=Object.freeze({status:`unknown`}),Z=Object.freeze({status:`complete`});function Q(e,t){return e===`complete`?Z:e===`error`?t===void 0?{status:`error`}:{status:`error`,error:t}:X}const _e=()=>{let e=globalThis.crypto?.randomUUID?.();return e===void 0?`client-${Math.random().toString(16).slice(2)}-${Date.now().toString(16)}`:e},ve=[`schema_version`,`next_mutation_id`,`cookie`];function ye(e){for(let t of ve){let n=g(e,t);if(n!==null&&!Number.isInteger(Number(n)))return t}return null}function be(e){return e.includes(`above the Origin head`)}function xe(e){return $(e)}function $(e){return e.logoutBehavior!==void 0&&(j(e.db),_(e.db,F,e.logoutBehavior)),new B({db:e.db,schema:e.schema,mutations:n(e.mutations),socket:e.socket,ctx:e.ctx??{},token:e.token,userId:e.userId??null,clientId:e.clientId,onSchemaEvent:e.onSchemaEvent})}Object.defineProperty(exports,"_",{enumerable:!0,get:function(){return p}}),Object.defineProperty(exports,"a",{enumerable:!0,get:function(){return F}}),Object.defineProperty(exports,"b",{enumerable:!0,get:function(){return n}}),Object.defineProperty(exports,"c",{enumerable:!0,get:function(){return K}}),Object.defineProperty(exports,"d",{enumerable:!0,get:function(){return P}}),Object.defineProperty(exports,"f",{enumerable:!0,get:function(){return M}}),Object.defineProperty(exports,"g",{enumerable:!0,get:function(){return m}}),Object.defineProperty(exports,"h",{enumerable:!0,get:function(){return h}}),Object.defineProperty(exports,"i",{enumerable:!0,get:function(){return I}}),Object.defineProperty(exports,"l",{enumerable:!0,get:function(){return N}}),Object.defineProperty(exports,"m",{enumerable:!0,get:function(){return _}}),Object.defineProperty(exports,"n",{enumerable:!0,get:function(){return $}}),Object.defineProperty(exports,"o",{enumerable:!0,get:function(){return U}}),Object.defineProperty(exports,"p",{enumerable:!0,get:function(){return g}}),Object.defineProperty(exports,"r",{enumerable:!0,get:function(){return B}}),Object.defineProperty(exports,"s",{enumerable:!0,get:function(){return W}}),Object.defineProperty(exports,"t",{enumerable:!0,get:function(){return xe}}),Object.defineProperty(exports,"u",{enumerable:!0,get:function(){return j}}),Object.defineProperty(exports,"v",{enumerable:!0,get:function(){return r}}),Object.defineProperty(exports,"y",{enumerable:!0,get:function(){return i}});