@supalive/core 1.20.1 → 1.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/dist/ephemeral-BVeIVuYw.js +276 -0
  2. package/dist/ephemeral-BVeIVuYw.js.map +1 -0
  3. package/dist/ephemeral-CkidSG9y.js +295 -0
  4. package/dist/ephemeral-CkidSG9y.js.map +1 -0
  5. package/dist/index-CzW0TkpK.d.ts +2729 -0
  6. package/dist/index-CzW0TkpK.d.ts.map +1 -0
  7. package/dist/index-DB7_pv9P.d.ts +2746 -0
  8. package/dist/index-DB7_pv9P.d.ts.map +1 -0
  9. package/dist/logger-BJNCJkFh.js +463 -0
  10. package/dist/logger-BJNCJkFh.js.map +1 -0
  11. package/dist/logger-By9Pw-X2.js +454 -0
  12. package/dist/logger-By9Pw-X2.js.map +1 -0
  13. package/dist/mysql-Bvc_iWUy.d.ts +114 -0
  14. package/dist/mysql-Bvc_iWUy.d.ts.map +1 -0
  15. package/dist/mysql-CcIuIAt0.js +622 -0
  16. package/dist/mysql-CcIuIAt0.js.map +1 -0
  17. package/dist/mysql-DTK2f5oU.d.ts +114 -0
  18. package/dist/mysql-DTK2f5oU.d.ts.map +1 -0
  19. package/dist/one-shot-query-BNYLU19W.js +573 -0
  20. package/dist/one-shot-query-BNYLU19W.js.map +1 -0
  21. package/dist/one-shot-query-BSK7adwG.js +573 -0
  22. package/dist/one-shot-query-BSK7adwG.js.map +1 -0
  23. package/dist/one-shot-query-DAAHGTcI.js +573 -0
  24. package/dist/one-shot-query-DAAHGTcI.js.map +1 -0
  25. package/dist/overlap-checker-DseE1Lvh.js +300 -0
  26. package/dist/overlap-checker-DseE1Lvh.js.map +1 -0
  27. package/dist/postgres-Bp7SMb9D.d.ts +118 -0
  28. package/dist/postgres-Bp7SMb9D.d.ts.map +1 -0
  29. package/dist/postgres-C5CbPBcp.d.ts +118 -0
  30. package/dist/postgres-C5CbPBcp.d.ts.map +1 -0
  31. package/dist/postgres-CLhoKn_R.js +867 -0
  32. package/dist/postgres-CLhoKn_R.js.map +1 -0
  33. package/dist/realtime_db-CTGOrFuV.js +463 -0
  34. package/dist/realtime_db-CTGOrFuV.js.map +1 -0
  35. package/dist/src/client/index.d.ts +2 -2
  36. package/dist/src/client/index.js +6 -5
  37. package/dist/src/client/index.js.map +1 -1
  38. package/dist/src/exports/mysql.d.ts +1 -1
  39. package/dist/src/exports/mysql.js +1 -1
  40. package/dist/src/exports/postgres.d.ts +1 -1
  41. package/dist/src/exports/postgres.js +1 -1
  42. package/dist/src/exports/procedure.d.ts +2 -2
  43. package/dist/src/exports/procedure.d.ts.map +1 -1
  44. package/dist/src/exports/procedure.js +2 -1
  45. package/dist/src/exports/procedure.js.map +1 -1
  46. package/dist/src/exports/schema-sql.d.ts +1 -1
  47. package/dist/src/exports/server.d.ts +56 -5
  48. package/dist/src/exports/server.d.ts.map +1 -1
  49. package/dist/src/exports/server.js +168 -10
  50. package/dist/src/exports/server.js.map +1 -1
  51. package/dist/src/exports/sub-manager-worker-entry.js +1 -1
  52. package/dist/src/exports/types.d.ts +2 -2
  53. package/dist/src/exports/types.js +2 -2
  54. package/dist/sub-worker-dispatch-C20KTMfg.js +925 -0
  55. package/dist/sub-worker-dispatch-C20KTMfg.js.map +1 -0
  56. package/dist/sub-worker-dispatch-CQCuwZPm.js +925 -0
  57. package/dist/sub-worker-dispatch-CQCuwZPm.js.map +1 -0
  58. package/dist/sub-worker-dispatch-Dwrp3c40.js +925 -0
  59. package/dist/sub-worker-dispatch-Dwrp3c40.js.map +1 -0
  60. package/dist/types_client_rpc-CqUVHTwT.js +163 -0
  61. package/dist/types_client_rpc-CqUVHTwT.js.map +1 -0
  62. package/dist/types_db-DPMeNETt.js +173 -0
  63. package/dist/types_db-DPMeNETt.js.map +1 -0
  64. package/dist/types_server-hiwqDm4G.d.ts +737 -0
  65. package/dist/types_server-hiwqDm4G.d.ts.map +1 -0
  66. package/dist/types_server-xHmZ6baD.d.ts +737 -0
  67. package/dist/types_server-xHmZ6baD.d.ts.map +1 -0
  68. package/package.json +1 -1
