@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.
- package/dist/index-BuWa503L.d.ts +2445 -0
- package/dist/index-BuWa503L.d.ts.map +1 -0
- package/dist/index-CBIjmPUS.d.ts +2444 -0
- package/dist/index-CBIjmPUS.d.ts.map +1 -0
- package/dist/index-CKo6HvJc.d.ts +2455 -0
- package/dist/index-CKo6HvJc.d.ts.map +1 -0
- package/dist/index-DShKjpzF.d.ts +2453 -0
- package/dist/index-DShKjpzF.d.ts.map +1 -0
- package/dist/index-Dcxf_Gm-.d.ts +2454 -0
- package/dist/index-Dcxf_Gm-.d.ts.map +1 -0
- package/dist/index-DsBAE32T.d.ts +2459 -0
- package/dist/index-DsBAE32T.d.ts.map +1 -0
- package/dist/logger-DrXccWFZ.js +346 -0
- package/dist/logger-DrXccWFZ.js.map +1 -0
- package/dist/mysql-B2u0Ye4x.d.ts +114 -0
- package/dist/mysql-B2u0Ye4x.d.ts.map +1 -0
- package/dist/mysql-BHW-tAqt.d.ts +114 -0
- package/dist/mysql-BHW-tAqt.d.ts.map +1 -0
- package/dist/mysql-C79_qNQO.d.ts +114 -0
- package/dist/mysql-C79_qNQO.d.ts.map +1 -0
- package/dist/mysql-C_TTCZ8j.d.ts +114 -0
- package/dist/mysql-C_TTCZ8j.d.ts.map +1 -0
- package/dist/mysql-CyzJqrvO.d.ts +114 -0
- package/dist/mysql-CyzJqrvO.d.ts.map +1 -0
- package/dist/mysql-ygH2EMGn.js +622 -0
- package/dist/mysql-ygH2EMGn.js.map +1 -0
- package/dist/mysql-z9Jefhd6.d.ts +114 -0
- package/dist/mysql-z9Jefhd6.d.ts.map +1 -0
- package/dist/object-storage-DTVmZq3l.d.ts +110 -0
- package/dist/object-storage-DTVmZq3l.d.ts.map +1 -0
- package/dist/one-shot-query-9mCboWaB.js +565 -0
- package/dist/one-shot-query-9mCboWaB.js.map +1 -0
- package/dist/one-shot-query-B9SjfcCD.js +550 -0
- package/dist/one-shot-query-B9SjfcCD.js.map +1 -0
- package/dist/one-shot-query-BBiv9LGP.js +565 -0
- package/dist/one-shot-query-BBiv9LGP.js.map +1 -0
- package/dist/one-shot-query-Bxz_N-NE.js +563 -0
- package/dist/one-shot-query-Bxz_N-NE.js.map +1 -0
- package/dist/one-shot-query-DE9oQU40.js +550 -0
- package/dist/one-shot-query-DE9oQU40.js.map +1 -0
- package/dist/one-shot-query-adoWqGLs.js +565 -0
- package/dist/one-shot-query-adoWqGLs.js.map +1 -0
- package/dist/overlap-checker-CCgq_Tpa.js +284 -0
- package/dist/overlap-checker-CCgq_Tpa.js.map +1 -0
- package/dist/postgres-B2FLN1Kw.d.ts +118 -0
- package/dist/postgres-B2FLN1Kw.d.ts.map +1 -0
- package/dist/postgres-C7xHWbij.d.ts +118 -0
- package/dist/postgres-C7xHWbij.d.ts.map +1 -0
- package/dist/postgres-DBMAv_dq.d.ts +118 -0
- package/dist/postgres-DBMAv_dq.d.ts.map +1 -0
- package/dist/postgres-DBuP9ON_.d.ts +118 -0
- package/dist/postgres-DBuP9ON_.d.ts.map +1 -0
- package/dist/postgres-DwiUT_A6.js +867 -0
- package/dist/postgres-DwiUT_A6.js.map +1 -0
- package/dist/postgres-I4sgdBnG.d.ts +118 -0
- package/dist/postgres-I4sgdBnG.d.ts.map +1 -0
- package/dist/postgres-SM8muZqN.d.ts +118 -0
- package/dist/postgres-SM8muZqN.d.ts.map +1 -0
- package/dist/router-DP2ThAwh.js.map +1 -1
- package/dist/src/client/index.d.ts +1 -1
- package/dist/src/client/index.js +47 -50
- package/dist/src/client/index.js.map +1 -1
- package/dist/src/exports/mysql.d.ts +1 -1
- package/dist/src/exports/mysql.js +1 -1
- package/dist/src/exports/postgres.d.ts +1 -1
- package/dist/src/exports/postgres.js +1 -1
- package/dist/src/exports/procedure.d.ts +1 -1
- package/dist/src/exports/schema-sql.d.ts +1 -1
- package/dist/src/exports/server.d.ts +11 -4
- package/dist/src/exports/server.d.ts.map +1 -1
- package/dist/src/exports/server.js +31 -410
- package/dist/src/exports/server.js.map +1 -1
- package/dist/src/exports/storage.d.ts +4 -2
- package/dist/src/exports/storage.d.ts.map +1 -1
- package/dist/src/exports/storage.js +23 -1
- package/dist/src/exports/storage.js.map +1 -1
- package/dist/src/exports/sub-manager-worker-entry.js +1 -1
- package/dist/src/exports/types.d.ts +2 -2
- package/dist/sub-worker-dispatch-BLujn13f.js +792 -0
- package/dist/sub-worker-dispatch-BLujn13f.js.map +1 -0
- package/dist/types_server-BCst4TfZ.d.ts +703 -0
- package/dist/types_server-BCst4TfZ.d.ts.map +1 -0
- package/dist/types_server-BYwLkwbC.d.ts +691 -0
- package/dist/types_server-BYwLkwbC.d.ts.map +1 -0
- package/dist/types_server-Bvrohyc8.d.ts +691 -0
- package/dist/types_server-Bvrohyc8.d.ts.map +1 -0
- package/dist/types_server-C6oCm9oC.d.ts +691 -0
- package/dist/types_server-C6oCm9oC.d.ts.map +1 -0
- package/dist/types_server-CHJLfdxb.d.ts +691 -0
- package/dist/types_server-CHJLfdxb.d.ts.map +1 -0
- package/dist/types_server-DnO_fp-F.d.ts +691 -0
- package/dist/types_server-DnO_fp-F.d.ts.map +1 -0
- package/dist/types_server-gSgzZHuD.d.ts +703 -0
- package/dist/types_server-gSgzZHuD.d.ts.map +1 -0
- package/package.json +3 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"one-shot-query-adoWqGLs.js","names":["defaultLogger","otelContext","otelContext"],"sources":["../src/server/sub_hash.ts","../src/observability.ts","../src/db/read-routing.ts","../src/server/one-shot-query.ts"],"sourcesContent":["// Server-only hashing helpers. Kept off the `helper.ts` module so a browser\n// bundle that only imports `stableStringify` doesn't pull in `hash-wasm`.\n\nimport { sha1 } from \"hash-wasm\";\nimport { stableStringify } from \"../helper\";\n\n/** Compute stable hash using json-stable-stringify and SHA1. */\nexport async function getHashOf(input: unknown): Promise<string> {\n const stableStr = typeof input === \"string\" ? input : stableStringify(input);\n return sha1(stableStr ?? \"\");\n}\n\n/**\n * Default identity literal used when neither a procedure-level\n * `queryIdentity` override nor a per-session user is available.\n */\nexport const ANONYMOUS_IDENTITY = \"anonymous\";\n\n/**\n * Resolve the segmentation key that participates in the subId/cacheKey hash for\n * a query. Shared by every query transport (WS subscribe, WS one-shot call, and\n * HTTP RPC) so identity — and therefore cache segmentation — is computed\n * identically regardless of how the query is invoked. Priority:\n *\n * 1. Procedure-level `queryIdentity` override:\n * - `false` → `ANONYMOUS_IDENTITY` (cache shared across users)\n * - `string` → that literal\n * - `function` → serverCtx + input → string | null | undefined\n * 2. `getUserId(serverCtx)` → falls back to `ANONYMOUS_IDENTITY` when it\n * returns null/undefined or isn't configured.\n */\nexport function resolveQueryIdentity<TContext>(\n procedure: {\n queryIdentity?:\n | false\n | string\n | ((serverCtx: TContext | undefined, input: unknown) => string | null | undefined);\n },\n serverCtx: TContext | undefined,\n input: unknown,\n getUserId?: (ctx: TContext | undefined) => string | null,\n): string {\n const override = procedure.queryIdentity;\n if (override !== undefined) {\n if (override === false) return ANONYMOUS_IDENTITY;\n if (typeof override === \"string\") return override;\n if (typeof override === \"function\") {\n const value = override(serverCtx, input);\n if (value === null || value === undefined) return ANONYMOUS_IDENTITY;\n return value;\n }\n }\n if (!getUserId) return ANONYMOUS_IDENTITY;\n return getUserId(serverCtx) ?? ANONYMOUS_IDENTITY;\n}\n\n/**\n * Compute a subscription id from hash of procedure + input + queryIdentity.\n *\n * `queryIdentity` is the segmentation key for cache/subscriptions. By default\n * it is the authenticated user's id (so two users get distinct subscriptions\n * for the same query), but a procedure may override it to share cache/subs\n * across users (e.g. for a public, user-independent query).\n */\nexport async function generateSubscriptionId(\n procedure: string,\n input: unknown | string,\n queryIdentity: string,\n): Promise<string> {\n const inputStr = typeof input === 'string' ? input : stableStringify(input);\n const hash = await getHashOf(`${procedure}_${inputStr}_${queryIdentity}`);\n return `sub_${hash}`;\n}\n\n/**\n * Compute cache key from query name + args + queryIdentity. The identity\n * participates so that user-specific results don't poison each other's\n * cache when a procedure does authorize per-user.\n */\nexport async function generateCacheKey(\n queryName: string,\n input: unknown | string,\n queryIdentity: string,\n): Promise<string> {\n const argsStr = typeof input === 'string' ? input : stableStringify(input);\n const hash = await getHashOf(`${queryName}_${argsStr}_${queryIdentity}`);\n return `ck_${hash}`;\n}\n","// ─── Observability ────────────────────────────────────────────────────────────\n//\n// One seam for the three signals a running Supalive server emits:\n//\n// • Structured LOGS — one line per operation (via the injectable pino logger).\n// • METRICS — request counter + duration histogram + active\n// connection/subscription up-down counters, through the\n// OpenTelemetry metrics API.\n// • EVENTS — an optional `onEvent` callback fed the same typed\n// record, so an app can drive its own metrics/tracing\n// without Supalive depending on any specific backend.\n//\n// The OpenTelemetry API used here is a NO-OP until the host app registers an SDK\n// (a MeterProvider / TracerProvider). Core therefore carries near-zero overhead\n// and zero vendor lock-in: the app owns the exporter (Prometheus, OTLP, …) and\n// switches backends by config alone. See `packages/backend/src/telemetry.ts`.\n//\n// tracing plugs into the SAME `record()` call site: it starts a span\n// per operation and propagates W3C trace context from the client. Nothing at the\n// call sites needs to change.\n\nimport type { Level, Logger } from \"pino\";\nimport {\n metrics,\n trace,\n context as otelContext,\n propagation,\n SpanKind,\n SpanStatusCode,\n ROOT_CONTEXT,\n isSpanContextValid,\n type Counter,\n type Histogram,\n type UpDownCounter,\n type Tracer,\n type Span,\n type Context,\n} from \"@opentelemetry/api\";\nimport { logger as defaultLogger } from \"./logger\";\nimport type { HandlerLogger } from \"./router/procedure\";\n\n// ─── Injectable root logger ───────────────────────────────────────────────────\n//\n// Modules migrated to observability read their base logger from here rather than\n// from the module-level singleton in `logger.ts`, so an app can swap in its own\n// configured pino instance (transports, redaction, bindings) with\n// `configureRootLogger()` at boot. Un-migrated modules keep using the singleton;\n// migration is incremental.\n\nlet rootLogger: Logger = defaultLogger;\n\n/** Replace the process-wide root logger. Call once at startup, before creating\n * the server, to route Supalive's request/lifecycle logs through your own pino\n * instance. */\nexport function configureRootLogger(logger: Logger): void {\n rootLogger = logger;\n}\n\n/** The current root logger (defaults to the env-configured singleton). */\nexport function getRootLogger(): Logger {\n return rootLogger;\n}\n\n// ─── Operation event ──────────────────────────────────────────────────────────\n\nexport type OperationTransport = \"ws\" | \"http\" | \"internal\";\nexport type OperationKind =\n | \"query\"\n | \"mutation\"\n | \"action\"\n | \"subscribe\"\n | \"unsubscribe\"\n | \"job\"\n | \"connection\";\nexport type OperationOutcome = \"ok\" | \"error\";\n\n/**\n * A single completed operation. Emitted once per WS call / subscribe / HTTP RPC\n * request / job run, and handed to metrics, the log line, and the `onEvent`\n * hook. High-cardinality fields (`reqId`, `userId`, `sessionId`) are logged/\n * forwarded but NEVER used as metric attributes.\n */\nexport interface OperationEvent {\n /** Coarse operation family, e.g. \"rpc\" | \"subscribe\" | \"unsubscribe\" | \"job\" | \"connection\". */\n op: string;\n transport: OperationTransport;\n procedure?: string;\n kind?: OperationKind;\n outcome: OperationOutcome;\n /** Wall-clock duration in milliseconds. */\n durationMs: number;\n /** Error code for a failed op (e.g. NOT_FOUND, EXECUTION_ERROR, RATE_LIMITED). */\n code?: string;\n userId?: string | null;\n sessionId?: string;\n /** Per-request correlation id (WS request id, or a generated HTTP id). */\n reqId?: string;\n /** The thrown error, when the failure was an unexpected exception (→ error level). */\n error?: unknown;\n /** Extra structured fields to include on the log line only. */\n extra?: Record<string, unknown>;\n /** The operation's OTel span, when one was started. `record()` finalizes and\n * ends it (status + attributes + exception). */\n span?: Span;\n}\n\n/** Whether to emit a log line per request. Metrics + `onEvent` fire regardless. */\nexport type RequestLogMode = \"all\" | \"errors\" | \"off\";\n\nexport interface ObservabilityOptions {\n /** Base logger for request/lifecycle lines. Defaults to {@link getRootLogger}. */\n logger?: Logger;\n /** Level override applied to the derived request logger. */\n level?: Level;\n /** Per-request log policy. `\"all\"` logs successes at info + failures; `\"errors\"`\n * logs only failures; `\"off\"` logs neither (metrics/onEvent still fire).\n * Default `\"all\"`. */\n requestLog?: RequestLogMode;\n /** Callback fed every {@link OperationEvent}. Errors thrown by it are swallowed. */\n onEvent?: (e: OperationEvent) => void;\n /** OpenTelemetry meter name. Default `\"@supalive/core\"`. */\n meterName?: string;\n /** OpenTelemetry meter version. Defaults to the core package version. */\n meterVersion?: string;\n}\n\n// Error codes that represent an EXPECTED client-side rejection rather than a\n// server fault. These log at `warn`; anything else (or a thrown exception) logs\n// at `error`.\nconst CLIENT_ERROR_CODES = new Set([\n \"NOT_FOUND\",\n \"BAD_REQUEST\",\n \"INVALID_MESSAGE\",\n \"PARSE_ERROR\",\n \"UNAUTHENTICATED\",\n \"RATE_LIMITED\",\n \"METHOD_NOT_ALLOWED\",\n \"TOO_MANY_CONCURRENT_MUTATIONS\",\n \"TOO_MANY_CONCURRENT_ACTIONS\",\n \"SUBSCRIBE_ERROR\",\n]);\n\n/**\n * The bound observability surface for one server instance: an injectable logger,\n * OTel metric instruments, and the `record()` seam every operation funnels\n * through. Built once in the server constructor and shared with the HTTP RPC\n * handler.\n */\nexport class Observability {\n readonly logger: Logger;\n private readonly requestLog: RequestLogMode;\n private readonly onEvent?: (e: OperationEvent) => void;\n\n private readonly requestCounter: Counter;\n private readonly durationHistogram: Histogram;\n private readonly activeConnections: UpDownCounter;\n private readonly activeSubscriptions: UpDownCounter;\n private readonly tracer: Tracer;\n\n constructor(opts: ObservabilityOptions = {}) {\n const base = opts.logger ?? getRootLogger();\n this.logger = opts.level ? base.child({}, { level: opts.level }) : base;\n this.requestLog = opts.requestLog ?? \"all\";\n this.onEvent = opts.onEvent;\n\n const meterName = opts.meterName ?? \"@supalive/core\";\n const meter = metrics.getMeter(\n meterName,\n opts.meterVersion,\n );\n this.tracer = trace.getTracer(\n meterName,\n opts.meterVersion,\n );\n this.requestCounter = meter.createCounter(\"supalive_requests_total\", {\n description: \"Total Supalive operations (WS calls, subscribes, HTTP RPC, jobs).\",\n });\n this.durationHistogram = meter.createHistogram(\"supalive_request_duration_ms\", {\n description: \"Operation duration in milliseconds.\",\n unit: \"ms\",\n });\n this.activeConnections = meter.createUpDownCounter(\"supalive_active_connections\", {\n description: \"Currently open WebSocket connections.\",\n });\n this.activeSubscriptions = meter.createUpDownCounter(\"supalive_active_subscriptions\", {\n description: \"Currently registered live-query subscriptions on this instance.\",\n });\n }\n\n /** Record one completed operation: metrics + optional onEvent + one log line,\n * and finalize the op's span (status/attributes/exception) when present. */\n record(e: OperationEvent): void {\n // Finalize the span first so trace ids are available to the log line/event.\n const traceId = this.finalizeSpan(e);\n if (traceId) {\n (e.extra ??= {}).traceId = traceId;\n }\n\n // Metrics — bounded-cardinality attributes only.\n const attrs = {\n transport: e.transport,\n procedure: e.procedure ?? \"\",\n kind: e.kind ?? \"\",\n outcome: e.outcome,\n code: e.code ?? \"\",\n };\n this.requestCounter.add(1, attrs);\n this.durationHistogram.record(e.durationMs, attrs);\n\n // App-owned metrics/tracing seam.\n if (this.onEvent) {\n try {\n this.onEvent(e);\n } catch {\n /* an app hook must never break request handling */\n }\n }\n\n // One structured log line.\n if (this.requestLog === \"off\") return;\n if (e.outcome === \"ok\") {\n if (this.requestLog !== \"all\") return;\n this.logger.info(logFields(e), summary(e));\n return;\n }\n // outcome === \"error\"\n const fields = logFields(e);\n const expected = !e.error && e.code != null && CLIENT_ERROR_CODES.has(e.code);\n if (expected) {\n this.logger.warn(fields, summary(e));\n } else {\n this.logger.error({ ...fields, err: e.error }, summary(e));\n }\n }\n\n // ─── Tracing ────────────────────────────────────────────────────────────────\n\n /**\n * Start a SERVER span for an operation, as a child of `parent` (or the active\n * context). No-op — returns a non-recording span — until a TracerProvider is\n * registered by the app, so this costs nothing when tracing is disabled.\n * Returns the span plus a context that has it active, for `context.with(...)`\n * wrapping so nested handler work / `ctx.span` children attach correctly.\n */\n startSpan(\n name: string,\n opts: {\n transport?: OperationTransport;\n procedure?: string;\n kind?: OperationKind;\n parent?: Context;\n } = {},\n ): { span: Span; context: Context } {\n const parent = opts.parent ?? otelContext.active();\n const span = this.tracer.startSpan(\n name,\n {\n kind: SpanKind.SERVER,\n attributes: {\n ...(opts.transport ? { \"supalive.transport\": opts.transport } : {}),\n ...(opts.procedure ? { \"supalive.procedure\": opts.procedure } : {}),\n ...(opts.kind ? { \"supalive.kind\": opts.kind } : {}),\n },\n },\n parent,\n );\n return { span, context: trace.setSpan(parent, span) };\n }\n\n /**\n * Continue a trace from an inbound carrier (HTTP headers, or a WS message's\n * `trace` field). Returns a context to pass as `startSpan({ parent })`. When\n * no propagator is registered / no trace context is present, returns the root\n * context (a fresh trace).\n */\n extractContext(\n carrier: Record<string, string | string[] | undefined>,\n ): Context {\n return propagation.extract(ROOT_CONTEXT, carrier);\n }\n\n /** Set the span's final status/attributes from the event, then end it.\n * Returns the (valid) trace id for log correlation, or undefined. */\n private finalizeSpan(e: OperationEvent): string | undefined {\n const span = e.span;\n if (!span) return undefined;\n try {\n span.setAttribute(\"supalive.outcome\", e.outcome);\n if (e.code) span.setAttribute(\"supalive.code\", e.code);\n if (e.procedure) span.setAttribute(\"supalive.procedure\", e.procedure);\n if (e.kind) span.setAttribute(\"supalive.kind\", e.kind);\n if (e.outcome === \"error\") {\n if (e.error instanceof Error) span.recordException(e.error);\n span.setStatus({ code: SpanStatusCode.ERROR, message: e.code });\n } else {\n span.setStatus({ code: SpanStatusCode.OK });\n }\n } finally {\n span.end();\n }\n const sc = span.spanContext();\n return isSpanContextValid(sc) ? sc.traceId : undefined;\n }\n\n /** A request-scoped child logger for `ctx.log`, bound with the given fields\n * (reqId, procedure, and trace id when tracing is on). */\n childLogger(fields: Record<string, unknown>): Logger {\n return this.logger.child(fields);\n }\n\n /**\n * A LAZY request-scoped `ctx.log`. Most handlers never log, and pino's\n * `.child()` (bindings merge + serialization) isn't free — so this defers the\n * child creation until the first `debug/info/warn/error` call and reuses it\n * after. When a handler logs nothing, no child logger is ever allocated; the\n * only per-request cost is this small delegator object. Returns the narrow\n * {@link HandlerLogger} surface the handler context exposes.\n */\n lazyChildLogger(fields: Record<string, unknown>): HandlerLogger {\n const base = this.logger;\n let child: Logger | undefined;\n const get = (): Logger => (child ??= base.child(fields));\n return {\n debug: (...args: any[]) => (get().debug as (...a: any[]) => void)(...args),\n info: (...args: any[]) => (get().info as (...a: any[]) => void)(...args),\n warn: (...args: any[]) => (get().warn as (...a: any[]) => void)(...args),\n error: (...args: any[]) => (get().error as (...a: any[]) => void)(...args),\n };\n }\n\n /** The span's trace id, or undefined when tracing is off / the span is\n * non-recording. For binding onto `ctx.log`. */\n traceIdOf(span: Span | undefined): string | undefined {\n if (!span) return undefined;\n const sc = span.spanContext();\n return isSpanContextValid(sc) ? sc.traceId : undefined;\n }\n\n connectionOpened(): void {\n this.activeConnections.add(1);\n }\n connectionClosed(): void {\n this.activeConnections.add(-1);\n }\n subscriptionAdded(n = 1): void {\n this.activeSubscriptions.add(n);\n }\n subscriptionRemoved(n = 1): void {\n this.activeSubscriptions.add(-n);\n }\n}\n\n/** Start marker for an operation; pass to {@link elapsedMs} to get its duration. */\nexport function startTimer(): number {\n return performance.now();\n}\n\nexport function elapsedMs(startedAt: number): number {\n return Math.round((performance.now() - startedAt) * 1000) / 1000;\n}\n\n// ─── Query-execution span ─────────────────────────────────────────────────────\n\nconst coreTracer = trace.getTracer(\"@supalive/core\");\n\n/** What triggered a query execution — a client request/subscribe, or a background\n * reactive recompute (live-query invalidation / sub-manager recovery). */\nexport type QueryTrigger = \"request\" | \"subscribe\" | \"recompute\" | \"recovery\";\n\n/**\n * Run a query handler inside a `query <procedureName>` span, nested under\n * `parentContext` (the operation span) or the active context. The span is handed\n * to `run` so it becomes the query handler's `ctx.span` and the parent of any DB\n * spans. Cache HITs never call this (no execution), so a span appearing means a\n * real recompute — including the otherwise-invisible background reactive path,\n * where `parentContext` is omitted and the span is a root.\n *\n * No-op-cheap when tracing is off: the started span is non-recording, so we skip\n * the `context.with` and just run.\n */\nexport function traceQueryExecution<T>(\n procedureName: string,\n trigger: QueryTrigger,\n parentContext: Context | undefined,\n run: (span: Span) => Promise<T>,\n): Promise<T> {\n const parent = parentContext ?? otelContext.active();\n const span = coreTracer.startSpan(\n `query ${procedureName}`,\n {\n attributes: {\n \"supalive.kind\": \"query\",\n \"supalive.procedure\": procedureName,\n \"supalive.trigger\": trigger,\n },\n },\n parent,\n );\n const exec = async (): Promise<T> => {\n try {\n const r = await run(span);\n span.setStatus({ code: SpanStatusCode.OK });\n return r;\n } catch (err) {\n if (err instanceof Error) span.recordException(err);\n span.setStatus({ code: SpanStatusCode.ERROR });\n throw err;\n } finally {\n span.end();\n }\n };\n return span.isRecording()\n ? otelContext.with(trace.setSpan(parent, span), exec)\n : exec();\n}\n\nfunction logFields(e: OperationEvent): Record<string, unknown> {\n const f: Record<string, unknown> = {\n op: e.op,\n transport: e.transport,\n outcome: e.outcome,\n durMs: e.durationMs,\n };\n if (e.procedure) f.procedure = e.procedure;\n if (e.kind) f.kind = e.kind;\n if (e.code) f.code = e.code;\n if (e.reqId) f.reqId = e.reqId;\n if (e.sessionId) f.sessionId = e.sessionId;\n if (e.userId != null) f.userId = e.userId;\n if (e.extra) Object.assign(f, e.extra);\n return f;\n}\n\nfunction summary(e: OperationEvent): string {\n const label = e.procedure ? `${e.kind ?? e.op} ${e.procedure}` : e.op;\n return `${e.transport} ${label} ${e.outcome}`;\n}\n","import type { Database } from \"./db\";\nimport type { SupaliveDb } from \"./realtime_db\";\nimport type { DbReader } from \"./context\";\nimport type { ConnOpts, QueryCtx } from \"../router/procedure\";\n\n/**\n * Resolved read-connection decision for one request. Shared by every read path\n * that can offload to a replica (the one-shot query cache and the internal\n * caller) so the \"may I read the replica?\" rule lives in ONE place.\n *\n * `primary*` is always the writer; `replica*` aliases the primary when no\n * replica is configured. `read*` is the DEFAULT connection a read SHOULD use\n * given the caller's high-water — replica when it's caught up, else primary.\n */\nexport interface ReadRouting {\n /** Primary (writer) connection + its latest snapshot ts. */\n primaryImpl: Database;\n primaryTs: bigint;\n /** Replica connection + its applied snapshot ts. Aliases primary when none. */\n replicaImpl: Database;\n replicaTs: bigint;\n /** The chosen default read connection: replica when caught up, else primary. */\n readImpl: Database;\n readTs: bigint;\n /** True when the default resolved to a DISTINCT replica (not the primary). */\n useReplica: boolean;\n}\n\n/**\n * Decide which connection a read should run on, by commit_ts high-water — the\n * single routing rule used by both the one-shot query path and the internal\n * caller.\n *\n * `minTs` is the reader's floor: the largest commit/snapshot ts it has already\n * observed and must not read behind (read-your-writes + monotonic reads). It is\n * clamped to `primaryTs` here, so a bogus/oversized value can at most force the\n * primary — never an error, never a value beyond what the primary has. A read\n * may use the replica ONLY when the replica has applied at least this floor\n * (`replicaTs >= minTs`); otherwise the replica is lagging past what the caller\n * has seen and the read stays on the primary.\n *\n * `primaryTs` is passed in (rather than loaded here) so it stays consistent with\n * whatever snapshot the caller already loaded at its seam — e.g. the one-shot\n * path uses it as the cache-freshness upper bound. The replica's ts is loaded\n * here, and only when a replica is actually configured (no wasted round trip on\n * primary-only deployments).\n */\nexport async function resolveReadRouting(\n db: SupaliveDb,\n primaryLastSnapshotTs: bigint,\n minTs: bigint,\n): Promise<ReadRouting> {\n const primaryImpl = db.impl;\n minTs = minTs == 0n ? primaryLastSnapshotTs : minTs;\n // Clamp: the floor can never exceed what the primary itself has committed.\n const floor = minTs > primaryLastSnapshotTs ? primaryLastSnapshotTs : minTs;\n\n const replicaDb = db.replica;\n const replicaImpl = replicaDb ? replicaDb.impl : primaryImpl;\n const replicaTs = replicaDb ? await replicaImpl.getLatestSnapshotTimestamp() : primaryLastSnapshotTs;\n\n const useReplica = replicaDb !== undefined && replicaTs >= floor;\n return {\n primaryImpl,\n primaryTs: primaryLastSnapshotTs,\n replicaImpl,\n replicaTs,\n readImpl: useReplica ? replicaImpl : primaryImpl,\n readTs: useReplica ? replicaTs : primaryLastSnapshotTs,\n useReplica,\n };\n}\n\n/**\n * Build the per-handler `ctx.usePrimaryConn()` / `ctx.useReplicaConn()` methods\n * for a query, bound to a routing decision. Shared so the override semantics are\n * identical everywhere:\n *\n * • `usePrimaryConn()` — force the primary (always safe).\n * • `useReplicaConn({ readOwnWrite: true })` — force the primary (read-your-writes).\n * • `useReplicaConn()` — the SAME caught-up gate as the default: the replica\n * when it has applied the caller's floor, else the primary. It never binds a\n * replica that is lagging past `minTs`, so opting in can't serve stale reads.\n *\n * Must be called before the handler's first read (`useConnection` throws\n * otherwise) — that's the contract the ctx methods expose to handlers.\n */\nexport function makeConnRouting(\n reader: DbReader,\n routing: ReadRouting,\n): Pick<QueryCtx, \"usePrimaryConn\" | \"useReplicaConn\"> {\n return {\n usePrimaryConn: (_opts?: ConnOpts) =>\n reader.useConnection(routing.primaryImpl, routing.primaryTs),\n useReplicaConn: (opts?: ConnOpts) =>\n reader.useConnection(\n opts?.readOwnWrite ? routing.primaryImpl : routing.readImpl,\n opts?.readOwnWrite ? routing.primaryTs : routing.readTs,\n ),\n };\n}\n","import type { SupaliveDb } from \"../db/realtime_db\";\nimport type { CacheLayer } from \"../db/cache\";\nimport type { ReadEntry } from \"../db/types_db\";\nimport type { RegisteredProcedure } from \"../router/router\";\nimport { DbReader, DbWriter, TxContext } from \"../db/context\";\nimport type { QueryCtx, HandlerObservability } from \"../router/procedure\";\nimport { parentConn } from \"../router/procedure\";\nimport { context as otelContext, trace } from \"@opentelemetry/api\";\nimport type { Context } from \"@opentelemetry/api\";\nimport { traceQueryExecution } from \"../observability\";\nimport { traceCacheCall } from \"../db/db-trace\";\nimport { stableStringify } from \"../helper\";\nimport { generateCacheKey, resolveQueryIdentity } from \"./sub_hash\";\nimport { evaluateCacheFreshness } from \"./overlap-checker\";\nimport { resolveReadRouting, makeConnRouting } from \"../db/read-routing\";\nimport { logger } from \"../logger\";\n\nexport interface OneShotQueryDeps<TContext> {\n /** Same SupaliveDb the server/subscription path uses. */\n db: SupaliveDb;\n /** Shared query cache (Redis-backed). */\n cache: CacheLayer;\n /** Same identity resolver as subscriptions, so cache keys are segmented\n * identically whether a query arrives over WS subscribe, WS one-shot, or\n * HTTP RPC. Return `null` for anonymous (the cache's anonymous identity\n * segment — never `undefined`, which would require a per-call fallback). */\n getUserId: (ctx: TContext | undefined) => string | null;\n /** Reads the current in-memory retention watermark — the oldest retained\n * commit-log ts (see {@link RetentionWatermark}). A plain variable read, no\n * per-query round trip. */\n getMinRetainedTs: () => bigint;\n}\n\n/**\n * Parent context for the `query <name>` span. IMPLICIT linking is the primary\n * mechanism: the caller's handler already runs inside `otelContext.with(\n * started.context, ...)`, so the active span IS the correct parent — return\n * `undefined` and let {@link traceQueryExecution} default to the ambient\n * context. The EXPLICIT fallback (the span carried on the caller's handlerObs,\n * i.e. the parent handler's `ctx.span`) is used only when the ambient context\n * is root — e.g. a detached/fire-and-forget internal call.\n */\nfunction parentContextFor(handlerObs: HandlerObservability): Context | undefined {\n if (trace.getSpan(otelContext.active())) return undefined;\n return handlerObs.span ? trace.setSpan(otelContext.active(), handlerObs.span) : undefined;\n}\n\n/**\n * Execute a one-shot query (WS `call` or HTTP RPC) with the same caching model\n * as a live subscription — but without registering a tracked subscription.\n *\n * Flow (mirrors `handleSubscribe` / worker `register`):\n * 1. Resolve `queryIdentity` → `cacheKey` (identical segmentation to subs).\n * 2. Pick the read connection ONCE (replica when it's caught up to the client's\n * `clientMinTs`, else primary) and use it for BOTH the freshness scan and\n * the row reads, so a cache HIT offloads the primary too — not just a miss.\n * 3. Freshness check via {@link evaluateCacheFreshness} on that connection:\n * cached metadata + the retention watermark + a between-ts commit-log scan.\n * 4. Fresh → return the cached data.\n * Stale/miss → recompute via `queryInternalOn` (on the chosen connection)\n * and write BOTH the data and the metadata (readSet + snapshotTs) back, so\n * a later one-shot or a fresh subscription can reuse it.\n *\n * The retention guard is what makes an untracked one-shot safe: nothing holds\n * the prune watermark down for it, so an entry whose snapshot has fallen below\n * the oldest retained commit log is treated as a miss (its freshness scan would\n * be incomplete).\n *\n * `lastSnapshotTs` is the primary freshness upper bound — the latest snapshot ts\n * as of this call. The caller loads it (via `db.getLatestSnapshotTimestamp()`)\n * and passes it in, exactly like the subscribe path loads it before `register`,\n * so the snapshot read lives at the caller seam rather than being hidden here.\n *\n * `clientMinTs` is the caller's read high-water (the largest commit/snapshot ts\n * it has already observed). It gates replica routing: a query only runs on the\n * replica when the replica has applied at least `clientMinTs` — otherwise reads\n * (and the freshness scan) stay on the primary, preserving read-your-writes and\n * monotonic reads. It is clamped to `lastSnapshotTs` here, so a client can at\n * most force the primary (never a value beyond it). Returns the data plus the\n * `ts` the result reflects, which the caller echoes so the client can advance\n * its high-water.\n */\nexport async function runCachedQuery<TContext>(\n deps: OneShotQueryDeps<TContext>,\n /** Request-scoped `ctx.log`/`ctx.span`, threaded into the query handler's\n * context (the WS/HTTP call sites supply them). */\n handlerObs: HandlerObservability,\n procedure: RegisteredProcedure<TContext>,\n procedureName: string,\n input: unknown,\n serverCtx: TContext | undefined,\n lastSnapshotTs: bigint,\n clientMinTs?: bigint,\n): Promise<{ data: unknown; ts: bigint; readSet: ReadEntry[] }> {\n const queryIdentity = resolveQueryIdentity(procedure, serverCtx, input, deps.getUserId);\n const cacheKey = await generateCacheKey(procedureName, stableStringify(input), queryIdentity);\n\n // ── Read routing (chosen ONCE, then used for BOTH freshness and the row\n // reads) ──────────────────────────────────────────────────────────────────\n // The shared router (db/read-routing.ts) clamps the client high-water and\n // picks the replica only when it has applied at least that ts (read-your-writes\n // + monotonic reads), else the primary. A one-shot query DEFAULTS to that\n // choice — including the freshness scan, which runs on the SAME connection at\n // the SAME ts, so a cache HIT offloads the primary too, not just a miss.\n const routing = await resolveReadRouting(deps.db, lastSnapshotTs, clientMinTs ?? 0n);\n const readTs = routing.readTs;\n\n // Cache freshness (metadata fetch + between-ts commit-log scan) under a\n // `cache.freshness` span, so a trace shows this work even on a HIT — which\n // otherwise produces no DB/query span at all. Zero-cost when tracing is off.\n const { metadata, freshness } = await traceCacheCall(\"freshness\", async () => {\n const metadata = await deps.cache.getQueryCacheMetaData(cacheKey);\n const freshness = await evaluateCacheFreshness(routing.readImpl, metadata, readTs, deps.getMinRetainedTs());\n return { metadata, freshness };\n });\n\n if (freshness.fresh) {\n const cached = await traceCacheCall(\"get\", async (setHit) => {\n const c = await deps.cache.getQueryCacheFor(cacheKey);\n // A HIT is only a real hit when the data blob is present AND its version\n // matches the metadata freshness was validated against (see below).\n setHit(!!(c && metadata && c.version === metadata.version));\n return c;\n });\n // Metadata says fresh, but only trust the data blob when it is actually\n // present AND its version matches the metadata we validated — the data key\n // may have been evicted (shorter TTL) or, under a concurrent write, the two\n // keys may have diverged. Either way, fall through to recompute.\n if (cached && metadata && cached.version === metadata.version) {\n // Mirror the subscription worker's register path (sub-worker.ts): the\n // cache is still valid at a *newer* snapshot, so push the stored horizon\n // forward (preserving data version + readSet). Without this an untracked\n // one-shot re-validates from the same old snapshot on every call, so its\n // between-ts scan window grows without bound between recomputes. Advancing\n // it collapses the next scan to ≈ empty until the next write, and keeps the\n // horizon above the retention watermark. Fire-and-forget: monotonic CAS\n // that won't clobber a fresher recompute, so it adds no latency.\n if (readTs > metadata.lastSnapshotTs) {\n deps.cache\n .advanceQueryCacheMetadata(cacheKey, {\n lastSnapshotTs: readTs,\n version: metadata.version,\n readSet: metadata.readSet,\n })\n .catch((err) => logger.error(err, \"one-shot horizon advance failed\"));\n }\n // The served data reflects its own compute snapshot (metadata horizon) and\n // was just proven valid up to readTs — report the max so the client's\n // high-water never goes backward relative to the bytes it received.\n const servedTs = readTs > metadata.lastSnapshotTs ? readTs : metadata.lastSnapshotTs;\n return { data: cached.data, ts: servedTs, readSet: metadata.readSet };\n }\n }\n\n // Miss / stale → recompute. Reads default to the chosen connection (replica\n // when caught up, else primary); the handler can override before its first\n // read via ctx.usePrimaryConn()/useReplicaConn(). The cache entry is stamped\n // with the FINAL connection's snapshot ts (result.ts), so a later freshness\n // scan uses the correct lower bound.\n const result = await deps.db.queryInternalOn({ impl: routing.readImpl, beginTs: readTs }, async (ctx) => {\n const fn = procedure.fn as (c: QueryCtx<TContext>, input: unknown) => Promise<unknown>;\n // Wrap the recompute in a `query <name>` span (child of the operation span\n // via the active context — or the caller's explicit fallback span when the\n // ambient context is root). Only reached on a cache MISS, so a span here\n // marks an actual recompute. The span becomes the handler's `ctx.span`.\n return traceQueryExecution(procedureName, \"request\", parentContextFor(handlerObs), (querySpan) => {\n const qctx: QueryCtx<TContext> = {\n db: ctx,\n serverCtx,\n ...makeConnRouting(ctx, routing),\n log: handlerObs.log,\n span: querySpan,\n };\n return fn(qctx, input);\n });\n });\n\n // version == the compute ts, so the data blob and metadata are written with a\n // matching version and cross-check on read.\n await deps.cache\n .setQueryCacheAndMetadataFor(cacheKey, result.data, {\n lastSnapshotTs: result.ts,\n version: String(result.ts),\n readSet: result.readSet,\n })\n .catch((err) => logger.error(err, \"one-shot query cache write failed\"));\n\n return { data: result.data, ts: result.ts, readSet: result.readSet };\n}\n\n/**\n * Cache-first evaluation of a nested query JOINED to a parent transaction — the\n * path behind `caller.<proc>.runQuery` when invoked from a query or mutation\n * handler (`parent` is the parent's live {@link DbReader}/{@link DbWriter}).\n *\n * The nested query shares the parent's SNAPSHOT + readSet: it reads on the\n * parent's own bound connection (`parent.db`) at the parent's `beginTs`, so the\n * freshness scan and any replica/primary connection the parent was routed to\n * stay consistent — a rebind can never mix snapshots. Reads made on a cache\n * MISS accumulate straight into the parent's readSet (making the parent re-run /\n * OCC-conflict on concurrent changes to what the nested query observed), and on\n * a HIT the cached entry's readSet is appended to the parent instead, so the\n * parent's subscription invalidation / OCC fencing still covers the rows the\n * nested query depends on.\n *\n * Mutation parents need two guards that query parents don't:\n * 1. **Pending writes** (the mutation has already written in this tx) — the\n * cache is bypassed entirely: the nested query must see the parent's\n * uncommitted writes (reads-your-writes), which the committed cache can't\n * reflect, and caching that state would leak uncommitted data to other\n * readers.\n * 2. **No write-back** — a `DbWriter`'s `beginTs` is `latest + 1` (a phantom,\n * not a real committed ts), so a write-back would stamp a horizon no reader\n * can ever reach and (worse) invert the freshness scan window. Rather than\n * clamp it, mutation parents simply never write back: nothing rebuilds a\n * nested read after the parent commits (unlike a tracked subscription), and\n * the mutation's own commit usually invalidates the entry anyway. They only\n * CONSUME cache hits written by query parents / one-shots — the cached\n * entry's readSet is folded into the parent so OCC fencing still holds, and\n * the horizon is never advanced for the same phantom-ts reason.\n */\nexport async function runCachedQueryInParent<TContext>(\n deps: OneShotQueryDeps<TContext>,\n handlerObs: HandlerObservability,\n procedure: RegisteredProcedure<TContext>,\n procedureName: string,\n input: unknown,\n serverCtx: TContext | undefined,\n parent: DbReader | DbWriter,\n): Promise<unknown> {\n const isMutationParent = parent instanceof DbWriter;\n const hasPendingWrites = isMutationParent && parent.internalGetWriteSet().length > 0;\n\n // Bypass cache entirely while the parent mutation has uncommitted writes —\n // reads-your-writes wins, and we never cache uncommitted state (see above).\n const cache = hasPendingWrites ? undefined : deps.cache;\n const readImpl = parent.db;\n const readTs = parent.beginTs;\n\n const runOnParent = (): Promise<unknown> =>\n traceQueryExecution(procedureName, \"request\", parentContextFor(handlerObs), (querySpan) => {\n const qctx: QueryCtx<TContext> = {\n db: parent,\n serverCtx,\n ...parentConn,\n log: handlerObs.log,\n span: querySpan,\n };\n const fn = (procedure.fn as (c: QueryCtx<TContext>, i: unknown) => Promise<unknown>);\n return fn(qctx, input);\n });\n\n // Same identity segmentation as every other query transport, so a nested call\n // shares cache entries with the equivalent client one-shot / subscription.\n const cacheKey = cache\n ? await generateCacheKey(\n procedureName,\n stableStringify(input),\n resolveQueryIdentity(procedure, serverCtx, input, deps.getUserId),\n )\n : undefined;\n\n if (cache && cacheKey) {\n const { metadata, freshness } = await traceCacheCall(\"freshness\", async () => {\n const metadata = await cache.getQueryCacheMetaData(cacheKey);\n const freshness = await evaluateCacheFreshness(readImpl, metadata, readTs, deps.getMinRetainedTs());\n return { metadata, freshness };\n });\n\n if (freshness.fresh) {\n const cached = await traceCacheCall(\"get\", async (setHit) => {\n const c = await cache.getQueryCacheFor(cacheKey);\n setHit(!!(c && metadata && c.version === metadata.version));\n return c;\n });\n if (cached && metadata && cached.version === metadata.version) {\n // Fold the nested query's dependencies into the parent's readSet so the\n // parent re-runs / OCC-fences when any of them change, exactly as if it\n // had recomputed.\n parent.appendInternalReadSet(metadata.readSet);\n // Horizon advance only for a query parent: its beginTs is a real\n // committed snapshot. A mutation parent's beginTs is `latest + 1`, which\n // would invert the scan window for every later reader.\n if (!isMutationParent && readTs > metadata.lastSnapshotTs) {\n cache\n .advanceQueryCacheMetadata(cacheKey, {\n lastSnapshotTs: readTs,\n version: metadata.version,\n readSet: metadata.readSet,\n })\n .catch((err) => logger.error(err, \"nested query horizon advance failed\"));\n }\n return cached.data;\n }\n }\n }\n\n // Miss / stale / cache bypassed → run against the parent reader. The readSet\n // delta (this query's own reads) is what gets stamped into the cache; the\n // entries themselves already live in the parent's readSet. Only QUERY parents\n // write back: a mutation parent's beginTs is a phantom (`latest + 1`) that must\n // never become a horizon, so a mutation-parent miss recomputes on the parent\n // without caching (see the mutation-parent guard in the docs above).\n const beforeLen = parent.internalGetReadSet().length;\n const data = await runOnParent();\n\n if (!isMutationParent && cache && cacheKey) {\n // Query-parent beginTs is a real committed snapshot — the exact upper bound\n // of what the nested reads observed.\n const nestedReadSet = parent.internalGetReadSet().slice(beforeLen);\n await cache\n .setQueryCacheAndMetadataFor(cacheKey, data, {\n lastSnapshotTs: parent.beginTs,\n version: String(parent.beginTs),\n readSet: nestedReadSet,\n })\n .catch((err) => logger.error(err, \"nested query cache write failed\"));\n }\n\n return data;\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import { t as tableIdOf } from "./table-id-DyrJBKAQ.js";
|
|
2
|
+
import { a as utf8Decode, i as bytesEqual } from "./helper-zdJT5FUc.js";
|
|
3
|
+
import { a as jsonPathExtract, i as jsonPathExists, r as jsonContains } from "./query-LJranz0c.js";
|
|
4
|
+
import { C as normalizeIdToBytes } from "./types_db-OUou3o2Z.js";
|
|
5
|
+
//#region src/db/db.ts
|
|
6
|
+
/** Fixed key for the PG advisory lock that serializes core init across processes. */
|
|
7
|
+
const CORE_INIT_LOCK_KEY = 895124309;
|
|
8
|
+
/** Name for the MySQL `GET_LOCK` that serializes core init across processes. */
|
|
9
|
+
const CORE_INIT_LOCK_NAME = "supalive_core_init";
|
|
10
|
+
var LazyCommitTsParam = class LazyCommitTsParam {
|
|
11
|
+
value;
|
|
12
|
+
constructor(value) {
|
|
13
|
+
this.value = value;
|
|
14
|
+
}
|
|
15
|
+
toPostgres(prepareValue) {
|
|
16
|
+
return prepareValue(this.value);
|
|
17
|
+
}
|
|
18
|
+
toString() {
|
|
19
|
+
return this.value.toString();
|
|
20
|
+
}
|
|
21
|
+
validate() {
|
|
22
|
+
const p = new LazyCommitTsParam(void 0);
|
|
23
|
+
const hasNotObjConstructor = p.constructor !== Object;
|
|
24
|
+
const hasNotToJSON = typeof p.toJSON !== "function";
|
|
25
|
+
console.log("constructor must not object", hasNotObjConstructor);
|
|
26
|
+
console.log("should not have toJSON", hasNotToJSON);
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
//#endregion
|
|
30
|
+
//#region src/server/overlap-checker.ts
|
|
31
|
+
function mapReadEntryToRaw(entry) {
|
|
32
|
+
if (entry.kind === "point") return {
|
|
33
|
+
kind: "point",
|
|
34
|
+
tableId: tableIdOf(entry.table),
|
|
35
|
+
id: normalizeIdToBytes(entry.id),
|
|
36
|
+
table: entry.table
|
|
37
|
+
};
|
|
38
|
+
else return {
|
|
39
|
+
kind: "range",
|
|
40
|
+
tableId: tableIdOf(entry.table),
|
|
41
|
+
table: entry.table,
|
|
42
|
+
predicate: entry.predicate,
|
|
43
|
+
columns: entry.columns,
|
|
44
|
+
offset: entry.offset,
|
|
45
|
+
orderBy: entry.orderBy,
|
|
46
|
+
limit: entry.limit
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
function mapReadSetToRaw(readSet) {
|
|
50
|
+
return readSet.map(mapReadEntryToRaw);
|
|
51
|
+
}
|
|
52
|
+
function mapWriteEntryToRaw(entry) {
|
|
53
|
+
return {
|
|
54
|
+
table: entry.table,
|
|
55
|
+
tableId: entry.tableId,
|
|
56
|
+
id: normalizeIdToBytes(entry.id),
|
|
57
|
+
op: entry.op,
|
|
58
|
+
data: entry.data,
|
|
59
|
+
prevData: entry.prevData
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
function mapWriteSetToRaw(writeSet) {
|
|
63
|
+
return writeSet.map(mapWriteEntryToRaw);
|
|
64
|
+
}
|
|
65
|
+
function evaluatePredicateAgainstRow(rowData, predicate) {
|
|
66
|
+
switch (predicate.kind) {
|
|
67
|
+
case "and": {
|
|
68
|
+
let hasUnknown = false;
|
|
69
|
+
for (const child of predicate.children) {
|
|
70
|
+
const r = evaluatePredicateAgainstRow(rowData, child);
|
|
71
|
+
if (r === "no") return "no";
|
|
72
|
+
if (r === "unknown") hasUnknown = true;
|
|
73
|
+
}
|
|
74
|
+
return hasUnknown ? "unknown" : "yes";
|
|
75
|
+
}
|
|
76
|
+
case "or": {
|
|
77
|
+
let hasUnknown = false;
|
|
78
|
+
for (const child of predicate.children) {
|
|
79
|
+
const r = evaluatePredicateAgainstRow(rowData, child);
|
|
80
|
+
if (r === "yes") return "yes";
|
|
81
|
+
if (r === "unknown") hasUnknown = true;
|
|
82
|
+
}
|
|
83
|
+
return hasUnknown ? "unknown" : "no";
|
|
84
|
+
}
|
|
85
|
+
case "leaf": {
|
|
86
|
+
if (!(predicate.column in rowData)) return "unknown";
|
|
87
|
+
const val = rowData[predicate.column];
|
|
88
|
+
switch (predicate.operator) {
|
|
89
|
+
case "=": return val === predicate.value ? "yes" : "no";
|
|
90
|
+
case "!=": return val !== predicate.value ? "yes" : "no";
|
|
91
|
+
case ">": return val > predicate.value ? "yes" : "no";
|
|
92
|
+
case ">=": return val >= predicate.value ? "yes" : "no";
|
|
93
|
+
case "<": return val < predicate.value ? "yes" : "no";
|
|
94
|
+
case "<=": return val <= predicate.value ? "yes" : "no";
|
|
95
|
+
case "between": return val >= predicate.value && val <= predicate.value2 ? "yes" : "no";
|
|
96
|
+
case "in": return Array.isArray(predicate.value) && predicate.value.includes(val) ? "yes" : "no";
|
|
97
|
+
case "like":
|
|
98
|
+
case "contains":
|
|
99
|
+
case "ilike":
|
|
100
|
+
case "icontains": {
|
|
101
|
+
if (typeof val !== "string" || typeof predicate.value !== "string") return "no";
|
|
102
|
+
const haystack = predicate.operator === "ilike" || predicate.operator === "icontains" ? val.toLowerCase() : val;
|
|
103
|
+
const needle = predicate.operator === "ilike" || predicate.operator === "icontains" ? predicate.value.toLowerCase() : predicate.value;
|
|
104
|
+
const pattern = predicate.operator === "contains" || predicate.operator === "icontains" ? `%${needle}%` : needle;
|
|
105
|
+
return new RegExp("^" + pattern.replace(/%/g, ".*").replace(/_/g, ".") + "$").test(haystack) ? "yes" : "no";
|
|
106
|
+
}
|
|
107
|
+
case "jsonContains": {
|
|
108
|
+
const candidate = decodeJsonLeafValue(predicate);
|
|
109
|
+
const target = predicate.path !== void 0 ? jsonPathExtract(val, predicate.path) : val;
|
|
110
|
+
if (target === void 0) return "no";
|
|
111
|
+
return jsonContains(target, candidate) ? "yes" : "no";
|
|
112
|
+
}
|
|
113
|
+
case "jsonContainedBy": {
|
|
114
|
+
const candidate = decodeJsonLeafValue(predicate);
|
|
115
|
+
const target = predicate.path !== void 0 ? jsonPathExtract(val, predicate.path) : val;
|
|
116
|
+
if (target === void 0) return "no";
|
|
117
|
+
return jsonContains(candidate, target) ? "yes" : "no";
|
|
118
|
+
}
|
|
119
|
+
case "jsonHasKey": {
|
|
120
|
+
if (predicate.mode && Array.isArray(predicate.value)) {
|
|
121
|
+
const paths = predicate.value;
|
|
122
|
+
if (paths.length === 0) return "no";
|
|
123
|
+
return (predicate.mode === "all" ? paths.every((p) => jsonPathExists(val, p)) : paths.some((p) => jsonPathExists(val, p))) ? "yes" : "no";
|
|
124
|
+
}
|
|
125
|
+
if (predicate.raw) return jsonPathExists(val, predicate.value) ? "yes" : "no";
|
|
126
|
+
const key = predicate.value;
|
|
127
|
+
if (val === null || typeof val !== "object") return "no";
|
|
128
|
+
if (Array.isArray(val)) return val.includes(key) ? "yes" : "no";
|
|
129
|
+
return key in val ? "yes" : "no";
|
|
130
|
+
}
|
|
131
|
+
case "jsonArrayContains": {
|
|
132
|
+
const candidate = decodeJsonLeafValue(predicate);
|
|
133
|
+
const target = predicate.path !== void 0 ? jsonPathExtract(val, predicate.path) : val;
|
|
134
|
+
if (!Array.isArray(target)) return "no";
|
|
135
|
+
return target.some((el) => jsonContains(el, candidate)) ? "yes" : "no";
|
|
136
|
+
}
|
|
137
|
+
case "isNull": return val === null ? "yes" : "no";
|
|
138
|
+
case "isNotNull": return val !== null ? "yes" : "no";
|
|
139
|
+
default: return "unknown";
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
function decodeJsonLeafValue(leaf) {
|
|
145
|
+
if (!leaf.raw) return leaf.value;
|
|
146
|
+
if (typeof leaf.value !== "string") return leaf.value;
|
|
147
|
+
try {
|
|
148
|
+
return JSON.parse(leaf.value);
|
|
149
|
+
} catch {
|
|
150
|
+
return leaf.value;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
function parseLogData(log) {
|
|
154
|
+
if (log._parsedData !== void 0) return log._parsedData;
|
|
155
|
+
const data = log.data.length > 0 ? JSON.parse(utf8Decode(log.data)) : null;
|
|
156
|
+
log._parsedData = data;
|
|
157
|
+
return data;
|
|
158
|
+
}
|
|
159
|
+
function checkLogsAffectReadSet(logs, rawReadSet) {
|
|
160
|
+
for (const log of logs) for (const rawRead of rawReadSet) {
|
|
161
|
+
if (!bytesEqual(log.tableId, rawRead.tableId)) continue;
|
|
162
|
+
if (rawRead.kind === "point" ? checkPointReadOverlap(log, rawRead) : checkRangeReadOverlap(log, rawRead)) return true;
|
|
163
|
+
}
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Stateless freshness check shared by the subscription worker's `register` and
|
|
168
|
+
* the main server's one-shot query path. Decides whether a query's cached
|
|
169
|
+
* result (described by `metadata`) is still valid at `currentTs`.
|
|
170
|
+
*
|
|
171
|
+
* It is a pure function of Redis (`metadata`, `minRetainedTs`) + the DB
|
|
172
|
+
* commit-log probes (`hasCommitForPoints` for point-only tables,
|
|
173
|
+
* `getCommitLogsBetweenTs` for range tables) + the read set — it holds no
|
|
174
|
+
* worker-local state, so it is safe to run outside the sub-manager.
|
|
175
|
+
*
|
|
176
|
+
* `minRetainedTs` is the retention watermark: the oldest commit-log ts still
|
|
177
|
+
* retained after prune.
|
|
178
|
+
*
|
|
179
|
+
* Two ways a cache entry is deemed stale:
|
|
180
|
+
* 1. **Retention guard** — `metadata.lastSnapshotTs < minRetainedTs` (or the
|
|
181
|
+
* watermark is unknown). The between-ts scan would start below the oldest
|
|
182
|
+
* retained commit log, so it could silently miss pruned commits → we cannot
|
|
183
|
+
* prove freshness and must recompute. This is the case that a one-shot
|
|
184
|
+
* (untracked by any subscription, so nothing holds the retention watermark
|
|
185
|
+
* down) can hit.
|
|
186
|
+
* 2. **Overlap** — a commit in `(lastSnapshotTs, currentTs]` touched the read
|
|
187
|
+
* set.
|
|
188
|
+
*/
|
|
189
|
+
async function evaluateCacheFreshness(db, metadata, currentTs, minRetainedTs) {
|
|
190
|
+
if (!metadata) return {
|
|
191
|
+
fresh: false,
|
|
192
|
+
readSet: [],
|
|
193
|
+
rawReadSet: []
|
|
194
|
+
};
|
|
195
|
+
const readSet = metadata.readSet;
|
|
196
|
+
const rawReadSet = mapReadSetToRaw(readSet);
|
|
197
|
+
if (minRetainedTs === null || metadata.lastSnapshotTs < minRetainedTs) return {
|
|
198
|
+
fresh: false,
|
|
199
|
+
readSet,
|
|
200
|
+
rawReadSet
|
|
201
|
+
};
|
|
202
|
+
if (rawReadSet.length === 0) return {
|
|
203
|
+
fresh: true,
|
|
204
|
+
readSet,
|
|
205
|
+
rawReadSet
|
|
206
|
+
};
|
|
207
|
+
const beginTs = metadata.lastSnapshotTs + 1n;
|
|
208
|
+
const endTs = currentTs + 1n;
|
|
209
|
+
const rangeTables = /* @__PURE__ */ new Map();
|
|
210
|
+
for (const r of rawReadSet) if (r.kind === "range") rangeTables.set(r.table, r.tableId);
|
|
211
|
+
const pointProbes = [];
|
|
212
|
+
for (const r of rawReadSet) if (r.kind === "point" && !rangeTables.has(r.table)) pointProbes.push({
|
|
213
|
+
tableId: r.tableId,
|
|
214
|
+
id: r.id
|
|
215
|
+
});
|
|
216
|
+
if (pointProbes.length > 0) {
|
|
217
|
+
if (await db.hasCommitForPoints(beginTs, endTs, pointProbes)) return {
|
|
218
|
+
fresh: false,
|
|
219
|
+
readSet,
|
|
220
|
+
rawReadSet
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
if (rangeTables.size > 0) {
|
|
224
|
+
if (checkLogsAffectReadSet(await db.getCommitLogsBetweenTs(beginTs, endTs, [...rangeTables.values()]), rawReadSet)) return {
|
|
225
|
+
fresh: false,
|
|
226
|
+
readSet,
|
|
227
|
+
rawReadSet
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
return {
|
|
231
|
+
fresh: true,
|
|
232
|
+
readSet,
|
|
233
|
+
rawReadSet
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
function checkPointReadOverlap(log, rawRead) {
|
|
237
|
+
return bytesEqual(log.id, rawRead.id);
|
|
238
|
+
}
|
|
239
|
+
function checkRangeReadOverlap(log, rawRead) {
|
|
240
|
+
const logData = parseLogData(log);
|
|
241
|
+
if (!logData) return true;
|
|
242
|
+
const op = logData.op ?? "update";
|
|
243
|
+
const data = logData.data;
|
|
244
|
+
const prevData = logData.prevData;
|
|
245
|
+
return checkRowWriteAffectsRange(op, prevData, data, rawRead.predicate);
|
|
246
|
+
}
|
|
247
|
+
function checkWriteSetAffectsReadSet(writeSet, rawReadSet) {
|
|
248
|
+
for (const write of writeSet) for (const rawRead of rawReadSet) {
|
|
249
|
+
if (!bytesEqual(write.tableId, rawRead.tableId)) continue;
|
|
250
|
+
if (checkWriteAffectsRawReadEntry(write, rawRead)) return true;
|
|
251
|
+
}
|
|
252
|
+
return false;
|
|
253
|
+
}
|
|
254
|
+
function checkWriteAffectsRawReadEntry(write, rawRead) {
|
|
255
|
+
if (rawRead.kind === "point") return bytesEqual(write.id, rawRead.id);
|
|
256
|
+
return checkRowWriteAffectsRange(write.op, write.prevData, write.data, rawRead.predicate);
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Evaluate whether a write affects a range subscription, given the row's
|
|
260
|
+
* prior and post state. The write is considered affecting if EITHER side
|
|
261
|
+
* matched the predicate — that catches:
|
|
262
|
+
* - rows entering the result set (prev "no", post "yes")
|
|
263
|
+
* - rows leaving the result set (prev "yes", post "no")
|
|
264
|
+
* - rows changing while still in the set (prev "yes", post "yes")
|
|
265
|
+
* Skipped only if both sides are a definite "no".
|
|
266
|
+
*
|
|
267
|
+
* When prevData is missing (e.g. inserts, or commit logs predating this
|
|
268
|
+
* change), we fall back to conservative "unknown" on that side.
|
|
269
|
+
*/
|
|
270
|
+
function checkRowWriteAffectsRange(op, prevData, data, predicate) {
|
|
271
|
+
const prevVerdict = prevData ? evaluatePredicateAgainstRow(prevData, predicate) : op === "insert" ? "no" : "unknown";
|
|
272
|
+
let postVerdict;
|
|
273
|
+
if (op === "delete") postVerdict = "no";
|
|
274
|
+
else if (data) postVerdict = evaluatePredicateAgainstRow(prevData ? {
|
|
275
|
+
...prevData,
|
|
276
|
+
...data
|
|
277
|
+
} : data, predicate);
|
|
278
|
+
else postVerdict = "unknown";
|
|
279
|
+
return prevVerdict !== "no" || postVerdict !== "no";
|
|
280
|
+
}
|
|
281
|
+
//#endregion
|
|
282
|
+
export { mapWriteSetToRaw as a, LazyCommitTsParam as c, mapReadSetToRaw as i, checkWriteSetAffectsReadSet as n, CORE_INIT_LOCK_KEY as o, evaluateCacheFreshness as r, CORE_INIT_LOCK_NAME as s, checkLogsAffectReadSet as t };
|
|
283
|
+
|
|
284
|
+
//# sourceMappingURL=overlap-checker-CCgq_Tpa.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"overlap-checker-CCgq_Tpa.js","names":[],"sources":["../src/db/db.ts","../src/server/overlap-checker.ts"],"sourcesContent":["import { CacheLayer } from \"./cache\";\nimport type { CommitLogEntry, PointRead, RawPointRead, RawReadEntry } from \"./types_db\";\nimport { ReadEntry, WriteEntry } from \"./types_db\";\n\nexport interface DbQueryResult<T = any> {\n rows: T[];\n rowCount?: number | null;\n /**\n * Auto-increment / LAST_INSERT_ID value from the driver, when applicable\n * (MySQL `ResultSetHeader.insertId`). Used to read the advanced value back\n * from `UPDATE ... SET id = LAST_INSERT_ID(id + 1)` without a second\n * round-trip. Undefined for Postgres / SELECTs.\n */\n insertId?: number | bigint;\n}\n\nexport interface RawClient {\n query<T = any>(sql: string, params?: unknown[]): Promise<DbQueryResult<T>>;\n}\n\nexport interface PooledClient extends RawClient {\n release(): void;\n}\n\nexport type DbType = \"postgres\" | \"mysql\";\n\n/** Fixed key for the PG advisory lock that serializes core init across processes. */\nexport const CORE_INIT_LOCK_KEY = 895124309;\n/** Name for the MySQL `GET_LOCK` that serializes core init across processes. */\nexport const CORE_INIT_LOCK_NAME = \"supalive_core_init\";\n\nexport interface PendingPointEntry {\n entry: PointRead;\n /* Table name and point read id = key = `${tableName} ${entry.id}` */\n key: string;\n}\n\nexport interface PreparedQuery {\n sql: string;\n params: unknown[] | unknown[][];\n operation: \"insertSingle\" | \"insertBatch\" | \"updateSingle\" | \"updateBatch\" | \"deleteSingle\" | \"deleteBatch\" | \"pointReadValidation\" | \"commit_logs\";\n table: string;\n entryId?: unknown;\n expectedTs?: bigint,\n entries?: WriteEntry[],\n pendingsEntry?: PendingPointEntry[];\n};\n\nexport interface PreparedQueries {\n commits: PreparedQuery[];\n validateReadPoints?: PreparedQuery[];\n hasRangeRead: boolean;\n tableIds?: Uint8Array[];\n rawReadSet?: RawReadEntry[],\n}\n\n/**\n * Bound to a single dedicated connection that holds the core-init lock. Passed\n * to the callback of {@link Database.withCoreInitLock} so version reads/writes\n * and migration statements all run under the same lock.\n */\nexport interface CoreInitLockCtx {\n exec(sql: string, params?: unknown[]): Promise<DbQueryResult>;\n getVersion(): Promise<bigint>;\n setVersion(version: bigint): Promise<void>;\n}\n\nexport interface Database {\n readonly dbType: DbType;\n readonly sqlBuilder: SqlBuilder;\n columnTypes: Map<string, Map<string, string>>;\n /**\n * Current core schema version stored in `metadata.core_version`, or `0n`\n * when unset. Read on a pooled connection for the lock-free fast path of\n * {@link initCore}.\n */\n getCoreVersion(): Promise<bigint>;\n /**\n * The app's current schema-migration marker, stored in\n * `metadata.app_migration`, or `0n` when unset. This is the 14-digit\n * timestamp of the last applied migration file (e.g. `20260726112333` for\n * `0001_20260726112333.sql`), written by the deploy pipeline's tracked\n * migrate step. Used to key the bootstrap column-type cache\n * (`supalive:schema:types:<app_migration>`) so a schema change automatically\n * invalidates cold instances' cached types — see `bootstrapDbTypes`.\n */\n getAppMigration(): Promise<bigint>;\n /**\n * Runtime bootstrap statements to apply when migrating core TO `toVersion`\n * (i.e. the `toVersion - 1` → `toVersion` step): data seeds, backfills, or a\n * schema change that must be applied directly at runtime. Dialect-specific;\n * the core schema (tables, indexes, the `global_commit_ts` sequence/counter)\n * lives in the generated schema instead — see `coreTablesSql`.\n */\n coreMigrationStatements(toVersion: bigint): string[];\n /**\n * Run `fn` while holding the cross-process core-init lock (PG\n * `pg_advisory_xact_lock`, MySQL `GET_LOCK`) on a single dedicated\n * connection, so concurrent servers can't race the core migration. All of\n * `fn`'s work runs on that locked connection via {@link CoreInitLockCtx}.\n */\n withCoreInitLock(fn: (ctx: CoreInitLockCtx) => Promise<void>): Promise<void>;\n bootstrapColumnTypes(opts: { redis?: CacheLayer; cacheKey: string; tables?: string[]; }): Promise<void>;\n query<T = any>(sql: string, params?: unknown[]): Promise<DbQueryResult<T>>;\n getClient(): Promise<PooledClient>;\n getTransactionClient(): Promise<TxDatabase>;\n getLatestSnapshotTimestamp(): Promise<bigint>;\n updateLatestSnapshotTimestamp(commitTs: bigint): Promise<void>;\n /**\n * The retention watermark: the boundary `ts` below which commit_logs have\n * been pruned, so any cache entry whose `lastSnapshotTs` is below it can no\n * longer be freshness-checked (its scan window would miss deleted rows) and\n * must be recomputed. Persisted in the `metadata` table (survives restarts)\n * as a cheap PK read — callers load it into an in-memory variable at startup\n * and keep it fresh via pub/sub rather than reading it per query. Returns the\n * -1n genesis when nothing has been pruned yet (matching the empty-DB snapshot\n * ts), so every entry is scannable.\n */\n getMinSnapshotTimestamp(): Promise<bigint>;\n /**\n * Advance the retention watermark. Monotonic: only ever moves forward (a\n * stale writer can never lower it). Called by the sub-manager right before it\n * prunes commit_logs below `ts`.\n */\n updateMinSnapshotTimestamp(ts: bigint): Promise<void>;\n getNextTimestamp(): Promise<bigint>;\n prepareCommitWrites(readSet: ReadEntry[], writeSet: WriteEntry[], commitTs: LazyCommitTsParam): Promise<PreparedQueries>;\n commitWrites(beginTs: bigint, queries: PreparedQueries, commitTs: bigint): Promise<{ success: boolean }>;\n close(): Promise<void>;\n /**\n * Delete commit_logs older than `ts` (exclusive). Used by the periodic\n * retention sweep in SubscriptionManager so the table doesn't grow\n * unbounded.\n */\n pruneCommitLogsBefore(ts: bigint): Promise<bigint>;\n /**\n * Oldest `ts` still present in commit_logs (i.e. the retention watermark\n * after the last prune), or `null` when the table is empty. A cache entry\n * whose `lastSnapshotTs` is below it can no longer be freshness-checked via\n * {@link getCommitLogsBetweenTs} (the logs it would need were pruned), so the\n * one-shot/register cache paths recompute instead of trusting it.\n */\n getMinCommitLogTs(): Promise<bigint | null>;\n /**\n * Half-open range scan: ts >= beginTs AND ts < endTs. Used by the\n * subscription-recovery path to replay logs in a specific time window.\n */\n getCommitLogsBetweenTs(beginTs: bigint, endTs: bigint, tableIds: Uint8Array[]): Promise<CommitLogEntry[]>;\n /**\n * Existence-only freshness probe for point reads: does any commit in the\n * half-open window `[beginTs, endTs)` touch one of the exact `(tableId, id)`\n * pairs in `points`? Returns as soon as one match is found.\n *\n * Point-read overlap is a pure key match (`checkPointReadOverlap`), so it can\n * be answered entirely in SQL with a `(table_id, id)` tuple `IN` against the\n * `commit_logs_by_table_and_id (table_id, id, ts)` index — no row `data`\n * blobs fetched, no JSON parsed. This is the precise-filtering path used by\n * {@link CacheFreshness} for tables whose read set is point-reads-only; tables\n * that also carry a range read still go through {@link getCommitLogsBetweenTs}\n * because their predicates must be evaluated in-app. `points` empty → `false`.\n */\n hasCommitForPoints(\n beginTs: bigint,\n endTs: bigint,\n points: { tableId: Uint8Array; id: Uint8Array }[],\n ): Promise<boolean>;\n /**\n * Conflict-check scan for OCC: ts >= sinceTs AND ts <> excludeTs. The\n * exclude is our own commitTs so we don't conflict with ourselves; the\n * lack of an upper bound is intentional, so a concurrent peer that\n * happens to have a higher commitTs but committed before us is still\n * visible. (MVCC keeps in-flight peers invisible until they commit.)\n */\n getCommitLogsSinceTs(sinceTs: bigint, excludeTs: bigint, tableIds: Uint8Array[]): Promise<CommitLogEntry[]>;\n /**\n * For each `update`/`delete` entry, attach `prevData` (consumed by the\n * invalidator) and `expectedCommitTs` (the CAS fence).\n *\n * `expectedCommitTs` is preferentially copied from a prior point-read of\n * the same row in `readSet` — that's the value the handler's logic\n * actually depended on, so the CAS check enforces beginTs-level isolation\n * for read-then-update patterns. If no prior read exists (\"blind\"\n * write), we fall back to the value observed in this batch fetch (weak\n * OCC, sufficient for writes that don't depend on prior state).\n *\n * `prevData` always comes from this batch fetch (SELECT * per table) so\n * the invalidator has the full row regardless of which columns the\n * handler's read selected.\n *\n * If the target row no longer exists at fetch time, `expectedCommitTs`\n * is left undefined and the per-row CAS will match nothing — surfaced\n * as OccConflictError.\n */\n attachPrevDataToWriteSet(writeSet: WriteEntry[], readSet: ReadEntry[]): Promise<void>;\n}\n\nexport interface TxDatabase extends Database {\n begin(): Promise<void>;\n commit(): Promise<void>;\n rollback(): Promise<void>;\n release(): void;\n}\n\nexport interface JsonOpOptions {\n /**\n * When true, the bound `value` parameter is treated as already prepared\n * for the underlying SQL (e.g. a literal JSON path string for\n * jsonHasKey, a JSON-encoded literal for the contains family). When\n * false/omitted, the value was prepared client-side from a JS value.\n */\n raw?: boolean;\n /**\n * 1-based positional index of an extra bound parameter holding a\n * jsonpath that scopes the contains-family check to a sub-document of\n * the target column. When omitted, the check runs against the whole\n * column value.\n */\n pathIndex?: number;\n /**\n * jsonHasKey only: when set, multiple path parameters are bound starting\n * at `paramIndex`, and the predicate evaluates whether any (`one`) or\n * every (`all`) path exists in the column.\n */\n mode?: \"one\" | \"all\";\n /** Number of bound path parameters in multi-path mode. */\n pathCount?: number;\n}\n\nexport interface SqlBuilder {\n placeholder(index: number): string;\n like(col: string, paramIndex: number): string;\n ilike(col: string, paramIndex: number): string;\n icontains(col: string, paramIndex: number): string;\n /** PG @> ; MySQL JSON_CONTAINS */\n jsonContains(col: string, paramIndex: number, opts?: JsonOpOptions): string;\n /** PG <@ ; MySQL JSON_CONTAINS(candidate, target) */\n jsonContainedBy(col: string, paramIndex: number, opts?: JsonOpOptions): string;\n /**\n * Key/path existence. With `opts.raw === true`, the bound param is a JSON\n * path expression (PG `jsonb_path_exists`, MySQL `JSON_CONTAINS_PATH`).\n * Otherwise the bound param is a literal top-level key (PG `?`, MySQL\n * `JSON_CONTAINS_PATH($.\"<key>\")` with server-side quoting).\n * With `opts.pathCount` set, emits the multi-path form keyed by `mode`.\n */\n jsonHasKey(col: string, paramIndex: number, opts?: JsonOpOptions): string;\n /** Array membership: target array contains the given element. */\n jsonArrayContains(col: string, paramIndex: number, opts?: JsonOpOptions): string;\n}\n\nexport class LazyCommitTsParam {\n constructor(public value: unknown) { }\n\n // used by postgres\n toPostgres(prepareValue: (value: any) => any): any {\n return prepareValue(this.value);\n }\n\n // used by mysql\n toString() {\n return (this.value as any).toString();\n }\n\n validate() {\n const p = new LazyCommitTsParam(undefined);\n const hasNotObjConstructor = p.constructor !== Object;\n const hasNotToJSON = typeof (p as any).toJSON !== 'function';\n console.log('constructor must not object', hasNotObjConstructor);\n console.log('should not have toJSON', hasNotToJSON);\n }\n}","import type {\n ReadEntry, CommitLogEntry,\n Predicate, WriteEntry, WriteOp,\n RawReadEntry, RawPointRead, RawRangeRead, LeafPredicate,\n QueryCacheMetadata,\n} from \"../db/types_db\";\nimport { normalizeIdToBytes } from \"../db/types_db\";\nimport { bytesEqual, utf8Decode } from \"../db/bytes\";\nimport { tableIdOf } from \"../db/table-id\";\nimport { jsonContains, jsonPathExists, jsonPathExtract } from \"../db/query\";\nimport type { Database } from \"../db/db\";\n\nexport interface RawWriteEntry {\n table: string;\n tableId: Uint8Array;\n id: Uint8Array;\n op: WriteOp;\n data?: Record<string, unknown>;\n prevData?: Record<string, unknown>;\n}\n\nexport function mapReadEntryToRaw(entry: ReadEntry): RawReadEntry {\n // tableId is not carried on a stored/wire ReadEntry — it is `utf8(table)` and\n // is reconstructed here (memoized, interned) from the table name. This is the\n // single producer of RawReadEntry, so every consumer (freshness partition,\n // checkLogsAffectReadSet, the invalidator, the commit-time OCC scan) gets the\n // derived bytes regardless of the source.\n if (entry.kind === \"point\") {\n return {\n kind: \"point\",\n tableId: tableIdOf(entry.table),\n id: normalizeIdToBytes(entry.id),\n table: entry.table,\n };\n } else {\n return {\n kind: \"range\",\n tableId: tableIdOf(entry.table),\n table: entry.table,\n predicate: entry.predicate,\n columns: entry.columns,\n offset: entry.offset,\n orderBy: entry.orderBy,\n limit: entry.limit,\n };\n }\n}\n\nexport function mapReadSetToRaw(readSet: ReadEntry[]): RawReadEntry[] {\n return readSet.map(mapReadEntryToRaw);\n}\n\nexport function mapWriteEntryToRaw(entry: WriteEntry): RawWriteEntry {\n return {\n table: entry.table,\n tableId: entry.tableId,\n id: normalizeIdToBytes(entry.id),\n op: entry.op,\n data: entry.data,\n prevData: entry.prevData,\n };\n}\n\nexport function mapWriteSetToRaw(writeSet: WriteEntry[]): RawWriteEntry[] {\n return writeSet.map(mapWriteEntryToRaw);\n}\n\nexport type OverlapVerdict = \"yes\" | \"no\" | \"unknown\";\n\nexport function evaluatePredicateAgainstRow(\n rowData: Record<string, unknown>,\n predicate: Predicate\n): OverlapVerdict {\n switch (predicate.kind) {\n case \"and\": {\n let hasUnknown = false;\n for (const child of predicate.children) {\n const r = evaluatePredicateAgainstRow(rowData, child);\n if (r === \"no\") return \"no\";\n if (r === \"unknown\") hasUnknown = true;\n }\n return hasUnknown ? \"unknown\" : \"yes\";\n }\n case \"or\": {\n let hasUnknown = false;\n for (const child of predicate.children) {\n const r = evaluatePredicateAgainstRow(rowData, child);\n if (r === \"yes\") return \"yes\";\n if (r === \"unknown\") hasUnknown = true;\n }\n return hasUnknown ? \"unknown\" : \"no\";\n }\n case \"leaf\": {\n if (!(predicate.column in rowData)) return \"unknown\";\n const val = rowData[predicate.column];\n switch (predicate.operator) {\n case \"=\": return val === predicate.value ? \"yes\" : \"no\";\n case \"!=\": return val !== predicate.value ? \"yes\" : \"no\";\n case \">\": return (val as number) > (predicate.value as number) ? \"yes\" : \"no\";\n case \">=\": return (val as number) >= (predicate.value as number) ? \"yes\" : \"no\";\n case \"<\": return (val as number) < (predicate.value as number) ? \"yes\" : \"no\";\n case \"<=\": return (val as number) <= (predicate.value as number) ? \"yes\" : \"no\";\n case \"between\":\n return (val as number) >= (predicate.value as number) &&\n (val as number) <= (predicate.value2 as number) ? \"yes\" : \"no\";\n case \"in\":\n return Array.isArray(predicate.value) && (predicate.value as unknown[]).includes(val) ? \"yes\" : \"no\";\n case \"like\":\n case \"contains\":\n case \"ilike\":\n case \"icontains\": {\n if (typeof val !== \"string\" || typeof predicate.value !== \"string\") return \"no\";\n const haystack = (predicate.operator === \"ilike\" || predicate.operator === \"icontains\")\n ? val.toLowerCase() : val;\n const needle = (predicate.operator === \"ilike\" || predicate.operator === \"icontains\")\n ? (predicate.value as string).toLowerCase() : predicate.value as string;\n const pattern = (predicate.operator === \"contains\" || predicate.operator === \"icontains\")\n ? `%${needle}%` : needle;\n const regex = new RegExp(\"^\" + pattern.replace(/%/g, \".*\").replace(/_/g, \".\") + \"$\");\n return regex.test(haystack) ? \"yes\" : \"no\";\n }\n case \"jsonContains\": {\n const candidate = decodeJsonLeafValue(predicate);\n const target = predicate.path !== undefined ? jsonPathExtract(val, predicate.path) : val;\n if (target === undefined) return \"no\";\n return jsonContains(target, candidate) ? \"yes\" : \"no\";\n }\n case \"jsonContainedBy\": {\n const candidate = decodeJsonLeafValue(predicate);\n const target = predicate.path !== undefined ? jsonPathExtract(val, predicate.path) : val;\n if (target === undefined) return \"no\";\n return jsonContains(candidate, target) ? \"yes\" : \"no\";\n }\n case \"jsonHasKey\": {\n if (predicate.mode && Array.isArray(predicate.value)) {\n const paths = predicate.value as string[];\n if (paths.length === 0) return \"no\";\n const verdict = predicate.mode === \"all\"\n ? paths.every(p => jsonPathExists(val, p))\n : paths.some(p => jsonPathExists(val, p));\n return verdict ? \"yes\" : \"no\";\n }\n if (predicate.raw) {\n return jsonPathExists(val, predicate.value as string) ? \"yes\" : \"no\";\n }\n const key = predicate.value as string;\n if (val === null || typeof val !== \"object\") return \"no\";\n if (Array.isArray(val)) return (val as unknown[]).includes(key) ? \"yes\" : \"no\";\n return key in (val as object) ? \"yes\" : \"no\";\n }\n case \"jsonArrayContains\": {\n const candidate = decodeJsonLeafValue(predicate);\n const target = predicate.path !== undefined ? jsonPathExtract(val, predicate.path) : val;\n if (!Array.isArray(target)) return \"no\";\n return (target as unknown[]).some(el => jsonContains(el, candidate)) ? \"yes\" : \"no\";\n }\n case \"isNull\": return val === null ? \"yes\" : \"no\";\n case \"isNotNull\": return val !== null ? \"yes\" : \"no\";\n default: return \"unknown\";\n }\n }\n }\n}\n\nfunction decodeJsonLeafValue(leaf: LeafPredicate): unknown {\n if (!leaf.raw) return leaf.value;\n if (typeof leaf.value !== \"string\") return leaf.value;\n try { return JSON.parse(leaf.value); } catch { return leaf.value; }\n}\n\nfunction parseLogData(log: CommitLogEntry): Record<string, unknown> | null {\n if (log._parsedData !== undefined) {\n return log._parsedData;\n }\n const data = log.data.length > 0\n ? JSON.parse(utf8Decode(log.data)) as Record<string, unknown>\n : null;\n log._parsedData = data;\n return data;\n}\n\n\nexport function checkLogsAffectReadSet(\n logs: CommitLogEntry[],\n rawReadSet: RawReadEntry[]\n): boolean {\n for (const log of logs) {\n for (const rawRead of rawReadSet) {\n if (!bytesEqual(log.tableId, rawRead.tableId)) continue;\n\n const res = rawRead.kind === \"point\" ?\n checkPointReadOverlap(log, rawRead) :\n checkRangeReadOverlap(log, rawRead);\n\n if (res) {\n return true;\n }\n }\n }\n return false;\n}\n\nexport interface CacheFreshness {\n /** true → the cached data for this key is still valid as of `currentTs`. */\n fresh: boolean;\n /** The cached read set (empty when there is no usable metadata). */\n readSet: ReadEntry[];\n rawReadSet: RawReadEntry[];\n}\n\n/**\n * Stateless freshness check shared by the subscription worker's `register` and\n * the main server's one-shot query path. Decides whether a query's cached\n * result (described by `metadata`) is still valid at `currentTs`.\n *\n * It is a pure function of Redis (`metadata`, `minRetainedTs`) + the DB\n * commit-log probes (`hasCommitForPoints` for point-only tables,\n * `getCommitLogsBetweenTs` for range tables) + the read set — it holds no\n * worker-local state, so it is safe to run outside the sub-manager.\n *\n * `minRetainedTs` is the retention watermark: the oldest commit-log ts still\n * retained after prune.\n *\n * Two ways a cache entry is deemed stale:\n * 1. **Retention guard** — `metadata.lastSnapshotTs < minRetainedTs` (or the\n * watermark is unknown). The between-ts scan would start below the oldest\n * retained commit log, so it could silently miss pruned commits → we cannot\n * prove freshness and must recompute. This is the case that a one-shot\n * (untracked by any subscription, so nothing holds the retention watermark\n * down) can hit.\n * 2. **Overlap** — a commit in `(lastSnapshotTs, currentTs]` touched the read\n * set.\n */\nexport async function evaluateCacheFreshness(\n db: Pick<Database, \"getCommitLogsBetweenTs\" | \"hasCommitForPoints\">,\n metadata: QueryCacheMetadata | null,\n currentTs: bigint,\n minRetainedTs: bigint | null,\n): Promise<CacheFreshness> {\n if (!metadata) return { fresh: false, readSet: [], rawReadSet: [] };\n\n const readSet = metadata.readSet;\n const rawReadSet = mapReadSetToRaw(readSet);\n\n if (minRetainedTs === null || metadata.lastSnapshotTs < minRetainedTs) {\n return { fresh: false, readSet, rawReadSet };\n }\n\n // A query that read nothing can't be invalidated by any commit — permanently\n // fresh (until TTL eviction). Skip the commit-log scan entirely.\n if (rawReadSet.length === 0) return { fresh: true, readSet, rawReadSet };\n\n // Window (lastSnapshotTs, currentTs]: commits strictly AFTER the cached\n // snapshot (the data already reflects everything <= lastSnapshotTs) up to and\n // INCLUDING currentTs. The DB scans take a half-open [begin, end), so +1n on\n // both ends models that inclusive upper bound. Missing the upper bound would\n // drop a mutation committed at exactly currentTs — and a one-shot query has no\n // push-invalidation backstop, so it would serve stale.\n const beginTs = metadata.lastSnapshotTs + 1n;\n const endTs = currentTs + 1n;\n\n // Partition the read set by how precisely a commit-log check can be pushed to\n // the DB:\n // • range tables — a commit only matters if the row's prev/post state\n // satisfies the predicate, which only the in-app evaluator can decide, so\n // the row `data` for these tables must be fetched.\n // • point-only tables (carry no range read) — overlap is a pure\n // (table_id, id) key match, answerable by an existence probe with no blob\n // fetch or JSON parse.\n // A point read whose table ALSO carries a range read is subsumed by that\n // table's row fetch (checkLogsAffectReadSet re-checks it against those rows),\n // so it is intentionally excluded from the point probe.\n //\n // Grouping is keyed by the table NAME (`r.table`), not the tableId bytes: a\n // tableId is a fixed 16-byte digest of the name (tableNameToId), fully derived\n // from it — and unlike the byte array (which is freshly deserialized from the\n // Redis metadata here, so has no stable object identity) a string is a free\n // Map/Set key with no hashing or bytesEqual. The map VALUE still carries the\n // tableId bytes, which the SQL probes need.\n const rangeTables = new Map<string, Uint8Array>();\n for (const r of rawReadSet) {\n if (r.kind === \"range\") rangeTables.set(r.table, r.tableId);\n }\n\n const pointProbes: { tableId: Uint8Array; id: Uint8Array }[] = [];\n for (const r of rawReadSet) {\n if (r.kind === \"point\" && !rangeTables.has(r.table)) {\n pointProbes.push({ tableId: r.tableId, id: r.id });\n }\n }\n\n // Point-only tables: one existence probe on exact (table_id, id) pairs — no\n // data blobs fetched, no predicates evaluated.\n if (pointProbes.length > 0) {\n if (await db.hasCommitForPoints(beginTs, endTs, pointProbes)) {\n return { fresh: false, readSet, rawReadSet };\n }\n }\n\n // Range tables: fetch the window's logs for just those tables and evaluate\n // predicates (and any point reads subsumed onto them) in-app.\n if (rangeTables.size > 0) {\n const logs = await db.getCommitLogsBetweenTs(beginTs, endTs, [...rangeTables.values()]);\n if (checkLogsAffectReadSet(logs, rawReadSet)) {\n return { fresh: false, readSet, rawReadSet };\n }\n }\n\n return { fresh: true, readSet, rawReadSet };\n}\n\nfunction checkPointReadOverlap(\n log: CommitLogEntry,\n rawRead: RawPointRead,\n): boolean {\n return bytesEqual(log.id, rawRead.id);\n}\n\nfunction checkRangeReadOverlap(\n log: CommitLogEntry,\n rawRead: RawRangeRead\n): boolean {\n const logData = parseLogData(log);\n\n // No envelope at all → can't reason, must conflict-out.\n if (!logData) { \n return true;\n }\n\n // Commit logs serialize the full WriteEntry (see PgDatabase.insertCommitLogs),\n // so extract the row state from it. Same prev-OR-post check as the\n // in-memory invalidator path.\n const op = (logData.op as WriteOp | undefined) ?? \"update\";\n const data = logData.data as Record<string, unknown> | undefined;\n const prevData = logData.prevData as Record<string, unknown> | undefined;\n\n return checkRowWriteAffectsRange(op, prevData, data, rawRead.predicate);\n}\n\nexport function checkWriteSetAffectsReadSet(\n writeSet: RawWriteEntry[],\n rawReadSet: RawReadEntry[]\n): boolean {\n for (const write of writeSet) {\n for (const rawRead of rawReadSet) {\n if (!bytesEqual(write.tableId, rawRead.tableId)) continue;\n\n if (checkWriteAffectsRawReadEntry(write, rawRead)) {\n return true;\n }\n }\n }\n return false;\n}\n\nfunction checkWriteAffectsRawReadEntry(\n write: RawWriteEntry,\n rawRead: RawReadEntry\n): boolean {\n if (rawRead.kind === \"point\") {\n return bytesEqual(write.id, rawRead.id);\n }\n return checkRowWriteAffectsRange(\n write.op,\n write.prevData,\n write.data,\n rawRead.predicate,\n );\n}\n\n/**\n * Evaluate whether a write affects a range subscription, given the row's\n * prior and post state. The write is considered affecting if EITHER side\n * matched the predicate — that catches:\n * - rows entering the result set (prev \"no\", post \"yes\")\n * - rows leaving the result set (prev \"yes\", post \"no\")\n * - rows changing while still in the set (prev \"yes\", post \"yes\")\n * Skipped only if both sides are a definite \"no\".\n *\n * When prevData is missing (e.g. inserts, or commit logs predating this\n * change), we fall back to conservative \"unknown\" on that side.\n */\nfunction checkRowWriteAffectsRange(\n op: WriteOp,\n prevData: Record<string, unknown> | undefined,\n data: Record<string, unknown> | undefined,\n predicate: RawRangeRead[\"predicate\"],\n): boolean {\n const prevVerdict: OverlapVerdict = prevData\n ? evaluatePredicateAgainstRow(prevData, predicate)\n : (op === \"insert\" ? \"no\" : \"unknown\");\n\n let postVerdict: OverlapVerdict;\n if (op === \"delete\") {\n postVerdict = \"no\";\n } else if (data) {\n // Merge over prevData so unchanged columns carry over — otherwise a\n // partial update (e.g. {title: \"x\"}) would be \"unknown\" on every\n // predicate column it doesn't touch.\n const post = prevData ? { ...prevData, ...data } : data;\n postVerdict = evaluatePredicateAgainstRow(post, predicate);\n } else {\n postVerdict = \"unknown\";\n }\n\n return prevVerdict !== \"no\" || postVerdict !== \"no\";\n}"],"mappings":"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"}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { Jn as DbQueryResult, Kn as CoreInitLockCtx, Qn as PreparedQueries, Xn as LazyCommitTsParam, Yn as DbType, Zn as PooledClient, er as SqlBuilder, ii as WriteEntry, nr as CacheLayer, qn as Database, ti as ReadEntry, tr as TxDatabase, wr as CommitLogEntry } from "./index-CBIjmPUS.js";
|
|
2
|
+
import { Pool, PoolConfig } from "pg";
|
|
3
|
+
import { Logger } from "pino";
|
|
4
|
+
|
|
5
|
+
//#region src/db/postgres.d.ts
|
|
6
|
+
declare class PgDatabase implements Database {
|
|
7
|
+
constructor(logger: Logger, config: PoolConfig, pool?: Pool | undefined);
|
|
8
|
+
private logger;
|
|
9
|
+
config: PoolConfig;
|
|
10
|
+
private pool;
|
|
11
|
+
readonly sqlBuilder: SqlBuilder;
|
|
12
|
+
readonly dbType: DbType;
|
|
13
|
+
columnTypes: Map<string, Map<string, string>>;
|
|
14
|
+
protected sqlCache: Map<string, string>;
|
|
15
|
+
/**
|
|
16
|
+
* Fetches actual column types from Postgres for `table` and returns a
|
|
17
|
+
* map of DB column name → PG type string (as produced by `format_type`,
|
|
18
|
+
* e.g. `bigint`, `integer`, `character varying(32)`, `text[]`, `jsonb`).
|
|
19
|
+
* No caching here — see {@link getColumnTypes} for the cached path.
|
|
20
|
+
*/
|
|
21
|
+
loadColumnTypesFromPg(table: string): Promise<Map<string, string>>;
|
|
22
|
+
/**
|
|
23
|
+
* Synchronous column-type lookup. The apply path calls this on every
|
|
24
|
+
* batched write, so it can't await — populate the cache via
|
|
25
|
+
* {@link bootstrapColumnTypes} at server start. Throws if `table` is
|
|
26
|
+
* missing (programmer error / bootstrap skipped).
|
|
27
|
+
*/
|
|
28
|
+
getColumnTypes(table: string): Map<string, string> | null;
|
|
29
|
+
/**
|
|
30
|
+
* Eagerly load column types into the in-memory cache. With a Redis
|
|
31
|
+
* client supplied, attempts the cache key first and falls through to
|
|
32
|
+
* Postgres on miss (writing the result back to Redis). This is the
|
|
33
|
+
* "one server queries PG, the rest read from Redis" pattern for cold
|
|
34
|
+
* starts in a multi-instance deploy. Without Redis, always queries PG.
|
|
35
|
+
*
|
|
36
|
+
* Pass `tables` to limit the load to a specific list; otherwise every
|
|
37
|
+
* user table in the connected database is loaded.
|
|
38
|
+
*
|
|
39
|
+
* The Redis cache key should encode whatever invalidates types in your
|
|
40
|
+
* environment (typically a schema version / deploy version). The
|
|
41
|
+
* subscriber in supalive-server.ts is expected to publish a reload
|
|
42
|
+
* signal after migrations and invoke {@link invalidateColumnTypes}.
|
|
43
|
+
*/
|
|
44
|
+
bootstrapColumnTypes(opts: {
|
|
45
|
+
redis?: CacheLayer;
|
|
46
|
+
cacheKey: string;
|
|
47
|
+
tables?: string[];
|
|
48
|
+
}): Promise<void>;
|
|
49
|
+
/**
|
|
50
|
+
* Invalidate cached types (and dependent SQL) for `table`, or the entire
|
|
51
|
+
* cache if `table` is omitted. The Redis-pubsub reload signal in
|
|
52
|
+
* supalive-server.ts is expected to call this on schema changes.
|
|
53
|
+
*/
|
|
54
|
+
private invalidateColumnTypes;
|
|
55
|
+
/** Enumerate the user-defined tables in the connected database — used
|
|
56
|
+
* by bootstrapColumnTypes() when no explicit list is provided. */
|
|
57
|
+
private listUserTables;
|
|
58
|
+
getCoreVersion(): Promise<bigint>;
|
|
59
|
+
getAppMigration(): Promise<bigint>;
|
|
60
|
+
coreMigrationStatements(toVersion: bigint): string[];
|
|
61
|
+
withCoreInitLock(fn: (ctx: CoreInitLockCtx) => Promise<void>): Promise<void>;
|
|
62
|
+
query<T = any>(sql: string, params?: unknown[]): Promise<DbQueryResult<T>>;
|
|
63
|
+
getClient(): Promise<PooledClient>;
|
|
64
|
+
getTransactionClient(): Promise<TxDatabase>;
|
|
65
|
+
/** Internal: replace this instance's caches with shared references from
|
|
66
|
+
* a parent. Used by PgDatabase.getTransactionClient. */
|
|
67
|
+
adoptCaches(columnTypes: Map<string, Map<string, string>>, sqlCache: Map<string, string>): void;
|
|
68
|
+
getLatestSnapshotTimestamp(): Promise<bigint>;
|
|
69
|
+
updateLatestSnapshotTimestamp(commitTs: bigint): Promise<void>;
|
|
70
|
+
getMinSnapshotTimestamp(): Promise<bigint>;
|
|
71
|
+
updateMinSnapshotTimestamp(ts: bigint): Promise<void>;
|
|
72
|
+
getNextTimestamp(): Promise<bigint>;
|
|
73
|
+
attachPrevDataToWriteSet(writeSet: WriteEntry[], readSet: ReadEntry[]): Promise<void>;
|
|
74
|
+
prepareCommitWrites(readSet: ReadEntry[], writeSet: WriteEntry[], commitTs: LazyCommitTsParam): Promise<PreparedQueries>;
|
|
75
|
+
private pgPrepareBatchWrites;
|
|
76
|
+
private pgPrepareInsertSingle;
|
|
77
|
+
private pgPrepareInsertBatch;
|
|
78
|
+
private pgPrepareUpdateSingle;
|
|
79
|
+
private pgPrepareUpdateBatch;
|
|
80
|
+
private pgPrepareDeleteSingle;
|
|
81
|
+
private pgPrepareDeleteBatch;
|
|
82
|
+
private buildOrGetInsertSql;
|
|
83
|
+
private buildOrGetUpdateSql;
|
|
84
|
+
private buildOrGetDeleteSql;
|
|
85
|
+
private pgPrepareCommitLogs;
|
|
86
|
+
/**
|
|
87
|
+
* Point-only OCC check. For each genuine point read, compare the row's
|
|
88
|
+
* current commit_ts against the value captured at read time. A `null`
|
|
89
|
+
* expectedCommitTs means "row didn't exist when we read"; any row found
|
|
90
|
+
* now is a conflict. Reads whose row is being written in this same txn
|
|
91
|
+
* are skipped — the CAS UPDATE/DELETE already validated those.
|
|
92
|
+
*/
|
|
93
|
+
private pgPrepareValidatePointReadsByCommitTs;
|
|
94
|
+
commitWrites(beginTs: bigint, queries: PreparedQueries, commitTs: bigint): Promise<{
|
|
95
|
+
success: boolean;
|
|
96
|
+
}>;
|
|
97
|
+
private pgApplyWrites;
|
|
98
|
+
/**
|
|
99
|
+
* Point-only OCC check. For each genuine point read, compare the row's
|
|
100
|
+
* current commit_ts against the value captured at read time. A `null`
|
|
101
|
+
* expectedCommitTs means "row didn't exist when we read"; any row found
|
|
102
|
+
* now is a conflict. Reads whose row is being written in this same txn
|
|
103
|
+
* are skipped — the CAS UPDATE/DELETE already validated those.
|
|
104
|
+
*/
|
|
105
|
+
private validatePointReadsByCommitTs;
|
|
106
|
+
pruneCommitLogsBefore(ts: bigint): Promise<bigint>;
|
|
107
|
+
getMinCommitLogTs(): Promise<bigint | null>;
|
|
108
|
+
getCommitLogsBetweenTs(beginTs: bigint, endTs: bigint, tableIds: Uint8Array[]): Promise<CommitLogEntry[]>;
|
|
109
|
+
hasCommitForPoints(beginTs: bigint, endTs: bigint, points: {
|
|
110
|
+
tableId: Uint8Array;
|
|
111
|
+
id: Uint8Array;
|
|
112
|
+
}[]): Promise<boolean>;
|
|
113
|
+
getCommitLogsSinceTs(sinceTs: bigint, excludeTs: bigint, tableIds: Uint8Array[]): Promise<CommitLogEntry[]>;
|
|
114
|
+
close(): Promise<void>;
|
|
115
|
+
}
|
|
116
|
+
//#endregion
|
|
117
|
+
export { PgDatabase as t };
|
|
118
|
+
//# sourceMappingURL=postgres-B2FLN1Kw.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"postgres-B2FLN1Kw.d.ts","names":[],"sources":["../src/db/postgres.ts"],"mappings":";;;;;cAiBa,UAAA,YAAsB,QAAA;cAE/B,MAAA,EAAQ,MAAA,EACR,MAAA,EAAQ,UAAA,EACR,IAAA,GAAM,IAAA;EAAA,QAWA,MAAA;EACD,MAAA,EAAQ,UAAA;EAAA,QACP,IAAA;EAAA,SACC,UAAA,EAAY,UAAA;EAAA,SACZ,MAAA,EAAQ,MAAA;EAMV,WAAA,EAAa,GAAA,SAAY,GAAA;EAAA,UAGtB,QAAA,EAAU,GAAA;EAHY;;;;;;EAW1B,qBAAA,CAAsB,KAAA,WAAgB,OAAA,CAAQ,GAAA;EAoDhD;;;;;;EA5BJ,cAAA,CAAe,KAAA,WAAgB,GAAA;EAgKoC;;;;;;;;;;;;;;;EAxI7D,oBAAA,CAAqB,IAAA;IACzB,KAAA,GAAQ,UAAA;IACR,QAAA;IACA,MAAA;EAAA,IACE,OAAA;EAgSO;;;;;EAAA,QAtPH,qBAAA;EAm4B+D;;EAAA,QAr3BzD,cAAA;EAUR,cAAA,IAAkB,OAAA;EAOlB,eAAA,IAAmB,OAAA;EAWzB,uBAAA,CAAwB,SAAA;EAUlB,gBAAA,CAAiB,EAAA,GAAK,GAAA,EAAK,eAAA,KAAoB,OAAA,SAAgB,OAAA;EAsC/D,KAAA,UAAe,GAAA,UAAa,MAAA,eAAyB,OAAA,CAAQ,aAAA,CAAc,CAAA;EAe3E,SAAA,IAAa,OAAA,CAAQ,YAAA;EAIrB,oBAAA,IAAwB,OAAA,CAAQ,UAAA;EA/OL;;EA2PjC,WAAA,CACE,WAAA,EAAa,GAAA,SAAY,GAAA,mBACzB,QAAA,EAAU,GAAA;EAMN,0BAAA,IAA8B,OAAA;EAK9B,6BAAA,CAA8B,QAAA,WAAmB,OAAA;EAOjD,uBAAA,IAA2B,OAAA;EAO3B,0BAAA,CAA2B,EAAA,WAAa,OAAA;EAOxC,gBAAA,IAAoB,OAAA;EAuBpB,wBAAA,CAAyB,QAAA,EAAU,UAAA,IAAc,OAAA,EAAS,SAAA,KAAc,OAAA;EAgExE,mBAAA,CACJ,OAAA,EAAS,SAAA,IACT,QAAA,EAAU,UAAA,IACV,QAAA,EAAU,iBAAA,GACT,OAAA,CAAQ,eAAA;EAAA,QAqBG,oBAAA;EAAA,QA2BN,qBAAA;EAAA,QAkBM,oBAAA;EAAA,QA+DN,qBAAA;EAAA,QAwBM,oBAAA;EAAA,QAsEN,qBAAA;EAAA,QAgBM,oBAAA;EAAA,QAiDN,mBAAA;EAAA,QAqBA,mBAAA;EAAA,QA4BA,mBAAA;EAAA,QAcM,mBAAA;EA9rBkB;;;;;;;EAAA,QA4uBxB,qCAAA;EAkDF,YAAA,CACJ,OAAA,UACA,OAAA,EAAS,eAAA,EACT,QAAA,WACC,OAAA;IAAU,OAAA;EAAA;EAAA,QAqCC,aAAA;EA3wBJ;;;;;;;EAAA,QAs3BI,4BAAA;EA0CR,qBAAA,CAAsB,EAAA,WAAa,OAAA;EAQnC,iBAAA,IAAqB,OAAA;EAQrB,sBAAA,CAAuB,OAAA,UAAiB,KAAA,UAAe,QAAA,EAAU,UAAA,KAAe,OAAA,CAAQ,cAAA;EAmBxF,kBAAA,CACJ,OAAA,UACA,KAAA,UACA,MAAA;IAAU,OAAA,EAAS,UAAA;IAAY,EAAA,EAAI,UAAA;EAAA,MAClC,OAAA;EAsBG,oBAAA,CAAqB,OAAA,UAAiB,SAAA,UAAmB,QAAA,EAAU,UAAA,KAAe,OAAA,CAAQ,cAAA;EAmB1F,KAAA,IAAS,OAAA;AAAA"}
|