@supalive/core 0.1.4 → 0.1.5

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 (212) hide show
  1. package/README.md +17 -2
  2. package/dist/helper-CiacMqje.d.ts +14 -0
  3. package/dist/helper-CiacMqje.d.ts.map +1 -0
  4. package/dist/helper-zdJT5FUc.js +58 -0
  5. package/dist/helper-zdJT5FUc.js.map +1 -0
  6. package/dist/index-CTyXKcq_.d.ts +1736 -0
  7. package/dist/index-CTyXKcq_.d.ts.map +1 -0
  8. package/dist/mysql-C8SN443m.d.ts +104 -0
  9. package/dist/mysql-C8SN443m.d.ts.map +1 -0
  10. package/dist/mysql-DELSRrpY.js +569 -0
  11. package/dist/mysql-DELSRrpY.js.map +1 -0
  12. package/dist/overlap-checker-CVkbbuEQ.js +187 -0
  13. package/dist/overlap-checker-CVkbbuEQ.js.map +1 -0
  14. package/dist/postgres-DE-XyNAQ.js +823 -0
  15. package/dist/postgres-DE-XyNAQ.js.map +1 -0
  16. package/dist/postgres-D_gwCHx-.d.ts +108 -0
  17. package/dist/postgres-D_gwCHx-.d.ts.map +1 -0
  18. package/dist/query-CbQLjeME.js +713 -0
  19. package/dist/query-CbQLjeME.js.map +1 -0
  20. package/dist/realtime_db-D9p2X2PH.js +246 -0
  21. package/dist/realtime_db-D9p2X2PH.js.map +1 -0
  22. package/dist/router-DlTYWpop.js +81 -0
  23. package/dist/router-DlTYWpop.js.map +1 -0
  24. package/dist/schema-SKcIWHfo.js +167 -0
  25. package/dist/schema-SKcIWHfo.js.map +1 -0
  26. package/dist/src/client/index.d.ts +2 -17
  27. package/dist/src/client/index.js +1151 -18
  28. package/dist/src/client/index.js.map +1 -1
  29. package/dist/src/exports/mysql.d.ts +2 -2
  30. package/dist/src/exports/mysql.js +2 -2
  31. package/dist/src/exports/postgres.d.ts +2 -2
  32. package/dist/src/exports/postgres.js +2 -2
  33. package/dist/src/exports/procedure.d.ts +3 -5
  34. package/dist/src/exports/procedure.js +5 -5
  35. package/dist/src/exports/server.d.ts +368 -2
  36. package/dist/src/exports/server.d.ts.map +1 -1
  37. package/dist/src/exports/server.js +1346 -2
  38. package/dist/src/exports/server.js.map +1 -1
  39. package/dist/src/exports/subscription-manager-worker-entry.d.ts +1 -0
  40. package/dist/src/exports/subscription-manager-worker-entry.js +115 -0
  41. package/dist/src/exports/subscription-manager-worker-entry.js.map +1 -0
  42. package/dist/src/exports/types.d.ts +4 -6
  43. package/dist/src/exports/types.js +5 -6
  44. package/dist/subscription-worker-BurdPO8d.js +511 -0
  45. package/dist/subscription-worker-BurdPO8d.js.map +1 -0
  46. package/dist/types_client_rpc-CIsjQQGG.js +105 -0
  47. package/dist/types_client_rpc-CIsjQQGG.js.map +1 -0
  48. package/dist/types_db-Ctf-ksoy.js +168 -0
  49. package/dist/types_db-Ctf-ksoy.js.map +1 -0
  50. package/dist/types_server-Ch56y8L3.d.ts +216 -0
  51. package/dist/types_server-Ch56y8L3.d.ts.map +1 -0
  52. package/dist/types_server-Dk6o_B7N.js +24 -0
  53. package/dist/types_server-Dk6o_B7N.js.map +1 -0
  54. package/package.json +3 -3
  55. package/dist/src/client/caller_client.d.ts +0 -42
  56. package/dist/src/client/caller_client.d.ts.map +0 -1
  57. package/dist/src/client/caller_client.js +0 -77
  58. package/dist/src/client/caller_client.js.map +0 -1
  59. package/dist/src/client/client.d.ts +0 -149
  60. package/dist/src/client/client.d.ts.map +0 -1
  61. package/dist/src/client/client.js +0 -308
  62. package/dist/src/client/client.js.map +0 -1
  63. package/dist/src/client/index.d.ts.map +0 -1
  64. package/dist/src/client/sub_key.d.ts +0 -20
  65. package/dist/src/client/sub_key.d.ts.map +0 -1
  66. package/dist/src/client/sub_key.js +0 -37
  67. package/dist/src/client/sub_key.js.map +0 -1
  68. package/dist/src/client/ws_client_manager.d.ts +0 -268
  69. package/dist/src/client/ws_client_manager.d.ts.map +0 -1
  70. package/dist/src/client/ws_client_manager.js +0 -1013
  71. package/dist/src/client/ws_client_manager.js.map +0 -1
  72. package/dist/src/config.d.ts +0 -117
  73. package/dist/src/config.d.ts.map +0 -1
  74. package/dist/src/config.js +0 -2
  75. package/dist/src/config.js.map +0 -1
  76. package/dist/src/db/bytes.d.ts +0 -4
  77. package/dist/src/db/bytes.d.ts.map +0 -1
  78. package/dist/src/db/bytes.js +0 -24
  79. package/dist/src/db/bytes.js.map +0 -1
  80. package/dist/src/db/cache.d.ts +0 -30
  81. package/dist/src/db/cache.d.ts.map +0 -1
  82. package/dist/src/db/cache.js +0 -153
  83. package/dist/src/db/cache.js.map +0 -1
  84. package/dist/src/db/codec.d.ts +0 -10
  85. package/dist/src/db/codec.d.ts.map +0 -1
  86. package/dist/src/db/codec.js +0 -73
  87. package/dist/src/db/codec.js.map +0 -1
  88. package/dist/src/db/context.d.ts +0 -28
  89. package/dist/src/db/context.d.ts.map +0 -1
  90. package/dist/src/db/context.js +0 -147
  91. package/dist/src/db/context.js.map +0 -1
  92. package/dist/src/db/db.d.ts +0 -154
  93. package/dist/src/db/db.d.ts.map +0 -1
  94. package/dist/src/db/db.js +0 -23
  95. package/dist/src/db/db.js.map +0 -1
  96. package/dist/src/db/mysql.d.ts +0 -120
  97. package/dist/src/db/mysql.d.ts.map +0 -1
  98. package/dist/src/db/mysql.js +0 -708
  99. package/dist/src/db/mysql.js.map +0 -1
  100. package/dist/src/db/postgres.d.ts +0 -116
  101. package/dist/src/db/postgres.d.ts.map +0 -1
  102. package/dist/src/db/postgres.js +0 -998
  103. package/dist/src/db/postgres.js.map +0 -1
  104. package/dist/src/db/query.d.ts +0 -174
  105. package/dist/src/db/query.d.ts.map +0 -1
  106. package/dist/src/db/query.js +0 -790
  107. package/dist/src/db/query.js.map +0 -1
  108. package/dist/src/db/query_definition.d.ts +0 -54
  109. package/dist/src/db/query_definition.d.ts.map +0 -1
  110. package/dist/src/db/query_definition.js +0 -47
  111. package/dist/src/db/query_definition.js.map +0 -1
  112. package/dist/src/db/realtime_db.d.ts +0 -19
  113. package/dist/src/db/realtime_db.d.ts.map +0 -1
  114. package/dist/src/db/realtime_db.js +0 -98
  115. package/dist/src/db/realtime_db.js.map +0 -1
  116. package/dist/src/db/schema.d.ts +0 -110
  117. package/dist/src/db/schema.d.ts.map +0 -1
  118. package/dist/src/db/schema.js +0 -47
  119. package/dist/src/db/schema.js.map +0 -1
  120. package/dist/src/db/types_client_rpc.d.ts +0 -157
  121. package/dist/src/db/types_client_rpc.d.ts.map +0 -1
  122. package/dist/src/db/types_client_rpc.js +0 -106
  123. package/dist/src/db/types_client_rpc.js.map +0 -1
  124. package/dist/src/db/types_db.d.ts +0 -236
  125. package/dist/src/db/types_db.d.ts.map +0 -1
  126. package/dist/src/db/types_db.js +0 -188
  127. package/dist/src/db/types_db.js.map +0 -1
  128. package/dist/src/exports/mysql.d.ts.map +0 -1
  129. package/dist/src/exports/mysql.js.map +0 -1
  130. package/dist/src/exports/postgres.d.ts.map +0 -1
  131. package/dist/src/exports/postgres.js.map +0 -1
  132. package/dist/src/exports/procedure.d.ts.map +0 -1
  133. package/dist/src/exports/procedure.js.map +0 -1
  134. package/dist/src/exports/types.d.ts.map +0 -1
  135. package/dist/src/exports/types.js.map +0 -1
  136. package/dist/src/helper.d.ts +0 -11
  137. package/dist/src/helper.d.ts.map +0 -1
  138. package/dist/src/helper.js +0 -45
  139. package/dist/src/helper.js.map +0 -1
  140. package/dist/src/logger.d.ts +0 -7
  141. package/dist/src/logger.d.ts.map +0 -1
  142. package/dist/src/logger.js +0 -47
  143. package/dist/src/logger.js.map +0 -1
  144. package/dist/src/router/index.d.ts +0 -4
  145. package/dist/src/router/index.d.ts.map +0 -1
  146. package/dist/src/router/index.js +0 -4
  147. package/dist/src/router/index.js.map +0 -1
  148. package/dist/src/router/procedure.d.ts +0 -204
  149. package/dist/src/router/procedure.d.ts.map +0 -1
  150. package/dist/src/router/procedure.js +0 -122
  151. package/dist/src/router/procedure.js.map +0 -1
  152. package/dist/src/router/router.d.ts +0 -88
  153. package/dist/src/router/router.d.ts.map +0 -1
  154. package/dist/src/router/router.js +0 -88
  155. package/dist/src/router/router.js.map +0 -1
  156. package/dist/src/server/hash.d.ts +0 -12
  157. package/dist/src/server/hash.d.ts.map +0 -1
  158. package/dist/src/server/hash.js +0 -20
  159. package/dist/src/server/hash.js.map +0 -1
  160. package/dist/src/server/hash.test.d.ts +0 -2
  161. package/dist/src/server/hash.test.d.ts.map +0 -1
  162. package/dist/src/server/hash.test.js +0 -43
  163. package/dist/src/server/hash.test.js.map +0 -1
  164. package/dist/src/server/index.d.ts +0 -9
  165. package/dist/src/server/index.d.ts.map +0 -1
  166. package/dist/src/server/index.js +0 -8
  167. package/dist/src/server/index.js.map +0 -1
  168. package/dist/src/server/overlap-checker.d.ts +0 -18
  169. package/dist/src/server/overlap-checker.d.ts.map +0 -1
  170. package/dist/src/server/overlap-checker.js +0 -247
  171. package/dist/src/server/overlap-checker.js.map +0 -1
  172. package/dist/src/server/sub_hash.d.ts +0 -23
  173. package/dist/src/server/sub_hash.d.ts.map +0 -1
  174. package/dist/src/server/sub_hash.js +0 -38
  175. package/dist/src/server/sub_hash.js.map +0 -1
  176. package/dist/src/server/subscription-manager-client.d.ts +0 -120
  177. package/dist/src/server/subscription-manager-client.d.ts.map +0 -1
  178. package/dist/src/server/subscription-manager-client.js +0 -207
  179. package/dist/src/server/subscription-manager-client.js.map +0 -1
  180. package/dist/src/server/subscription-manager-types.d.ts +0 -168
  181. package/dist/src/server/subscription-manager-types.d.ts.map +0 -1
  182. package/dist/src/server/subscription-manager-types.js +0 -94
  183. package/dist/src/server/subscription-manager-types.js.map +0 -1
  184. package/dist/src/server/subscription-manager-worker-entry.d.ts +0 -2
  185. package/dist/src/server/subscription-manager-worker-entry.d.ts.map +0 -1
  186. package/dist/src/server/subscription-manager-worker-entry.js +0 -127
  187. package/dist/src/server/subscription-manager-worker-entry.js.map +0 -1
  188. package/dist/src/server/subscription-manager-worker-protocol.d.ts +0 -40
  189. package/dist/src/server/subscription-manager-worker-protocol.d.ts.map +0 -1
  190. package/dist/src/server/subscription-manager-worker-protocol.js +0 -20
  191. package/dist/src/server/subscription-manager-worker-protocol.js.map +0 -1
  192. package/dist/src/server/subscription-manager.d.ts +0 -32
  193. package/dist/src/server/subscription-manager.d.ts.map +0 -1
  194. package/dist/src/server/subscription-manager.js +0 -249
  195. package/dist/src/server/subscription-manager.js.map +0 -1
  196. package/dist/src/server/subscription-manager.test.d.ts +0 -2
  197. package/dist/src/server/subscription-manager.test.d.ts.map +0 -1
  198. package/dist/src/server/subscription-manager.test.js +0 -610
  199. package/dist/src/server/subscription-manager.test.js.map +0 -1
  200. package/dist/src/server/subscription-worker.d.ts +0 -32
  201. package/dist/src/server/subscription-worker.d.ts.map +0 -1
  202. package/dist/src/server/subscription-worker.js +0 -190
  203. package/dist/src/server/subscription-worker.js.map +0 -1
  204. package/dist/src/server/supalive-server.d.ts +0 -158
  205. package/dist/src/server/supalive-server.d.ts.map +0 -1
  206. package/dist/src/server/supalive-server.js +0 -1048
  207. package/dist/src/server/supalive-server.js.map +0 -1
  208. package/dist/src/server/types_server.d.ts +0 -96
  209. package/dist/src/server/types_server.d.ts.map +0 -1
  210. package/dist/src/server/types_server.js +0 -36
  211. package/dist/src/server/types_server.js.map +0 -1
  212. package/dist/tsconfig.tsbuildinfo +0 -1
