@supalive/core 1.9.0 → 1.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/dist/db-ClCicssQ.js.map +1 -1
  2. package/dist/index-BQRlJY_1.d.ts +2192 -0
  3. package/dist/index-BQRlJY_1.d.ts.map +1 -0
  4. package/dist/index-C_Zew1wH.d.ts +2209 -0
  5. package/dist/index-C_Zew1wH.d.ts.map +1 -0
  6. package/dist/index-D8O39FoZ.d.ts +2192 -0
  7. package/dist/index-D8O39FoZ.d.ts.map +1 -0
  8. package/dist/index-WMhtVzJA.d.ts +2192 -0
  9. package/dist/index-WMhtVzJA.d.ts.map +1 -0
  10. package/dist/mysql-BXy4mk-9.d.ts +113 -0
  11. package/dist/mysql-BXy4mk-9.d.ts.map +1 -0
  12. package/dist/mysql-CBSuONm5.d.ts +109 -0
  13. package/dist/mysql-CBSuONm5.d.ts.map +1 -0
  14. package/dist/mysql-CDenA4XE.d.ts +109 -0
  15. package/dist/mysql-CDenA4XE.d.ts.map +1 -0
  16. package/dist/mysql-D-PGYc1L.js +609 -0
  17. package/dist/mysql-D-PGYc1L.js.map +1 -0
  18. package/dist/mysql-DAfFK5v1.d.ts +109 -0
  19. package/dist/mysql-DAfFK5v1.d.ts.map +1 -0
  20. package/dist/mysql-txOOzccX.js +609 -0
  21. package/dist/mysql-txOOzccX.js.map +1 -0
  22. package/dist/overlap-checker-BoWNco2y.js +258 -0
  23. package/dist/overlap-checker-BoWNco2y.js.map +1 -0
  24. package/dist/overlap-checker-DvRqQOTe.js +265 -0
  25. package/dist/overlap-checker-DvRqQOTe.js.map +1 -0
  26. package/dist/postgres-BKGLPrKt.d.ts +113 -0
  27. package/dist/postgres-BKGLPrKt.d.ts.map +1 -0
  28. package/dist/postgres-CKBWXyY8.d.ts +113 -0
  29. package/dist/postgres-CKBWXyY8.d.ts.map +1 -0
  30. package/dist/postgres-CapFFTTn.d.ts +117 -0
  31. package/dist/postgres-CapFFTTn.d.ts.map +1 -0
  32. package/dist/postgres-Cn9vTBX7.js +864 -0
  33. package/dist/postgres-Cn9vTBX7.js.map +1 -0
  34. package/dist/postgres-DQxKs5Nu.d.ts +113 -0
  35. package/dist/postgres-DQxKs5Nu.d.ts.map +1 -0
  36. package/dist/postgres-tLkdxhFu.js +864 -0
  37. package/dist/postgres-tLkdxhFu.js.map +1 -0
  38. package/dist/src/client/index.d.ts +1 -1
  39. package/dist/src/exports/mysql.d.ts +1 -1
  40. package/dist/src/exports/mysql.js +1 -1
  41. package/dist/src/exports/postgres.d.ts +1 -1
  42. package/dist/src/exports/postgres.js +1 -1
  43. package/dist/src/exports/procedure.d.ts +1 -1
  44. package/dist/src/exports/schema-sql.d.ts +1 -1
  45. package/dist/src/exports/server.d.ts +24 -124
  46. package/dist/src/exports/server.d.ts.map +1 -1
  47. package/dist/src/exports/server.js +137 -32
  48. package/dist/src/exports/server.js.map +1 -1
  49. package/dist/src/exports/sub-manager-worker-entry.d.ts +1 -0
  50. package/dist/src/exports/sub-manager-worker-entry.js +71 -0
  51. package/dist/src/exports/sub-manager-worker-entry.js.map +1 -0
  52. package/dist/src/exports/subscription-manager-worker-entry.js +3 -49
  53. package/dist/src/exports/subscription-manager-worker-entry.js.map +1 -1
  54. package/dist/src/exports/types.d.ts +2 -2
  55. package/dist/sub-worker-dispatch-Cokz99D1.js +810 -0
  56. package/dist/sub-worker-dispatch-Cokz99D1.js.map +1 -0
  57. package/dist/sub-worker-dispatch-DohrszOw.js +810 -0
  58. package/dist/sub-worker-dispatch-DohrszOw.js.map +1 -0
  59. package/dist/sub-worker-dispatch-G8V8asfR.js +810 -0
  60. package/dist/sub-worker-dispatch-G8V8asfR.js.map +1 -0
  61. package/dist/subscription-worker-dispatch-CSQV-PO3.js +810 -0
  62. package/dist/subscription-worker-dispatch-CSQV-PO3.js.map +1 -0
  63. package/dist/types_server-AQ8Jl2_R.d.ts +521 -0
  64. package/dist/types_server-AQ8Jl2_R.d.ts.map +1 -0
  65. package/dist/types_server-BB2WvXEA.d.ts +521 -0
  66. package/dist/types_server-BB2WvXEA.d.ts.map +1 -0
  67. package/dist/types_server-DU9JMSXd.d.ts +521 -0
  68. package/dist/types_server-DU9JMSXd.d.ts.map +1 -0
  69. package/dist/types_server-SPElhjga.d.ts +521 -0
  70. package/dist/types_server-SPElhjga.d.ts.map +1 -0
  71. package/package.json +1 -1
