@supalive/core 1.6.0 → 1.7.1

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.
@@ -1,7 +1,7 @@
1
1
  import { $n as RegisterSubscriptionParamsSchema, At as DevScheduler, Bt as SupaliveDb, E as Context, Ft as JobScheduler, It as QStashScheduler, Jn as AffectedSubscription, Lt as QStashSchedulerOptions, Mt as EnqueueOptions, Nt as IncomingJobRequest, Pt as JobClient, Qn as RegisterSubscriptionParams, Rn as Database, Rt as ScheduleHandle, Xn as InvalidateWritesetParamsSchema, Yn as InvalidateWritesetParams, Zn as InvalidateWritesetResult, ar as UnregisterSubscriptionsResult, cr as UpdateSubscriptionReadSetResult, er as RegisterSubscriptionResult, ir as UnregisterSubscriptionsParams, jt as DevSchedulerOptions, nr as UnregisterSubscriptionParamsSchema, or as UpdateSubscriptionReadSetParams, qn as CacheLayer, rr as UnregisterSubscriptionResult, rt as Router, sr as UpdateSubscriptionReadSetParamsSchema, tr as UnregisterSubscriptionParams, zt as ScheduledJobDef } from "../../index-DTNBHQPY.js";
2
2
  import { t as MySqlDatabase } from "../../mysql-BRyidTZD.js";
3
3
  import { t as PgDatabase } from "../../postgres-Dh4ATweM.js";
4
- import { b as SupaliveServerConfig, p as DatabaseConfig, u as SubId, x as UpstashConfig, y as SubscriptionManagerConfig } from "../../types_server-eJnBeRoI.js";
4
+ import { b as SupaliveServerConfig, p as DatabaseConfig, u as SubId, x as UpstashConfig, y as SubscriptionManagerConfig } from "../../types_server-DCD3Kqln.js";
5
5
  import pino, { Level } from "pino";
6
6
  import Redis$1 from "ioredis";
7
7
 
@@ -526,13 +526,17 @@ var FixedWindowRateLimiter = class {
526
526
  //#region src/server/http-rpc.ts
527
527
  const logger$3 = logger.child({ name: "HTTP_RPC" }, { level: logLevelOf("SERVER_LOG_LEVEL") });
528
528
  /**
529
- * Handles the one-shot HTTP RPC transport: `POST {path}/<procedureName>` for
530
- * `query`/`mutation`/`action`. Subscriptions are not offered here (WS only).
529
+ * Handles the one-shot HTTP RPC transport: a single `POST {path}` endpoint for
530
+ * `query`/`mutation`/`action`. The procedure name travels in the request body
531
+ * (`{ name, input }`), not the URL — so the wire never reveals which procedures
532
+ * exist and a transport-level error (which typically echoes the failing URL)
533
+ * can't leak a procedure name. Subscriptions are not offered here (WS only).
531
534
  * Kept independent of the WebSocket server so it can be unit-tested against a
532
535
  * plain `http.Server` with a stub db/registry.
533
536
  *
534
- * Request flow: CORS → (OPTIONS preflight) → rate limit (pre-auth) → procedure
535
- * lookup (internal/job rejected) → auth → validate input → run → respond.
537
+ * Request flow: CORS → (OPTIONS preflight) → rate limit (pre-auth) → read body →
538
+ * parse `{ name, input }` → procedure lookup (internal/job rejected) → auth →
539
+ * validate input → run → respond.
536
540
  */
537
541
  var HttpRpcHandler = class {
538
542
  deps;
@@ -548,9 +552,10 @@ var HttpRpcHandler = class {
548
552
  this.rateLimiter = new FixedWindowRateLimiter(rl.windowMs ?? 6e4, rl.max ?? 120);
549
553
  }
550
554
  }
551
- /** True when this handler owns the given request path. */
555
+ /** True when this handler owns the given request path. The procedure name is
556
+ * carried in the body now, so a single exact path is the whole surface. */
552
557
  handlesPath(path) {
553
- return path === this.path || path.startsWith(`${this.path}/`);
558
+ return path === this.path;
554
559
  }
555
560
  stop() {
556
561
  this.rateLimiter?.stop();
@@ -567,13 +572,6 @@ var HttpRpcHandler = class {
567
572
  error: "method not allowed",
568
573
  code: "METHOD_NOT_ALLOWED"
569
574
  });
570
- const prefix = `${this.path}/`;
571
- const name = path.startsWith(prefix) ? decodeURIComponent(path.slice(prefix.length)) : "";
572
- if (!name || name.includes("/")) return this.send(res, 404, {
573
- success: false,
574
- error: "unknown procedure",
575
- code: "NOT_FOUND"
576
- });
577
575
  if (this.rateLimiter) {
578
576
  const keyBy = this.rpc.rateLimit ? this.rpc.rateLimit.keyBy : void 0;
579
577
  const key = keyBy ? keyBy(req) : clientIp(req);
@@ -587,10 +585,25 @@ var HttpRpcHandler = class {
587
585
  });
588
586
  }
589
587
  }
588
+ const rawBody = await readBody(req);
589
+ let name;
590
+ let rawInput;
591
+ try {
592
+ const envelope = rawBody.trim().length > 0 ? JSON.parse(rawBody) : {};
593
+ if (!envelope || typeof envelope !== "object" || typeof envelope.name !== "string") throw new Error("missing procedure name");
594
+ name = envelope.name;
595
+ rawInput = envelope.input ?? {};
596
+ } catch {
597
+ return this.send(res, 400, {
598
+ success: false,
599
+ error: "invalid request",
600
+ code: "BAD_REQUEST"
601
+ });
602
+ }
590
603
  const procedure = this.deps.getRegistry()?.getProcedure(name);
591
604
  if (!procedure || procedure.internal || procedure.type === "job") return this.send(res, 404, {
592
605
  success: false,
593
- error: `Procedure "${name}" not found`,
606
+ error: "not found",
594
607
  code: "NOT_FOUND"
595
608
  });
596
609
  const auth = await this.resolveAuth(req);
@@ -599,11 +612,9 @@ var HttpRpcHandler = class {
599
612
  error: auth.message,
600
613
  code: "UNAUTHENTICATED"
601
614
  });
602
- const rawBody = await readBody(req);
603
615
  let input;
