@supalive/core 0.1.4 → 0.1.6

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