@supalive/core 1.18.0 → 1.20.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 (95) hide show
  1. package/dist/index-BuWa503L.d.ts +2445 -0
  2. package/dist/index-BuWa503L.d.ts.map +1 -0
  3. package/dist/index-CBIjmPUS.d.ts +2444 -0
  4. package/dist/index-CBIjmPUS.d.ts.map +1 -0
  5. package/dist/index-CKo6HvJc.d.ts +2455 -0
  6. package/dist/index-CKo6HvJc.d.ts.map +1 -0
  7. package/dist/index-DShKjpzF.d.ts +2453 -0
  8. package/dist/index-DShKjpzF.d.ts.map +1 -0
  9. package/dist/index-Dcxf_Gm-.d.ts +2454 -0
  10. package/dist/index-Dcxf_Gm-.d.ts.map +1 -0
  11. package/dist/index-DsBAE32T.d.ts +2459 -0
  12. package/dist/index-DsBAE32T.d.ts.map +1 -0
  13. package/dist/logger-DrXccWFZ.js +346 -0
  14. package/dist/logger-DrXccWFZ.js.map +1 -0
  15. package/dist/mysql-B2u0Ye4x.d.ts +114 -0
  16. package/dist/mysql-B2u0Ye4x.d.ts.map +1 -0
  17. package/dist/mysql-BHW-tAqt.d.ts +114 -0
  18. package/dist/mysql-BHW-tAqt.d.ts.map +1 -0
  19. package/dist/mysql-C79_qNQO.d.ts +114 -0
  20. package/dist/mysql-C79_qNQO.d.ts.map +1 -0
  21. package/dist/mysql-C_TTCZ8j.d.ts +114 -0
  22. package/dist/mysql-C_TTCZ8j.d.ts.map +1 -0
  23. package/dist/mysql-CyzJqrvO.d.ts +114 -0
  24. package/dist/mysql-CyzJqrvO.d.ts.map +1 -0
  25. package/dist/mysql-ygH2EMGn.js +622 -0
  26. package/dist/mysql-ygH2EMGn.js.map +1 -0
  27. package/dist/mysql-z9Jefhd6.d.ts +114 -0
  28. package/dist/mysql-z9Jefhd6.d.ts.map +1 -0
  29. package/dist/object-storage-DTVmZq3l.d.ts +110 -0
  30. package/dist/object-storage-DTVmZq3l.d.ts.map +1 -0
  31. package/dist/one-shot-query-9mCboWaB.js +565 -0
  32. package/dist/one-shot-query-9mCboWaB.js.map +1 -0
  33. package/dist/one-shot-query-B9SjfcCD.js +550 -0
  34. package/dist/one-shot-query-B9SjfcCD.js.map +1 -0
  35. package/dist/one-shot-query-BBiv9LGP.js +565 -0
  36. package/dist/one-shot-query-BBiv9LGP.js.map +1 -0
  37. package/dist/one-shot-query-Bxz_N-NE.js +563 -0
  38. package/dist/one-shot-query-Bxz_N-NE.js.map +1 -0
  39. package/dist/one-shot-query-DE9oQU40.js +550 -0
  40. package/dist/one-shot-query-DE9oQU40.js.map +1 -0
  41. package/dist/one-shot-query-adoWqGLs.js +565 -0
  42. package/dist/one-shot-query-adoWqGLs.js.map +1 -0
  43. package/dist/overlap-checker-CCgq_Tpa.js +284 -0
  44. package/dist/overlap-checker-CCgq_Tpa.js.map +1 -0
  45. package/dist/postgres-B2FLN1Kw.d.ts +118 -0
  46. package/dist/postgres-B2FLN1Kw.d.ts.map +1 -0
  47. package/dist/postgres-C7xHWbij.d.ts +118 -0
  48. package/dist/postgres-C7xHWbij.d.ts.map +1 -0
  49. package/dist/postgres-DBMAv_dq.d.ts +118 -0
  50. package/dist/postgres-DBMAv_dq.d.ts.map +1 -0
  51. package/dist/postgres-DBuP9ON_.d.ts +118 -0
  52. package/dist/postgres-DBuP9ON_.d.ts.map +1 -0
  53. package/dist/postgres-DwiUT_A6.js +867 -0
  54. package/dist/postgres-DwiUT_A6.js.map +1 -0
  55. package/dist/postgres-I4sgdBnG.d.ts +118 -0
  56. package/dist/postgres-I4sgdBnG.d.ts.map +1 -0
  57. package/dist/postgres-SM8muZqN.d.ts +118 -0
  58. package/dist/postgres-SM8muZqN.d.ts.map +1 -0
  59. package/dist/router-DP2ThAwh.js.map +1 -1
  60. package/dist/src/client/index.d.ts +1 -1
  61. package/dist/src/client/index.js +47 -50
  62. package/dist/src/client/index.js.map +1 -1
  63. package/dist/src/exports/mysql.d.ts +1 -1
  64. package/dist/src/exports/mysql.js +1 -1
  65. package/dist/src/exports/postgres.d.ts +1 -1
  66. package/dist/src/exports/postgres.js +1 -1
  67. package/dist/src/exports/procedure.d.ts +1 -1
  68. package/dist/src/exports/schema-sql.d.ts +1 -1
  69. package/dist/src/exports/server.d.ts +11 -4
  70. package/dist/src/exports/server.d.ts.map +1 -1
  71. package/dist/src/exports/server.js +31 -410
  72. package/dist/src/exports/server.js.map +1 -1
  73. package/dist/src/exports/storage.d.ts +4 -2
  74. package/dist/src/exports/storage.d.ts.map +1 -1
  75. package/dist/src/exports/storage.js +23 -1
  76. package/dist/src/exports/storage.js.map +1 -1
  77. package/dist/src/exports/sub-manager-worker-entry.js +1 -1
  78. package/dist/src/exports/types.d.ts +2 -2
  79. package/dist/sub-worker-dispatch-BLujn13f.js +792 -0
  80. package/dist/sub-worker-dispatch-BLujn13f.js.map +1 -0
  81. package/dist/types_server-BCst4TfZ.d.ts +703 -0
  82. package/dist/types_server-BCst4TfZ.d.ts.map +1 -0
  83. package/dist/types_server-BYwLkwbC.d.ts +691 -0
  84. package/dist/types_server-BYwLkwbC.d.ts.map +1 -0
  85. package/dist/types_server-Bvrohyc8.d.ts +691 -0
  86. package/dist/types_server-Bvrohyc8.d.ts.map +1 -0
  87. package/dist/types_server-C6oCm9oC.d.ts +691 -0
  88. package/dist/types_server-C6oCm9oC.d.ts.map +1 -0
  89. package/dist/types_server-CHJLfdxb.d.ts +691 -0
  90. package/dist/types_server-CHJLfdxb.d.ts.map +1 -0
  91. package/dist/types_server-DnO_fp-F.d.ts +691 -0
  92. package/dist/types_server-DnO_fp-F.d.ts.map +1 -0
  93. package/dist/types_server-gSgzZHuD.d.ts +703 -0
  94. package/dist/types_server-gSgzZHuD.d.ts.map +1 -0
  95. package/package.json +3 -1