604
616
  try {
605
- const parsed = rawBody.trim().length > 0 ? JSON.parse(rawBody) : {};
606
- input = procedure.inputSchema.parse(parsed);
617
+ input = procedure.inputSchema.parse(rawInput);
607
618
  } catch (err) {
608
619
  logger$3.warn(err, `RPC "${name}" received invalid input`);
609
620
  return this.send(res, 400, {
@@ -1 +1 @@
1
- {"version":3,"file":"server.js","names":["logger","baseLogger","types.RegisterSubscriptionResultSchema","types.RegisterSubscriptionBatchResultSchema","types.UpdateSubscriptionReadSetResultSchema","types.UnregisterSubscriptionResultSchema","types.UnregisterSubscriptionsResultSchema","types.InvalidateWritesetResultSchema","logger","baseLogger","Client","logger","baseLogger","logger","baseLogger","uuidv4","logger","baseLogger","WebSocketServer"],"sources":["../../../src/server/sub_hash.ts","../../../src/server/one-shot-query.ts","../../../src/server/subscription-manager-client.ts","../../../src/jobs/scheduler.ts","../../../src/server/rate-limit.ts","../../../src/server/http-rpc.ts","../../../src/server/supalive-server.ts","../../../src/server/hash.ts","../../../src/server/subscription-manager.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","import type { SupaliveDb } from \"../db/realtime_db\";\nimport type { CacheLayer } from \"../db/cache\";\nimport type { RegisteredProcedure } from \"../router/router\";\nimport type { QueryCtx } from \"../router/procedure\";\nimport { stableStringify } from \"../helper\";\nimport { generateCacheKey, resolveQueryIdentity } from \"./sub_hash\";\nimport { evaluateCacheFreshness } from \"./overlap-checker\";\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. */\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 * 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. Freshness check via {@link evaluateCacheFreshness}: cached metadata +\n * the retention watermark + a between-ts commit-log scan.\n * 3. Fresh → return the cached data.\n * Stale/miss → recompute via `queryInternal` and write BOTH the data and\n * the metadata (readSet + snapshotTs) back, so a later one-shot or a fresh\n * 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 freshness upper bound — the latest snapshot ts as of\n * this call. The caller loads it (via `db.getLatestSnapshotTimestamp()`) and\n * passes it in, exactly like the subscribe path loads it before `register`, so\n * the snapshot read lives at the caller seam rather than being hidden here.\n */\nexport async function runCachedQuery<TContext>(\n deps: OneShotQueryDeps<TContext>,\n procedure: RegisteredProcedure<TContext>,\n procedureName: string,\n input: unknown,\n serverCtx: TContext | undefined,\n lastSnapshotTs: bigint,\n): Promise<unknown> {\n const queryIdentity = resolveQueryIdentity(procedure, serverCtx, input, deps.getUserId);\n const cacheKey = await generateCacheKey(procedureName, stableStringify(input), queryIdentity);\n\n const metadata = await deps.cache.getQueryCacheMetaData(cacheKey);\n const freshness = await evaluateCacheFreshness(deps.db.impl, metadata, lastSnapshotTs, deps.getMinRetainedTs());\n\n if (freshness.fresh) {\n const cached = await deps.cache.getQueryCacheFor(cacheKey);\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) return cached.data;\n }\n\n const result = await deps.db.queryInternal(async (ctx) => {\n const fn = procedure.fn as (ctx: QueryCtx<TContext>, input: unknown) => Promise<unknown>;\n return fn({ db: ctx, serverCtx }, input);\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 result.data;\n}\n","import { Client } from \"rpc-websockets\";\nimport z, { type ZodType } from \"zod\";\nimport * as types from \"./subscription-manager-types\";\nimport type { WriteEntry } from \"../db/types_db\";\nimport { logger as baseLogger, logLevelOf } from \"../logger\";\nimport { supaliveStringify } from \"../helper\";\n\nconst logger = baseLogger.child(\n { name: \"SUBS_MGR_CLIENT\" },\n { level: logLevelOf(\"SUB_MANAGER_CLIENT_LOG_LEVEL\") },\n);\n\n/**\n * Callback the app server registers to process invalidations that were\n * deferred during a sub-manager outage. When the sub-manager reconnects and\n * the buffered writeSets are flushed in one batched RPC, the resulting\n * `affected[]` is handed to this callback — which is expected to do the\n * same work as the normal per-mutation `reexecuteAffectedQuery` path.\n */\nexport type AffectedHandler = (\n affected: types.AffectedSubscription[],\n) => Promise<void>;\n\n/**\n * Callback the app server registers to drive recovery on sub-manager\n * reconnect. Iterates `serverSubscriptions`, reads `sl:reg:<subId>` from\n * Redis, and re-registers each known sub. Resolves when complete; only then\n * does the client's buffer flush and gated calls release.\n */\nexport type RecoveryDriver = () => Promise<void>;\n\nexport interface SubManagerClientOptions {\n url: string;\n /**\n * Maximum number of buffered WriteEntry records held during an outage.\n * If exceeded, the buffer is dropped — the commit-log replay path during\n * the next register call will catch up any missed invalidations, at the\n * cost of a brief recompute spike. Default 50_000 entries.\n */\n invalidateBufferLimit?: number;\n}\n\ntype ReadyState = \"ready\" | \"busy\";\n\nexport const subscriptionManagerDataPack = {\n encode: (data: unknown) => supaliveStringify(data),\n decode: (data: string) => JSON.parse(data),\n};\n\n\n/**\n * RPC client for the (single) sub-manager process.\n *\n * Design (Convex-inspired):\n * • The sub-manager is in-memory and ephemeral. There is no version\n * protocol, no client-side registration registry, no OUT_OF_SYNC\n * replay logic. All recovery is driven from the app server using\n * state it already has (`serverSubscriptions`) plus registration\n * records persisted to Redis at `sl:reg:<subId>`.\n *\n * • Out-of-order `updateSubscriptionReadSet` calls are filtered server\n * side using `lastSnapshotTs` (monotonic DB commit timestamp).\n *\n * • On rpc-websockets reconnect, the client:\n * 1. Transitions to `busy` (any in-flight `invalidateWriteset`\n * buffers its writeSet).\n * 2. Invokes the app server's `recoveryDriver` to re-register all\n * known subs.\n * 3. Flushes the buffer as one batched `invalidateWriteset` RPC and\n * hands the resulting `affected[]` to `affectedHandler`.\n * 4. Transitions to `ready`.\n *\n * • During an extended outage, the buffer is bounded; if exceeded, it\n * is dropped and we rely on the commit-log replay built into\n * `SubscriptionWorker.register()` to catch each sub up on the next\n * re-register.\n */\nexport class SubManagerClient {\n private client: Client;\n private state: ReadyState = \"busy\";\n private hasConnectedOnce = false;\n\n /** Resolves whenever state transitions to 'ready'. Replaced on each busy→ready cycle. */\n private readyPromise: Promise<void>;\n private resolveReady!: () => void;\n\n /** Writeset entries accumulated while state === 'busy'. Flushed as one RPC after recovery. */\n private bufferedWriteSets: WriteEntry[] = [];\n private readonly bufferLimit: number;\n private bufferDroppedDuringOutage = false;\n\n private recoveryDriver?: RecoveryDriver;\n private affectedHandler?: AffectedHandler;\n\n constructor(urlOrOptions: string | SubManagerClientOptions) {\n const opts = typeof urlOrOptions === \"string\"\n ? { url: urlOrOptions }\n : urlOrOptions;\n\n this.bufferLimit = opts.invalidateBufferLimit ?? 50_000;\n this.readyPromise = new Promise<void>((r) => { this.resolveReady = r; });\n\n this.client = new Client(opts.url, {\n dataPack: subscriptionManagerDataPack,\n });\n this.client.setAutoReconnect(true);\n this.client.setMaxReconnects(0);\n // Ensure the dataPack is set even if rpc-websockets doesn't pass it through to the Client constructor\n this.client.dataPack = subscriptionManagerDataPack;\n\n this.client.on(\"open\", this.handleOpen.bind(this));\n this.client.on(\"close\", this.handleClose.bind(this));\n this.client.on(\"error\", (err: unknown) => logger.warn(err, \"sub-manager client socket error\"));\n }\n\n /**\n * Wire up the app-server-driven recovery. Must be called before any\n * disconnect/reconnect cycle for buffered writeSets to be flushed and\n * subscriptions re-registered.\n */\n setRecoveryDriver(driver: RecoveryDriver): void {\n this.recoveryDriver = driver;\n }\n\n /**\n * Wire up the handler invoked when batched invalidate results land\n * after recovery. The handler is expected to drive\n * `reexecuteAffectedQuery` for each entry.\n */\n setAffectedHandler(handler: AffectedHandler): void {\n this.affectedHandler = handler;\n }\n\n // ─── RPC methods ────────────────────────────────────────────────────────\n\n async registerSubscription(\n params: types.RegisterSubscriptionParams,\n ): Promise<types.RegisterSubscriptionResult> {\n return this.call(\"registerSubscription\", params, types.RegisterSubscriptionResultSchema);\n }\n\n async registerSubscriptionBatch(\n params: types.RegisterSubscriptionParams[],\n ): Promise<types.RegisterSubscriptionResult[]> {\n return this.call(\"registerSubscriptionBatch\", params, types.RegisterSubscriptionBatchResultSchema);\n }\n\n async updateSubscriptionReadSet(\n params: types.UpdateSubscriptionReadSetParams,\n ): Promise<types.UpdateSubscriptionReadSetResult> {\n return this.call(\"updateSubscriptionReadSet\", params, types.UpdateSubscriptionReadSetResultSchema);\n }\n\n async unregisterSubscription(\n params: types.UnregisterSubscriptionParams,\n ): Promise<types.UnregisterSubscriptionResult> {\n return this.call(\"unregisterSubscription\", params, types.UnregisterSubscriptionResultSchema);\n }\n\n async unregisterSubscriptions(\n params: types.UnregisterSubscriptionsParams,\n ): Promise<types.UnregisterSubscriptionsResult> {\n return this.call(\"unregisterSubscriptions\", params, types.UnregisterSubscriptionsResultSchema);\n }\n\n /**\n * Sends the writeSet to the sub-manager for invalidation, OR buffers it\n * if the client is currently disconnected / recovering. Returns\n * `{ affected: [] }` while buffering — the actual `affected` from the\n * buffered batch is delivered via the registered `affectedHandler`\n * after recovery completes.\n */\n async invalidateWriteset(\n params: types.InvalidateWritesetParams,\n ): Promise<types.InvalidateWritesetResult> {\n if (this.state === \"busy\") {\n this.appendToBuffer(params.writeSet);\n return { affected: [] };\n }\n return this.call(\"invalidateWriteset\", params, types.InvalidateWritesetResultSchema);\n }\n\n // ─── Internal ───────────────────────────────────────────────────────────\n\n /**\n * Single point through which RPCs go. Throws on transport / server error.\n * `registerSubscription`, `updateSubscriptionReadSet`, and\n * `unregisterSubscription` are NOT gated on the ready state: during a\n * disconnect they will fail at the socket layer, and during recovery\n * (which itself uses these methods) gating would deadlock.\n */\n private async call<T extends ZodType>(\n procName: string,\n params: unknown,\n resType: T,\n ): Promise<z.infer<T>> {\n const result = await this.client.call(procName, params);\n return resType.parse(result) as z.infer<T>;\n }\n\n private appendToBuffer(writeSet: WriteEntry[]): void {\n if (this.bufferedWriteSets.length + writeSet.length > this.bufferLimit) {\n if (!this.bufferDroppedDuringOutage) {\n logger.warn(\n `Invalidate buffer would exceed ${this.bufferLimit}; dropping buffered entries. ` +\n `Subscriptions will be caught up via commit-log replay on re-register.`,\n );\n this.bufferDroppedDuringOutage = true;\n }\n this.bufferedWriteSets = [];\n return;\n }\n this.bufferedWriteSets.push(...writeSet);\n }\n\n private async flushBuffer(): Promise<void> {\n if (this.bufferedWriteSets.length === 0) return;\n const writeSet = this.bufferedWriteSets;\n this.bufferedWriteSets = [];\n this.bufferDroppedDuringOutage = false;\n\n try {\n const result = await this.call(\n \"invalidateWriteset\",\n { writeSet },\n types.InvalidateWritesetResultSchema,\n );\n if (this.affectedHandler && result.affected.length > 0) {\n await this.affectedHandler(result.affected);\n }\n } catch (err) {\n logger.error(err, \"Failed to flush invalidate buffer after recovery\");\n }\n }\n\n private markBusy(): void {\n if (this.state === \"busy\") return;\n this.state = \"busy\";\n this.readyPromise = new Promise<void>((r) => { this.resolveReady = r; });\n }\n\n private markReady(): void {\n if (this.state === \"ready\") return;\n this.state = \"ready\";\n this.resolveReady();\n }\n\n private handleClose(): void {\n logger.info(\"sub-manager client: socket closed\");\n this.markBusy();\n }\n\n private handleOpen(): void {\n if (!this.hasConnectedOnce) {\n this.hasConnectedOnce = true;\n logger.info(\"sub-manager client: initial connect\");\n this.markReady();\n return;\n }\n logger.info(\"sub-manager client: reconnected — starting recovery\");\n void this.runRecovery();\n }\n\n private async runRecovery(): Promise<void> {\n // Stay 'busy' for the duration of recovery so concurrent\n // invalidateWriteset calls keep buffering.\n try {\n if (this.recoveryDriver) {\n await this.recoveryDriver();\n } else {\n logger.warn(\"sub-manager client: no recoveryDriver configured — skipping recovery step\");\n }\n await this.flushBuffer();\n } catch (err) {\n logger.error(err, \"Recovery failed; state will be marked ready anyway\");\n } finally {\n logger.info(\"sub-manager client: recovery complete\");\n this.markReady();\n }\n }\n\n /**\n * Test/inspection helpers.\n */\n /** @internal */\n isReady(): boolean { return this.state === \"ready\"; }\n /** @internal */\n bufferSize(): number { return this.bufferedWriteSets.length; }\n}\n","import { Client, Receiver } from \"@upstash/qstash\";\nimport { logger as baseLogger, logLevelOf } from \"../logger\";\n\nconst logger = baseLogger.child(\n { name: \"SCHEDULER\" },\n { level: logLevelOf(\"SCHEDULER_LOG_LEVEL\") },\n);\n\n// ─── Scheduler Types ─────────────────────────────────────────────────────────\n\n/** A recurring job to keep synced on the scheduler side. */\nexport interface ScheduledJobDef {\n /** Job procedure name (also the last path segment of its endpoint). */\n name: string;\n /** Cron expression, e.g. `\"0 3 * * *\"`. */\n cron: string;\n}\n\n/** Options for a precise, one-shot job run. */\nexport interface EnqueueOptions {\n /** Run after this many seconds from now. */\n delaySeconds?: number;\n /** Run at this absolute time. Overrides `delaySeconds`. */\n runAt?: Date;\n /**\n * Idempotency key. If two enqueues share the same id within the provider's\n * dedup window, only the first is delivered. Use it to make \"schedule the\n * expiry of X\" safe to call more than once.\n */\n dedupeId?: string;\n}\n\n/** Handle returned by {@link JobScheduler.enqueue}; pass its `id` to `cancel`. */\nexport interface ScheduleHandle {\n id: string;\n}\n\n/** The pieces of an inbound webhook the scheduler needs to authenticate it. */\nexport interface IncomingJobRequest {\n /** Job name parsed from the request path. */\n name: string;\n /** Raw (unparsed) request body — signatures are computed over these bytes. */\n rawBody: string;\n /** Request headers, keys lowercased. */\n headers: Record<string, string | undefined>;\n /** Full request URL the webhook was delivered to. */\n url: string;\n}\n\n/**\n * Pluggable scheduler. The server owns exactly one instance and uses it for\n * both directions:\n * • inbound — `verify` authenticates a webhook before the job runs.\n * • outbound — `syncSchedules` (recurring, at startup) and `enqueue`/`cancel`\n * (precise one-shots, at runtime) drive the provider.\n *\n * Both dev and production implementations POST to the same HTTP job endpoint,\n * so the server's dispatch path is exercised identically in every environment.\n */\nexport interface JobScheduler {\n /** Authenticate an inbound webhook. Return false (or throw) to reject. */\n verify(req: IncomingJobRequest): Promise<boolean>;\n\n /**\n * Make the provider's recurring schedules match `jobs` exactly (create,\n * update, and remove stale ones). Called once at server startup.\n * `endpointFor(name)` yields the absolute URL the provider should POST to.\n */\n syncSchedules(\n jobs: ScheduledJobDef[],\n endpointFor: (name: string) => string,\n ): Promise<void>;\n\n /** Enqueue a precise one-shot run of `name` with `body` at `endpoint`. */\n enqueue(\n name: string,\n body: unknown,\n endpoint: string,\n opts?: EnqueueOptions,\n ): Promise<ScheduleHandle>;\n\n /** Cancel a previously enqueued one-shot by its handle id. */\n cancel(handleId: string): Promise<void>;\n}\n\nfunction delayMsFrom(opts?: EnqueueOptions): number {\n if (opts?.runAt) return Math.max(0, opts.runAt.getTime() - Date.now());\n if (opts?.delaySeconds && opts.delaySeconds > 0) return opts.delaySeconds * 1000;\n return 0;\n}\n\n// ─── Dev Scheduler ───────────────────────────────────────────────────────────\n\nexport interface DevSchedulerOptions {\n /**\n * How often, in ms, to fire each recurring job in dev. Dev intentionally\n * ignores exact cron timing (that is the production scheduler's job); it\n * just fires often enough to exercise the handler. Default 60_000.\n */\n recurringIntervalMs?: number;\n /**\n * Override the HTTP client (for tests). Defaults to the global `fetch`.\n */\n fetchImpl?: typeof fetch;\n}\n\n/**\n * In-process scheduler for local development and tests. Recurring jobs fire on\n * a fixed interval; one-shots fire on an exact `setTimeout` (so precise timing\n * is faithfully testable). Every fire POSTs to the real HTTP job endpoint, and\n * `verify` accepts everything — there is no signing in dev.\n */\nexport class DevScheduler implements JobScheduler {\n private readonly intervalMs: number;\n private readonly fetchImpl: typeof fetch;\n private intervals = new Map<string, ReturnType<typeof setInterval>>();\n private timeouts = new Map<string, ReturnType<typeof setTimeout>>();\n\n constructor(opts: DevSchedulerOptions = {}) {\n this.intervalMs = opts.recurringIntervalMs ?? 60_000;\n this.fetchImpl = opts.fetchImpl ?? fetch;\n }\n\n async verify(_req: IncomingJobRequest): Promise<boolean> {\n return true;\n }\n\n async syncSchedules(\n jobs: ScheduledJobDef[],\n endpointFor: (name: string) => string,\n ): Promise<void> {\n // Rebuild from scratch so a re-sync reflects removed jobs.\n for (const timer of this.intervals.values()) clearInterval(timer);\n this.intervals.clear();\n\n for (const job of jobs) {\n const endpoint = endpointFor(job.name);\n const timer = setInterval(() => {\n this.post(endpoint, {}).catch((err) =>\n logger.error(err, `dev recurring job \"${job.name}\" failed`),\n );\n }, this.intervalMs);\n // Don't keep the process alive purely for the dev poller.\n timer.unref?.();\n this.intervals.set(job.name, timer);\n logger.info(\n `dev schedule \"${job.name}\" (cron \"${job.cron}\") → firing every ${this.intervalMs}ms`,\n );\n }\n }\n\n async enqueue(\n name: string,\n body: unknown,\n endpoint: string,\n opts?: EnqueueOptions,\n ): Promise<ScheduleHandle> {\n const id = crypto.randomUUID();\n const timer = setTimeout(() => {\n this.timeouts.delete(id);\n this.post(endpoint, body).catch((err) =>\n logger.error(err, `dev one-shot job \"${name}\" failed`),\n );\n }, delayMsFrom(opts));\n timer.unref?.();\n this.timeouts.set(id, timer);\n return { id };\n }\n\n async cancel(handleId: string): Promise<void> {\n const timer = this.timeouts.get(handleId);\n if (timer) {\n clearTimeout(timer);\n this.timeouts.delete(handleId);\n }\n }\n\n /** Stop every dev timer (call on server shutdown / in test teardown). */\n stop(): void {\n for (const t of this.intervals.values()) clearInterval(t);\n for (const t of this.timeouts.values()) clearTimeout(t);\n this.intervals.clear();\n this.timeouts.clear();\n }\n\n private async post(endpoint: string, body: unknown): Promise<void> {\n const res = await this.fetchImpl(endpoint, {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\" },\n body: JSON.stringify(body ?? {}),\n });\n if (!res.ok) {\n throw new Error(`job endpoint ${endpoint} returned ${res.status}`);\n }\n }\n}\n\n// ─── QStash Scheduler (production) ───────────────────────────────────────────\n\nexport interface QStashSchedulerOptions {\n /** QStash Base url (`QSTASH_BASE_URL`). */\n baseUrl: string;\n /** QStash REST token (`QSTASH_TOKEN`). */\n token: string;\n /** Current signing key for webhook verification (`QSTASH_CURRENT_SIGNING_KEY`). */\n currentSigningKey: string;\n /** Next signing key, for zero-downtime key rotation (`QSTASH_NEXT_SIGNING_KEY`). */\n nextSigningKey: string;\n /* explicitly set qstash to dev mode to use local qstash server */\n devMode: boolean;\n}\n\n/**\n * Production scheduler backed by Upstash QStash. Externalizes the timer so a\n * horizontally-scaled deployment fires each cron tick exactly once (QStash\n * calls one HTTP endpoint), with at-least-once retries and signed webhooks.\n */\nexport class QStashScheduler implements JobScheduler {\n private readonly client: Client;\n private readonly receiver: Receiver;\n\n constructor(opts: QStashSchedulerOptions) {\n this.client = new Client({ baseUrl: opts.baseUrl ,token: opts.token, devMode: opts.devMode });\n this.receiver = new Receiver({\n currentSigningKey: opts.currentSigningKey,\n nextSigningKey: opts.nextSigningKey,\n });\n }\n\n async verify(req: IncomingJobRequest): Promise<boolean> {\n const signature = req.headers[\"upstash-signature\"];\n if (!signature) return false;\n try {\n return await this.receiver.verify({\n signature,\n body: req.rawBody,\n url: req.url,\n });\n } catch (err) {\n logger.warn(err, `signature verification failed for job \"${req.name}\"`);\n return false;\n }\n }\n\n async syncSchedules(\n jobs: ScheduledJobDef[],\n endpointFor: (name: string) => string,\n ): Promise<void> {\n // A deterministic scheduleId per job makes create an idempotent upsert and\n // lets us reconcile: anything QStash still has that we no longer declare is\n // a stale schedule and gets removed.\n const desired = new Map(jobs.map((j) => [scheduleIdFor(j.name), j]));\n\n let existing: Awaited<ReturnType<Client[\"schedules\"][\"list\"]>> = [];\n try {\n existing = await this.client.schedules.list();\n } catch (err) {\n logger.error(err, \"failed to list QStash schedules; skipping reconcile of stale schedules\");\n }\n\n for (const schedule of existing) {\n const id = schedule.scheduleId;\n if (id?.startsWith(SCHEDULE_ID_PREFIX) && !desired.has(id)) {\n try {\n await this.client.schedules.delete(id);\n logger.info(`removed stale QStash schedule ${id}`);\n } catch (err) {\n logger.error(err, `failed to remove stale schedule ${id}`);\n }\n }\n }\n\n for (const [scheduleId, job] of desired) {\n try {\n await this.client.schedules.create({\n scheduleId,\n destination: endpointFor(job.name),\n cron: job.cron,\n });\n logger.info(`synced QStash schedule \"${job.name}\" (${job.cron})`);\n } catch (err) {\n logger.error(err, `failed to sync schedule for job \"${job.name}\"`);\n throw err;\n }\n }\n }\n\n async enqueue(\n name: string,\n body: unknown,\n endpoint: string,\n opts?: EnqueueOptions,\n ): Promise<ScheduleHandle> {\n const res = await this.client.publishJSON({\n url: endpoint,\n body,\n ...(opts?.runAt\n ? { notBefore: Math.floor(opts.runAt.getTime() / 1000) }\n : opts?.delaySeconds\n ? { delay: opts.delaySeconds }\n : {}),\n ...(opts?.dedupeId ? { deduplicationId: opts.dedupeId } : {}),\n });\n const messageId = (res as { messageId?: string }).messageId;\n if (!messageId) throw new Error(`QStash publish for job \"${name}\" returned no messageId`);\n return { id: messageId };\n }\n\n async cancel(handleId: string): Promise<void> {\n await this.client.messages.cancel(handleId);\n }\n}\n\nconst SCHEDULE_ID_PREFIX = \"supalive-job-\";\n\nfunction scheduleIdFor(jobName: string): string {\n return `${SCHEDULE_ID_PREFIX}${jobName}`;\n}\n\n// ─── Job Client (app-facing) ─────────────────────────────────────────────────\n\n/**\n * Thin app-facing handle for scheduling precise one-shots from inside\n * procedures. The server builds one (via `getJobClient()`) that already knows\n * how to turn a job name into its endpoint, so callers just say\n * `jobs.enqueue(\"expireSpendAuth\", { id }, { runAt })`.\n */\nexport class JobClient {\n constructor(\n private readonly scheduler: JobScheduler,\n private readonly endpointFor: (name: string) => string,\n ) {}\n\n enqueue(name: string, body: unknown, opts?: EnqueueOptions): Promise<ScheduleHandle> {\n return this.scheduler.enqueue(name, body, this.endpointFor(name), opts);\n }\n\n cancel(handleId: string): Promise<void> {\n return this.scheduler.cancel(handleId);\n }\n}\n","/**\n * Fixed-window rate limiter. Per-instance and in-memory: each key gets a\n * counter that resets every `windowMs`. Good enough to blunt abuse of the HTTP\n * RPC endpoint (which does a JWT verify + possibly a DB read per call) without\n * the latency of a shared Redis counter. A background sweep evicts expired\n * buckets so memory stays bounded under churny key sets (e.g. many IPs).\n */\nexport interface RateLimitResult {\n allowed: boolean;\n /** Milliseconds until the current window resets (0 when allowed). */\n retryAfterMs: number;\n /** Remaining allowance in the current window (0 when blocked). */\n remaining: number;\n}\n\nexport class FixedWindowRateLimiter {\n private readonly buckets = new Map<string, { count: number; resetAt: number }>();\n private sweepTimer?: ReturnType<typeof setInterval>;\n\n constructor(\n private readonly windowMs: number,\n private readonly max: number,\n ) {\n this.sweepTimer = setInterval(() => this.sweep(), Math.max(windowMs, 1000));\n // Never keep the process alive just for the sweep.\n this.sweepTimer.unref?.();\n }\n\n check(key: string, now: number = Date.now()): RateLimitResult {\n let bucket = this.buckets.get(key);\n if (!bucket || bucket.resetAt <= now) {\n bucket = { count: 0, resetAt: now + this.windowMs };\n this.buckets.set(key, bucket);\n }\n bucket.count++;\n if (bucket.count > this.max) {\n return { allowed: false, retryAfterMs: bucket.resetAt - now, remaining: 0 };\n }\n return { allowed: true, retryAfterMs: 0, remaining: this.max - bucket.count };\n }\n\n private sweep(now: number = Date.now()): void {\n for (const [key, bucket] of this.buckets) {\n if (bucket.resetAt <= now) this.buckets.delete(key);\n }\n }\n\n /** Stop the background sweep (call on server shutdown / test teardown). */\n stop(): void {\n if (this.sweepTimer) clearInterval(this.sweepTimer);\n this.sweepTimer = undefined;\n }\n}\n","import type { IncomingMessage, ServerResponse } from \"http\";\nimport type { ZodType } from \"zod\";\nimport { supaliveStringify } from \"../helper\";\nimport { AuthenticationError } from \"../db/types_client_rpc\";\nimport type { SupaliveDb } from \"../db/realtime_db\";\nimport type { ContextRegistry, RegisteredProcedure } from \"../router/router\";\nimport type { MutationCtx, ActionCtx } from \"../router/procedure\";\nimport type { RpcConfig } from \"../config\";\nimport type { ObjectStorage } from \"../storage\";\nimport type { JobClient } from \"../jobs/scheduler\";\nimport { FixedWindowRateLimiter } from \"./rate-limit\";\nimport { runCachedQuery, type OneShotQueryDeps } from \"./one-shot-query\";\nimport { logger as baseLogger, logLevelOf } from \"../logger\";\n\nconst logger = baseLogger.child(\n { name: \"HTTP_RPC\" },\n { level: logLevelOf(\"SERVER_LOG_LEVEL\") },\n);\n\nexport interface HttpRpcDeps<TContext> {\n /** Lazily fetch the registry (set after `registerRouter`). */\n getRegistry: () => ContextRegistry | undefined;\n /** The server's SupaliveDb — the SAME instance whose after-commit hook drives\n * subscription invalidation, so an HTTP mutation pushes live updates too.\n * Used for mutations/actions; queries go through `oneShotDeps`. */\n db: SupaliveDb;\n /** Optional object storage exposed to ACTION handlers as `ctx.storage`. */\n storage: ObjectStorage;\n /** Optional job client exposed to ACTION handlers as `ctx.scheduler`. */\n scheduler: JobClient;\n /** Shared, pre-built one-shot query cache deps (db + cache + getUserId +\n * getMinRetainedTs). Same object the WS `call` path uses, so HTTP one-shot queries\n * cache/segment identically. */\n oneShotDeps: OneShotQueryDeps<TContext>;\n /** Same auth callback used by the WebSocket path. */\n verifyAuth?: (data: Record<string, unknown>, sessionId: string) => Promise<TContext | null>;\n rpc: RpcConfig<TContext>;\n}\n\n/**\n * Handles the one-shot HTTP RPC transport: `POST {path}/<procedureName>` for\n * `query`/`mutation`/`action`. Subscriptions are not offered here (WS only).\n * Kept independent of the WebSocket server so it can be unit-tested against a\n * plain `http.Server` with a stub db/registry.\n *\n * Request flow: CORS → (OPTIONS preflight) → rate limit (pre-auth) → procedure\n * lookup (internal/job rejected) → auth → validate input → run → respond.\n */\nexport class HttpRpcHandler<TContext> {\n private readonly path: string;\n private readonly rpc: RpcConfig<TContext>;\n private readonly rateLimiter?: FixedWindowRateLimiter;\n\n constructor(private readonly deps: HttpRpcDeps<TContext>) {\n this.rpc = deps.rpc;\n this.path = normalizeRpcPath(deps.rpc.path ?? \"/_rpc\");\n if (deps.rpc.rateLimit !== false) {\n const rl = deps.rpc.rateLimit ?? {};\n this.rateLimiter = new FixedWindowRateLimiter(rl.windowMs ?? 60_000, rl.max ?? 120);\n }\n }\n\n /** True when this handler owns the given request path. */\n handlesPath(path: string): boolean {\n return path === this.path || path.startsWith(`${this.path}/`);\n }\n\n stop(): void {\n this.rateLimiter?.stop();\n }\n\n async handle(req: IncomingMessage, res: ServerResponse, path: string): Promise<void> {\n this.applyCors(req, res);\n\n if (req.method === \"OPTIONS\") {\n res.writeHead(204);\n res.end();\n return;\n }\n if (req.method !== \"POST\") {\n return this.send(res, 405, { success: false, error: \"method not allowed\", code: \"METHOD_NOT_ALLOWED\" });\n }\n\n const prefix = `${this.path}/`;\n const name = path.startsWith(prefix) ? decodeURIComponent(path.slice(prefix.length)) : \"\";\n if (!name || name.includes(\"/\")) {\n return this.send(res, 404, { success: false, error: \"unknown procedure\", code: \"NOT_FOUND\" });\n }\n\n // Rate limit BEFORE auth so a flood can't hammer the (expensive) verify.\n if (this.rateLimiter) {\n // `rateLimit` is a config object here (rateLimiter only exists when it\n // isn't `false`), so a `keyBy` override is read straight off it.\n const keyBy = this.rpc.rateLimit ? this.rpc.rateLimit.keyBy : undefined;\n const key = keyBy ? keyBy(req) : clientIp(req);\n const rl = this.rateLimiter.check(key);\n if (!rl.allowed) {\n res.setHeader(\"retry-after\", String(Math.ceil(rl.retryAfterMs / 1000)));\n return this.send(res, 429, { success: false, error: \"rate limit exceeded\", code: \"RATE_LIMITED\" });\n }\n }\n\n const procedure = this.deps.getRegistry()?.getProcedure(name);\n // Internal procedures and jobs are server-only; subscriptions aren't offered\n // over HTTP. Report NOT_FOUND uniformly so nothing is enumerable.\n if (!procedure || procedure.internal || procedure.type === \"job\") {\n return this.send(res, 404, { success: false, error: `Procedure \"${name}\" not found`, code: \"NOT_FOUND\" });\n }\n\n const auth = await this.resolveAuth(req);\n if (!auth.ok) {\n return this.send(res, 401, { success: false, error: auth.message, code: \"UNAUTHENTICATED\" });\n }\n\n const rawBody = await readBody(req);\n let input: unknown;\n try {\n const parsed = rawBody.trim().length > 0 ? JSON.parse(rawBody) : {};\n input = (procedure.inputSchema as ZodType).parse(parsed);\n } catch (err) {\n logger.warn(err, `RPC \"${name}\" received invalid input`);\n return this.send(res, 400, { success: false, error: \"invalid input\", code: \"BAD_REQUEST\" });\n }\n\n try {\n const data = await this.runProcedure(procedure, input, auth.ctx);\n return this.send(res, 200, { success: true, data });\n } catch (err) {\n logger.error(err, `RPC \"${name}\" failed`);\n return this.send(res, 500, {\n success: false,\n error: err instanceof Error ? err.message : \"execution failed\",\n code: \"EXECUTION_ERROR\",\n });\n }\n }\n\n private async runProcedure(\n procedure: RegisteredProcedure<TContext>,\n input: unknown,\n serverCtx: TContext | undefined,\n ): Promise<unknown> {\n if (procedure.type === \"query\") {\n // Same cache/identity/freshness model as a WS one-shot query — reuses the\n // server's shared one-shot deps. Load the freshness upper bound here, the\n // same way the WS path does before calling runCachedQuery.\n const lastSnapshotTs = await this.deps.db.impl.getLatestSnapshotTimestamp();\n return runCachedQuery(\n this.deps.oneShotDeps,\n procedure,\n procedure.name,\n input,\n serverCtx,\n lastSnapshotTs,\n );\n }\n if (procedure.type === \"mutation\") {\n // Direct (no per-session queue); OCC makes concurrent HTTP mutations safe,\n // and the db's after-commit hook invalidates subscriptions like a WS write.\n const res = await this.deps.db.mutation(async (ctx) => {\n const fn = procedure.fn as (c: MutationCtx<TContext>, i: unknown) => Promise<unknown>;\n return fn({ db: ctx, serverCtx }, input);\n });\n return res.result;\n }\n // action\n const fn = procedure.fn as (c: ActionCtx<TContext>, i: unknown) => Promise<unknown>;\n return fn(\n { db: this.deps.db, serverCtx, storage: this.deps.storage, scheduler: this.deps.scheduler },\n input,\n );\n }\n\n private async resolveAuth(\n req: IncomingMessage,\n ): Promise<{ ok: true; ctx: TContext | undefined } | { ok: false; message: string }> {\n const extractor = this.rpc.auth ?? defaultHttpAuth;\n const data = extractor(req);\n // No verifyAuth configured, or no auth data → anonymous (mirrors WS).\n if (!this.deps.verifyAuth || !data) return { ok: true, ctx: undefined };\n try {\n const ctx = await this.deps.verifyAuth(data, `http-${crypto.randomUUID()}`);\n if (!ctx) return { ok: false, message: \"Invalid authentication\" };\n return { ok: true, ctx };\n } catch (err) {\n if (err instanceof AuthenticationError) {\n return { ok: false, message: `Invalid authentication: ${err.message}` };\n }\n return { ok: false, message: \"Invalid authentication\" };\n }\n }\n\n private applyCors(req: IncomingMessage, res: ServerResponse) {\n const cors = this.rpc.cors;\n if (!cors) return;\n const originHeader = req.headers[\"origin\"];\n const origin = Array.isArray(originHeader) ? originHeader[0] : originHeader;\n\n if (cors.origins === \"*\") {\n res.setHeader(\"access-control-allow-origin\", \"*\");\n } else if (origin && cors.origins.includes(origin)) {\n res.setHeader(\"access-control-allow-origin\", origin);\n res.setHeader(\"vary\", \"Origin\");\n } else {\n return; // origin not allowed → emit no CORS headers\n }\n res.setHeader(\"access-control-allow-methods\", \"POST, OPTIONS\");\n res.setHeader(\"access-control-allow-headers\", \"authorization, content-type\");\n res.setHeader(\"access-control-max-age\", \"86400\");\n }\n\n private send(res: ServerResponse, status: number, body: unknown): void {\n res.writeHead(status, { \"content-type\": \"application/json\" });\n res.end(supaliveStringify(body));\n }\n}\n\n// ─── helpers ─────────────────────────────────────────────────────────────────\n\nfunction normalizeRpcPath(p: string): string {\n let s = p.startsWith(\"/\") ? p : `/${p}`;\n if (s.length > 1 && s.endsWith(\"/\")) s = s.slice(0, -1);\n return s;\n}\n\nfunction defaultHttpAuth(req: IncomingMessage): Record<string, unknown> | undefined {\n const header = req.headers[\"authorization\"];\n const value = Array.isArray(header) ? header[0] : header;\n if (!value) return undefined;\n const match = /^Bearer\\s+(.+)$/i.exec(value);\n return match ? { token: match[1] } : undefined;\n}\n\nfunction clientIp(req: IncomingMessage): string {\n const xff = req.headers[\"x-forwarded-for\"];\n if (xff) {\n const value = Array.isArray(xff) ? xff[0] : xff;\n const first = value.split(\",\")[0]?.trim();\n if (first) return first;\n }\n return req.socket?.remoteAddress ?? \"unknown\";\n}\n\nfunction readBody(req: IncomingMessage, maxBytes = 1024 * 1024): Promise<string> {\n return new Promise((resolve, reject) => {\n const chunks: Buffer[] = [];\n let size = 0;\n req.on(\"data\", (chunk: Buffer) => {\n size += chunk.length;\n if (size > maxBytes) {\n reject(new Error(\"request body too large\"));\n req.destroy();\n return;\n }\n chunks.push(chunk);\n });\n req.on(\"end\", () => resolve(Buffer.concat(chunks).toString(\"utf8\")));\n req.on(\"error\", reject);\n });\n}\n","import { createServer, IncomingMessage, type ServerResponse, type Server } from \"http\";\nimport { WebSocketServer, WebSocket } from \"ws\";\nimport { v4 as uuidv4 } from \"uuid\";\nimport type { ZodType } from \"zod\";\nimport Redis from \"ioredis\";\nimport { retry, handleAll, ExponentialBackoff } from \"cockatiel\";\nimport { stableStringify, supaliveStringify } from \"../helper\";\nimport {\n type QueryCtx,\n type MutationCtx,\n type ActionCtx,\n ContextRegistry,\n getContextRegistry,\n} from \"../router\";\nimport { sleep, SupaliveDb } from \"../db/realtime_db\";\nimport type { WriteEntry, CommitTs, LiveResult, MutationResult } from \"../db/types_db\";\nimport { AuthenticationError, ClientMessage, ClientMessageSchema, Context, ServerMessage } from \"../db/types_client_rpc\";\nimport {\n type Session,\n type InstanceId,\n type SubId,\n type PendingMutation,\n RemoteSubscriptionMessageSchema,\n RemoteRecomputeRace,\n SubscriptionEntry,\n RemoteSubscriptionMessage,\n} from \"./types_server\";\nimport { type Router } from \"../router/router\";\nimport {\n generateSubscriptionId,\n generateCacheKey,\n getHashOf,\n resolveQueryIdentity,\n} from \"./sub_hash\";\nimport { runCachedQuery, type OneShotQueryDeps } from \"./one-shot-query\";\nimport { RetentionWatermark, RETENTION_WATERMARK_CHANNEL } from \"./retention-watermark\";\nimport type { RegisteredProcedure } from \"../router/router\";\nimport { CacheLayer } from \"../db/cache\";\nimport { SubManagerClient } from \"./subscription-manager-client\";\nimport { SupaliveServerConfig } from \"../config\";\nimport { appInstanceId, logger as baseLogger, logLevelOf } from \"../logger\";\nimport { AffectedSubscription, RegistrationRecord } from \"./subscription-manager-types\";\nimport { bootstrapDbTypes } from \"../db/init_db\";\nimport { JobClient, type JobScheduler, type EnqueueOptions, type ScheduleHandle } from \"../jobs/scheduler\";\nimport type { JobCtx } from \"../router/procedure\";\nimport type { ObjectStorage } from \"../storage\";\nimport { HttpRpcHandler } from \"./http-rpc\";\n\nconst redisRetryPolicy = retry(handleAll, {\n maxAttempts: 3,\n backoff: new ExponentialBackoff({\n initialDelay: 100,\n maxDelay: 5000,\n exponent: 2,\n }),\n});\n\n/**\n * Global broadcast channel used to signal column-type cache invalidation\n * across every running server instance. Publishers (migration tooling,\n * the publishSchemaReload helper below) send a JSON message of shape\n * `{ type: \"schema:reload\"; tables?: string[]; originInstance?: string }`.\n * Subscribers re-bootstrap from PG (or Redis) so the next batched write\n * sees up-to-date column metadata without a process restart.\n */\nconst SCHEMA_RELOAD_CHANNEL = \"supalive:schema:reload\";\n\n/**\n * Per-socket cap on pending mutations. The Nth+1 mutation from a single\n * client gets rejected with TOO_MANY_CONCURRENT_MUTATIONS rather than\n * being enqueued — this is rate-limit pressure on a misbehaving client,\n * not a hard system limit.\n *\n * Mirrors Convex's OPERATION_QUEUE_BUFFER_SIZE (1000). Mutations drain\n * one-at-a-time per socket, so a healthy client never approaches this.\n */\nconst MAX_MUTATION_QUEUE_BUFFER = 1000;\n\n/**\n * Per-socket cap on in-flight actions. Actions run in parallel (unlike\n * mutations) because they may do long external work (HTTP, queue calls);\n * the cap exists so one socket can't open thousands of outbound\n * connections from inside the server.\n */\nconst MAX_ACTION_CONCURRENCY = 1000;\n\n// ─── Heartbeat constants ─────────────────────────────────────\n//\n// Two independent jobs run on a single per-session interval:\n//\n// 1. Dead-TCP detection: terminate sockets that have gone fully silent\n// (no inbound text/ping/pong) past `CLIENT_TIMEOUT_MS`. Otherwise\n// emit a ws-protocol ping (browser auto-pongs, refreshing `session.lastReceivedAt`).\n// 2. Client-watchdog refresh: if the application channel has been idle\n// for at least `HEARTBEAT_INTERVAL_MS`, send a `{type:\"ping\"}` JSON\n// frame so the client's inactivity watchdog stays fresh. Busy\n// sockets skip this (the `lastSentAt` check makes it idle-only).\n//\n// Both fire on the same `heartbeatTimer` — one `setInterval` per session.\nconst HEARTBEAT_INTERVAL_MS = Number(\n process.env.SUPALIVE_HEARTBEAT_INTERVAL_MS ?? 15_000,\n);\nconst CLIENT_TIMEOUT_MS = 120_000;\n\n/**\n * TTL on the Redis lock that elects the recompute winner when the writer\n * doesn't hold the sub locally. Each (subId, commitTs) gets its own lock\n * key, so this only needs to bound the worst-case recompute duration —\n * if the winner crashes mid-recompute, the lock expires and the next\n * mutation triggers a new race.\n */\nconst RECOMPUTE_LOCK_TTL_SECONDS = Number(\n process.env.SUPALIVE_RECOMPUTE_LOCK_TTL_SECONDS ?? 10,\n);\n\nconst logger = baseLogger.child({\n \"name\": \"SUPALIVE\",\n},\n { level: logLevelOf(\"SERVER_LOG_LEVEL\") },\n);\n\n/**\n * Supalive WebSocket Server\n */\nexport class SupaliveWebSocketServer<TContext extends Context> {\n private config: SupaliveServerConfig<TContext>;\n private instanceId: InstanceId;\n\n private httpServer: Server;\n private wss: WebSocketServer;\n\n private cache: CacheLayer;\n private redisSub: Redis;\n /** App-level pub/sub channels this instance listens on (see config.appPubSub). */\n private readonly appPubSubChannels: Set<string>;\n private db: SupaliveDb;\n private contextRegistry!: ContextRegistry;\n private router?: Router<any, TContext>;\n\n // In-memory retention watermark (oldest retained commit-log ts): loaded from the\n // DB at startup and kept fresh by the sub-manager's Redis broadcast. Read on\n // the one-shot query hot path via `oneShotDeps.getMinRetainedTs` (a plain\n // variable, no per-query round trip).\n private retentionWatermark = new RetentionWatermark();\n // Built once — the one-shot query cache deps are static aside from the live\n // watermark, which `getMinRetainedTs` reads by reference. Shared by the WS `call` path\n // and the HTTP RPC handler.\n private oneShotDeps: OneShotQueryDeps<TContext>;\n\n private scheduler: JobScheduler;\n private jobPath: string;\n private jobClient: JobClient;\n private rpcHandler?: HttpRpcHandler<TContext>;\n /** Optional object storage exposed to every procedure as `ctx.storage`. */\n private storage: ObjectStorage;\n\n private sessions: Map<WebSocket, Session<TContext>> = new Map();\n private serverSubscriptions: Map<SubId, SubscriptionEntry> = new Map();\n private subManagerClient!: SubManagerClient;\n\n private stats = {\n activeConnections: 0,\n totalConnections: 0,\n activeSubscriptions: 0,\n };\n\n constructor(config: SupaliveServerConfig<TContext>) {\n this.config = {\n port: 3000,\n host: \"0.0.0.0\",\n contextName: \"default\",\n sessionTtlSeconds: 3600,\n ...config,\n };\n this.instanceId = appInstanceId;\n\n if (!config.subManagerUrl) {\n throw new Error(\"subManagerUrl config is required (WebSocket URL of the sub-manager process)\");\n }\n this.subManagerClient = new SubManagerClient(config.subManagerUrl);\n this.cache = config.cacheLayer;\n this.redisSub = config.redisSubClient;\n this.appPubSubChannels = new Set(config.appPubSub?.channels ?? []);\n this.db = config.database;\n this.storage = config.storage;\n\n // Invalidate subscriptions after EVERY committed mutation, whatever its\n // source — client WS call, action, job, or the internal caller (as long as\n // it shares this db instance). Invalidation is instance-independent (it goes\n // through the central sub-manager), so a server-side write reaches every\n // WS node's subscribers. Fire-and-forget: the client's own response never\n // waits on it, and handleMutationCommit swallows/logs its own errors.\n this.db.setOnAfterCommit((writeSet, commitTs) => {\n this.handleMutationCommit(writeSet, commitTs).catch((err) =>\n logger.error(err, \"post-commit invalidation failed\"),\n );\n });\n\n this.scheduler = config.scheduler;\n this.jobPath = normalizeJobPath(config.jobPath ?? \"/_jobs\");\n this.jobClient = new JobClient(this.scheduler, (name) => this.endpointFor(name));\n\n // Static deps for cached one-shot queries (WS `call` + HTTP RPC). Built once;\n // `getMinRetainedTs` reads the live in-memory watermark by reference.\n this.oneShotDeps = {\n db: this.db,\n cache: this.cache,\n getUserId: config.getUserId,\n getMinRetainedTs: () => this.retentionWatermark.get(),\n };\n\n // Opt-in one-shot HTTP RPC transport (query/mutation/action). Shares this\n // server's db + verifyAuth + procedure registry; subscriptions stay WS-only.\n if (config.rpc) {\n this.rpcHandler = new HttpRpcHandler<TContext>({\n getRegistry: () => this.contextRegistry,\n db: this.db,\n oneShotDeps: this.oneShotDeps,\n verifyAuth: config.verifyAuth,\n rpc: config.rpc,\n storage: this.storage,\n scheduler: this.jobClient,\n });\n }\n\n // Wire the sub-manager client's recovery callbacks to drive re-registration\n // from this app server's serverSubscriptions whenever the RPC reconnects,\n // and to process the resulting batched invalidate flush.\n this.subManagerClient.setRecoveryDriver(this.runSubManagerRecovery.bind(this));\n this.subManagerClient.setAffectedHandler(this.runAffectedHandler.bind(this));\n\n // The HTTP server hosts the WS upgrade AND the job webhook endpoint. WS\n // traffic arrives as `upgrade` events (handled by WebSocketServer below),\n // so this plain `request` handler only ever sees the scheduler's POSTs and\n // health checks — the two never overlap.\n this.httpServer = createServer((req, res) => {\n this.handleHttpRequest(req, res).catch((err) => {\n logger.error(err, \"HTTP request handler error\");\n if (!res.headersSent) {\n res.writeHead(500, { \"content-type\": \"application/json\" });\n res.end(JSON.stringify({ error: \"internal error\" }));\n }\n });\n });\n this.wss = new WebSocketServer({\n server: this.httpServer,\n autoPong: true,\n maxPayload: 100 * 1024 * 1024,\n // backlog: //max pending connections queue (default: 511)\n });\n\n this.setupWebSocketHandlers();\n this.subscribeToInstanceChannel();\n }\n\n /**\n * Per-session heartbeat. Fires every `HEARTBEAT_INTERVAL_MS` and does\n * both jobs on the same tick:\n *\n * 1. Inactivity check + ws-protocol ping (dead-TCP detection).\n * 2. App-level `{type:\"ping\"}` if the channel has been idle for at\n * least one heartbeat interval (client-watchdog refresh).\n *\n * The `lastSentAt` gate on (2) preserves the original \"only ping when\n * idle\" semantics: a session producing sub:updates faster than the\n * heartbeat interval never emits a redundant app-ping.\n */\n private startSessionHeartbeat(session: Session<TContext>): void {\n session.heartbeatTimer = setInterval(() => {\n // (1) Dead-TCP detection.\n if (Date.now() - session.lastReceivedAt > CLIENT_TIMEOUT_MS) {\n logger.info(`ws inactivity → terminate session ${session.id}`);\n try { session.ws.terminate(); } catch { /* close handler cleans up */ }\n return;\n }\n try {\n session.ws.ping();\n } catch {\n // socket already broken — close handler will clean up\n }\n\n // (2) Client-watchdog refresh, idle-only.\n if (Date.now() - session.lastSentAt >= HEARTBEAT_INTERVAL_MS) {\n this.sendMessage(session, { type: \"ping\" });\n }\n }, HEARTBEAT_INTERVAL_MS);\n }\n\n private clearSessionTimers(session: Session<TContext>): void {\n if (session.heartbeatTimer) {\n clearInterval(session.heartbeatTimer);\n session.heartbeatTimer = null;\n }\n }\n\n registerRouter(router: Router<any, TContext>): void {\n this.router = router;\n this.contextRegistry = getContextRegistry(router.contextName);\n }\n\n async start(): Promise<void> {\n // Seed the in-memory retention watermark from the DB before serving; thereafter\n // the sub-manager's Redis broadcast keeps it fresh. Non-fatal on failure —\n // a 0 watermark is safe (nothing pruned → every cache entry is scannable).\n try {\n await this.retentionWatermark.loadFrom(this.db.impl);\n } catch (err) {\n logger.error(err, \"failed to load initial retention watermark\");\n }\n await new Promise<void>((resolve) => {\n this.httpServer.listen(this.config.port, this.config.host, () => {\n logger.info(`Server: ${this.instanceId} listening on ${this.config.host}:${this.config.port}`);\n resolve();\n });\n });\n await this.syncJobSchedules();\n }\n\n /**\n * Reconcile every registered recurring (cron) job with the scheduler. Safe\n * to call more than once. No-op when no scheduler is configured or the router\n * declares no cron jobs. Runs after the HTTP listener is up so the endpoints\n * the scheduler registers are already reachable.\n */\n private async syncJobSchedules(): Promise<void> {\n if (!this.scheduler || !this.contextRegistry) return;\n const jobs = this.contextRegistry\n .getJobProcedures()\n .filter((p) => !!p.cron)\n .map((p) => ({ name: p.name, cron: p.cron! }));\n if (jobs.length === 0) return;\n try {\n await this.scheduler.syncSchedules(jobs, (name) => this.endpointFor(name));\n logger.info(`Synced ${jobs.length} recurring job schedule(s)`);\n } catch (err) {\n logger.error(err, \"Failed to sync job schedules\");\n }\n }\n\n /** Absolute URL the scheduler POSTs to for a given job name. */\n endpointFor(name: string): string {\n return `${this.config.publicUrl}${this.jobPath}/${name}`;\n }\n\n /**\n * Enqueue a precise, one-shot run of a job (e.g. \"expire this record at\n * exactly `expiresAt`\"). Returns a handle whose `id` can be passed to\n * {@link cancelJob}. Requires a configured scheduler.\n */\n async scheduleJob(name: string, body: unknown, opts?: EnqueueOptions): Promise<ScheduleHandle> {\n if (!this.jobClient) throw new Error(\"No scheduler configured; cannot schedule jobs\");\n return this.jobClient.enqueue(name, body, opts);\n }\n\n /** Cancel a one-shot previously created by {@link scheduleJob}. */\n async cancelJob(handleId: string): Promise<void> {\n if (!this.jobClient) throw new Error(\"No scheduler configured; cannot cancel jobs\");\n return this.jobClient.cancel(handleId);\n }\n\n /**\n * The app-facing {@link JobClient} — hand it to your service layer so\n * procedures can schedule/cancel one-shots without reaching for the server.\n */\n getJobClient(): JobClient {\n return this.jobClient;\n }\n\n async stop(): Promise<void> {\n this.wss.close();\n for (const [ws] of this.sessions) {\n ws.close(1001, \"Server shutting down\");\n }\n this.rpcHandler?.stop();\n this.httpServer.close();\n logger.info(\"Server stopped\");\n }\n\n getStats() {\n return {\n instanceId: this.instanceId,\n activeConnections: this.stats.activeConnections,\n totalConnections: this.stats.totalConnections,\n activeSubscriptions: this.stats.activeSubscriptions,\n };\n }\n\n getInstanceId(): string {\n return this.instanceId;\n }\n\n /**\n * Plain-HTTP entrypoint (non-WS). Serves a liveness probe and the job\n * webhook: `POST {jobPath}/<jobName>`. Every job POST is authenticated by the\n * configured scheduler before the (server-only) job handler runs.\n */\n private async handleHttpRequest(req: IncomingMessage, res: ServerResponse): Promise<void> {\n const rawUrl = req.url ?? \"/\";\n const path = rawUrl.split(\"?\")[0]!;\n\n // Liveness probe — cheap, unauthenticated.\n if (req.method === \"GET\" && (path === \"/_health\" || path === `${this.jobPath}/_health`)) {\n return sendJson(res, 200, { status: \"ok\", instanceId: this.instanceId });\n }\n\n // One-shot HTTP RPC (query/mutation/action), if enabled.\n if (this.rpcHandler && this.rpcHandler.handlesPath(path)) {\n return this.rpcHandler.handle(req, res, path);\n }\n\n const jobPrefix = `${this.jobPath}/`;\n if (!path.startsWith(jobPrefix)) {\n return sendJson(res, 404, { error: \"not found\" });\n }\n if (req.method !== \"POST\") {\n return sendJson(res, 405, { error: \"method not allowed\" });\n }\n\n const name = decodeURIComponent(path.slice(jobPrefix.length));\n if (!name || name.includes(\"/\")) {\n return sendJson(res, 404, { error: \"unknown job\" });\n }\n\n if (!this.scheduler) {\n return sendJson(res, 503, { error: \"jobs not enabled\" });\n }\n\n const rawBody = await readRequestBody(req);\n\n // Authenticate the webhook BEFORE any lookup or work — a forged POST must\n // not even learn whether a job name exists.\n let verified = false;\n try {\n verified = await this.scheduler.verify({\n name,\n rawBody,\n headers: lowercaseHeaders(req.headers),\n url: this.endpointFor(name),\n });\n } catch (err) {\n logger.warn(err, `job \"${name}\" verification threw`);\n }\n if (!verified) {\n return sendJson(res, 401, { error: \"invalid signature\" });\n }\n\n const procedure = this.contextRegistry?.getProcedure(name);\n if (!procedure || procedure.type !== \"job\") {\n return sendJson(res, 404, { error: \"unknown job\" });\n }\n\n let input: unknown;\n try {\n const parsed = rawBody.trim().length > 0 ? JSON.parse(rawBody) : {};\n input = (procedure.inputSchema as ZodType).parse(parsed);\n } catch (err) {\n logger.warn(err, `job \"${name}\" received an invalid payload`);\n return sendJson(res, 400, { error: \"invalid payload\" });\n }\n\n try {\n const serverCtx = this.config.jobContext ? await this.config.jobContext() : undefined;\n const fn = procedure.fn as (ctx: JobCtx<TContext>, input: unknown) => Promise<unknown>;\n const result = await fn({ db: this.db, serverCtx }, input);\n logger.info(`job \"${name}\" completed`);\n return sendJson(res, 200, { ok: true, result });\n } catch (err) {\n // Return 500 so the scheduler retries (jobs must be idempotent).\n logger.error(err, `job \"${name}\" failed`);\n return sendJson(res, 500, { error: \"job failed\" });\n }\n }\n\n private setupWebSocketHandlers(): void {\n this.wss.on(\"connection\", (ws, req) => {\n this.handleConnection(ws, req).catch((err) => {\n logger.error(err, \"Connection handler error\")\n });\n });\n }\n\n private async handleConnection(ws: WebSocket, req: IncomingMessage): Promise<void> {\n this.stats.totalConnections++;\n this.stats.activeConnections++;\n\n const session: Session<TContext> = {\n id: uuidv4(),\n ws,\n serverCtx: undefined,\n subIds: new Set(),\n mutationQueue: [],\n mutationInFlight: false,\n actionInFlight: 0,\n lastReceivedAt: Date.now(),\n lastSentAt: Date.now(),\n heartbeatTimer: null,\n };\n logger.info(`New session: ${session.id}`);\n\n this.sessions.set(ws, session);\n this.sendMessage(session, { type: \"connected\", sessionId: session.id });\n\n // Any inbound frame (text, ws-ping, ws-pong) refreshes the liveness\n // clock. This is the only thing keeping `CLIENT_TIMEOUT_MS` from\n // tripping on a silent-but-live client, since the browser auto-pong\n // is the only thing such a client emits.\n ws.on(\"message\", (data) => {\n session.lastReceivedAt = Date.now();\n this.handleMessage(session, data.toString()).catch((err) => {\n logger.error(err, \"Message handler error\");\n });\n });\n ws.on(\"ping\", () => { session.lastReceivedAt = Date.now(); });\n ws.on(\"pong\", () => { session.lastReceivedAt = Date.now(); });\n\n this.startSessionHeartbeat(session);\n\n ws.on(\"close\", (code) => {\n logger.info(`Connection closed: ${session.id}, code: ${code}`);\n this.clearSessionTimers(session);\n this.handleDisconnect(session);\n this.sessions.delete(ws);\n this.stats.activeConnections--;\n });\n\n ws.on(\"error\", (err) => {\n logger.error(err, \"Websocket error\");\n });\n }\n\n private async handleMessage(session: Session<TContext>, message: string): Promise<void> {\n try {\n const rawMsg = JSON.parse(message);\n const parseResult = ClientMessageSchema.safeParse(rawMsg);\n if (!parseResult.success) {\n this.sendError(session, \"\", \"Invalid message format\", \"INVALID_MESSAGE\");\n return;\n }\n await this.processMessage(session, parseResult.data);\n } catch (err) {\n logger.error(err, \"Message handling error\");\n this.sendError(session, \"\", \"Parse error\", \"PARSE_ERROR\");\n }\n }\n\n private async processMessage(session: Session<TContext>, msg: ClientMessage): Promise<void> {\n switch (msg.type) {\n case \"auth\":\n await this.handleAuth(session, msg.data);\n break;\n case \"call\":\n await this.handleCall(session, msg.id, msg.procedure, msg.input);\n break;\n case \"subscribe\":\n await this.handleSubscribe(session, msg.id, msg.procedure, msg.input);\n break;\n case \"unsubscribe\":\n await this.handleUnsubscribe(session, msg.subId);\n break;\n }\n }\n\n private async handleAuth(session: Session<TContext>, data?: Record<string, unknown>): Promise<void> {\n if (!this.config.verifyAuth || !data) {\n this.sendMessage(session, { type: \"auth:success\", userId: \"anonymous\" });\n return;\n }\n\n try {\n const authResult = await this.config.verifyAuth(data, session.id);\n if (!authResult) {\n this.sendMessage(session, { type: \"auth:failed\", message: \"Invalid authentication\" });\n return;\n }\n\n session.serverCtx = authResult;\n this.sendMessage(session, { type: \"auth:success\", userId: authResult.user.userId });\n } catch (e) {\n if (e instanceof AuthenticationError) {\n this.sendMessage(session, {\n type: \"auth:failed\",\n message: \"Invalid authentication: \" + e.message,\n extra: e.extra,\n });\n } else {\n this.sendMessage(session, { type: \"auth:failed\", message: \"Invalid authentication\" });\n }\n }\n }\n\n /**\n * Park a mutation on this session's FIFO queue and resolve when it\n * eventually drains. The drainer is fire-and-forget: it picks up any\n * newly enqueued op as long as one drain loop is running on this\n * session. Callers (handleCall) own the response/error for their own\n * op via the returned promise.\n */\n private enqueueMutation(\n session: Session<TContext>,\n procedure: RegisteredProcedure<TContext>,\n validatedInput: unknown,\n ): Promise<MutationResult<unknown>> {\n return new Promise<MutationResult<unknown>>((resolve, reject) => {\n const pending: PendingMutation<TContext> = {\n procedure,\n validatedInput,\n resolve,\n reject,\n };\n session.mutationQueue.push(pending);\n this.drainMutationQueue(session).catch((err) =>\n logger.error(err, \"drainMutationQueue crashed\"),\n );\n });\n }\n\n /**\n * Drain the per-session mutation queue serially. Only one drain loop\n * runs at a time per session — re-entrant calls bail immediately, the\n * loop already in progress will pick up the new op before exiting.\n */\n private async drainMutationQueue(session: Session<TContext>): Promise<void> {\n if (session.mutationInFlight) return;\n session.mutationInFlight = true;\n try {\n while (session.mutationQueue.length > 0) {\n const op = session.mutationQueue.shift()!;\n try {\n const fn = op.procedure.fn as (\n ctx: MutationCtx<TContext>,\n input: unknown,\n ) => Promise<unknown>;\n const result = await this.db.mutation(async (ctx) => {\n return fn({ db: ctx, serverCtx: session.serverCtx }, op.validatedInput);\n });\n op.resolve(result);\n } catch (err) {\n op.reject(err);\n }\n }\n } finally {\n session.mutationInFlight = false;\n }\n }\n\n private async handleCall(\n session: Session<TContext>,\n id: string,\n procedureName: string,\n input: unknown\n ): Promise<void> {\n if (!this.router) {\n this.sendError(session, id, \"Router not registered\", \"SERVER_ERROR\");\n return;\n }\n\n const procedure = this.contextRegistry.getProcedure(procedureName);\n if (!procedure || procedure.type === \"job\") {\n this.sendError(session, id, `Procedure \"${procedureName}\" not found`, \"NOT_FOUND\");\n return;\n }\n\n try {\n const validatedInput = (procedure.inputSchema as ZodType).parse(input);\n\n if (procedure.type === \"query\") {\n // One-shot query: same cache/identity/freshness model as a live\n // subscription (see runCachedQuery), just without tracking a sub.\n // Load the freshness upper bound here, mirroring handleSubscribe which\n // loads lastSnapshotTs before registering.\n const lastSnapshotTs = await this.db.impl.getLatestSnapshotTimestamp();\n const result = await runCachedQuery(\n this.oneShotDeps,\n procedure,\n procedureName,\n validatedInput,\n session.serverCtx,\n lastSnapshotTs,\n );\n\n this.sendMessage(session, {\n type: \"response\",\n id: id,\n success: true,\n data: result,\n });\n } else if (procedure.type === \"mutation\") {\n if (session.mutationQueue.length >= MAX_MUTATION_QUEUE_BUFFER) {\n this.sendError(\n session,\n id,\n `Too many concurrent mutations on this connection (max ${MAX_MUTATION_QUEUE_BUFFER})`,\n \"TOO_MANY_CONCURRENT_MUTATIONS\",\n );\n return;\n }\n\n const mutationResult = await this.enqueueMutation(session, procedure, validatedInput);\n\n // Invalidation is driven by the db's after-commit hook (wired in the\n // constructor) for every mutation source, so we don't invalidate again\n // here — we just return the result to the caller.\n this.sendMessage(session, {\n type: \"response\",\n id: id,\n success: true,\n data: mutationResult.result,\n });\n } else if (procedure.type === \"action\") {\n if (session.actionInFlight >= MAX_ACTION_CONCURRENCY) {\n this.sendError(\n session,\n id,\n `Too many concurrent actions on this connection (max ${MAX_ACTION_CONCURRENCY})`,\n \"TOO_MANY_CONCURRENT_ACTIONS\",\n );\n return;\n }\n\n session.actionInFlight++;\n let result: unknown;\n try {\n const fn = procedure.fn as (ctx: ActionCtx<TContext>, input: unknown) => Promise<unknown>;\n result = await fn(\n { db: this.db, serverCtx: session.serverCtx, storage: this.storage, scheduler: this.jobClient },\n validatedInput,\n );\n } finally {\n session.actionInFlight--;\n }\n\n this.sendMessage(session, {\n type: \"response\",\n id: id,\n success: true,\n data: result,\n });\n }\n\n\n } catch (err) {\n logger.error(err, `Error executing \"${procedureName}\"`);\n this.sendError(\n session,\n id,\n err instanceof Error ? err.message : \"Execution failed\",\n \"EXECUTION_ERROR\"\n );\n }\n }\n\n private async handleSubscribe(\n session: Session<TContext>,\n id: string,\n procedureName: string,\n input: unknown\n ): Promise<void> {\n if (!this.router) {\n this.sendError(session, id, \"Router not registered\", \"SERVER_ERROR\");\n return;\n }\n\n const procedure = this.contextRegistry.getProcedure(procedureName);\n if (!procedure || procedure.type !== \"query\") {\n this.sendError(session, id, `Procedure \"${procedureName}\" not found or not a query`, \"NOT_FOUND\");\n return;\n }\n\n try {\n const validatedInput = procedure.inputSchema.parse(input);\n const queryIdentity = resolveQueryIdentity(procedure, session.serverCtx, validatedInput, this.config.getUserId);\n const inputJsonStable = stableStringify(validatedInput);\n const subId = await generateSubscriptionId(procedureName, inputJsonStable, queryIdentity);\n const cacheKey = await generateCacheKey(procedureName, inputJsonStable, queryIdentity);\n const existingServerSub = this.serverSubscriptions.get(subId);\n\n let data: unknown;\n let dataHash: string;\n let liveResult: LiveResult<unknown> | null = null;\n\n // track this session subscribed this subscription\n session.subIds.add(subId);\n\n if (existingServerSub) {\n existingServerSub.subscribedTimes += 1;\n\n const cached = await this.cache.getQueryCacheFor(existingServerSub.cacheKey);\n if (cached) {\n data = cached.data;\n } else {\n liveResult = await this.executeLiveQuery(procedureName, validatedInput, session.serverCtx);\n data = liveResult.data;\n }\n } else {\n const lastSnapshotTs = await this.db.impl.getLatestSnapshotTimestamp();\n const subManagerResult = await this.subManagerClient.registerSubscription({\n subId,\n cacheKey,\n queryName: procedureName,\n args: validatedInput,\n queryIdentity,\n instanceName: this.instanceId,\n lastSnapshotTs: lastSnapshotTs,\n });\n\n const cached = await this.cache.getQueryCacheFor(subManagerResult.cacheKey);\n // Serve the cached blob only when the sub-manager says it's fresh AND its\n // version matches the metadata that freshness was validated against — a\n // divergent data/metadata pair (torn read, cross-process write race) is\n // treated as a miss and recomputed rather than served stale.\n if (cached && !subManagerResult.recompute && cached.version === subManagerResult.version) {\n data = cached.data;\n } else {\n liveResult = await this.executeLiveQuery(procedureName, validatedInput, session.serverCtx);\n data = liveResult.data;\n }\n\n this.serverSubscriptions.set(subId, {\n subId,\n cacheKey,\n subscribedTimes: 1,\n });\n this.stats.activeSubscriptions++;\n }\n\n if (liveResult) {\n const [updateResult] = await Promise.all([\n this.subManagerClient.updateSubscriptionReadSet({\n cacheKey,\n subId,\n lastSnapshotTs: liveResult.ts,\n readSet: liveResult.readSet,\n instanceName: this.instanceId,\n }),\n // version == the compute ts; the sub-worker writes the matching\n // metadata version via updateSubscriptionReadSet above.\n this.cache.setQueryCacheFor(cacheKey, data, String(liveResult.ts)),\n ]);\n\n // If the sub-manager just told us this subId isn't tracked (we hit\n // a window between its register and a separate restart), our register\n // above should have rebuilt the state — log loudly if not.\n if (!updateResult.tracked) {\n logger.warn(\n `updateSubscriptionReadSet returned tracked:false immediately after register for ${subId} ` +\n `— possible sub-manager restart mid-subscribe`,\n );\n }\n }\n\n dataHash = await getHashOf(data);\n\n // Hand the canonical subId back to the client so it doesn't need to\n // recompute the hash locally (which would require pulling in wasm and\n // wouldn't reflect per-procedure `queryIdentity` overrides).\n this.sendMessage(session, { type: \"response\", id, success: true, subId });\n this.sendMessage(session, { type: \"sub:update\", subId, data, dataHash });\n\n logger.info(`Subscribed: ${subId} for session ${session.id}`);\n } catch (err) {\n logger.error(err, `Error subscribing to \"${procedureName}\"`);\n this.sendError(session, id, err instanceof Error ? err.message : \"Subscribe failed\", \"SUBSCRIBE_ERROR\");\n }\n }\n\n private async handleUnsubscribe(session: Session<TContext>, subId: string): Promise<void> {\n const serverSub = this.serverSubscriptions.get(subId);\n if (!serverSub) {\n session.subIds.delete(subId);\n return;\n }\n\n serverSub.subscribedTimes -= 1;\n if (serverSub.subscribedTimes <= 0) {\n session.subIds.delete(subId);\n this.serverSubscriptions.delete(subId);\n this.stats.activeSubscriptions--;\n\n try {\n await this.subManagerClient.unregisterSubscription({ subId, instanceName: this.instanceId });\n } catch (err) {\n logger.error(err, \"Failed to unregister from sub-manager:\");\n }\n }\n\n logger.info(`Unsubscribed: ${subId} for session ${session.id}, remaining server subs: ${serverSub.subscribedTimes}`);\n }\n\n private async handleDisconnect(session: Session<TContext>): Promise<void> {\n logger.info(`Session disconnected: ${session.id}`);\n\n let unregisterSubs: SubscriptionEntry[] | undefined = undefined;\n for (const subId of session.subIds) {\n const sub = this.serverSubscriptions.get(subId);\n if (!sub) continue;\n\n sub.subscribedTimes -= 1;\n if (sub.subscribedTimes <= 0) {\n if (!unregisterSubs) {\n unregisterSubs = [];\n }\n\n unregisterSubs.push(sub);\n }\n }\n\n if (!unregisterSubs || unregisterSubs.length === 0) return;\n\n try {\n await this.subManagerClient.unregisterSubscriptions(\n unregisterSubs.map((sub) => ({ subId: sub.subId, instanceName: this.instanceId })),\n );\n } catch (err) {\n logger.error(err, \"Failed to unregister from sub-manager:\");\n }\n\n for (const sub of unregisterSubs) {\n // if still not subscribing\n if (sub.subscribedTimes <= 0) {\n this.serverSubscriptions.delete(sub.subId);\n this.stats.activeSubscriptions--;\n }\n }\n\n logger.info(`All Subscription sessions is disconnected: ${session.id}, subs: ${unregisterSubs.length}`);\n }\n\n /**\n * Drives recovery on a sub-manager reconnect. Iterates this app server's\n * `serverSubscriptions`, reads each subId's registration record from\n * Redis (`sl:reg:<subId>`), and re-registers it with the sub-manager.\n *\n * • If the Redis record is missing, another app-server instance fully\n * unregistered the sub during our disconnect. We drop it locally and\n * send `sub:gone` to every session that was holding it.\n * • If the sub-manager reports `recompute: true` (cache-metadata + commit\n * log replay detected staleness), we kick a reexec for that sub.\n *\n * Returns after all subs have been processed. Only then does the\n * SubManagerClient flush its buffered invalidate writeSets.\n */\n private async runSubManagerRecovery(): Promise<void> {\n const snapshot = Array.from(this.serverSubscriptions.values());\n if (snapshot.length === 0) {\n logger.info(\"Sub-manager recovery: no subscriptions to restore\");\n return;\n }\n\n logger.info(`Sub-manager recovery: restoring ${snapshot.length} subscription(s)`);\n\n const lastSnapshotTs = await this.db.impl.getLatestSnapshotTimestamp();\n const subIds = snapshot.map((e) => e.subId);\n const recordsMap = await this.cache.fetchRegistrationRecordsBatch(subIds);\n\n const registerParams: Array<{\n entry: typeof snapshot[0];\n record: RegistrationRecord;\n }> = [];\n for (const entry of snapshot) {\n const record = recordsMap.get(entry.subId) ?? null;\n if (!record) {\n this.dropSubscriptionLocally(entry.subId, \"registration not found after recovery\");\n continue;\n }\n registerParams.push({ entry, record });\n }\n\n const recompute: AffectedSubscription[] = [];\n const BATCH_SIZE = 50;\n for (let i = 0; i < registerParams.length; i += BATCH_SIZE) {\n const batch = registerParams.slice(i, i + BATCH_SIZE);\n const params = batch.map(({ entry, record }) => ({\n subId: entry.subId,\n cacheKey: entry.cacheKey,\n queryName: record.queryName,\n args: record.args,\n queryIdentity: record.queryIdentity,\n instanceName: this.instanceId,\n lastSnapshotTs,\n }));\n try {\n const results = await this.subManagerClient.registerSubscriptionBatch(params);\n for (let j = 0; j < batch.length; j++) {\n const { entry, record } = batch[j]!;\n const result = results[j]!;\n if (result.recompute) {\n recompute.push({\n subId: entry.subId,\n queryName: record.queryName,\n args: record.args,\n cacheKey: entry.cacheKey,\n notifyInstances: [],\n });\n }\n }\n } catch (err) {\n logger.error(err, `Sub-manager recovery: failed to batch register ${i}-${i + batch.length}`);\n }\n }\n\n await Promise.allSettled(recompute.map((a) => this.reexecuteAffectedQuery(a)));\n\n logger.info(`Sub-manager recovery: done (${recompute.length} reexec'd)`);\n }\n\n private async runAffectedHandler(affected: AffectedSubscription[]) {\n await Promise.allSettled(\n affected.map((a) => this.reexecuteAffectedQuery(a)),\n );\n }\n\n /**\n * Removes a subscription from this app server's state and notifies every\n * session that was holding it. Used during recovery when the central\n * registration record is gone, or after a `tracked: false` reply from the\n * sub-manager that we can't reconcile.\n */\n private dropSubscriptionLocally(subId: string, reason: string): void {\n const serverSub = this.serverSubscriptions.get(subId);\n let notified = 0;\n if (serverSub) {\n this.serverSubscriptions.delete(subId);\n this.stats.activeSubscriptions = Math.max(0, this.stats.activeSubscriptions - 1);\n\n for (const [, session] of this.sessions) {\n if (session.subIds.has(subId)) {\n notified++;\n session.subIds.delete(subId);\n this.sendMessage(session, { type: \"sub:gone\", subId, reason });\n }\n }\n }\n\n logger.warn(`Dropped subscription ${subId} locally (${reason}); notified ${notified} session(s)`);\n }\n\n private async handleMutationCommit(writeSet: WriteEntry[], commitTs: CommitTs): Promise<void> {\n if (writeSet.length === 0) return;\n\n const tables = [...new Set(writeSet.map((w) => w.table))];\n logger.debug(`Mutation committed for tables: [${tables.join(\", \")}]`);\n\n try {\n const result = await this.subManagerClient.invalidateWriteset({ writeSet });\n logger.debug({ affected: result }, \"Sub-managers invalidation result\");\n\n if (result?.affected?.length > 0) {\n await Promise.allSettled(\n result.affected.map((affected) => this.dispatchAffected(affected, commitTs)),\n );\n }\n } catch (err) {\n logger.error(err, \"Failed to notify sub-managers\");\n }\n }\n\n /**\n * Route an affected sub to whichever instance can actually run the\n * recompute:\n * • If this instance has a local session for the sub, recompute here\n * (fast path) and broadcast the result to every other instance.\n * • Otherwise broadcast a `recompute-race` to every instance in\n * `notifyInstances`. Each receiver tries to acquire a Redis lock\n * keyed by (subId, commitTs); the winner recomputes and publishes\n * an `update` via the normal broadcast path, losers no-op.\n *\n * If `notifyInstances` is empty (the sub has no live holders), we drop\n * the message — the next subscribe will recompute fresh state.\n */\n private async dispatchAffected(affected: AffectedSubscription, commitTs: CommitTs): Promise<void> {\n if (this.hasLocalSessionFor(affected.subId)) {\n await this.reexecuteAffectedQuery(affected);\n return;\n }\n\n if (affected.notifyInstances.length === 0) {\n logger.debug(\n `dispatchAffected: no live instances hold ${affected.subId} — dropping`,\n );\n return;\n }\n\n await this.publishRecomputeRace(affected, commitTs);\n }\n\n private hasLocalSessionFor(subId: SubId): boolean {\n for (const [, session] of this.sessions) {\n if (session.subIds.has(subId)) return true;\n }\n return false;\n }\n\n private async publishRecomputeRace(\n affected: AffectedSubscription,\n commitTs: CommitTs,\n ): Promise<void> {\n const payload = supaliveStringify({\n type: \"recompute-race\",\n subId: affected.subId,\n cacheKey: affected.cacheKey,\n queryName: affected.queryName,\n args: affected.args,\n notifyInstances: affected.notifyInstances,\n commitTs: commitTs.toString(),\n originInstance: this.instanceId,\n });\n\n const publishPromises = affected.notifyInstances.map(async (instance) => {\n if (instance === this.instanceId) return;\n\n const channel = `sub:instance:${instance}`;\n try {\n await redisRetryPolicy.execute(() => this.cache.redis.publish(channel, payload));\n } catch (err) {\n logger.error(\n err,\n `Failed to publish recompute-race to ${channel} after many retries`,\n );\n }\n });\n\n await Promise.all(publishPromises);\n }\n\n private async reexecuteAffectedQuery(affected: AffectedSubscription): Promise<void> {\n let foundSession: Session<TContext> | null = null;\n\n for (const [, session] of this.sessions) {\n if (session.subIds.has(affected.subId)) {\n foundSession = session;\n break;\n }\n }\n\n if (!foundSession) {\n // Should only happen if every local session unsubscribed between\n // dispatchAffected's check and now. dispatchAffected forwards to\n // another instance when no local session exists, so we'd lose the\n // update for everyone in this rare race. Accept the gap.\n logger.debug(`Session not found for subId: ${affected.subId}`);\n return;\n }\n\n try {\n const liveResult = await this.executeLiveQuery(affected.queryName, affected.args, foundSession.serverCtx);\n const dataHash = await getHashOf(liveResult.data);\n\n const [updateResult] = await Promise.all([\n this.subManagerClient.updateSubscriptionReadSet({\n cacheKey: affected.cacheKey,\n subId: affected.subId,\n lastSnapshotTs: liveResult.ts,\n readSet: liveResult.readSet,\n instanceName: this.instanceId,\n }),\n this.cache.setQueryCacheFor(affected.cacheKey, liveResult.data, String(liveResult.ts)),\n ]);\n\n // `tracked: false` means the sub-manager has no record of this subId\n // (it restarted before we recovered, or a concurrent unregister wiped\n // it). Drop the sub locally and let clients re-subscribe.\n if (!updateResult.tracked) {\n this.dropSubscriptionLocally(affected.subId, \"no longer tracked by sub-manager\");\n return;\n }\n\n // find any session with same subId and send update\n for (const [, session] of this.sessions) {\n if (session.subIds.has(affected.subId)) {\n this.sendMessage(session, {\n type: \"sub:update\",\n subId: affected.subId,\n data: liveResult.data,\n dataHash,\n });\n }\n }\n\n if (affected.notifyInstances.length > 0) {\n await this.publishSubscriptionUpdate(\n affected.subId,\n liveResult.data,\n dataHash,\n affected.notifyInstances\n );\n }\n\n logger.debug(`Re-executed affected query for subId: ${affected.subId}`);\n } catch (err) {\n logger.error(err, `Failed to re-execute query for subId ${affected.subId}`);\n }\n }\n\n private async executeLiveQuery(\n procedureName: string,\n input: unknown,\n serverCtx?: TContext\n ): Promise<LiveResult<unknown>> {\n const procedure = this.contextRegistry.getProcedure(procedureName);\n if (!procedure || procedure.type !== \"query\") {\n throw new Error(`Procedure not defined: ${procedureName}`);\n }\n\n return this.db.liveQuery(async (ctx) => {\n const fn = procedure.fn as (ctx: QueryCtx<TContext>, input: unknown) => Promise<unknown>;\n return fn({ db: ctx, serverCtx }, input);\n });\n }\n\n private sendMessage(session: Session<TContext>, message: ServerMessage): void {\n if (session.ws.readyState !== WebSocket.OPEN) return;\n session.ws.send(supaliveStringify(message));\n // Mark the channel as having had recent outbound traffic. The\n // heartbeat tick uses this to skip the idle-only app-level ping\n // for busy sessions (no point pinging a client that just got a\n // sub:update).\n session.lastSentAt = Date.now();\n }\n\n private sendMessageRaw(session: Session<TContext>, message: string): void {\n if (session.ws.readyState === WebSocket.OPEN) {\n session.ws.send(message);\n }\n }\n\n private sendError(session: Session<TContext>, id: string, error: string, code: string): void {\n this.sendMessage(session, { type: \"response\", id, success: false, error, code });\n }\n\n private async subscribeToInstanceChannel(): Promise<void> {\n const instanceChannel = `sub:instance:${this.instanceId}`;\n const schemaChannel = SCHEMA_RELOAD_CHANNEL;\n const watermarkChannel = RETENTION_WATERMARK_CHANNEL;\n\n // App-level channels (config.appPubSub) ride this SAME subscribe connection —\n // no extra Redis connection per instance.\n const appChannels = [...this.appPubSubChannels];\n await this.redisSub.subscribe(instanceChannel, schemaChannel, watermarkChannel, ...appChannels);\n this.redisSub.on(\"connect\", () => {\n logger.info(\n `Subscribed to Redis channels: ${[instanceChannel, schemaChannel, watermarkChannel, ...appChannels].join(\", \")}`,\n );\n });\n this.redisSub.on(\"message\", (channel: string, message: string) => {\n if (channel === instanceChannel) {\n this.handleRemoteInstanceMessage(message).catch((err) => {\n logger.error(err, \"Error handling remote instance message\");\n });\n } else if (channel === schemaChannel) {\n this.handleSchemaReload().catch((err) => {\n logger.error(err, \"Error handling schema reload\");\n });\n } else if (channel === watermarkChannel) {\n // Sub-manager advertised a new retention watermark after a prune. Adopt it\n // (monotonic — a stale/out-of-order message can't lower it).\n try {\n this.retentionWatermark.set(BigInt(message));\n } catch (err) {\n logger.error(err, `Bad retention watermark message: ${message}`);\n }\n } else if (this.appPubSubChannels.has(channel)) {\n // App-level message (auth revocations, etc.). The handler owns decoding;\n // we just guard against it throwing.\n Promise.resolve(this.config.appPubSub?.onMessage(channel, message)).catch((err) =>\n logger.error(err, `Error in appPubSub handler for channel ${channel}`),\n );\n }\n });\n this.redisSub.on(\"error\", (err: Error) => {\n logger.error(err, `Subscriber error for channels: ${instanceChannel}, ${schemaChannel}`)\n });\n }\n\n /**\n * React to a `schema:reload` broadcast: invalidate the in-memory column\n * type cache for the named tables (or all if omitted) and re-bootstrap\n * from Postgres so subsequent batched writes see the fresh types. Skips\n * messages published by this instance (we already have the new types).\n */\n private async handleSchemaReload(): Promise<void> {\n logger.info(`Schema reload request received in instance ${this.instanceId} — reloading schema...`);\n try {\n await sleep(1000 + (Math.random() * 5000)); // jitter to avoid thundering herd if many instances\n await bootstrapDbTypes(this.db, this.cache, true);\n logger.info(`Schema reload applied in instance ${this.instanceId}`);\n } catch (err) {\n logger.error(err, \"Failed to reload schema\");\n return;\n }\n }\n\n /**\n * Broadcast a schema-reload signal to every server instance (including\n * this one — the handler skips self-originated messages, so calling\n * after a local migration is safe). Pass `tables` to scope the reload;\n * omit to flush everyone's cache.\n */\n async publishSchemaReload(tables?: string[]): Promise<void> {\n const payload = JSON.stringify({\n type: \"schema:reload\",\n tables,\n originInstance: this.instanceId,\n });\n await redisRetryPolicy.execute(() =>\n this.cache.redis.publish(SCHEMA_RELOAD_CHANNEL, payload));\n }\n\n /**\n * Publish an app-level message (see {@link SupaliveServerConfig.appPubSub}) to\n * a Redis channel. Rides the shared command connection and is delivered to\n * every instance subscribed to `channel`, INCLUDING this one — so a handler\n * should tolerate/skip self-originated messages if that matters. The message\n * is an opaque string; the app owns encoding. Uses the standard Redis retry\n * policy; callers typically fire-and-forget.\n */\n async publishAppMessage(channel: string, message: string): Promise<void> {\n await redisRetryPolicy.execute(() => this.cache.redis.publish(channel, message));\n }\n\n private async handleRemoteInstanceMessage(message: string): Promise<void> {\n let parsed: RemoteSubscriptionMessage;\n try {\n parsed = RemoteSubscriptionMessageSchema.parse(JSON.parse(message));\n } catch (err) {\n logger.error(err, \"Failed to parse remote instance message\");\n return;\n }\n\n if (parsed.originInstance === this.instanceId) return;\n\n if (parsed.type === \"update\") {\n for (const [, session] of this.sessions) {\n if (!session.subIds.has(parsed.subId)) continue;\n\n const subEntry = this.serverSubscriptions.get(parsed.subId);\n if (!subEntry) continue;\n\n this.sendMessage(session, {\n type: \"sub:update\",\n subId: parsed.subId,\n data: parsed.data,\n dataHash: parsed.dataHash,\n });\n }\n return;\n }\n\n // type === \"recompute-race\" — writer didn't hold the sub locally and\n // broadcast a race hint to every notifyInstance. The first to acquire\n // the lock recomputes; the rest no-op.\n await this.handleRemoteRecomputeRace(parsed);\n }\n\n private async handleRemoteRecomputeRace(req: RemoteRecomputeRace): Promise<void> {\n if (!this.hasLocalSessionFor(req.subId)) {\n // We're in notifyInstances per sub-manager but the sub has since\n // been dropped here. Another holder may still pick it up — no warn.\n return;\n }\n\n const won = await this.cache.tryAcquireRecomputeLock(\n this.instanceId, req.subId, req.commitTs, RECOMPUTE_LOCK_TTL_SECONDS,\n );\n if (!won) {\n logger.debug(\n `recompute-race: lost lock for ${req.subId}@${req.commitTs}, waiting for winner's update`,\n );\n return;\n }\n\n await this.reexecuteAffectedQuery({\n subId: req.subId,\n queryName: req.queryName,\n args: req.args,\n cacheKey: req.cacheKey,\n notifyInstances: req.notifyInstances,\n });\n }\n\n private async publishSubscriptionUpdate(\n subId: string,\n data: unknown,\n dataHash: string,\n notifyInstances: string[]\n ): Promise<void> {\n const payload = supaliveStringify({\n type: \"update\",\n subId,\n data,\n dataHash,\n originInstance: this.instanceId,\n });\n\n const publishPromises = notifyInstances.map(async (instance) => {\n if (instance === this.instanceId) return;\n\n const channel = `sub:instance:${instance}`;\n try {\n await redisRetryPolicy.execute(() => this.cache.redis.publish(channel, payload));\n } catch (err) {\n logger.error(err, `Failed to publish to ${channel} after many retries`);\n }\n });\n\n await Promise.all(publishPromises);\n }\n}\n\n// ─── HTTP helpers (job endpoint) ─────────────────────────────────────────────\n\n/** Normalize a job path to a leading-slash, no-trailing-slash prefix. */\nfunction normalizeJobPath(p: string): string {\n let s = p.startsWith(\"/\") ? p : `/${p}`;\n if (s.length > 1 && s.endsWith(\"/\")) s = s.slice(0, -1);\n return s;\n}\n\nfunction sendJson(res: ServerResponse, status: number, body: unknown): void {\n const payload = supaliveStringify(body);\n res.writeHead(status, { \"content-type\": \"application/json\" });\n res.end(payload);\n}\n\nfunction lowercaseHeaders(headers: IncomingMessage[\"headers\"]): Record<string, string | undefined> {\n const out: Record<string, string | undefined> = {};\n for (const [k, v] of Object.entries(headers)) {\n out[k.toLowerCase()] = Array.isArray(v) ? v.join(\",\") : v;\n }\n return out;\n}\n\n/** Buffer an incoming request body to a string, capped to guard against abuse. */\nfunction readRequestBody(req: IncomingMessage, maxBytes = 1024 * 1024): Promise<string> {\n return new Promise((resolve, reject) => {\n const chunks: Buffer[] = [];\n let size = 0;\n req.on(\"data\", (chunk: Buffer) => {\n size += chunk.length;\n if (size > maxBytes) {\n reject(new Error(\"request body too large\"));\n req.destroy();\n return;\n }\n chunks.push(chunk);\n });\n req.on(\"end\", () => resolve(Buffer.concat(chunks).toString(\"utf8\")));\n req.on(\"error\", reject);\n });\n}\n","/**\n * FNV-1a 32-bit hash.\n *\n * Used for routing subscription operations to logical workers inside the\n * sub-manager process. Same input always returns the same 32-bit unsigned\n * integer in both JavaScript and Go, so a future Go port of the sub-manager\n * can use the identical routing without rebalancing keys.\n *\n * Reference: http://www.isthe.com/chongo/tech/comp/fnv/\n */\nexport function fnv1a32(s: string): number {\n let h = 0x811c9dc5;\n for (let i = 0; i < s.length; i++) {\n h ^= s.charCodeAt(i);\n // h = h * 16777619, expressed as shifts to stay in 32-bit space\n h = (h + ((h << 1) + (h << 4) + (h << 7) + (h << 8) + (h << 24))) >>> 0;\n }\n return h >>> 0;\n}\n","import { Server as WebSocketServer } from \"rpc-websockets\";\nimport { Worker } from \"node:worker_threads\";\nimport { fileURLToPath } from \"node:url\";\nimport { SubscriptionManagerConfig } from \"../config\";\nimport { fnv1a32 } from \"./hash\";\nimport { subscriptionManagerDataPack } from \"./subscription-manager-client\";\n\nimport { logger as baseLogger, logLevelOf } from \"../logger\";\nimport {\n AffectedSubscription,\n InvalidateWritesetResult,\n RegisterSubscriptionBatchResult,\n RegisterSubscriptionParams,\n RegisterSubscriptionResult,\n UnregisterSubscriptionParams,\n UnregisterSubscriptionResult,\n UnregisterSubscriptionsParams,\n UnregisterSubscriptionsResult,\n UpdateSubscriptionReadSetResult,\n} from \"./subscription-manager-types\";\nimport { groupByToMap } from \"../helper\";\nimport {\n MsgType,\n type WorkerInitPayload,\n type WorkerRequest,\n type WorkerResponse,\n} from \"./subscription-manager-worker-protocol\";\n\n// Re-export SubscriptionWorker so existing test imports\n// (`import { SubscriptionWorker } from \"./subscription-manager\"`) keep working.\nexport { SubscriptionWorker } from \"./subscription-worker\";\n\nconst logger = baseLogger.child(\n { name: \"SUBS_MGR\" },\n { level: logLevelOf(\"SUBS_MANAGER_SERVER_LOG_LEVEL\") },\n);\n\n// Resolve the entry script next to this compiled file. `import.meta.url` works\n// for both Bun (running TS source) and Node (running emitted JS) — we just\n// swap the extension based on the current module's path.\nfunction resolveWorkerEntry(): { url: URL; isTs: boolean } {\n const here = new URL(import.meta.url);\n const isTs = here.pathname.endsWith(\".ts\");\n const ext = isTs ? \".ts\" : \".js\";\n return {\n url: new URL(`./subscription-manager-worker-entry${ext}`, import.meta.url),\n isTs,\n };\n}\n\n// When the entry is a `.ts` file and we're running on Node (not Bun), the\n// worker thread inherits no module loader — Node can strip types but its bare\n// ESM resolver requires explicit file extensions, which our extensionless\n// internal imports don't satisfy. Register tsx as the worker loader so it\n// resolves the same way the parent module does.\nfunction workerExecArgv(isTs: boolean): string[] | undefined {\n if (!isTs) return undefined;\n if (typeof (globalThis as any).Bun !== \"undefined\") return undefined;\n return [\"--import\", \"tsx\"];\n}\n\n// ─── Worker-thread proxy ─────────────────────────────────────────────────────\n\ninterface PendingRequest {\n resolve: (value: unknown) => void;\n reject: (err: Error) => void;\n}\n\nclass SubscriptionWorkerHandle {\n readonly id: number;\n private worker: Worker;\n private pending = new Map<number, PendingRequest>();\n private nextReqId = 1;\n private readyPromise: Promise<void>;\n\n constructor(id: number, init: WorkerInitPayload) {\n this.id = id;\n const entry = resolveWorkerEntry();\n this.worker = new Worker(fileURLToPath(entry.url), {\n execArgv: workerExecArgv(entry.isTs),\n });\n\n this.worker.on(\"message\", (res: WorkerResponse) => {\n const pending = this.pending.get(res.reqId);\n if (!pending) return;\n\n this.pending.delete(res.reqId);\n if (res.ok) {\n pending.resolve(res.result);\n } else {\n const err = new Error(res.error.message);\n if (res.error.stack) err.stack = res.error.stack;\n pending.reject(err);\n }\n });\n\n this.worker.on(\"error\", (err: unknown) => {\n logger.error(err, `[worker ${this.id}] worker thread errored`);\n const e = err instanceof Error ? err : new Error(String(err));\n for (const [, p] of this.pending) p.reject(e);\n this.pending.clear();\n });\n\n this.readyPromise = this.send<null>(MsgType.Init, init).then(() => { });\n }\n\n ready(): Promise<void> {\n return this.readyPromise;\n }\n\n send<T>(type: MsgType, payload: unknown): Promise<T> {\n const reqId = this.nextReqId++;\n return new Promise<T>((resolve, reject) => {\n this.pending.set(reqId, {\n resolve: resolve as (v: unknown) => void,\n reject,\n });\n const req: WorkerRequest = { type, reqId, payload };\n this.worker.postMessage(req);\n });\n }\n\n async shutdown(): Promise<void> {\n try {\n await this.send<null>(MsgType.Shutdown, null);\n } catch (_) { /* ignore */ }\n await this.worker.terminate();\n }\n}\n\n// ─── Dispatcher ──────────────────────────────────────────────────────────────\n\n/**\n * One process per deployment. Routes each subscription operation to one of N\n * worker threads by hashing subId. Worker threads run in parallel on separate\n * CPU cores; this dispatcher does no Zod parsing — it reads `subId` directly\n * from the decoded payload (the internal channel is trusted) and forwards the\n * raw payload. The worker validates and parses with Zod before executing.\n */\nexport class SubscriptionManager {\n private wss: WebSocketServer;\n private port: number;\n private workers: SubscriptionWorkerHandle[];\n private pruneInterval: ReturnType<typeof setInterval> | null = null;\n private static readonly PRUNE_INTERVAL_MS = 7 * 24 * 60 * 60 * 1000;\n\n constructor(config: SubscriptionManagerConfig) {\n this.port = config.port;\n this.wss = new WebSocketServer({ port: this.port }, subscriptionManagerDataPack);\n this.wss.dataPack = subscriptionManagerDataPack;\n\n const numWorkers = resolveWorkerCount(config.workers);\n const maxConnectionsPerWorker = resolveMaxConnectionsPerWorker(config, numWorkers);\n\n logger.info(`Spawning ${numWorkers} subscription worker${numWorkers === 1 ? \"\" : \"s\"}`);\n this.workers = Array.from({ length: numWorkers }, (_, i) =>\n new SubscriptionWorkerHandle(i + 1, {\n workerId: i + 1,\n database: config.database,\n upstash: config.upstash,\n cacheTTLSeconds: config.cacheTTLSeconds,\n maxConnectionsPerWorker,\n }),\n );\n\n this.registerMethods();\n }\n\n async start(): Promise<void> {\n await Promise.all(this.workers.map((w) => w.ready()));\n logger.info(`Listening on port ${this.port}`);\n // Seed the retention watermark before serving so the query-cache freshness\n // check has an accurate watermark immediately (it survives restarts in the\n // metadata table; only pruning moves it afterward). Worker 0 seeds/repairs\n // it from the DB and returns it; fan it out to every worker's in-memory copy\n // (and, via Redis, to app-server instances).\n try {\n const minRetainedTs = await this.workers[0]!.send<bigint>(MsgType.RefreshRetentionWatermark, null);\n await this.broadcastRetentionWatermark(minRetainedTs);\n } catch (err) {\n logger.error(err, \"initial retention watermark seed failed\");\n }\n this.pruneInterval = setInterval(\n () => { this.pruneCommitLogs().catch((err) => logger.error(err, \"commit_logs prune failed\")); },\n SubscriptionManager.PRUNE_INTERVAL_MS,\n );\n if (this.pruneInterval && typeof this.pruneInterval.unref === \"function\") {\n this.pruneInterval.unref();\n }\n }\n\n async stop(): Promise<void> {\n if (this.pruneInterval) {\n clearInterval(this.pruneInterval);\n this.pruneInterval = null;\n }\n this.wss.close();\n await Promise.all(this.workers.map((w) => w.shutdown()));\n logger.info(\"Stopped\");\n }\n\n private async pruneCommitLogs(): Promise<void> {\n const minPerWorker = await Promise.all(\n this.workers.map((w) => w.send<bigint | null>(MsgType.MinSubscriptionTs, null)),\n );\n let minRetainedTs: bigint | null = null;\n for (const m of minPerWorker) {\n if (m === null) continue;\n if (minRetainedTs === null || m < minRetainedTs) minRetainedTs = m;\n }\n\n if (minRetainedTs === null) {\n const latest = await this.workers[0]!.send<bigint>(MsgType.LatestSnapshotTs, null);\n if (latest <= 0n) return;\n minRetainedTs = latest;\n }\n\n // Worker 0 advertises the new watermark (persist + Redis publish) then\n // deletes. Fan the same watermark out to the other workers' in-memory copies.\n const deleted = await this.workers[0]!.send<bigint>(MsgType.Prune, minRetainedTs);\n await this.broadcastRetentionWatermark(minRetainedTs);\n if (deleted > 0n) {\n logger.info(`pruned ${deleted} commit_logs older than ts=${minRetainedTs}`);\n }\n }\n\n /** Fan a watermark value out to every worker's in-memory copy. Worker 0 (the\n * pruner/seeder) already has it, but re-sending is harmless — {@link\n * SubscriptionWorker.setRetentionWatermark} is monotonic. */\n private async broadcastRetentionWatermark(ts: bigint): Promise<void> {\n await Promise.all(\n this.workers.map((w) =>\n w.send(MsgType.SetRetentionWatermark, ts).catch((err) =>\n logger.error(err, \"retention watermark broadcast to worker failed\"),\n ),\n ),\n );\n }\n\n workerCount(): number {\n return this.workers.length;\n }\n\n workerIndexFor(subId: string): number {\n return fnv1a32(subId) % this.workers.length;\n }\n\n private workerFor(subId: string): SubscriptionWorkerHandle {\n return this.workers[this.workerIndexFor(subId)]!;\n }\n\n private registerMethods(): void {\n this.wss.register(\"registerSubscription\", this.registerSubscription.bind(this));\n this.wss.register(\"registerSubscriptionBatch\", this.registerSubscriptionBatch.bind(this));\n this.wss.register(\"updateSubscriptionReadSet\", this.updateSubscriptionReadSet.bind(this));\n this.wss.register(\"invalidateWriteset\", this.invalidateWriteset.bind(this));\n this.wss.register(\"unregisterSubscription\", this.unregisterSubscription.bind(this));\n this.wss.register(\"unregisterSubscriptions\", this.unregisterSubscriptions.bind(this));\n }\n\n private readSubId(params: unknown): string {\n return (params as { subId: string }).subId;\n }\n\n private async registerSubscription(params: unknown): Promise<RegisterSubscriptionResult> {\n const subId = this.readSubId(params);\n return this.workerFor(subId).send<RegisterSubscriptionResult>(MsgType.Register, params);\n }\n\n private async registerSubscriptionBatch(params: unknown): Promise<RegisterSubscriptionBatchResult> {\n // Group entries by destination worker, dispatch one batch per worker, then\n // restore the original order.\n const arr = params as RegisterSubscriptionParams[];\n const tagged = arr.map((p, index) => ({ index, p, worker: this.workerFor(p.subId) }));\n const byWorker = groupByToMap(tagged, (t) => t.worker);\n\n const settled = await Promise.all(\n [...byWorker.entries()].map(async ([worker, items]) => {\n const results = await worker.send<RegisterSubscriptionResult[]>(\n MsgType.RegisterBatch,\n items.map((t) => t.p),\n );\n return items.map((t, i) => ({ index: t.index, result: results[i]! }));\n }),\n );\n\n const result: RegisterSubscriptionBatchResult = new Array(arr.length);\n for (const { index, result: r } of settled.flat()) {\n result[index] = r;\n }\n return result;\n }\n\n private async updateSubscriptionReadSet(params: unknown): Promise<UpdateSubscriptionReadSetResult> {\n const subId = this.readSubId(params);\n return this.workerFor(subId).send<UpdateSubscriptionReadSetResult>(MsgType.UpdateReadSet, params);\n }\n\n private async unregisterSubscription(params: unknown): Promise<UnregisterSubscriptionResult> {\n const subId = this.readSubId(params);\n return this.workerFor(subId).send<UnregisterSubscriptionResult>(MsgType.Unregister, params);\n }\n\n private async unregisterSubscriptions(params: unknown): Promise<UnregisterSubscriptionsResult> {\n const arr = params as UnregisterSubscriptionParams[];\n const tagged = arr.map((p, index) => ({ index, p, worker: this.workerFor(p.subId) }));\n const byWorker = groupByToMap(tagged, (t) => t.worker);\n\n const settled = await Promise.all(\n [...byWorker.entries()].map(async ([worker, items]) => {\n const results = await worker.send<UnregisterSubscriptionResult[]>(\n MsgType.Unregisters,\n items.map((t) => t.p) satisfies UnregisterSubscriptionsParams,\n );\n return items.map((t, i) => ({ index: t.index, result: results[i]! }));\n }),\n );\n\n const result: UnregisterSubscriptionsResult = new Array(arr.length);\n for (const { index, result: r } of settled.flat()) {\n result[index] = r;\n }\n return result;\n }\n\n private async invalidateWriteset(params: unknown): Promise<InvalidateWritesetResult> {\n // Fan the raw payload out to every worker. Each parses its own copy of the\n // writeSet (cheap relative to the scan) and returns its affected subs.\n const perWorker = await Promise.all(\n this.workers.map((w) => w.send<AffectedSubscription[]>(MsgType.Invalidate, params)),\n );\n return { affected: perWorker.flat() };\n }\n}\n\nfunction resolveWorkerCount(configured?: number): number {\n if (typeof configured === \"number\" && configured > 0) return Math.floor(configured);\n\n const envVal = process.env.SUPALIVE_SUB_MANAGER_WORKERS;\n if (envVal) {\n const n = parseInt(envVal, 10);\n if (Number.isFinite(n) && n > 0) return n;\n }\n return 1;\n}\n\nfunction resolveMaxConnectionsPerWorker(config: SubscriptionManagerConfig, workers: number): number {\n if (config.maxConnectionsPerWorker && config.maxConnectionsPerWorker > 0) {\n return Math.floor(config.maxConnectionsPerWorker);\n }\n const total = config.database.maxConnections ?? (config.database.type === \"mysql\" ? 20 : 10);\n return Math.max(1, Math.floor(total / workers));\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"server.js","names":["logger","baseLogger","types.RegisterSubscriptionResultSchema","types.RegisterSubscriptionBatchResultSchema","types.UpdateSubscriptionReadSetResultSchema","types.UnregisterSubscriptionResultSchema","types.UnregisterSubscriptionsResultSchema","types.InvalidateWritesetResultSchema","logger","baseLogger","Client","logger","baseLogger","logger","baseLogger","uuidv4","logger","baseLogger","WebSocketServer"],"sources":["../../../src/server/sub_hash.ts","../../../src/server/one-shot-query.ts","../../../src/server/subscription-manager-client.ts","../../../src/jobs/scheduler.ts","../../../src/server/rate-limit.ts","../../../src/server/http-rpc.ts","../../../src/server/supalive-server.ts","../../../src/server/hash.ts","../../../src/server/subscription-manager.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","import type { SupaliveDb } from \"../db/realtime_db\";\nimport type { CacheLayer } from \"../db/cache\";\nimport type { RegisteredProcedure } from \"../router/router\";\nimport type { QueryCtx } from \"../router/procedure\";\nimport { stableStringify } from \"../helper\";\nimport { generateCacheKey, resolveQueryIdentity } from \"./sub_hash\";\nimport { evaluateCacheFreshness } from \"./overlap-checker\";\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. */\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 * 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. Freshness check via {@link evaluateCacheFreshness}: cached metadata +\n * the retention watermark + a between-ts commit-log scan.\n * 3. Fresh → return the cached data.\n * Stale/miss → recompute via `queryInternal` and write BOTH the data and\n * the metadata (readSet + snapshotTs) back, so a later one-shot or a fresh\n * 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 freshness upper bound — the latest snapshot ts as of\n * this call. The caller loads it (via `db.getLatestSnapshotTimestamp()`) and\n * passes it in, exactly like the subscribe path loads it before `register`, so\n * the snapshot read lives at the caller seam rather than being hidden here.\n */\nexport async function runCachedQuery<TContext>(\n deps: OneShotQueryDeps<TContext>,\n procedure: RegisteredProcedure<TContext>,\n procedureName: string,\n input: unknown,\n serverCtx: TContext | undefined,\n lastSnapshotTs: bigint,\n): Promise<unknown> {\n const queryIdentity = resolveQueryIdentity(procedure, serverCtx, input, deps.getUserId);\n const cacheKey = await generateCacheKey(procedureName, stableStringify(input), queryIdentity);\n\n const metadata = await deps.cache.getQueryCacheMetaData(cacheKey);\n const freshness = await evaluateCacheFreshness(deps.db.impl, metadata, lastSnapshotTs, deps.getMinRetainedTs());\n\n if (freshness.fresh) {\n const cached = await deps.cache.getQueryCacheFor(cacheKey);\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) return cached.data;\n }\n\n const result = await deps.db.queryInternal(async (ctx) => {\n const fn = procedure.fn as (ctx: QueryCtx<TContext>, input: unknown) => Promise<unknown>;\n return fn({ db: ctx, serverCtx }, input);\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 result.data;\n}\n","import { Client } from \"rpc-websockets\";\nimport z, { type ZodType } from \"zod\";\nimport * as types from \"./subscription-manager-types\";\nimport type { WriteEntry } from \"../db/types_db\";\nimport { logger as baseLogger, logLevelOf } from \"../logger\";\nimport { supaliveStringify } from \"../helper\";\n\nconst logger = baseLogger.child(\n { name: \"SUBS_MGR_CLIENT\" },\n { level: logLevelOf(\"SUB_MANAGER_CLIENT_LOG_LEVEL\") },\n);\n\n/**\n * Callback the app server registers to process invalidations that were\n * deferred during a sub-manager outage. When the sub-manager reconnects and\n * the buffered writeSets are flushed in one batched RPC, the resulting\n * `affected[]` is handed to this callback — which is expected to do the\n * same work as the normal per-mutation `reexecuteAffectedQuery` path.\n */\nexport type AffectedHandler = (\n affected: types.AffectedSubscription[],\n) => Promise<void>;\n\n/**\n * Callback the app server registers to drive recovery on sub-manager\n * reconnect. Iterates `serverSubscriptions`, reads `sl:reg:<subId>` from\n * Redis, and re-registers each known sub. Resolves when complete; only then\n * does the client's buffer flush and gated calls release.\n */\nexport type RecoveryDriver = () => Promise<void>;\n\nexport interface SubManagerClientOptions {\n url: string;\n /**\n * Maximum number of buffered WriteEntry records held during an outage.\n * If exceeded, the buffer is dropped — the commit-log replay path during\n * the next register call will catch up any missed invalidations, at the\n * cost of a brief recompute spike. Default 50_000 entries.\n */\n invalidateBufferLimit?: number;\n}\n\ntype ReadyState = \"ready\" | \"busy\";\n\nexport const subscriptionManagerDataPack = {\n encode: (data: unknown) => supaliveStringify(data),\n decode: (data: string) => JSON.parse(data),\n};\n\n\n/**\n * RPC client for the (single) sub-manager process.\n *\n * Design (Convex-inspired):\n * • The sub-manager is in-memory and ephemeral. There is no version\n * protocol, no client-side registration registry, no OUT_OF_SYNC\n * replay logic. All recovery is driven from the app server using\n * state it already has (`serverSubscriptions`) plus registration\n * records persisted to Redis at `sl:reg:<subId>`.\n *\n * • Out-of-order `updateSubscriptionReadSet` calls are filtered server\n * side using `lastSnapshotTs` (monotonic DB commit timestamp).\n *\n * • On rpc-websockets reconnect, the client:\n * 1. Transitions to `busy` (any in-flight `invalidateWriteset`\n * buffers its writeSet).\n * 2. Invokes the app server's `recoveryDriver` to re-register all\n * known subs.\n * 3. Flushes the buffer as one batched `invalidateWriteset` RPC and\n * hands the resulting `affected[]` to `affectedHandler`.\n * 4. Transitions to `ready`.\n *\n * • During an extended outage, the buffer is bounded; if exceeded, it\n * is dropped and we rely on the commit-log replay built into\n * `SubscriptionWorker.register()` to catch each sub up on the next\n * re-register.\n */\nexport class SubManagerClient {\n private client: Client;\n private state: ReadyState = \"busy\";\n private hasConnectedOnce = false;\n\n /** Resolves whenever state transitions to 'ready'. Replaced on each busy→ready cycle. */\n private readyPromise: Promise<void>;\n private resolveReady!: () => void;\n\n /** Writeset entries accumulated while state === 'busy'. Flushed as one RPC after recovery. */\n private bufferedWriteSets: WriteEntry[] = [];\n private readonly bufferLimit: number;\n private bufferDroppedDuringOutage = false;\n\n private recoveryDriver?: RecoveryDriver;\n private affectedHandler?: AffectedHandler;\n\n constructor(urlOrOptions: string | SubManagerClientOptions) {\n const opts = typeof urlOrOptions === \"string\"\n ? { url: urlOrOptions }\n : urlOrOptions;\n\n this.bufferLimit = opts.invalidateBufferLimit ?? 50_000;\n this.readyPromise = new Promise<void>((r) => { this.resolveReady = r; });\n\n this.client = new Client(opts.url, {\n dataPack: subscriptionManagerDataPack,\n });\n this.client.setAutoReconnect(true);\n this.client.setMaxReconnects(0);\n // Ensure the dataPack is set even if rpc-websockets doesn't pass it through to the Client constructor\n this.client.dataPack = subscriptionManagerDataPack;\n\n this.client.on(\"open\", this.handleOpen.bind(this));\n this.client.on(\"close\", this.handleClose.bind(this));\n this.client.on(\"error\", (err: unknown) => logger.warn(err, \"sub-manager client socket error\"));\n }\n\n /**\n * Wire up the app-server-driven recovery. Must be called before any\n * disconnect/reconnect cycle for buffered writeSets to be flushed and\n * subscriptions re-registered.\n */\n setRecoveryDriver(driver: RecoveryDriver): void {\n this.recoveryDriver = driver;\n }\n\n /**\n * Wire up the handler invoked when batched invalidate results land\n * after recovery. The handler is expected to drive\n * `reexecuteAffectedQuery` for each entry.\n */\n setAffectedHandler(handler: AffectedHandler): void {\n this.affectedHandler = handler;\n }\n\n // ─── RPC methods ────────────────────────────────────────────────────────\n\n async registerSubscription(\n params: types.RegisterSubscriptionParams,\n ): Promise<types.RegisterSubscriptionResult> {\n return this.call(\"registerSubscription\", params, types.RegisterSubscriptionResultSchema);\n }\n\n async registerSubscriptionBatch(\n params: types.RegisterSubscriptionParams[],\n ): Promise<types.RegisterSubscriptionResult[]> {\n return this.call(\"registerSubscriptionBatch\", params, types.RegisterSubscriptionBatchResultSchema);\n }\n\n async updateSubscriptionReadSet(\n params: types.UpdateSubscriptionReadSetParams,\n ): Promise<types.UpdateSubscriptionReadSetResult> {\n return this.call(\"updateSubscriptionReadSet\", params, types.UpdateSubscriptionReadSetResultSchema);\n }\n\n async unregisterSubscription(\n params: types.UnregisterSubscriptionParams,\n ): Promise<types.UnregisterSubscriptionResult> {\n return this.call(\"unregisterSubscription\", params, types.UnregisterSubscriptionResultSchema);\n }\n\n async unregisterSubscriptions(\n params: types.UnregisterSubscriptionsParams,\n ): Promise<types.UnregisterSubscriptionsResult> {\n return this.call(\"unregisterSubscriptions\", params, types.UnregisterSubscriptionsResultSchema);\n }\n\n /**\n * Sends the writeSet to the sub-manager for invalidation, OR buffers it\n * if the client is currently disconnected / recovering. Returns\n * `{ affected: [] }` while buffering — the actual `affected` from the\n * buffered batch is delivered via the registered `affectedHandler`\n * after recovery completes.\n */\n async invalidateWriteset(\n params: types.InvalidateWritesetParams,\n ): Promise<types.InvalidateWritesetResult> {\n if (this.state === \"busy\") {\n this.appendToBuffer(params.writeSet);\n return { affected: [] };\n }\n return this.call(\"invalidateWriteset\", params, types.InvalidateWritesetResultSchema);\n }\n\n // ─── Internal ───────────────────────────────────────────────────────────\n\n /**\n * Single point through which RPCs go. Throws on transport / server error.\n * `registerSubscription`, `updateSubscriptionReadSet`, and\n * `unregisterSubscription` are NOT gated on the ready state: during a\n * disconnect they will fail at the socket layer, and during recovery\n * (which itself uses these methods) gating would deadlock.\n */\n private async call<T extends ZodType>(\n procName: string,\n params: unknown,\n resType: T,\n ): Promise<z.infer<T>> {\n const result = await this.client.call(procName, params);\n return resType.parse(result) as z.infer<T>;\n }\n\n private appendToBuffer(writeSet: WriteEntry[]): void {\n if (this.bufferedWriteSets.length + writeSet.length > this.bufferLimit) {\n if (!this.bufferDroppedDuringOutage) {\n logger.warn(\n `Invalidate buffer would exceed ${this.bufferLimit}; dropping buffered entries. ` +\n `Subscriptions will be caught up via commit-log replay on re-register.`,\n );\n this.bufferDroppedDuringOutage = true;\n }\n this.bufferedWriteSets = [];\n return;\n }\n this.bufferedWriteSets.push(...writeSet);\n }\n\n private async flushBuffer(): Promise<void> {\n if (this.bufferedWriteSets.length === 0) return;\n const writeSet = this.bufferedWriteSets;\n this.bufferedWriteSets = [];\n this.bufferDroppedDuringOutage = false;\n\n try {\n const result = await this.call(\n \"invalidateWriteset\",\n { writeSet },\n types.InvalidateWritesetResultSchema,\n );\n if (this.affectedHandler && result.affected.length > 0) {\n await this.affectedHandler(result.affected);\n }\n } catch (err) {\n logger.error(err, \"Failed to flush invalidate buffer after recovery\");\n }\n }\n\n private markBusy(): void {\n if (this.state === \"busy\") return;\n this.state = \"busy\";\n this.readyPromise = new Promise<void>((r) => { this.resolveReady = r; });\n }\n\n private markReady(): void {\n if (this.state === \"ready\") return;\n this.state = \"ready\";\n this.resolveReady();\n }\n\n private handleClose(): void {\n logger.info(\"sub-manager client: socket closed\");\n this.markBusy();\n }\n\n private handleOpen(): void {\n if (!this.hasConnectedOnce) {\n this.hasConnectedOnce = true;\n logger.info(\"sub-manager client: initial connect\");\n this.markReady();\n return;\n }\n logger.info(\"sub-manager client: reconnected — starting recovery\");\n void this.runRecovery();\n }\n\n private async runRecovery(): Promise<void> {\n // Stay 'busy' for the duration of recovery so concurrent\n // invalidateWriteset calls keep buffering.\n try {\n if (this.recoveryDriver) {\n await this.recoveryDriver();\n } else {\n logger.warn(\"sub-manager client: no recoveryDriver configured — skipping recovery step\");\n }\n await this.flushBuffer();\n } catch (err) {\n logger.error(err, \"Recovery failed; state will be marked ready anyway\");\n } finally {\n logger.info(\"sub-manager client: recovery complete\");\n this.markReady();\n }\n }\n\n /**\n * Test/inspection helpers.\n */\n /** @internal */\n isReady(): boolean { return this.state === \"ready\"; }\n /** @internal */\n bufferSize(): number { return this.bufferedWriteSets.length; }\n}\n","import { Client, Receiver } from \"@upstash/qstash\";\nimport { logger as baseLogger, logLevelOf } from \"../logger\";\n\nconst logger = baseLogger.child(\n { name: \"SCHEDULER\" },\n { level: logLevelOf(\"SCHEDULER_LOG_LEVEL\") },\n);\n\n// ─── Scheduler Types ─────────────────────────────────────────────────────────\n\n/** A recurring job to keep synced on the scheduler side. */\nexport interface ScheduledJobDef {\n /** Job procedure name (also the last path segment of its endpoint). */\n name: string;\n /** Cron expression, e.g. `\"0 3 * * *\"`. */\n cron: string;\n}\n\n/** Options for a precise, one-shot job run. */\nexport interface EnqueueOptions {\n /** Run after this many seconds from now. */\n delaySeconds?: number;\n /** Run at this absolute time. Overrides `delaySeconds`. */\n runAt?: Date;\n /**\n * Idempotency key. If two enqueues share the same id within the provider's\n * dedup window, only the first is delivered. Use it to make \"schedule the\n * expiry of X\" safe to call more than once.\n */\n dedupeId?: string;\n}\n\n/** Handle returned by {@link JobScheduler.enqueue}; pass its `id` to `cancel`. */\nexport interface ScheduleHandle {\n id: string;\n}\n\n/** The pieces of an inbound webhook the scheduler needs to authenticate it. */\nexport interface IncomingJobRequest {\n /** Job name parsed from the request path. */\n name: string;\n /** Raw (unparsed) request body — signatures are computed over these bytes. */\n rawBody: string;\n /** Request headers, keys lowercased. */\n headers: Record<string, string | undefined>;\n /** Full request URL the webhook was delivered to. */\n url: string;\n}\n\n/**\n * Pluggable scheduler. The server owns exactly one instance and uses it for\n * both directions:\n * • inbound — `verify` authenticates a webhook before the job runs.\n * • outbound — `syncSchedules` (recurring, at startup) and `enqueue`/`cancel`\n * (precise one-shots, at runtime) drive the provider.\n *\n * Both dev and production implementations POST to the same HTTP job endpoint,\n * so the server's dispatch path is exercised identically in every environment.\n */\nexport interface JobScheduler {\n /** Authenticate an inbound webhook. Return false (or throw) to reject. */\n verify(req: IncomingJobRequest): Promise<boolean>;\n\n /**\n * Make the provider's recurring schedules match `jobs` exactly (create,\n * update, and remove stale ones). Called once at server startup.\n * `endpointFor(name)` yields the absolute URL the provider should POST to.\n */\n syncSchedules(\n jobs: ScheduledJobDef[],\n endpointFor: (name: string) => string,\n ): Promise<void>;\n\n /** Enqueue a precise one-shot run of `name` with `body` at `endpoint`. */\n enqueue(\n name: string,\n body: unknown,\n endpoint: string,\n opts?: EnqueueOptions,\n ): Promise<ScheduleHandle>;\n\n /** Cancel a previously enqueued one-shot by its handle id. */\n cancel(handleId: string): Promise<void>;\n}\n\nfunction delayMsFrom(opts?: EnqueueOptions): number {\n if (opts?.runAt) return Math.max(0, opts.runAt.getTime() - Date.now());\n if (opts?.delaySeconds && opts.delaySeconds > 0) return opts.delaySeconds * 1000;\n return 0;\n}\n\n// ─── Dev Scheduler ───────────────────────────────────────────────────────────\n\nexport interface DevSchedulerOptions {\n /**\n * How often, in ms, to fire each recurring job in dev. Dev intentionally\n * ignores exact cron timing (that is the production scheduler's job); it\n * just fires often enough to exercise the handler. Default 60_000.\n */\n recurringIntervalMs?: number;\n /**\n * Override the HTTP client (for tests). Defaults to the global `fetch`.\n */\n fetchImpl?: typeof fetch;\n}\n\n/**\n * In-process scheduler for local development and tests. Recurring jobs fire on\n * a fixed interval; one-shots fire on an exact `setTimeout` (so precise timing\n * is faithfully testable). Every fire POSTs to the real HTTP job endpoint, and\n * `verify` accepts everything — there is no signing in dev.\n */\nexport class DevScheduler implements JobScheduler {\n private readonly intervalMs: number;\n private readonly fetchImpl: typeof fetch;\n private intervals = new Map<string, ReturnType<typeof setInterval>>();\n private timeouts = new Map<string, ReturnType<typeof setTimeout>>();\n\n constructor(opts: DevSchedulerOptions = {}) {\n this.intervalMs = opts.recurringIntervalMs ?? 60_000;\n this.fetchImpl = opts.fetchImpl ?? fetch;\n }\n\n async verify(_req: IncomingJobRequest): Promise<boolean> {\n return true;\n }\n\n async syncSchedules(\n jobs: ScheduledJobDef[],\n endpointFor: (name: string) => string,\n ): Promise<void> {\n // Rebuild from scratch so a re-sync reflects removed jobs.\n for (const timer of this.intervals.values()) clearInterval(timer);\n this.intervals.clear();\n\n for (const job of jobs) {\n const endpoint = endpointFor(job.name);\n const timer = setInterval(() => {\n this.post(endpoint, {}).catch((err) =>\n logger.error(err, `dev recurring job \"${job.name}\" failed`),\n );\n }, this.intervalMs);\n // Don't keep the process alive purely for the dev poller.\n timer.unref?.();\n this.intervals.set(job.name, timer);\n logger.info(\n `dev schedule \"${job.name}\" (cron \"${job.cron}\") → firing every ${this.intervalMs}ms`,\n );\n }\n }\n\n async enqueue(\n name: string,\n body: unknown,\n endpoint: string,\n opts?: EnqueueOptions,\n ): Promise<ScheduleHandle> {\n const id = crypto.randomUUID();\n const timer = setTimeout(() => {\n this.timeouts.delete(id);\n this.post(endpoint, body).catch((err) =>\n logger.error(err, `dev one-shot job \"${name}\" failed`),\n );\n }, delayMsFrom(opts));\n timer.unref?.();\n this.timeouts.set(id, timer);\n return { id };\n }\n\n async cancel(handleId: string): Promise<void> {\n const timer = this.timeouts.get(handleId);\n if (timer) {\n clearTimeout(timer);\n this.timeouts.delete(handleId);\n }\n }\n\n /** Stop every dev timer (call on server shutdown / in test teardown). */\n stop(): void {\n for (const t of this.intervals.values()) clearInterval(t);\n for (const t of this.timeouts.values()) clearTimeout(t);\n this.intervals.clear();\n this.timeouts.clear();\n }\n\n private async post(endpoint: string, body: unknown): Promise<void> {\n const res = await this.fetchImpl(endpoint, {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\" },\n body: JSON.stringify(body ?? {}),\n });\n if (!res.ok) {\n throw new Error(`job endpoint ${endpoint} returned ${res.status}`);\n }\n }\n}\n\n// ─── QStash Scheduler (production) ───────────────────────────────────────────\n\nexport interface QStashSchedulerOptions {\n /** QStash Base url (`QSTASH_BASE_URL`). */\n baseUrl: string;\n /** QStash REST token (`QSTASH_TOKEN`). */\n token: string;\n /** Current signing key for webhook verification (`QSTASH_CURRENT_SIGNING_KEY`). */\n currentSigningKey: string;\n /** Next signing key, for zero-downtime key rotation (`QSTASH_NEXT_SIGNING_KEY`). */\n nextSigningKey: string;\n /* explicitly set qstash to dev mode to use local qstash server */\n devMode: boolean;\n}\n\n/**\n * Production scheduler backed by Upstash QStash. Externalizes the timer so a\n * horizontally-scaled deployment fires each cron tick exactly once (QStash\n * calls one HTTP endpoint), with at-least-once retries and signed webhooks.\n */\nexport class QStashScheduler implements JobScheduler {\n private readonly client: Client;\n private readonly receiver: Receiver;\n\n constructor(opts: QStashSchedulerOptions) {\n this.client = new Client({ baseUrl: opts.baseUrl ,token: opts.token, devMode: opts.devMode });\n this.receiver = new Receiver({\n currentSigningKey: opts.currentSigningKey,\n nextSigningKey: opts.nextSigningKey,\n });\n }\n\n async verify(req: IncomingJobRequest): Promise<boolean> {\n const signature = req.headers[\"upstash-signature\"];\n if (!signature) return false;\n try {\n return await this.receiver.verify({\n signature,\n body: req.rawBody,\n url: req.url,\n });\n } catch (err) {\n logger.warn(err, `signature verification failed for job \"${req.name}\"`);\n return false;\n }\n }\n\n async syncSchedules(\n jobs: ScheduledJobDef[],\n endpointFor: (name: string) => string,\n ): Promise<void> {\n // A deterministic scheduleId per job makes create an idempotent upsert and\n // lets us reconcile: anything QStash still has that we no longer declare is\n // a stale schedule and gets removed.\n const desired = new Map(jobs.map((j) => [scheduleIdFor(j.name), j]));\n\n let existing: Awaited<ReturnType<Client[\"schedules\"][\"list\"]>> = [];\n try {\n existing = await this.client.schedules.list();\n } catch (err) {\n logger.error(err, \"failed to list QStash schedules; skipping reconcile of stale schedules\");\n }\n\n for (const schedule of existing) {\n const id = schedule.scheduleId;\n if (id?.startsWith(SCHEDULE_ID_PREFIX) && !desired.has(id)) {\n try {\n await this.client.schedules.delete(id);\n logger.info(`removed stale QStash schedule ${id}`);\n } catch (err) {\n logger.error(err, `failed to remove stale schedule ${id}`);\n }\n }\n }\n\n for (const [scheduleId, job] of desired) {\n try {\n await this.client.schedules.create({\n scheduleId,\n destination: endpointFor(job.name),\n cron: job.cron,\n });\n logger.info(`synced QStash schedule \"${job.name}\" (${job.cron})`);\n } catch (err) {\n logger.error(err, `failed to sync schedule for job \"${job.name}\"`);\n throw err;\n }\n }\n }\n\n async enqueue(\n name: string,\n body: unknown,\n endpoint: string,\n opts?: EnqueueOptions,\n ): Promise<ScheduleHandle> {\n const res = await this.client.publishJSON({\n url: endpoint,\n body,\n ...(opts?.runAt\n ? { notBefore: Math.floor(opts.runAt.getTime() / 1000) }\n : opts?.delaySeconds\n ? { delay: opts.delaySeconds }\n : {}),\n ...(opts?.dedupeId ? { deduplicationId: opts.dedupeId } : {}),\n });\n const messageId = (res as { messageId?: string }).messageId;\n if (!messageId) throw new Error(`QStash publish for job \"${name}\" returned no messageId`);\n return { id: messageId };\n }\n\n async cancel(handleId: string): Promise<void> {\n await this.client.messages.cancel(handleId);\n }\n}\n\nconst SCHEDULE_ID_PREFIX = \"supalive-job-\";\n\nfunction scheduleIdFor(jobName: string): string {\n return `${SCHEDULE_ID_PREFIX}${jobName}`;\n}\n\n// ─── Job Client (app-facing) ─────────────────────────────────────────────────\n\n/**\n * Thin app-facing handle for scheduling precise one-shots from inside\n * procedures. The server builds one (via `getJobClient()`) that already knows\n * how to turn a job name into its endpoint, so callers just say\n * `jobs.enqueue(\"expireSpendAuth\", { id }, { runAt })`.\n */\nexport class JobClient {\n constructor(\n private readonly scheduler: JobScheduler,\n private readonly endpointFor: (name: string) => string,\n ) {}\n\n enqueue(name: string, body: unknown, opts?: EnqueueOptions): Promise<ScheduleHandle> {\n return this.scheduler.enqueue(name, body, this.endpointFor(name), opts);\n }\n\n cancel(handleId: string): Promise<void> {\n return this.scheduler.cancel(handleId);\n }\n}\n","/**\n * Fixed-window rate limiter. Per-instance and in-memory: each key gets a\n * counter that resets every `windowMs`. Good enough to blunt abuse of the HTTP\n * RPC endpoint (which does a JWT verify + possibly a DB read per call) without\n * the latency of a shared Redis counter. A background sweep evicts expired\n * buckets so memory stays bounded under churny key sets (e.g. many IPs).\n */\nexport interface RateLimitResult {\n allowed: boolean;\n /** Milliseconds until the current window resets (0 when allowed). */\n retryAfterMs: number;\n /** Remaining allowance in the current window (0 when blocked). */\n remaining: number;\n}\n\nexport class FixedWindowRateLimiter {\n private readonly buckets = new Map<string, { count: number; resetAt: number }>();\n private sweepTimer?: ReturnType<typeof setInterval>;\n\n constructor(\n private readonly windowMs: number,\n private readonly max: number,\n ) {\n this.sweepTimer = setInterval(() => this.sweep(), Math.max(windowMs, 1000));\n // Never keep the process alive just for the sweep.\n this.sweepTimer.unref?.();\n }\n\n check(key: string, now: number = Date.now()): RateLimitResult {\n let bucket = this.buckets.get(key);\n if (!bucket || bucket.resetAt <= now) {\n bucket = { count: 0, resetAt: now + this.windowMs };\n this.buckets.set(key, bucket);\n }\n bucket.count++;\n if (bucket.count > this.max) {\n return { allowed: false, retryAfterMs: bucket.resetAt - now, remaining: 0 };\n }\n return { allowed: true, retryAfterMs: 0, remaining: this.max - bucket.count };\n }\n\n private sweep(now: number = Date.now()): void {\n for (const [key, bucket] of this.buckets) {\n if (bucket.resetAt <= now) this.buckets.delete(key);\n }\n }\n\n /** Stop the background sweep (call on server shutdown / test teardown). */\n stop(): void {\n if (this.sweepTimer) clearInterval(this.sweepTimer);\n this.sweepTimer = undefined;\n }\n}\n","import type { IncomingMessage, ServerResponse } from \"http\";\nimport type { ZodType } from \"zod\";\nimport { supaliveStringify } from \"../helper\";\nimport { AuthenticationError } from \"../db/types_client_rpc\";\nimport type { SupaliveDb } from \"../db/realtime_db\";\nimport type { ContextRegistry, RegisteredProcedure } from \"../router/router\";\nimport type { MutationCtx, ActionCtx } from \"../router/procedure\";\nimport type { RpcConfig } from \"../config\";\nimport type { ObjectStorage } from \"../storage\";\nimport type { JobClient } from \"../jobs/scheduler\";\nimport { FixedWindowRateLimiter } from \"./rate-limit\";\nimport { runCachedQuery, type OneShotQueryDeps } from \"./one-shot-query\";\nimport { logger as baseLogger, logLevelOf } from \"../logger\";\n\nconst logger = baseLogger.child(\n { name: \"HTTP_RPC\" },\n { level: logLevelOf(\"SERVER_LOG_LEVEL\") },\n);\n\nexport interface HttpRpcDeps<TContext> {\n /** Lazily fetch the registry (set after `registerRouter`). */\n getRegistry: () => ContextRegistry | undefined;\n /** The server's SupaliveDb — the SAME instance whose after-commit hook drives\n * subscription invalidation, so an HTTP mutation pushes live updates too.\n * Used for mutations/actions; queries go through `oneShotDeps`. */\n db: SupaliveDb;\n /** Optional object storage exposed to ACTION handlers as `ctx.storage`. */\n storage: ObjectStorage;\n /** Optional job client exposed to ACTION handlers as `ctx.scheduler`. */\n scheduler: JobClient;\n /** Shared, pre-built one-shot query cache deps (db + cache + getUserId +\n * getMinRetainedTs). Same object the WS `call` path uses, so HTTP one-shot queries\n * cache/segment identically. */\n oneShotDeps: OneShotQueryDeps<TContext>;\n /** Same auth callback used by the WebSocket path. */\n verifyAuth?: (data: Record<string, unknown>, sessionId: string) => Promise<TContext | null>;\n rpc: RpcConfig<TContext>;\n}\n\n/**\n * Handles the one-shot HTTP RPC transport: a single `POST {path}` endpoint for\n * `query`/`mutation`/`action`. The procedure name travels in the request body\n * (`{ name, input }`), not the URL — so the wire never reveals which procedures\n * exist and a transport-level error (which typically echoes the failing URL)\n * can't leak a procedure name. Subscriptions are not offered here (WS only).\n * Kept independent of the WebSocket server so it can be unit-tested against a\n * plain `http.Server` with a stub db/registry.\n *\n * Request flow: CORS → (OPTIONS preflight) → rate limit (pre-auth) → read body →\n * parse `{ name, input }` → procedure lookup (internal/job rejected) → auth →\n * validate input → run → respond.\n */\nexport class HttpRpcHandler<TContext> {\n private readonly path: string;\n private readonly rpc: RpcConfig<TContext>;\n private readonly rateLimiter?: FixedWindowRateLimiter;\n\n constructor(private readonly deps: HttpRpcDeps<TContext>) {\n this.rpc = deps.rpc;\n this.path = normalizeRpcPath(deps.rpc.path ?? \"/_rpc\");\n if (deps.rpc.rateLimit !== false) {\n const rl = deps.rpc.rateLimit ?? {};\n this.rateLimiter = new FixedWindowRateLimiter(rl.windowMs ?? 60_000, rl.max ?? 120);\n }\n }\n\n /** True when this handler owns the given request path. The procedure name is\n * carried in the body now, so a single exact path is the whole surface. */\n handlesPath(path: string): boolean {\n return path === this.path;\n }\n\n stop(): void {\n this.rateLimiter?.stop();\n }\n\n async handle(req: IncomingMessage, res: ServerResponse, path: string): Promise<void> {\n this.applyCors(req, res);\n\n if (req.method === \"OPTIONS\") {\n res.writeHead(204);\n res.end();\n return;\n }\n if (req.method !== \"POST\") {\n return this.send(res, 405, { success: false, error: \"method not allowed\", code: \"METHOD_NOT_ALLOWED\" });\n }\n\n // Rate limit BEFORE auth so a flood can't hammer the (expensive) verify.\n // Keyed off the request (IP by default), so it needs no body — do it first.\n if (this.rateLimiter) {\n // `rateLimit` is a config object here (rateLimiter only exists when it\n // isn't `false`), so a `keyBy` override is read straight off it.\n const keyBy = this.rpc.rateLimit ? this.rpc.rateLimit.keyBy : undefined;\n const key = keyBy ? keyBy(req) : clientIp(req);\n const rl = this.rateLimiter.check(key);\n if (!rl.allowed) {\n res.setHeader(\"retry-after\", String(Math.ceil(rl.retryAfterMs / 1000)));\n return this.send(res, 429, { success: false, error: \"rate limit exceeded\", code: \"RATE_LIMITED\" });\n }\n }\n\n // The procedure name and input both live in the body: `{ name, input }`.\n const rawBody = await readBody(req);\n let name: string;\n let rawInput: unknown;\n try {\n const envelope = rawBody.trim().length > 0 ? JSON.parse(rawBody) : {};\n if (!envelope || typeof envelope !== \"object\" || typeof envelope.name !== \"string\") {\n throw new Error(\"missing procedure name\");\n }\n name = envelope.name;\n rawInput = envelope.input ?? {};\n } catch {\n // A malformed envelope reveals nothing about the API surface.\n return this.send(res, 400, { success: false, error: \"invalid request\", code: \"BAD_REQUEST\" });\n }\n\n const procedure = this.deps.getRegistry()?.getProcedure(name);\n // Internal procedures and jobs are server-only; subscriptions aren't offered\n // over HTTP. Report NOT_FOUND uniformly (and without the name) so nothing is\n // enumerable from the response.\n if (!procedure || procedure.internal || procedure.type === \"job\") {\n return this.send(res, 404, { success: false, error: \"not found\", code: \"NOT_FOUND\" });\n }\n\n const auth = await this.resolveAuth(req);\n if (!auth.ok) {\n return this.send(res, 401, { success: false, error: auth.message, code: \"UNAUTHENTICATED\" });\n }\n\n let input: unknown;\n try {\n input = (procedure.inputSchema as ZodType).parse(rawInput);\n } catch (err) {\n logger.warn(err, `RPC \"${name}\" received invalid input`);\n return this.send(res, 400, { success: false, error: \"invalid input\", code: \"BAD_REQUEST\" });\n }\n\n try {\n const data = await this.runProcedure(procedure, input, auth.ctx);\n return this.send(res, 200, { success: true, data });\n } catch (err) {\n logger.error(err, `RPC \"${name}\" failed`);\n return this.send(res, 500, {\n success: false,\n error: err instanceof Error ? err.message : \"execution failed\",\n code: \"EXECUTION_ERROR\",\n });\n }\n }\n\n private async runProcedure(\n procedure: RegisteredProcedure<TContext>,\n input: unknown,\n serverCtx: TContext | undefined,\n ): Promise<unknown> {\n if (procedure.type === \"query\") {\n // Same cache/identity/freshness model as a WS one-shot query — reuses the\n // server's shared one-shot deps. Load the freshness upper bound here, the\n // same way the WS path does before calling runCachedQuery.\n const lastSnapshotTs = await this.deps.db.impl.getLatestSnapshotTimestamp();\n return runCachedQuery(\n this.deps.oneShotDeps,\n procedure,\n procedure.name,\n input,\n serverCtx,\n lastSnapshotTs,\n );\n }\n if (procedure.type === \"mutation\") {\n // Direct (no per-session queue); OCC makes concurrent HTTP mutations safe,\n // and the db's after-commit hook invalidates subscriptions like a WS write.\n const res = await this.deps.db.mutation(async (ctx) => {\n const fn = procedure.fn as (c: MutationCtx<TContext>, i: unknown) => Promise<unknown>;\n return fn({ db: ctx, serverCtx }, input);\n });\n return res.result;\n }\n // action\n const fn = procedure.fn as (c: ActionCtx<TContext>, i: unknown) => Promise<unknown>;\n return fn(\n { db: this.deps.db, serverCtx, storage: this.deps.storage, scheduler: this.deps.scheduler },\n input,\n );\n }\n\n private async resolveAuth(\n req: IncomingMessage,\n ): Promise<{ ok: true; ctx: TContext | undefined } | { ok: false; message: string }> {\n const extractor = this.rpc.auth ?? defaultHttpAuth;\n const data = extractor(req);\n // No verifyAuth configured, or no auth data → anonymous (mirrors WS).\n if (!this.deps.verifyAuth || !data) return { ok: true, ctx: undefined };\n try {\n const ctx = await this.deps.verifyAuth(data, `http-${crypto.randomUUID()}`);\n if (!ctx) return { ok: false, message: \"Invalid authentication\" };\n return { ok: true, ctx };\n } catch (err) {\n if (err instanceof AuthenticationError) {\n return { ok: false, message: `Invalid authentication: ${err.message}` };\n }\n return { ok: false, message: \"Invalid authentication\" };\n }\n }\n\n private applyCors(req: IncomingMessage, res: ServerResponse) {\n const cors = this.rpc.cors;\n if (!cors) return;\n const originHeader = req.headers[\"origin\"];\n const origin = Array.isArray(originHeader) ? originHeader[0] : originHeader;\n\n if (cors.origins === \"*\") {\n res.setHeader(\"access-control-allow-origin\", \"*\");\n } else if (origin && cors.origins.includes(origin)) {\n res.setHeader(\"access-control-allow-origin\", origin);\n res.setHeader(\"vary\", \"Origin\");\n } else {\n return; // origin not allowed → emit no CORS headers\n }\n res.setHeader(\"access-control-allow-methods\", \"POST, OPTIONS\");\n res.setHeader(\"access-control-allow-headers\", \"authorization, content-type\");\n res.setHeader(\"access-control-max-age\", \"86400\");\n }\n\n private send(res: ServerResponse, status: number, body: unknown): void {\n res.writeHead(status, { \"content-type\": \"application/json\" });\n res.end(supaliveStringify(body));\n }\n}\n\n// ─── helpers ─────────────────────────────────────────────────────────────────\n\nfunction normalizeRpcPath(p: string): string {\n let s = p.startsWith(\"/\") ? p : `/${p}`;\n if (s.length > 1 && s.endsWith(\"/\")) s = s.slice(0, -1);\n return s;\n}\n\nfunction defaultHttpAuth(req: IncomingMessage): Record<string, unknown> | undefined {\n const header = req.headers[\"authorization\"];\n const value = Array.isArray(header) ? header[0] : header;\n if (!value) return undefined;\n const match = /^Bearer\\s+(.+)$/i.exec(value);\n return match ? { token: match[1] } : undefined;\n}\n\nfunction clientIp(req: IncomingMessage): string {\n const xff = req.headers[\"x-forwarded-for\"];\n if (xff) {\n const value = Array.isArray(xff) ? xff[0] : xff;\n const first = value.split(\",\")[0]?.trim();\n if (first) return first;\n }\n return req.socket?.remoteAddress ?? \"unknown\";\n}\n\nfunction readBody(req: IncomingMessage, maxBytes = 1024 * 1024): Promise<string> {\n return new Promise((resolve, reject) => {\n const chunks: Buffer[] = [];\n let size = 0;\n req.on(\"data\", (chunk: Buffer) => {\n size += chunk.length;\n if (size > maxBytes) {\n reject(new Error(\"request body too large\"));\n req.destroy();\n return;\n }\n chunks.push(chunk);\n });\n req.on(\"end\", () => resolve(Buffer.concat(chunks).toString(\"utf8\")));\n req.on(\"error\", reject);\n });\n}\n","import { createServer, IncomingMessage, type ServerResponse, type Server } from \"http\";\nimport { WebSocketServer, WebSocket } from \"ws\";\nimport { v4 as uuidv4 } from \"uuid\";\nimport type { ZodType } from \"zod\";\nimport Redis from \"ioredis\";\nimport { retry, handleAll, ExponentialBackoff } from \"cockatiel\";\nimport { stableStringify, supaliveStringify } from \"../helper\";\nimport {\n type QueryCtx,\n type MutationCtx,\n type ActionCtx,\n ContextRegistry,\n getContextRegistry,\n} from \"../router\";\nimport { sleep, SupaliveDb } from \"../db/realtime_db\";\nimport type { WriteEntry, CommitTs, LiveResult, MutationResult } from \"../db/types_db\";\nimport { AuthenticationError, ClientMessage, ClientMessageSchema, Context, ServerMessage } from \"../db/types_client_rpc\";\nimport {\n type Session,\n type InstanceId,\n type SubId,\n type PendingMutation,\n RemoteSubscriptionMessageSchema,\n RemoteRecomputeRace,\n SubscriptionEntry,\n RemoteSubscriptionMessage,\n} from \"./types_server\";\nimport { type Router } from \"../router/router\";\nimport {\n generateSubscriptionId,\n generateCacheKey,\n getHashOf,\n resolveQueryIdentity,\n} from \"./sub_hash\";\nimport { runCachedQuery, type OneShotQueryDeps } from \"./one-shot-query\";\nimport { RetentionWatermark, RETENTION_WATERMARK_CHANNEL } from \"./retention-watermark\";\nimport type { RegisteredProcedure } from \"../router/router\";\nimport { CacheLayer } from \"../db/cache\";\nimport { SubManagerClient } from \"./subscription-manager-client\";\nimport { SupaliveServerConfig } from \"../config\";\nimport { appInstanceId, logger as baseLogger, logLevelOf } from \"../logger\";\nimport { AffectedSubscription, RegistrationRecord } from \"./subscription-manager-types\";\nimport { bootstrapDbTypes } from \"../db/init_db\";\nimport { JobClient, type JobScheduler, type EnqueueOptions, type ScheduleHandle } from \"../jobs/scheduler\";\nimport type { JobCtx } from \"../router/procedure\";\nimport type { ObjectStorage } from \"../storage\";\nimport { HttpRpcHandler } from \"./http-rpc\";\n\nconst redisRetryPolicy = retry(handleAll, {\n maxAttempts: 3,\n backoff: new ExponentialBackoff({\n initialDelay: 100,\n maxDelay: 5000,\n exponent: 2,\n }),\n});\n\n/**\n * Global broadcast channel used to signal column-type cache invalidation\n * across every running server instance. Publishers (migration tooling,\n * the publishSchemaReload helper below) send a JSON message of shape\n * `{ type: \"schema:reload\"; tables?: string[]; originInstance?: string }`.\n * Subscribers re-bootstrap from PG (or Redis) so the next batched write\n * sees up-to-date column metadata without a process restart.\n */\nconst SCHEMA_RELOAD_CHANNEL = \"supalive:schema:reload\";\n\n/**\n * Per-socket cap on pending mutations. The Nth+1 mutation from a single\n * client gets rejected with TOO_MANY_CONCURRENT_MUTATIONS rather than\n * being enqueued — this is rate-limit pressure on a misbehaving client,\n * not a hard system limit.\n *\n * Mirrors Convex's OPERATION_QUEUE_BUFFER_SIZE (1000). Mutations drain\n * one-at-a-time per socket, so a healthy client never approaches this.\n */\nconst MAX_MUTATION_QUEUE_BUFFER = 1000;\n\n/**\n * Per-socket cap on in-flight actions. Actions run in parallel (unlike\n * mutations) because they may do long external work (HTTP, queue calls);\n * the cap exists so one socket can't open thousands of outbound\n * connections from inside the server.\n */\nconst MAX_ACTION_CONCURRENCY = 1000;\n\n// ─── Heartbeat constants ─────────────────────────────────────\n//\n// Two independent jobs run on a single per-session interval:\n//\n// 1. Dead-TCP detection: terminate sockets that have gone fully silent\n// (no inbound text/ping/pong) past `CLIENT_TIMEOUT_MS`. Otherwise\n// emit a ws-protocol ping (browser auto-pongs, refreshing `session.lastReceivedAt`).\n// 2. Client-watchdog refresh: if the application channel has been idle\n// for at least `HEARTBEAT_INTERVAL_MS`, send a `{type:\"ping\"}` JSON\n// frame so the client's inactivity watchdog stays fresh. Busy\n// sockets skip this (the `lastSentAt` check makes it idle-only).\n//\n// Both fire on the same `heartbeatTimer` — one `setInterval` per session.\nconst HEARTBEAT_INTERVAL_MS = Number(\n process.env.SUPALIVE_HEARTBEAT_INTERVAL_MS ?? 15_000,\n);\nconst CLIENT_TIMEOUT_MS = 120_000;\n\n/**\n * TTL on the Redis lock that elects the recompute winner when the writer\n * doesn't hold the sub locally. Each (subId, commitTs) gets its own lock\n * key, so this only needs to bound the worst-case recompute duration —\n * if the winner crashes mid-recompute, the lock expires and the next\n * mutation triggers a new race.\n */\nconst RECOMPUTE_LOCK_TTL_SECONDS = Number(\n process.env.SUPALIVE_RECOMPUTE_LOCK_TTL_SECONDS ?? 10,\n);\n\nconst logger = baseLogger.child({\n \"name\": \"SUPALIVE\",\n},\n { level: logLevelOf(\"SERVER_LOG_LEVEL\") },\n);\n\n/**\n * Supalive WebSocket Server\n */\nexport class SupaliveWebSocketServer<TContext extends Context> {\n private config: SupaliveServerConfig<TContext>;\n private instanceId: InstanceId;\n\n private httpServer: Server;\n private wss: WebSocketServer;\n\n private cache: CacheLayer;\n private redisSub: Redis;\n /** App-level pub/sub channels this instance listens on (see config.appPubSub). */\n private readonly appPubSubChannels: Set<string>;\n private db: SupaliveDb;\n private contextRegistry!: ContextRegistry;\n private router?: Router<any, TContext>;\n\n // In-memory retention watermark (oldest retained commit-log ts): loaded from the\n // DB at startup and kept fresh by the sub-manager's Redis broadcast. Read on\n // the one-shot query hot path via `oneShotDeps.getMinRetainedTs` (a plain\n // variable, no per-query round trip).\n private retentionWatermark = new RetentionWatermark();\n // Built once — the one-shot query cache deps are static aside from the live\n // watermark, which `getMinRetainedTs` reads by reference. Shared by the WS `call` path\n // and the HTTP RPC handler.\n private oneShotDeps: OneShotQueryDeps<TContext>;\n\n private scheduler: JobScheduler;\n private jobPath: string;\n private jobClient: JobClient;\n private rpcHandler?: HttpRpcHandler<TContext>;\n /** Optional object storage exposed to every procedure as `ctx.storage`. */\n private storage: ObjectStorage;\n\n private sessions: Map<WebSocket, Session<TContext>> = new Map();\n private serverSubscriptions: Map<SubId, SubscriptionEntry> = new Map();\n private subManagerClient!: SubManagerClient;\n\n private stats = {\n activeConnections: 0,\n totalConnections: 0,\n activeSubscriptions: 0,\n };\n\n constructor(config: SupaliveServerConfig<TContext>) {\n this.config = {\n port: 3000,\n host: \"0.0.0.0\",\n contextName: \"default\",\n sessionTtlSeconds: 3600,\n ...config,\n };\n this.instanceId = appInstanceId;\n\n if (!config.subManagerUrl) {\n throw new Error(\"subManagerUrl config is required (WebSocket URL of the sub-manager process)\");\n }\n this.subManagerClient = new SubManagerClient(config.subManagerUrl);\n this.cache = config.cacheLayer;\n this.redisSub = config.redisSubClient;\n this.appPubSubChannels = new Set(config.appPubSub?.channels ?? []);\n this.db = config.database;\n this.storage = config.storage;\n\n // Invalidate subscriptions after EVERY committed mutation, whatever its\n // source — client WS call, action, job, or the internal caller (as long as\n // it shares this db instance). Invalidation is instance-independent (it goes\n // through the central sub-manager), so a server-side write reaches every\n // WS node's subscribers. Fire-and-forget: the client's own response never\n // waits on it, and handleMutationCommit swallows/logs its own errors.\n this.db.setOnAfterCommit((writeSet, commitTs) => {\n this.handleMutationCommit(writeSet, commitTs).catch((err) =>\n logger.error(err, \"post-commit invalidation failed\"),\n );\n });\n\n this.scheduler = config.scheduler;\n this.jobPath = normalizeJobPath(config.jobPath ?? \"/_jobs\");\n this.jobClient = new JobClient(this.scheduler, (name) => this.endpointFor(name));\n\n // Static deps for cached one-shot queries (WS `call` + HTTP RPC). Built once;\n // `getMinRetainedTs` reads the live in-memory watermark by reference.\n this.oneShotDeps = {\n db: this.db,\n cache: this.cache,\n getUserId: config.getUserId,\n getMinRetainedTs: () => this.retentionWatermark.get(),\n };\n\n // Opt-in one-shot HTTP RPC transport (query/mutation/action). Shares this\n // server's db + verifyAuth + procedure registry; subscriptions stay WS-only.\n if (config.rpc) {\n this.rpcHandler = new HttpRpcHandler<TContext>({\n getRegistry: () => this.contextRegistry,\n db: this.db,\n oneShotDeps: this.oneShotDeps,\n verifyAuth: config.verifyAuth,\n rpc: config.rpc,\n storage: this.storage,\n scheduler: this.jobClient,\n });\n }\n\n // Wire the sub-manager client's recovery callbacks to drive re-registration\n // from this app server's serverSubscriptions whenever the RPC reconnects,\n // and to process the resulting batched invalidate flush.\n this.subManagerClient.setRecoveryDriver(this.runSubManagerRecovery.bind(this));\n this.subManagerClient.setAffectedHandler(this.runAffectedHandler.bind(this));\n\n // The HTTP server hosts the WS upgrade AND the job webhook endpoint. WS\n // traffic arrives as `upgrade` events (handled by WebSocketServer below),\n // so this plain `request` handler only ever sees the scheduler's POSTs and\n // health checks — the two never overlap.\n this.httpServer = createServer((req, res) => {\n this.handleHttpRequest(req, res).catch((err) => {\n logger.error(err, \"HTTP request handler error\");\n if (!res.headersSent) {\n res.writeHead(500, { \"content-type\": \"application/json\" });\n res.end(JSON.stringify({ error: \"internal error\" }));\n }\n });\n });\n this.wss = new WebSocketServer({\n server: this.httpServer,\n autoPong: true,\n maxPayload: 100 * 1024 * 1024,\n // backlog: //max pending connections queue (default: 511)\n });\n\n this.setupWebSocketHandlers();\n this.subscribeToInstanceChannel();\n }\n\n /**\n * Per-session heartbeat. Fires every `HEARTBEAT_INTERVAL_MS` and does\n * both jobs on the same tick:\n *\n * 1. Inactivity check + ws-protocol ping (dead-TCP detection).\n * 2. App-level `{type:\"ping\"}` if the channel has been idle for at\n * least one heartbeat interval (client-watchdog refresh).\n *\n * The `lastSentAt` gate on (2) preserves the original \"only ping when\n * idle\" semantics: a session producing sub:updates faster than the\n * heartbeat interval never emits a redundant app-ping.\n */\n private startSessionHeartbeat(session: Session<TContext>): void {\n session.heartbeatTimer = setInterval(() => {\n // (1) Dead-TCP detection.\n if (Date.now() - session.lastReceivedAt > CLIENT_TIMEOUT_MS) {\n logger.info(`ws inactivity → terminate session ${session.id}`);\n try { session.ws.terminate(); } catch { /* close handler cleans up */ }\n return;\n }\n try {\n session.ws.ping();\n } catch {\n // socket already broken — close handler will clean up\n }\n\n // (2) Client-watchdog refresh, idle-only.\n if (Date.now() - session.lastSentAt >= HEARTBEAT_INTERVAL_MS) {\n this.sendMessage(session, { type: \"ping\" });\n }\n }, HEARTBEAT_INTERVAL_MS);\n }\n\n private clearSessionTimers(session: Session<TContext>): void {\n if (session.heartbeatTimer) {\n clearInterval(session.heartbeatTimer);\n session.heartbeatTimer = null;\n }\n }\n\n registerRouter(router: Router<any, TContext>): void {\n this.router = router;\n this.contextRegistry = getContextRegistry(router.contextName);\n }\n\n async start(): Promise<void> {\n // Seed the in-memory retention watermark from the DB before serving; thereafter\n // the sub-manager's Redis broadcast keeps it fresh. Non-fatal on failure —\n // a 0 watermark is safe (nothing pruned → every cache entry is scannable).\n try {\n await this.retentionWatermark.loadFrom(this.db.impl);\n } catch (err) {\n logger.error(err, \"failed to load initial retention watermark\");\n }\n await new Promise<void>((resolve) => {\n this.httpServer.listen(this.config.port, this.config.host, () => {\n logger.info(`Server: ${this.instanceId} listening on ${this.config.host}:${this.config.port}`);\n resolve();\n });\n });\n await this.syncJobSchedules();\n }\n\n /**\n * Reconcile every registered recurring (cron) job with the scheduler. Safe\n * to call more than once. No-op when no scheduler is configured or the router\n * declares no cron jobs. Runs after the HTTP listener is up so the endpoints\n * the scheduler registers are already reachable.\n */\n private async syncJobSchedules(): Promise<void> {\n if (!this.scheduler || !this.contextRegistry) return;\n const jobs = this.contextRegistry\n .getJobProcedures()\n .filter((p) => !!p.cron)\n .map((p) => ({ name: p.name, cron: p.cron! }));\n if (jobs.length === 0) return;\n try {\n await this.scheduler.syncSchedules(jobs, (name) => this.endpointFor(name));\n logger.info(`Synced ${jobs.length} recurring job schedule(s)`);\n } catch (err) {\n logger.error(err, \"Failed to sync job schedules\");\n }\n }\n\n /** Absolute URL the scheduler POSTs to for a given job name. */\n endpointFor(name: string): string {\n return `${this.config.publicUrl}${this.jobPath}/${name}`;\n }\n\n /**\n * Enqueue a precise, one-shot run of a job (e.g. \"expire this record at\n * exactly `expiresAt`\"). Returns a handle whose `id` can be passed to\n * {@link cancelJob}. Requires a configured scheduler.\n */\n async scheduleJob(name: string, body: unknown, opts?: EnqueueOptions): Promise<ScheduleHandle> {\n if (!this.jobClient) throw new Error(\"No scheduler configured; cannot schedule jobs\");\n return this.jobClient.enqueue(name, body, opts);\n }\n\n /** Cancel a one-shot previously created by {@link scheduleJob}. */\n async cancelJob(handleId: string): Promise<void> {\n if (!this.jobClient) throw new Error(\"No scheduler configured; cannot cancel jobs\");\n return this.jobClient.cancel(handleId);\n }\n\n /**\n * The app-facing {@link JobClient} — hand it to your service layer so\n * procedures can schedule/cancel one-shots without reaching for the server.\n */\n getJobClient(): JobClient {\n return this.jobClient;\n }\n\n async stop(): Promise<void> {\n this.wss.close();\n for (const [ws] of this.sessions) {\n ws.close(1001, \"Server shutting down\");\n }\n this.rpcHandler?.stop();\n this.httpServer.close();\n logger.info(\"Server stopped\");\n }\n\n getStats() {\n return {\n instanceId: this.instanceId,\n activeConnections: this.stats.activeConnections,\n totalConnections: this.stats.totalConnections,\n activeSubscriptions: this.stats.activeSubscriptions,\n };\n }\n\n getInstanceId(): string {\n return this.instanceId;\n }\n\n /**\n * Plain-HTTP entrypoint (non-WS). Serves a liveness probe and the job\n * webhook: `POST {jobPath}/<jobName>`. Every job POST is authenticated by the\n * configured scheduler before the (server-only) job handler runs.\n */\n private async handleHttpRequest(req: IncomingMessage, res: ServerResponse): Promise<void> {\n const rawUrl = req.url ?? \"/\";\n const path = rawUrl.split(\"?\")[0]!;\n\n // Liveness probe — cheap, unauthenticated.\n if (req.method === \"GET\" && (path === \"/_health\" || path === `${this.jobPath}/_health`)) {\n return sendJson(res, 200, { status: \"ok\", instanceId: this.instanceId });\n }\n\n // One-shot HTTP RPC (query/mutation/action), if enabled.\n if (this.rpcHandler && this.rpcHandler.handlesPath(path)) {\n return this.rpcHandler.handle(req, res, path);\n }\n\n const jobPrefix = `${this.jobPath}/`;\n if (!path.startsWith(jobPrefix)) {\n return sendJson(res, 404, { error: \"not found\" });\n }\n if (req.method !== \"POST\") {\n return sendJson(res, 405, { error: \"method not allowed\" });\n }\n\n const name = decodeURIComponent(path.slice(jobPrefix.length));\n if (!name || name.includes(\"/\")) {\n return sendJson(res, 404, { error: \"unknown job\" });\n }\n\n if (!this.scheduler) {\n return sendJson(res, 503, { error: \"jobs not enabled\" });\n }\n\n const rawBody = await readRequestBody(req);\n\n // Authenticate the webhook BEFORE any lookup or work — a forged POST must\n // not even learn whether a job name exists.\n let verified = false;\n try {\n verified = await this.scheduler.verify({\n name,\n rawBody,\n headers: lowercaseHeaders(req.headers),\n url: this.endpointFor(name),\n });\n } catch (err) {\n logger.warn(err, `job \"${name}\" verification threw`);\n }\n if (!verified) {\n return sendJson(res, 401, { error: \"invalid signature\" });\n }\n\n const procedure = this.contextRegistry?.getProcedure(name);\n if (!procedure || procedure.type !== \"job\") {\n return sendJson(res, 404, { error: \"unknown job\" });\n }\n\n let input: unknown;\n try {\n const parsed = rawBody.trim().length > 0 ? JSON.parse(rawBody) : {};\n input = (procedure.inputSchema as ZodType).parse(parsed);\n } catch (err) {\n logger.warn(err, `job \"${name}\" received an invalid payload`);\n return sendJson(res, 400, { error: \"invalid payload\" });\n }\n\n try {\n const serverCtx = this.config.jobContext ? await this.config.jobContext() : undefined;\n const fn = procedure.fn as (ctx: JobCtx<TContext>, input: unknown) => Promise<unknown>;\n const result = await fn({ db: this.db, serverCtx }, input);\n logger.info(`job \"${name}\" completed`);\n return sendJson(res, 200, { ok: true, result });\n } catch (err) {\n // Return 500 so the scheduler retries (jobs must be idempotent).\n logger.error(err, `job \"${name}\" failed`);\n return sendJson(res, 500, { error: \"job failed\" });\n }\n }\n\n private setupWebSocketHandlers(): void {\n this.wss.on(\"connection\", (ws, req) => {\n this.handleConnection(ws, req).catch((err) => {\n logger.error(err, \"Connection handler error\")\n });\n });\n }\n\n private async handleConnection(ws: WebSocket, req: IncomingMessage): Promise<void> {\n this.stats.totalConnections++;\n this.stats.activeConnections++;\n\n const session: Session<TContext> = {\n id: uuidv4(),\n ws,\n serverCtx: undefined,\n subIds: new Set(),\n mutationQueue: [],\n mutationInFlight: false,\n actionInFlight: 0,\n lastReceivedAt: Date.now(),\n lastSentAt: Date.now(),\n heartbeatTimer: null,\n };\n logger.info(`New session: ${session.id}`);\n\n this.sessions.set(ws, session);\n this.sendMessage(session, { type: \"connected\", sessionId: session.id });\n\n // Any inbound frame (text, ws-ping, ws-pong) refreshes the liveness\n // clock. This is the only thing keeping `CLIENT_TIMEOUT_MS` from\n // tripping on a silent-but-live client, since the browser auto-pong\n // is the only thing such a client emits.\n ws.on(\"message\", (data) => {\n session.lastReceivedAt = Date.now();\n this.handleMessage(session, data.toString()).catch((err) => {\n logger.error(err, \"Message handler error\");\n });\n });\n ws.on(\"ping\", () => { session.lastReceivedAt = Date.now(); });\n ws.on(\"pong\", () => { session.lastReceivedAt = Date.now(); });\n\n this.startSessionHeartbeat(session);\n\n ws.on(\"close\", (code) => {\n logger.info(`Connection closed: ${session.id}, code: ${code}`);\n this.clearSessionTimers(session);\n this.handleDisconnect(session);\n this.sessions.delete(ws);\n this.stats.activeConnections--;\n });\n\n ws.on(\"error\", (err) => {\n logger.error(err, \"Websocket error\");\n });\n }\n\n private async handleMessage(session: Session<TContext>, message: string): Promise<void> {\n try {\n const rawMsg = JSON.parse(message);\n const parseResult = ClientMessageSchema.safeParse(rawMsg);\n if (!parseResult.success) {\n this.sendError(session, \"\", \"Invalid message format\", \"INVALID_MESSAGE\");\n return;\n }\n await this.processMessage(session, parseResult.data);\n } catch (err) {\n logger.error(err, \"Message handling error\");\n this.sendError(session, \"\", \"Parse error\", \"PARSE_ERROR\");\n }\n }\n\n private async processMessage(session: Session<TContext>, msg: ClientMessage): Promise<void> {\n switch (msg.type) {\n case \"auth\":\n await this.handleAuth(session, msg.data);\n break;\n case \"call\":\n await this.handleCall(session, msg.id, msg.procedure, msg.input);\n break;\n case \"subscribe\":\n await this.handleSubscribe(session, msg.id, msg.procedure, msg.input);\n break;\n case \"unsubscribe\":\n await this.handleUnsubscribe(session, msg.subId);\n break;\n }\n }\n\n private async handleAuth(session: Session<TContext>, data?: Record<string, unknown>): Promise<void> {\n if (!this.config.verifyAuth || !data) {\n this.sendMessage(session, { type: \"auth:success\", userId: \"anonymous\" });\n return;\n }\n\n try {\n const authResult = await this.config.verifyAuth(data, session.id);\n if (!authResult) {\n this.sendMessage(session, { type: \"auth:failed\", message: \"Invalid authentication\" });\n return;\n }\n\n session.serverCtx = authResult;\n this.sendMessage(session, { type: \"auth:success\", userId: authResult.user.userId });\n } catch (e) {\n if (e instanceof AuthenticationError) {\n this.sendMessage(session, {\n type: \"auth:failed\",\n message: \"Invalid authentication: \" + e.message,\n extra: e.extra,\n });\n } else {\n this.sendMessage(session, { type: \"auth:failed\", message: \"Invalid authentication\" });\n }\n }\n }\n\n /**\n * Park a mutation on this session's FIFO queue and resolve when it\n * eventually drains. The drainer is fire-and-forget: it picks up any\n * newly enqueued op as long as one drain loop is running on this\n * session. Callers (handleCall) own the response/error for their own\n * op via the returned promise.\n */\n private enqueueMutation(\n session: Session<TContext>,\n procedure: RegisteredProcedure<TContext>,\n validatedInput: unknown,\n ): Promise<MutationResult<unknown>> {\n return new Promise<MutationResult<unknown>>((resolve, reject) => {\n const pending: PendingMutation<TContext> = {\n procedure,\n validatedInput,\n resolve,\n reject,\n };\n session.mutationQueue.push(pending);\n this.drainMutationQueue(session).catch((err) =>\n logger.error(err, \"drainMutationQueue crashed\"),\n );\n });\n }\n\n /**\n * Drain the per-session mutation queue serially. Only one drain loop\n * runs at a time per session — re-entrant calls bail immediately, the\n * loop already in progress will pick up the new op before exiting.\n */\n private async drainMutationQueue(session: Session<TContext>): Promise<void> {\n if (session.mutationInFlight) return;\n session.mutationInFlight = true;\n try {\n while (session.mutationQueue.length > 0) {\n const op = session.mutationQueue.shift()!;\n try {\n const fn = op.procedure.fn as (\n ctx: MutationCtx<TContext>,\n input: unknown,\n ) => Promise<unknown>;\n const result = await this.db.mutation(async (ctx) => {\n return fn({ db: ctx, serverCtx: session.serverCtx }, op.validatedInput);\n });\n op.resolve(result);\n } catch (err) {\n op.reject(err);\n }\n }\n } finally {\n session.mutationInFlight = false;\n }\n }\n\n private async handleCall(\n session: Session<TContext>,\n id: string,\n procedureName: string,\n input: unknown\n ): Promise<void> {\n if (!this.router) {\n this.sendError(session, id, \"Router not registered\", \"SERVER_ERROR\");\n return;\n }\n\n const procedure = this.contextRegistry.getProcedure(procedureName);\n if (!procedure || procedure.type === \"job\") {\n this.sendError(session, id, `Procedure \"${procedureName}\" not found`, \"NOT_FOUND\");\n return;\n }\n\n try {\n const validatedInput = (procedure.inputSchema as ZodType).parse(input);\n\n if (procedure.type === \"query\") {\n // One-shot query: same cache/identity/freshness model as a live\n // subscription (see runCachedQuery), just without tracking a sub.\n // Load the freshness upper bound here, mirroring handleSubscribe which\n // loads lastSnapshotTs before registering.\n const lastSnapshotTs = await this.db.impl.getLatestSnapshotTimestamp();\n const result = await runCachedQuery(\n this.oneShotDeps,\n procedure,\n procedureName,\n validatedInput,\n session.serverCtx,\n lastSnapshotTs,\n );\n\n this.sendMessage(session, {\n type: \"response\",\n id: id,\n success: true,\n data: result,\n });\n } else if (procedure.type === \"mutation\") {\n if (session.mutationQueue.length >= MAX_MUTATION_QUEUE_BUFFER) {\n this.sendError(\n session,\n id,\n `Too many concurrent mutations on this connection (max ${MAX_MUTATION_QUEUE_BUFFER})`,\n \"TOO_MANY_CONCURRENT_MUTATIONS\",\n );\n return;\n }\n\n const mutationResult = await this.enqueueMutation(session, procedure, validatedInput);\n\n // Invalidation is driven by the db's after-commit hook (wired in the\n // constructor) for every mutation source, so we don't invalidate again\n // here — we just return the result to the caller.\n this.sendMessage(session, {\n type: \"response\",\n id: id,\n success: true,\n data: mutationResult.result,\n });\n } else if (procedure.type === \"action\") {\n if (session.actionInFlight >= MAX_ACTION_CONCURRENCY) {\n this.sendError(\n session,\n id,\n `Too many concurrent actions on this connection (max ${MAX_ACTION_CONCURRENCY})`,\n \"TOO_MANY_CONCURRENT_ACTIONS\",\n );\n return;\n }\n\n session.actionInFlight++;\n let result: unknown;\n try {\n const fn = procedure.fn as (ctx: ActionCtx<TContext>, input: unknown) => Promise<unknown>;\n result = await fn(\n { db: this.db, serverCtx: session.serverCtx, storage: this.storage, scheduler: this.jobClient },\n validatedInput,\n );\n } finally {\n session.actionInFlight--;\n }\n\n this.sendMessage(session, {\n type: \"response\",\n id: id,\n success: true,\n data: result,\n });\n }\n\n\n } catch (err) {\n logger.error(err, `Error executing \"${procedureName}\"`);\n this.sendError(\n session,\n id,\n err instanceof Error ? err.message : \"Execution failed\",\n \"EXECUTION_ERROR\"\n );\n }\n }\n\n private async handleSubscribe(\n session: Session<TContext>,\n id: string,\n procedureName: string,\n input: unknown\n ): Promise<void> {\n if (!this.router) {\n this.sendError(session, id, \"Router not registered\", \"SERVER_ERROR\");\n return;\n }\n\n const procedure = this.contextRegistry.getProcedure(procedureName);\n if (!procedure || procedure.type !== \"query\") {\n this.sendError(session, id, `Procedure \"${procedureName}\" not found or not a query`, \"NOT_FOUND\");\n return;\n }\n\n try {\n const validatedInput = procedure.inputSchema.parse(input);\n const queryIdentity = resolveQueryIdentity(procedure, session.serverCtx, validatedInput, this.config.getUserId);\n const inputJsonStable = stableStringify(validatedInput);\n const subId = await generateSubscriptionId(procedureName, inputJsonStable, queryIdentity);\n const cacheKey = await generateCacheKey(procedureName, inputJsonStable, queryIdentity);\n const existingServerSub = this.serverSubscriptions.get(subId);\n\n let data: unknown;\n let dataHash: string;\n let liveResult: LiveResult<unknown> | null = null;\n\n // track this session subscribed this subscription\n session.subIds.add(subId);\n\n if (existingServerSub) {\n existingServerSub.subscribedTimes += 1;\n\n const cached = await this.cache.getQueryCacheFor(existingServerSub.cacheKey);\n if (cached) {\n data = cached.data;\n } else {\n liveResult = await this.executeLiveQuery(procedureName, validatedInput, session.serverCtx);\n data = liveResult.data;\n }\n } else {\n const lastSnapshotTs = await this.db.impl.getLatestSnapshotTimestamp();\n const subManagerResult = await this.subManagerClient.registerSubscription({\n subId,\n cacheKey,\n queryName: procedureName,\n args: validatedInput,\n queryIdentity,\n instanceName: this.instanceId,\n lastSnapshotTs: lastSnapshotTs,\n });\n\n const cached = await this.cache.getQueryCacheFor(subManagerResult.cacheKey);\n // Serve the cached blob only when the sub-manager says it's fresh AND its\n // version matches the metadata that freshness was validated against — a\n // divergent data/metadata pair (torn read, cross-process write race) is\n // treated as a miss and recomputed rather than served stale.\n if (cached && !subManagerResult.recompute && cached.version === subManagerResult.version) {\n data = cached.data;\n } else {\n liveResult = await this.executeLiveQuery(procedureName, validatedInput, session.serverCtx);\n data = liveResult.data;\n }\n\n this.serverSubscriptions.set(subId, {\n subId,\n cacheKey,\n subscribedTimes: 1,\n });\n this.stats.activeSubscriptions++;\n }\n\n if (liveResult) {\n const [updateResult] = await Promise.all([\n this.subManagerClient.updateSubscriptionReadSet({\n cacheKey,\n subId,\n lastSnapshotTs: liveResult.ts,\n readSet: liveResult.readSet,\n instanceName: this.instanceId,\n }),\n // version == the compute ts; the sub-worker writes the matching\n // metadata version via updateSubscriptionReadSet above.\n this.cache.setQueryCacheFor(cacheKey, data, String(liveResult.ts)),\n ]);\n\n // If the sub-manager just told us this subId isn't tracked (we hit\n // a window between its register and a separate restart), our register\n // above should have rebuilt the state — log loudly if not.\n if (!updateResult.tracked) {\n logger.warn(\n `updateSubscriptionReadSet returned tracked:false immediately after register for ${subId} ` +\n `— possible sub-manager restart mid-subscribe`,\n );\n }\n }\n\n dataHash = await getHashOf(data);\n\n // Hand the canonical subId back to the client so it doesn't need to\n // recompute the hash locally (which would require pulling in wasm and\n // wouldn't reflect per-procedure `queryIdentity` overrides).\n this.sendMessage(session, { type: \"response\", id, success: true, subId });\n this.sendMessage(session, { type: \"sub:update\", subId, data, dataHash });\n\n logger.info(`Subscribed: ${subId} for session ${session.id}`);\n } catch (err) {\n logger.error(err, `Error subscribing to \"${procedureName}\"`);\n this.sendError(session, id, err instanceof Error ? err.message : \"Subscribe failed\", \"SUBSCRIBE_ERROR\");\n }\n }\n\n private async handleUnsubscribe(session: Session<TContext>, subId: string): Promise<void> {\n const serverSub = this.serverSubscriptions.get(subId);\n if (!serverSub) {\n session.subIds.delete(subId);\n return;\n }\n\n serverSub.subscribedTimes -= 1;\n if (serverSub.subscribedTimes <= 0) {\n session.subIds.delete(subId);\n this.serverSubscriptions.delete(subId);\n this.stats.activeSubscriptions--;\n\n try {\n await this.subManagerClient.unregisterSubscription({ subId, instanceName: this.instanceId });\n } catch (err) {\n logger.error(err, \"Failed to unregister from sub-manager:\");\n }\n }\n\n logger.info(`Unsubscribed: ${subId} for session ${session.id}, remaining server subs: ${serverSub.subscribedTimes}`);\n }\n\n private async handleDisconnect(session: Session<TContext>): Promise<void> {\n logger.info(`Session disconnected: ${session.id}`);\n\n let unregisterSubs: SubscriptionEntry[] | undefined = undefined;\n for (const subId of session.subIds) {\n const sub = this.serverSubscriptions.get(subId);\n if (!sub) continue;\n\n sub.subscribedTimes -= 1;\n if (sub.subscribedTimes <= 0) {\n if (!unregisterSubs) {\n unregisterSubs = [];\n }\n\n unregisterSubs.push(sub);\n }\n }\n\n if (!unregisterSubs || unregisterSubs.length === 0) return;\n\n try {\n await this.subManagerClient.unregisterSubscriptions(\n unregisterSubs.map((sub) => ({ subId: sub.subId, instanceName: this.instanceId })),\n );\n } catch (err) {\n logger.error(err, \"Failed to unregister from sub-manager:\");\n }\n\n for (const sub of unregisterSubs) {\n // if still not subscribing\n if (sub.subscribedTimes <= 0) {\n this.serverSubscriptions.delete(sub.subId);\n this.stats.activeSubscriptions--;\n }\n }\n\n logger.info(`All Subscription sessions is disconnected: ${session.id}, subs: ${unregisterSubs.length}`);\n }\n\n /**\n * Drives recovery on a sub-manager reconnect. Iterates this app server's\n * `serverSubscriptions`, reads each subId's registration record from\n * Redis (`sl:reg:<subId>`), and re-registers it with the sub-manager.\n *\n * • If the Redis record is missing, another app-server instance fully\n * unregistered the sub during our disconnect. We drop it locally and\n * send `sub:gone` to every session that was holding it.\n * • If the sub-manager reports `recompute: true` (cache-metadata + commit\n * log replay detected staleness), we kick a reexec for that sub.\n *\n * Returns after all subs have been processed. Only then does the\n * SubManagerClient flush its buffered invalidate writeSets.\n */\n private async runSubManagerRecovery(): Promise<void> {\n const snapshot = Array.from(this.serverSubscriptions.values());\n if (snapshot.length === 0) {\n logger.info(\"Sub-manager recovery: no subscriptions to restore\");\n return;\n }\n\n logger.info(`Sub-manager recovery: restoring ${snapshot.length} subscription(s)`);\n\n const lastSnapshotTs = await this.db.impl.getLatestSnapshotTimestamp();\n const subIds = snapshot.map((e) => e.subId);\n const recordsMap = await this.cache.fetchRegistrationRecordsBatch(subIds);\n\n const registerParams: Array<{\n entry: typeof snapshot[0];\n record: RegistrationRecord;\n }> = [];\n for (const entry of snapshot) {\n const record = recordsMap.get(entry.subId) ?? null;\n if (!record) {\n this.dropSubscriptionLocally(entry.subId, \"registration not found after recovery\");\n continue;\n }\n registerParams.push({ entry, record });\n }\n\n const recompute: AffectedSubscription[] = [];\n const BATCH_SIZE = 50;\n for (let i = 0; i < registerParams.length; i += BATCH_SIZE) {\n const batch = registerParams.slice(i, i + BATCH_SIZE);\n const params = batch.map(({ entry, record }) => ({\n subId: entry.subId,\n cacheKey: entry.cacheKey,\n queryName: record.queryName,\n args: record.args,\n queryIdentity: record.queryIdentity,\n instanceName: this.instanceId,\n lastSnapshotTs,\n }));\n try {\n const results = await this.subManagerClient.registerSubscriptionBatch(params);\n for (let j = 0; j < batch.length; j++) {\n const { entry, record } = batch[j]!;\n const result = results[j]!;\n if (result.recompute) {\n recompute.push({\n subId: entry.subId,\n queryName: record.queryName,\n args: record.args,\n cacheKey: entry.cacheKey,\n notifyInstances: [],\n });\n }\n }\n } catch (err) {\n logger.error(err, `Sub-manager recovery: failed to batch register ${i}-${i + batch.length}`);\n }\n }\n\n await Promise.allSettled(recompute.map((a) => this.reexecuteAffectedQuery(a)));\n\n logger.info(`Sub-manager recovery: done (${recompute.length} reexec'd)`);\n }\n\n private async runAffectedHandler(affected: AffectedSubscription[]) {\n await Promise.allSettled(\n affected.map((a) => this.reexecuteAffectedQuery(a)),\n );\n }\n\n /**\n * Removes a subscription from this app server's state and notifies every\n * session that was holding it. Used during recovery when the central\n * registration record is gone, or after a `tracked: false` reply from the\n * sub-manager that we can't reconcile.\n */\n private dropSubscriptionLocally(subId: string, reason: string): void {\n const serverSub = this.serverSubscriptions.get(subId);\n let notified = 0;\n if (serverSub) {\n this.serverSubscriptions.delete(subId);\n this.stats.activeSubscriptions = Math.max(0, this.stats.activeSubscriptions - 1);\n\n for (const [, session] of this.sessions) {\n if (session.subIds.has(subId)) {\n notified++;\n session.subIds.delete(subId);\n this.sendMessage(session, { type: \"sub:gone\", subId, reason });\n }\n }\n }\n\n logger.warn(`Dropped subscription ${subId} locally (${reason}); notified ${notified} session(s)`);\n }\n\n private async handleMutationCommit(writeSet: WriteEntry[], commitTs: CommitTs): Promise<void> {\n if (writeSet.length === 0) return;\n\n const tables = [...new Set(writeSet.map((w) => w.table))];\n logger.debug(`Mutation committed for tables: [${tables.join(\", \")}]`);\n\n try {\n const result = await this.subManagerClient.invalidateWriteset({ writeSet });\n logger.debug({ affected: result }, \"Sub-managers invalidation result\");\n\n if (result?.affected?.length > 0) {\n await Promise.allSettled(\n result.affected.map((affected) => this.dispatchAffected(affected, commitTs)),\n );\n }\n } catch (err) {\n logger.error(err, \"Failed to notify sub-managers\");\n }\n }\n\n /**\n * Route an affected sub to whichever instance can actually run the\n * recompute:\n * • If this instance has a local session for the sub, recompute here\n * (fast path) and broadcast the result to every other instance.\n * • Otherwise broadcast a `recompute-race` to every instance in\n * `notifyInstances`. Each receiver tries to acquire a Redis lock\n * keyed by (subId, commitTs); the winner recomputes and publishes\n * an `update` via the normal broadcast path, losers no-op.\n *\n * If `notifyInstances` is empty (the sub has no live holders), we drop\n * the message — the next subscribe will recompute fresh state.\n */\n private async dispatchAffected(affected: AffectedSubscription, commitTs: CommitTs): Promise<void> {\n if (this.hasLocalSessionFor(affected.subId)) {\n await this.reexecuteAffectedQuery(affected);\n return;\n }\n\n if (affected.notifyInstances.length === 0) {\n logger.debug(\n `dispatchAffected: no live instances hold ${affected.subId} — dropping`,\n );\n return;\n }\n\n await this.publishRecomputeRace(affected, commitTs);\n }\n\n private hasLocalSessionFor(subId: SubId): boolean {\n for (const [, session] of this.sessions) {\n if (session.subIds.has(subId)) return true;\n }\n return false;\n }\n\n private async publishRecomputeRace(\n affected: AffectedSubscription,\n commitTs: CommitTs,\n ): Promise<void> {\n const payload = supaliveStringify({\n type: \"recompute-race\",\n subId: affected.subId,\n cacheKey: affected.cacheKey,\n queryName: affected.queryName,\n args: affected.args,\n notifyInstances: affected.notifyInstances,\n commitTs: commitTs.toString(),\n originInstance: this.instanceId,\n });\n\n const publishPromises = affected.notifyInstances.map(async (instance) => {\n if (instance === this.instanceId) return;\n\n const channel = `sub:instance:${instance}`;\n try {\n await redisRetryPolicy.execute(() => this.cache.redis.publish(channel, payload));\n } catch (err) {\n logger.error(\n err,\n `Failed to publish recompute-race to ${channel} after many retries`,\n );\n }\n });\n\n await Promise.all(publishPromises);\n }\n\n private async reexecuteAffectedQuery(affected: AffectedSubscription): Promise<void> {\n let foundSession: Session<TContext> | null = null;\n\n for (const [, session] of this.sessions) {\n if (session.subIds.has(affected.subId)) {\n foundSession = session;\n break;\n }\n }\n\n if (!foundSession) {\n // Should only happen if every local session unsubscribed between\n // dispatchAffected's check and now. dispatchAffected forwards to\n // another instance when no local session exists, so we'd lose the\n // update for everyone in this rare race. Accept the gap.\n logger.debug(`Session not found for subId: ${affected.subId}`);\n return;\n }\n\n try {\n const liveResult = await this.executeLiveQuery(affected.queryName, affected.args, foundSession.serverCtx);\n const dataHash = await getHashOf(liveResult.data);\n\n const [updateResult] = await Promise.all([\n this.subManagerClient.updateSubscriptionReadSet({\n cacheKey: affected.cacheKey,\n subId: affected.subId,\n lastSnapshotTs: liveResult.ts,\n readSet: liveResult.readSet,\n instanceName: this.instanceId,\n }),\n this.cache.setQueryCacheFor(affected.cacheKey, liveResult.data, String(liveResult.ts)),\n ]);\n\n // `tracked: false` means the sub-manager has no record of this subId\n // (it restarted before we recovered, or a concurrent unregister wiped\n // it). Drop the sub locally and let clients re-subscribe.\n if (!updateResult.tracked) {\n this.dropSubscriptionLocally(affected.subId, \"no longer tracked by sub-manager\");\n return;\n }\n\n // find any session with same subId and send update\n for (const [, session] of this.sessions) {\n if (session.subIds.has(affected.subId)) {\n this.sendMessage(session, {\n type: \"sub:update\",\n subId: affected.subId,\n data: liveResult.data,\n dataHash,\n });\n }\n }\n\n if (affected.notifyInstances.length > 0) {\n await this.publishSubscriptionUpdate(\n affected.subId,\n liveResult.data,\n dataHash,\n affected.notifyInstances\n );\n }\n\n logger.debug(`Re-executed affected query for subId: ${affected.subId}`);\n } catch (err) {\n logger.error(err, `Failed to re-execute query for subId ${affected.subId}`);\n }\n }\n\n private async executeLiveQuery(\n procedureName: string,\n input: unknown,\n serverCtx?: TContext\n ): Promise<LiveResult<unknown>> {\n const procedure = this.contextRegistry.getProcedure(procedureName);\n if (!procedure || procedure.type !== \"query\") {\n throw new Error(`Procedure not defined: ${procedureName}`);\n }\n\n return this.db.liveQuery(async (ctx) => {\n const fn = procedure.fn as (ctx: QueryCtx<TContext>, input: unknown) => Promise<unknown>;\n return fn({ db: ctx, serverCtx }, input);\n });\n }\n\n private sendMessage(session: Session<TContext>, message: ServerMessage): void {\n if (session.ws.readyState !== WebSocket.OPEN) return;\n session.ws.send(supaliveStringify(message));\n // Mark the channel as having had recent outbound traffic. The\n // heartbeat tick uses this to skip the idle-only app-level ping\n // for busy sessions (no point pinging a client that just got a\n // sub:update).\n session.lastSentAt = Date.now();\n }\n\n private sendMessageRaw(session: Session<TContext>, message: string): void {\n if (session.ws.readyState === WebSocket.OPEN) {\n session.ws.send(message);\n }\n }\n\n private sendError(session: Session<TContext>, id: string, error: string, code: string): void {\n this.sendMessage(session, { type: \"response\", id, success: false, error, code });\n }\n\n private async subscribeToInstanceChannel(): Promise<void> {\n const instanceChannel = `sub:instance:${this.instanceId}`;\n const schemaChannel = SCHEMA_RELOAD_CHANNEL;\n const watermarkChannel = RETENTION_WATERMARK_CHANNEL;\n\n // App-level channels (config.appPubSub) ride this SAME subscribe connection —\n // no extra Redis connection per instance.\n const appChannels = [...this.appPubSubChannels];\n await this.redisSub.subscribe(instanceChannel, schemaChannel, watermarkChannel, ...appChannels);\n this.redisSub.on(\"connect\", () => {\n logger.info(\n `Subscribed to Redis channels: ${[instanceChannel, schemaChannel, watermarkChannel, ...appChannels].join(\", \")}`,\n );\n });\n this.redisSub.on(\"message\", (channel: string, message: string) => {\n if (channel === instanceChannel) {\n this.handleRemoteInstanceMessage(message).catch((err) => {\n logger.error(err, \"Error handling remote instance message\");\n });\n } else if (channel === schemaChannel) {\n this.handleSchemaReload().catch((err) => {\n logger.error(err, \"Error handling schema reload\");\n });\n } else if (channel === watermarkChannel) {\n // Sub-manager advertised a new retention watermark after a prune. Adopt it\n // (monotonic — a stale/out-of-order message can't lower it).\n try {\n this.retentionWatermark.set(BigInt(message));\n } catch (err) {\n logger.error(err, `Bad retention watermark message: ${message}`);\n }\n } else if (this.appPubSubChannels.has(channel)) {\n // App-level message (auth revocations, etc.). The handler owns decoding;\n // we just guard against it throwing.\n Promise.resolve(this.config.appPubSub?.onMessage(channel, message)).catch((err) =>\n logger.error(err, `Error in appPubSub handler for channel ${channel}`),\n );\n }\n });\n this.redisSub.on(\"error\", (err: Error) => {\n logger.error(err, `Subscriber error for channels: ${instanceChannel}, ${schemaChannel}`)\n });\n }\n\n /**\n * React to a `schema:reload` broadcast: invalidate the in-memory column\n * type cache for the named tables (or all if omitted) and re-bootstrap\n * from Postgres so subsequent batched writes see the fresh types. Skips\n * messages published by this instance (we already have the new types).\n */\n private async handleSchemaReload(): Promise<void> {\n logger.info(`Schema reload request received in instance ${this.instanceId} — reloading schema...`);\n try {\n await sleep(1000 + (Math.random() * 5000)); // jitter to avoid thundering herd if many instances\n await bootstrapDbTypes(this.db, this.cache, true);\n logger.info(`Schema reload applied in instance ${this.instanceId}`);\n } catch (err) {\n logger.error(err, \"Failed to reload schema\");\n return;\n }\n }\n\n /**\n * Broadcast a schema-reload signal to every server instance (including\n * this one — the handler skips self-originated messages, so calling\n * after a local migration is safe). Pass `tables` to scope the reload;\n * omit to flush everyone's cache.\n */\n async publishSchemaReload(tables?: string[]): Promise<void> {\n const payload = JSON.stringify({\n type: \"schema:reload\",\n tables,\n originInstance: this.instanceId,\n });\n await redisRetryPolicy.execute(() =>\n this.cache.redis.publish(SCHEMA_RELOAD_CHANNEL, payload));\n }\n\n /**\n * Publish an app-level message (see {@link SupaliveServerConfig.appPubSub}) to\n * a Redis channel. Rides the shared command connection and is delivered to\n * every instance subscribed to `channel`, INCLUDING this one — so a handler\n * should tolerate/skip self-originated messages if that matters. The message\n * is an opaque string; the app owns encoding. Uses the standard Redis retry\n * policy; callers typically fire-and-forget.\n */\n async publishAppMessage(channel: string, message: string): Promise<void> {\n await redisRetryPolicy.execute(() => this.cache.redis.publish(channel, message));\n }\n\n private async handleRemoteInstanceMessage(message: string): Promise<void> {\n let parsed: RemoteSubscriptionMessage;\n try {\n parsed = RemoteSubscriptionMessageSchema.parse(JSON.parse(message));\n } catch (err) {\n logger.error(err, \"Failed to parse remote instance message\");\n return;\n }\n\n if (parsed.originInstance === this.instanceId) return;\n\n if (parsed.type === \"update\") {\n for (const [, session] of this.sessions) {\n if (!session.subIds.has(parsed.subId)) continue;\n\n const subEntry = this.serverSubscriptions.get(parsed.subId);\n if (!subEntry) continue;\n\n this.sendMessage(session, {\n type: \"sub:update\",\n subId: parsed.subId,\n data: parsed.data,\n dataHash: parsed.dataHash,\n });\n }\n return;\n }\n\n // type === \"recompute-race\" — writer didn't hold the sub locally and\n // broadcast a race hint to every notifyInstance. The first to acquire\n // the lock recomputes; the rest no-op.\n await this.handleRemoteRecomputeRace(parsed);\n }\n\n private async handleRemoteRecomputeRace(req: RemoteRecomputeRace): Promise<void> {\n if (!this.hasLocalSessionFor(req.subId)) {\n // We're in notifyInstances per sub-manager but the sub has since\n // been dropped here. Another holder may still pick it up — no warn.\n return;\n }\n\n const won = await this.cache.tryAcquireRecomputeLock(\n this.instanceId, req.subId, req.commitTs, RECOMPUTE_LOCK_TTL_SECONDS,\n );\n if (!won) {\n logger.debug(\n `recompute-race: lost lock for ${req.subId}@${req.commitTs}, waiting for winner's update`,\n );\n return;\n }\n\n await this.reexecuteAffectedQuery({\n subId: req.subId,\n queryName: req.queryName,\n args: req.args,\n cacheKey: req.cacheKey,\n notifyInstances: req.notifyInstances,\n });\n }\n\n private async publishSubscriptionUpdate(\n subId: string,\n data: unknown,\n dataHash: string,\n notifyInstances: string[]\n ): Promise<void> {\n const payload = supaliveStringify({\n type: \"update\",\n subId,\n data,\n dataHash,\n originInstance: this.instanceId,\n });\n\n const publishPromises = notifyInstances.map(async (instance) => {\n if (instance === this.instanceId) return;\n\n const channel = `sub:instance:${instance}`;\n try {\n await redisRetryPolicy.execute(() => this.cache.redis.publish(channel, payload));\n } catch (err) {\n logger.error(err, `Failed to publish to ${channel} after many retries`);\n }\n });\n\n await Promise.all(publishPromises);\n }\n}\n\n// ─── HTTP helpers (job endpoint) ─────────────────────────────────────────────\n\n/** Normalize a job path to a leading-slash, no-trailing-slash prefix. */\nfunction normalizeJobPath(p: string): string {\n let s = p.startsWith(\"/\") ? p : `/${p}`;\n if (s.length > 1 && s.endsWith(\"/\")) s = s.slice(0, -1);\n return s;\n}\n\nfunction sendJson(res: ServerResponse, status: number, body: unknown): void {\n const payload = supaliveStringify(body);\n res.writeHead(status, { \"content-type\": \"application/json\" });\n res.end(payload);\n}\n\nfunction lowercaseHeaders(headers: IncomingMessage[\"headers\"]): Record<string, string | undefined> {\n const out: Record<string, string | undefined> = {};\n for (const [k, v] of Object.entries(headers)) {\n out[k.toLowerCase()] = Array.isArray(v) ? v.join(\",\") : v;\n }\n return out;\n}\n\n/** Buffer an incoming request body to a string, capped to guard against abuse. */\nfunction readRequestBody(req: IncomingMessage, maxBytes = 1024 * 1024): Promise<string> {\n return new Promise((resolve, reject) => {\n const chunks: Buffer[] = [];\n let size = 0;\n req.on(\"data\", (chunk: Buffer) => {\n size += chunk.length;\n if (size > maxBytes) {\n reject(new Error(\"request body too large\"));\n req.destroy();\n return;\n }\n chunks.push(chunk);\n });\n req.on(\"end\", () => resolve(Buffer.concat(chunks).toString(\"utf8\")));\n req.on(\"error\", reject);\n });\n}\n","/**\n * FNV-1a 32-bit hash.\n *\n * Used for routing subscription operations to logical workers inside the\n * sub-manager process. Same input always returns the same 32-bit unsigned\n * integer in both JavaScript and Go, so a future Go port of the sub-manager\n * can use the identical routing without rebalancing keys.\n *\n * Reference: http://www.isthe.com/chongo/tech/comp/fnv/\n */\nexport function fnv1a32(s: string): number {\n let h = 0x811c9dc5;\n for (let i = 0; i < s.length; i++) {\n h ^= s.charCodeAt(i);\n // h = h * 16777619, expressed as shifts to stay in 32-bit space\n h = (h + ((h << 1) + (h << 4) + (h << 7) + (h << 8) + (h << 24))) >>> 0;\n }\n return h >>> 0;\n}\n","import { Server as WebSocketServer } from \"rpc-websockets\";\nimport { Worker } from \"node:worker_threads\";\nimport { fileURLToPath } from \"node:url\";\nimport { SubscriptionManagerConfig } from \"../config\";\nimport { fnv1a32 } from \"./hash\";\nimport { subscriptionManagerDataPack } from \"./subscription-manager-client\";\n\nimport { logger as baseLogger, logLevelOf } from \"../logger\";\nimport {\n AffectedSubscription,\n InvalidateWritesetResult,\n RegisterSubscriptionBatchResult,\n RegisterSubscriptionParams,\n RegisterSubscriptionResult,\n UnregisterSubscriptionParams,\n UnregisterSubscriptionResult,\n UnregisterSubscriptionsParams,\n UnregisterSubscriptionsResult,\n UpdateSubscriptionReadSetResult,\n} from \"./subscription-manager-types\";\nimport { groupByToMap } from \"../helper\";\nimport {\n MsgType,\n type WorkerInitPayload,\n type WorkerRequest,\n type WorkerResponse,\n} from \"./subscription-manager-worker-protocol\";\n\n// Re-export SubscriptionWorker so existing test imports\n// (`import { SubscriptionWorker } from \"./subscription-manager\"`) keep working.\nexport { SubscriptionWorker } from \"./subscription-worker\";\n\nconst logger = baseLogger.child(\n { name: \"SUBS_MGR\" },\n { level: logLevelOf(\"SUBS_MANAGER_SERVER_LOG_LEVEL\") },\n);\n\n// Resolve the entry script next to this compiled file. `import.meta.url` works\n// for both Bun (running TS source) and Node (running emitted JS) — we just\n// swap the extension based on the current module's path.\nfunction resolveWorkerEntry(): { url: URL; isTs: boolean } {\n const here = new URL(import.meta.url);\n const isTs = here.pathname.endsWith(\".ts\");\n const ext = isTs ? \".ts\" : \".js\";\n return {\n url: new URL(`./subscription-manager-worker-entry${ext}`, import.meta.url),\n isTs,\n };\n}\n\n// When the entry is a `.ts` file and we're running on Node (not Bun), the\n// worker thread inherits no module loader — Node can strip types but its bare\n// ESM resolver requires explicit file extensions, which our extensionless\n// internal imports don't satisfy. Register tsx as the worker loader so it\n// resolves the same way the parent module does.\nfunction workerExecArgv(isTs: boolean): string[] | undefined {\n if (!isTs) return undefined;\n if (typeof (globalThis as any).Bun !== \"undefined\") return undefined;\n return [\"--import\", \"tsx\"];\n}\n\n// ─── Worker-thread proxy ─────────────────────────────────────────────────────\n\ninterface PendingRequest {\n resolve: (value: unknown) => void;\n reject: (err: Error) => void;\n}\n\nclass SubscriptionWorkerHandle {\n readonly id: number;\n private worker: Worker;\n private pending = new Map<number, PendingRequest>();\n private nextReqId = 1;\n private readyPromise: Promise<void>;\n\n constructor(id: number, init: WorkerInitPayload) {\n this.id = id;\n const entry = resolveWorkerEntry();\n this.worker = new Worker(fileURLToPath(entry.url), {\n execArgv: workerExecArgv(entry.isTs),\n });\n\n this.worker.on(\"message\", (res: WorkerResponse) => {\n const pending = this.pending.get(res.reqId);\n if (!pending) return;\n\n this.pending.delete(res.reqId);\n if (res.ok) {\n pending.resolve(res.result);\n } else {\n const err = new Error(res.error.message);\n if (res.error.stack) err.stack = res.error.stack;\n pending.reject(err);\n }\n });\n\n this.worker.on(\"error\", (err: unknown) => {\n logger.error(err, `[worker ${this.id}] worker thread errored`);\n const e = err instanceof Error ? err : new Error(String(err));\n for (const [, p] of this.pending) p.reject(e);\n this.pending.clear();\n });\n\n this.readyPromise = this.send<null>(MsgType.Init, init).then(() => { });\n }\n\n ready(): Promise<void> {\n return this.readyPromise;\n }\n\n send<T>(type: MsgType, payload: unknown): Promise<T> {\n const reqId = this.nextReqId++;\n return new Promise<T>((resolve, reject) => {\n this.pending.set(reqId, {\n resolve: resolve as (v: unknown) => void,\n reject,\n });\n const req: WorkerRequest = { type, reqId, payload };\n this.worker.postMessage(req);\n });\n }\n\n async shutdown(): Promise<void> {\n try {\n await this.send<null>(MsgType.Shutdown, null);\n } catch (_) { /* ignore */ }\n await this.worker.terminate();\n }\n}\n\n// ─── Dispatcher ──────────────────────────────────────────────────────────────\n\n/**\n * One process per deployment. Routes each subscription operation to one of N\n * worker threads by hashing subId. Worker threads run in parallel on separate\n * CPU cores; this dispatcher does no Zod parsing — it reads `subId` directly\n * from the decoded payload (the internal channel is trusted) and forwards the\n * raw payload. The worker validates and parses with Zod before executing.\n */\nexport class SubscriptionManager {\n private wss: WebSocketServer;\n private port: number;\n private workers: SubscriptionWorkerHandle[];\n private pruneInterval: ReturnType<typeof setInterval> | null = null;\n private static readonly PRUNE_INTERVAL_MS = 7 * 24 * 60 * 60 * 1000;\n\n constructor(config: SubscriptionManagerConfig) {\n this.port = config.port;\n this.wss = new WebSocketServer({ port: this.port }, subscriptionManagerDataPack);\n this.wss.dataPack = subscriptionManagerDataPack;\n\n const numWorkers = resolveWorkerCount(config.workers);\n const maxConnectionsPerWorker = resolveMaxConnectionsPerWorker(config, numWorkers);\n\n logger.info(`Spawning ${numWorkers} subscription worker${numWorkers === 1 ? \"\" : \"s\"}`);\n this.workers = Array.from({ length: numWorkers }, (_, i) =>\n new SubscriptionWorkerHandle(i + 1, {\n workerId: i + 1,\n database: config.database,\n upstash: config.upstash,\n cacheTTLSeconds: config.cacheTTLSeconds,\n maxConnectionsPerWorker,\n }),\n );\n\n this.registerMethods();\n }\n\n async start(): Promise<void> {\n await Promise.all(this.workers.map((w) => w.ready()));\n logger.info(`Listening on port ${this.port}`);\n // Seed the retention watermark before serving so the query-cache freshness\n // check has an accurate watermark immediately (it survives restarts in the\n // metadata table; only pruning moves it afterward). Worker 0 seeds/repairs\n // it from the DB and returns it; fan it out to every worker's in-memory copy\n // (and, via Redis, to app-server instances).\n try {\n const minRetainedTs = await this.workers[0]!.send<bigint>(MsgType.RefreshRetentionWatermark, null);\n await this.broadcastRetentionWatermark(minRetainedTs);\n } catch (err) {\n logger.error(err, \"initial retention watermark seed failed\");\n }\n this.pruneInterval = setInterval(\n () => { this.pruneCommitLogs().catch((err) => logger.error(err, \"commit_logs prune failed\")); },\n SubscriptionManager.PRUNE_INTERVAL_MS,\n );\n if (this.pruneInterval && typeof this.pruneInterval.unref === \"function\") {\n this.pruneInterval.unref();\n }\n }\n\n async stop(): Promise<void> {\n if (this.pruneInterval) {\n clearInterval(this.pruneInterval);\n this.pruneInterval = null;\n }\n this.wss.close();\n await Promise.all(this.workers.map((w) => w.shutdown()));\n logger.info(\"Stopped\");\n }\n\n private async pruneCommitLogs(): Promise<void> {\n const minPerWorker = await Promise.all(\n this.workers.map((w) => w.send<bigint | null>(MsgType.MinSubscriptionTs, null)),\n );\n let minRetainedTs: bigint | null = null;\n for (const m of minPerWorker) {\n if (m === null) continue;\n if (minRetainedTs === null || m < minRetainedTs) minRetainedTs = m;\n }\n\n if (minRetainedTs === null) {\n const latest = await this.workers[0]!.send<bigint>(MsgType.LatestSnapshotTs, null);\n if (latest <= 0n) return;\n minRetainedTs = latest;\n }\n\n // Worker 0 advertises the new watermark (persist + Redis publish) then\n // deletes. Fan the same watermark out to the other workers' in-memory copies.\n const deleted = await this.workers[0]!.send<bigint>(MsgType.Prune, minRetainedTs);\n await this.broadcastRetentionWatermark(minRetainedTs);\n if (deleted > 0n) {\n logger.info(`pruned ${deleted} commit_logs older than ts=${minRetainedTs}`);\n }\n }\n\n /** Fan a watermark value out to every worker's in-memory copy. Worker 0 (the\n * pruner/seeder) already has it, but re-sending is harmless — {@link\n * SubscriptionWorker.setRetentionWatermark} is monotonic. */\n private async broadcastRetentionWatermark(ts: bigint): Promise<void> {\n await Promise.all(\n this.workers.map((w) =>\n w.send(MsgType.SetRetentionWatermark, ts).catch((err) =>\n logger.error(err, \"retention watermark broadcast to worker failed\"),\n ),\n ),\n );\n }\n\n workerCount(): number {\n return this.workers.length;\n }\n\n workerIndexFor(subId: string): number {\n return fnv1a32(subId) % this.workers.length;\n }\n\n private workerFor(subId: string): SubscriptionWorkerHandle {\n return this.workers[this.workerIndexFor(subId)]!;\n }\n\n private registerMethods(): void {\n this.wss.register(\"registerSubscription\", this.registerSubscription.bind(this));\n this.wss.register(\"registerSubscriptionBatch\", this.registerSubscriptionBatch.bind(this));\n this.wss.register(\"updateSubscriptionReadSet\", this.updateSubscriptionReadSet.bind(this));\n this.wss.register(\"invalidateWriteset\", this.invalidateWriteset.bind(this));\n this.wss.register(\"unregisterSubscription\", this.unregisterSubscription.bind(this));\n this.wss.register(\"unregisterSubscriptions\", this.unregisterSubscriptions.bind(this));\n }\n\n private readSubId(params: unknown): string {\n return (params as { subId: string }).subId;\n }\n\n private async registerSubscription(params: unknown): Promise<RegisterSubscriptionResult> {\n const subId = this.readSubId(params);\n return this.workerFor(subId).send<RegisterSubscriptionResult>(MsgType.Register, params);\n }\n\n private async registerSubscriptionBatch(params: unknown): Promise<RegisterSubscriptionBatchResult> {\n // Group entries by destination worker, dispatch one batch per worker, then\n // restore the original order.\n const arr = params as RegisterSubscriptionParams[];\n const tagged = arr.map((p, index) => ({ index, p, worker: this.workerFor(p.subId) }));\n const byWorker = groupByToMap(tagged, (t) => t.worker);\n\n const settled = await Promise.all(\n [...byWorker.entries()].map(async ([worker, items]) => {\n const results = await worker.send<RegisterSubscriptionResult[]>(\n MsgType.RegisterBatch,\n items.map((t) => t.p),\n );\n return items.map((t, i) => ({ index: t.index, result: results[i]! }));\n }),\n );\n\n const result: RegisterSubscriptionBatchResult = new Array(arr.length);\n for (const { index, result: r } of settled.flat()) {\n result[index] = r;\n }\n return result;\n }\n\n private async updateSubscriptionReadSet(params: unknown): Promise<UpdateSubscriptionReadSetResult> {\n const subId = this.readSubId(params);\n return this.workerFor(subId).send<UpdateSubscriptionReadSetResult>(MsgType.UpdateReadSet, params);\n }\n\n private async unregisterSubscription(params: unknown): Promise<UnregisterSubscriptionResult> {\n const subId = this.readSubId(params);\n return this.workerFor(subId).send<UnregisterSubscriptionResult>(MsgType.Unregister, params);\n }\n\n private async unregisterSubscriptions(params: unknown): Promise<UnregisterSubscriptionsResult> {\n const arr = params as UnregisterSubscriptionParams[];\n const tagged = arr.map((p, index) => ({ index, p, worker: this.workerFor(p.subId) }));\n const byWorker = groupByToMap(tagged, (t) => t.worker);\n\n const settled = await Promise.all(\n [...byWorker.entries()].map(async ([worker, items]) => {\n const results = await worker.send<UnregisterSubscriptionResult[]>(\n MsgType.Unregisters,\n items.map((t) => t.p) satisfies UnregisterSubscriptionsParams,\n );\n return items.map((t, i) => ({ index: t.index, result: results[i]! }));\n }),\n );\n\n const result: UnregisterSubscriptionsResult = new Array(arr.length);\n for (const { index, result: r } of settled.flat()) {\n result[index] = r;\n }\n return result;\n }\n\n private async invalidateWriteset(params: unknown): Promise<InvalidateWritesetResult> {\n // Fan the raw payload out to every worker. Each parses its own copy of the\n // writeSet (cheap relative to the scan) and returns its affected subs.\n const perWorker = await Promise.all(\n this.workers.map((w) => w.send<AffectedSubscription[]>(MsgType.Invalidate, params)),\n );\n return { affected: perWorker.flat() };\n }\n}\n\nfunction resolveWorkerCount(configured?: number): number {\n if (typeof configured === \"number\" && configured > 0) return Math.floor(configured);\n\n const envVal = process.env.SUPALIVE_SUB_MANAGER_WORKERS;\n if (envVal) {\n const n = parseInt(envVal, 10);\n if (Number.isFinite(n) && n > 0) return n;\n }\n return 1;\n}\n\nfunction resolveMaxConnectionsPerWorker(config: SubscriptionManagerConfig, workers: number): number {\n if (config.maxConnectionsPerWorker && config.maxConnectionsPerWorker > 0) {\n return Math.floor(config.maxConnectionsPerWorker);\n }\n const total = config.database.maxConnections ?? (config.database.type === \"mysql\" ? 20 : 10);\n return Math.max(1, Math.floor(total / workers));\n}\n"],"mappings":"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"}
@@ -1,4 +1,4 @@
1
1
  import { $r as normalizeIdToBytes, A as ResponseMessageSchema, Ar as Predicate, B as SubscriptionUpdateMessageSchema, Br as RawPointReadSchema, C as ClientMessageSchema, Cr as NO_RETRY, D as ErrorMessage, Dr as OrPredicateSchema, E as Context, Er as OrPredicate, F as SubscribeMessage, Fr as QueryCacheMetadataSchema, Gr as ReadEntry, H as UnsubscribeMessageSchema, Hr as RawRangeReadSchema, I as SubscribeMessageSchema, Ir as QuerySpec, Jr as WriteEntry, Kr as ReadEntrySchema, L as SubscriptionGoneMessage, Lr as RangeRead, M as ServerMessageSchema, Mr as QueryCacheEntry, N as ServerPingMessage, Nr as QueryCacheEntrySchema, O as IsInternal, Or as PointRead, P as ServerPingMessageSchema, Pr as QueryCacheMetadata, Qr as bytesFromJson, R as SubscriptionGoneMessageSchema, Rr as RangeReadSchema, S as ClientMessage, Sr as MutationResult, T as ConnectedMessageSchema, Tr as OccConflictError, Ur as RawReadEntry, V as UnsubscribeMessage, Vr as RawRangeRead, Wr as RawReadEntrySchema, Xr as WriteOp, Yr as WriteEntrySchema, Zr as WriteOpSchema, _ as AuthSuccessMessage, _r as CompareOperatorSchema, b as CallMessage, br as LeafPredicateSchema, dr as BigIntSchema, ei as normalizeToBytes, fr as CachedPgMetadata, g as AuthMessageSchema, gr as CompareOperator, h as AuthMessage, hr as CommitTs, j as ServerMessage, jr as PredicateSchema, k as ResponseMessage, kr as PointReadSchema, lr as AndPredicate, m as AuthFailedMessageSchema, mr as CommitLogEntry, p as AuthFailedMessage, pr as CachedPgMetadataSchema, qr as RetryConfig, ur as AndPredicateSchema, v as AuthSuccessMessageSchema, vr as DEFAULT_RETRY, w as ConnectedMessage, wr as OccAbortError, x as CallMessageSchema, xr as LiveResult, y as AuthenticationError, yr as LeafPredicate, z as SubscriptionUpdateMessage, zr as RawPointRead } from "../../index-DTNBHQPY.js";
2
2
  import { n as stableStringify, r as supaliveStringify, t as groupByToMap } from "../../helper-CiacMqje.js";
3
- import { _ as RpcCorsConfig, a as RemoteSubscriptionMessage, b as SupaliveServerConfig, c as RemoteSubscriptionUpdateSchema, d as SubscriptionEntry, f as AppPubSubConfig, g as RpcConfig, h as PostgresConfig, i as RemoteRecomputeRaceSchema, l as Session, m as MySQLConfig, n as PendingMutation, o as RemoteSubscriptionMessageSchema, p as DatabaseConfig, r as RemoteRecomputeRace, s as RemoteSubscriptionUpdate, t as InstanceId, u as SubId, v as RpcRateLimitConfig, x as UpstashConfig, y as SubscriptionManagerConfig } from "../../types_server-eJnBeRoI.js";
3
+ import { _ as RpcCorsConfig, a as RemoteSubscriptionMessage, b as SupaliveServerConfig, c as RemoteSubscriptionUpdateSchema, d as SubscriptionEntry, f as AppPubSubConfig, g as RpcConfig, h as PostgresConfig, i as RemoteRecomputeRaceSchema, l as Session, m as MySQLConfig, n as PendingMutation, o as RemoteSubscriptionMessageSchema, p as DatabaseConfig, r as RemoteRecomputeRace, s as RemoteSubscriptionUpdate, t as InstanceId, u as SubId, v as RpcRateLimitConfig, x as UpstashConfig, y as SubscriptionManagerConfig } from "../../types_server-DCD3Kqln.js";
4
4
  export { AndPredicate, AndPredicateSchema, AppPubSubConfig, AuthFailedMessage, AuthFailedMessageSchema, AuthMessage, AuthMessageSchema, AuthSuccessMessage, AuthSuccessMessageSchema, AuthenticationError, BigIntSchema, CachedPgMetadata, CachedPgMetadataSchema, CallMessage, CallMessageSchema, ClientMessage, ClientMessageSchema, CommitLogEntry, CommitTs, CompareOperator, CompareOperatorSchema, ConnectedMessage, ConnectedMessageSchema, Context, DEFAULT_RETRY, DatabaseConfig, ErrorMessage, InstanceId, IsInternal, LeafPredicate, LeafPredicateSchema, LiveResult, MutationResult, MySQLConfig, NO_RETRY, OccAbortError, OccConflictError, OrPredicate, OrPredicateSchema, PendingMutation, PointRead, PointReadSchema, PostgresConfig, Predicate, PredicateSchema, QueryCacheEntry, QueryCacheEntrySchema, QueryCacheMetadata, QueryCacheMetadataSchema, QuerySpec, RangeRead, RangeReadSchema, RawPointRead, RawPointReadSchema, RawRangeRead, RawRangeReadSchema, RawReadEntry, RawReadEntrySchema, ReadEntry, ReadEntrySchema, RemoteRecomputeRace, RemoteRecomputeRaceSchema, RemoteSubscriptionMessage, RemoteSubscriptionMessageSchema, RemoteSubscriptionUpdate, RemoteSubscriptionUpdateSchema, ResponseMessage, ResponseMessageSchema, RetryConfig, RpcConfig, RpcCorsConfig, RpcRateLimitConfig, ServerMessage, ServerMessageSchema, ServerPingMessage, ServerPingMessageSchema, Session, SubId, SubscribeMessage, SubscribeMessageSchema, SubscriptionEntry, SubscriptionGoneMessage, SubscriptionGoneMessageSchema, SubscriptionManagerConfig, SubscriptionUpdateMessage, SubscriptionUpdateMessageSchema, SupaliveServerConfig, UnsubscribeMessage, UnsubscribeMessageSchema, UpstashConfig, WriteEntry, WriteEntrySchema, WriteOp, WriteOpSchema, bytesFromJson, groupByToMap, normalizeIdToBytes, normalizeToBytes, stableStringify, supaliveStringify };
@@ -0,0 +1,328 @@
1
+ import { Bt as SupaliveDb, Ft as JobScheduler, Sr as MutationResult, nt as RegisteredProcedure, qn as CacheLayer } from "./index-DTNBHQPY.js";
2
+ import { r as ObjectStorage } from "./object-storage-354KU6Mj.js";
3
+ import z from "zod";
4
+ import { IncomingMessage } from "http";
5
+ import { WebSocket } from "ws";
6
+ import { Level } from "pino";
7
+ import { Redis } from "ioredis";
8
+
9
+ //#region src/config.d.ts
10
+ /** CORS policy for the HTTP RPC endpoint. */
11
+ interface RpcCorsConfig {
12
+ /**
13
+ * Allowed origins. `"*"` sends `Access-Control-Allow-Origin: *`; a list
14
+ * echoes back a request's `Origin` only when it matches (and sets `Vary`).
15
+ * Omit `cors` entirely to send no CORS headers (same-origin only).
16
+ */
17
+ origins: string[] | "*";
18
+ }
19
+ /** Rate-limit policy for the HTTP RPC endpoint (fixed window, per instance). */
20
+ interface RpcRateLimitConfig {
21
+ /** Window length in ms. Default 60000. */
22
+ windowMs?: number;
23
+ /** Max requests per window per key. Default 120. */
24
+ max?: number;
25
+ /**
26
+ * Derive the limiter key from the request. Default: client IP
27
+ * (`X-Forwarded-For` first hop, else socket address). Return a stable string.
28
+ */
29
+ keyBy?: (req: IncomingMessage) => string;
30
+ }
31
+ /**
32
+ * Enables a one-shot HTTP RPC transport for `query`/`mutation`/`action`
33
+ * (no subscriptions — those stay WebSocket-only). Lets clients that don't need
34
+ * live queries call procedures without holding a socket open. Omit to disable
35
+ * the endpoint entirely.
36
+ */
37
+ interface RpcConfig<TContext = unknown> {
38
+ /**
39
+ * The single endpoint path. Default `/_rpc`. Every procedure is called as
40
+ * `POST {path}` with `{ name, input }` in the body — the name is never in the
41
+ * URL. Keep this distinct from `jobPath`.
42
+ */
43
+ path?: string;
44
+ /**
45
+ * Map an HTTP request to the auth `data` handed to `verifyAuth` (the same
46
+ * callback the WebSocket path uses). Default: `Authorization: Bearer <token>`
47
+ * → `{ token }`. Return `undefined` for an anonymous request.
48
+ */
49
+ auth?: (req: IncomingMessage) => Record<string, unknown> | undefined;
50
+ /** CORS policy. Omit for same-origin only (no CORS headers). */
51
+ cors?: RpcCorsConfig;
52
+ /** Rate limiting. Omit for defaults (120/min per IP); set `false` to disable. */
53
+ rateLimit?: RpcRateLimitConfig | false;
54
+ }
55
+ interface UpstashConfig {
56
+ url: string;
57
+ token: string;
58
+ redisUrl: string;
59
+ }
60
+ /** Database configuration for PostgreSQL */
61
+ interface PostgresConfig {
62
+ type: "postgres";
63
+ connectionString: string;
64
+ /** Optional max connections for the Postgres connection pool. Defaults to 10.*/
65
+ maxConnections?: number;
66
+ /** Optional min connections for the Postgres connection pool. Defaults to 1.*/
67
+ minConnections?: number;
68
+ /** Optional idle timeout for Postgres connections in milliseconds. Defaults to 30000 (30 seconds). */
69
+ idleTimeoutMillis?: number;
70
+ /** Optional connection timeout for acquiring Postgres connections in milliseconds. Defaults to 7000 (7 seconds). */
71
+ connectionTimeoutMillis?: number;
72
+ }
73
+ /** Database configuration for MySQL */
74
+ interface MySQLConfig {
75
+ type: "mysql";
76
+ connectionString: string;
77
+ /** Optional max connections for the MySQL connection pool. Defaults to 20.*/
78
+ maxConnections?: number;
79
+ /** Optional min connections for the MySQL connection pool. Defaults to 1.*/
80
+ minConnections?: number;
81
+ /** Optional idle timeout for MySQL connections in milliseconds. Defaults to 30000 (30 seconds). */
82
+ idleTimeoutMillis?: number;
83
+ /** Optional connection timeout for acquiring MySQL connections in milliseconds. Defaults to 7000 (7 seconds). */
84
+ connectionTimeoutMillis?: number;
85
+ /** Optional flag to queue connection requests when pool is exhausted. Defaults to true (queue requests). */
86
+ queueLimit?: number;
87
+ }
88
+ type DatabaseConfig = PostgresConfig | MySQLConfig;
89
+ /**
90
+ * Configuration for the Supalive WebSocket server.
91
+ */
92
+ interface SupaliveServerConfig<TContext = Record<string, unknown>> {
93
+ /** Server port (default: 3000) */
94
+ port?: number;
95
+ /** Server host (default: "0.0.0.0") */
96
+ host?: string;
97
+ cacheLayer: CacheLayer;
98
+ redisSubClient: Redis;
99
+ /**
100
+ * Subscription Manager WebSocket URL for RPC communication.
101
+ * A single sub-manager process owns all subscription state for this
102
+ * deployment. Internally it spawns N logical workers (configured on the
103
+ * sub-manager side) and routes by hash(subId) to spread CPU across them.
104
+ */
105
+ subManagerUrl: string;
106
+ /** Database instance */
107
+ database: SupaliveDb;
108
+ /**
109
+ * Optional authentication callback.
110
+ * Called when client sends auth message.
111
+ * Return null to reject authentication.
112
+ */
113
+ verifyAuth?: (data: Record<string, unknown>, sessionId: string) => Promise<TContext | null>;
114
+ /**
115
+ * Server context name for procedure routing.
116
+ * Must match the router's contextName.
117
+ */
118
+ contextName?: string;
119
+ /**
120
+ * Session TTL in seconds (default: 3600 = 1 hour).
121
+ * Sessions are kept alive while connected and expire after disconnect.
122
+ */
123
+ sessionTtlSeconds?: number;
124
+ /**
125
+ * Enable detailed logging.
126
+ */
127
+ logLevel?: Level;
128
+ /**
129
+ * Extract user ID from context for cache key generation.
130
+ * If not provided, caching is disabled (each sub recomputes independently).
131
+ */
132
+ getUserId?: (ctx: TContext | undefined) => string | null;
133
+ /**
134
+ * Cache TTL in seconds (default: 3600).
135
+ * How long query results are cached in Redis.
136
+ */
137
+ cacheTtlSeconds?: number;
138
+ /**
139
+ * Scheduler backing the job system. When provided, the server exposes an
140
+ * HTTP endpoint (`{jobPath}/<jobName>`) that the scheduler POSTs to, and
141
+ * syncs every registered recurring (cron) job at startup. Omit to disable
142
+ * jobs entirely. Use `DevScheduler` locally and `QStashScheduler` in prod.
143
+ */
144
+ scheduler: JobScheduler;
145
+ /**
146
+ * Builds the system server context handed to a job handler's `serverCtx`.
147
+ * Jobs run without a client, so this is where a deployment injects its
148
+ * "system"/service identity. Called per job invocation.
149
+ */
150
+ jobContext?: () => TContext | Promise<TContext>;
151
+ /**
152
+ * Absolute, externally-reachable base URL of this server (no trailing
153
+ * slash), e.g. `https://api.example.com` in prod or `http://127.0.0.1:3000`
154
+ * in dev. Used to build the job endpoint the scheduler calls.
155
+ */
156
+ publicUrl: string;
157
+ /**
158
+ * URL path prefix for the job webhook endpoint. Default `/_jobs`. The full
159
+ * endpoint for a job is `{publicUrl}{jobPath}/<jobName>`.
160
+ */
161
+ jobPath?: string;
162
+ /**
163
+ * Enable the one-shot HTTP RPC transport (query/mutation/action over a
164
+ * single `POST {rpc.path}` endpoint, with `{ name, input }` in the body).
165
+ * Uses the same `verifyAuth` as the WebSocket path. Omit to disable. See
166
+ * {@link RpcConfig}.
167
+ */
168
+ rpc?: RpcConfig<TContext>;
169
+ /**
170
+ * Object storage, exposed to every procedure as `ctx.storage`. When set,
171
+ * query/mutation/action/job handlers can presign uploads/downloads and
172
+ * resolve public URLs without reaching for a module-level singleton. Omit
173
+ * to leave `ctx.storage` undefined (handlers should treat uploads as
174
+ * unconfigured). Build one with `S3CompatibleStorage` from
175
+ * `@supalive/core/storage`.
176
+ */
177
+ storage: ObjectStorage;
178
+ /**
179
+ * Optional application-level Redis pub/sub for cross-instance app messages
180
+ * that are NOT part of the subscription/cache machinery — e.g. auth-token
181
+ * revocations, feature-flag flips, targeted cache busts.
182
+ *
183
+ * The server subscribes to `channels` on its EXISTING Redis subscribe
184
+ * connection (no extra connection) and hands each received message to
185
+ * `onMessage`. Publish from anywhere with
186
+ * `server.publishAppMessage(channel, message)`, which rides the shared
187
+ * command connection. Messages are opaque strings — the app owns encoding.
188
+ */
189
+ appPubSub?: AppPubSubConfig;
190
+ }
191
+ /**
192
+ * App-level pub/sub wiring (see {@link SupaliveServerConfig.appPubSub}). A thin,
193
+ * generic cross-instance message bus layered on the server's Redis connections.
194
+ */
195
+ interface AppPubSubConfig {
196
+ /** Channels this instance subscribes to. */
197
+ channels: string[];
198
+ /**
199
+ * Handler for a message received on one of `channels`. Errors are caught and
200
+ * logged by the server, so a throwing handler can't crash the subscriber.
201
+ */
202
+ onMessage: (channel: string, message: string) => void | Promise<void>;
203
+ }
204
+ interface SubscriptionManagerConfig {
205
+ port: number;
206
+ upstash: {
207
+ url: string;
208
+ token: string;
209
+ redisUrl: string;
210
+ };
211
+ database: DatabaseConfig;
212
+ cacheTTLSeconds: number;
213
+ /**
214
+ * Number of logical workers inside the sub-manager process.
215
+ *
216
+ * Subscriptions are routed by FNV-1a(subId) % workers so that all operations
217
+ * for a given subId always land on the same worker (preserving the existing
218
+ * `instances: Set<string>` dedup). Each worker owns an independent
219
+ * subscriptions Map; on invalidate, all workers scan their own slice in
220
+ * parallel (logically — JS still runs one at a time inside one event loop,
221
+ * but per-worker scans are smaller so total work is split N ways).
222
+ *
223
+ * Defaults to `SUPALIVE_SUB_MANAGER_WORKERS` env var if set, else 1.
224
+ */
225
+ workers?: number;
226
+ /**
227
+ * Connection pool size *per worker*. If omitted, falls back to
228
+ * `floor(database.maxConnections / workers)` (min 1) so the total pool
229
+ * size stays close to the legacy single-worker configuration.
230
+ */
231
+ maxConnectionsPerWorker?: number;
232
+ }
233
+ //#endregion
234
+ //#region src/server/types_server.d.ts
235
+ type InstanceId = string;
236
+ type SubId = string;
237
+ /** Local tracking of an active subscription (per-session) */
238
+ interface SubscriptionEntry {
239
+ subId: SubId;
240
+ cacheKey: string;
241
+ subscribedTimes: number;
242
+ }
243
+ /**
244
+ * A mutation that has been validated and parked on the per-session FIFO
245
+ * queue, waiting for the in-flight mutation on this socket to finish. Each
246
+ * `handleCall` for a mutation owns one of these and awaits its own
247
+ * resolve/reject so it can send the response.
248
+ */
249
+ interface PendingMutation<TContext = Record<string, unknown>> {
250
+ procedure: RegisteredProcedure<TContext>;
251
+ validatedInput: unknown;
252
+ resolve: (result: MutationResult<unknown>) => void;
253
+ reject: (err: unknown) => void;
254
+ }
255
+ /** WebSocket session (runtime, with WebSocket) */
256
+ interface Session<TContext = Record<string, unknown>> {
257
+ id: string;
258
+ ws: WebSocket;
259
+ /** Auth context from verifyAuth callback */
260
+ serverCtx?: TContext;
261
+ subIds: Set<string>;
262
+ /**
263
+ * Per-socket FIFO mutation queue. at most one mutation
264
+ * drains at a time so a single client cannot saturate the OCC retry
265
+ * loop or fan out concurrent commits that race the same rows.
266
+ */
267
+ mutationQueue: PendingMutation<TContext>[];
268
+ mutationInFlight: boolean;
269
+ /** Per-socket count of currently executing actions. */
270
+ actionInFlight: number;
271
+ /**
272
+ * Wall-clock of the last inbound frame (text message, ws-ping, or ws-pong).
273
+ * Used by the per-session heartbeat to terminate sockets that have gone
274
+ * silent past `CLIENT_TIMEOUT_MS`.
275
+ */
276
+ lastReceivedAt: number;
277
+ /**
278
+ * Wall-clock of the last outbound frame. Gates the idle-only app-level
279
+ * ping so a busy session (frequent sub:updates etc.) doesn't emit a
280
+ * redundant `{type:"ping"}` on every heartbeat tick.
281
+ */
282
+ lastSentAt: number;
283
+ /**
284
+ * Per-session combined heartbeat. Single `setInterval` that handles
285
+ * both dead-TCP detection (ws-ping or terminate) and client-watchdog
286
+ * refresh (idle-only app-level ping) on each tick.
287
+ */
288
+ heartbeatTimer: ReturnType<typeof setInterval> | null;
289
+ }
290
+ declare const RemoteSubscriptionUpdateSchema: z.ZodObject<{
291
+ type: z.ZodLiteral<"update">;
292
+ subId: z.ZodString;
293
+ data: z.ZodUnknown;
294
+ dataHash: z.ZodString;
295
+ originInstance: z.ZodString;
296
+ }, z.core.$strip>;
297
+ type RemoteSubscriptionUpdate = z.infer<typeof RemoteSubscriptionUpdateSchema>;
298
+ declare const RemoteRecomputeRaceSchema: z.ZodObject<{
299
+ type: z.ZodLiteral<"recompute-race">;
300
+ subId: z.ZodString;
301
+ cacheKey: z.ZodString;
302
+ queryName: z.ZodString;
303
+ args: z.ZodUnknown;
304
+ notifyInstances: z.ZodArray<z.ZodString>;
305
+ commitTs: z.ZodString;
306
+ originInstance: z.ZodString;
307
+ }, z.core.$strip>;
308
+ type RemoteRecomputeRace = z.infer<typeof RemoteRecomputeRaceSchema>;
309
+ declare const RemoteSubscriptionMessageSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
310
+ type: z.ZodLiteral<"update">;
311
+ subId: z.ZodString;
312
+ data: z.ZodUnknown;
313
+ dataHash: z.ZodString;
314
+ originInstance: z.ZodString;
315
+ }, z.core.$strip>, z.ZodObject<{
316
+ type: z.ZodLiteral<"recompute-race">;
317
+ subId: z.ZodString;
318
+ cacheKey: z.ZodString;
319
+ queryName: z.ZodString;
320
+ args: z.ZodUnknown;
321
+ notifyInstances: z.ZodArray<z.ZodString>;
322
+ commitTs: z.ZodString;
323
+ originInstance: z.ZodString;
324
+ }, z.core.$strip>], "type">;
325
+ type RemoteSubscriptionMessage = z.infer<typeof RemoteSubscriptionMessageSchema>;
326
+ //#endregion
327
+ export { RpcCorsConfig as _, RemoteSubscriptionMessage as a, SupaliveServerConfig as b, RemoteSubscriptionUpdateSchema as c, SubscriptionEntry as d, AppPubSubConfig as f, RpcConfig as g, PostgresConfig as h, RemoteRecomputeRaceSchema as i, Session as l, MySQLConfig as m, PendingMutation as n, RemoteSubscriptionMessageSchema as o, DatabaseConfig as p, RemoteRecomputeRace as r, RemoteSubscriptionUpdate as s, InstanceId as t, SubId as u, RpcRateLimitConfig as v, UpstashConfig as x, SubscriptionManagerConfig as y };
328
+ //# sourceMappingURL=types_server-DCD3Kqln.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types_server-DCD3Kqln.d.ts","names":[],"sources":["../src/config.ts","../src/server/types_server.ts"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@supalive/core",
3
- "version": "1.6.0",
3
+ "version": "1.7.1",
4
4
  "type": "module",
5
5
  "description": "Core for supalive — reactive database framework",
6
6
  "license": "MIT",