@@ -0,0 +1 @@
1
+ {"version":3,"file":"subscription-worker-dispatch-CSQV-PO3.js","names":["uuidv4","logger","baseLogger","logger","Redis","baseLogger"],"sources":["../src/logger.ts","../src/server/retention-watermark.ts","../src/server/subscription-manager-types.ts","../src/db/cache.ts","../src/db/init_db.ts","../src/server/subscription-worker.ts","../src/server/subscription-worker-dispatch.ts"],"sourcesContent":["import pino, { Level } from \"pino\";\nimport { v4 as uuidv4 } from \"uuid\";\nimport pinoPretty from \"pino-pretty\";\nconst _ = pinoPretty; // Prevents tree shaking of pino-pretty\n\nexport const appInstanceId = `instance-${uuidv4().slice(0, 8)}`;\nexport const isProduction = process.env.ENV === \"prod\";\nexport const logger = pino({\n level: process.env.LOG_LEVEL || \"info\",\n timestamp: pino.stdTimeFunctions.isoTime,\n redact: { paths: [\"user.password\"] },\n transport: isProduction ? undefined : {\n target: 'pino-pretty',\n options: {\n colorize: true\n },\n },\n serializers: {\n \"columnsTypes\": (map) => {\n if (map instanceof Map) return serializeMapValue(map);\n return map;\n },\n },\n base: {\n \"srv\": appInstanceId,\n },\n});\n\nexport function logLevelOf(name: string): Level {\n const defaultLogLevel = process.env.DEFAULT_LOG_LEVEL || \"info\";\n return (process.env[name]?.toLowerCase() || defaultLogLevel) as Level;\n}\n\nexport function serializeMapValue<K extends string | number | symbol, V>(map: Map<K, V>): Record<K, V> {\n const obj: Record<K, any> = {} as Record<K, any>;\n for (const [k, v] of map.entries()) {\n obj[k] = {} as V;\n if (v instanceof Map) {\n for (const [innerK, innerV] of v.entries()) {\n obj[k][innerK] = innerV;\n }\n } else {\n obj[k] = v;\n }\n }\n\n return obj as Record<K, V>;\n}","import type { Database } from \"../db/db\";\n\n/** Redis pub/sub channel the sub-manager publishes the retention watermark on\n * after each prune (and at startup). App-server instances subscribe and update\n * their in-memory {@link RetentionWatermark}. Payload is the watermark `ts` as a\n * string. */\nexport const RETENTION_WATERMARK_CHANNEL = \"supalive:retention_watermark\";\n\n/**\n * In-memory holder for the commit-log retention watermark: the oldest commit-log\n * ts still retained after prune (see {@link Database.getMinSnapshotTimestamp}).\n * Read on the hot path by the query-cache freshness check — a plain variable, no\n * per-check Redis/DB round trip. Loaded once at startup from the `metadata`\n * table and kept fresh out-of-band: the sub-manager broadcasts the new watermark\n * (in-process to its workers, over Redis pub/sub to app servers) each time it\n * prunes.\n *\n * `set` is monotonic — the watermark only ever moves forward (pruning never\n * un-deletes rows), so an out-of-order or stale update can never lower it, which\n * keeps the freshness guard on the safe side (an over-estimate only costs an\n * extra recompute; an under-estimate could serve stale data).\n *\n * Initial value is the -1n genesis (not 0), matching `getMinSnapshotTimestamp`\n * and the empty-DB snapshot ts — so before anything is pruned the guard never\n * rejects a genesis-era entry (whose snapshot is -1).\n */\nexport class RetentionWatermark {\n private ts: bigint = -1n;\n\n get(): bigint {\n return this.ts;\n }\n\n set(ts: bigint): void {\n if (ts > this.ts) this.ts = ts;\n }\n\n /** Seed from the persisted watermark (metadata table). Returns the current value. */\n async loadFrom(db: Pick<Database, \"getMinSnapshotTimestamp\">): Promise<bigint> {\n this.set(await db.getMinSnapshotTimestamp());\n return this.ts;\n }\n}\n","import z from \"zod\";\nimport { BigIntSchema, RawReadEntry, ReadEntry, ReadEntrySchema, WriteEntrySchema } from \"../db/types_db\";\n\n// ─── RPC Schemas ──────────────────────────────────────────────────────────────\n//\n// The wire protocol intentionally has NO `_clientVersion` / `_resync` /\n// `serverVersion` fields. Ordering of concurrent `updateSubscriptionReadSet`\n// calls is enforced by `lastSnapshotTs` (which is monotonic — it comes from\n// `Database.getLatestSnapshotTimestamp()`). Recovery after a sub-manager\n// restart is driven by the app server iterating its own\n// `serverSubscriptions` map and re-registering each one using params\n// persisted in Redis under `sl:reg:<subId>`.\n\nexport const RegisterSubscriptionParamsSchema = z.object({\n /**\n * Caller-computed FNV-1a routing key + identity for this subscription.\n * The sub-manager trusts these values rather than re-deriving them from\n * (queryName, args, queryIdentity); this avoids one stringify+SHA1 per\n * register RPC. Trust is fine: the sub-manager RPC channel is fully\n * internal.\n */\n subId: z.string(),\n cacheKey: z.string(),\n queryName: z.string(),\n args: z.unknown(),\n /**\n * Segmentation key used in the subId/cacheKey hash. Defaults to the\n * authenticated user's id (\"anonymous\" when none), unless the\n * procedure overrides it via `QueryConfig.queryIdentity`.\n */\n queryIdentity: z.string(),\n instanceName: z.string(),\n lastSnapshotTs: BigIntSchema,\n});\nexport type RegisterSubscriptionParams = z.infer<typeof RegisterSubscriptionParamsSchema>;\n\nexport const RegisterSubscriptionResultSchema = z.object({\n cacheKey: z.string(),\n recompute: z.boolean(),\n readSet: z.array(ReadEntrySchema),\n // Version of the cached data the freshness check validated. The app server\n // serves the cached data blob only when its version matches this (guards a\n // data/metadata divergence). Absent when the worker had no metadata to\n // validate against (e.g. a plain recompute), in which case the server does\n // not serve from cache anyway.\n version: z.string().optional(),\n});\nexport type RegisterSubscriptionResult = z.infer<typeof RegisterSubscriptionResultSchema>;\n\nexport const RegisterSubscriptionBatchParamsSchema = z.array(RegisterSubscriptionParamsSchema);\nexport type RegisterSubscriptionBatchParams = z.infer<typeof RegisterSubscriptionBatchParamsSchema>;\n\nexport const RegisterSubscriptionBatchResultSchema = z.array(RegisterSubscriptionResultSchema);\nexport type RegisterSubscriptionBatchResult = z.infer<typeof RegisterSubscriptionBatchResultSchema>;\n\nexport const UpdateSubscriptionReadSetParamsSchema = z.object({\n cacheKey: z.string(),\n subId: z.string(),\n instanceName: z.string(),\n lastSnapshotTs: BigIntSchema,\n readSet: z.array(ReadEntrySchema),\n});\nexport type UpdateSubscriptionReadSetParams = z.infer<typeof UpdateSubscriptionReadSetParamsSchema>;\n\n/**\n * `tracked: false` means the sub-manager has no record of this subId (e.g.\n * a restart wiped state and the caller hasn't re-registered yet). The app\n * server should treat this as a signal to register the sub before retrying.\n */\nexport const UpdateSubscriptionReadSetResultSchema = z.object({\n tracked: z.boolean(),\n});\nexport type UpdateSubscriptionReadSetResult = z.infer<typeof UpdateSubscriptionReadSetResultSchema>;\n\nexport const UnregisterSubscriptionParamsSchema = z.object({\n subId: z.string(),\n instanceName: z.string(),\n});\nexport type UnregisterSubscriptionParams = z.infer<typeof UnregisterSubscriptionParamsSchema>;\n\nexport const UnregisterSubscriptionsParamsSchema = z.array(UnregisterSubscriptionParamsSchema);\nexport type UnregisterSubscriptionsParams = z.infer<typeof UnregisterSubscriptionsParamsSchema>;\n\nexport const UnregisterSubscriptionResultSchema = z.object({ ok: z.boolean() });\nexport type UnregisterSubscriptionResult = z.infer<typeof UnregisterSubscriptionResultSchema>;\n\nexport const UnregisterSubscriptionsResultSchema = z.array(z.object({ ok: z.boolean() }));\nexport type UnregisterSubscriptionsResult = z.infer<typeof UnregisterSubscriptionsResultSchema>;\n\nexport const InvalidateWritesetParamsSchema = z.object({\n writeSet: z.array(WriteEntrySchema),\n});\nexport type InvalidateWritesetParams = z.infer<typeof InvalidateWritesetParamsSchema>;\n\nexport const AffectedSubscriptionSchema = z.object({\n subId: z.string(),\n queryName: z.string(),\n args: z.unknown(),\n cacheKey: z.string(),\n /**\n * Every live instance that currently holds this subId. Routing rules:\n * • If the writer is in this list, it recomputes locally and publishes\n * an `update` to the rest (its own id is filtered by the publisher).\n * • Otherwise the writer broadcasts a `recompute-race` to every entry;\n * receivers race for a Redis lock and the winner recomputes.\n */\n notifyInstances: z.array(z.string()),\n});\nexport type AffectedSubscription = z.infer<typeof AffectedSubscriptionSchema>;\n\nexport const InvalidateWritesetResultSchema = z.object({\n affected: z.array(AffectedSubscriptionSchema),\n});\nexport type InvalidateWritesetResult = z.infer<typeof InvalidateWritesetResultSchema>;\n\n// ─── Registration record persisted to Redis ──────────────────────────────────\n//\n// Stored at `sl:reg:<subId>` by the sub-manager when a brand-new subscription\n// is created, and deleted by the sub-manager when the last instance leaves.\n// The app server reads this on reconnect to know how to re-register its\n// known subIds.\n\nexport const RegistrationRecordSchema = z.object({\n queryName: z.string(),\n args: z.unknown(),\n queryIdentity: z.string(),\n cacheKey: z.string(),\n});\nexport type RegistrationRecord = z.infer<typeof RegistrationRecordSchema>;\n\nexport interface TrackedSubscription {\n subId: string;\n queryName: string;\n args: unknown;\n cacheKey: string;\n lastSnapshotTs: bigint;\n readSet: ReadEntry[];\n rawReadSet: RawReadEntry[];\n /** All app server instances that currently hold this subscription. */\n instances: Set<string>;\n timeout: NodeJS.Timeout | null;\n}","import type { Redis as UpstashRedis } from \"@upstash/redis\";\nimport {\n CachedPgMetadata,\n CachedPgMetadataSchema,\n QueryCacheEntry, QueryCacheEntrySchema,\n QueryCacheMetadata, QueryCacheMetadataSchema\n} from \"./types_db\";\nimport { retry, handleAll, ExponentialBackoff } from \"cockatiel\";\nimport { RegistrationRecord, RegistrationRecordSchema } from \"../server/subscription-manager-types\";\nimport Redis from \"ioredis\";\nimport { createHash } from \"node:crypto\";\nimport { logger as baseLogger, logLevelOf } from \"../logger\";\nimport { supaliveStringify } from \"../helper\";\n\nconst logger = baseLogger.child({\n \"name\": \"CACHE_LAYER\",\n},\n { level: logLevelOf(\"SERVER_LOG_LEVEL\") },\n);\n\nexport const redisRetryPolicy = retry(handleAll, {\n maxAttempts: 3,\n backoff: new ExponentialBackoff({\n initialDelay: 5,\n maxDelay: 2300,\n exponent: 2,\n }),\n});\n\n// ─── Query-cache Lua scripts ────────────────────────────────────────────────\n//\n// The query cache is two string keys per query — data (`sl:cache:*`) and\n// metadata (`sl:meta:*`). They are written by several code paths across two\n// processes (app server + sub-worker), so a plain pipeline can't keep them\n// consistent (and on a non-atomic client a pipeline isn't even atomic). These\n// scripts make each write ATOMIC and MONOTONIC:\n// • atomic — data + metadata are set in one server-side script.\n// • monotonic (CAS) — a write is applied only when it is NOT older than what\n// is stored, so a slow/older writer can never clobber a newer entry.\n// \"Newer\" is compared on `version` (the data's compute-ts, a decimal string;\n// `tonumber` is exact for the real ts range and handles the -1 genesis value).\n// cjson is used ONLY to READ the stored comparand; the value written is always\n// the caller-serialized ARGV string, so we never hit cjson's empty-array\n// re-encode pitfall.\n\n/** Atomic one-shot recompute write: set data + metadata iff incoming version\n * (ARGV[3]) is not older than the stored one. KEYS: data, meta. */\nconst WRITE_DATA_AND_META = `\nlocal cur = redis.call('GET', KEYS[2])\nif cur then\n local ok, m = pcall(cjson.decode, cur)\n if ok and type(m) == 'table' and m.version and tonumber(ARGV[3]) < tonumber(m.version) then\n return 0\n end\nend\nredis.call('SET', KEYS[1], ARGV[1], 'EX', tonumber(ARGV[4]))\nredis.call('SET', KEYS[2], ARGV[2], 'EX', tonumber(ARGV[4]))\nreturn 1`;\n\n/** Data-only write (subscribe path): set data iff incoming version (ARGV[2]) is\n * not older than the stored metadata's version. KEYS: data, meta. */\nconst WRITE_DATA_ONLY = `\nlocal cur = redis.call('GET', KEYS[2])\nif cur then\n local ok, m = pcall(cjson.decode, cur)\n if ok and type(m) == 'table' and m.version and tonumber(ARGV[2]) < tonumber(m.version) then\n return 0\n end\nend\nredis.call('SET', KEYS[1], ARGV[1], 'EX', tonumber(ARGV[3]))\nreturn 1`;\n\n/** Metadata-only replace (subscription recompute): set meta iff incoming version\n * (ARGV[2]) is not older than the stored one. KEYS: meta. */\nconst REPLACE_META = `\nlocal cur = redis.call('GET', KEYS[1])\nif cur then\n local ok, m = pcall(cjson.decode, cur)\n if ok and type(m) == 'table' and m.version and tonumber(ARGV[2]) < tonumber(m.version) then\n return 0\n end\nend\nredis.call('SET', KEYS[1], ARGV[1], 'EX', tonumber(ARGV[3]))\nreturn 1`;\n\n/** Metadata-only horizon advance (still-fresh subscription): bump lastSnapshotTs\n * ONLY, without recomputing. Applies ARGV[1] iff the new horizon (ARGV[2]) is\n * strictly ahead of the stored one AND no newer computation (stored.version >\n * ARGV[3], the version the caller based its advance on) has landed meanwhile —\n * so an advance never reverts a fresh recompute. KEYS: meta. */\nconst ADVANCE_META = `\nlocal cur = redis.call('GET', KEYS[1])\nif not cur then return 0 end\nlocal ok, m = pcall(cjson.decode, cur)\nif not ok or type(m) ~= 'table' then return 0 end\nif tonumber(ARGV[2]) <= tonumber(m.lastSnapshotTs) then return 0 end\nif m.version and tonumber(m.version) > tonumber(ARGV[3]) then return 0 end\nredis.call('SET', KEYS[1], ARGV[1], 'EX', tonumber(ARGV[4]))\nreturn 1`;\n\nexport class CacheLayer {\n public redis: Redis | UpstashRedis;\n private readonly ttlSeconds: number;\n /** Cached EVALSHA digests keyed by Lua source (see evalScript). */\n private readonly scriptShaCache = new Map<string, string>();\n\n constructor(upstash: Redis | UpstashRedis, ttlSeconds: number = 3600) {\n this.redis = upstash;\n this.ttlSeconds = ttlSeconds;\n }\n\n /**\n * Run a Lua script via EVALSHA using the script's SHA1 (computed locally — the\n * same digest Redis keys scripts by, so no SCRIPT LOAD round-trip is needed on\n * the hot path). On NOSCRIPT (the script isn't in Redis's cache yet, e.g. first\n * use or after a Redis restart/failover) load it explicitly via SCRIPT LOAD and\n * re-run via EVALSHA — so the script ends up cached server-side and subsequent\n * EVALSHA calls hit. The runtime client is always ioredis (see initCacheLayer).\n */\n private async evalScript(src: string, keys: string[], args: (string | number)[]): Promise<unknown> {\n const redis = this.redis as Redis;\n let sha = this.scriptShaCache.get(src);\n if (!sha) {\n sha = createHash(\"sha1\").update(src).digest(\"hex\");\n this.scriptShaCache.set(src, sha);\n }\n return redisRetryPolicy.execute(async () => {\n try {\n return await redis.evalsha(sha!, keys.length, ...keys, ...args);\n } catch (err) {\n if (err instanceof Error && err.message.includes(\"NOSCRIPT\")) {\n // Not in Redis's script cache yet — load it, then execute by digest.\n const loadedSha = (await redis.call(\"SCRIPT\", \"LOAD\", src)) as string;\n this.scriptShaCache.set(src, loadedSha);\n return await redis.evalsha(loadedSha, keys.length, ...keys, ...args);\n }\n throw err;\n }\n });\n }\n\n /** One-shot recompute: atomically write data + metadata (CAS on version). */\n async setQueryCacheAndMetadataFor(\n cacheKey: string, data: unknown,\n metaData: QueryCacheMetadata,\n ttlSeconds?: number,\n ): Promise<void> {\n const ttl = ttlSeconds ?? this.ttlSeconds;\n const dataJson = supaliveStringify({ data, version: metaData.version });\n const metaJson = supaliveStringify(metaData);\n await this.evalScript(\n WRITE_DATA_AND_META,\n [this.getQueryCacheDataKey(cacheKey), this.getQeruyCacheMetaKey(cacheKey)],\n [dataJson, metaJson, metaData.version, String(ttl)],\n );\n }\n\n /** Subscribe path: write the data blob only (CAS against the metadata's\n * version). The metadata is written separately by the sub-worker; the\n * `version` links the two so a divergent pair is detected on read. */\n async setQueryCacheFor(cacheKey: string, data: unknown, version: string, ttlSeconds?: number): Promise<void> {\n const ttl = ttlSeconds ?? this.ttlSeconds;\n const dataJson = supaliveStringify({ data, version });\n await this.evalScript(\n WRITE_DATA_ONLY,\n [this.getQueryCacheDataKey(cacheKey), this.getQeruyCacheMetaKey(cacheKey)],\n [dataJson, version, String(ttl)],\n );\n }\n\n async getQueryCacheFor(cacheKey: string): Promise<QueryCacheEntry | null> {\n const dataRaw = await redisRetryPolicy.execute(() => this.redis.get(this.getQueryCacheDataKey(cacheKey)));\n\n if (!dataRaw) return null;\n\n try {\n const parsed = typeof dataRaw === \"string\" ? JSON.parse(dataRaw) : dataRaw;\n return QueryCacheEntrySchema.parse(parsed);\n } catch {\n return null;\n }\n }\n\n /** Subscription recompute: replace the whole metadata (CAS on version). */\n async replaceQueryCacheMetadata(cacheKey: string, metaData: QueryCacheMetadata, ttlSeconds?: number): Promise<void> {\n const ttl = ttlSeconds ?? this.ttlSeconds;\n const metaJson = supaliveStringify(metaData);\n await this.evalScript(\n REPLACE_META,\n [this.getQeruyCacheMetaKey(cacheKey)],\n [metaJson, metaData.version, String(ttl)],\n );\n }\n\n /** Still-fresh subscription: advance `lastSnapshotTs` only, preserving the\n * data's `version` + `readSet` (no recompute). Monotonic and non-clobbering\n * — see ADVANCE_META. `metaData` carries the new horizon plus the version +\n * readSet the caller read, which the script keeps unless a newer computation\n * has since landed. */\n async advanceQueryCacheMetadata(cacheKey: string, metaData: QueryCacheMetadata, ttlSeconds?: number): Promise<void> {\n const ttl = ttlSeconds ?? this.ttlSeconds;\n const metaJson = supaliveStringify(metaData);\n await this.evalScript(\n ADVANCE_META,\n [this.getQeruyCacheMetaKey(cacheKey)],\n [metaJson, String(metaData.lastSnapshotTs), metaData.version, String(ttl)],\n );\n }\n\n async getQueryCacheMetaData(cacheKey: string): Promise<QueryCacheMetadata | null> {\n const dataRaw = await redisRetryPolicy.execute(() => this.redis.get(this.getQeruyCacheMetaKey(cacheKey)));\n\n if (!dataRaw) return null;\n\n try {\n const parsed = typeof dataRaw === \"string\" ? JSON.parse(dataRaw) : dataRaw;\n return QueryCacheMetadataSchema.parse(parsed);\n } catch {\n return null;\n }\n }\n\n async persistRegistrationRecord(subId: string, record: RegistrationRecord): Promise<void> {\n const data = supaliveStringify(record);\n await redisRetryPolicy.execute(() => this.redis.set(this.getRegistrationKey(subId), data));\n }\n\n async deleteRegistrationRecord(subId: string): Promise<void> {\n await redisRetryPolicy.execute(() => this.redis.del(this.getRegistrationKey(subId)));\n }\n\n async deleteRegistrationsRecord(subIds: string[]): Promise<void> {\n if (subIds.length === 0) return;\n\n await redisRetryPolicy.execute(() =>\n this.redis.del(...subIds.map((subId) => this.getRegistrationKey(subId))),\n );\n }\n\n async fetchRegistrationRecordsBatch(\n subIds: string[],\n ): Promise<Map<string, RegistrationRecord | null>> {\n const result = new Map<string, RegistrationRecord | null>();\n const CHUNK_SIZE = 100;\n\n for (let i = 0; i < subIds.length; i += CHUNK_SIZE) {\n const chunk = subIds.slice(i, i + CHUNK_SIZE);\n const keys = chunk.map((id) => `sl:reg:${id}`);\n try {\n if (keys.length === 0) continue;\n\n const rawValues = await redisRetryPolicy.execute(() =>\n this.redis.mget(...keys),\n );\n\n for (let j = 0; j < chunk.length; j++) {\n const subId = chunk[j]!;\n const raw = rawValues[j];\n const rawJson = typeof raw === \"string\" ? JSON.parse(raw) : raw;\n\n if (!raw) {\n result.set(subId, null);\n } else {\n try {\n const parsed = RegistrationRecordSchema.parse(rawJson);\n result.set(subId, parsed);\n } catch {\n logger.error(`Failed to parse sl:reg:${subId} during batch recovery`);\n result.set(subId, null);\n }\n }\n }\n } catch (err) {\n logger.error(err, \"Failed to fetch registration records batch\");\n for (const subId of chunk) {\n result.set(subId, null);\n }\n }\n }\n return result;\n }\n\n async getCachedTableColumnTypes(key: string): Promise<CachedPgMetadata | null> {\n const dataRaw = await redisRetryPolicy.execute(() => this.redis.get(key));\n if (!dataRaw) return null;\n\n try {\n const parsed = typeof dataRaw === \"string\" ? JSON.parse(dataRaw) : dataRaw;\n return CachedPgMetadataSchema.parse(parsed);\n } catch {\n return null;\n }\n }\n\n async setCachedTableColumnsTypes(key: string, value: CachedPgMetadata): Promise<void> {\n await redisRetryPolicy.execute(() =>\n this.redis.set(key, supaliveStringify(value)),\n );\n }\n\n /**\n * Race participant: try to claim `lock:recompute:<subId>:<commitTs>`\n * with `SET NX EX`. The single Redis round-trip both arbitrates and\n * sets a TTL fallback in case the winner crashes mid-recompute.\n */\n async tryAcquireRecomputeLock(instanceId: string, subId: string, commitTs: string, ttl: number): Promise<boolean> {\n const key = `lock:recompute:${subId}:${commitTs}`;\n return redisRetryPolicy.execute(async () => {\n try {\n const res = await this.redis.set(key, instanceId, \"EX\", ttl, \"NX\");\n return res === \"OK\";\n } catch (err) {\n logger.error(err, `Failed to acquire recompute lock ${key}`);\n // Fail-closed: if Redis is misbehaving, don't run a recompute that\n // we can't coordinate. The next mutation will trigger another race.\n return false;\n }\n });\n }\n\n\n private getRegistrationKey(subId: string): string {\n return `sl:reg:${subId}`;\n }\n\n private getQueryCacheDataKey(cacheKey: string): string {\n return `sl:cache:${cacheKey}`;\n }\n\n private getQeruyCacheMetaKey(cacheKey: string): string {\n return `sl:meta:${cacheKey}`;\n }\n}","import { DatabaseConfig, SupaliveServerConfig, UpstashConfig } from \"../config\";\nimport { logger } from \"../logger\";\nimport { CacheLayer } from \"./cache\";\nimport { Database } from \"./db\";\nimport { MySqlDatabase } from \"./mysql\";\nimport { PgDatabase } from \"./postgres\";\nimport { SupaliveDb } from \"./realtime_db\";\nimport Redis from \"ioredis\";\n\n/**\n * Current core runtime-bootstrap version. Bump this whenever core needs a new\n * runtime database operation — a data seed, a backfill, or a schema change that\n * must be applied directly at runtime rather than through the migration\n * pipeline. Each increment adds a case to `Database.coreMigrationStatements`.\n *\n * The core *schema* (the `commit_logs`/`metadata` tables and the\n * `global_commit_ts` sequence/counter) is NOT versioned here — it comes from\n * the generated schema (`coreTablesSql` / `generateSchemaSql`), which must be\n * applied to the database BEFORE `initCore` runs.\n */\nexport const CORE_VERSION = 1n;\n\n/**\n * Bring the core runtime state up to {@link CORE_VERSION}. Idempotent and safe\n * to call from every process at startup: a lock-free version read short-circuits\n * the common \"already current\" case, and the actual migration runs under a\n * cross-process lock with a re-check so only one process applies it.\n *\n * Assumes the core schema already exists (apply the generated schema first);\n * the `metadata` table is where the version and seed rows live.\n */\nexport async function initCore(db: Database): Promise<void> {\n // Fast path: no lock, no transaction.\n const current = await db.getCoreVersion();\n if (current >= CORE_VERSION) return;\n\n await db.withCoreInitLock(async (ctx) => {\n // Re-check under the lock — another process may have migrated while we\n // were waiting to acquire it.\n const from = await ctx.getVersion();\n if (from >= CORE_VERSION) return;\n\n for (let v = from + 1n; v <= CORE_VERSION; v++) {\n const statements = db.coreMigrationStatements(v);\n logger.info(`Migrating core schema ${v - 1n} -> ${v} (${statements.length} statements)`);\n for (const sql of statements) {\n await ctx.exec(sql);\n }\n }\n\n await ctx.setVersion(CORE_VERSION);\n logger.info(`Core schema now at version ${CORE_VERSION}`);\n });\n}\n\n/**\n * Default and env override for the per-statement DB timeout. PG enforces\n * this via `statement_timeout` (server cancels), MySQL via the per-query\n * `timeout` option (driver issues KILL QUERY). Set 0 to disable.\n */\nexport const DB_QUERY_TIMEOUT_MS = Number(\n process.env.SUPALIVE_DB_QUERY_TIMEOUT_MS ?? 30_000,\n);\n\nexport type InitDbOptions = {\n DbConfig: DatabaseConfig;\n CacheLayer: CacheLayer;\n forceBootstrapReload?: boolean;\n workerId?: number;\n};\n\nexport async function initDatabase(options: InitDbOptions): Promise<SupaliveDb> {\n const impl = createDatabase(options.DbConfig, options.workerId);\n const supaliveDb = SupaliveDb.create({ db: impl });\n // Seed core runtime state (metadata rows, MySQL counter seed) up to date.\n // The core schema (tables, indexes, global_commit_ts) is expected to already\n // exist from the applied schema.\n await initCore(impl);\n await bootstrapDbTypes(supaliveDb, options.CacheLayer, options.forceBootstrapReload ?? false);\n return supaliveDb;\n}\n\nexport function createDatabase(config: DatabaseConfig, workerId?: number): PgDatabase | MySqlDatabase {\n const bindings: Record<string, any> = {\n \"db\": config.type,\n };\n if (workerId) {\n bindings[\"worker\"] = workerId;\n }\n\n if (config.type === \"postgres\") {\n return new PgDatabase(\n logger.child(bindings), {\n ...config,\n connectionString: config.connectionString,\n min: config.minConnections ?? 0,\n max: config.maxConnections ?? 20,\n connectionTimeoutMillis: config.connectionTimeoutMillis ?? 15_000,\n idleTimeoutMillis: config.idleTimeoutMillis ?? 30_000,\n // server-enforced per-statement timeout; pg cancels the running\n // query when it fires. 0 disables it.\n statement_timeout: DB_QUERY_TIMEOUT_MS > 0 ? DB_QUERY_TIMEOUT_MS : undefined,\n });\n } else {\n return new MySqlDatabase(\n logger.child(bindings), {\n ...config,\n connectionLimit: config.maxConnections ?? 20,\n connectTimeout: config.connectionTimeoutMillis ?? 15_000,\n // mysql2 pool: 0 = wait forever for a free conn (default). queueLimit\n // bounds the wait queue length so backpressure surfaces as an error\n // rather than unbounded growth.\n queueLimit: config.queueLimit ?? 0,\n // driver-side per-statement timeout applied uniformly in\n // MySqlDatabase.query.\n queryTimeoutMs: DB_QUERY_TIMEOUT_MS,\n });\n }\n}\n\n/**\n * Eagerly load column types into the apply-path cache at server start.\n * In production, the result is cached in Redis under\n * `supalive:schema:types:${SUPALIVE_SCHEMA_VERSION ?? \"v1\"}` so other\n * instances starting cold can skip the pg_attribute round-trip. In any\n * other env we go straight to PG every time so local schema iteration\n * is never blocked by stale Redis state. Migrations should publish a\n * `schema:reload` message (see {@link publishSchemaReload}) after\n * applying DDL — every instance picks that up and re-bootstraps.\n */\nexport async function bootstrapDbTypes(db: SupaliveDb, cache: CacheLayer, forceReload = false): Promise<void> {\n // TODO: remove this check if we needs mysql column types\n if (db.impl.dbType === \"mysql\") {\n // currently we don't use mysql column type\n return;\n }\n\n const inProd = process.env.NODE_ENV === \"production\";\n const version = process.env.SUPALIVE_SCHEMA_VERSION ?? \"v1\";\n const key = `supalive:schema:types:${version}`;\n try {\n await db.impl.bootstrapColumnTypes({\n cacheKey: key,\n redis: inProd && !forceReload ? cache : undefined,\n });\n logger.info({ columnsTypes: db.impl.columnTypes }, `Bootstrapped db column types (redis=${inProd}, key=${key})`);\n } catch (err) {\n logger.error(err, \"Failed to bootstrap db column types\");\n throw err;\n }\n}\n\nexport type InitCacheLayerOptions = {\n upstash: UpstashConfig;\n /* Default 3600 */\n cacheTtlSeconds?: number;\n /* Default 15_000 */\n connectionTimeout?: number;\n /* Default 1 */\n commandMaxRetries?: number;\n /* Default 5 */\n commandSubMaxRetries?: number;\n /* Default true */\n createSubClient?: boolean;\n};\n\nexport function initCacheLayer(config: InitCacheLayerOptions): { cacheLayer: CacheLayer, redisSubClient?: Redis } {\n const redis = new Redis(config.upstash.redisUrl, {\n connectTimeout: config.connectionTimeout ?? 15_000,\n maxRetriesPerRequest: config.commandMaxRetries ?? 1,\n keepAlive: 3000,\n });\n \n const redisSub = config.createSubClient ? new Redis(config.upstash.redisUrl, {\n connectTimeout: config.connectionTimeout ?? 15_000,\n maxRetriesPerRequest: config.commandSubMaxRetries ?? 5,\n disableClientInfo: true,\n retryStrategy: (times) => {\n if (times > 5) return null;\n return Math.min(times * 200, 2000);\n }\n }) : undefined;\n\n return {\n cacheLayer: new CacheLayer(redis, config.cacheTtlSeconds ?? 3600),\n redisSubClient: redisSub\n };\n}\n\n","import type { Database } from \"../db/db\";\nimport type { RawReadEntry, ReadEntry } from \"../db/types_db\";\nimport type { CacheLayer } from \"../db/cache\";\nimport {\n checkWriteSetAffectsReadSet,\n evaluateCacheFreshness,\n mapReadSetToRaw,\n type RawWriteEntry,\n} from \"./overlap-checker\";\nimport {\n AffectedSubscription,\n RegisterSubscriptionParams,\n RegisterSubscriptionResult,\n RegistrationRecord,\n TrackedSubscription,\n UnregisterSubscriptionParams,\n UnregisterSubscriptionResult,\n UnregisterSubscriptionsParams,\n UnregisterSubscriptionsResult,\n UpdateSubscriptionReadSetParams,\n UpdateSubscriptionReadSetResult,\n} from \"./subscription-manager-types\";\nimport { logger as baseLogger, logLevelOf } from \"../logger\";\nimport { RetentionWatermark, RETENTION_WATERMARK_CHANNEL } from \"./retention-watermark\";\n\n// A logical worker that owns a disjoint slice of subscriptions. The dispatcher\n// routes each operation here by FNV-1a(subId) % numWorkers. Each worker is\n// independent: no shared state, no locks, no cross-worker coordination.\n// invalidate() is a pure function of the worker's own state + the writeSet.\n\nexport class SubscriptionWorker {\n readonly id: number;\n private subscriptions = new Map<string, TrackedSubscription>();\n private cache: CacheLayer;\n private db: Database;\n // In-memory retained commit-log watermark. Seeded at startup and moved forward on\n // prune; read on the register hot path instead of a per-check Redis GET.\n private retentionWatermark = new RetentionWatermark();\n public logger: ReturnType<typeof baseLogger.child>;\n\n constructor(id: number, cache: CacheLayer, db: Database) {\n this.id = id;\n this.cache = cache;\n this.db = db;\n this.logger = baseLogger.child(\n { name: \"SUBS_WRK\", wid: id },\n { level: logLevelOf(\"SUBS_MANAGER_SERVER_LOG_LEVEL\") },\n );\n }\n\n size(): number {\n return this.subscriptions.size;\n }\n\n has(subId: string): boolean {\n return this.subscriptions.has(subId);\n }\n\n /**\n * Lowest `lastSnapshotTs` across this worker's subscriptions, or `null`\n * when there are none. Used by the dispatcher to derive a safe prune watermark.\n */\n minSubscriptionTs(): bigint | null {\n let min: bigint | null = null;\n for (const sub of this.subscriptions.values()) {\n if (min === null || sub.lastSnapshotTs < min) min = sub.lastSnapshotTs;\n }\n return min;\n }\n\n /**\n * Seed/repair the persisted retained watermark from the actual oldest retained\n * commit log (`min(ts)`), then load it into memory and broadcast it. Called\n * once at sub-manager startup — the `min(ts)` scan is off the hot path, and\n * it corrects `metadata.min_retained_ts` even for a DB that was pruned before\n * this feature existed. Returns the resulting watermark so the manager can\n * broadcast it to the other workers.\n */\n async refreshRetentionWatermark(): Promise<bigint> {\n const real = (await this.db.getMinCommitLogTs()) ?? 0n;\n await this.db.updateMinSnapshotTimestamp(real);\n // Read back so our in-memory copy matches the persisted (monotonic) value.\n const persisted = await this.db.getMinSnapshotTimestamp();\n this.retentionWatermark.set(persisted);\n await this.publishRetentionWatermark(persisted);\n return this.retentionWatermark.get();\n }\n\n /** Adopt a watermark broadcast by the manager (in-process from the pruning\n * worker). Monotonic, so an out-of-order message can't lower it. */\n setRetentionWatermark(ts: bigint): void {\n this.retentionWatermark.set(ts);\n }\n\n /**\n * Prune commit logs older than `minRetainedTs`. Advertises the new watermark FIRST —\n * persist it, bump our in-memory copy, and broadcast it over Redis — and only\n * then deletes, so no reader ever trusts a cache entry whose freshness scan\n * would include rows we're about to remove. Returns rows deleted.\n */\n async prune(minRetainedTs: bigint): Promise<bigint> {\n await this.db.updateMinSnapshotTimestamp(minRetainedTs);\n this.retentionWatermark.set(minRetainedTs);\n await this.publishRetentionWatermark(minRetainedTs);\n const deleted = await this.db.pruneCommitLogsBefore(minRetainedTs);\n return deleted;\n }\n\n /** Broadcast the watermark to app-server instances over Redis pub/sub. Workers in\n * this process get it in-process via {@link setRetentionWatermark}. */\n private async publishRetentionWatermark(ts: bigint): Promise<void> {\n try {\n await this.cache.redis.publish(RETENTION_WATERMARK_CHANNEL, ts.toString());\n } catch (err) {\n this.logger.error(err, \"failed to publish commit-log watermark\");\n }\n }\n\n async register(\n params: RegisterSubscriptionParams,\n ): Promise<RegisterSubscriptionResult> {\n const { subId, cacheKey, queryName, args, queryIdentity, instanceName, lastSnapshotTs } = params;\n this.logger.info(`registering subscription: ${subId} from ${instanceName}`);\n\n const existing = this.subscriptions.get(subId);\n\n if (existing && !existing.timeout) {\n existing.instances.add(instanceName);\n return {\n cacheKey,\n recompute: false,\n readSet: existing.readSet,\n };\n }\n\n if (existing?.timeout) {\n this.logger.info(`reactivating subscription: ${subId} from: ${instanceName}`);\n clearTimeout(existing.timeout);\n existing.timeout = null;\n }\n\n const metadata = await this.cache.getQueryCacheMetaData(cacheKey);\n const freshness = await evaluateCacheFreshness(this.db, metadata, lastSnapshotTs, this.retentionWatermark.get());\n let needsRecompute = !freshness.fresh;\n const readSet: ReadEntry[] = freshness.readSet;\n const rawReadSet: RawReadEntry[] = freshness.rawReadSet;\n\n if (!needsRecompute && metadata && lastSnapshotTs > metadata.lastSnapshotTs) {\n // Cache is still valid at a newer snapshot — advance the stored horizon\n // so the next freshness scan starts from here (and stays above the\n // watermark). Preserve the data's version + readSet; the CAS in\n // advanceQueryCacheMetadata won't revert a fresher recompute.\n this.cache\n .advanceQueryCacheMetadata(cacheKey, { lastSnapshotTs, version: metadata.version, readSet })\n .catch((err) => this.logger.error(err, `Failed to advance cache metadata for ${cacheKey}`));\n }\n\n let newSubscription: TrackedSubscription = {\n subId,\n queryName,\n args,\n cacheKey,\n lastSnapshotTs: lastSnapshotTs,\n readSet,\n rawReadSet,\n instances: new Set([instanceName]),\n timeout: null,\n };\n\n const createdWhileChecking = this.subscriptions.get(subId);\n if (createdWhileChecking) {\n if (createdWhileChecking.timeout) {\n clearTimeout(createdWhileChecking.timeout);\n createdWhileChecking.timeout = null;\n }\n\n if (createdWhileChecking.lastSnapshotTs > lastSnapshotTs) {\n newSubscription = {\n ...newSubscription,\n ...createdWhileChecking,\n instances: new Set([...createdWhileChecking.instances, instanceName]),\n };\n needsRecompute = false;\n }\n }\n\n this.subscriptions.set(subId, newSubscription);\n\n this.persistRegistration(subId, { queryName, args, queryIdentity, cacheKey });\n\n // `version` lets the app server verify the cached data blob it serves\n // matches the metadata we validated (undefined when there was no metadata,\n // i.e. a plain recompute — the server won't serve from cache then anyway).\n return { cacheKey, recompute: needsRecompute, readSet, version: metadata?.version };\n }\n\n async updateReadSet(\n params: UpdateSubscriptionReadSetParams,\n ): Promise<UpdateSubscriptionReadSetResult> {\n const { cacheKey, subId, instanceName, lastSnapshotTs, readSet } = params;\n this.logger.info(`updating subscription readSet: ${subId} from ${instanceName}`);\n\n const existing = this.subscriptions.get(subId);\n if (!existing) {\n return { tracked: false };\n }\n\n if (lastSnapshotTs < existing.lastSnapshotTs) {\n return { tracked: true };\n }\n\n const rawReadSet = mapReadSetToRaw(readSet);\n this.subscriptions.set(subId, { ...existing, readSet, rawReadSet, lastSnapshotTs });\n // Fresh recompute: version == the compute ts (== lastSnapshotTs here). The\n // server writes the matching data blob with this same version.\n await this.cache.replaceQueryCacheMetadata(cacheKey, { lastSnapshotTs, version: String(lastSnapshotTs), readSet });\n\n return { tracked: true };\n }\n\n async unregister(\n params: UnregisterSubscriptionParams,\n jitter: number = 0,\n ): Promise<UnregisterSubscriptionResult> {\n const { subId, instanceName } = params;\n this.logger.info(`unregistering subscription: ${subId} from: ${instanceName}`);\n\n const sub = this.subscriptions.get(subId);\n if (!sub) return { ok: true };\n\n if (sub.timeout) {\n clearTimeout(sub.timeout);\n }\n\n if (sub.instances.size > 1) {\n sub.instances.delete(instanceName);\n return { ok: true };\n }\n\n this.logger.debug(`scheduling unregister subscription: ${subId} from: ${instanceName} in 15s`);\n sub.timeout = setTimeout(async () => {\n sub.instances.delete(instanceName);\n\n if (sub.instances.size === 0) {\n this.logger.info(`unregistered subscription: ${subId} from: ${instanceName}`);\n this.subscriptions.delete(subId);\n await this.deleteRegistration(subId);\n }\n }, 15_000 + jitter);\n\n return { ok: true };\n }\n\n async unregisters(\n params: UnregisterSubscriptionsParams,\n ): Promise<UnregisterSubscriptionsResult> {\n this.logger.info({ subIds: params }, `scheduling unregistering subscriptions`);\n for (const param of params) {\n this.unregister(param, Math.floor(Math.random() * 100));\n }\n\n return params.map(() => ({ ok: true }));\n }\n\n /**\n * Synchronous, CPU-bound. Scans this worker's slice of subscriptions and\n * returns the ones affected by the writeSet.\n */\n invalidate(rawWriteSet: RawWriteEntry[]): AffectedSubscription[] {\n const affected: AffectedSubscription[] = [];\n\n for (const [, sub] of this.subscriptions) {\n if (sub.instances.size === 0) continue;\n if (!checkWriteSetAffectsReadSet(rawWriteSet, sub.rawReadSet)) continue;\n\n affected.push({\n subId: sub.subId,\n queryName: sub.queryName,\n args: sub.args,\n cacheKey: sub.cacheKey,\n notifyInstances: Array.from(sub.instances),\n });\n }\n\n return affected;\n }\n\n private async persistRegistration(subId: string, record: RegistrationRecord): Promise<void> {\n try {\n await this.cache.persistRegistrationRecord(subId, record);\n } catch (err) {\n this.logger.error(err, `[worker ${this.id}] Failed to persist registration for ${subId}`);\n }\n }\n\n private async deleteRegistration(subId: string): Promise<void> {\n try {\n await this.cache.deleteRegistrationRecord(subId);\n } catch (err) {\n this.logger.error(err, `[worker ${this.id}] Failed to delete registration for ${subId}`);\n }\n }\n}\n","import type { Database } from \"../db/db\";\nimport { mapWriteSetToRaw } from \"./overlap-checker\";\nimport {\n InvalidateWritesetParamsSchema,\n RegisterSubscriptionBatchParamsSchema,\n RegisterSubscriptionParamsSchema,\n UnregisterSubscriptionParamsSchema,\n UnregisterSubscriptionsParamsSchema,\n UpdateSubscriptionReadSetParamsSchema,\n type InvalidateWritesetParams,\n type RegisterSubscriptionParams,\n type UnregisterSubscriptionParams,\n type UnregisterSubscriptionsParams,\n type UpdateSubscriptionReadSetParams,\n} from \"./subscription-manager-types\";\nimport type { SubscriptionWorker } from \"./subscription-worker\";\nimport { MsgType } from \"./subscription-manager-worker-protocol\";\n\n/**\n * Dispatch a single worker operation against an already-constructed\n * {@link SubscriptionWorker} + {@link Database}. Shared by:\n *\n * • the worker-thread entry (`subscription-manager-worker-entry.ts`), where\n * the payload just crossed a `postMessage` boundary and is untrusted, so it\n * is Zod-parsed against the matching schema; and\n * • the in-process {@link InlineWorkerHandle}, where the payload is the\n * manager's own already-typed object (no thread hop, no structured-clone) —\n * `trusted: true` skips the redundant parse so the call goes straight\n * through to the worker method.\n *\n * `MsgType.Init` is NOT handled here: it constructs the worker/db, which is\n * bootstrap logic owned by each caller (the thread entry builds its own pool;\n * the inline handle is handed live instances).\n */\nexport async function dispatchWorkerMessage(\n worker: SubscriptionWorker,\n db: Database,\n type: MsgType,\n payload: unknown,\n opts?: { trusted?: boolean },\n): Promise<unknown> {\n const trusted = opts?.trusted ?? false;\n\n switch (type) {\n case MsgType.Register: {\n const params = trusted\n ? (payload as RegisterSubscriptionParams)\n : RegisterSubscriptionParamsSchema.parse(payload);\n return worker.register(params);\n }\n case MsgType.RegisterBatch: {\n const params = trusted\n ? (payload as RegisterSubscriptionParams[])\n : RegisterSubscriptionBatchParamsSchema.parse(payload);\n return Promise.all(params.map((p) => worker.register(p)));\n }\n case MsgType.UpdateReadSet: {\n const params = trusted\n ? (payload as UpdateSubscriptionReadSetParams)\n : UpdateSubscriptionReadSetParamsSchema.parse(payload);\n return worker.updateReadSet(params);\n }\n case MsgType.Unregister: {\n const params = trusted\n ? (payload as UnregisterSubscriptionParams)\n : UnregisterSubscriptionParamsSchema.parse(payload);\n return worker.unregister(params);\n }\n case MsgType.Unregisters: {\n const params = trusted\n ? (payload as UnregisterSubscriptionsParams)\n : UnregisterSubscriptionsParamsSchema.parse(payload);\n return worker.unregisters(params);\n }\n case MsgType.Invalidate: {\n const { writeSet } = trusted\n ? (payload as InvalidateWritesetParams)\n : InvalidateWritesetParamsSchema.parse(payload);\n return worker.invalidate(mapWriteSetToRaw(writeSet));\n }\n case MsgType.MinSubscriptionTs: {\n return worker.minSubscriptionTs();\n }\n case MsgType.LatestSnapshotTs: {\n return db.getLatestSnapshotTimestamp();\n }\n case MsgType.Prune: {\n return worker.prune(payload as bigint);\n }\n case MsgType.RefreshRetentionWatermark: {\n return worker.refreshRetentionWatermark();\n }\n case MsgType.SetRetentionWatermark: {\n worker.setRetentionWatermark(payload as bigint);\n return null;\n }\n case MsgType.Shutdown: {\n try { await db.close(); } catch (_) { /* ignore */ }\n return null;\n }\n case MsgType.Init:\n throw new Error(\"MsgType.Init must be handled by the caller (worker bootstrap)\");\n default:\n throw new Error(`Unknown MsgType: ${type satisfies never}`);\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,521 @@
1
+ import { $n as AffectedSubscription, Dr as MutationResult, Ht as SupaliveDb, Lt as JobScheduler, Qn as CacheLayer, Un as Database, ar as RegisterSubscriptionResult, cr as UnregisterSubscriptionResult, dr as UpdateSubscriptionReadSetParams, er as InvalidateWritesetParams, lr as UnregisterSubscriptionsParams, nr as InvalidateWritesetResult, nt as RegisteredProcedure, or as UnregisterSubscriptionParams, pr as UpdateSubscriptionReadSetResult, rr as RegisterSubscriptionParams, ur as UnregisterSubscriptionsResult } from "./index-WMhtVzJA.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/server/subscription-manager-client.d.ts
10
+ /**
11
+ * Callback the app server registers to process invalidations that were
12
+ * deferred during a sub-manager outage. When the sub-manager reconnects and
13
+ * the buffered writeSets are flushed in one batched RPC, the resulting
14
+ * `affected[]` is handed to this callback — which is expected to do the
15
+ * same work as the normal per-mutation `reexecuteAffectedQuery` path.
16
+ */
17
+ type AffectedHandler = (affected: AffectedSubscription[]) => Promise<void>;
18
+ /**
19
+ * Callback the app server registers to drive recovery on sub-manager
20
+ * reconnect. Iterates `serverSubscriptions`, reads `sl:reg:<subId>` from
21
+ * Redis, and re-registers each known sub. Resolves when complete; only then
22
+ * does the client's buffer flush and gated calls release.
23
+ */
24
+ type RecoveryDriver = () => Promise<void>;
25
+ /**
26
+ * The transport-agnostic contract the app server uses to talk to the
27
+ * subscription manager. Implemented by:
28
+ *
29
+ * • {@link SubManagerClient} — the WebSocket RPC client, for a sub-manager
30
+ * running in its own process (`subManagerUrl`); and
31
+ * • the object returned by `SubscriptionManager.localLink()` — a direct
32
+ * in-process link when the manager is embedded in the app server (no
33
+ * socket, no port, no serialization).
34
+ *
35
+ * Injecting this into `SupaliveServerConfig.subManager` lets a deployment run
36
+ * combined (one process) now and split the sub-manager out later by switching
37
+ * back to `subManagerUrl`, with no other code changes.
38
+ */
39
+ interface SubManagerLink {
40
+ registerSubscription(params: RegisterSubscriptionParams): Promise<RegisterSubscriptionResult>;
41
+ registerSubscriptionBatch(params: RegisterSubscriptionParams[]): Promise<RegisterSubscriptionResult[]>;
42
+ updateSubscriptionReadSet(params: UpdateSubscriptionReadSetParams): Promise<UpdateSubscriptionReadSetResult>;
43
+ unregisterSubscription(params: UnregisterSubscriptionParams): Promise<UnregisterSubscriptionResult>;
44
+ unregisterSubscriptions(params: UnregisterSubscriptionsParams): Promise<UnregisterSubscriptionsResult>;
45
+ invalidateWriteset(params: InvalidateWritesetParams): Promise<InvalidateWritesetResult>;
46
+ /**
47
+ * Register recovery for the reconnect path. A no-op for the in-process link,
48
+ * which never disconnects.
49
+ */
50
+ setRecoveryDriver(driver: RecoveryDriver): void;
51
+ /**
52
+ * Register the post-recovery batched-invalidate handler. A no-op for the
53
+ * in-process link.
54
+ */
55
+ setAffectedHandler(handler: AffectedHandler): void;
56
+ }
57
+ interface SubManagerClientOptions {
58
+ url: string;
59
+ /**
60
+ * Maximum number of buffered WriteEntry records held during an outage.
61
+ * If exceeded, the buffer is dropped — the commit-log replay path during
62
+ * the next register call will catch up any missed invalidations, at the
63
+ * cost of a brief recompute spike. Default 50_000 entries.
64
+ */
65
+ invalidateBufferLimit?: number;
66
+ }
67
+ /**
68
+ * RPC client for the (single) sub-manager process.
69
+ *
70
+ * Design (Convex-inspired):
71
+ * • The sub-manager is in-memory and ephemeral. There is no version
72
+ * protocol, no client-side registration registry, no OUT_OF_SYNC
73
+ * replay logic. All recovery is driven from the app server using
74
+ * state it already has (`serverSubscriptions`) plus registration
75
+ * records persisted to Redis at `sl:reg:<subId>`.
76
+ *
77
+ * • Out-of-order `updateSubscriptionReadSet` calls are filtered server
78
+ * side using `lastSnapshotTs` (monotonic DB commit timestamp).
79
+ *
80
+ * • On rpc-websockets reconnect, the client:
81
+ * 1. Transitions to `busy` (any in-flight `invalidateWriteset`
82
+ * buffers its writeSet).
83
+ * 2. Invokes the app server's `recoveryDriver` to re-register all
84
+ * known subs.
85
+ * 3. Flushes the buffer as one batched `invalidateWriteset` RPC and
86
+ * hands the resulting `affected[]` to `affectedHandler`.
87
+ * 4. Transitions to `ready`.
88
+ *
89
+ * • During an extended outage, the buffer is bounded; if exceeded, it
90
+ * is dropped and we rely on the commit-log replay built into
91
+ * `SubscriptionWorker.register()` to catch each sub up on the next
92
+ * re-register.
93
+ */
94
+ declare class SubManagerClient implements SubManagerLink {
95
+ private client;
96
+ private state;
97
+ private hasConnectedOnce;
98
+ /** Resolves whenever state transitions to 'ready'. Replaced on each busy→ready cycle. */
99
+ private readyPromise;
100
+ private resolveReady;
101
+ /** Writeset entries accumulated while state === 'busy'. Flushed as one RPC after recovery. */
102
+ private bufferedWriteSets;
103
+ private readonly bufferLimit;
104
+ private bufferDroppedDuringOutage;
105
+ private recoveryDriver?;
106
+ private affectedHandler?;
107
+ constructor(urlOrOptions: string | SubManagerClientOptions);
108
+ /**
109
+ * Wire up the app-server-driven recovery. Must be called before any
110
+ * disconnect/reconnect cycle for buffered writeSets to be flushed and
111
+ * subscriptions re-registered.
112
+ */
113
+ setRecoveryDriver(driver: RecoveryDriver): void;
114
+ /**
115
+ * Wire up the handler invoked when batched invalidate results land
116
+ * after recovery. The handler is expected to drive
117
+ * `reexecuteAffectedQuery` for each entry.
118
+ */
119
+ setAffectedHandler(handler: AffectedHandler): void;
120
+ registerSubscription(params: RegisterSubscriptionParams): Promise<RegisterSubscriptionResult>;
121
+ registerSubscriptionBatch(params: RegisterSubscriptionParams[]): Promise<RegisterSubscriptionResult[]>;
122
+ updateSubscriptionReadSet(params: UpdateSubscriptionReadSetParams): Promise<UpdateSubscriptionReadSetResult>;
123
+ unregisterSubscription(params: UnregisterSubscriptionParams): Promise<UnregisterSubscriptionResult>;
124
+ unregisterSubscriptions(params: UnregisterSubscriptionsParams): Promise<UnregisterSubscriptionsResult>;
125
+ /**
126
+ * Sends the writeSet to the sub-manager for invalidation, OR buffers it
127
+ * if the client is currently disconnected / recovering. Returns
128
+ * `{ affected: [] }` while buffering — the actual `affected` from the
129
+ * buffered batch is delivered via the registered `affectedHandler`
130
+ * after recovery completes.
131
+ */
132
+ invalidateWriteset(params: InvalidateWritesetParams): Promise<InvalidateWritesetResult>;
133
+ /**
134
+ * Single point through which RPCs go. Throws on transport / server error.
135
+ * `registerSubscription`, `updateSubscriptionReadSet`, and
136
+ * `unregisterSubscription` are NOT gated on the ready state: during a
137
+ * disconnect they will fail at the socket layer, and during recovery
138
+ * (which itself uses these methods) gating would deadlock.
139
+ */
140
+ private call;
141
+ private appendToBuffer;
142
+ private flushBuffer;
143
+ private markBusy;
144
+ private markReady;
145
+ private handleClose;
146
+ private handleOpen;
147
+ private runRecovery;
148
+ /**
149
+ * Test/inspection helpers.
150
+ */
151
+ /** @internal */
152
+ isReady(): boolean;
153
+ /** @internal */
154
+ bufferSize(): number;
155
+ }
156
+ //#endregion
157
+ //#region src/config.d.ts
158
+ /** CORS policy for the HTTP RPC endpoint. */
159
+ interface RpcCorsConfig {
160
+ /**
161
+ * Allowed origins. `"*"` sends `Access-Control-Allow-Origin: *`; a list
162
+ * echoes back a request's `Origin` only when it matches (and sets `Vary`).
163
+ * Omit `cors` entirely to send no CORS headers (same-origin only).
164
+ */
165
+ origins: string[] | "*";
166
+ }
167
+ /** Rate-limit policy for the HTTP RPC endpoint (fixed window, per instance). */
168
+ interface RpcRateLimitConfig {
169
+ /** Window length in ms. Default 60000. */
170
+ windowMs?: number;
171
+ /** Max requests per window per key. Default 120. */
172
+ max?: number;
173
+ /**
174
+ * Derive the limiter key from the request. Default: client IP
175
+ * (`X-Forwarded-For` first hop, else socket address). Return a stable string.
176
+ */
177
+ keyBy?: (req: IncomingMessage) => string;
178
+ }
179
+ /**
180
+ * Enables a one-shot HTTP RPC transport for `query`/`mutation`/`action`
181
+ * (no subscriptions — those stay WebSocket-only). Lets clients that don't need
182
+ * live queries call procedures without holding a socket open. Omit to disable
183
+ * the endpoint entirely.
184
+ */
185
+ interface RpcConfig<TContext = unknown> {
186
+ /**
187
+ * The single endpoint path. Default `/_rpc`. Every procedure is called as
188
+ * `POST {path}` with `{ name, input }` in the body — the name is never in the
189
+ * URL. Keep this distinct from `jobPath`.
190
+ */
191
+ path?: string;
192
+ /**
193
+ * Map an HTTP request to the auth `data` handed to `verifyAuth` (the same
194
+ * callback the WebSocket path uses). Default: `Authorization: Bearer <token>`
195
+ * → `{ token }`. Return `undefined` for an anonymous request.
196
+ */
197
+ auth?: (req: IncomingMessage) => Record<string, unknown> | undefined;
198
+ /** CORS policy. Omit for same-origin only (no CORS headers). */
199
+ cors?: RpcCorsConfig;
200
+ /** Rate limiting. Omit for defaults (120/min per IP); set `false` to disable. */
201
+ rateLimit?: RpcRateLimitConfig | false;
202
+ }
203
+ interface UpstashConfig {
204
+ url: string;
205
+ token: string;
206
+ redisUrl: string;
207
+ }
208
+ /** Database configuration for PostgreSQL */
209
+ interface PostgresConfig {
210
+ type: "postgres";
211
+ connectionString: string;
212
+ /** Optional max connections for the Postgres connection pool. Defaults to 10.*/
213
+ maxConnections?: number;
214
+ /** Optional min connections for the Postgres connection pool. Defaults to 1.*/
215
+ minConnections?: number;
216
+ /** Optional idle timeout for Postgres connections in milliseconds. Defaults to 30000 (30 seconds). */
217
+ idleTimeoutMillis?: number;
218
+ /** Optional connection timeout for acquiring Postgres connections in milliseconds. Defaults to 7000 (7 seconds). */
219
+ connectionTimeoutMillis?: number;
220
+ }
221
+ /** Database configuration for MySQL */
222
+ interface MySQLConfig {
223
+ type: "mysql";
224
+ connectionString: string;
225
+ /** Optional max connections for the MySQL connection pool. Defaults to 20.*/
226
+ maxConnections?: number;
227
+ /** Optional min connections for the MySQL connection pool. Defaults to 1.*/
228
+ minConnections?: number;
229
+ /** Optional idle timeout for MySQL connections in milliseconds. Defaults to 30000 (30 seconds). */
230
+ idleTimeoutMillis?: number;
231
+ /** Optional connection timeout for acquiring MySQL connections in milliseconds. Defaults to 7000 (7 seconds). */
232
+ connectionTimeoutMillis?: number;
233
+ /** Optional flag to queue connection requests when pool is exhausted. Defaults to true (queue requests). */
234
+ queueLimit?: number;
235
+ }
236
+ type DatabaseConfig = PostgresConfig | MySQLConfig;
237
+ /**
238
+ * Configuration for the Supalive WebSocket server.
239
+ */
240
+ interface SupaliveServerConfig<TContext = Record<string, unknown>> {
241
+ /** Server port (default: 3000) */
242
+ port?: number;
243
+ /** Server host (default: "0.0.0.0") */
244
+ host?: string;
245
+ cacheLayer: CacheLayer;
246
+ redisSubClient: Redis;
247
+ /**
248
+ * Subscription Manager WebSocket URL for RPC communication.
249
+ * A single sub-manager process owns all subscription state for this
250
+ * deployment. Internally it spawns N logical workers (configured on the
251
+ * sub-manager side) and routes by hash(subId) to spread CPU across them.
252
+ *
253
+ * Provide EITHER this OR {@link subManager} (an in-process link). Exactly
254
+ * one is required.
255
+ */
256
+ subManagerUrl?: string;
257
+ /**
258
+ * In-process subscription-manager link, as an alternative to
259
+ * {@link subManagerUrl}. Use this to embed the sub-manager in the same
260
+ * process as the app server (obtain it from
261
+ * `new SubscriptionManager({ listen: false, inline: true, ... }).localLink()`).
262
+ * The server then talks to the manager via direct method calls — no socket,
263
+ * no port, no serialization. Split the sub-manager into its own process
264
+ * later by switching back to `subManagerUrl` with no other changes.
265
+ */
266
+ subManager?: SubManagerLink;
267
+ /** Database instance */
268
+ database: SupaliveDb;
269
+ /**
270
+ * Optional authentication callback.
271
+ * Called when client sends auth message.
272
+ * Return null to reject authentication.
273
+ */
274
+ verifyAuth?: (data: Record<string, unknown>, sessionId: string) => Promise<TContext | null>;
275
+ /**
276
+ * Server context name for procedure routing.
277
+ * Must match the router's contextName.
278
+ */
279
+ contextName?: string;
280
+ /**
281
+ * Session TTL in seconds (default: 3600 = 1 hour).
282
+ * Sessions are kept alive while connected and expire after disconnect.
283
+ */
284
+ sessionTtlSeconds?: number;
285
+ /**
286
+ * Enable detailed logging.
287
+ */
288
+ logLevel?: Level;
289
+ /**
290
+ * Extract user ID from context for cache key generation.
291
+ * If not provided, caching is disabled (each sub recomputes independently).
292
+ */
293
+ getUserId?: (ctx: TContext | undefined) => string | null;
294
+ /**
295
+ * Cache TTL in seconds (default: 3600).
296
+ * How long query results are cached in Redis.
297
+ */
298
+ cacheTtlSeconds?: number;
299
+ /**
300
+ * Scheduler backing the job system. When provided, the server exposes an
301
+ * HTTP endpoint (`{jobPath}/<jobName>`) that the scheduler POSTs to, and
302
+ * syncs every registered recurring (cron) job at startup. Omit to disable
303
+ * jobs entirely. Use `DevScheduler` locally and `QStashScheduler` in prod.
304
+ */
305
+ scheduler: JobScheduler;
306
+ /**
307
+ * Builds the system server context handed to a job handler's `serverCtx`.
308
+ * Jobs run without a client, so this is where a deployment injects its
309
+ * "system"/service identity. Called per job invocation.
310
+ */
311
+ jobContext?: () => TContext | Promise<TContext>;
312
+ /**
313
+ * Absolute, externally-reachable base URL of this server (no trailing
314
+ * slash), e.g. `https://api.example.com` in prod or `http://127.0.0.1:3000`
315
+ * in dev. Used to build the job endpoint the scheduler calls.
316
+ */
317
+ publicUrl: string;
318
+ /**
319
+ * URL path prefix for the job webhook endpoint. Default `/_jobs`. The full
320
+ * endpoint for a job is `{publicUrl}{jobPath}/<jobName>`.
321
+ */
322
+ jobPath?: string;
323
+ /**
324
+ * Enable the one-shot HTTP RPC transport (query/mutation/action over a
325
+ * single `POST {rpc.path}` endpoint, with `{ name, input }` in the body).
326
+ * Uses the same `verifyAuth` as the WebSocket path. Omit to disable. See
327
+ * {@link RpcConfig}.
328
+ */
329
+ rpc?: RpcConfig<TContext>;
330
+ /**
331
+ * Object storage, exposed to every procedure as `ctx.storage`. When set,
332
+ * query/mutation/action/job handlers can presign uploads/downloads and
333
+ * resolve public URLs without reaching for a module-level singleton. Omit
334
+ * to leave `ctx.storage` undefined (handlers should treat uploads as
335
+ * unconfigured). Build one with `S3CompatibleStorage` from
336
+ * `@supalive/core/storage`.
337
+ */
338
+ storage: ObjectStorage;
339
+ /**
340
+ * Optional application-level Redis pub/sub for cross-instance app messages
341
+ * that are NOT part of the subscription/cache machinery — e.g. auth-token
342
+ * revocations, feature-flag flips, targeted cache busts.
343
+ *
344
+ * The server subscribes to `channels` on its EXISTING Redis subscribe
345
+ * connection (no extra connection) and hands each received message to
346
+ * `onMessage`. Publish from anywhere with
347
+ * `server.publishAppMessage(channel, message)`, which rides the shared
348
+ * command connection. Messages are opaque strings — the app owns encoding.
349
+ */
350
+ appPubSub?: AppPubSubConfig;
351
+ }
352
+ /**
353
+ * App-level pub/sub wiring (see {@link SupaliveServerConfig.appPubSub}). A thin,
354
+ * generic cross-instance message bus layered on the server's Redis connections.
355
+ */
356
+ interface AppPubSubConfig {
357
+ /** Channels this instance subscribes to. */
358
+ channels: string[];
359
+ /**
360
+ * Handler for a message received on one of `channels`. Errors are caught and
361
+ * logged by the server, so a throwing handler can't crash the subscriber.
362
+ */
363
+ onMessage: (channel: string, message: string) => void | Promise<void>;
364
+ }
365
+ interface SubscriptionManagerConfig {
366
+ /**
367
+ * Port for the rpc-websockets listener. Required unless {@link listen} is
368
+ * `false` (embedded/in-process mode, where no socket is opened).
369
+ */
370
+ port?: number;
371
+ /**
372
+ * Upstash/Redis connection for the worker's own cache layer. Required in
373
+ * the default (own-pool) mode; omit when reusing an existing {@link cache}
374
+ * via {@link inline}.
375
+ */
376
+ upstash?: {
377
+ url: string;
378
+ token: string;
379
+ redisUrl: string;
380
+ };
381
+ /**
382
+ * Database connection the worker builds its own pool from. Required in the
383
+ * default mode; omit when reusing an existing {@link db} via {@link inline}.
384
+ */
385
+ database?: DatabaseConfig;
386
+ cacheTTLSeconds: number;
387
+ /**
388
+ * Open the rpc-websockets listener (default `true`). Set to `false` to
389
+ * embed the manager in another process and drive it via
390
+ * {@link SubscriptionManager.localLink}.
391
+ */
392
+ listen?: boolean;
393
+ /**
394
+ * Run the subscription worker inline on the main event loop instead of a
395
+ * `worker_threads` Worker (default `false`). Eliminates the postMessage /
396
+ * structured-clone hop, at the cost of no cross-core parallelism — always a
397
+ * single worker. Intended for embedded (`listen: false`) deployments with
398
+ * light realtime load. Provide {@link db} + {@link cache} to reuse the host
399
+ * server's pool/cache rather than opening new ones.
400
+ */
401
+ inline?: boolean;
402
+ /** In-process `Database` to reuse in {@link inline} mode (borrowed; not closed on stop). */
403
+ db?: Database;
404
+ /** In-process `CacheLayer` to reuse in {@link inline} mode. */
405
+ cache?: CacheLayer;
406
+ /**
407
+ * Number of logical workers inside the sub-manager process.
408
+ *
409
+ * Subscriptions are routed by FNV-1a(subId) % workers so that all operations
410
+ * for a given subId always land on the same worker (preserving the existing
411
+ * `instances: Set<string>` dedup). Each worker owns an independent
412
+ * subscriptions Map; on invalidate, all workers scan their own slice in
413
+ * parallel (logically — JS still runs one at a time inside one event loop,
414
+ * but per-worker scans are smaller so total work is split N ways).
415
+ *
416
+ * Defaults to `SUPALIVE_SUB_MANAGER_WORKERS` env var if set, else 1.
417
+ */
418
+ workers?: number;
419
+ /**
420
+ * Connection pool size *per worker*. If omitted, falls back to
421
+ * `floor(database.maxConnections / workers)` (min 1) so the total pool
422
+ * size stays close to the legacy single-worker configuration.
423
+ */
424
+ maxConnectionsPerWorker?: number;
425
+ }
426
+ //#endregion
427
+ //#region src/server/types_server.d.ts
428
+ type InstanceId = string;
429
+ type SubId = string;
430
+ /** Local tracking of an active subscription (per-session) */
431
+ interface SubscriptionEntry {
432
+ subId: SubId;
433
+ cacheKey: string;
434
+ subscribedTimes: number;
435
+ }
436
+ /**
437
+ * A mutation that has been validated and parked on the per-session FIFO
438
+ * queue, waiting for the in-flight mutation on this socket to finish. Each
439
+ * `handleCall` for a mutation owns one of these and awaits its own
440
+ * resolve/reject so it can send the response.
441
+ */
442
+ interface PendingMutation<TContext = Record<string, unknown>> {
443
+ procedure: RegisteredProcedure<TContext>;
444
+ validatedInput: unknown;
445
+ resolve: (result: MutationResult<unknown>) => void;
446
+ reject: (err: unknown) => void;
447
+ }
448
+ /** WebSocket session (runtime, with WebSocket) */
449
+ interface Session<TContext = Record<string, unknown>> {
450
+ id: string;
451
+ ws: WebSocket;
452
+ /** Auth context from verifyAuth callback */
453
+ serverCtx?: TContext;
454
+ subIds: Set<string>;
455
+ /**
456
+ * Per-socket FIFO mutation queue. at most one mutation
457
+ * drains at a time so a single client cannot saturate the OCC retry
458
+ * loop or fan out concurrent commits that race the same rows.
459
+ */
460
+ mutationQueue: PendingMutation<TContext>[];
461
+ mutationInFlight: boolean;
462
+ /** Per-socket count of currently executing actions. */
463
+ actionInFlight: number;
464
+ /**
465
+ * Wall-clock of the last inbound frame (text message, ws-ping, or ws-pong).
466
+ * Used by the per-session heartbeat to terminate sockets that have gone
467
+ * silent past `CLIENT_TIMEOUT_MS`.
468
+ */
469
+ lastReceivedAt: number;
470
+ /**
471
+ * Wall-clock of the last outbound frame. Gates the idle-only app-level
472
+ * ping so a busy session (frequent sub:updates etc.) doesn't emit a
473
+ * redundant `{type:"ping"}` on every heartbeat tick.
474
+ */
475
+ lastSentAt: number;
476
+ /**
477
+ * Per-session combined heartbeat. Single `setInterval` that handles
478
+ * both dead-TCP detection (ws-ping or terminate) and client-watchdog
479
+ * refresh (idle-only app-level ping) on each tick.
480
+ */
481
+ heartbeatTimer: ReturnType<typeof setInterval> | null;
482
+ }
483
+ declare const RemoteSubscriptionUpdateSchema: z.ZodObject<{
484
+ type: z.ZodLiteral<"update">;
485
+ subId: z.ZodString;
486
+ data: z.ZodUnknown;
487
+ dataHash: z.ZodString;
488
+ originInstance: z.ZodString;
489
+ }, z.core.$strip>;
490
+ type RemoteSubscriptionUpdate = z.infer<typeof RemoteSubscriptionUpdateSchema>;
491
+ declare const RemoteRecomputeRaceSchema: z.ZodObject<{
492
+ type: z.ZodLiteral<"recompute-race">;
493
+ subId: z.ZodString;
494
+ cacheKey: z.ZodString;
495
+ queryName: z.ZodString;
496
+ args: z.ZodUnknown;
497
+ notifyInstances: z.ZodArray<z.ZodString>;
498
+ commitTs: z.ZodString;
499
+ originInstance: z.ZodString;
500
+ }, z.core.$strip>;
501
+ type RemoteRecomputeRace = z.infer<typeof RemoteRecomputeRaceSchema>;
502
+ declare const RemoteSubscriptionMessageSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
503
+ type: z.ZodLiteral<"update">;
504
+ subId: z.ZodString;
505
+ data: z.ZodUnknown;
506
+ dataHash: z.ZodString;
507
+ originInstance: z.ZodString;
508
+ }, z.core.$strip>, z.ZodObject<{
509
+ type: z.ZodLiteral<"recompute-race">;
510
+ subId: z.ZodString;
511
+ cacheKey: z.ZodString;
512
+ queryName: z.ZodString;
513
+ args: z.ZodUnknown;
514
+ notifyInstances: z.ZodArray<z.ZodString>;
515
+ commitTs: z.ZodString;
516
+ originInstance: z.ZodString;
517
+ }, z.core.$strip>], "type">;
518
+ type RemoteSubscriptionMessage = z.infer<typeof RemoteSubscriptionMessageSchema>;
519
+ //#endregion
520
+ export { SubManagerLink as C, SubManagerClient as S, 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 };
521
+ //# sourceMappingURL=types_server-AQ8Jl2_R.d.ts.map