@@ -0,0 +1,565 @@
1
+ import { c as parentConn } from "./router-DP2ThAwh.js";
2
+ import { n as stableStringify } from "./helper-zdJT5FUc.js";
3
+ import { s as traceCacheCall } from "./query-LJranz0c.js";
4
+ import { r as evaluateCacheFreshness } from "./overlap-checker-CCgq_Tpa.js";
5
+ import { i as logger, l as DbWriter } from "./logger-DrXccWFZ.js";
6
+ import { ROOT_CONTEXT, SpanKind, SpanStatusCode, context, isSpanContextValid, metrics, propagation, trace } from "@opentelemetry/api";
7
+ import { sha1 } from "hash-wasm";
8
+ //#region src/server/sub_hash.ts
9
+ /** Compute stable hash using json-stable-stringify and SHA1. */
10
+ async function getHashOf(input) {
11
+ return sha1((typeof input === "string" ? input : stableStringify(input)) ?? "");
12
+ }
13
+ /**
14
+ * Default identity literal used when neither a procedure-level
15
+ * `queryIdentity` override nor a per-session user is available.
16
+ */
17
+ const ANONYMOUS_IDENTITY = "anonymous";
18
+ /**
19
+ * Resolve the segmentation key that participates in the subId/cacheKey hash for
20
+ * a query. Shared by every query transport (WS subscribe, WS one-shot call, and
21
+ * HTTP RPC) so identity — and therefore cache segmentation — is computed
22
+ * identically regardless of how the query is invoked. Priority:
23
+ *
24
+ * 1. Procedure-level `queryIdentity` override:
25
+ * - `false` → `ANONYMOUS_IDENTITY` (cache shared across users)
26
+ * - `string` → that literal
27
+ * - `function` → serverCtx + input → string | null | undefined
28
+ * 2. `getUserId(serverCtx)` → falls back to `ANONYMOUS_IDENTITY` when it
29
+ * returns null/undefined or isn't configured.
30
+ */
31
+ function resolveQueryIdentity(procedure, serverCtx, input, getUserId) {
32
+ const override = procedure.queryIdentity;
33
+ if (override !== void 0) {
34
+ if (override === false) return ANONYMOUS_IDENTITY;
35
+ if (typeof override === "string") return override;
36
+ if (typeof override === "function") {
37
+ const value = override(serverCtx, input);
38
+ if (value === null || value === void 0) return ANONYMOUS_IDENTITY;
39
+ return value;
40
+ }
41
+ }
42
+ if (!getUserId) return ANONYMOUS_IDENTITY;
43
+ return getUserId(serverCtx) ?? "anonymous";
44
+ }
45
+ /**
46
+ * Compute a subscription id from hash of procedure + input + queryIdentity.
47
+ *
48
+ * `queryIdentity` is the segmentation key for cache/subscriptions. By default
49
+ * it is the authenticated user's id (so two users get distinct subscriptions
50
+ * for the same query), but a procedure may override it to share cache/subs
51
+ * across users (e.g. for a public, user-independent query).
52
+ */
53
+ async function generateSubscriptionId(procedure, input, queryIdentity) {
54
+ return `sub_${await getHashOf(`${procedure}_${typeof input === "string" ? input : stableStringify(input)}_${queryIdentity}`)}`;
55
+ }
56
+ /**
57
+ * Compute cache key from query name + args + queryIdentity. The identity
58
+ * participates so that user-specific results don't poison each other's
59
+ * cache when a procedure does authorize per-user.
60
+ */
61
+ async function generateCacheKey(queryName, input, queryIdentity) {
62
+ return `ck_${await getHashOf(`${queryName}_${typeof input === "string" ? input : stableStringify(input)}_${queryIdentity}`)}`;
63
+ }
64
+ //#endregion
65
+ //#region src/observability.ts
66
+ let rootLogger = logger;
67
+ /** Replace the process-wide root logger. Call once at startup, before creating
68
+ * the server, to route Supalive's request/lifecycle logs through your own pino
69
+ * instance. */
70
+ function configureRootLogger(logger) {
71
+ rootLogger = logger;
72
+ }
73
+ /** The current root logger (defaults to the env-configured singleton). */
74
+ function getRootLogger() {
75
+ return rootLogger;
76
+ }
77
+ const CLIENT_ERROR_CODES = /* @__PURE__ */ new Set([
78
+ "NOT_FOUND",
79
+ "BAD_REQUEST",
80
+ "INVALID_MESSAGE",
81
+ "PARSE_ERROR",
82
+ "UNAUTHENTICATED",
83
+ "RATE_LIMITED",
84
+ "METHOD_NOT_ALLOWED",
85
+ "TOO_MANY_CONCURRENT_MUTATIONS",
86
+ "TOO_MANY_CONCURRENT_ACTIONS",
87
+ "SUBSCRIBE_ERROR"
88
+ ]);
89
+ /**
90
+ * The bound observability surface for one server instance: an injectable logger,
91
+ * OTel metric instruments, and the `record()` seam every operation funnels
92
+ * through. Built once in the server constructor and shared with the HTTP RPC
93
+ * handler.
94
+ */
95
+ var Observability = class {
96
+ logger;
97
+ requestLog;
98
+ onEvent;
99
+ requestCounter;
100
+ durationHistogram;
101
+ activeConnections;
102
+ activeSubscriptions;
103
+ tracer;
104
+ constructor(opts = {}) {
105
+ const base = opts.logger ?? getRootLogger();
106
+ this.logger = opts.level ? base.child({}, { level: opts.level }) : base;
107
+ this.requestLog = opts.requestLog ?? "all";
108
+ this.onEvent = opts.onEvent;
109
+ const meterName = opts.meterName ?? "@supalive/core";
110
+ const meter = metrics.getMeter(meterName, opts.meterVersion);
111
+ this.tracer = trace.getTracer(meterName, opts.meterVersion);
112
+ this.requestCounter = meter.createCounter("supalive_requests_total", { description: "Total Supalive operations (WS calls, subscribes, HTTP RPC, jobs)." });
113
+ this.durationHistogram = meter.createHistogram("supalive_request_duration_ms", {
114
+ description: "Operation duration in milliseconds.",
115
+ unit: "ms"
116
+ });
117
+ this.activeConnections = meter.createUpDownCounter("supalive_active_connections", { description: "Currently open WebSocket connections." });
118
+ this.activeSubscriptions = meter.createUpDownCounter("supalive_active_subscriptions", { description: "Currently registered live-query subscriptions on this instance." });
119
+ }
120
+ /** Record one completed operation: metrics + optional onEvent + one log line,
121
+ * and finalize the op's span (status/attributes/exception) when present. */
122
+ record(e) {
123
+ const traceId = this.finalizeSpan(e);
124
+ if (traceId) (e.extra ??= {}).traceId = traceId;
125
+ const attrs = {
126
+ transport: e.transport,
127
+ procedure: e.procedure ?? "",
128
+ kind: e.kind ?? "",
129
+ outcome: e.outcome,
130
+ code: e.code ?? ""
131
+ };
132
+ this.requestCounter.add(1, attrs);
133
+ this.durationHistogram.record(e.durationMs, attrs);
134
+ if (this.onEvent) try {
135
+ this.onEvent(e);
136
+ } catch {}
137
+ if (this.requestLog === "off") return;
138
+ if (e.outcome === "ok") {
139
+ if (this.requestLog !== "all") return;
140
+ this.logger.info(logFields(e), summary(e));
141
+ return;
142
+ }
143
+ const fields = logFields(e);
144
+ if (!e.error && e.code != null && CLIENT_ERROR_CODES.has(e.code)) this.logger.warn(fields, summary(e));
145
+ else this.logger.error({
146
+ ...fields,
147
+ err: e.error
148
+ }, summary(e));
149
+ }
150
+ /**
151
+ * Start a SERVER span for an operation, as a child of `parent` (or the active
152
+ * context). No-op — returns a non-recording span — until a TracerProvider is
153
+ * registered by the app, so this costs nothing when tracing is disabled.
154
+ * Returns the span plus a context that has it active, for `context.with(...)`
155
+ * wrapping so nested handler work / `ctx.span` children attach correctly.
156
+ */
157
+ startSpan(name, opts = {}) {
158
+ const parent = opts.parent ?? context.active();
159
+ const span = this.tracer.startSpan(name, {
160
+ kind: SpanKind.SERVER,
161
+ attributes: {
162
+ ...opts.transport ? { "supalive.transport": opts.transport } : {},
163
+ ...opts.procedure ? { "supalive.procedure": opts.procedure } : {},
164
+ ...opts.kind ? { "supalive.kind": opts.kind } : {}
165
+ }
166
+ }, parent);
167
+ return {
168
+ span,
169
+ context: trace.setSpan(parent, span)
170
+ };
171
+ }
172
+ /**
173
+ * Continue a trace from an inbound carrier (HTTP headers, or a WS message's
174
+ * `trace` field). Returns a context to pass as `startSpan({ parent })`. When
175
+ * no propagator is registered / no trace context is present, returns the root
176
+ * context (a fresh trace).
177
+ */
178
+ extractContext(carrier) {
179
+ return propagation.extract(ROOT_CONTEXT, carrier);
180
+ }
181
+ /** Set the span's final status/attributes from the event, then end it.
182
+ * Returns the (valid) trace id for log correlation, or undefined. */
183
+ finalizeSpan(e) {
184
+ const span = e.span;
185
+ if (!span) return void 0;
186
+ try {
187
+ span.setAttribute("supalive.outcome", e.outcome);
188
+ if (e.code) span.setAttribute("supalive.code", e.code);
189
+ if (e.procedure) span.setAttribute("supalive.procedure", e.procedure);
190
+ if (e.kind) span.setAttribute("supalive.kind", e.kind);
191
+ if (e.outcome === "error") {
192
+ if (e.error instanceof Error) span.recordException(e.error);
193
+ span.setStatus({
194
+ code: SpanStatusCode.ERROR,
195
+ message: e.code
196
+ });
197
+ } else span.setStatus({ code: SpanStatusCode.OK });
198
+ } finally {
199
+ span.end();
200
+ }
201
+ const sc = span.spanContext();
202
+ return isSpanContextValid(sc) ? sc.traceId : void 0;
203
+ }
204
+ /** A request-scoped child logger for `ctx.log`, bound with the given fields
205
+ * (reqId, procedure, and trace id when tracing is on). */
206
+ childLogger(fields) {
207
+ return this.logger.child(fields);
208
+ }
209
+ /**
210
+ * A LAZY request-scoped `ctx.log`. Most handlers never log, and pino's
211
+ * `.child()` (bindings merge + serialization) isn't free — so this defers the
212
+ * child creation until the first `debug/info/warn/error` call and reuses it
213
+ * after. When a handler logs nothing, no child logger is ever allocated; the
214
+ * only per-request cost is this small delegator object. Returns the narrow
215
+ * {@link HandlerLogger} surface the handler context exposes.
216
+ */
217
+ lazyChildLogger(fields) {
218
+ const base = this.logger;
219
+ let child;
220
+ const get = () => child ??= base.child(fields);
221
+ return {
222
+ debug: (...args) => get().debug(...args),
223
+ info: (...args) => get().info(...args),
224
+ warn: (...args) => get().warn(...args),
225
+ error: (...args) => get().error(...args)
226
+ };
227
+ }
228
+ /** The span's trace id, or undefined when tracing is off / the span is
229
+ * non-recording. For binding onto `ctx.log`. */
230
+ traceIdOf(span) {
231
+ if (!span) return void 0;
232
+ const sc = span.spanContext();
233
+ return isSpanContextValid(sc) ? sc.traceId : void 0;
234
+ }
235
+ connectionOpened() {
236
+ this.activeConnections.add(1);
237
+ }
238
+ connectionClosed() {
239
+ this.activeConnections.add(-1);
240
+ }
241
+ subscriptionAdded(n = 1) {
242
+ this.activeSubscriptions.add(n);
243
+ }
244
+ subscriptionRemoved(n = 1) {
245
+ this.activeSubscriptions.add(-n);
246
+ }
247
+ };
248
+ /** Start marker for an operation; pass to {@link elapsedMs} to get its duration. */
249
+ function startTimer() {
250
+ return performance.now();
251
+ }
252
+ function elapsedMs(startedAt) {
253
+ return Math.round((performance.now() - startedAt) * 1e3) / 1e3;
254
+ }
255
+ const coreTracer = trace.getTracer("@supalive/core");
256
+ /**
257
+ * Run a query handler inside a `query <procedureName>` span, nested under
258
+ * `parentContext` (the operation span) or the active context. The span is handed
259
+ * to `run` so it becomes the query handler's `ctx.span` and the parent of any DB
260
+ * spans. Cache HITs never call this (no execution), so a span appearing means a
261
+ * real recompute — including the otherwise-invisible background reactive path,
262
+ * where `parentContext` is omitted and the span is a root.
263
+ *
264
+ * No-op-cheap when tracing is off: the started span is non-recording, so we skip
265
+ * the `context.with` and just run.
266
+ */
267
+ function traceQueryExecution(procedureName, trigger, parentContext, run) {
268
+ const parent = parentContext ?? context.active();
269
+ const span = coreTracer.startSpan(`query ${procedureName}`, { attributes: {
270
+ "supalive.kind": "query",
271
+ "supalive.procedure": procedureName,
272
+ "supalive.trigger": trigger
273
+ } }, parent);
274
+ const exec = async () => {
275
+ try {
276
+ const r = await run(span);
277
+ span.setStatus({ code: SpanStatusCode.OK });
278
+ return r;
279
+ } catch (err) {
280
+ if (err instanceof Error) span.recordException(err);
281
+ span.setStatus({ code: SpanStatusCode.ERROR });
282
+ throw err;
283
+ } finally {
284
+ span.end();
285
+ }
286
+ };
287
+ return span.isRecording() ? context.with(trace.setSpan(parent, span), exec) : exec();
288
+ }
289
+ function logFields(e) {
290
+ const f = {
291
+ op: e.op,
292
+ transport: e.transport,
293
+ outcome: e.outcome,
294
+ durMs: e.durationMs
295
+ };
296
+ if (e.procedure) f.procedure = e.procedure;
297
+ if (e.kind) f.kind = e.kind;
298
+ if (e.code) f.code = e.code;
299
+ if (e.reqId) f.reqId = e.reqId;
300
+ if (e.sessionId) f.sessionId = e.sessionId;
301
+ if (e.userId != null) f.userId = e.userId;
302
+ if (e.extra) Object.assign(f, e.extra);
303
+ return f;
304
+ }
305
+ function summary(e) {
306
+ const label = e.procedure ? `${e.kind ?? e.op} ${e.procedure}` : e.op;
307
+ return `${e.transport} ${label} ${e.outcome}`;
308
+ }
309
+ //#endregion
310
+ //#region src/db/read-routing.ts
311
+ /**
312
+ * Decide which connection a read should run on, by commit_ts high-water — the
313
+ * single routing rule used by both the one-shot query path and the internal
314
+ * caller.
315
+ *
316
+ * `minTs` is the reader's floor: the largest commit/snapshot ts it has already
317
+ * observed and must not read behind (read-your-writes + monotonic reads). It is
318
+ * clamped to `primaryTs` here, so a bogus/oversized value can at most force the
319
+ * primary — never an error, never a value beyond what the primary has. A read
320
+ * may use the replica ONLY when the replica has applied at least this floor
321
+ * (`replicaTs >= minTs`); otherwise the replica is lagging past what the caller
322
+ * has seen and the read stays on the primary.
323
+ *
324
+ * `primaryTs` is passed in (rather than loaded here) so it stays consistent with
325
+ * whatever snapshot the caller already loaded at its seam — e.g. the one-shot
326
+ * path uses it as the cache-freshness upper bound. The replica's ts is loaded
327
+ * here, and only when a replica is actually configured (no wasted round trip on
328
+ * primary-only deployments).
329
+ */
330
+ async function resolveReadRouting(db, primaryLastSnapshotTs, minTs) {
331
+ const primaryImpl = db.impl;
332
+ minTs = minTs == 0n ? primaryLastSnapshotTs : minTs;
333
+ const floor = minTs > primaryLastSnapshotTs ? primaryLastSnapshotTs : minTs;
334
+ const replicaDb = db.replica;
335
+ const replicaImpl = replicaDb ? replicaDb.impl : primaryImpl;
336
+ const replicaTs = replicaDb ? await replicaImpl.getLatestSnapshotTimestamp() : primaryLastSnapshotTs;
337
+ const useReplica = replicaDb !== void 0 && replicaTs >= floor;
338
+ return {
339
+ primaryImpl,
340
+ primaryTs: primaryLastSnapshotTs,
341
+ replicaImpl,
342
+ replicaTs,
343
+ readImpl: useReplica ? replicaImpl : primaryImpl,
344
+ readTs: useReplica ? replicaTs : primaryLastSnapshotTs,
345
+ useReplica
346
+ };
347
+ }
348
+ /**
349
+ * Build the per-handler `ctx.usePrimaryConn()` / `ctx.useReplicaConn()` methods
350
+ * for a query, bound to a routing decision. Shared so the override semantics are
351
+ * identical everywhere:
352
+ *
353
+ * • `usePrimaryConn()` — force the primary (always safe).
354
+ * • `useReplicaConn({ readOwnWrite: true })` — force the primary (read-your-writes).
355
+ * • `useReplicaConn()` — the SAME caught-up gate as the default: the replica
356
+ * when it has applied the caller's floor, else the primary. It never binds a
357
+ * replica that is lagging past `minTs`, so opting in can't serve stale reads.
358
+ *
359
+ * Must be called before the handler's first read (`useConnection` throws
360
+ * otherwise) — that's the contract the ctx methods expose to handlers.
361
+ */
362
+ function makeConnRouting(reader, routing) {
363
+ return {
364
+ usePrimaryConn: (_opts) => reader.useConnection(routing.primaryImpl, routing.primaryTs),
365
+ useReplicaConn: (opts) => reader.useConnection(opts?.readOwnWrite ? routing.primaryImpl : routing.readImpl, opts?.readOwnWrite ? routing.primaryTs : routing.readTs)
366
+ };
367
+ }
368
+ //#endregion
369
+ //#region src/server/one-shot-query.ts
370
+ /**
371
+ * Parent context for the `query <name>` span. IMPLICIT linking is the primary
372
+ * mechanism: the caller's handler already runs inside `otelContext.with(
373
+ * started.context, ...)`, so the active span IS the correct parent — return
374
+ * `undefined` and let {@link traceQueryExecution} default to the ambient
375
+ * context. The EXPLICIT fallback (the span carried on the caller's handlerObs,
376
+ * i.e. the parent handler's `ctx.span`) is used only when the ambient context
377
+ * is root — e.g. a detached/fire-and-forget internal call.
378
+ */
379
+ function parentContextFor(handlerObs) {
380
+ if (trace.getSpan(context.active())) return void 0;
381
+ return handlerObs.span ? trace.setSpan(context.active(), handlerObs.span) : void 0;
382
+ }
383
+ /**
384
+ * Execute a one-shot query (WS `call` or HTTP RPC) with the same caching model
385
+ * as a live subscription — but without registering a tracked subscription.
386
+ *
387
+ * Flow (mirrors `handleSubscribe` / worker `register`):
388
+ * 1. Resolve `queryIdentity` → `cacheKey` (identical segmentation to subs).
389
+ * 2. Pick the read connection ONCE (replica when it's caught up to the client's
390
+ * `clientMinTs`, else primary) and use it for BOTH the freshness scan and
391
+ * the row reads, so a cache HIT offloads the primary too — not just a miss.
392
+ * 3. Freshness check via {@link evaluateCacheFreshness} on that connection:
393
+ * cached metadata + the retention watermark + a between-ts commit-log scan.
394
+ * 4. Fresh → return the cached data.
395
+ * Stale/miss → recompute via `queryInternalOn` (on the chosen connection)
396
+ * and write BOTH the data and the metadata (readSet + snapshotTs) back, so
397
+ * a later one-shot or a fresh subscription can reuse it.
398
+ *
399
+ * The retention guard is what makes an untracked one-shot safe: nothing holds
400
+ * the prune watermark down for it, so an entry whose snapshot has fallen below
401
+ * the oldest retained commit log is treated as a miss (its freshness scan would
402
+ * be incomplete).
403
+ *
404
+ * `lastSnapshotTs` is the primary freshness upper bound — the latest snapshot ts
405
+ * as of this call. The caller loads it (via `db.getLatestSnapshotTimestamp()`)
406
+ * and passes it in, exactly like the subscribe path loads it before `register`,
407
+ * so the snapshot read lives at the caller seam rather than being hidden here.
408
+ *
409
+ * `clientMinTs` is the caller's read high-water (the largest commit/snapshot ts
410
+ * it has already observed). It gates replica routing: a query only runs on the
411
+ * replica when the replica has applied at least `clientMinTs` — otherwise reads
412
+ * (and the freshness scan) stay on the primary, preserving read-your-writes and
413
+ * monotonic reads. It is clamped to `lastSnapshotTs` here, so a client can at
414
+ * most force the primary (never a value beyond it). Returns the data plus the
415
+ * `ts` the result reflects, which the caller echoes so the client can advance
416
+ * its high-water.
417
+ */
418
+ async function runCachedQuery(deps, handlerObs, procedure, procedureName, input, serverCtx, lastSnapshotTs, clientMinTs) {
419
+ const queryIdentity = resolveQueryIdentity(procedure, serverCtx, input, deps.getUserId);
420
+ const cacheKey = await generateCacheKey(procedureName, stableStringify(input), queryIdentity);
421
+ const routing = await resolveReadRouting(deps.db, lastSnapshotTs, clientMinTs ?? 0n);
422
+ const readTs = routing.readTs;
423
+ const { metadata, freshness } = await traceCacheCall("freshness", async () => {
424
+ const metadata = await deps.cache.getQueryCacheMetaData(cacheKey);
425
+ return {
426
+ metadata,
427
+ freshness: await evaluateCacheFreshness(routing.readImpl, metadata, readTs, deps.getMinRetainedTs())
428
+ };
429
+ });
430
+ if (freshness.fresh) {
431
+ const cached = await traceCacheCall("get", async (setHit) => {
432
+ const c = await deps.cache.getQueryCacheFor(cacheKey);
433
+ setHit(!!(c && metadata && c.version === metadata.version));
434
+ return c;
435
+ });
436
+ if (cached && metadata && cached.version === metadata.version) {
437
+ if (readTs > metadata.lastSnapshotTs) deps.cache.advanceQueryCacheMetadata(cacheKey, {
438
+ lastSnapshotTs: readTs,
439
+ version: metadata.version,
440
+ readSet: metadata.readSet
441
+ }).catch((err) => logger.error(err, "one-shot horizon advance failed"));
442
+ const servedTs = readTs > metadata.lastSnapshotTs ? readTs : metadata.lastSnapshotTs;
443
+ return {
444
+ data: cached.data,
445
+ ts: servedTs,
446
+ readSet: metadata.readSet
447
+ };
448
+ }
449
+ }
450
+ const result = await deps.db.queryInternalOn({
451
+ impl: routing.readImpl,
452
+ beginTs: readTs
453
+ }, async (ctx) => {
454
+ const fn = procedure.fn;
455
+ return traceQueryExecution(procedureName, "request", parentContextFor(handlerObs), (querySpan) => {
456
+ const qctx = {
457
+ db: ctx,
458
+ serverCtx,
459
+ ...makeConnRouting(ctx, routing),
460
+ log: handlerObs.log,
461
+ span: querySpan
462
+ };
463
+ return fn(qctx, input);
464
+ });
465
+ });
466
+ await deps.cache.setQueryCacheAndMetadataFor(cacheKey, result.data, {
467
+ lastSnapshotTs: result.ts,
468
+ version: result.ts.toString(),
469
+ readSet: result.readSet
470
+ }).catch((err) => logger.error(err, "one-shot query cache write failed"));
471
+ return {
472
+ data: result.data,
473
+ ts: result.ts,
474
+ readSet: result.readSet
475
+ };
476
+ }
477
+ /**
478
+ * Cache-first evaluation of a nested query JOINED to a parent transaction — the
479
+ * path behind `caller.<proc>.runQuery` when invoked from a query or mutation
480
+ * handler (`parent` is the parent's live {@link DbReader}/{@link DbWriter}).
481
+ *
482
+ * The nested query shares the parent's SNAPSHOT + readSet: it reads on the
483
+ * parent's own bound connection (`parent.db`) at the parent's `beginTs`, so the
484
+ * freshness scan and any replica/primary connection the parent was routed to
485
+ * stay consistent — a rebind can never mix snapshots. Reads made on a cache
486
+ * MISS accumulate straight into the parent's readSet (making the parent re-run /
487
+ * OCC-conflict on concurrent changes to what the nested query observed), and on
488
+ * a HIT the cached entry's readSet is appended to the parent instead, so the
489
+ * parent's subscription invalidation / OCC fencing still covers the rows the
490
+ * nested query depends on.
491
+ *
492
+ * Mutation parents need two guards that query parents don't:
493
+ * 1. **Pending writes** (the mutation has already written in this tx) — the
494
+ * cache is bypassed entirely: the nested query must see the parent's
495
+ * uncommitted writes (reads-your-writes), which the committed cache can't
496
+ * reflect, and caching that state would leak uncommitted data to other
497
+ * readers.
498
+ * 2. **No write-back** — a `DbWriter`'s `beginTs` is `latest + 1` (a phantom,
499
+ * not a real committed ts), so a write-back would stamp a horizon no reader
500
+ * can ever reach and (worse) invert the freshness scan window. Rather than
501
+ * clamp it, mutation parents simply never write back: nothing rebuilds a
502
+ * nested read after the parent commits (unlike a tracked subscription), and
503
+ * the mutation's own commit usually invalidates the entry anyway. They only
504
+ * CONSUME cache hits written by query parents / one-shots — the cached
505
+ * entry's readSet is folded into the parent so OCC fencing still holds, and
506
+ * the horizon is never advanced for the same phantom-ts reason.
507
+ */
508
+ async function runCachedQueryInParent(deps, handlerObs, procedure, procedureName, input, serverCtx, parent) {
509
+ const isMutationParent = parent instanceof DbWriter;
510
+ const cache = isMutationParent && parent.internalGetWriteSet().length > 0 ? void 0 : deps.cache;
511
+ const readImpl = parent.db;
512
+ const readTs = parent.beginTs;
513
+ const runOnParent = () => traceQueryExecution(procedureName, "request", parentContextFor(handlerObs), (querySpan) => {
514
+ const qctx = {
515
+ db: parent,
516
+ serverCtx,
517
+ ...parentConn,
518
+ log: handlerObs.log,
519
+ span: querySpan
520
+ };
521
+ const fn = procedure.fn;
522
+ return fn(qctx, input);
523
+ });
524
+ const cacheKey = cache ? await generateCacheKey(procedureName, stableStringify(input), resolveQueryIdentity(procedure, serverCtx, input, deps.getUserId)) : void 0;
525
+ if (cache && cacheKey) {
526
+ const { metadata, freshness } = await traceCacheCall("freshness", async () => {
527
+ const metadata = await cache.getQueryCacheMetaData(cacheKey);
528
+ return {
529
+ metadata,
530
+ freshness: await evaluateCacheFreshness(readImpl, metadata, readTs, deps.getMinRetainedTs())
531
+ };
532
+ });
533
+ if (freshness.fresh) {
534
+ const cached = await traceCacheCall("get", async (setHit) => {
535
+ const c = await cache.getQueryCacheFor(cacheKey);
536
+ setHit(!!(c && metadata && c.version === metadata.version));
537
+ return c;
538
+ });
539
+ if (cached && metadata && cached.version === metadata.version) {
540
+ parent.appendInternalReadSet(metadata.readSet);
541
+ if (!isMutationParent && readTs > metadata.lastSnapshotTs) cache.advanceQueryCacheMetadata(cacheKey, {
542
+ lastSnapshotTs: readTs,
543
+ version: metadata.version,
544
+ readSet: metadata.readSet
545
+ }).catch((err) => logger.error(err, "nested query horizon advance failed"));
546
+ return cached.data;
547
+ }
548
+ }
549
+ }
550
+ const beforeLen = parent.internalGetReadSet().length;
551
+ const data = await runOnParent();
552
+ if (!isMutationParent && cache && cacheKey) {
553
+ const nestedReadSet = parent.internalGetReadSet().slice(beforeLen);
554
+ await cache.setQueryCacheAndMetadataFor(cacheKey, data, {
555
+ lastSnapshotTs: parent.beginTs,
556
+ version: parent.beginTs.toString(),
557
+ readSet: nestedReadSet
558
+ }).catch((err) => logger.error(err, "nested query cache write failed"));
559
+ }
560
+ return data;
561
+ }
562
+ //#endregion
563
+ export { elapsedMs as a, traceQueryExecution as c, generateSubscriptionId as d, getHashOf as f, configureRootLogger as i, ANONYMOUS_IDENTITY as l, runCachedQueryInParent as n, getRootLogger as o, resolveQueryIdentity as p, Observability as r, startTimer as s, runCachedQuery as t, generateCacheKey as u };
564
+
565
+ //# sourceMappingURL=one-shot-query-BBiv9LGP.js.map