@@ -0,0 +1,925 @@
1
+ import { r as supaliveStringify } from "./helper-zdJT5FUc.js";
2
+ import { o as EphemeralStore } from "./ephemeral-CkidSG9y.js";
3
+ import { a as mapWriteSetToRaw, i as mapReadSetToRaw, n as checkWriteSetAffectsReadSet, r as evaluateCacheFreshness } from "./overlap-checker-DseE1Lvh.js";
4
+ import { b as WriteEntrySchema, m as QueryCacheMetadataSchema, n as BigIntSchema, p as QueryCacheEntrySchema, r as CachedPgMetadataSchema, y as ReadEntrySchema } from "./types_db-DPMeNETt.js";
5
+ import { t as MySqlDatabase } from "./mysql-CcIuIAt0.js";
6
+ import { t as PgDatabase } from "./postgres-CLhoKn_R.js";
7
+ import { a as logLevelOf, o as logger$1, t as SupaliveDb } from "./realtime_db-CTGOrFuV.js";
8
+ import z from "zod";
9
+ import { ExponentialBackoff, handleAll, retry } from "cockatiel";
10
+ import { createHash } from "node:crypto";
11
+ import Redis from "ioredis";
12
+ //#region src/server/retention-watermark.ts
13
+ /** Redis pub/sub channel the sub-manager publishes the retention watermark on
14
+ * after each prune (and at startup). App-server instances subscribe and update
15
+ * their in-memory {@link RetentionWatermark}. Payload is the watermark `ts` as a
16
+ * string. */
17
+ const RETENTION_WATERMARK_CHANNEL = "supalive:retention_watermark";
18
+ /**
19
+ * In-memory holder for the commit-log retention watermark: the oldest commit-log
20
+ * ts still retained after prune (see {@link Database.getMinSnapshotTimestamp}).
21
+ * Read on the hot path by the query-cache freshness check — a plain variable, no
22
+ * per-check Redis/DB round trip. Loaded once at startup from the `metadata`
23
+ * table and kept fresh out-of-band: the sub-manager broadcasts the new watermark
24
+ * (in-process to its workers, over Redis pub/sub to app servers) each time it
25
+ * prunes.
26
+ *
27
+ * `set` is monotonic — the watermark only ever moves forward (pruning never
28
+ * un-deletes rows), so an out-of-order or stale update can never lower it, which
29
+ * keeps the freshness guard on the safe side (an over-estimate only costs an
30
+ * extra recompute; an under-estimate could serve stale data).
31
+ *
32
+ * Initial value is the -1n genesis (not 0), matching `getMinSnapshotTimestamp`
33
+ * and the empty-DB snapshot ts — so before anything is pruned the guard never
34
+ * rejects a genesis-era entry (whose snapshot is -1).
35
+ */
36
+ var RetentionWatermark = class {
37
+ ts = -1n;
38
+ get() {
39
+ return this.ts;
40
+ }
41
+ set(ts) {
42
+ if (ts > this.ts) this.ts = ts;
43
+ }
44
+ /** Seed from the persisted watermark (metadata table). Returns the current value. */
45
+ async loadFrom(db) {
46
+ this.set(await db.getMinSnapshotTimestamp());
47
+ return this.ts;
48
+ }
49
+ };
50
+ //#endregion
51
+ //#region src/server/sub-manager-types.ts
52
+ const RegisterSubscriptionParamsSchema = z.object({
53
+ /**
54
+ * Caller-computed FNV-1a routing key + identity for this subscription.
55
+ * The sub-manager trusts these values rather than re-deriving them from
56
+ * (queryName, args, queryIdentity); this avoids one stringify+SHA1 per
57
+ * register RPC. Trust is fine: the sub-manager RPC channel is fully
58
+ * internal.
59
+ */
60
+ subId: z.string(),
61
+ cacheKey: z.string(),
62
+ queryName: z.string(),
63
+ args: z.unknown(),
64
+ /**
65
+ * Segmentation key used in the subId/cacheKey hash. Defaults to the
66
+ * authenticated user's id ("anonymous" when none), unless the
67
+ * procedure overrides it via `QueryConfig.queryIdentity`.
68
+ */
69
+ queryIdentity: z.string(),
70
+ instanceName: z.string(),
71
+ lastSnapshotTs: BigIntSchema
72
+ });
73
+ const RegisterSubscriptionResultSchema = z.object({
74
+ cacheKey: z.string(),
75
+ recompute: z.boolean(),
76
+ readSet: z.array(ReadEntrySchema),
77
+ /**
78
+ * Ephemeral dependencies recovered from the cache metadata, mirroring
79
+ * `readSet`. Non-empty always implies `recompute: true` (ephemeral state has
80
+ * no commit log, so freshness can never be proven) — it is returned so the
81
+ * worker's key index survives a restart without waiting for the app server's
82
+ * first `updateSubscriptionReadSet`.
83
+ */
84
+ ephemeralKeys: z.array(z.string()).default([]),
85
+ version: z.string().optional()
86
+ });
87
+ const RegisterSubscriptionBatchParamsSchema = z.array(RegisterSubscriptionParamsSchema);
88
+ const RegisterSubscriptionBatchResultSchema = z.array(RegisterSubscriptionResultSchema);
89
+ const UpdateSubscriptionReadSetParamsSchema = z.object({
90
+ cacheKey: z.string(),
91
+ subId: z.string(),
92
+ instanceName: z.string(),
93
+ lastSnapshotTs: BigIntSchema,
94
+ readSet: z.array(ReadEntrySchema),
95
+ /** Ephemeral keys the recompute read (db/ephemeral.ts). Replaces the
96
+ * previously tracked set for this subId, so keys a handler stopped reading
97
+ * stop invalidating it. */
98
+ ephemeralKeys: z.array(z.string()).default([])
99
+ });
100
+ /**
101
+ * `tracked: false` means the sub-manager has no record of this subId (e.g.
102
+ * a restart wiped state and the caller hasn't re-registered yet). The app
103
+ * server should treat this as a signal to register the sub before retrying.
104
+ */
105
+ const UpdateSubscriptionReadSetResultSchema = z.object({ tracked: z.boolean() });
106
+ const UnregisterSubscriptionParamsSchema = z.object({
107
+ subId: z.string(),
108
+ instanceName: z.string()
109
+ });
110
+ const UnregisterSubscriptionsParamsSchema = z.array(UnregisterSubscriptionParamsSchema);
111
+ const UnregisterSubscriptionResultSchema = z.object({ ok: z.boolean() });
112
+ const UnregisterSubscriptionsResultSchema = z.array(z.object({ ok: z.boolean() }));
113
+ const InvalidateWritesetParamsSchema = z.object({ writeSet: z.array(WriteEntrySchema) });
114
+ /**
115
+ * Ephemeral counterpart of {@link InvalidateWritesetParamsSchema}. Carries just
116
+ * the written keys — matching is an exact key lookup in the worker's index, with
117
+ * no predicate to evaluate and no row images to compare, so there is nothing
118
+ * else to send. The recompute re-reads the current value from Redis, so the
119
+ * VALUE never travels on this path either.
120
+ */
121
+ const InvalidateEphemeralParamsSchema = z.object({
122
+ keys: z.array(z.string()),
123
+ /**
124
+ * Monotonic write sequence (db/ephemeral.ts). Stands in for `commitTs` as
125
+ * the cross-instance recompute-lock discriminator, so every instance racing
126
+ * over the same write agrees on which write it is racing for.
127
+ */
128
+ version: z.string()
129
+ });
130
+ const AffectedSubscriptionSchema = z.object({
131
+ subId: z.string(),
132
+ queryName: z.string(),
133
+ args: z.unknown(),
134
+ cacheKey: z.string(),
135
+ /**
136
+ * Every live instance that currently holds this subId. Routing rules:
137
+ * • If the writer is in this list, it recomputes locally and publishes
138
+ * an `update` to the rest (its own id is filtered by the publisher).
139
+ * • Otherwise the writer broadcasts a `recompute-race` to every entry;
140
+ * receivers race for a Redis lock and the winner recomputes.
141
+ */
142
+ notifyInstances: z.array(z.string())
143
+ });
144
+ const InvalidateWritesetResultSchema = z.object({ affected: z.array(AffectedSubscriptionSchema) });
145
+ const InvalidateEphemeralResultSchema = InvalidateWritesetResultSchema;
146
+ const RegistrationRecordSchema = z.object({
147
+ queryName: z.string(),
148
+ args: z.unknown(),
149
+ queryIdentity: z.string(),
150
+ cacheKey: z.string()
151
+ });
152
+ //#endregion
153
+ //#region src/db/cache.ts
154
+ const logger = logger$1.child({ "name": "CACHE_LAYER" }, { level: logLevelOf("SERVER_LOG_LEVEL") });
155
+ const redisRetryPolicy = retry(handleAll, {
156
+ maxAttempts: 3,
157
+ backoff: new ExponentialBackoff({
158
+ initialDelay: 5,
159
+ maxDelay: 2300,
160
+ exponent: 2
161
+ })
162
+ });
163
+ /** Atomic one-shot recompute write: set data + metadata iff incoming version
164
+ * (ARGV[3]) is not older than the stored one. KEYS: data, meta. */
165
+ const WRITE_DATA_AND_META = `
166
+ local cur = redis.call('GET', KEYS[2])
167
+ if cur then
168
+ local ok, m = pcall(cjson.decode, cur)
169
+ if ok and type(m) == 'table' and m.version and tonumber(ARGV[3]) < tonumber(m.version) then
170
+ return 0
171
+ end
172
+ end
173
+ redis.call('SET', KEYS[1], ARGV[1], 'EX', tonumber(ARGV[4]))
174
+ redis.call('SET', KEYS[2], ARGV[2], 'EX', tonumber(ARGV[4]))
175
+ return 1`;
176
+ /** Data-only write (subscribe path): set data iff incoming version (ARGV[2]) is
177
+ * not older than the stored metadata's version. KEYS: data, meta. */
178
+ const WRITE_DATA_ONLY = `
179
+ local cur = redis.call('GET', KEYS[2])
180
+ if cur then
181
+ local ok, m = pcall(cjson.decode, cur)
182
+ if ok and type(m) == 'table' and m.version and tonumber(ARGV[2]) < tonumber(m.version) then
183
+ return 0
184
+ end
185
+ end
186
+ redis.call('SET', KEYS[1], ARGV[1], 'EX', tonumber(ARGV[3]))
187
+ return 1`;
188
+ /** Metadata-only replace (subscription recompute): set meta iff incoming version
189
+ * (ARGV[2]) is not older than the stored one. KEYS: meta. */
190
+ const REPLACE_META = `
191
+ local cur = redis.call('GET', KEYS[1])
192
+ if cur then
193
+ local ok, m = pcall(cjson.decode, cur)
194
+ if ok and type(m) == 'table' and m.version and tonumber(ARGV[2]) < tonumber(m.version) then
195
+ return 0
196
+ end
197
+ end
198
+ redis.call('SET', KEYS[1], ARGV[1], 'EX', tonumber(ARGV[3]))
199
+ return 1`;
200
+ /** Metadata-only horizon advance (still-fresh subscription): bump lastSnapshotTs
201
+ * ONLY, without recomputing. Applies ARGV[1] iff the new horizon (ARGV[2]) is
202
+ * strictly ahead of the stored one AND no newer computation (stored.version >
203
+ * ARGV[3], the version the caller based its advance on) has landed meanwhile —
204
+ * so an advance never reverts a fresh recompute. KEYS: meta. */
205
+ const ADVANCE_META = `
206
+ local cur = redis.call('GET', KEYS[1])
207
+ if not cur then return 0 end
208
+ local ok, m = pcall(cjson.decode, cur)
209
+ if not ok or type(m) ~= 'table' then return 0 end
210
+ if tonumber(ARGV[2]) <= tonumber(m.lastSnapshotTs) then return 0 end
211
+ if m.version and tonumber(m.version) > tonumber(ARGV[3]) then return 0 end
212
+ redis.call('SET', KEYS[1], ARGV[1], 'EX', tonumber(ARGV[4]))
213
+ return 1`;
214
+ var CacheLayer = class {
215
+ redis;
216
+ ttlSeconds;
217
+ /** Cached EVALSHA digests keyed by Lua source (see evalScript). */
218
+ scriptShaCache = /* @__PURE__ */ new Map();
219
+ constructor(redis, ttlSeconds = 3600) {
220
+ this.redis = redis;
221
+ this.ttlSeconds = ttlSeconds;
222
+ }
223
+ /**
224
+ * Run a Lua script via EVALSHA using the script's SHA1 (computed locally — the
225
+ * same digest Redis keys scripts by, so no SCRIPT LOAD round-trip is needed on
226
+ * the hot path). On NOSCRIPT (the script isn't in Redis's cache yet, e.g. first
227
+ * use or after a Redis restart/failover) load it explicitly via SCRIPT LOAD and
228
+ * re-run via EVALSHA — so the script ends up cached server-side and subsequent
229
+ * EVALSHA calls hit. The runtime client is always ioredis (see initCacheLayer).
230
+ */
231
+ async evalScript(src, keys, args) {
232
+ const redis = this.redis;
233
+ let sha = this.scriptShaCache.get(src);
234
+ if (!sha) {
235
+ sha = createHash("sha1").update(src).digest("hex");
236
+ this.scriptShaCache.set(src, sha);
237
+ }
238
+ return redisRetryPolicy.execute(async () => {
239
+ try {
240
+ return await redis.evalsha(sha, keys.length, ...keys, ...args);
241
+ } catch (err) {
242
+ if (err instanceof Error && err.message.includes("NOSCRIPT")) {
243
+ const loadedSha = await redis.call("SCRIPT", "LOAD", src);
244
+ this.scriptShaCache.set(src, loadedSha);
245
+ return await redis.evalsha(loadedSha, keys.length, ...keys, ...args);
246
+ }
247
+ throw err;
248
+ }
249
+ });
250
+ }
251
+ /** One-shot recompute: atomically write data + metadata (CAS on version). */
252
+ async setQueryCacheAndMetadataFor(cacheKey, data, metaData, ttlSeconds) {
253
+ const ttl = ttlSeconds ?? this.ttlSeconds;
254
+ const dataJson = supaliveStringify({
255
+ data,
256
+ version: metaData.version,
257
+ lastSnapshotTs: metaData.lastSnapshotTs
258
+ });
259
+ const metaJson = supaliveStringify(metaData);
260
+ await this.evalScript(WRITE_DATA_AND_META, [this.getQueryCacheDataKey(cacheKey), this.getQeruyCacheMetaKey(cacheKey)], [
261
+ dataJson,
262
+ metaJson,
263
+ metaData.version,
264
+ String(ttl)
265
+ ]);
266
+ }
267
+ /** Subscribe path: write the data blob only (CAS against the metadata's
268
+ * version). The metadata is written separately by the sub-worker; the
269
+ * `version` links the two so a divergent pair is detected on read. Takes the
270
+ * snapshot ts directly (rather than a pre-stringified version) so the entry
271
+ * can carry `lastSnapshotTs` as its own field; `version` is derived from it,
272
+ * preserving the `version === String(ts)` convention the CAS relies on. */
273
+ async setQueryCacheFor(cacheKey, data, lastSnapshotTs, ttlSeconds) {
274
+ const ttl = ttlSeconds ?? this.ttlSeconds;
275
+ const version = String(lastSnapshotTs);
276
+ const dataJson = supaliveStringify({
277
+ data,
278
+ version,
279
+ lastSnapshotTs
280
+ });
281
+ await this.evalScript(WRITE_DATA_ONLY, [this.getQueryCacheDataKey(cacheKey), this.getQeruyCacheMetaKey(cacheKey)], [
282
+ dataJson,
283
+ version,
284
+ String(ttl)
285
+ ]);
286
+ }
287
+ async getQueryCacheFor(cacheKey) {
288
+ const dataRaw = await redisRetryPolicy.execute(() => this.redis.get(this.getQueryCacheDataKey(cacheKey)));
289
+ if (!dataRaw) return null;
290
+ try {
291
+ const parsed = typeof dataRaw === "string" ? JSON.parse(dataRaw) : dataRaw;
292
+ return QueryCacheEntrySchema.parse(parsed);
293
+ } catch {
294
+ return null;
295
+ }
296
+ }
297
+ /** Subscription recompute: replace the whole metadata (CAS on version). */
298
+ async replaceQueryCacheMetadata(cacheKey, metaData, ttlSeconds) {
299
+ const ttl = ttlSeconds ?? this.ttlSeconds;
300
+ const metaJson = supaliveStringify(metaData);
301
+ await this.evalScript(REPLACE_META, [this.getQeruyCacheMetaKey(cacheKey)], [
302
+ metaJson,
303
+ metaData.version,
304
+ String(ttl)
305
+ ]);
306
+ }
307
+ /** Still-fresh subscription: advance `lastSnapshotTs` only, preserving the
308
+ * data's `version` + `readSet` (no recompute). Monotonic and non-clobbering
309
+ * — see ADVANCE_META. `metaData` carries the new horizon plus the version +
310
+ * readSet the caller read, which the script keeps unless a newer computation
311
+ * has since landed. */
312
+ async advanceQueryCacheMetadata(cacheKey, metaData, ttlSeconds) {
313
+ const ttl = ttlSeconds ?? this.ttlSeconds;
314
+ const metaJson = supaliveStringify(metaData);
315
+ await this.evalScript(ADVANCE_META, [this.getQeruyCacheMetaKey(cacheKey)], [
316
+ metaJson,
317
+ String(metaData.lastSnapshotTs),
318
+ metaData.version,
319
+ String(ttl)
320
+ ]);
321
+ }
322
+ async getQueryCacheMetaData(cacheKey) {
323
+ const dataRaw = await redisRetryPolicy.execute(() => this.redis.get(this.getQeruyCacheMetaKey(cacheKey)));
324
+ if (!dataRaw) return null;
325
+ try {
326
+ const parsed = typeof dataRaw === "string" ? JSON.parse(dataRaw) : dataRaw;
327
+ return QueryCacheMetadataSchema.parse(parsed);
328
+ } catch {
329
+ return null;
330
+ }
331
+ }
332
+ async persistRegistrationRecord(subId, record) {
333
+ const data = supaliveStringify(record);
334
+ await redisRetryPolicy.execute(() => this.redis.set(this.getRegistrationKey(subId), data));
335
+ }
336
+ async deleteRegistrationRecord(subId) {
337
+ await redisRetryPolicy.execute(() => this.redis.del(this.getRegistrationKey(subId)));
338
+ }
339
+ async deleteRegistrationsRecord(subIds) {
340
+ if (subIds.length === 0) return;
341
+ await redisRetryPolicy.execute(() => this.redis.del(...subIds.map((subId) => this.getRegistrationKey(subId))));
342
+ }
343
+ async fetchRegistrationRecordsBatch(subIds) {
344
+ const result = /* @__PURE__ */ new Map();
345
+ const CHUNK_SIZE = 100;
346
+ for (let i = 0; i < subIds.length; i += CHUNK_SIZE) {
347
+ const chunk = subIds.slice(i, i + CHUNK_SIZE);
348
+ const keys = chunk.map((id) => `sl:reg:${id}`);
349
+ try {
350
+ if (keys.length === 0) continue;
351
+ const rawValues = await redisRetryPolicy.execute(() => this.redis.mget(...keys));
352
+ for (let j = 0; j < chunk.length; j++) {
353
+ const subId = chunk[j];
354
+ const raw = rawValues[j];
355
+ const rawJson = typeof raw === "string" ? JSON.parse(raw) : raw;
356
+ if (!raw) result.set(subId, null);
357
+ else try {
358
+ const parsed = RegistrationRecordSchema.parse(rawJson);
359
+ result.set(subId, parsed);
360
+ } catch {
361
+ logger.error(`Failed to parse sl:reg:${subId} during batch recovery`);
362
+ result.set(subId, null);
363
+ }
364
+ }
365
+ } catch (err) {
366
+ logger.error(err, "Failed to fetch registration records batch");
367
+ for (const subId of chunk) result.set(subId, null);
368
+ }
369
+ }
370
+ return result;
371
+ }
372
+ async getCachedTableColumnTypes(key) {
373
+ const dataRaw = await redisRetryPolicy.execute(() => this.redis.get(key));
374
+ if (!dataRaw) return null;
375
+ try {
376
+ const parsed = typeof dataRaw === "string" ? JSON.parse(dataRaw) : dataRaw;
377
+ return CachedPgMetadataSchema.parse(parsed);
378
+ } catch {
379
+ return null;
380
+ }
381
+ }
382
+ async setCachedTableColumnsTypes(key, value) {
383
+ await redisRetryPolicy.execute(() => this.redis.set(key, supaliveStringify(value)));
384
+ }
385
+ /**
386
+ * Race participant: try to claim `lock:recompute:<subId>:<commitTs>`
387
+ * with `SET NX EX`. The single Redis round-trip both arbitrates and
388
+ * sets a TTL fallback in case the winner crashes mid-recompute.
389
+ */
390
+ async tryAcquireRecomputeLock(instanceId, subId, commitTs, ttl) {
391
+ const key = `lock:recompute:${subId}:${commitTs}`;
392
+ return redisRetryPolicy.execute(async () => {
393
+ try {
394
+ return await this.redis.set(key, instanceId, "EX", ttl, "NX") === "OK";
395
+ } catch (err) {
396
+ logger.error(err, `Failed to acquire recompute lock ${key}`);
397
+ return false;
398
+ }
399
+ });
400
+ }
401
+ /** Raw value at an ephemeral key, or `null` when absent/expired. */
402
+ async ephemeralGet(key) {
403
+ const raw = await redisRetryPolicy.execute(() => this.redis.get(key));
404
+ return typeof raw === "string" ? raw : null;
405
+ }
406
+ /** Raw values for `keys`, positionally aligned; `null` for each absent key. */
407
+ async ephemeralGetMany(keys) {
408
+ if (keys.length === 0) return [];
409
+ return (await redisRetryPolicy.execute(() => this.redis.mget(...keys))).map((v) => typeof v === "string" ? v : null);
410
+ }
411
+ getRegistrationKey(subId) {
412
+ return `sl:reg:${subId}`;
413
+ }
414
+ getQueryCacheDataKey(cacheKey) {
415
+ return `sl:cache:${cacheKey}`;
416
+ }
417
+ getQeruyCacheMetaKey(cacheKey) {
418
+ return `sl:meta:${cacheKey}`;
419
+ }
420
+ };
421
+ //#endregion
422
+ //#region src/db/init_db.ts
423
+ /**
424
+ * Current core runtime-bootstrap version. Bump this whenever core needs a new
425
+ * runtime database operation — a data seed, a backfill, or a schema change that
426
+ * must be applied directly at runtime rather than through the migration
427
+ * pipeline. Each increment adds a case to `Database.coreMigrationStatements`.
428
+ *
429
+ * The core *schema* (the `commit_logs`/`metadata` tables and the
430
+ * `global_commit_ts` sequence/counter) is NOT versioned here — it comes from
431
+ * the generated schema (`coreTablesSql` / `generateSchemaSql`), which must be
432
+ * applied to the database BEFORE `initCore` runs.
433
+ */
434
+ const CORE_VERSION = 1n;
435
+ /**
436
+ * Bring the core runtime state up to {@link CORE_VERSION}. Idempotent and safe
437
+ * to call from every process at startup: a lock-free version read short-circuits
438
+ * the common "already current" case, and the actual migration runs under a
439
+ * cross-process lock with a re-check so only one process applies it.
440
+ *
441
+ * Assumes the core schema already exists (apply the generated schema first);
442
+ * the `metadata` table is where the version and seed rows live.
443
+ */
444
+ async function initCore(db) {
445
+ if (await db.getCoreVersion() >= 1n) return;
446
+ await db.withCoreInitLock(async (ctx) => {
447
+ const from = await ctx.getVersion();
448
+ if (from >= 1n) return;
449
+ for (let v = from + 1n; v <= CORE_VERSION; v++) {
450
+ const statements = db.coreMigrationStatements(v);
451
+ logger$1.info(`Migrating core schema ${v - 1n} -> ${v} (${statements.length} statements)`);
452
+ for (const sql of statements) await ctx.exec(sql);
453
+ }
454
+ await ctx.setVersion(CORE_VERSION);
455
+ logger$1.info(`Core schema now at version ${CORE_VERSION}`);
456
+ });
457
+ }
458
+ /**
459
+ * Default and env override for the per-statement DB timeout. PG enforces
460
+ * this via `statement_timeout` (server cancels), MySQL via the per-query
461
+ * `timeout` option (driver issues KILL QUERY). Set 0 to disable.
462
+ */
463
+ const DB_QUERY_TIMEOUT_MS = Number(process.env.SUPALIVE_DB_QUERY_TIMEOUT_MS ?? 3e4);
464
+ async function initDatabase(options) {
465
+ const impl = createDatabase(options.DbConfig, options.workerId);
466
+ const replicaImpl = options.ReplicaDbConfig ? createDatabase(options.ReplicaDbConfig, options.workerId) : void 0;
467
+ const ephemeral = new EphemeralStore(options.CacheLayer, { defaultTtlSeconds: options.ephemeralTtlSeconds });
468
+ const supaliveDb = SupaliveDb.create({
469
+ db: impl,
470
+ ephemeral,
471
+ replica: replicaImpl
472
+ });
473
+ await initCore(impl);
474
+ await bootstrapDbTypes(supaliveDb, options.CacheLayer, options.forceBootstrapReload ?? false);
475
+ return supaliveDb;
476
+ }
477
+ function createDatabase(config, workerId) {
478
+ const bindings = { "db": config.type };
479
+ if (workerId) bindings["worker"] = workerId;
480
+ if (config.type === "postgres") return new PgDatabase(logger$1.child(bindings), {
481
+ ...config,
482
+ connectionString: config.connectionString,
483
+ min: config.minConnections ?? 0,
484
+ max: config.maxConnections ?? 20,
485
+ connectionTimeoutMillis: config.connectionTimeoutMillis ?? 15e3,
486
+ idleTimeoutMillis: config.idleTimeoutMillis ?? 3e4,
487
+ statement_timeout: DB_QUERY_TIMEOUT_MS > 0 ? DB_QUERY_TIMEOUT_MS : void 0
488
+ });
489
+ else return new MySqlDatabase(logger$1.child(bindings), {
490
+ uri: config.connectionString,
491
+ supportBigNumbers: true,
492
+ connectionLimit: config.maxConnections ?? 20,
493
+ connectTimeout: config.connectionTimeoutMillis ?? 15e3,
494
+ idleTimeout: config.idleTimeoutMillis ?? 3e4,
495
+ queueLimit: config.queueLimit ?? 0,
496
+ timezone: "Z",
497
+ queryTimeoutMs: DB_QUERY_TIMEOUT_MS
498
+ });
499
+ }
500
+ /**
501
+ * Eagerly load column types into the apply-path cache at server start.
502
+ * In production, the result is cached in Redis under
503
+ * `supalive:schema:types:${app_migration}` (the applied-migration marker from
504
+ * the `metadata` table, via {@link Database.getAppMigration}) so other
505
+ * instances starting cold can skip the pg_attribute round-trip — and a schema
506
+ * migration bumps that marker, so the key changes and stale types aren't
507
+ * reused. In any
508
+ * other env we go straight to PG every time so local schema iteration
509
+ * is never blocked by stale Redis state. Migrations should publish a
510
+ * `schema:reload` message (see {@link publishSchemaReload}) after
511
+ * applying DDL — every instance picks that up and re-bootstraps.
512
+ */
513
+ async function bootstrapDbTypes(db, cache, forceReload = false) {
514
+ if (db.impl.dbType === "mysql") return;
515
+ const inProd = process.env.NODE_ENV === "production";
516
+ const key = `supalive:schema:types:${await db.impl.getAppMigration()}`;
517
+ try {
518
+ await db.impl.bootstrapColumnTypes({
519
+ cacheKey: key,
520
+ redis: inProd && !forceReload ? cache : void 0
521
+ });
522
+ logger$1.info({ columnsTypes: db.impl.columnTypes }, `Bootstrapped db column types (redis=${inProd}, key=${key})`);
523
+ } catch (err) {
524
+ logger$1.error(err, "Failed to bootstrap db column types");
525
+ throw err;
526
+ }
527
+ }
528
+ function initCacheLayer(config) {
529
+ const redis = new Redis(config.upstash.redisUrl, {
530
+ connectTimeout: config.connectionTimeout ?? 15e3,
531
+ maxRetriesPerRequest: config.commandMaxRetries ?? 1,
532
+ keepAlive: 1e4
533
+ });
534
+ const redisSub = config.createSubClient ? new Redis(config.upstash.redisUrl, {
535
+ connectTimeout: config.connectionTimeout ?? 15e3,
536
+ maxRetriesPerRequest: config.commandSubMaxRetries ?? 5,
537
+ disableClientInfo: true,
538
+ retryStrategy: (times) => {
539
+ if (times > 5) return null;
540
+ return Math.min(times * 200, 2e3);
541
+ }
542
+ }) : void 0;
543
+ return {
544
+ cacheLayer: new CacheLayer(redis, config.cacheTtlSeconds ?? 3600),
545
+ redisSubClient: redisSub
546
+ };
547
+ }
548
+ //#endregion
549
+ //#region src/server/sub-worker.ts
550
+ var SubscriptionWorker = class {
551
+ id;
552
+ subscriptions = /* @__PURE__ */ new Map();
553
+ /**
554
+ * Reverse index `ephemeral key → subIds that read it`, kept in sync with each
555
+ * tracked subscription's `ephemeralKeys`. This is the whole ephemeral
556
+ * invalidation path: an exact Map lookup per written key, versus the full
557
+ * slice scan + predicate evaluation the write-set path needs.
558
+ */
559
+ ephemeralIndex = /* @__PURE__ */ new Map();
560
+ cache;
561
+ db;
562
+ retentionWatermark = new RetentionWatermark();
563
+ logger;
564
+ constructor(id, cache, db) {
565
+ this.id = id;
566
+ this.cache = cache;
567
+ this.db = db;
568
+ this.logger = logger$1.child({
569
+ name: "SUBS_WRK",
570
+ wid: id
571
+ }, { level: logLevelOf("SUBS_MANAGER_SERVER_LOG_LEVEL") });
572
+ }
573
+ size() {
574
+ return this.subscriptions.size;
575
+ }
576
+ /**
577
+ * Point `subId`'s ephemeral index entries at exactly `keys`, adding the new
578
+ * ones and dropping any the subscription no longer reads. Called on every
579
+ * register/recompute, because a handler's key set is data-dependent — a
580
+ * conditional branch can start or stop reading a key between runs, and a key
581
+ * left behind would keep waking a subscription that no longer cares.
582
+ */
583
+ reindexEphemeralKeys(subId, keys) {
584
+ const next = new Set(keys);
585
+ const previous = this.subscriptions.get(subId)?.ephemeralKeys;
586
+ if (previous) for (const key of previous) {
587
+ if (next.has(key)) continue;
588
+ this.dropEphemeralIndexEntry(key, subId);
589
+ }
590
+ for (const key of next) {
591
+ let holders = this.ephemeralIndex.get(key);
592
+ if (!holders) this.ephemeralIndex.set(key, holders = /* @__PURE__ */ new Set());
593
+ holders.add(subId);
594
+ }
595
+ }
596
+ /** Remove every ephemeral index entry pointing at `subId`. */
597
+ clearEphemeralKeys(subId) {
598
+ const keys = this.subscriptions.get(subId)?.ephemeralKeys;
599
+ if (!keys) return;
600
+ for (const key of keys) this.dropEphemeralIndexEntry(key, subId);
601
+ }
602
+ /** Drop one (key, subId) pair, deleting the bucket once it empties so the
603
+ * index doesn't accumulate empty Sets for churning keys. */
604
+ dropEphemeralIndexEntry(key, subId) {
605
+ const holders = this.ephemeralIndex.get(key);
606
+ if (!holders) return;
607
+ holders.delete(subId);
608
+ if (holders.size === 0) this.ephemeralIndex.delete(key);
609
+ }
610
+ /** Number of distinct ephemeral keys currently indexed (diagnostics/tests). */
611
+ ephemeralKeyCount() {
612
+ return this.ephemeralIndex.size;
613
+ }
614
+ has(subId) {
615
+ return this.subscriptions.has(subId);
616
+ }
617
+ /**
618
+ * Lowest `lastSnapshotTs` across this worker's subscriptions, or `null`
619
+ * when there are none. Used by the dispatcher to derive a safe prune watermark.
620
+ */
621
+ minSubscriptionTs() {
622
+ let min = null;
623
+ for (const sub of this.subscriptions.values()) if (min === null || sub.lastSnapshotTs < min) min = sub.lastSnapshotTs;
624
+ return min;
625
+ }
626
+ /**
627
+ * Seed/repair the persisted retained watermark from the actual oldest retained
628
+ * commit log (`min(ts)`), then load it into memory and broadcast it. Called
629
+ * once at sub-manager startup — the `min(ts)` scan is off the hot path, and
630
+ * it corrects `metadata.min_retained_ts` even for a DB that was pruned before
631
+ * this feature existed. Returns the resulting watermark so the manager can
632
+ * broadcast it to the other workers.
633
+ */
634
+ async refreshRetentionWatermark() {
635
+ const real = await this.db.getMinCommitLogTs() ?? 0n;
636
+ await this.db.updateMinSnapshotTimestamp(real);
637
+ const persisted = await this.db.getMinSnapshotTimestamp();
638
+ this.retentionWatermark.set(persisted);
639
+ await this.publishRetentionWatermark(persisted);
640
+ return this.retentionWatermark.get();
641
+ }
642
+ /** Adopt a watermark broadcast by the manager (in-process from the pruning
643
+ * worker). Monotonic, so an out-of-order message can't lower it. */
644
+ setRetentionWatermark(ts) {
645
+ this.retentionWatermark.set(ts);
646
+ }
647
+ /**
648
+ * Prune commit logs older than `minRetainedTs`. Advertises the new watermark FIRST —
649
+ * persist it, bump our in-memory copy, and broadcast it over Redis — and only
650
+ * then deletes, so no reader ever trusts a cache entry whose freshness scan
651
+ * would include rows we're about to remove. Returns rows deleted.
652
+ */
653
+ async prune(minRetainedTs) {
654
+ await this.db.updateMinSnapshotTimestamp(minRetainedTs);
655
+ this.retentionWatermark.set(minRetainedTs);
656
+ await this.publishRetentionWatermark(minRetainedTs);
657
+ return await this.db.pruneCommitLogsBefore(minRetainedTs);
658
+ }
659
+ /** Broadcast the watermark to app-server instances over Redis pub/sub. Workers in
660
+ * this process get it in-process via {@link setRetentionWatermark}. */
661
+ async publishRetentionWatermark(ts) {
662
+ try {
663
+ await this.cache.redis.publish(RETENTION_WATERMARK_CHANNEL, ts.toString());
664
+ } catch (err) {
665
+ this.logger.error(err, "failed to publish commit-log watermark");
666
+ }
667
+ }
668
+ async register(params) {
669
+ const { subId, cacheKey, queryName, args, queryIdentity, instanceName, lastSnapshotTs } = params;
670
+ this.logger.info(`registering subscription: ${subId} from ${instanceName}`);
671
+ const existing = this.subscriptions.get(subId);
672
+ if (existing && !existing.timeout) {
673
+ existing.instances.add(instanceName);
674
+ return {
675
+ cacheKey,
676
+ recompute: false,
677
+ readSet: existing.readSet,
678
+ ephemeralKeys: existing.ephemeralKeys
679
+ };
680
+ }
681
+ if (existing?.timeout) {
682
+ this.logger.info(`reactivating subscription: ${subId} from: ${instanceName}`);
683
+ clearTimeout(existing.timeout);
684
+ existing.timeout = null;
685
+ }
686
+ const metadata = await this.cache.getQueryCacheMetaData(cacheKey);
687
+ const freshness = await evaluateCacheFreshness(this.db, metadata, lastSnapshotTs, this.retentionWatermark.get());
688
+ let needsRecompute = !freshness.fresh;
689
+ const readSet = freshness.readSet;
690
+ const rawReadSet = freshness.rawReadSet;
691
+ const ephemeralKeys = freshness.ephemeralKeys;
692
+ if (!needsRecompute && metadata && lastSnapshotTs > metadata.lastSnapshotTs) this.cache.advanceQueryCacheMetadata(cacheKey, {
693
+ lastSnapshotTs,
694
+ version: metadata.version,
695
+ readSet,
696
+ ephemeralKeys
697
+ }).catch((err) => this.logger.error(err, `Failed to advance cache metadata for ${cacheKey}`));
698
+ let newSubscription = {
699
+ subId,
700
+ queryName,
701
+ args,
702
+ cacheKey,
703
+ lastSnapshotTs,
704
+ readSet,
705
+ rawReadSet,
706
+ ephemeralKeys,
707
+ instances: /* @__PURE__ */ new Set([instanceName]),
708
+ timeout: null
709
+ };
710
+ const createdWhileChecking = this.subscriptions.get(subId);
711
+ if (createdWhileChecking) {
712
+ if (createdWhileChecking.timeout) {
713
+ clearTimeout(createdWhileChecking.timeout);
714
+ createdWhileChecking.timeout = null;
715
+ }
716
+ if (createdWhileChecking.lastSnapshotTs > lastSnapshotTs) {
717
+ newSubscription = {
718
+ ...newSubscription,
719
+ ...createdWhileChecking,
720
+ instances: /* @__PURE__ */ new Set([...createdWhileChecking.instances, instanceName])
721
+ };
722
+ needsRecompute = false;
723
+ }
724
+ }
725
+ this.reindexEphemeralKeys(subId, newSubscription.ephemeralKeys);
726
+ this.subscriptions.set(subId, newSubscription);
727
+ this.persistRegistration(subId, {
728
+ queryName,
729
+ args,
730
+ queryIdentity,
731
+ cacheKey
732
+ });
733
+ return {
734
+ cacheKey,
735
+ recompute: needsRecompute,
736
+ readSet,
737
+ ephemeralKeys: newSubscription.ephemeralKeys,
738
+ version: metadata?.version
739
+ };
740
+ }
741
+ async updateReadSet(params) {
742
+ const { cacheKey, subId, instanceName, lastSnapshotTs, readSet, ephemeralKeys } = params;
743
+ this.logger.info(`updating subscription readSet: ${subId} from ${instanceName}`);
744
+ const existing = this.subscriptions.get(subId);
745
+ if (!existing) return { tracked: false };
746
+ if (lastSnapshotTs < existing.lastSnapshotTs) return { tracked: true };
747
+ const rawReadSet = mapReadSetToRaw(readSet);
748
+ this.reindexEphemeralKeys(subId, ephemeralKeys);
749
+ this.subscriptions.set(subId, {
750
+ ...existing,
751
+ readSet,
752
+ rawReadSet,
753
+ ephemeralKeys,
754
+ lastSnapshotTs
755
+ });
756
+ await this.cache.replaceQueryCacheMetadata(cacheKey, {
757
+ lastSnapshotTs,
758
+ version: String(lastSnapshotTs),
759
+ readSet,
760
+ ephemeralKeys
761
+ });
762
+ return { tracked: true };
763
+ }
764
+ async unregister(params, jitter = 0) {
765
+ const { subId, instanceName } = params;
766
+ this.logger.info(`unregistering subscription: ${subId} from: ${instanceName}`);
767
+ const sub = this.subscriptions.get(subId);
768
+ if (!sub) return { ok: true };
769
+ if (sub.timeout) clearTimeout(sub.timeout);
770
+ if (sub.instances.size > 1) {
771
+ sub.instances.delete(instanceName);
772
+ return { ok: true };
773
+ }
774
+ this.logger.debug(`scheduling unregister subscription: ${subId} from: ${instanceName} in 15s`);
775
+ sub.timeout = setTimeout(async () => {
776
+ sub.instances.delete(instanceName);
777
+ if (sub.instances.size === 0) {
778
+ this.logger.info(`unregistered subscription: ${subId} from: ${instanceName}`);
779
+ this.clearEphemeralKeys(subId);
780
+ this.subscriptions.delete(subId);
781
+ await this.deleteRegistration(subId);
782
+ }
783
+ }, 15e3 + jitter);
784
+ return { ok: true };
785
+ }
786
+ async unregisters(params) {
787
+ this.logger.info({ subIds: params }, `scheduling unregistering subscriptions`);
788
+ for (const param of params) this.unregister(param, Math.floor(Math.random() * 100));
789
+ return params.map(() => ({ ok: true }));
790
+ }
791
+ /**
792
+ * Synchronous, CPU-bound. Scans this worker's slice of subscriptions and
793
+ * returns the ones affected by the writeSet.
794
+ */
795
+ invalidate(rawWriteSet) {
796
+ const affected = [];
797
+ for (const [, sub] of this.subscriptions) {
798
+ if (sub.instances.size === 0) continue;
799
+ if (!checkWriteSetAffectsReadSet(rawWriteSet, sub.rawReadSet)) continue;
800
+ affected.push({
801
+ subId: sub.subId,
802
+ queryName: sub.queryName,
803
+ args: sub.args,
804
+ cacheKey: sub.cacheKey,
805
+ notifyInstances: Array.from(sub.instances)
806
+ });
807
+ }
808
+ return affected;
809
+ }
810
+ /**
811
+ * Ephemeral counterpart of {@link invalidate}. Synchronous and CPU-bound like
812
+ * its sibling, but resolved by exact key lookup rather than a slice scan: an
813
+ * ephemeral key has no predicate to satisfy, so any subscription that READ the
814
+ * key is affected by definition.
815
+ *
816
+ * A subscription reading several written keys appears once — the writes of one
817
+ * mutation flush as a single batch, and one recompute already reflects all of
818
+ * them.
819
+ */
820
+ invalidateEphemeral(keys) {
821
+ const affected = [];
822
+ const seen = /* @__PURE__ */ new Set();
823
+ for (const key of keys) {
824
+ const holders = this.ephemeralIndex.get(key);
825
+ if (!holders) continue;
826
+ for (const subId of holders) {
827
+ if (seen.has(subId)) continue;
828
+ seen.add(subId);
829
+ const sub = this.subscriptions.get(subId);
830
+ if (!sub || sub.instances.size === 0) continue;
831
+ affected.push({
832
+ subId: sub.subId,
833
+ queryName: sub.queryName,
834
+ args: sub.args,
835
+ cacheKey: sub.cacheKey,
836
+ notifyInstances: Array.from(sub.instances)
837
+ });
838
+ }
839
+ }
840
+ return affected;
841
+ }
842
+ async persistRegistration(subId, record) {
843
+ try {
844
+ await this.cache.persistRegistrationRecord(subId, record);
845
+ } catch (err) {
846
+ this.logger.error(err, `[worker ${this.id}] Failed to persist registration for ${subId}`);
847
+ }
848
+ }
849
+ async deleteRegistration(subId) {
850
+ try {
851
+ await this.cache.deleteRegistrationRecord(subId);
852
+ } catch (err) {
853
+ this.logger.error(err, `[worker ${this.id}] Failed to delete registration for ${subId}`);
854
+ }
855
+ }
856
+ };
857
+ //#endregion
858
+ //#region src/server/sub-worker-dispatch.ts
859
+ /**
860
+ * Dispatch a single worker operation against an already-constructed
861
+ * {@link SubscriptionWorker} + {@link Database}. Shared by:
862
+ *
863
+ * • the worker-thread entry (`subscription-manager-worker-entry.ts`), where
864
+ * the payload just crossed a `postMessage` boundary and is untrusted, so it
865
+ * is Zod-parsed against the matching schema; and
866
+ * • the in-process {@link InlineWorkerHandle}, where the payload is the
867
+ * manager's own already-typed object (no thread hop, no structured-clone) —
868
+ * `trusted: true` skips the redundant parse so the call goes straight
869
+ * through to the worker method.
870
+ *
871
+ * `MsgType.Init` is NOT handled here: it constructs the worker/db, which is
872
+ * bootstrap logic owned by each caller (the thread entry builds its own pool;
873
+ * the inline handle is handed live instances).
874
+ */
875
+ async function dispatchWorkerMessage(worker, db, type, payload, opts) {
876
+ const trusted = opts?.trusted ?? false;
877
+ switch (type) {
878
+ case 2: {
879
+ const params = trusted ? payload : RegisterSubscriptionParamsSchema.parse(payload);
880
+ return worker.register(params);
881
+ }
882
+ case 3: {
883
+ const params = trusted ? payload : RegisterSubscriptionBatchParamsSchema.parse(payload);
884
+ return Promise.all(params.map((p) => worker.register(p)));
885
+ }
886
+ case 4: {
887
+ const params = trusted ? payload : UpdateSubscriptionReadSetParamsSchema.parse(payload);
888
+ return worker.updateReadSet(params);
889
+ }
890
+ case 5: {
891
+ const params = trusted ? payload : UnregisterSubscriptionParamsSchema.parse(payload);
892
+ return worker.unregister(params);
893
+ }
894
+ case 6: {
895
+ const params = trusted ? payload : UnregisterSubscriptionsParamsSchema.parse(payload);
896
+ return worker.unregisters(params);
897
+ }
898
+ case 7: {
899
+ const { writeSet } = trusted ? payload : InvalidateWritesetParamsSchema.parse(payload);
900
+ return worker.invalidate(mapWriteSetToRaw(writeSet));
901
+ }
902
+ case 14: {
903
+ const { keys } = trusted ? payload : InvalidateEphemeralParamsSchema.parse(payload);
904
+ return worker.invalidateEphemeral(keys);
905
+ }
906
+ case 8: return worker.minSubscriptionTs();
907
+ case 9: return db.getLatestSnapshotTimestamp();
908
+ case 10: return worker.prune(payload);
909
+ case 12: return worker.refreshRetentionWatermark();
910
+ case 13:
911
+ worker.setRetentionWatermark(payload);
912
+ return null;
913
+ case 11:
914
+ try {
915
+ await db.close();
916
+ } catch (_) {}
917
+ return null;
918
+ case 1: throw new Error("MsgType.Init must be handled by the caller (worker bootstrap)");
919
+ default: throw new Error(`Unknown MsgType: ${type}`);
920
+ }
921
+ }
922
+ //#endregion
923
+ export { RETENTION_WATERMARK_CHANNEL as C, UpdateSubscriptionReadSetResultSchema as S, RegisterSubscriptionResultSchema as _, bootstrapDbTypes as a, UnregisterSubscriptionsResultSchema as b, initCore as c, InvalidateEphemeralParamsSchema as d, InvalidateEphemeralResultSchema as f, RegisterSubscriptionParamsSchema as g, RegisterSubscriptionBatchResultSchema as h, DB_QUERY_TIMEOUT_MS as i, initDatabase as l, InvalidateWritesetResultSchema as m, SubscriptionWorker as n, createDatabase as o, InvalidateWritesetParamsSchema as p, CORE_VERSION as r, initCacheLayer as s, dispatchWorkerMessage as t, CacheLayer as u, UnregisterSubscriptionParamsSchema as v, RetentionWatermark as w, UpdateSubscriptionReadSetParamsSchema as x, UnregisterSubscriptionResultSchema as y };
924
+
925
+ //# sourceMappingURL=sub-worker-dispatch-Dwrp3c40.js.map