@doync/client 0.3.0 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapter.cjs +1 -1
- package/dist/adapter.d.cts +22 -7
- package/dist/adapter.d.cts.map +1 -1
- package/dist/adapter.d.ts +22 -7
- package/dist/adapter.d.ts.map +1 -1
- package/dist/adapter.js +1 -1
- package/dist/adapter.js.map +1 -1
- package/dist/{client-Db0Jfwuy.js → client-C97IzM56.js} +2 -2
- package/dist/{client-Db0Jfwuy.js.map → client-C97IzM56.js.map} +1 -1
- package/dist/{client-Bv6QMenB.cjs → client-CR5SoP5D.cjs} +1 -1
- package/dist/client-OAVaC3pt.d.ts.map +1 -1
- package/dist/client-OPt8Axyp.d.cts.map +1 -1
- package/dist/internal.cjs +1 -1
- package/dist/internal.js +1 -1
- package/package.json +3 -3
- package/src/engine.ts +11 -2
- package/src/socket-reconnect.ts +177 -40
package/dist/adapter.cjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("./client-
|
|
1
|
+
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("./client-CR5SoP5D.cjs");let t=require("@doync/core/internal");function n(e){let n=e.schedule??((e,t)=>{let n=setTimeout(e,t);return()=>clearTimeout(n)}),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=e.random??Math.random,c=null,l=[],u=!1,d=0,f=!1,p=!1,m=null,h=!1;function g(){let e=null;return{arm(t,r){e?.();let i=!0,a=n(()=>{i&&(i=!1,e=null,t())},r);e=()=>{i=!1,typeof a==`function`&&a()}},clear(){e?.(),e=null}}}let _=g(),v=g(),y=g(),b=g();function x(){v.clear(),y.clear()}function S(){m?.(),m=null,_.clear()}function C(t,n){c===t&&(c=null,x(),S(),u||e.onDisconnected?.({code:n,wasError:p}),u||j())}function w(e){e.onclose=null,e.onerror=null,e.onopen=null,e.onmessage=null}function T(e){c===e&&(w(e),e.close(),C(e,void 0))}function E(e){_.arm(()=>{c!==e||e.readyState!==`open`||T(e)},5e3)}function D(){S(),m=r(()=>{let e=c;e?.readyState===`open`&&(E(e),e.send(t.PING_FRAME))},o)}function O(e){v.clear(),e.readyState===`connecting`&&v.arm(()=>{c!==e||e.readyState!==`connecting`||T(e)},3e4)}function k(e){y.arm(()=>{c!==e||e.readyState!==`open`||(d=0)},5e3)}function A(){if(!(!c||c.readyState!==`open`))for(;l.length>0;){let e=l.shift();e!==void 0&&c.send(e)}}function j(){if(h)return;let e=Math.min(i*2**d,a),t=Math.min(1,Math.max(0,s())),n=e/2+e/2*t;d++,b.arm(()=>{u||h||M()},n)}function M(){if(h||c&&c.readyState!==`closed`)return;x(),S(),e.onConnecting?.();let t=e.connect();c=t,p=!1,t.onopen=()=>{c===t&&(v.clear(),k(t),f&&(l.length=0),f=!0,e.onReconnect(),A(),D())},t.onmessage=n=>{c===t&&(_.clear(),e.onMessage?.(n))},t.onerror=()=>{c===t&&(p=!0)},t.onclose=e=>{C(t,e)},O(t)}return{get isConnected(){return c?.readyState===`open`},open(){h||(u=!1,M())},close(){u=!0,S(),x(),b.clear(),c?.close(),c=null},reconnect(){if(h||c===null)return;u=!1;let t=c;c=null,S(),x(),w(t),e.onDisconnected?.({}),j(),t.close()},reconnectNow(){h||c&&c.readyState!==`closed`||(u=!1,b.clear(),M())},__simulateDisconnect(){h=!0,b.clear(),c!==null&&(u=!1,T(c))},__simulateReconnect(){h=!1,u=!1,b.clear(),M()},send(e){l.push(e),A()}}}exports.createClient=e.t,exports.createReconnectingSocket=n;
|
package/dist/adapter.d.cts
CHANGED
|
@@ -43,8 +43,8 @@ interface ReconnectingSocketConfig {
|
|
|
43
43
|
onDisconnected?(info: SocketCloseInfo): void;
|
|
44
44
|
/** Server→client frame (JSON text). */
|
|
45
45
|
onMessage?(data: string): void;
|
|
46
|
-
/** Schedule a
|
|
47
|
-
schedule?(callback: () => void, delayMs: number): void;
|
|
46
|
+
/** Schedule a timer after `delayMs`; return a canceller when supported. */
|
|
47
|
+
schedule?(callback: () => void, delayMs: number): void | (() => void);
|
|
48
48
|
/**
|
|
49
49
|
* Register a repeating keepalive tick; return a canceller (defaults to
|
|
50
50
|
* `setInterval` / `clearInterval`).
|
|
@@ -54,8 +54,13 @@ interface ReconnectingSocketConfig {
|
|
|
54
54
|
backoffBaseMs?: number;
|
|
55
55
|
/** Backoff cap in ms. Default 10_000. */
|
|
56
56
|
backoffMaxMs?: number;
|
|
57
|
-
/**
|
|
57
|
+
/**
|
|
58
|
+
* Keepalive ping period in ms while open. Each ping arms a 5s watchdog.
|
|
59
|
+
* Default 30_000.
|
|
60
|
+
*/
|
|
58
61
|
pingIntervalMs?: number;
|
|
62
|
+
/** Random source for reconnect jitter. Defaults to `Math.random`. */
|
|
63
|
+
random?: () => number;
|
|
59
64
|
}
|
|
60
65
|
/** Lifecycle handle for one reconnecting server socket. */
|
|
61
66
|
interface ReconnectingSocket {
|
|
@@ -69,16 +74,26 @@ interface ReconnectingSocket {
|
|
|
69
74
|
reconnect(): void;
|
|
70
75
|
/**
|
|
71
76
|
* Skip pending backoff and dial immediately if not open. No-op while open or
|
|
72
|
-
* dial already in flight. Use for mobile foreground
|
|
77
|
+
* dial already in flight. Use for mobile foreground lifecycle nudges.
|
|
73
78
|
*/
|
|
74
79
|
reconnectNow(): void;
|
|
80
|
+
/**
|
|
81
|
+
* @internal Test-only socket interruption that suppresses subsequent
|
|
82
|
+
* dialing.
|
|
83
|
+
*/
|
|
84
|
+
__simulateDisconnect(): void;
|
|
85
|
+
/**
|
|
86
|
+
* @internal Test-only release of the simulated interruption followed by an
|
|
87
|
+
* immediate dial.
|
|
88
|
+
*/
|
|
89
|
+
__simulateReconnect(): void;
|
|
75
90
|
/** Enqueue a client→server frame; flushed once open. */
|
|
76
91
|
send(frame: string): void;
|
|
77
92
|
}
|
|
78
93
|
/**
|
|
79
|
-
* Create a reconnecting socket coordinator: buffers sends, exponential
|
|
80
|
-
* on unexpected close, keepalive
|
|
81
|
-
* reconnect (the re-handshake resends pending pushes).
|
|
94
|
+
* Create a reconnecting socket coordinator: buffers sends, jittered exponential
|
|
95
|
+
* backoff on unexpected close, keepalive pings with a wire watchdog, and drops
|
|
96
|
+
* the stale outbox on reconnect (the re-handshake resends pending pushes).
|
|
82
97
|
*/
|
|
83
98
|
declare function createReconnectingSocket(config: ReconnectingSocketConfig): ReconnectingSocket;
|
|
84
99
|
//#endregion
|
package/dist/adapter.d.cts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"adapter.d.cts","names":[],"sources":["../src/socket-reconnect.ts"],"mappings":";;;;;;
|
|
1
|
+
{"version":3,"file":"adapter.d.cts","names":[],"sources":["../src/socket-reconnect.ts"],"mappings":";;;;;;AAWA;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;;;;;EAKhC,gBAAA,EAAkB,QAAA,cAAsB,EAAA;EAe1C;EAbE,aAAA;;EAEA,YAAA;;;;;EAKA,cAAA;;EAEA,MAAA;AAAA;;UAIe,kBAAA;EAyBV;EAAA,SAvBI,WAAA;EAsCK;EApCd,IAAA;EAsCC;EApCD,KAAA;;EAEA,SAAA;EAkCC;AAAA;;;EA7BD,YAAA;;;;;EAKA,oBAAA;;;;;EAKA,mBAAA;;EAEA,IAAA,CAAK,KAAA;AAAA;;;;;;iBAeS,wBAAA,CACd,MAAA,EAAQ,wBAAA,GACP,kBAAA"}
|
package/dist/adapter.d.ts
CHANGED
|
@@ -43,8 +43,8 @@ interface ReconnectingSocketConfig {
|
|
|
43
43
|
onDisconnected?(info: SocketCloseInfo): void;
|
|
44
44
|
/** Server→client frame (JSON text). */
|
|
45
45
|
onMessage?(data: string): void;
|
|
46
|
-
/** Schedule a
|
|
47
|
-
schedule?(callback: () => void, delayMs: number): void;
|
|
46
|
+
/** Schedule a timer after `delayMs`; return a canceller when supported. */
|
|
47
|
+
schedule?(callback: () => void, delayMs: number): void | (() => void);
|
|
48
48
|
/**
|
|
49
49
|
* Register a repeating keepalive tick; return a canceller (defaults to
|
|
50
50
|
* `setInterval` / `clearInterval`).
|
|
@@ -54,8 +54,13 @@ interface ReconnectingSocketConfig {
|
|
|
54
54
|
backoffBaseMs?: number;
|
|
55
55
|
/** Backoff cap in ms. Default 10_000. */
|
|
56
56
|
backoffMaxMs?: number;
|
|
57
|
-
/**
|
|
57
|
+
/**
|
|
58
|
+
* Keepalive ping period in ms while open. Each ping arms a 5s watchdog.
|
|
59
|
+
* Default 30_000.
|
|
60
|
+
*/
|
|
58
61
|
pingIntervalMs?: number;
|
|
62
|
+
/** Random source for reconnect jitter. Defaults to `Math.random`. */
|
|
63
|
+
random?: () => number;
|
|
59
64
|
}
|
|
60
65
|
/** Lifecycle handle for one reconnecting server socket. */
|
|
61
66
|
interface ReconnectingSocket {
|
|
@@ -69,16 +74,26 @@ interface ReconnectingSocket {
|
|
|
69
74
|
reconnect(): void;
|
|
70
75
|
/**
|
|
71
76
|
* Skip pending backoff and dial immediately if not open. No-op while open or
|
|
72
|
-
* dial already in flight. Use for mobile foreground
|
|
77
|
+
* dial already in flight. Use for mobile foreground lifecycle nudges.
|
|
73
78
|
*/
|
|
74
79
|
reconnectNow(): void;
|
|
80
|
+
/**
|
|
81
|
+
* @internal Test-only socket interruption that suppresses subsequent
|
|
82
|
+
* dialing.
|
|
83
|
+
*/
|
|
84
|
+
__simulateDisconnect(): void;
|
|
85
|
+
/**
|
|
86
|
+
* @internal Test-only release of the simulated interruption followed by an
|
|
87
|
+
* immediate dial.
|
|
88
|
+
*/
|
|
89
|
+
__simulateReconnect(): void;
|
|
75
90
|
/** Enqueue a client→server frame; flushed once open. */
|
|
76
91
|
send(frame: string): void;
|
|
77
92
|
}
|
|
78
93
|
/**
|
|
79
|
-
* Create a reconnecting socket coordinator: buffers sends, exponential
|
|
80
|
-
* on unexpected close, keepalive
|
|
81
|
-
* reconnect (the re-handshake resends pending pushes).
|
|
94
|
+
* Create a reconnecting socket coordinator: buffers sends, jittered exponential
|
|
95
|
+
* backoff on unexpected close, keepalive pings with a wire watchdog, and drops
|
|
96
|
+
* the stale outbox on reconnect (the re-handshake resends pending pushes).
|
|
82
97
|
*/
|
|
83
98
|
declare function createReconnectingSocket(config: ReconnectingSocketConfig): ReconnectingSocket;
|
|
84
99
|
//#endregion
|
package/dist/adapter.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"adapter.d.ts","names":[],"sources":["../src/socket-reconnect.ts"],"mappings":";;;;;;
|
|
1
|
+
{"version":3,"file":"adapter.d.ts","names":[],"sources":["../src/socket-reconnect.ts"],"mappings":";;;;;;AAWA;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;;;;;EAKhC,gBAAA,EAAkB,QAAA,cAAsB,EAAA;EAe1C;EAbE,aAAA;;EAEA,YAAA;;;;;EAKA,cAAA;;EAEA,MAAA;AAAA;;UAIe,kBAAA;EAyBV;EAAA,SAvBI,WAAA;EAsCK;EApCd,IAAA;EAsCC;EApCD,KAAA;;EAEA,SAAA;EAkCC;AAAA;;;EA7BD,YAAA;;;;;EAKA,oBAAA;;;;;EAKA,mBAAA;;EAEA,IAAA,CAAK,KAAA;AAAA;;;;;;iBAeS,wBAAA,CACd,MAAA,EAAQ,wBAAA,GACP,kBAAA"}
|
package/dist/adapter.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{t as e}from"./client-
|
|
1
|
+
import{t as e}from"./client-C97IzM56.js";import{PING_FRAME as t}from"@doync/core/internal";function n(e){let n=e.schedule??((e,t)=>{let n=setTimeout(e,t);return()=>clearTimeout(n)}),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=e.random??Math.random,c=null,l=[],u=!1,d=0,f=!1,p=!1,m=null,h=!1;function g(){let e=null;return{arm(t,r){e?.();let i=!0,a=n(()=>{i&&(i=!1,e=null,t())},r);e=()=>{i=!1,typeof a==`function`&&a()}},clear(){e?.(),e=null}}}let _=g(),v=g(),y=g(),b=g();function x(){v.clear(),y.clear()}function S(){m?.(),m=null,_.clear()}function C(t,n){c===t&&(c=null,x(),S(),u||e.onDisconnected?.({code:n,wasError:p}),u||j())}function w(e){e.onclose=null,e.onerror=null,e.onopen=null,e.onmessage=null}function T(e){c===e&&(w(e),e.close(),C(e,void 0))}function E(e){_.arm(()=>{c!==e||e.readyState!==`open`||T(e)},5e3)}function D(){S(),m=r(()=>{let e=c;e?.readyState===`open`&&(E(e),e.send(t))},o)}function O(e){v.clear(),e.readyState===`connecting`&&v.arm(()=>{c!==e||e.readyState!==`connecting`||T(e)},3e4)}function k(e){y.arm(()=>{c!==e||e.readyState!==`open`||(d=0)},5e3)}function A(){if(!(!c||c.readyState!==`open`))for(;l.length>0;){let e=l.shift();e!==void 0&&c.send(e)}}function j(){if(h)return;let e=Math.min(i*2**d,a),t=Math.min(1,Math.max(0,s())),n=e/2+e/2*t;d++,b.arm(()=>{u||h||M()},n)}function M(){if(h||c&&c.readyState!==`closed`)return;x(),S(),e.onConnecting?.();let t=e.connect();c=t,p=!1,t.onopen=()=>{c===t&&(v.clear(),k(t),f&&(l.length=0),f=!0,e.onReconnect(),A(),D())},t.onmessage=n=>{c===t&&(_.clear(),e.onMessage?.(n))},t.onerror=()=>{c===t&&(p=!0)},t.onclose=e=>{C(t,e)},O(t)}return{get isConnected(){return c?.readyState===`open`},open(){h||(u=!1,M())},close(){u=!0,S(),x(),b.clear(),c?.close(),c=null},reconnect(){if(h||c===null)return;u=!1;let t=c;c=null,S(),x(),w(t),e.onDisconnected?.({}),j(),t.close()},reconnectNow(){h||c&&c.readyState!==`closed`||(u=!1,b.clear(),M())},__simulateDisconnect(){h=!0,b.clear(),c!==null&&(u=!1,T(c))},__simulateReconnect(){h=!1,u=!1,b.clear(),M()},send(e){l.push(e),A()}}}export{e as createClient,n as createReconnectingSocket};
|
|
2
2
|
//# sourceMappingURL=adapter.js.map
|
package/dist/adapter.js.map
CHANGED
|
@@ -1 +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; fresh-client pin\n // change, ADR-0037). Detach before close so:\n // 1. isConnected flips immediately (async browser onclose would leave the\n // pin stale for a tick);\n // 2. a follow-up reconnectNow() can dial under the new pin without\n // no-op'ing on a still-`open` readyState (heal race, ADR-0037 / #361);\n // 3. the abandoned onclose does not null a later socket or double-schedule\n // (#319). Report the drop ourselves because that onclose is going to\n // no-op on the detached generation. No-op if no socket. Backoff is the\n // default recovery; callers that know the pin moved call reconnectNow()\n // to skip it.\n if (socket === null) return\n closedByUs = false\n const stale = socket\n socket = null\n stopPing()\n // Detach handlers so a synchronous close (test fakes) or a queued\n // browser onclose cannot double-report or double-schedule.\n stale.onclose = null\n stale.onerror = null\n stale.onopen = null\n stale.onmessage = null\n config.onDisconnected?.({})\n scheduleReconnect()\n stale.close()\n },\n reconnectNow(): void {\n // Foreground/online nudge (#185) and mid-session pin change after\n // reconnect() (ADR-0037): skip backoff and dial now. Open is a no-op\n // (caller must reconnect() first). Connecting is a no-op (a dial already\n // carries the pin for this attempt).\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,CAYhB,GAAI,IAAW,KAAM,OACrB,EAAa,GACb,IAAM,EAAQ,EACd,EAAS,KACT,EAAS,EAGT,EAAM,QAAU,KAChB,EAAM,QAAU,KAChB,EAAM,OAAS,KACf,EAAM,UAAY,KAClB,EAAO,iBAAiB,CAAC,CAAC,EAC1B,EAAkB,EAClB,EAAM,MAAM,CACd,EACA,cAAqB,CAKf,GAAU,EAAO,aAAe,WACpC,EAAa,GACb,IACA,EAAQ,EACV,EACA,KAAK,EAAqB,CACxB,EAAO,KAAK,CAAK,EACjB,EAAY,CACd,CACF,CACF"}
|
|
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, jittered exponential backoff on unexpected close, keepalive\n// pings with a wire watchdog, and drops stale outbox on reconnect (re-handshake\n// 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 timer after `delayMs`; return a canceller when supported. */\n schedule?(callback: () => void, delayMs: number): void | (() => 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 /**\n * Keepalive ping period in ms while open. Each ping arms a 5s watchdog.\n * Default 30_000.\n */\n pingIntervalMs?: number\n /** Random source for reconnect jitter. Defaults to `Math.random`. */\n random?: () => 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 lifecycle nudges.\n */\n reconnectNow(): void\n /**\n * @internal Test-only socket interruption that suppresses subsequent\n * dialing.\n */\n __simulateDisconnect(): void\n /**\n * @internal Test-only release of the simulated interruption followed by an\n * immediate dial.\n */\n __simulateReconnect(): 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\nconst WATCHDOG_TIMEOUT_MS = 5_000\nconst CONNECTION_ATTEMPT_TIMEOUT_MS = 30_000\nconst UPTIME_RESET_MS = 5_000\n\n/**\n * Create a reconnecting socket coordinator: buffers sends, jittered exponential\n * backoff on unexpected close, keepalive pings with a wire watchdog, and drops\n * the stale outbox on 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 const handle = setTimeout(cb, delay)\n return () => clearTimeout(handle)\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 const random = config.random ?? Math.random\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 /**\n * Transport error on the CURRENT socket before its close => error status vs\n * disconnected. Reset on every dial; stale-socket handlers are detached or\n * identity-guarded, so only the current socket writes it.\n */\n let errored = false\n /** Cancel keepalive interval; null when not open. */\n let cancelPing: (() => void) | null = null\n /** Suppresses all dialing while the test seam holds the socket down. */\n let __simulatedDown = false\n\n /**\n * One pending timer at a time: arming replaces the previous, clearing voids\n * it. The `active` flag makes clearing effective even for injected schedules\n * that return no canceller (#185).\n */\n function oneShotTimer() {\n let cancel: (() => void) | null = null\n return {\n arm(callback: () => void, delayMs: number): void {\n cancel?.()\n let active = true\n const c = schedule(() => {\n if (!active) return\n active = false\n cancel = null\n callback()\n }, delayMs)\n cancel = () => {\n active = false\n if (typeof c === 'function') c()\n }\n },\n clear(): void {\n cancel?.()\n cancel = null\n },\n }\n }\n\n /** Watchdog armed by the latest ping. */\n const watchdog = oneShotTimer()\n /** Timeout for the current connecting socket. */\n const attemptTimeout = oneShotTimer()\n /** Uptime gate for the current open socket. */\n const uptimeReset = oneShotTimer()\n /** Pending backoff dial. */\n const backoff = oneShotTimer()\n\n function clearDialTimers(): void {\n attemptTimeout.clear()\n uptimeReset.clear()\n }\n\n function stopPing(): void {\n cancelPing?.()\n cancelPing = null\n watchdog.clear()\n }\n\n function handleUnexpectedClose(\n sock: ReconnectSocket,\n code: number | undefined,\n ): void {\n if (socket !== sock) return\n socket = null\n clearDialTimers()\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 function detachHandlers(sock: ReconnectSocket): void {\n sock.onclose = null\n sock.onerror = null\n sock.onopen = null\n sock.onmessage = null\n }\n\n function forceUnexpectedClose(sock: ReconnectSocket): void {\n if (socket !== sock) return\n detachHandlers(sock)\n sock.close()\n handleUnexpectedClose(sock, undefined)\n }\n\n function armWatchdog(sock: ReconnectSocket): void {\n watchdog.arm(() => {\n if (socket !== sock || sock.readyState !== 'open') return\n forceUnexpectedClose(sock)\n }, WATCHDOG_TIMEOUT_MS)\n }\n\n function startPing(): void {\n stopPing()\n cancelPing = scheduleInterval(() => {\n const sock = socket\n // Exact DO keepalive bytes (ADR-0016); skip if socket closed between ticks.\n if (sock?.readyState === 'open') {\n armWatchdog(sock)\n sock.send(PING_FRAME)\n }\n }, pingIntervalMs)\n }\n\n function armAttemptTimeout(sock: ReconnectSocket): void {\n attemptTimeout.clear()\n if (sock.readyState !== 'connecting') return\n attemptTimeout.arm(() => {\n if (socket !== sock || sock.readyState !== 'connecting') return\n forceUnexpectedClose(sock)\n }, CONNECTION_ATTEMPT_TIMEOUT_MS)\n }\n\n function armUptimeReset(sock: ReconnectSocket): void {\n uptimeReset.arm(() => {\n if (socket !== sock || sock.readyState !== 'open') return\n attempt = 0\n }, UPTIME_RESET_MS)\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 if (__simulatedDown) return\n const curve = Math.min(backoffBaseMs * 2 ** attempt, backoffMaxMs)\n const sample = Math.min(1, Math.max(0, random()))\n const delay = curve / 2 + sample * (curve / 2)\n attempt++\n backoff.arm(() => {\n if (closedByUs || __simulatedDown) return\n connect()\n }, delay)\n }\n\n function connect(): void {\n if (__simulatedDown) return\n if (socket && socket.readyState !== 'closed') return\n clearDialTimers()\n stopPing()\n // Surface connecting through the whole backoff, not just first open (#102).\n config.onConnecting?.()\n const next = config.connect()\n socket = next\n 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 attemptTimeout.clear()\n armUptimeReset(next)\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 watchdog.clear()\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 handleUnexpectedClose(next, code)\n }\n armAttemptTimeout(next)\n }\n\n return {\n get isConnected(): boolean {\n return socket?.readyState === 'open'\n },\n open(): void {\n if (__simulatedDown) return\n closedByUs = false\n connect()\n },\n close(): void {\n closedByUs = true\n stopPing()\n clearDialTimers()\n // Cancel pending backoff so close after drop stays silent.\n backoff.clear()\n socket?.close()\n socket = null\n },\n reconnect(): void {\n // Engine drop-and-reopen (client-ahead skew, ADR-0020; fresh-client pin\n // change, ADR-0037). Detach before close so:\n // 1. isConnected flips immediately (async browser onclose would leave the\n // pin stale for a tick);\n // 2. a follow-up reconnectNow() can dial under the new pin without\n // no-op'ing on a still-`open` readyState (heal race, ADR-0037 / #361);\n // 3. the abandoned onclose does not null a later socket or double-schedule\n // (#319). Report the drop ourselves because that onclose is going to\n // no-op on the detached generation. No-op if no socket. Backoff is the\n // default recovery; callers that know the pin moved call reconnectNow()\n // to skip it.\n if (__simulatedDown || socket === null) return\n closedByUs = false\n const stale = socket\n socket = null\n stopPing()\n clearDialTimers()\n // Detach handlers so a synchronous close (test fakes) or a queued\n // browser onclose cannot double-report or double-schedule.\n detachHandlers(stale)\n config.onDisconnected?.({})\n scheduleReconnect()\n stale.close()\n },\n reconnectNow(): void {\n // Foreground lifecycle nudge (#185) and mid-session pin change after\n // reconnect() (ADR-0037): skip backoff and dial now. Open is a no-op\n // (caller must reconnect() first). Connecting is a no-op (a dial already\n // carries the pin for this attempt).\n if (__simulatedDown || (socket && socket.readyState !== 'closed')) return\n closedByUs = false\n // The immediate dial supersedes any pending backoff dial.\n backoff.clear()\n connect()\n },\n __simulateDisconnect(): void {\n __simulatedDown = true\n backoff.clear()\n if (socket === null) return\n // The simulated teardown reports through the unexpected-drop path.\n closedByUs = false\n forceUnexpectedClose(socket)\n },\n __simulateReconnect(): void {\n __simulatedDown = false\n closedByUs = false\n backoff.clear()\n connect()\n },\n send(frame: string): void {\n outbox.push(frame)\n flushOutbox()\n },\n }\n}\n"],"mappings":"2FAgHA,SAAgB,EACd,EACoB,CACpB,IAAM,EACJ,EAAO,YACL,EAAI,IAAU,CACd,IAAM,EAAS,WAAW,EAAI,CAAK,EACnC,UAAa,aAAa,CAAM,CAClC,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,IAC1C,EAAS,EAAO,QAAU,KAAK,OAEjC,EAAiC,KAC/B,EAAmB,CAAC,EAEtB,EAAa,GAEb,EAAU,EAKV,EAAY,GAMZ,EAAU,GAEV,EAAkC,KAElC,EAAkB,GAOtB,SAAS,GAAe,CACtB,IAAI,EAA8B,KAClC,MAAO,CACL,IAAI,EAAsB,EAAuB,CAC/C,IAAS,EACT,IAAI,EAAS,GACP,EAAI,MAAe,CAClB,IACL,EAAS,GACT,EAAS,KACT,EAAS,EACX,EAAG,CAAO,EACV,MAAe,CACb,EAAS,GACL,OAAO,GAAM,YAAY,EAAE,CACjC,CACF,EACA,OAAc,CACZ,IAAS,EACT,EAAS,IACX,CACF,CACF,CAGA,IAAM,EAAW,EAAa,EAExB,EAAiB,EAAa,EAE9B,EAAc,EAAa,EAE3B,EAAU,EAAa,EAE7B,SAAS,GAAwB,CAC/B,EAAe,MAAM,EACrB,EAAY,MAAM,CACpB,CAEA,SAAS,GAAiB,CACxB,IAAa,EACb,EAAa,KACb,EAAS,MAAM,CACjB,CAEA,SAAS,EACP,EACA,EACM,CACF,IAAW,IACf,EAAS,KACT,EAAgB,EAChB,EAAS,EAEJ,GAAY,EAAO,iBAAiB,CAAE,OAAM,SAAU,CAAQ,CAAC,EAE/D,GAAY,EAAkB,EACrC,CAEA,SAAS,EAAe,EAA6B,CACnD,EAAK,QAAU,KACf,EAAK,QAAU,KACf,EAAK,OAAS,KACd,EAAK,UAAY,IACnB,CAEA,SAAS,EAAqB,EAA6B,CACrD,IAAW,IACf,EAAe,CAAI,EACnB,EAAK,MAAM,EACX,EAAsB,EAAM,IAAA,EAAS,EACvC,CAEA,SAAS,EAAY,EAA6B,CAChD,EAAS,QAAU,CACb,IAAW,GAAQ,EAAK,aAAe,QAC3C,EAAqB,CAAI,CAC3B,EAAG,GAAmB,CACxB,CAEA,SAAS,GAAkB,CACzB,EAAS,EACT,EAAa,MAAuB,CAClC,IAAM,EAAO,EAET,GAAM,aAAe,SACvB,EAAY,CAAI,EAChB,EAAK,KAAK,CAAU,EAExB,EAAG,CAAc,CACnB,CAEA,SAAS,EAAkB,EAA6B,CACtD,EAAe,MAAM,EACjB,EAAK,aAAe,cACxB,EAAe,QAAU,CACnB,IAAW,GAAQ,EAAK,aAAe,cAC3C,EAAqB,CAAI,CAC3B,EAAG,GAA6B,CAClC,CAEA,SAAS,EAAe,EAA6B,CACnD,EAAY,QAAU,CAChB,IAAW,GAAQ,EAAK,aAAe,SAC3C,EAAU,EACZ,EAAG,GAAe,CACpB,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,GAAI,EAAiB,OACrB,IAAM,EAAQ,KAAK,IAAI,EAAgB,GAAK,EAAS,CAAY,EAC3D,EAAS,KAAK,IAAI,EAAG,KAAK,IAAI,EAAG,EAAO,CAAC,CAAC,EAC1C,EAAQ,EAAQ,EAAc,EAAQ,EAAlB,EAC1B,IACA,EAAQ,QAAU,CACZ,GAAc,GAClB,EAAQ,CACV,EAAG,CAAK,CACV,CAEA,SAAS,GAAgB,CAEvB,GADI,GACA,GAAU,EAAO,aAAe,SAAU,OAC9C,EAAgB,EAChB,EAAS,EAET,EAAO,eAAe,EACtB,IAAM,EAAO,EAAO,QAAQ,EAC5B,EAAS,EACT,EAAU,GACV,EAAK,WAAe,CAGd,IAAW,IACf,EAAe,MAAM,EACrB,EAAe,CAAI,EAEf,IAAW,EAAO,OAAS,GAC/B,EAAY,GACZ,EAAO,YAAY,EACnB,EAAY,EACZ,EAAU,EACZ,EACA,EAAK,UAAa,GAAiB,CAC7B,IAAW,IACf,EAAS,MAAM,EACf,EAAO,YAAY,CAAI,EACzB,EACA,EAAK,YAAgB,CACf,IAAW,IACf,EAAU,GACZ,EACA,EAAK,QAAW,GAAkB,CAMhC,EAAsB,EAAM,CAAI,CAClC,EACA,EAAkB,CAAI,CACxB,CAEA,MAAO,CACL,IAAI,aAAuB,CACzB,OAAO,GAAQ,aAAe,MAChC,EACA,MAAa,CACP,IACJ,EAAa,GACb,EAAQ,EACV,EACA,OAAc,CACZ,EAAa,GACb,EAAS,EACT,EAAgB,EAEhB,EAAQ,MAAM,EACd,GAAQ,MAAM,EACd,EAAS,IACX,EACA,WAAkB,CAYhB,GAAI,GAAmB,IAAW,KAAM,OACxC,EAAa,GACb,IAAM,EAAQ,EACd,EAAS,KACT,EAAS,EACT,EAAgB,EAGhB,EAAe,CAAK,EACpB,EAAO,iBAAiB,CAAC,CAAC,EAC1B,EAAkB,EAClB,EAAM,MAAM,CACd,EACA,cAAqB,CAKf,GAAoB,GAAU,EAAO,aAAe,WACxD,EAAa,GAEb,EAAQ,MAAM,EACd,EAAQ,EACV,EACA,sBAA6B,CAC3B,EAAkB,GAClB,EAAQ,MAAM,EACV,IAAW,OAEf,EAAa,GACb,EAAqB,CAAM,EAC7B,EACA,qBAA4B,CAC1B,EAAkB,GAClB,EAAa,GACb,EAAQ,MAAM,EACd,EAAQ,CACV,EACA,KAAK,EAAqB,CACxB,EAAO,KAAK,CAAK,EACjB,EAAY,CACd,CACF,CACF"}
|
|
@@ -10,7 +10,7 @@ import{declaredSchemaVersion as e,decodeArgs as t,decodeImageValue as n,decodePk
|
|
|
10
10
|
cookie = excluded.cookie,
|
|
11
11
|
released_at = excluded.released_at,
|
|
12
12
|
ttl_ms = excluded.ttl_ms`,t.instance,t.cookie,t.releasedAt,t.ttlMs)}function C(e,t){e.exec(`DELETE FROM __doync_release_stamp WHERE instance = ?`,t)}function be(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(e){if(!c(e))throw Error(`doync: refusing to quote non-identifier ${JSON.stringify(e)}`);return`"${e}"`}function O(e,t){let n=JSON.parse(e),i=u(t);if(!Array.isArray(n)||n.length!==i.length)throw Error(`doync: malformed pk ${e} for table ${t.name} (expected ${i.length} key part(s))`);return n.map(e=>r(e))}function k(e){return u(e).map(e=>`${D(e)} = ?`).join(` AND `)}function xe(e,t){let r=Object.keys(t);for(let t of r)if(!c(t))throw Error(`doync: patch image for table ${e.name} has an invalid column name ${JSON.stringify(t)}`);return{columns:r,values:r.map(e=>n(t[e]))}}function Se(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 A(e){return _e(e)}function j(t,n){h(t,`schema_version`)===null&&M(t,n,0,e(n))}function M(e,t,n,r){for(let i of de(t,n,r))e.execBatch(i);g(e,`schema_version`,String(r))}function N(e,t){for(let n of t.tables)e.exec(`DROP TABLE IF EXISTS ${D(n.name)}`)}function Ce(e){for(let t of e.tables)if(s(t.name))throw Error(`doync: synced table ${t.name} uses the reserved __doync_ prefix`)}const P=`logout_behavior`,F=`connected_clock`,I=`doync_optimistic`,L=`doync_body`;var R=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())}},z=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??null,this.#a=e.token,this.#o=e.userId??null,this.#S=e.onSchemaEvent,this.#c=e.generateId??je,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.#rt(!1)},status:e=>this.#it(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(n){let r=this.#e;r.exec(`PRAGMA foreign_keys = OFF`),Ce(this.#t);let{rolledBack:i}=A(r);i?(this.#Ge(r,e(this.#t)),this.#u=null):j(r,this.#t),this.#y=e(this.#t);let a=Ne(r);a!==null&&(console.error(`doync: durable meta "${a}" 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.#Ke(r)),this.#b=this.#G(r,`schema_version`)??0;let o=h(r,`client_id`);this.#s=o??n.clientId??this.#c(),o===null&&g(r,`client_id`,this.#s),this.#l=this.#G(r,`next_mutation_id`)??1,this.#u=this.#G(r,`cookie`);let s=Number(h(r,F));Number.isInteger(s)&&s>=0?this.#f=s:(h(r,`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,a!==null&&this.#$e(`forget`,`corrupt durable meta "${a}" — forgot the local store and reset to a fresh identity`),this.#T=r.exec(`SELECT mutation_id, name, args, idem_key FROM __doync_pending ORDER BY mutation_id ASC`).map(e=>{let n=JSON.parse(String(e.args));return{mutationId:Number(e.mutation_id),name:String(e.name),wireArgs:n,args:t(n,`mutation args`),key:e.idem_key===null?void 0:String(e.idem_key)}});for(let e of this.#T){let t=this.#ut(e.mutationId,!0);e.key!==void 0&&(this.#D.set(e.key,t.pair),this.#O.add(e.key))}this.#ge({includeStampless:!1}),r.drainWrittenTables();let c=this.#Q();B(c)&&(this.#V=!0,c.then(()=>this.#J(),()=>this.#J()))}#G(e,t){let n=h(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}B(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 ${I}`),this.#_=!0,this.#A.clear(),V(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(B(e))return e.then(()=>this.#$(t+1))}}#ee(){this.#_&&=(this.#e.exec(`ROLLBACK TO ${I}`),this.#e.exec(`RELEASE ${I}`),!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`),V(this.#Q(),()=>{throw e})}let n=G(this.#e.drainWrittenTables());return V(this.#Q(),()=>new Set([...t,...n,...this.#R]))}#ne(e){this.#e.exec(`SAVEPOINT ${L}`);let t;try{let n=this.#n[e.name];if(n===void 0)throw Error(`unknown mutation "${e.name}"`);t=n.body({args:e.args,ctx:this.#i,sql:this.#ae()})}catch(t){this.#ie(e.mutationId,t);return}if(B(t))return t.then(()=>this.#re(e.mutationId),t=>this.#ie(e.mutationId,t));this.#re(e.mutationId)}#re(e){this.#e.exec(`RELEASE ${L}`),this.#A.delete(e)}#ie(e,t){this.#e.exec(`ROLLBACK TO ${L}`),this.#e.exec(`RELEASE ${L}`),this.#A.set(e,t)}#ae(){return{exec:(e,...t)=>this.#e.exec(e,...t.map(e=>typeof e==`boolean`?+!!e:e))}}#oe(e){this.#r.send({type:`push`,clientId:this.#s,mutationId:e.mutationId,name:e.name,args:e.wireArgs})}mutate(e,t,n){let r=n?.key;if(r!==void 0){let e=this.#D.get(r);if(e!==void 0)return e}let a=e.name;if(a===void 0||a===``)return K(Error(`doync: mutate requires a registered mutation — wrap it in defineMutations(...) so it carries a dotted name`));let o=this.#n[a];if(o===void 0)return K(Error(`doync: unknown mutation "${a}"`));let s,c;try{s=l(o.args,t,`mutation args`),c=i(s,`mutation args`)}catch(e){return K(e)}let u=this.#dt(!1);return r!==void 0&&this.#D.set(r,u.pair),this.#K(()=>this.#se({name:a,args:s,wireArgs:c,key:r,promise:u})),u.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 V(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,g(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.#ft(n,null),this.#st(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),V(this.#le(n,!0),e=>{this.#ft(n,r),this.#pt(n,r),i?.client.catch(()=>{}),i?.server.catch(()=>{}),this.#st(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,g(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 H;let{query:n,args:r,options:i}=W(`subscribe`,e,t);if(i?.skip)return new H;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 R({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 ke;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(()=>C(this.#e,e)),this.#d&&this.#r.send({type:`subscribe`,queries:[this.#we(e,{heldAt:r})]})}else n.phase===`error`&&this.#d?(this.#lt(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 f({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(e){let t=this.#M.get(e);t===void 0||--t.count>0||(this.#M.delete(e),this.#d&&this.#r.send({type:`unsubscribe`,queries:[{name:t.name,args:t.args===void 0?void 0:i(t.args,`query args`)}]}),this.#me(e,t.ttl),t.read!==null&&this.#ve(e,t.read))}#me(e,t){let n=this.#u;if(n===null)return;let r={instance:e,cookie:n,releasedAt:this.#f,ttlMs:ve(t)};this.#K(()=>V(this.#te(()=>{ye(this.#e,r),this.#ge({includeStampless:!0}),this.#he()}),e=>this.#st(e)))}#he(){g(this.#e,F,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(e){if(U(e))return new Oe;let{query:t,args:n}=W(`once`,e),r=t.name,a=t.resolve({args:n,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=n===void 0?void 0:i(n,`query args`),l=new Te(o,a.decode,{start:()=>{this.#N.set(s,{view:l,name:r,args:c}),this.#d&&this.#r.send({type:`once`,id:s,name:r,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 R({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 f({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.#rt(!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.#lt(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(e,t){let n=this.#M.get(e),r=t===void 0?this.#Te(e):t.heldAt;return{name:n?.name??e,args:n?.args===void 0?void 0:i(n.args,`query args`),...n?.ttl===void 0?{}:{ttl:n.ttl},...r===void 0?{}:{heldAt:r}}}#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.#at(!0);return}switch(this.#h&&e.type!==`error`&&this.#at(!1),e.type){case`schemaSkew`:this.#Be(e);break;case`schema`:this.#Ve(e);break;case`subscribeAck`:this.#et();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.#lt(t.instance,`acked`)}break;case`pokeStart`:this.#et(),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,e.reason);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.#Ze(`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),Pe(e.message)){this.#K(()=>this.#Ze(`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.#lt(t,`error`,n)}}#Oe(e){let t=Error(e);for(let[e,n]of this.#M)(n.phase===`pending`||n.phase===`acked`)&&this.#lt(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(Ae))}}#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 V(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,g(i,`cookie`,String(t)),this.#he(),this.#T=this.#T.filter(e=>!a.includes(e.mutationId))}),e=>{for(let e of a)this.#pt(e,null);for(let e of a)this.#k.delete(e);this.#st(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.#lt(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 (?, ?, ?, ?)
|
|
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(e){if(!c(e))throw Error(`doync: refusing to quote non-identifier ${JSON.stringify(e)}`);return`"${e}"`}function O(e,t){let n=JSON.parse(e),i=u(t);if(!Array.isArray(n)||n.length!==i.length)throw Error(`doync: malformed pk ${e} for table ${t.name} (expected ${i.length} key part(s))`);return n.map(e=>r(e))}function k(e){return u(e).map(e=>`${D(e)} = ?`).join(` AND `)}function xe(e,t){let r=Object.keys(t);for(let t of r)if(!c(t))throw Error(`doync: patch image for table ${e.name} has an invalid column name ${JSON.stringify(t)}`);return{columns:r,values:r.map(e=>n(t[e]))}}function Se(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 A(e){return _e(e)}function j(t,n){h(t,`schema_version`)===null&&M(t,n,0,e(n))}function M(e,t,n,r){for(let i of de(t,n,r))e.execBatch(i);g(e,`schema_version`,String(r))}function N(e,t){for(let n of t.tables)e.exec(`DROP TABLE IF EXISTS ${D(n.name)}`)}function Ce(e){for(let t of e.tables)if(s(t.name))throw Error(`doync: synced table ${t.name} uses the reserved __doync_ prefix`)}const P=`logout_behavior`,F=`connected_clock`,I=`doync_optimistic`,L=`doync_body`;var R=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())}},z=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??null,this.#a=e.token,this.#o=e.userId??null,this.#S=e.onSchemaEvent,this.#c=e.generateId??je,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.#rt(!1)},status:e=>this.#it(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(n){let r=this.#e;r.exec(`PRAGMA foreign_keys = OFF`),Ce(this.#t);let{rolledBack:i}=A(r);i?(this.#Ge(r,e(this.#t)),this.#u=null):j(r,this.#t),this.#y=e(this.#t);let a=Ne(r);a!==null&&(console.error(`doync: durable meta "${a}" 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.#Ke(r)),this.#b=this.#G(r,`schema_version`)??0;let o=h(r,`client_id`);this.#s=o??n.clientId??this.#c(),o===null&&g(r,`client_id`,this.#s),this.#l=this.#G(r,`next_mutation_id`)??1,this.#u=this.#G(r,`cookie`);let s=Number(h(r,F));Number.isInteger(s)&&s>=0?this.#f=s:(h(r,`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,a!==null&&this.#$e(`forget`,`corrupt durable meta "${a}" — forgot the local store and reset to a fresh identity`),this.#T=r.exec(`SELECT mutation_id, name, args, idem_key FROM __doync_pending ORDER BY mutation_id ASC`).map(e=>{let n=JSON.parse(String(e.args));return{mutationId:Number(e.mutation_id),name:String(e.name),wireArgs:n,args:t(n,`mutation args`),key:e.idem_key===null?void 0:String(e.idem_key)}});for(let e of this.#T){let t=this.#ut(e.mutationId,!0);e.key!==void 0&&(this.#D.set(e.key,t.pair),this.#O.add(e.key))}this.#ge({includeStampless:!1}),r.drainWrittenTables();let c=this.#Q();B(c)&&(this.#V=!0,c.then(()=>this.#J(),()=>this.#J()))}#G(e,t){let n=h(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}B(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 ${I}`),this.#_=!0,this.#A.clear(),V(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(B(e))return e.then(()=>this.#$(t+1))}}#ee(){this.#_&&=(this.#e.exec(`ROLLBACK TO ${I}`),this.#e.exec(`RELEASE ${I}`),!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`),V(this.#Q(),()=>{throw e})}let n=G(this.#e.drainWrittenTables());return V(this.#Q(),()=>new Set([...t,...n,...this.#R]))}#ne(e){this.#e.exec(`SAVEPOINT ${L}`);let t;try{let n=this.#n[e.name];if(n===void 0)throw Error(`unknown mutation "${e.name}"`);t=n.body({args:e.args,ctx:this.#i,sql:this.#ae()})}catch(t){this.#ie(e.mutationId,t);return}if(B(t))return t.then(()=>this.#re(e.mutationId),t=>this.#ie(e.mutationId,t));this.#re(e.mutationId)}#re(e){this.#e.exec(`RELEASE ${L}`),this.#A.delete(e)}#ie(e,t){this.#e.exec(`ROLLBACK TO ${L}`),this.#e.exec(`RELEASE ${L}`),this.#A.set(e,t)}#ae(){return{exec:(e,...t)=>this.#e.exec(e,...t.map(e=>typeof e==`boolean`?+!!e:e))}}#oe(e){this.#r.send({type:`push`,clientId:this.#s,mutationId:e.mutationId,name:e.name,args:e.wireArgs})}mutate(e,t,n){let r=n?.key;if(r!==void 0){let e=this.#D.get(r);if(e!==void 0)return e}let a=e.name;if(a===void 0||a===``)return K(Error(`doync: mutate requires a registered mutation — wrap it in defineMutations(...) so it carries a dotted name`));let o=this.#n[a];if(o===void 0)return K(Error(`doync: unknown mutation "${a}"`));let s,c;try{s=l(o.args,t,`mutation args`),c=i(s,`mutation args`)}catch(e){return K(e)}let u=this.#dt(!1);return r!==void 0&&this.#D.set(r,u.pair),this.#K(()=>this.#se({name:a,args:s,wireArgs:c,key:r,promise:u})),u.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 V(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,g(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.#ft(n,null),this.#st(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),V(this.#le(n,!0),e=>{this.#ft(n,r),this.#pt(n,r),i?.client.catch(()=>{}),i?.server.catch(()=>{}),this.#st(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,g(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 H;let{query:n,args:r,options:i}=W(`subscribe`,e,t);if(i?.skip)return new H;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 R({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 ke;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(()=>C(this.#e,e)),this.#d&&this.#r.send({type:`subscribe`,queries:[this.#we(e,{heldAt:r})]})}else n.phase===`error`&&this.#d?(this.#lt(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 f({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(e){let t=this.#M.get(e);t===void 0||--t.count>0||(this.#M.delete(e),this.#d&&this.#r.send({type:`unsubscribe`,queries:[{name:t.name,args:t.args===void 0?void 0:i(t.args,`query args`)}]}),this.#me(e,t.ttl),t.read!==null&&this.#ve(e,t.read))}#me(e,t){let n=this.#u;if(n===null)return;let r={instance:e,cookie:n,releasedAt:this.#f,ttlMs:ve(t)};this.#K(()=>V(this.#te(()=>{ye(this.#e,r),this.#ge({includeStampless:!0}),this.#he()}),e=>this.#st(e)))}#he(){g(this.#e,F,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(e){if(U(e))return new Oe;let{query:t,args:n}=W(`once`,e),r=t.name,a=t.resolve({args:n,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=n===void 0?void 0:i(n,`query args`),l=new Te(o,a.decode,{start:()=>{this.#N.set(s,{view:l,name:r,args:c}),this.#d&&this.#r.send({type:`once`,id:s,name:r,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 R({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 f({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.#rt(!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.#lt(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(e,t){let n=this.#M.get(e),r=t===void 0?this.#Te(e):t.heldAt;return{name:n?.name??e,args:n?.args===void 0?void 0:i(n.args,`query args`),...n?.ttl===void 0?{}:{ttl:n.ttl},...r===void 0?{}:{heldAt:r}}}#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.#at(!0);return}if(e.type===`pokeStart`&&e.clientId!==this.#s){this.#B.push(`doync: pokeStart addressed to client ${e.clientId}, this engine is ${this.#s}`);return}switch(this.#h&&e.type!==`error`&&this.#at(!1),e.type){case`schemaSkew`:this.#Be(e);break;case`schema`:this.#Ve(e);break;case`subscribeAck`:this.#et();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.#lt(t.instance,`acked`)}break;case`pokeStart`:this.#et(),this.#z=[];break;case`pokePart`:if(this.#z===null)this.#B.push(`doync: pokePart arrived with no open poke`);else for(let t of e.patches)this.#z.push(t);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,e.reason);break;case`onceStart`:this.#P.set(e.id,[]);break;case`oncePart`:{let t=this.#P.get(e.id);if(t===void 0)this.#B.push(`doync: oncePart arrived with no open once`);else for(let n of e.rows)t.push(n);break}case`onceEnd`:this.#ke(e);break;case`resyncRequired`:this.#B.push(e.message),this.#K(()=>this.#Ze(`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),Pe(e.message)){this.#K(()=>this.#Ze(`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.#lt(t,`error`,n)}}#Oe(e){let t=Error(e);for(let[e,n]of this.#M)(n.phase===`pending`||n.phase===`acked`)&&this.#lt(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(Ae))}}#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 V(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,g(i,`cookie`,String(t)),this.#he(),this.#T=this.#T.filter(e=>!a.includes(e.mutationId))}),e=>{for(let e of a)this.#pt(e,null);for(let e of a)this.#k.delete(e);this.#st(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.#lt(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
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}=xe(n,e.image);t.exec(`INSERT INTO ${D(n.name)} (${i.map(D).join(`, `)})
|
|
15
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),C(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=Se(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 Se(this.#t,r.table)}#ze(e,t,n){if(this.#k.add(e),n===`reaped`){let e=this.#T.length;this.#B.push(t),this.#K(()=>this.#Ze(`server forgot this client (reaped) — dropped ${e} pending write${e===1?``:`s`} — ${t}`,`reaped`));return}this.#K(()=>V(this.#le(e),n=>{this.#pt(e,Error(t)),this.#st(n)}))}#Be(e){let t=e.serverVersion===void 0?``:` (server v${e.serverVersion})`;e.reason===`client-stale`?(this.#$e(`reload`,`schema client-stale: bundle v${this.#y} is behind the Mirror${t} — reload for the new bundle`),this.#nt()):(this.#$e(`server-behind`,`schema client-ahead: bundle v${this.#y} is ahead of the Mirror${t} — backing off until it deploys`),this.#rt(!1),this.#r.reconnect())}#Ve(e){let t=e.version;if(t>this.#y){this.#$e(`reload`,`schema directive v${t} is above the bundled track v${this.#y} — reload for the new bundle`),this.#nt();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(()=>{M(this.#e,this.#t,this.#b,e)})}catch(e){return this.#Ze(t(e))}return we(n,t=>{this.#b=e,this.#st(new Set([...t,...this.#Qe()]))},e=>this.#Ze(t(e)))}resync(){this.#K(()=>this.#Ze(`resync() requested by the consumer`))}forget(){this.#K(()=>this.#Je(`forget() requested by the consumer`))}setLogoutBehavior(e){this.#Ue(P,e)}#Ue(e,t){this.#We(()=>v(this.#e,e,t))}#We(e){this.#K(()=>V(this.#te(e),()=>{}))}#Ge(e,t=this.#y){N(e,this.#t),e.exec(`DELETE FROM __doync_membership`),be(e),e.exec(`DELETE FROM __doync_pending`),e.exec(`DELETE FROM __doync_meta`),M(e,this.#t,0,t)}#Ke(e){this.#Ge(e),e.exec(`DELETE FROM __doync_prefs`)}#qe(e){let t=this.#e;this.#Xe(e),this.#s=this.#c(),g(t,`client_id`,this.#s),this.#u=null,this.#l=1,this.#f=0,this.#p=null,this.#T=[]}#Je(e){let t=this.#e,n=this.#te(()=>{this.#Ke(t),this.#qe(Error(`doync: ${e} — pending writes were forgotten`))});return this.#Ye(n,`forget`,`forgot the local store — ${e}`)}#Ye(e,t,n){return V(e,e=>{this.#b=this.#y,this.#j.clear(),this.#$e(t,n),this.#d&&(this.#rt(!1),this.#r.reconnect()),this.#st(new Set([...e,...this.#Qe()]))})}#Xe(e){for(let t of this.#E.keys())this.#ft(t,e),this.#pt(t,e);this.#D.clear(),this.#O.clear(),this.#k.clear(),this.#A.clear()}#Ze(e,t=`resync`){let n=this.#e,r=this.#te(()=>{this.#Ge(n),this.#qe(Error(`doync: ${e} — pending writes were dropped`))});return this.#Ye(r,t,t===`reaped`?`client state was reaped — ${e}`:`wiped and resyncing — ${e}`)}#Qe(){return this.#t.tables.map(e=>e.name)}onSchemaChange(e){return this.#C.add(e),()=>this.#C.delete(e)}#$e(e,t){let n={kind:e,message:t};this.#x=n,this.#S?.(n),this.#tt()}#et(){let e=this.#x?.kind;(e===`server-behind`||e===`resync`||e===`reaped`||e===`forget`)&&(this.#x=null,this.#tt())}#tt(){for(let e of this.#C)e()}#nt(){this.#w=!0,this.#rt(!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)}#rt(e){this.#ot(()=>{this.#d=e,e&&(this.#g=null)})}#it(e){this.#ot(()=>{this.#g=e})}#at(e){this.#ot(()=>{this.#h=e})}#ot(e){let t=this.connectionStatus;if(e(),this.connectionStatus!==t)for(let e of this.#v)e()}#st(e){let t=G(e),n=new Set(this.#U);this.#U.clear();for(let e of this.#ct()){let r=e.readSet;(n.has(e)||r===void 0||[...r].some(e=>t.has(e)))&&e.recompute()&&e.notify()}}*#ct(){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)}#lt(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()}#ut(e,t){let n=this.#E.get(e);if(n!==void 0)return n;let r=this.#dt(t);return this.#E.set(e,r),r}#dt(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}}#ft(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))}#pt(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 B(e){return e instanceof Promise}function V(e,t){return e instanceof Promise?e.then(t):t(e)}function we(e,t,n){return e instanceof Promise?e.then(t,n):t(e)}var H=class{current(){return Y}status(){return X}onChange(){return()=>{}}retain(){}release(){}},Te=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(e,t,n){if(o(t))return{query:t.query,args:t.args,options:n};throw Ee(e,t)}function Ee(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 ${De(t)}`)}function De(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 Oe=class{current(){return Y}onChange(){return()=>{}}get server(){return new Promise(()=>{})}dispose(){}};const ke={cleanup(){}};function Ae(e){let t={};for(let[r,i]of Object.entries(e))t[r]=n(i);return t}function G(e){return new Set([...e].filter(e=>!s(e)))}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 je=()=>{let e=globalThis.crypto?.randomUUID?.();return e===void 0?`client-${Math.random().toString(16).slice(2)}-${Date.now().toString(16)}`:e},Me=[`schema_version`,`next_mutation_id`,`cookie`];function Ne(e){for(let t of Me){let n=h(e,t);if(n!==null&&!Number.isInteger(Number(n)))return t}return null}function Pe(e){return e.includes(`above the Origin head`)}function Fe(e){return Ie(e)}function Ie(e){e.logoutBehavior!==void 0&&(A(e.db),v(e.db,P,e.logoutBehavior));let t=$(e.authData,e.ctxValidationSchema);return new z({db:e.db,schema:e.schema,mutations:a(e.mutations),socket:e.socket,ctx:t.ctx,token:t.token,userId:t.userId,clientId:e.clientId,onSchemaEvent:e.onSchemaEvent})}function $(e,t){if(e===null)return{userId:null,token:void 0,ctx:null};let n=e.ctx;return t!==void 0&&(n=ee(t,e.ctx,`auth context`)),{userId:e.userId,token:e.token,ctx:n}}export{v as _,F as a,p as b,W as c,A as d,N as f,g,_ as h,z as i,K as l,h as m,Ie as n,P as o,j as p,$ as r,U as s,Fe as t,M as u,de as v,m as y};
|
|
16
|
-
//# sourceMappingURL=client-
|
|
16
|
+
//# sourceMappingURL=client-C97IzM56.js.map
|