@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.
- package/README.md +17 -2
- package/dist/bytes-AuEkyxeU.js +19 -0
- package/dist/bytes-AuEkyxeU.js.map +1 -0
- package/dist/codec-BHAmGEIQ.js +62 -0
- package/dist/codec-BHAmGEIQ.js.map +1 -0
- package/dist/helper-CiacMqje.d.ts +14 -0
- package/dist/helper-CiacMqje.d.ts.map +1 -0
- package/dist/helper-wGbzz6zo.js +42 -0
- package/dist/helper-wGbzz6zo.js.map +1 -0
- package/dist/index-CaWMDqq0.d.ts +1754 -0
- package/dist/index-CaWMDqq0.d.ts.map +1 -0
- package/dist/mysql-Cxd4Uq5p.d.ts +104 -0
- package/dist/mysql-Cxd4Uq5p.d.ts.map +1 -0
- package/dist/mysql-bvnjYWvZ.js +570 -0
- package/dist/mysql-bvnjYWvZ.js.map +1 -0
- package/dist/overlap-checker-Ct_HP_iB.js +187 -0
- package/dist/overlap-checker-Ct_HP_iB.js.map +1 -0
- package/dist/postgres-B8u1YN8X.js +824 -0
- package/dist/postgres-B8u1YN8X.js.map +1 -0
- package/dist/postgres-Ck-xt72U.d.ts +108 -0
- package/dist/postgres-Ck-xt72U.d.ts.map +1 -0
- package/dist/procedure-DX87tyef.js +124 -0
- package/dist/procedure-DX87tyef.js.map +1 -0
- package/dist/query-Bh0RNxDz.js +673 -0
- package/dist/query-Bh0RNxDz.js.map +1 -0
- package/dist/realtime_db-DOCgqbwi.js +247 -0
- package/dist/realtime_db-DOCgqbwi.js.map +1 -0
- package/dist/router-DlTYWpop.js +81 -0
- package/dist/router-DlTYWpop.js.map +1 -0
- package/dist/schema-Dd7u0A1L.js +109 -0
- package/dist/schema-Dd7u0A1L.js.map +1 -0
- package/dist/src/client/index.d.ts +2 -17
- package/dist/src/client/index.js +1152 -18
- package/dist/src/client/index.js.map +1 -1
- package/dist/src/exports/mysql.d.ts +2 -2
- package/dist/src/exports/mysql.js +2 -2
- package/dist/src/exports/postgres.d.ts +2 -2
- package/dist/src/exports/postgres.js +2 -2
- package/dist/src/exports/procedure.d.ts +3 -5
- package/dist/src/exports/procedure.js +6 -5
- package/dist/src/exports/schema-sql.d.ts +18 -0
- package/dist/src/exports/schema-sql.d.ts.map +1 -0
- package/dist/src/exports/schema-sql.js +161 -0
- package/dist/src/exports/schema-sql.js.map +1 -0
- package/dist/src/exports/server.d.ts +368 -2
- package/dist/src/exports/server.d.ts.map +1 -1
- package/dist/src/exports/server.js +1346 -2
- package/dist/src/exports/server.js.map +1 -1
- package/dist/src/exports/subscription-manager-worker-entry.d.ts +1 -0
- package/dist/src/exports/subscription-manager-worker-entry.js +115 -0
- package/dist/src/exports/subscription-manager-worker-entry.js.map +1 -0
- package/dist/src/exports/types.d.ts +4 -6
- package/dist/src/exports/types.js +5 -6
- package/dist/subscription-worker-pahX0zxg.js +511 -0
- package/dist/subscription-worker-pahX0zxg.js.map +1 -0
- package/dist/types_client_rpc-CIsjQQGG.js +105 -0
- package/dist/types_client_rpc-CIsjQQGG.js.map +1 -0
- package/dist/types_db-DXj_l9n-.js +168 -0
- package/dist/types_db-DXj_l9n-.js.map +1 -0
- package/dist/types_server-Dk6o_B7N.js +24 -0
- package/dist/types_server-Dk6o_B7N.js.map +1 -0
- package/dist/types_server-TFo6SWDH.d.ts +216 -0
- package/dist/types_server-TFo6SWDH.d.ts.map +1 -0
- package/package.json +7 -3
- package/dist/src/client/caller_client.d.ts +0 -42
- package/dist/src/client/caller_client.d.ts.map +0 -1
- package/dist/src/client/caller_client.js +0 -77
- package/dist/src/client/caller_client.js.map +0 -1
- package/dist/src/client/client.d.ts +0 -149
- package/dist/src/client/client.d.ts.map +0 -1
- package/dist/src/client/client.js +0 -308
- package/dist/src/client/client.js.map +0 -1
- package/dist/src/client/index.d.ts.map +0 -1
- package/dist/src/client/sub_key.d.ts +0 -20
- package/dist/src/client/sub_key.d.ts.map +0 -1
- package/dist/src/client/sub_key.js +0 -37
- package/dist/src/client/sub_key.js.map +0 -1
- package/dist/src/client/ws_client_manager.d.ts +0 -268
- package/dist/src/client/ws_client_manager.d.ts.map +0 -1
- package/dist/src/client/ws_client_manager.js +0 -1013
- package/dist/src/client/ws_client_manager.js.map +0 -1
- package/dist/src/config.d.ts +0 -117
- package/dist/src/config.d.ts.map +0 -1
- package/dist/src/config.js +0 -2
- package/dist/src/config.js.map +0 -1
- package/dist/src/db/bytes.d.ts +0 -4
- package/dist/src/db/bytes.d.ts.map +0 -1
- package/dist/src/db/bytes.js +0 -24
- package/dist/src/db/bytes.js.map +0 -1
- package/dist/src/db/cache.d.ts +0 -30
- package/dist/src/db/cache.d.ts.map +0 -1
- package/dist/src/db/cache.js +0 -153
- package/dist/src/db/cache.js.map +0 -1
- package/dist/src/db/codec.d.ts +0 -10
- package/dist/src/db/codec.d.ts.map +0 -1
- package/dist/src/db/codec.js +0 -73
- package/dist/src/db/codec.js.map +0 -1
- package/dist/src/db/context.d.ts +0 -28
- package/dist/src/db/context.d.ts.map +0 -1
- package/dist/src/db/context.js +0 -147
- package/dist/src/db/context.js.map +0 -1
- package/dist/src/db/db.d.ts +0 -154
- package/dist/src/db/db.d.ts.map +0 -1
- package/dist/src/db/db.js +0 -23
- package/dist/src/db/db.js.map +0 -1
- package/dist/src/db/mysql.d.ts +0 -120
- package/dist/src/db/mysql.d.ts.map +0 -1
- package/dist/src/db/mysql.js +0 -708
- package/dist/src/db/mysql.js.map +0 -1
- package/dist/src/db/postgres.d.ts +0 -116
- package/dist/src/db/postgres.d.ts.map +0 -1
- package/dist/src/db/postgres.js +0 -998
- package/dist/src/db/postgres.js.map +0 -1
- package/dist/src/db/query.d.ts +0 -174
- package/dist/src/db/query.d.ts.map +0 -1
- package/dist/src/db/query.js +0 -790
- package/dist/src/db/query.js.map +0 -1
- package/dist/src/db/query_definition.d.ts +0 -54
- package/dist/src/db/query_definition.d.ts.map +0 -1
- package/dist/src/db/query_definition.js +0 -47
- package/dist/src/db/query_definition.js.map +0 -1
- package/dist/src/db/realtime_db.d.ts +0 -19
- package/dist/src/db/realtime_db.d.ts.map +0 -1
- package/dist/src/db/realtime_db.js +0 -98
- package/dist/src/db/realtime_db.js.map +0 -1
- package/dist/src/db/schema.d.ts +0 -110
- package/dist/src/db/schema.d.ts.map +0 -1
- package/dist/src/db/schema.js +0 -47
- package/dist/src/db/schema.js.map +0 -1
- package/dist/src/db/types_client_rpc.d.ts +0 -157
- package/dist/src/db/types_client_rpc.d.ts.map +0 -1
- package/dist/src/db/types_client_rpc.js +0 -106
- package/dist/src/db/types_client_rpc.js.map +0 -1
- package/dist/src/db/types_db.d.ts +0 -236
- package/dist/src/db/types_db.d.ts.map +0 -1
- package/dist/src/db/types_db.js +0 -188
- package/dist/src/db/types_db.js.map +0 -1
- package/dist/src/exports/mysql.d.ts.map +0 -1
- package/dist/src/exports/mysql.js.map +0 -1
- package/dist/src/exports/postgres.d.ts.map +0 -1
- package/dist/src/exports/postgres.js.map +0 -1
- package/dist/src/exports/procedure.d.ts.map +0 -1
- package/dist/src/exports/procedure.js.map +0 -1
- package/dist/src/exports/types.d.ts.map +0 -1
- package/dist/src/exports/types.js.map +0 -1
- package/dist/src/helper.d.ts +0 -11
- package/dist/src/helper.d.ts.map +0 -1
- package/dist/src/helper.js +0 -45
- package/dist/src/helper.js.map +0 -1
- package/dist/src/logger.d.ts +0 -7
- package/dist/src/logger.d.ts.map +0 -1
- package/dist/src/logger.js +0 -47
- package/dist/src/logger.js.map +0 -1
- package/dist/src/router/index.d.ts +0 -4
- package/dist/src/router/index.d.ts.map +0 -1
- package/dist/src/router/index.js +0 -4
- package/dist/src/router/index.js.map +0 -1
- package/dist/src/router/procedure.d.ts +0 -204
- package/dist/src/router/procedure.d.ts.map +0 -1
- package/dist/src/router/procedure.js +0 -122
- package/dist/src/router/procedure.js.map +0 -1
- package/dist/src/router/router.d.ts +0 -88
- package/dist/src/router/router.d.ts.map +0 -1
- package/dist/src/router/router.js +0 -88
- package/dist/src/router/router.js.map +0 -1
- package/dist/src/server/hash.d.ts +0 -12
- package/dist/src/server/hash.d.ts.map +0 -1
- package/dist/src/server/hash.js +0 -20
- package/dist/src/server/hash.js.map +0 -1
- package/dist/src/server/hash.test.d.ts +0 -2
- package/dist/src/server/hash.test.d.ts.map +0 -1
- package/dist/src/server/hash.test.js +0 -43
- package/dist/src/server/hash.test.js.map +0 -1
- package/dist/src/server/index.d.ts +0 -9
- package/dist/src/server/index.d.ts.map +0 -1
- package/dist/src/server/index.js +0 -8
- package/dist/src/server/index.js.map +0 -1
- package/dist/src/server/overlap-checker.d.ts +0 -18
- package/dist/src/server/overlap-checker.d.ts.map +0 -1
- package/dist/src/server/overlap-checker.js +0 -247
- package/dist/src/server/overlap-checker.js.map +0 -1
- package/dist/src/server/sub_hash.d.ts +0 -23
- package/dist/src/server/sub_hash.d.ts.map +0 -1
- package/dist/src/server/sub_hash.js +0 -38
- package/dist/src/server/sub_hash.js.map +0 -1
- package/dist/src/server/subscription-manager-client.d.ts +0 -120
- package/dist/src/server/subscription-manager-client.d.ts.map +0 -1
- package/dist/src/server/subscription-manager-client.js +0 -207
- package/dist/src/server/subscription-manager-client.js.map +0 -1
- package/dist/src/server/subscription-manager-types.d.ts +0 -168
- package/dist/src/server/subscription-manager-types.d.ts.map +0 -1
- package/dist/src/server/subscription-manager-types.js +0 -94
- package/dist/src/server/subscription-manager-types.js.map +0 -1
- package/dist/src/server/subscription-manager-worker-entry.d.ts +0 -2
- package/dist/src/server/subscription-manager-worker-entry.d.ts.map +0 -1
- package/dist/src/server/subscription-manager-worker-entry.js +0 -127
- package/dist/src/server/subscription-manager-worker-entry.js.map +0 -1
- package/dist/src/server/subscription-manager-worker-protocol.d.ts +0 -40
- package/dist/src/server/subscription-manager-worker-protocol.d.ts.map +0 -1
- package/dist/src/server/subscription-manager-worker-protocol.js +0 -20
- package/dist/src/server/subscription-manager-worker-protocol.js.map +0 -1
- package/dist/src/server/subscription-manager.d.ts +0 -32
- package/dist/src/server/subscription-manager.d.ts.map +0 -1
- package/dist/src/server/subscription-manager.js +0 -249
- package/dist/src/server/subscription-manager.js.map +0 -1
- package/dist/src/server/subscription-manager.test.d.ts +0 -2
- package/dist/src/server/subscription-manager.test.d.ts.map +0 -1
- package/dist/src/server/subscription-manager.test.js +0 -610
- package/dist/src/server/subscription-manager.test.js.map +0 -1
- package/dist/src/server/subscription-worker.d.ts +0 -32
- package/dist/src/server/subscription-worker.d.ts.map +0 -1
- package/dist/src/server/subscription-worker.js +0 -190
- package/dist/src/server/subscription-worker.js.map +0 -1
- package/dist/src/server/supalive-server.d.ts +0 -158
- package/dist/src/server/supalive-server.d.ts.map +0 -1
- package/dist/src/server/supalive-server.js +0 -1048
- package/dist/src/server/supalive-server.js.map +0 -1
- package/dist/src/server/types_server.d.ts +0 -96
- package/dist/src/server/types_server.d.ts.map +0 -1
- package/dist/src/server/types_server.js +0 -36
- package/dist/src/server/types_server.js.map +0 -1
- package/dist/tsconfig.tsbuildinfo +0 -1
package/dist/src/client/index.js
CHANGED
|
@@ -1,19 +1,1153 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|