@@ -1,19 +1,1152 @@
1
- // ── Query definitions ─────────────────────────────────────────────────────────
2
- export { defineQuery, _resetGlobalDefs } from "../db/query_definition";
3
- // ── Core DB ───────────────────────────────────────────────────────────────────
4
- export { SupaliveDb, sleep } from "../db/realtime_db";
5
- // ── Context ───────────────────────────────────────────────────────────────────
6
- export { TxContext, DbReader, DbWriter } from "../db/context";
7
- export { DbReader as ReadContext, DbWriter as WritableContext } from "../db/context";
8
- // ── Schema ────────────────────────────────────────────────────────────────────
9
- export { defineSchema, defineComputedField } from "../db/schema";
10
- export { matchesPredicate } from "../db/query";
11
- export * from "../db/types_db";
12
- export { NO_RETRY, DEFAULT_RETRY, OccConflictError, OccAbortError, } from "../db/types_db";
13
- // ── Router types (client-safe, no getContextRegistry) ─────────────────────────
14
- export { createQueryBuilder, createMutationBuilder, createActionBuilder, } from "../router/procedure";
15
- export { router, } from "../router/router";
16
- // ── Client ────────────────────────────────────────────────────────────────────
17
- export { WsClientManager, RPCError, } from "./ws_client_manager";
18
- export { createClient, } from "./client";
1
+ import { a as createMutationBuilder, i as createActionBuilder, n as defineSchema, o as createQueryBuilder, t as defineComputedField } from "../../schema-SKcIWHfo.js";
2
+ import { n as getContextRegistry, r as router } from "../../router-DlTYWpop.js";
3
+ import { o as matchesPredicate } from "../../query-CbQLjeME.js";
4
+ import { r as supaliveStringify } from "../../helper-zdJT5FUc.js";
5
+ import { C as normalizeIdToBytes, S as bytesFromJson, _ as RawRangeReadSchema, a as DEFAULT_RETRY, b as WriteEntrySchema, c as OccAbortError, d as PointReadSchema, f as PredicateSchema, g as RawPointReadSchema, h as RangeReadSchema, i as CompareOperatorSchema, l as OccConflictError, m as QueryCacheMetadataSchema, n as BigIntSchema, o as LeafPredicateSchema, p as QueryCacheEntrySchema, r as CachedPgMetadataSchema, s as NO_RETRY, t as AndPredicateSchema, u as OrPredicateSchema, v as RawReadEntrySchema, w as normalizeToBytes, x as WriteOpSchema, y as ReadEntrySchema } from "../../types_db-Ctf-ksoy.js";
6
+ import { l as ServerMessageSchema } from "../../types_client_rpc-CIsjQQGG.js";
7
+ import { a as TxContext, i as DbWriter, n as sleep, r as DbReader, t as SupaliveDb } from "../../realtime_db-D9p2X2PH.js";
8
+ import { sha1 } from "hash-wasm";
9
+ //#region src/db/query_definition.ts
10
+ const _globalDefs = /* @__PURE__ */ new Map();
11
+ /** For test teardown only — clears the global map between suites. */
12
+ function _resetGlobalDefs() {
13
+ _globalDefs.clear();
14
+ }
15
+ /**
16
+ * Create a named, typed query definition.
17
+ *
18
+ * The definition is automatically registered in a module-level global so
19
+ * the server can call registry.defineAll() instead of listing every query
20
+ * explicitly. On the client, pass the definition to createClientRpc() to
21
+ * get a fully typed subscribe() call.
22
+ *
23
+ * Usage:
24
+ * export const projectBoardQuery = defineQuery(
25
+ * "projectBoard",
26
+ * async (ctx, params: { projectId: string }) => {
27
+ * return ctx.query(Tasks)
28
+ * .select()
29
+ * .where(f => f.eq("projectId", params.projectId))
30
+ * .get();
31
+ * }
32
+ * );
33
+ */
34
+ function defineQuery(name, fn) {
35
+ if (_globalDefs.has(name)) throw new Error(`[defineQuery] "${name}" is already defined. Each query must have a unique name across the whole application.`);
36
+ const def = {
37
+ _type: "QueryDefinition",
38
+ name,
39
+ fn
40
+ };
41
+ _globalDefs.set(name, def);
42
+ return def;
43
+ }
44
+ //#endregion
45
+ //#region src/client/sub_key.ts
46
+ /**
47
+ * Compute a compact local key for a `(procedure, input)` pair.
48
+ *
49
+ * Output shape: `procedureName#16-hex-chars`.
50
+ *
51
+ * - `procedureName#` prefix keeps DevTools / log output legible.
52
+ * - 16 hex chars = 64 bits of SHA-1. Collision probability over N concurrent
53
+ * subscriptions is ~ N²/2⁶⁵ — comfortable up to ~10k subs per session.
54
+ *
55
+ * Async because `hash-wasm` lazily inits the wasm module on first call; the
56
+ * cost is paid once per process. Subsequent calls are effectively free.
57
+ */
58
+ async function computeLocalSubKey(procedure, input) {
59
+ return `${procedure}#${(await sha1(`${procedure}::${input}`)).slice(0, 16)}`;
60
+ }
61
+ /**
62
+ * Trigger the one-time `hash-wasm` init off the critical path so the first
63
+ * real `subscribe` doesn't pay the ~5–10ms wasm-bootstrap cost. Safe to call
64
+ * any number of times; resolves once the module is ready.
65
+ */
66
+ function prewarmHasher() {
67
+ return sha1("");
68
+ }
69
+ //#endregion
70
+ //#region src/client/ws_client_manager.ts
71
+ const WS_OPEN = 1;
72
+ var RPCError = class extends Error {
73
+ code;
74
+ constructor(message, code) {
75
+ super(message);
76
+ this.name = "RPCError";
77
+ this.code = code;
78
+ }
79
+ };
80
+ var WsClientManager = class {
81
+ ws = null;
82
+ options;
83
+ lastSuccessfulConnectionTime = 0;
84
+ reconnectionTimeout;
85
+ requestId = 0;
86
+ pendingRequests = /* @__PURE__ */ new Map();
87
+ /**
88
+ * Single source of truth for live subscriptions.
89
+ *
90
+ * Keying:
91
+ * - In-flight subscribe (no subId yet): `_pending:<requestId>`.
92
+ * - Established: the server-returned canonical `subId`.
93
+ *
94
+ * Routing `sub:update` / `sub:gone` is O(1) (`get(subId)`). Subscribe-time
95
+ * dedup scans `values()` for a matching `localKey` (O(N)), which is cheap
96
+ * since subscribes happen on component mounts, not in the hot path.
97
+ */
98
+ subscriptions = /* @__PURE__ */ new Map();
99
+ queuedCalls = [];
100
+ reconnectAttempts = 0;
101
+ reconnectTimer = null;
102
+ connectionState = "disconnected";
103
+ authState = "pending";
104
+ /**
105
+ * Authenticated user id from `auth:success`. Distinct from `queryIdentity`
106
+ * (server-side cache/sub segmentation key); this is just "who am I logged
107
+ * in as" for app display purposes. Null while unauthenticated / anonymous.
108
+ */
109
+ authUserId = null;
110
+ explicitlyClosed = false;
111
+ stateListeners = /* @__PURE__ */ new Set();
112
+ lastPublicState = "disconnected";
113
+ serverInactivityMs;
114
+ lastServerMessageAt = 0;
115
+ inactivityTimer = null;
116
+ pendingConnectResolve = null;
117
+ pendingConnectReject = null;
118
+ throttleEveryN;
119
+ throttleDelayMs;
120
+ throttleCounter = 0;
121
+ throttling = false;
122
+ visibilityListener = null;
123
+ onlineListener = null;
124
+ constructor(options) {
125
+ this.options = {
126
+ reconnectionTimeout: 18e4,
127
+ reconnect: {
128
+ enabled: true,
129
+ maxAttempts: 5,
130
+ delay: 1e3,
131
+ backoff: 1.5,
132
+ ...options.reconnect
133
+ },
134
+ ...options
135
+ };
136
+ this.reconnectionTimeout = this.options.reconnectionTimeout;
137
+ this.serverInactivityMs = options.heartbeat?.serverInactivityMs ?? 6e4;
138
+ this.throttleEveryN = options.throttle?.everyN ?? 100;
139
+ this.throttleDelayMs = options.throttle?.delayMs ?? 10;
140
+ this.installEnvironmentListeners();
141
+ prewarmHasher().catch(() => {});
142
+ }
143
+ /**
144
+ * Subscribe to client state transitions. Returns an unsubscribe fn.
145
+ * The listener is called immediately with the current state.
146
+ */
147
+ onState(listener) {
148
+ this.stateListeners.add(listener);
149
+ listener(this.computePublicState());
150
+ return () => {
151
+ this.stateListeners.delete(listener);
152
+ };
153
+ }
154
+ /** Current public state. */
155
+ getPublicState() {
156
+ return this.computePublicState();
157
+ }
158
+ /**
159
+ * Connect to the WebSocket server.
160
+ * Returns a promise that resolves when connected and authenticated.
161
+ */
162
+ async connect() {
163
+ this.explicitlyClosed = false;
164
+ if (this.connectionState == "connected" || this.connectionState == "connecting") return;
165
+ this.connectionState = "connecting";
166
+ this.emitState();
167
+ return new Promise(async (resolve, reject) => {
168
+ this.pendingConnectResolve = () => {
169
+ this.pendingConnectResolve = null;
170
+ this.pendingConnectReject = null;
171
+ resolve();
172
+ };
173
+ this.pendingConnectReject = (err) => {
174
+ this.pendingConnectResolve = null;
175
+ this.pendingConnectReject = null;
176
+ reject(err);
177
+ };
178
+ try {
179
+ const ws = new (await (getWebSocket()))(new URL(this.options.url).toString());
180
+ this.ws = ws;
181
+ ws.onopen = () => {
182
+ this.startInactivityWatchdog();
183
+ };
184
+ ws.onmessage = (event) => {
185
+ const data = typeof event.data === "string" ? event.data : event.data.toString();
186
+ this.handleMessage(data);
187
+ };
188
+ ws.onclose = () => {
189
+ this.handleClose();
190
+ };
191
+ ws.onerror = (err) => {
192
+ const error = err instanceof Error ? err : new Error(`WebSocket error: `, { cause: err });
193
+ this.handleError(error);
194
+ this.pendingConnectReject?.(error);
195
+ };
196
+ } catch (err) {
197
+ this.connectionState = "disconnected";
198
+ this.emitState();
199
+ this.pendingConnectReject?.(err);
200
+ }
201
+ });
202
+ }
203
+ /**
204
+ * Disconnect from the server.
205
+ */
206
+ disconnect() {
207
+ this.explicitlyClosed = true;
208
+ this.cleanup();
209
+ const oldWs = this.ws;
210
+ this.ws = null;
211
+ if (oldWs) {
212
+ oldWs.onmessage = null;
213
+ oldWs.onerror = null;
214
+ oldWs.onopen = null;
215
+ oldWs.onclose = null;
216
+ try {
217
+ oldWs.close();
218
+ } catch {}
219
+ }
220
+ this.removeEnvironmentListeners();
221
+ this.emitState();
222
+ }
223
+ /**
224
+ * Check if connected and authenticated.
225
+ */
226
+ isReady() {
227
+ return this.connectionState == "connected" && this.authState != "pending";
228
+ }
229
+ /**
230
+ * Check if authenticated.
231
+ */
232
+ isAuthenticated() {
233
+ return this.authState == "authenticated";
234
+ }
235
+ /**
236
+ * Check if anonymouse.
237
+ */
238
+ isAnonymouse() {
239
+ return this.authState == "anonymouse";
240
+ }
241
+ /**
242
+ * The authenticated user's id (from `auth:success`), or null when
243
+ * unauthenticated / anonymous. Useful for "Logged in as …" display.
244
+ *
245
+ * Not to be confused with server-side `queryIdentity`, which is the
246
+ * cache/subscription segmentation key (resolved entirely on the server).
247
+ */
248
+ getAuthUserId() {
249
+ return this.authUserId;
250
+ }
251
+ /**
252
+ * Call a procedure (query, mutation, or action).
253
+ *
254
+ * If the client is not ready (disconnected or still authenticating), the
255
+ * call is FIFO-queued and dispatched on the next ready state. The promise
256
+ * still resolves with the eventual server response.
257
+ *
258
+ * Pass `onSend` to be notified when the call actually leaves the queue
259
+ * (used by `useMutation` to flip `state` from "queued" → "loading").
260
+ */
261
+ async call(procedure, input, options) {
262
+ if (!this.isReady()) return new Promise((resolve, reject) => {
263
+ this.queuedCalls.push({
264
+ procedure,
265
+ input,
266
+ resolve,
267
+ reject,
268
+ onSend: options?.onSend
269
+ });
270
+ if (!this.explicitlyClosed && this.connectionState === "disconnected") this.connect().catch(() => {});
271
+ });
272
+ if (this.throttleEveryN <= 0) {
273
+ options?.onSend?.();
274
+ return this.dispatchCall(procedure, input);
275
+ }
276
+ if (this.throttling) return new Promise((resolve, reject) => {
277
+ this.queuedCalls.push({
278
+ procedure,
279
+ input,
280
+ resolve,
281
+ reject,
282
+ onSend: options?.onSend
283
+ });
284
+ });
285
+ this.throttleCounter++;
286
+ if (this.throttleCounter >= this.throttleEveryN) {
287
+ this.throttling = true;
288
+ await this.runThrottleLoop();
289
+ }
290
+ options?.onSend?.();
291
+ return this.dispatchCall(procedure, input);
292
+ }
293
+ /**
294
+ * Throttle drain loop. Each iteration: sleep `throttleDelayMs`, then
295
+ * dispatch up to `throttleEveryN` queued items. Exits as soon as the
296
+ * queue drains inside a single batch (burst over, back to direct
297
+ * dispatch on the next `call()`) or the connection goes away
298
+ * (reconnect path will restart us via `flushQueuedCalls`).
299
+ */
300
+ async runThrottleLoop() {
301
+ while (this.throttling) {
302
+ await new Promise((r) => setTimeout(r, this.throttleDelayMs));
303
+ if (!this.isReady()) {
304
+ this.throttling = false;
305
+ this.throttleCounter = 0;
306
+ return;
307
+ }
308
+ this.throttleCounter = 0;
309
+ while (this.throttleCounter < this.throttleEveryN && this.queuedCalls.length > 0) {
310
+ const item = this.queuedCalls.shift();
311
+ item.onSend?.();
312
+ this.dispatchCall(item.procedure, item.input).then(item.resolve, item.reject);
313
+ this.throttleCounter++;
314
+ }
315
+ if (this.queuedCalls.length === 0) {
316
+ this.throttling = false;
317
+ this.throttleCounter = 0;
318
+ return;
319
+ }
320
+ }
321
+ }
322
+ /**
323
+ * Issue the call against an established connection. Caller must ensure
324
+ * `isReady()` before invoking.
325
+ */
326
+ dispatchCall(procedure, input) {
327
+ const id = this.generateRequestId();
328
+ return new Promise((resolve, reject) => {
329
+ this.pendingRequests.set(id, {
330
+ resolve,
331
+ reject
332
+ });
333
+ this.send({
334
+ type: "call",
335
+ id,
336
+ procedure,
337
+ input
338
+ });
339
+ });
340
+ }
341
+ /**
342
+ * Flush queued calls in FIFO order. Called once `isReady()` becomes
343
+ * true. With the throttle enabled, dispatch the first batch
344
+ * synchronously then hand off to the drain loop so a large
345
+ * post-reconnect flush respects backpressure instead of dumping 10k
346
+ * frames in one tick.
347
+ */
348
+ flushQueuedCalls() {
349
+ if (this.queuedCalls.length === 0) return;
350
+ if (this.throttleEveryN <= 0) {
351
+ const queue = this.queuedCalls;
352
+ this.queuedCalls = [];
353
+ for (const item of queue) {
354
+ item.onSend?.();
355
+ this.dispatchCall(item.procedure, item.input).then(item.resolve, item.reject);
356
+ }
357
+ return;
358
+ }
359
+ this.throttleCounter = 0;
360
+ while (this.throttleCounter < this.throttleEveryN && this.queuedCalls.length > 0) {
361
+ const item = this.queuedCalls.shift();
362
+ item.onSend?.();
363
+ this.dispatchCall(item.procedure, item.input).then(item.resolve, item.reject);
364
+ this.throttleCounter++;
365
+ }
366
+ if (this.queuedCalls.length > 0) {
367
+ this.throttling = true;
368
+ this.runThrottleLoop();
369
+ }
370
+ }
371
+ /**
372
+ * Re-send `subscribe` for every existing subscription. The subId for each
373
+ * may legitimately change (e.g. `queryIdentity` was a function of user
374
+ * identity and the user changed during the disconnect), so we re-key the
375
+ * unified `subscriptions` Map: snapshot every record, drop them from the
376
+ * Map, then reinsert under a fresh `_pending:<reqId>` key. Each pending
377
+ * request's `resolve` rekeys the parked record under the new server-issued
378
+ * subId so subsequent `sub:update` frames route to the existing
379
+ * `onUpdate` callback (which the React handle is still holding).
380
+ */
381
+ resubscribeAll() {
382
+ if (this.subscriptions.size === 0) return;
383
+ const subs = Array.from(this.subscriptions.values());
384
+ this.subscriptions.clear();
385
+ for (const sub of subs) {
386
+ const id = this.generateRequestId();
387
+ const pendingKey = `_pending:${id}`;
388
+ sub.subId = null;
389
+ this.subscriptions.set(pendingKey, sub);
390
+ this.pendingRequests.set(id, {
391
+ subscribeResolve: (subId) => {
392
+ const parked = this.subscriptions.get(pendingKey);
393
+ if (!subId || !parked) {
394
+ this.subscriptions.delete(pendingKey);
395
+ this.rejectJoiners(parked, new RPCError("Subscribe response missing subId after replay", "PROTOCOL_ERROR"));
396
+ return;
397
+ }
398
+ parked.subId = subId;
399
+ this.subscriptions.delete(pendingKey);
400
+ this.subscriptions.set(subId, parked);
401
+ this.resolveJoiners(parked, subId);
402
+ },
403
+ reject: (err) => {
404
+ if (!(err instanceof RPCError) || err.code !== "DISCONNECTED") {
405
+ const parked = this.subscriptions.get(pendingKey);
406
+ this.subscriptions.delete(pendingKey);
407
+ this.rejectJoiners(parked, err);
408
+ }
409
+ console.error(`[WSClient] Failed to re-subscribe ${sub.procedure}:`, err);
410
+ }
411
+ });
412
+ this.send({
413
+ type: "subscribe",
414
+ id,
415
+ procedure: sub.procedure,
416
+ input: sub.input
417
+ });
418
+ }
419
+ }
420
+ /**
421
+ * Subscribe to a live query.
422
+ *
423
+ * The client does not compute the subId locally — it sends the
424
+ * (procedure, input) pair and receives the canonical subId on the
425
+ * subscribe response. This lets the server's per-procedure
426
+ * `queryIdentity` override be authoritative without forcing the client
427
+ * to mirror any of the resolution logic.
428
+ *
429
+ * Returns the server-assigned subId and an unsubscribe function.
430
+ */
431
+ async subscribe(procedure, input, serializedInput, onUpdate, onGone) {
432
+ await this.ensureConnected();
433
+ const localKey = await computeLocalSubKey(procedure, serializedInput);
434
+ const existing = this.findSubByLocalKey(localKey);
435
+ if (existing) {
436
+ existing.onUpdate = onUpdate;
437
+ existing.onGone = onGone;
438
+ if (existing.subId) return {
439
+ subId: existing.subId,
440
+ unsubscribe: () => this.unsubscribeByLocalKey(localKey, existing.subId)
441
+ };
442
+ return new Promise((resolve, reject) => {
443
+ const joinerTimeout = setTimeout(() => {
444
+ existing.pendingJoiners?.delete(joiner);
445
+ reject(new RPCError(`Subscribe timeout: ${procedure}`, "TIMEOUT"));
446
+ }, 3e4);
447
+ const joiner = {
448
+ resolve: (subId) => {
449
+ clearTimeout(joinerTimeout);
450
+ existing.pendingJoiners?.delete(joiner);
451
+ resolve({
452
+ subId,
453
+ unsubscribe: () => this.unsubscribeByLocalKey(localKey, subId)
454
+ });
455
+ },
456
+ reject: (err) => {
457
+ clearTimeout(joinerTimeout);
458
+ existing.pendingJoiners?.delete(joiner);
459
+ reject(err);
460
+ }
461
+ };
462
+ (existing.pendingJoiners ??= /* @__PURE__ */ new Set()).add(joiner);
463
+ });
464
+ }
465
+ const id = this.generateRequestId();
466
+ const pendingKey = `_pending:${id}`;
467
+ const record = {
468
+ subId: null,
469
+ localKey,
470
+ procedure,
471
+ input,
472
+ onUpdate,
473
+ onGone
474
+ };
475
+ this.subscriptions.set(pendingKey, record);
476
+ let timeout = null;
477
+ return new Promise((resolve, reject) => {
478
+ this.pendingRequests.set(id, {
479
+ subscribeResolve: (subId) => {
480
+ if (timeout) {
481
+ clearTimeout(timeout);
482
+ timeout = null;
483
+ }
484
+ const sub = this.subscriptions.get(pendingKey);
485
+ if (!subId || !sub) {
486
+ this.subscriptions.delete(pendingKey);
487
+ const err = new RPCError("Subscribe response missing subId", "PROTOCOL_ERROR");
488
+ this.rejectJoiners(sub, err);
489
+ reject(err);
490
+ return;
491
+ }
492
+ sub.subId = subId;
493
+ this.subscriptions.delete(pendingKey);
494
+ this.subscriptions.set(subId, sub);
495
+ resolve({
496
+ subId,
497
+ unsubscribe: () => this.unsubscribeByLocalKey(localKey, subId)
498
+ });
499
+ this.resolveJoiners(sub, subId);
500
+ },
501
+ reject: (err) => {
502
+ if (timeout) {
503
+ clearTimeout(timeout);
504
+ timeout = null;
505
+ }
506
+ if (!(err instanceof RPCError) || err.code !== "DISCONNECTED") {
507
+ const sub = this.subscriptions.get(pendingKey);
508
+ this.subscriptions.delete(pendingKey);
509
+ this.rejectJoiners(sub, err);
510
+ }
511
+ reject(err);
512
+ }
513
+ });
514
+ this.send({
515
+ type: "subscribe",
516
+ id,
517
+ procedure,
518
+ input
519
+ });
520
+ timeout = setTimeout(() => {
521
+ if (this.pendingRequests.has(id)) {
522
+ this.pendingRequests.delete(id);
523
+ const sub = this.subscriptions.get(pendingKey);
524
+ this.subscriptions.delete(pendingKey);
525
+ const err = new RPCError(`Subscribe timeout: ${procedure}`, "TIMEOUT");
526
+ this.rejectJoiners(sub, err);
527
+ reject(err);
528
+ }
529
+ }, 3e4);
530
+ });
531
+ }
532
+ findSubByLocalKey(localKey) {
533
+ for (const sub of this.subscriptions.values()) if (sub.localKey === localKey) return sub;
534
+ }
535
+ /** Resolve every joiner waiting on this Subscription with the canonical subId. */
536
+ resolveJoiners(sub, subId) {
537
+ if (!sub.pendingJoiners) return;
538
+ const snapshot = Array.from(sub.pendingJoiners);
539
+ for (const j of snapshot) j.resolve(subId);
540
+ }
541
+ /** Reject every joiner waiting on this Subscription with the given error. */
542
+ rejectJoiners(sub, err) {
543
+ if (!sub?.pendingJoiners) return;
544
+ const snapshot = Array.from(sub.pendingJoiners);
545
+ for (const j of snapshot) j.reject(err);
546
+ }
547
+ unsubscribeByLocalKey(localKey, subId) {
548
+ const sendUnsubscribe = (subId) => {
549
+ if (this.connectionState === "connected") this.send({
550
+ type: "unsubscribe",
551
+ subId
552
+ });
553
+ };
554
+ if (subId) {
555
+ this.subscriptions.delete(subId);
556
+ sendUnsubscribe(subId);
557
+ return;
558
+ }
559
+ for (const [key, sub] of this.subscriptions.entries()) {
560
+ if (sub.localKey !== localKey) continue;
561
+ this.subscriptions.delete(key);
562
+ this.rejectJoiners(sub, new RPCError("Subscription cancelled before response", "CANCELLED"));
563
+ sendUnsubscribe(sub.subId);
564
+ return;
565
+ }
566
+ }
567
+ async handleMessage(dataValue) {
568
+ this.lastServerMessageAt = Date.now();
569
+ let data = void 0;
570
+ try {
571
+ data = JSON.parse(dataValue);
572
+ } catch {
573
+ console.error("[WSClient] Failed to parse server message as JSON:", dataValue);
574
+ }
575
+ const parsed = ServerMessageSchema.safeParse(data);
576
+ if (!parsed.success) {
577
+ console.error("[WSClient] Invalid server message:", parsed.error.issues, data);
578
+ return;
579
+ }
580
+ const message = parsed.data;
581
+ switch (message.type) {
582
+ case "connected": {
583
+ this.connectionState = "connected";
584
+ this.reconnectAttempts = 0;
585
+ this.lastSuccessfulConnectionTime = Date.now();
586
+ const token = await this.options.token?.();
587
+ if (token) {
588
+ this.authState = "pending";
589
+ this.emitState();
590
+ this.send({
591
+ type: "auth",
592
+ data: { token }
593
+ });
594
+ } else {
595
+ this.authState = "anonymouse";
596
+ this.options.onConnect?.();
597
+ this.emitState();
598
+ this.afterReady();
599
+ this.pendingConnectResolve?.();
600
+ }
601
+ break;
602
+ }
603
+ case "auth:success":
604
+ this.authState = "authenticated";
605
+ this.authUserId = message.userId;
606
+ this.options.onConnect?.();
607
+ this.emitState();
608
+ this.afterReady();
609
+ this.pendingConnectResolve?.();
610
+ break;
611
+ case "auth:failed":
612
+ this.authState = "unauthenticated";
613
+ this.authUserId = null;
614
+ this.options.onConnect?.();
615
+ this.emitState();
616
+ this.afterReady();
617
+ this.pendingConnectResolve?.();
618
+ break;
619
+ case "response":
620
+ this.handleResponse(message);
621
+ break;
622
+ case "sub:update":
623
+ this.handleSubscriptionUpdate(message);
624
+ break;
625
+ case "sub:gone":
626
+ this.handleSubscriptionGone(message);
627
+ break;
628
+ case "ping": break;
629
+ default: console.warn("[WSClient] Unhandled server message type:", message);
630
+ }
631
+ }
632
+ /**
633
+ * Called once the client transitions to a "ready" state on a fresh
634
+ * connection. Replays live-query subscriptions and flushes any queued
635
+ * calls in FIFO order.
636
+ */
637
+ afterReady() {
638
+ this.resubscribeAll();
639
+ this.flushQueuedCalls();
640
+ }
641
+ handleResponse(message) {
642
+ const pending = this.pendingRequests.get(message.id);
643
+ if (!pending) return;
644
+ this.pendingRequests.delete(message.id);
645
+ if (message.success) if (pending.subscribeResolve) pending.subscribeResolve(message.subId);
646
+ else pending.resolve?.(message.data);
647
+ else pending.reject(new RPCError(message.error ?? "Unknown error", message.code));
648
+ }
649
+ handleSubscriptionUpdate(message) {
650
+ const sub = this.subscriptions.get(message.subId);
651
+ if (sub) sub.onUpdate(message.data);
652
+ }
653
+ /**
654
+ * Handles a `sub:gone` message: the server no longer tracks this
655
+ * subscription (e.g. it was fully unregistered while we were disconnected
656
+ * from the sub-manager). Notify the handle and drop the local entry so a
657
+ * re-subscribe is required to receive further updates.
658
+ */
659
+ handleSubscriptionGone(message) {
660
+ const sub = this.subscriptions.get(message.subId);
661
+ if (!sub) return;
662
+ this.subscriptions.delete(message.subId);
663
+ try {
664
+ sub.onGone?.(message.reason);
665
+ } catch (e) {
666
+ console.error("[WSClient] onGone listener threw:", e);
667
+ }
668
+ }
669
+ handleClose() {
670
+ if (this.connectionState === "disconnected") return;
671
+ this.connectionState = "disconnected";
672
+ this.authState = "pending";
673
+ this.ws = null;
674
+ this.stopInactivityWatchdog();
675
+ this.options.onDisconnect?.();
676
+ this.pendingConnectReject?.(new RPCError("Connection closed before ready", "DISCONNECTED"));
677
+ for (const pending of this.pendingRequests.values()) pending.reject(new RPCError("Connection closed", "DISCONNECTED"));
678
+ this.pendingRequests.clear();
679
+ this.emitState();
680
+ if (!this.explicitlyClosed && this.options.reconnect?.enabled) this.scheduleReconnect();
681
+ }
682
+ handleError(err) {
683
+ this.options.onError?.(err);
684
+ }
685
+ startInactivityWatchdog() {
686
+ this.stopInactivityWatchdog();
687
+ this.lastServerMessageAt = Date.now();
688
+ const tickMs = Math.max(1e3, Math.floor(this.serverInactivityMs / 4));
689
+ this.inactivityTimer = setInterval(() => {
690
+ if (Date.now() - this.lastServerMessageAt > this.serverInactivityMs) this.closeAndReconnect("InactiveServer");
691
+ }, tickMs);
692
+ }
693
+ stopInactivityWatchdog() {
694
+ if (this.inactivityTimer) {
695
+ clearInterval(this.inactivityTimer);
696
+ this.inactivityTimer = null;
697
+ }
698
+ }
699
+ /**
700
+ * Close the current WebSocket and schedule a reconnect via `handleClose`.
701
+ *
702
+ * Atomically swaps the old socket's event handlers to no-ops BEFORE
703
+ * `close()` so a frame in flight at the moment of close cannot fire
704
+ * `handleMessage` into a half-cleaned state — that was the source of
705
+ * the "browser says socket is alive but client says disconnected" race.
706
+ */
707
+ closeAndReconnect(reason) {
708
+ if (this.connectionState === "disconnected") return;
709
+ console.warn(`[WSClient] closeAndReconnect: ${reason}`);
710
+ const oldWs = this.ws;
711
+ if (oldWs) {
712
+ oldWs.onmessage = null;
713
+ oldWs.onerror = null;
714
+ oldWs.onopen = null;
715
+ oldWs.onclose = null;
716
+ try {
717
+ oldWs.close(4e3, reason);
718
+ } catch {}
719
+ }
720
+ this.handleClose();
721
+ }
722
+ installEnvironmentListeners() {
723
+ if (typeof document !== "undefined" && typeof document.addEventListener === "function" && !this.visibilityListener) {
724
+ this.visibilityListener = () => {
725
+ if (typeof document === "undefined") return;
726
+ if (document.visibilityState !== "visible") return;
727
+ if (this.connectionState !== "connected") return;
728
+ if (Date.now() - this.lastServerMessageAt > this.serverInactivityMs) this.closeAndReconnect("WokeUpStale");
729
+ };
730
+ document.addEventListener("visibilitychange", this.visibilityListener);
731
+ }
732
+ if (typeof window !== "undefined" && typeof window.addEventListener === "function" && !this.onlineListener) {
733
+ this.onlineListener = () => {
734
+ if (this.connectionState !== "disconnected") return;
735
+ if (this.explicitlyClosed) return;
736
+ if (this.reconnectTimer) {
737
+ clearTimeout(this.reconnectTimer);
738
+ this.reconnectTimer = null;
739
+ }
740
+ this.connect().catch(() => {});
741
+ };
742
+ window.addEventListener("online", this.onlineListener);
743
+ }
744
+ }
745
+ removeEnvironmentListeners() {
746
+ if (this.visibilityListener && typeof document !== "undefined") {
747
+ document.removeEventListener("visibilitychange", this.visibilityListener);
748
+ this.visibilityListener = null;
749
+ }
750
+ if (this.onlineListener && typeof window !== "undefined") {
751
+ window.removeEventListener("online", this.onlineListener);
752
+ this.onlineListener = null;
753
+ }
754
+ }
755
+ scheduleReconnect() {
756
+ if (this.reconnectAttempts >= (this.options.reconnect?.maxAttempts ?? 5)) {
757
+ console.error("[WSClient] Max reconnection attempts reached");
758
+ const giveUp = new RPCError("Max reconnection attempts reached", "DISCONNECTED");
759
+ for (const q of this.queuedCalls) q.reject(giveUp);
760
+ this.queuedCalls = [];
761
+ this.pendingConnectReject?.(giveUp);
762
+ return;
763
+ }
764
+ const base = (this.options.reconnect?.delay ?? 1e3) * Math.pow(this.options.reconnect?.backoff ?? 1.5, this.reconnectAttempts);
765
+ const delay = base + base * (Math.random() - .5);
766
+ this.reconnectAttempts++;
767
+ this.reconnectTimer = setTimeout(() => {
768
+ this.connect().catch((err) => {
769
+ console.error("[WSClient] Reconnection failed:", err);
770
+ });
771
+ }, delay);
772
+ }
773
+ computePublicState() {
774
+ if (this.connectionState === "disconnected") return "disconnected";
775
+ if (this.connectionState === "connecting") return "connecting";
776
+ if (this.authState === "pending") return "authenticating";
777
+ return "ready";
778
+ }
779
+ emitState() {
780
+ const next = this.computePublicState();
781
+ if (next === this.lastPublicState) return;
782
+ this.lastPublicState = next;
783
+ for (const l of this.stateListeners) try {
784
+ l(next);
785
+ } catch (e) {
786
+ console.error("[WSClient] state listener threw:", e);
787
+ }
788
+ }
789
+ async ensureConnected() {
790
+ if (this.connectionState != "connected") await this.connect();
791
+ }
792
+ send(message) {
793
+ if (this.ws && this.ws.readyState === WS_OPEN) this.ws.send(supaliveStringify(message));
794
+ }
795
+ cleanup() {
796
+ this.connectionState = "disconnected";
797
+ this.authState = "pending";
798
+ this.stopInactivityWatchdog();
799
+ if (this.reconnectTimer) {
800
+ clearTimeout(this.reconnectTimer);
801
+ this.reconnectTimer = null;
802
+ }
803
+ const teardown = new RPCError("Client disconnected", "DISCONNECTED");
804
+ for (const pending of this.pendingRequests.values()) pending.reject(teardown);
805
+ this.pendingRequests.clear();
806
+ for (const q of this.queuedCalls) q.reject(teardown);
807
+ this.queuedCalls = [];
808
+ this.throttling = false;
809
+ this.throttleCounter = 0;
810
+ this.pendingConnectReject?.(teardown);
811
+ }
812
+ generateRequestId() {
813
+ return `${++this.requestId}`;
814
+ }
815
+ };
816
+ let WebSocketImpl = null;
817
+ if (typeof globalThis !== "undefined" && globalThis.WebSocket) WebSocketImpl = globalThis.WebSocket;
818
+ async function getWebSocket() {
819
+ if (WebSocketImpl) return WebSocketImpl;
820
+ return (await import("ws")).WebSocket;
821
+ }
822
+ //#endregion
823
+ //#region src/client/client.ts
824
+ /**
825
+ * Create a fully typed RPC client with WebSocket transport.
826
+ */
827
+ function createClient(options) {
828
+ const wsClient = new WsClientManager({
829
+ url: options.url,
830
+ token: options.token,
831
+ reconnectionTimeout: options.reconnectionTimeout,
832
+ reconnect: options.reconnect,
833
+ heartbeat: options.heartbeat,
834
+ throttle: options.throttle,
835
+ onConnect: options.onConnect,
836
+ onDisconnect: options.onDisConnect
837
+ });
838
+ const liveQueryHandles = /* @__PURE__ */ new Map();
839
+ function liveQueryDedupKey(procedure, input) {
840
+ return `${procedure}::${input}`;
841
+ }
842
+ const cache = /* @__PURE__ */ new Map();
843
+ return new Proxy({}, { get(_target, procedureName) {
844
+ if (typeof procedureName !== "string") return;
845
+ switch (procedureName) {
846
+ case "connect": return () => wsClient.connect();
847
+ case "disconnect": return () => wsClient.disconnect();
848
+ case "isReady": return () => wsClient.isReady();
849
+ case "isAuthenticated": return () => wsClient.isAuthenticated();
850
+ case "isAnonymouse": return () => wsClient.isAnonymouse();
851
+ case "getAuthUserId": return () => wsClient.getAuthUserId();
852
+ case "onState": return (listener) => wsClient.onState(listener);
853
+ case "getPublicState": return () => wsClient.getPublicState();
854
+ }
855
+ const cachedProp = cache.get(procedureName);
856
+ if (cachedProp) return cachedProp;
857
+ const newProp = {
858
+ query: async (input) => {
859
+ return wsClient.call(procedureName, input);
860
+ },
861
+ mutate: async (input, opts) => {
862
+ return wsClient.call(procedureName, input, opts);
863
+ },
864
+ action: async (input, opts) => {
865
+ return wsClient.call(procedureName, input, opts);
866
+ },
867
+ liveQuery: (input, serializedInput) => {
868
+ const dedupKey = liveQueryDedupKey(procedureName, serializedInput);
869
+ const existing = liveQueryHandles.get(dedupKey);
870
+ if (existing) return existing;
871
+ const handle = new LiveQueryHandleImpl(wsClient, procedureName, input, serializedInput, () => liveQueryHandles.delete(dedupKey));
872
+ liveQueryHandles.set(dedupKey, handle);
873
+ return handle;
874
+ }
875
+ };
876
+ cache.set(procedureName, newProp);
877
+ return newProp;
878
+ } });
879
+ }
880
+ /**
881
+ * Sync external store for one (procedure, input) live query.
882
+ *
883
+ * - Constructor returns immediately; the wasm-hashed server subId is computed
884
+ * in `kickoffSubscribe()` so callers don't have to await.
885
+ * - `onState` / `onData` ref-count their listeners; the last to leave starts
886
+ * a 3 s grace timer before unsubscribing on the wire (so transient mounts
887
+ * in StrictMode don't churn server-side state).
888
+ * - Listens to `wsClient.onState`: any drop flips status to "stale", and the
889
+ * manager's own `resubscribeAll()` brings us back without our help.
890
+ * - On `sub:gone`, transitions to "error" and auto-retries on next ready.
891
+ */
892
+ var LiveQueryHandleImpl = class {
893
+ wsClient;
894
+ procedure;
895
+ input;
896
+ serializedInput;
897
+ onDispose;
898
+ state = {
899
+ data: void 0,
900
+ status: "connecting",
901
+ error: null
902
+ };
903
+ stateListeners = /* @__PURE__ */ new Set();
904
+ dataListeners = /* @__PURE__ */ new Set();
905
+ unsubscribeServer = null;
906
+ graceTimer = null;
907
+ subId = null;
908
+ wsStateOff = null;
909
+ /** Set once the disposer has fired (after the last unsubscribe). */
910
+ disposed = false;
911
+ /**
912
+ * rAF coalesce buffer for incoming `sub:update` payloads. Bursty
913
+ * mutations (e.g. 2000 spam transfers) can fire `sub:update` faster than
914
+ * the screen can repaint; collapsing to one commit per animation frame
915
+ * caps live-query re-renders at vsync without skipping the latest state.
916
+ * Tab in a background window naturally throttles down to ~1Hz via the
917
+ * browser's own rAF policy.
918
+ */
919
+ pendingData = null;
920
+ rafHandle = null;
921
+ constructor(wsClient, procedure, input, serializedInput, onDispose) {
922
+ this.wsClient = wsClient;
923
+ this.procedure = procedure;
924
+ this.input = input;
925
+ this.serializedInput = serializedInput;
926
+ this.onDispose = onDispose;
927
+ this.wsStateOff = wsClient.onState((next) => {
928
+ if (next === "ready" && this.state.status === "stale") {} else if (this.state.status === "success" && (next === "disconnected" || next === "connecting" || next === "authenticating")) this.setState({ status: "stale" });
929
+ });
930
+ this.kickoffSubscribe();
931
+ }
932
+ get() {
933
+ return this.state.data;
934
+ }
935
+ getState = () => this.state;
936
+ onState = (listener) => {
937
+ this.stateListeners.add(listener);
938
+ this.cancelGrace();
939
+ return () => {
940
+ this.stateListeners.delete(listener);
941
+ this.scheduleGraceUnsubscribe();
942
+ };
943
+ };
944
+ onData(callback) {
945
+ this.dataListeners.add(callback);
946
+ this.cancelGrace();
947
+ if (this.state.data !== void 0) callback(this.state.data);
948
+ return () => {
949
+ this.dataListeners.delete(callback);
950
+ this.scheduleGraceUnsubscribe();
951
+ };
952
+ }
953
+ refetch() {
954
+ if (this.disposed) return;
955
+ this.setState({
956
+ status: "loading",
957
+ error: null
958
+ });
959
+ this.kickoffSubscribe();
960
+ }
961
+ unsubscribe() {
962
+ if (this.disposed) return;
963
+ this.disposed = true;
964
+ this.cancelGrace();
965
+ if (this.rafHandle !== null && this.rafHandle !== -1 && typeof cancelAnimationFrame === "function") cancelAnimationFrame(this.rafHandle);
966
+ this.rafHandle = null;
967
+ this.pendingData = null;
968
+ if (this.unsubscribeServer) {
969
+ try {
970
+ this.unsubscribeServer();
971
+ } catch (e) {
972
+ console.error("[LiveQuery] unsubscribe threw:", e);
973
+ }
974
+ this.unsubscribeServer = null;
975
+ }
976
+ if (this.wsStateOff) {
977
+ this.wsStateOff();
978
+ this.wsStateOff = null;
979
+ }
980
+ this.stateListeners.clear();
981
+ this.dataListeners.clear();
982
+ this.onDispose();
983
+ }
984
+ async kickoffSubscribe() {
985
+ try {
986
+ const { subId, unsubscribe } = await this.wsClient.subscribe(this.procedure, this.input, this.serializedInput, (data) => this.handleData(data), (reason) => this.handleGone(reason));
987
+ if (this.disposed) {
988
+ try {
989
+ unsubscribe();
990
+ } catch {}
991
+ return;
992
+ }
993
+ this.subId = subId;
994
+ this.unsubscribeServer = unsubscribe;
995
+ } catch (err) {
996
+ if (this.disposed) return;
997
+ const e = err instanceof Error ? err : new Error(String(err));
998
+ this.setState({
999
+ status: "error",
1000
+ error: e
1001
+ });
1002
+ }
1003
+ }
1004
+ handleData(data) {
1005
+ if (this.disposed) return;
1006
+ this.pendingData = { value: data };
1007
+ if (this.rafHandle !== null) return;
1008
+ if (typeof requestAnimationFrame !== "function") {
1009
+ this.rafHandle = -1;
1010
+ queueMicrotask(() => {
1011
+ this.rafHandle = null;
1012
+ this.commitPendingData();
1013
+ });
1014
+ return;
1015
+ }
1016
+ this.rafHandle = requestAnimationFrame(() => {
1017
+ this.rafHandle = null;
1018
+ this.commitPendingData();
1019
+ });
1020
+ }
1021
+ commitPendingData() {
1022
+ if (this.disposed) return;
1023
+ const pending = this.pendingData;
1024
+ if (!pending) return;
1025
+ this.pendingData = null;
1026
+ this.setState({
1027
+ data: pending.value,
1028
+ status: "success",
1029
+ error: null
1030
+ });
1031
+ for (const cb of this.dataListeners) try {
1032
+ cb(pending.value);
1033
+ } catch (e) {
1034
+ console.error("[LiveQuery] data listener threw:", e);
1035
+ }
1036
+ }
1037
+ handleGone(reason) {
1038
+ if (this.disposed) return;
1039
+ this.setState({
1040
+ status: "error",
1041
+ error: new RPCError(reason ?? "subscription gone", "SUB_GONE")
1042
+ });
1043
+ this.unsubscribeServer = null;
1044
+ this.subId = null;
1045
+ this.kickoffSubscribe();
1046
+ }
1047
+ setState(patch) {
1048
+ const next = {
1049
+ ...this.state,
1050
+ ...patch
1051
+ };
1052
+ if (next.data === this.state.data && next.status === this.state.status && next.error === this.state.error) return;
1053
+ this.state = next;
1054
+ for (const l of this.stateListeners) try {
1055
+ l();
1056
+ } catch (e) {
1057
+ console.error("[LiveQuery] state listener threw:", e);
1058
+ }
1059
+ }
1060
+ cancelGrace() {
1061
+ if (this.graceTimer) {
1062
+ clearTimeout(this.graceTimer);
1063
+ this.graceTimer = null;
1064
+ }
1065
+ }
1066
+ scheduleGraceUnsubscribe() {
1067
+ if (this.stateListeners.size > 0 || this.dataListeners.size > 0) return;
1068
+ this.cancelGrace();
1069
+ this.graceTimer = setTimeout(() => {
1070
+ this.graceTimer = null;
1071
+ if (this.stateListeners.size === 0 && this.dataListeners.size === 0) this.unsubscribe();
1072
+ }, 3e3);
1073
+ }
1074
+ };
1075
+ //#endregion
1076
+ //#region src/client/caller_client.ts
1077
+ /**
1078
+ * Create a server-side caller that bypasses HTTP/WebSocket.
1079
+ *
1080
+ * For in-memory calls, input validation is skipped (data is already a JS object).
1081
+ * Query procedures also get a liveQuery() method for server-side use.
1082
+ */
1083
+ function createCaller(options) {
1084
+ const { isInternal = true, inContext } = options;
1085
+ const registry = getContextRegistry(inContext ?? "default");
1086
+ const cache = /* @__PURE__ */ new Map();
1087
+ let db;
1088
+ return new Proxy({}, { get(_target, procedureName) {
1089
+ if (typeof procedureName !== "string") return;
1090
+ if (procedureName == "init") return async (config) => {
1091
+ db = config.db;
1092
+ };
1093
+ const procedure = registry.getProcedure(procedureName);
1094
+ if (!procedure) throw new Error(`Procedure "${procedureName}" not found`);
1095
+ if (registry.isInternalProcedure(procedureName) && !isInternal) throw new Error(`Procedure "${procedureName}" is internal only`);
1096
+ const validateInput = (input) => {
1097
+ if (typeof input !== "string") return input;
1098
+ return procedure.inputSchema.parse(input);
1099
+ };
1100
+ const cachedProp = cache.get(procedureName);
1101
+ if (cachedProp) return cachedProp;
1102
+ const newProp = {
1103
+ query: async (input, serverCtx) => {
1104
+ if (procedure.type !== "query") throw new Error(`Procedure "${procedureName}" is not a query`);
1105
+ const validatedInput = validateInput(input);
1106
+ return db.query(async (dbCtx) => {
1107
+ const ctx = {
1108
+ db: dbCtx,
1109
+ serverCtx
1110
+ };
1111
+ return procedure.fn(ctx, validatedInput);
1112
+ });
1113
+ },
1114
+ mutate: async (input, serverCtx) => {
1115
+ if (procedure.type !== "mutation") throw new Error(`Procedure "${procedureName}" is not a mutation`);
1116
+ const validatedInput = validateInput(input);
1117
+ return (await db.mutation(async (dbCtx) => {
1118
+ const ctx = {
1119
+ db: dbCtx,
1120
+ serverCtx
1121
+ };
1122
+ return procedure.fn(ctx, validatedInput);
1123
+ })).result;
1124
+ },
1125
+ action: async (input, serverCtx) => {
1126
+ if (procedure.type !== "action") throw new Error(`Procedure "${procedureName}" is not an action`);
1127
+ const validatedInput = validateInput(input);
1128
+ const ctx = {
1129
+ db,
1130
+ serverCtx
1131
+ };
1132
+ return procedure.fn(ctx, validatedInput);
1133
+ },
1134
+ liveQuery: procedure.type === "query" ? async (input, serverCtx) => {
1135
+ const validatedInput = validateInput(input);
1136
+ return db.liveQuery(async (dbCtx) => {
1137
+ const ctx = {
1138
+ db: dbCtx,
1139
+ serverCtx
1140
+ };
1141
+ return procedure.fn(ctx, validatedInput);
1142
+ });
1143
+ } : void 0
1144
+ };
1145
+ cache.set(procedureName, newProp);
1146
+ return newProp;
1147
+ } });
1148
+ }
1149
+ //#endregion
1150
+ export { AndPredicateSchema, BigIntSchema, CachedPgMetadataSchema, CompareOperatorSchema, DEFAULT_RETRY, DbReader, DbReader as ReadContext, DbWriter, DbWriter as WritableContext, LeafPredicateSchema, NO_RETRY, OccAbortError, OccConflictError, OrPredicateSchema, PointReadSchema, PredicateSchema, QueryCacheEntrySchema, QueryCacheMetadataSchema, RPCError, RangeReadSchema, RawPointReadSchema, RawRangeReadSchema, RawReadEntrySchema, ReadEntrySchema, SupaliveDb, TxContext, WriteEntrySchema, WriteOpSchema, WsClientManager, _resetGlobalDefs, bytesFromJson, createActionBuilder, createCaller, createClient, createMutationBuilder, createQueryBuilder, defineComputedField, defineQuery, defineSchema, matchesPredicate, normalizeIdToBytes, normalizeToBytes, router, sleep };
1151
+
19
1152
  //# sourceMappingURL=index.js.map