@supalive/core 1.12.0 → 1.13.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 (203) hide show
  1. package/dist/db-ClCicssQ.js.map +1 -1
  2. package/dist/helper-zdJT5FUc.js +58 -0
  3. package/dist/helper-zdJT5FUc.js.map +1 -0
  4. package/dist/index-BIIcgvu8.d.ts +2300 -0
  5. package/dist/index-BIIcgvu8.d.ts.map +1 -0
  6. package/dist/index-BedlCLem.d.ts +2292 -0
  7. package/dist/index-BedlCLem.d.ts.map +1 -0
  8. package/dist/index-BnV8LnOf.d.ts +2286 -0
  9. package/dist/index-BnV8LnOf.d.ts.map +1 -0
  10. package/dist/index-BziK63TZ.d.ts +2271 -0
  11. package/dist/index-BziK63TZ.d.ts.map +1 -0
  12. package/dist/index-D31hMBDE.d.ts +2288 -0
  13. package/dist/index-D31hMBDE.d.ts.map +1 -0
  14. package/dist/index-DZhReQWB.d.ts +2288 -0
  15. package/dist/index-DZhReQWB.d.ts.map +1 -0
  16. package/dist/index-DlG8HDCO.d.ts +2215 -0
  17. package/dist/index-DlG8HDCO.d.ts.map +1 -0
  18. package/dist/index-Wgi_dGzj.d.ts +2215 -0
  19. package/dist/index-Wgi_dGzj.d.ts.map +1 -0
  20. package/dist/index-j3YTMNlF.d.ts +2223 -0
  21. package/dist/index-j3YTMNlF.d.ts.map +1 -0
  22. package/dist/mysql-BBbilD4I.js +614 -0
  23. package/dist/mysql-BBbilD4I.js.map +1 -0
  24. package/dist/mysql-BHJ2mrwZ.d.ts +113 -0
  25. package/dist/mysql-BHJ2mrwZ.d.ts.map +1 -0
  26. package/dist/mysql-BQbHJ_rK.js +617 -0
  27. package/dist/mysql-BQbHJ_rK.js.map +1 -0
  28. package/dist/mysql-BXoAON7c.d.ts +113 -0
  29. package/dist/mysql-BXoAON7c.d.ts.map +1 -0
  30. package/dist/mysql-BgfCUYGt.d.ts +113 -0
  31. package/dist/mysql-BgfCUYGt.d.ts.map +1 -0
  32. package/dist/mysql-Br7MqUXg.js +620 -0
  33. package/dist/mysql-Br7MqUXg.js.map +1 -0
  34. package/dist/mysql-BrueWtO9.d.ts +113 -0
  35. package/dist/mysql-BrueWtO9.d.ts.map +1 -0
  36. package/dist/mysql-CCo--z6n.d.ts +113 -0
  37. package/dist/mysql-CCo--z6n.d.ts.map +1 -0
  38. package/dist/mysql-CQcbcZB8.d.ts +113 -0
  39. package/dist/mysql-CQcbcZB8.d.ts.map +1 -0
  40. package/dist/mysql-CbgCY89y.d.ts +113 -0
  41. package/dist/mysql-CbgCY89y.d.ts.map +1 -0
  42. package/dist/mysql-CqD1ZnNY.js +619 -0
  43. package/dist/mysql-CqD1ZnNY.js.map +1 -0
  44. package/dist/mysql-D3Db2q3_.js +619 -0
  45. package/dist/mysql-D3Db2q3_.js.map +1 -0
  46. package/dist/mysql-DBrhTmYm.js +619 -0
  47. package/dist/mysql-DBrhTmYm.js.map +1 -0
  48. package/dist/mysql-DX7YA7IS.d.ts +113 -0
  49. package/dist/mysql-DX7YA7IS.d.ts.map +1 -0
  50. package/dist/mysql-DZ0_Rsjc.js +614 -0
  51. package/dist/mysql-DZ0_Rsjc.js.map +1 -0
  52. package/dist/mysql-Dko1MJqQ.js +619 -0
  53. package/dist/mysql-Dko1MJqQ.js.map +1 -0
  54. package/dist/mysql-F831ycnw.d.ts +113 -0
  55. package/dist/mysql-F831ycnw.d.ts.map +1 -0
  56. package/dist/mysql-TLPG62tv.js +618 -0
  57. package/dist/mysql-TLPG62tv.js.map +1 -0
  58. package/dist/mysql-e-ii74yG.js +613 -0
  59. package/dist/mysql-e-ii74yG.js.map +1 -0
  60. package/dist/overlap-checker-BBkqRdcs.js +259 -0
  61. package/dist/overlap-checker-BBkqRdcs.js.map +1 -0
  62. package/dist/overlap-checker-DHUJe-Yy.js +259 -0
  63. package/dist/overlap-checker-DHUJe-Yy.js.map +1 -0
  64. package/dist/overlap-checker-DWeQLpSD.js +259 -0
  65. package/dist/overlap-checker-DWeQLpSD.js.map +1 -0
  66. package/dist/overlap-checker-Kz7B_jsp.js +259 -0
  67. package/dist/overlap-checker-Kz7B_jsp.js.map +1 -0
  68. package/dist/postgres-7hJuQX4Y.d.ts +117 -0
  69. package/dist/postgres-7hJuQX4Y.d.ts.map +1 -0
  70. package/dist/postgres-B4H78IkW.js +864 -0
  71. package/dist/postgres-B4H78IkW.js.map +1 -0
  72. package/dist/postgres-B4h8D5U2.d.ts +117 -0
  73. package/dist/postgres-B4h8D5U2.d.ts.map +1 -0
  74. package/dist/postgres-BXFPamkP.js +864 -0
  75. package/dist/postgres-BXFPamkP.js.map +1 -0
  76. package/dist/postgres-CoR3TUMd.js +865 -0
  77. package/dist/postgres-CoR3TUMd.js.map +1 -0
  78. package/dist/postgres-D-RsG12a.d.ts +117 -0
  79. package/dist/postgres-D-RsG12a.d.ts.map +1 -0
  80. package/dist/postgres-D5Z1S3J9.d.ts +117 -0
  81. package/dist/postgres-D5Z1S3J9.d.ts.map +1 -0
  82. package/dist/postgres-DHs89iWT.js +864 -0
  83. package/dist/postgres-DHs89iWT.js.map +1 -0
  84. package/dist/postgres-DvBIRnYA.d.ts +117 -0
  85. package/dist/postgres-DvBIRnYA.d.ts.map +1 -0
  86. package/dist/postgres-Eo-lpUiX.d.ts +117 -0
  87. package/dist/postgres-Eo-lpUiX.d.ts.map +1 -0
  88. package/dist/postgres-VLAGIFLR.d.ts +117 -0
  89. package/dist/postgres-VLAGIFLR.d.ts.map +1 -0
  90. package/dist/postgres-hrQmDPUp.d.ts +117 -0
  91. package/dist/postgres-hrQmDPUp.d.ts.map +1 -0
  92. package/dist/postgres-pXrP9Fx0.d.ts +117 -0
  93. package/dist/postgres-pXrP9Fx0.d.ts.map +1 -0
  94. package/dist/query-BuOs9w-U.js +730 -0
  95. package/dist/query-BuOs9w-U.js.map +1 -0
  96. package/dist/query-Xw2hi6TB.js +730 -0
  97. package/dist/query-Xw2hi6TB.js.map +1 -0
  98. package/dist/read-routing-BUcQAwnd.js +62 -0
  99. package/dist/read-routing-BUcQAwnd.js.map +1 -0
  100. package/dist/read-routing-BXflWzQI.js +61 -0
  101. package/dist/read-routing-BXflWzQI.js.map +1 -0
  102. package/dist/read-routing-CcsLp0ow.js +61 -0
  103. package/dist/read-routing-CcsLp0ow.js.map +1 -0
  104. package/dist/realtime_db-B70vqtRZ.js +272 -0
  105. package/dist/realtime_db-B70vqtRZ.js.map +1 -0
  106. package/dist/realtime_db-BMysrWym.js +312 -0
  107. package/dist/realtime_db-BMysrWym.js.map +1 -0
  108. package/dist/realtime_db-Bi7S938D.js +312 -0
  109. package/dist/realtime_db-Bi7S938D.js.map +1 -0
  110. package/dist/realtime_db-CVze3gM0.js +312 -0
  111. package/dist/realtime_db-CVze3gM0.js.map +1 -0
  112. package/dist/realtime_db-DMipkUmD.js +312 -0
  113. package/dist/realtime_db-DMipkUmD.js.map +1 -0
  114. package/dist/realtime_db-XWzuq2JS.js +272 -0
  115. package/dist/realtime_db-XWzuq2JS.js.map +1 -0
  116. package/dist/router-Kgrlci8X.js +337 -0
  117. package/dist/router-Kgrlci8X.js.map +1 -0
  118. package/dist/router-g8qSQb8m.js +314 -0
  119. package/dist/router-g8qSQb8m.js.map +1 -0
  120. package/dist/schema-DLue3K2s.js +134 -0
  121. package/dist/schema-DLue3K2s.js.map +1 -0
  122. package/dist/schema-DfowI0ip.js +134 -0
  123. package/dist/schema-DfowI0ip.js.map +1 -0
  124. package/dist/schema-fKldWzwc.js +134 -0
  125. package/dist/schema-fKldWzwc.js.map +1 -0
  126. package/dist/src/client/index.d.ts +1 -1
  127. package/dist/src/client/index.js +69 -29
  128. package/dist/src/client/index.js.map +1 -1
  129. package/dist/src/exports/mysql.d.ts +1 -1
  130. package/dist/src/exports/mysql.js +1 -1
  131. package/dist/src/exports/postgres.d.ts +1 -1
  132. package/dist/src/exports/postgres.js +1 -1
  133. package/dist/src/exports/procedure.d.ts +1 -1
  134. package/dist/src/exports/procedure.js +5 -6
  135. package/dist/src/exports/procedure.js.map +1 -1
  136. package/dist/src/exports/schema-sql.d.ts +18 -1
  137. package/dist/src/exports/schema-sql.d.ts.map +1 -1
  138. package/dist/src/exports/schema-sql.js +91 -15
  139. package/dist/src/exports/schema-sql.js.map +1 -1
  140. package/dist/src/exports/server.d.ts +11 -4
  141. package/dist/src/exports/server.d.ts.map +1 -1
  142. package/dist/src/exports/server.js +136 -59
  143. package/dist/src/exports/server.js.map +1 -1
  144. package/dist/src/exports/sub-manager-worker-entry.js +1 -1
  145. package/dist/src/exports/types.d.ts +2 -2
  146. package/dist/src/exports/types.js +4 -4
  147. package/dist/sub-worker-dispatch-CO9nVdSG.js +813 -0
  148. package/dist/sub-worker-dispatch-CO9nVdSG.js.map +1 -0
  149. package/dist/sub-worker-dispatch-CQBknEL7.js +813 -0
  150. package/dist/sub-worker-dispatch-CQBknEL7.js.map +1 -0
  151. package/dist/sub-worker-dispatch-CQHeit6D.js +813 -0
  152. package/dist/sub-worker-dispatch-CQHeit6D.js.map +1 -0
  153. package/dist/sub-worker-dispatch-CmEW10it.js +813 -0
  154. package/dist/sub-worker-dispatch-CmEW10it.js.map +1 -0
  155. package/dist/sub-worker-dispatch-CunxuOPT.js +823 -0
  156. package/dist/sub-worker-dispatch-CunxuOPT.js.map +1 -0
  157. package/dist/sub-worker-dispatch-CvvFm40r.js +817 -0
  158. package/dist/sub-worker-dispatch-CvvFm40r.js.map +1 -0
  159. package/dist/sub-worker-dispatch-D15UHo1g.js +813 -0
  160. package/dist/sub-worker-dispatch-D15UHo1g.js.map +1 -0
  161. package/dist/sub-worker-dispatch-D8DY56ry.js +817 -0
  162. package/dist/sub-worker-dispatch-D8DY56ry.js.map +1 -0
  163. package/dist/sub-worker-dispatch-DJgWxtSQ.js +823 -0
  164. package/dist/sub-worker-dispatch-DJgWxtSQ.js.map +1 -0
  165. package/dist/sub-worker-dispatch-Dplqeubd.js +813 -0
  166. package/dist/sub-worker-dispatch-Dplqeubd.js.map +1 -0
  167. package/dist/sub-worker-dispatch-dXjUmSMN.js +813 -0
  168. package/dist/sub-worker-dispatch-dXjUmSMN.js.map +1 -0
  169. package/dist/table-id-BJdmctEQ.js +91 -0
  170. package/dist/table-id-BJdmctEQ.js.map +1 -0
  171. package/dist/table-id-DyrJBKAQ.js +105 -0
  172. package/dist/table-id-DyrJBKAQ.js.map +1 -0
  173. package/dist/types_client_rpc-BGMwPJst.js +123 -0
  174. package/dist/types_client_rpc-BGMwPJst.js.map +1 -0
  175. package/dist/types_client_rpc-ByGwoRCL.js +132 -0
  176. package/dist/types_client_rpc-ByGwoRCL.js.map +1 -0
  177. package/dist/types_db-BsR02Sph.js +171 -0
  178. package/dist/types_db-BsR02Sph.js.map +1 -0
  179. package/dist/types_db-CgGlOMmr.js +187 -0
  180. package/dist/types_db-CgGlOMmr.js.map +1 -0
  181. package/dist/types_db-OUou3o2Z.js +172 -0
  182. package/dist/types_db-OUou3o2Z.js.map +1 -0
  183. package/dist/types_server-B3z8hi-Z.d.ts +523 -0
  184. package/dist/types_server-B3z8hi-Z.d.ts.map +1 -0
  185. package/dist/types_server-BXBvz4T8.d.ts +523 -0
  186. package/dist/types_server-BXBvz4T8.d.ts.map +1 -0
  187. package/dist/types_server-BhC19R2a.js +27 -0
  188. package/dist/types_server-BhC19R2a.js.map +1 -0
  189. package/dist/types_server-CClUPZn1.d.ts +521 -0
  190. package/dist/types_server-CClUPZn1.d.ts.map +1 -0
  191. package/dist/types_server-CnYTSPhM.d.ts +523 -0
  192. package/dist/types_server-CnYTSPhM.d.ts.map +1 -0
  193. package/dist/types_server-DHpGUHB_.d.ts +521 -0
  194. package/dist/types_server-DHpGUHB_.d.ts.map +1 -0
  195. package/dist/types_server-Ded1u3bL.d.ts +521 -0
  196. package/dist/types_server-Ded1u3bL.d.ts.map +1 -0
  197. package/dist/types_server-fons7nmo.d.ts +523 -0
  198. package/dist/types_server-fons7nmo.d.ts.map +1 -0
  199. package/dist/types_server-okvBG4v_.d.ts +521 -0
  200. package/dist/types_server-okvBG4v_.d.ts.map +1 -0
  201. package/dist/types_server-pD8DiI7s.d.ts +521 -0
  202. package/dist/types_server-pD8DiI7s.d.ts.map +1 -0
  203. package/package.json +1 -1
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sub-worker-dispatch-dXjUmSMN.js","names":["uuidv4","logger","baseLogger","logger","Redis","baseLogger"],"sources":["../src/logger.ts","../src/server/retention-watermark.ts","../src/server/sub-manager-types.ts","../src/db/cache.ts","../src/db/init_db.ts","../src/server/sub-worker.ts","../src/server/sub-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/sub-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 // mysql2 parses a DSN from `uri` (NOT `connectionString`), and warns\n // on any unknown option — so map explicitly rather than spreading the\n // framework config, whose keys mysql2 doesn't recognize.\n uri: config.connectionString,\n supportBigNumbers: true,\n connectionLimit: config.maxConnections ?? 20,\n connectTimeout: config.connectionTimeoutMillis ?? 15_000,\n idleTimeout: config.idleTimeoutMillis ?? 30_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 // Store and return DATETIME values in UTC so timestamps behave like\n // Postgres TIMESTAMPTZ — the app works entirely in UTC Date objects\n // and the driver never applies a local-time offset.\n timezone: \"Z\",\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 \"./sub-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 \"./sub-manager-types\";\nimport type { SubscriptionWorker } from \"./sub-worker\";\nimport { MsgType } from \"./sub-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,91 @@
1
+ import { r as utf8Encode } from "./bytes-AuEkyxeU.js";
2
+ import z from "zod";
3
+ //#region src/db/codec.ts
4
+ const commitTsCodec = {
5
+ encode: (value) => value,
6
+ decode: (value) => {
7
+ if (typeof value === "string") return BigInt(value);
8
+ switch (typeof value) {
9
+ case "string":
10
+ case "number":
11
+ case "boolean":
12
+ case "bigint": return BigInt(value);
13
+ default: throw new Error(`Unsupported commitTs type: ${typeof value}`);
14
+ }
15
+ }
16
+ };
17
+ const identityCodec = {
18
+ encode: (value) => value,
19
+ decode: (value) => value
20
+ };
21
+ const numberCodec = {
22
+ encode: (value) => value,
23
+ decode: (value) => typeof value === "string" ? Number(value) : value
24
+ };
25
+ const booleanCodec = {
26
+ encode: (value) => value,
27
+ decode: (value) => {
28
+ if (typeof value === "boolean") return value;
29
+ if (typeof value === "number") return value !== 0;
30
+ if (typeof value === "string") {
31
+ const normalized = value.toLowerCase();
32
+ return normalized === "t" || normalized === "1" || normalized === "true";
33
+ }
34
+ return Boolean(value);
35
+ }
36
+ };
37
+ const dateCodec = {
38
+ encode: (value) => value,
39
+ decode: (value) => value instanceof Date ? value : new Date(value)
40
+ };
41
+ function unwrapZodType(field) {
42
+ let current = field;
43
+ while (current instanceof z.ZodOptional || current instanceof z.ZodNullable || current instanceof z.ZodDefault || current instanceof z.ZodCatch || current instanceof z.ZodReadonly) current = current.def.innerType;
44
+ return current;
45
+ }
46
+ function getDefaultCodec(field) {
47
+ if (field instanceof z.ZodNumber) return numberCodec;
48
+ if (field instanceof z.ZodBoolean) return booleanCodec;
49
+ if (field instanceof z.ZodDate) return dateCodec;
50
+ return identityCodec;
51
+ }
52
+ function normalizeCodec(codec, zodField) {
53
+ const fallback = getDefaultCodec(zodField);
54
+ if (!codec) return fallback;
55
+ return {
56
+ encode: codec.encode ?? fallback.encode,
57
+ decode: codec.decode ?? fallback.decode
58
+ };
59
+ }
60
+ //#endregion
61
+ //#region src/db/table-id.ts
62
+ /**
63
+ * Canonical table identifier: the UTF-8 bytes of the table name. A `tableId` is
64
+ * fully derived from the table name, so it is never stored redundantly — e.g.
65
+ * the query-cache `readSet` persists only `table` (the name) and reconstructs
66
+ * the bytes on read via {@link tableIdOf}. The physical `commit_logs.table_id`
67
+ * column and its indexes hold these same bytes.
68
+ */
69
+ function tableNameToId(tableName) {
70
+ return utf8Encode(tableName);
71
+ }
72
+ const tableIdCache = /* @__PURE__ */ new Map();
73
+ /**
74
+ * Memoized {@link tableNameToId}: encodes each distinct table name once and
75
+ * returns the same interned byte array on every later call. Rebuilding a raw
76
+ * read set happens on the freshness hot path and the commit-time OCC scan, so
77
+ * this avoids repeated UTF-8 encoding and per-entry allocation. The returned
78
+ * bytes are treated as immutable and must never be mutated in place.
79
+ */
80
+ function tableIdOf(name) {
81
+ let id = tableIdCache.get(name);
82
+ if (id === void 0) {
83
+ id = tableNameToId(name);
84
+ tableIdCache.set(name, id);
85
+ }
86
+ return id;
87
+ }
88
+ //#endregion
89
+ export { unwrapZodType as a, normalizeCodec as i, tableNameToId as n, commitTsCodec as r, tableIdOf as t };
90
+
91
+ //# sourceMappingURL=table-id-BJdmctEQ.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"table-id-BJdmctEQ.js","names":[],"sources":["../src/db/codec.ts","../src/db/table-id.ts"],"sourcesContent":["import z from \"zod\";\nimport { ColumnCodec } from \"./schema\";\n\nexport const commitTsCodec: ColumnCodec<bigint, unknown> = {\n encode: (value: bigint) => value,\n decode: (value: unknown) => {\n if (typeof value === \"string\") return BigInt(value);\n switch (typeof value) {\n case \"string\":\n case \"number\":\n case \"boolean\":\n case \"bigint\":\n return BigInt(value);\n default:\n throw new Error(`Unsupported commitTs type: ${typeof value}`);\n }\n },\n};\n\nexport const identityCodec: ColumnCodec<unknown, unknown> = {\n encode: (value) => value,\n decode: (value) => value,\n};\n\nexport const numberCodec: ColumnCodec<Number, unknown> = {\n encode: (value) => value,\n decode: (value) => (typeof value === \"string\" ? Number(value) : value as Number),\n};\n\nexport const booleanCodec: ColumnCodec<unknown, unknown> = {\n encode: (value) => value,\n decode: (value) => {\n if (typeof value === \"boolean\") return value;\n if (typeof value === \"number\") return value !== 0;\n if (typeof value === \"string\") {\n const normalized = value.toLowerCase();\n return normalized === \"t\" || normalized === \"1\" || normalized === \"true\";\n }\n return Boolean(value);\n },\n};\n\nexport const dateCodec: ColumnCodec<Date, unknown> = {\n encode: (value) => value,\n decode: (value) => (value instanceof Date ? value : new Date(value as string)),\n};\n\nexport function unwrapZodType(field: unknown): unknown {\n let current: unknown = field;\n while (\n current instanceof z.ZodOptional ||\n current instanceof z.ZodNullable ||\n current instanceof z.ZodDefault ||\n current instanceof z.ZodCatch ||\n current instanceof z.ZodReadonly\n ) {\n current = current.def.innerType;\n }\n return current;\n}\n\nexport function getDefaultCodec(field: unknown): ColumnCodec<any, unknown> {\n if (field instanceof z.ZodNumber) return numberCodec;\n if (field instanceof z.ZodBoolean) return booleanCodec;\n if (field instanceof z.ZodDate) return dateCodec;\n return identityCodec;\n}\n\nexport function normalizeCodec(\n codec: ColumnCodec<any, any> | undefined,\n zodField?: unknown\n): ColumnCodec<any, any> {\n const fallback = getDefaultCodec(zodField);\n if (!codec) return fallback;\n return {\n encode: codec.encode ?? fallback.encode,\n decode: codec.decode ?? fallback.decode,\n };\n}\n","import { utf8Encode } from \"./bytes\";\n\n/**\n * Canonical table identifier: the UTF-8 bytes of the table name. A `tableId` is\n * fully derived from the table name, so it is never stored redundantly — e.g.\n * the query-cache `readSet` persists only `table` (the name) and reconstructs\n * the bytes on read via {@link tableIdOf}. The physical `commit_logs.table_id`\n * column and its indexes hold these same bytes.\n */\nexport function tableNameToId(tableName: string): Uint8Array {\n return utf8Encode(tableName);\n}\n\nconst tableIdCache = new Map<string, Uint8Array>();\n\n/**\n * Memoized {@link tableNameToId}: encodes each distinct table name once and\n * returns the same interned byte array on every later call. Rebuilding a raw\n * read set happens on the freshness hot path and the commit-time OCC scan, so\n * this avoids repeated UTF-8 encoding and per-entry allocation. The returned\n * bytes are treated as immutable and must never be mutated in place.\n */\nexport function tableIdOf(name: string): Uint8Array {\n let id = tableIdCache.get(name);\n if (id === undefined) {\n id = tableNameToId(name);\n tableIdCache.set(name, id);\n }\n return id;\n}\n"],"mappings":";;;AAGA,MAAa,gBAA8C;CACvD,SAAS,UAAkB;CAC3B,SAAS,UAAmB;EACxB,IAAI,OAAO,UAAU,UAAU,OAAO,OAAO,KAAK;EAClD,QAAQ,OAAO,OAAf;GACI,KAAK;GACL,KAAK;GACL,KAAK;GACL,KAAK,UACD,OAAO,OAAO,KAAK;GACvB,SACI,MAAM,IAAI,MAAM,8BAA8B,OAAO,OAAO;EACpE;CACJ;AACJ;AAEA,MAAa,gBAA+C;CACxD,SAAS,UAAU;CACnB,SAAS,UAAU;AACvB;AAEA,MAAa,cAA4C;CACrD,SAAS,UAAU;CACnB,SAAS,UAAW,OAAO,UAAU,WAAW,OAAO,KAAK,IAAI;AACpE;AAEA,MAAa,eAA8C;CACvD,SAAS,UAAU;CACnB,SAAS,UAAU;EACf,IAAI,OAAO,UAAU,WAAW,OAAO;EACvC,IAAI,OAAO,UAAU,UAAU,OAAO,UAAU;EAChD,IAAI,OAAO,UAAU,UAAU;GAC3B,MAAM,aAAa,MAAM,YAAY;GACrC,OAAO,eAAe,OAAO,eAAe,OAAO,eAAe;EACtE;EACA,OAAO,QAAQ,KAAK;CACxB;AACJ;AAEA,MAAa,YAAwC;CACjD,SAAS,UAAU;CACnB,SAAS,UAAW,iBAAiB,OAAO,QAAQ,IAAI,KAAK,KAAe;AAChF;AAEA,SAAgB,cAAc,OAAyB;CACnD,IAAI,UAAmB;CACvB,OACI,mBAAmB,EAAE,eACrB,mBAAmB,EAAE,eACrB,mBAAmB,EAAE,cACrB,mBAAmB,EAAE,YACrB,mBAAmB,EAAE,aAErB,UAAU,QAAQ,IAAI;CAE1B,OAAO;AACX;AAEA,SAAgB,gBAAgB,OAA2C;CACvE,IAAI,iBAAiB,EAAE,WAAW,OAAO;CACzC,IAAI,iBAAiB,EAAE,YAAY,OAAO;CAC1C,IAAI,iBAAiB,EAAE,SAAS,OAAO;CACvC,OAAO;AACX;AAEA,SAAgB,eACZ,OACA,UACqB;CACrB,MAAM,WAAW,gBAAgB,QAAQ;CACzC,IAAI,CAAC,OAAO,OAAO;CACnB,OAAO;EACH,QAAQ,MAAM,UAAU,SAAS;EACjC,QAAQ,MAAM,UAAU,SAAS;CACrC;AACJ;;;;;;;;;;ACrEA,SAAgB,cAAc,WAA+B;CAC3D,OAAO,WAAW,SAAS;AAC7B;AAEA,MAAM,+BAAe,IAAI,IAAwB;;;;;;;;AASjD,SAAgB,UAAU,MAA0B;CAClD,IAAI,KAAK,aAAa,IAAI,IAAI;CAC9B,IAAI,OAAO,KAAA,GAAW;EACpB,KAAK,cAAc,IAAI;EACvB,aAAa,IAAI,MAAM,EAAE;CAC3B;CACA,OAAO;AACT"}
@@ -0,0 +1,105 @@
1
+ import z from "zod";
2
+ import { parse, v3 } from "uuid";
3
+ //#region src/db/codec.ts
4
+ const commitTsCodec = {
5
+ encode: (value) => value,
6
+ decode: (value) => {
7
+ if (typeof value === "string") return BigInt(value);
8
+ switch (typeof value) {
9
+ case "string":
10
+ case "number":
11
+ case "boolean":
12
+ case "bigint": return BigInt(value);
13
+ default: throw new Error(`Unsupported commitTs type: ${typeof value}`);
14
+ }
15
+ }
16
+ };
17
+ const identityCodec = {
18
+ encode: (value) => value,
19
+ decode: (value) => value
20
+ };
21
+ const numberCodec = {
22
+ encode: (value) => value,
23
+ decode: (value) => typeof value === "string" ? Number(value) : value
24
+ };
25
+ const booleanCodec = {
26
+ encode: (value) => value,
27
+ decode: (value) => {
28
+ if (typeof value === "boolean") return value;
29
+ if (typeof value === "number") return value !== 0;
30
+ if (typeof value === "string") {
31
+ const normalized = value.toLowerCase();
32
+ return normalized === "t" || normalized === "1" || normalized === "true";
33
+ }
34
+ return Boolean(value);
35
+ }
36
+ };
37
+ const dateCodec = {
38
+ encode: (value) => value,
39
+ decode: (value) => value instanceof Date ? value : new Date(value)
40
+ };
41
+ function unwrapZodType(field) {
42
+ let current = field;
43
+ while (current instanceof z.ZodOptional || current instanceof z.ZodNullable || current instanceof z.ZodDefault || current instanceof z.ZodCatch || current instanceof z.ZodReadonly) current = current.def.innerType;
44
+ return current;
45
+ }
46
+ function getDefaultCodec(field) {
47
+ if (field instanceof z.ZodNumber) return numberCodec;
48
+ if (field instanceof z.ZodBoolean) return booleanCodec;
49
+ if (field instanceof z.ZodDate) return dateCodec;
50
+ return identityCodec;
51
+ }
52
+ function normalizeCodec(codec, zodField) {
53
+ const fallback = getDefaultCodec(zodField);
54
+ if (!codec) return fallback;
55
+ return {
56
+ encode: codec.encode ?? fallback.encode,
57
+ decode: codec.decode ?? fallback.decode
58
+ };
59
+ }
60
+ //#endregion
61
+ //#region src/db/table-id.ts
62
+ /**
63
+ * Fixed namespace for deriving a stable table id from a table name. Any constant
64
+ * UUID works; this one is arbitrary and must never change (it would repartition
65
+ * every `commit_logs.table_id`).
66
+ */
67
+ const TABLE_ID_NAMESPACE = "1b671a64-40d5-491e-99b0-da01ff1f3341";
68
+ /**
69
+ * Canonical table identifier: a fixed **16-byte** digest of the table name
70
+ * (UUID v3, which is MD5-based). A `tableId` is fully derived from the table
71
+ * name, so it is never stored redundantly — the query-cache `readSet` persists
72
+ * only `table` (the name) and reconstructs the bytes on read via
73
+ * {@link tableIdOf}. The physical `commit_logs.table_id` column (MySQL
74
+ * `BINARY(16)`, Postgres `BYTEA`) and its indexes hold these same bytes.
75
+ *
76
+ * A fixed 16 bytes (rather than the raw UTF-8 of the name) is required for
77
+ * MySQL: `BINARY(16)` would overflow on a table name longer than 16 bytes and
78
+ * zero-pad shorter ones (breaking byte-exact id matching). MD5 sidesteps both.
79
+ * Only used for equality (never decoded back to a name), so a non-cryptographic
80
+ * digest is fine. Browser-safe and synchronous (via the `uuid` package), which
81
+ * matters because this module is reachable from the client bundle.
82
+ */
83
+ function tableNameToId(tableName) {
84
+ return parse(v3(tableName, TABLE_ID_NAMESPACE));
85
+ }
86
+ const tableIdCache = /* @__PURE__ */ new Map();
87
+ /**
88
+ * Memoized {@link tableNameToId}: hashes each distinct table name once and
89
+ * returns the same interned byte array on every later call. Rebuilding a raw
90
+ * read set happens on the freshness hot path and the commit-time OCC scan, so
91
+ * this avoids repeated hashing and per-entry allocation. The returned bytes are
92
+ * treated as immutable and must never be mutated in place.
93
+ */
94
+ function tableIdOf(name) {
95
+ let id = tableIdCache.get(name);
96
+ if (id === void 0) {
97
+ id = tableNameToId(name);
98
+ tableIdCache.set(name, id);
99
+ }
100
+ return id;
101
+ }
102
+ //#endregion
103
+ export { unwrapZodType as a, normalizeCodec as i, tableNameToId as n, commitTsCodec as r, tableIdOf as t };
104
+
105
+ //# sourceMappingURL=table-id-DyrJBKAQ.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"table-id-DyrJBKAQ.js","names":["uuidParse","uuidv3"],"sources":["../src/db/codec.ts","../src/db/table-id.ts"],"sourcesContent":["import z from \"zod\";\nimport { ColumnCodec } from \"./schema\";\n\nexport const commitTsCodec: ColumnCodec<bigint, unknown> = {\n encode: (value: bigint) => value,\n decode: (value: unknown) => {\n if (typeof value === \"string\") return BigInt(value);\n switch (typeof value) {\n case \"string\":\n case \"number\":\n case \"boolean\":\n case \"bigint\":\n return BigInt(value);\n default:\n throw new Error(`Unsupported commitTs type: ${typeof value}`);\n }\n },\n};\n\nexport const identityCodec: ColumnCodec<unknown, unknown> = {\n encode: (value) => value,\n decode: (value) => value,\n};\n\nexport const numberCodec: ColumnCodec<Number, unknown> = {\n encode: (value) => value,\n decode: (value) => (typeof value === \"string\" ? Number(value) : value as Number),\n};\n\nexport const booleanCodec: ColumnCodec<unknown, unknown> = {\n encode: (value) => value,\n decode: (value) => {\n if (typeof value === \"boolean\") return value;\n if (typeof value === \"number\") return value !== 0;\n if (typeof value === \"string\") {\n const normalized = value.toLowerCase();\n return normalized === \"t\" || normalized === \"1\" || normalized === \"true\";\n }\n return Boolean(value);\n },\n};\n\nexport const dateCodec: ColumnCodec<Date, unknown> = {\n encode: (value) => value,\n decode: (value) => (value instanceof Date ? value : new Date(value as string)),\n};\n\nexport function unwrapZodType(field: unknown): unknown {\n let current: unknown = field;\n while (\n current instanceof z.ZodOptional ||\n current instanceof z.ZodNullable ||\n current instanceof z.ZodDefault ||\n current instanceof z.ZodCatch ||\n current instanceof z.ZodReadonly\n ) {\n current = current.def.innerType;\n }\n return current;\n}\n\nexport function getDefaultCodec(field: unknown): ColumnCodec<any, unknown> {\n if (field instanceof z.ZodNumber) return numberCodec;\n if (field instanceof z.ZodBoolean) return booleanCodec;\n if (field instanceof z.ZodDate) return dateCodec;\n return identityCodec;\n}\n\nexport function normalizeCodec(\n codec: ColumnCodec<any, any> | undefined,\n zodField?: unknown\n): ColumnCodec<any, any> {\n const fallback = getDefaultCodec(zodField);\n if (!codec) return fallback;\n return {\n encode: codec.encode ?? fallback.encode,\n decode: codec.decode ?? fallback.decode,\n };\n}\n","import { v3 as uuidv3, parse as uuidParse } from \"uuid\";\n\n/**\n * Fixed namespace for deriving a stable table id from a table name. Any constant\n * UUID works; this one is arbitrary and must never change (it would repartition\n * every `commit_logs.table_id`).\n */\nconst TABLE_ID_NAMESPACE = \"1b671a64-40d5-491e-99b0-da01ff1f3341\";\n\n/**\n * Canonical table identifier: a fixed **16-byte** digest of the table name\n * (UUID v3, which is MD5-based). A `tableId` is fully derived from the table\n * name, so it is never stored redundantly — the query-cache `readSet` persists\n * only `table` (the name) and reconstructs the bytes on read via\n * {@link tableIdOf}. The physical `commit_logs.table_id` column (MySQL\n * `BINARY(16)`, Postgres `BYTEA`) and its indexes hold these same bytes.\n *\n * A fixed 16 bytes (rather than the raw UTF-8 of the name) is required for\n * MySQL: `BINARY(16)` would overflow on a table name longer than 16 bytes and\n * zero-pad shorter ones (breaking byte-exact id matching). MD5 sidesteps both.\n * Only used for equality (never decoded back to a name), so a non-cryptographic\n * digest is fine. Browser-safe and synchronous (via the `uuid` package), which\n * matters because this module is reachable from the client bundle.\n */\nexport function tableNameToId(tableName: string): Uint8Array {\n return uuidParse(uuidv3(tableName, TABLE_ID_NAMESPACE));\n}\n\nconst tableIdCache = new Map<string, Uint8Array>();\n\n/**\n * Memoized {@link tableNameToId}: hashes each distinct table name once and\n * returns the same interned byte array on every later call. Rebuilding a raw\n * read set happens on the freshness hot path and the commit-time OCC scan, so\n * this avoids repeated hashing and per-entry allocation. The returned bytes are\n * treated as immutable and must never be mutated in place.\n */\nexport function tableIdOf(name: string): Uint8Array {\n let id = tableIdCache.get(name);\n if (id === undefined) {\n id = tableNameToId(name);\n tableIdCache.set(name, id);\n }\n return id;\n}\n"],"mappings":";;;AAGA,MAAa,gBAA8C;CACvD,SAAS,UAAkB;CAC3B,SAAS,UAAmB;EACxB,IAAI,OAAO,UAAU,UAAU,OAAO,OAAO,KAAK;EAClD,QAAQ,OAAO,OAAf;GACI,KAAK;GACL,KAAK;GACL,KAAK;GACL,KAAK,UACD,OAAO,OAAO,KAAK;GACvB,SACI,MAAM,IAAI,MAAM,8BAA8B,OAAO,OAAO;EACpE;CACJ;AACJ;AAEA,MAAa,gBAA+C;CACxD,SAAS,UAAU;CACnB,SAAS,UAAU;AACvB;AAEA,MAAa,cAA4C;CACrD,SAAS,UAAU;CACnB,SAAS,UAAW,OAAO,UAAU,WAAW,OAAO,KAAK,IAAI;AACpE;AAEA,MAAa,eAA8C;CACvD,SAAS,UAAU;CACnB,SAAS,UAAU;EACf,IAAI,OAAO,UAAU,WAAW,OAAO;EACvC,IAAI,OAAO,UAAU,UAAU,OAAO,UAAU;EAChD,IAAI,OAAO,UAAU,UAAU;GAC3B,MAAM,aAAa,MAAM,YAAY;GACrC,OAAO,eAAe,OAAO,eAAe,OAAO,eAAe;EACtE;EACA,OAAO,QAAQ,KAAK;CACxB;AACJ;AAEA,MAAa,YAAwC;CACjD,SAAS,UAAU;CACnB,SAAS,UAAW,iBAAiB,OAAO,QAAQ,IAAI,KAAK,KAAe;AAChF;AAEA,SAAgB,cAAc,OAAyB;CACnD,IAAI,UAAmB;CACvB,OACI,mBAAmB,EAAE,eACrB,mBAAmB,EAAE,eACrB,mBAAmB,EAAE,cACrB,mBAAmB,EAAE,YACrB,mBAAmB,EAAE,aAErB,UAAU,QAAQ,IAAI;CAE1B,OAAO;AACX;AAEA,SAAgB,gBAAgB,OAA2C;CACvE,IAAI,iBAAiB,EAAE,WAAW,OAAO;CACzC,IAAI,iBAAiB,EAAE,YAAY,OAAO;CAC1C,IAAI,iBAAiB,EAAE,SAAS,OAAO;CACvC,OAAO;AACX;AAEA,SAAgB,eACZ,OACA,UACqB;CACrB,MAAM,WAAW,gBAAgB,QAAQ;CACzC,IAAI,CAAC,OAAO,OAAO;CACnB,OAAO;EACH,QAAQ,MAAM,UAAU,SAAS;EACjC,QAAQ,MAAM,UAAU,SAAS;CACrC;AACJ;;;;;;;;ACvEA,MAAM,qBAAqB;;;;;;;;;;;;;;;;AAiB3B,SAAgB,cAAc,WAA+B;CAC3D,OAAOA,MAAUC,GAAO,WAAW,kBAAkB,CAAC;AACxD;AAEA,MAAM,+BAAe,IAAI,IAAwB;;;;;;;;AASjD,SAAgB,UAAU,MAA0B;CAClD,IAAI,KAAK,aAAa,IAAI,IAAI;CAC9B,IAAI,OAAO,KAAA,GAAW;EACpB,KAAK,cAAc,IAAI;EACvB,aAAa,IAAI,MAAM,EAAE;CAC3B;CACA,OAAO;AACT"}
@@ -0,0 +1,123 @@
1
+ import z from "zod";
2
+ //#region src/db/types_client_rpc.ts
3
+ const AuthMessageSchema = z.object({
4
+ type: z.literal("auth"),
5
+ data: z.record(z.string(), z.any()).optional()
6
+ });
7
+ const CallMessageSchema = z.object({
8
+ type: z.literal("call"),
9
+ id: z.string(),
10
+ procedure: z.string(),
11
+ input: z.unknown(),
12
+ /**
13
+ * Client read high-water: the largest commit/snapshot ts the client has
14
+ * already observed (from a prior mutation's commitTs or a query's returned
15
+ * `ts`). Decimal string (bigint on the wire). The server routes a query's
16
+ * reads to the replica ONLY when the replica has applied at least this ts —
17
+ * giving read-your-writes + monotonic reads without a signed token. Omitted
18
+ * until the client has observed anything. The server clamps it to its own
19
+ * primary latest, so a client can't pin the primary forever with a bogus value.
20
+ */
21
+ minTs: z.string().optional()
22
+ });
23
+ const SubscribeMessageSchema = z.object({
24
+ type: z.literal("subscribe"),
25
+ id: z.string(),
26
+ procedure: z.string(),
27
+ input: z.unknown()
28
+ });
29
+ const UnsubscribeMessageSchema = z.object({
30
+ type: z.literal("unsubscribe"),
31
+ subId: z.string()
32
+ });
33
+ const ClientMessageSchema = z.union([
34
+ AuthMessageSchema,
35
+ CallMessageSchema,
36
+ SubscribeMessageSchema,
37
+ UnsubscribeMessageSchema
38
+ ]);
39
+ const ConnectedMessageSchema = z.object({
40
+ type: z.literal("connected"),
41
+ sessionId: z.string()
42
+ });
43
+ const AuthSuccessMessageSchema = z.object({
44
+ type: z.literal("auth:success"),
45
+ userId: z.string(),
46
+ extra: z.object().optional()
47
+ });
48
+ const AuthFailedMessageSchema = z.object({
49
+ type: z.literal("auth:failed"),
50
+ message: z.string(),
51
+ extra: z.record(z.string(), z.unknown()).optional()
52
+ });
53
+ const ResponseMessageSchema = z.object({
54
+ type: z.literal("response"),
55
+ id: z.string(),
56
+ success: z.boolean(),
57
+ data: z.unknown().optional(),
58
+ error: z.string().optional(),
59
+ code: z.string().optional(),
60
+ /**
61
+ * Populated by the server only for a successful `subscribe` response.
62
+ * Carries the canonical, server-computed subscription id so the client
63
+ * can route subsequent `sub:update` / `sub:gone` messages without
64
+ * recomputing the hash locally (which would require the wasm runtime
65
+ * and would not reflect server-side per-procedure `queryIdentity`
66
+ * overrides).
67
+ */
68
+ subId: z.string().optional(),
69
+ /**
70
+ * Observed commit/snapshot ts for a successful `call`. Decimal string
71
+ * (bigint on the wire). For a query it's the snapshot the reads ran at; for a
72
+ * mutation it's the commitTs. The client folds it into its read high-water
73
+ * (see {@link CallMessageSchema.minTs}) so later reads preserve
74
+ * read-your-writes / monotonic reads. Absent for actions and error responses.
75
+ */
76
+ ts: z.string().optional()
77
+ });
78
+ const SubscriptionUpdateMessageSchema = z.object({
79
+ type: z.literal("sub:update"),
80
+ subId: z.string(),
81
+ data: z.unknown(),
82
+ dataHash: z.string().optional()
83
+ });
84
+ /**
85
+ * Notifies a client that a subscription is no longer tracked on the server
86
+ * and will not receive further updates. Sent when, during a sub-manager
87
+ * recovery cycle, the registration record could not be found (e.g. another
88
+ * app-server instance fully unregistered the sub while we were disconnected).
89
+ * The client may choose to re-subscribe or surface the state to the user.
90
+ */
91
+ const SubscriptionGoneMessageSchema = z.object({
92
+ type: z.literal("sub:gone"),
93
+ subId: z.string(),
94
+ reason: z.string().optional()
95
+ });
96
+ /**
97
+ * Server-driven liveness signal. Sent when the channel has been idle for
98
+ * the configured app-ping interval (default 15s) and again after every
99
+ * outbound message. Pure no-op for the client: receiving any message
100
+ * (including this one) resets the client's inactivity watchdog. The client
101
+ * does NOT reply.
102
+ */
103
+ const ServerPingMessageSchema = z.object({ type: z.literal("ping") });
104
+ const ServerMessageSchema = z.union([
105
+ ConnectedMessageSchema,
106
+ AuthSuccessMessageSchema,
107
+ AuthFailedMessageSchema,
108
+ ResponseMessageSchema,
109
+ SubscriptionUpdateMessageSchema,
110
+ SubscriptionGoneMessageSchema,
111
+ ServerPingMessageSchema
112
+ ]);
113
+ var AuthenticationError = class extends Error {
114
+ extra;
115
+ constructor(message, extra = {}) {
116
+ super(message);
117
+ this.extra = extra;
118
+ }
119
+ };
120
+ //#endregion
121
+ export { CallMessageSchema as a, ResponseMessageSchema as c, SubscribeMessageSchema as d, SubscriptionGoneMessageSchema as f, AuthenticationError as i, ServerMessageSchema as l, UnsubscribeMessageSchema as m, AuthMessageSchema as n, ClientMessageSchema as o, SubscriptionUpdateMessageSchema as p, AuthSuccessMessageSchema as r, ConnectedMessageSchema as s, AuthFailedMessageSchema as t, ServerPingMessageSchema as u };
122
+
123
+ //# sourceMappingURL=types_client_rpc-BGMwPJst.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types_client_rpc-BGMwPJst.js","names":[],"sources":["../src/db/types_client_rpc.ts"],"sourcesContent":["import z from \"zod\";\n\nexport interface ErrorMessage {\n type: \"error\";\n id?: string;\n error: string;\n code?: string;\n}\n\n// ─── WebSocket Message Schemas ────────────────────────────────────────────────\n// Zod schemas for runtime validation of WebSocket messages\n\nexport const AuthMessageSchema = z.object({\n type: z.literal(\"auth\"),\n data: z.record(z.string(), z.any()).optional(),\n});\n\nexport const CallMessageSchema = z.object({\n type: z.literal(\"call\"),\n id: z.string(),\n procedure: z.string(),\n input: z.unknown(),\n /**\n * Client read high-water: the largest commit/snapshot ts the client has\n * already observed (from a prior mutation's commitTs or a query's returned\n * `ts`). Decimal string (bigint on the wire). The server routes a query's\n * reads to the replica ONLY when the replica has applied at least this ts —\n * giving read-your-writes + monotonic reads without a signed token. Omitted\n * until the client has observed anything. The server clamps it to its own\n * primary latest, so a client can't pin the primary forever with a bogus value.\n */\n minTs: z.string().optional(),\n});\n\nexport const SubscribeMessageSchema = z.object({\n type: z.literal(\"subscribe\"),\n id: z.string(),\n procedure: z.string(),\n input: z.unknown(),\n});\n\nexport const UnsubscribeMessageSchema = z.object({\n type: z.literal(\"unsubscribe\"),\n subId: z.string(),\n});\n\nexport const ClientMessageSchema = z.union([\n AuthMessageSchema,\n CallMessageSchema,\n SubscribeMessageSchema,\n UnsubscribeMessageSchema,\n]);\n\n// Infer TypeScript types from schemas\nexport type AuthMessage = z.infer<typeof AuthMessageSchema>;\nexport type CallMessage = z.infer<typeof CallMessageSchema>;\nexport type SubscribeMessage = z.infer<typeof SubscribeMessageSchema>;\nexport type UnsubscribeMessage = z.infer<typeof UnsubscribeMessageSchema>;\nexport type ClientMessage = z.infer<typeof ClientMessageSchema>;\n\n// ─── Server Message Schemas ───────────────────────────────────────────────────\n\nexport const ConnectedMessageSchema = z.object({\n type: z.literal(\"connected\"),\n sessionId: z.string(),\n});\n\nexport const AuthSuccessMessageSchema = z.object({\n type: z.literal(\"auth:success\"),\n userId: z.string(),\n extra: z.object().optional()\n});\n\nexport const AuthFailedMessageSchema = z.object({\n type: z.literal(\"auth:failed\"),\n message: z.string(),\n extra: z.record(z.string(), z.unknown()).optional()\n});\n\nexport const ResponseMessageSchema = z.object({\n type: z.literal(\"response\"),\n id: z.string(),\n success: z.boolean(),\n data: z.unknown().optional(),\n error: z.string().optional(),\n code: z.string().optional(),\n /**\n * Populated by the server only for a successful `subscribe` response.\n * Carries the canonical, server-computed subscription id so the client\n * can route subsequent `sub:update` / `sub:gone` messages without\n * recomputing the hash locally (which would require the wasm runtime\n * and would not reflect server-side per-procedure `queryIdentity`\n * overrides).\n */\n subId: z.string().optional(),\n /**\n * Observed commit/snapshot ts for a successful `call`. Decimal string\n * (bigint on the wire). For a query it's the snapshot the reads ran at; for a\n * mutation it's the commitTs. The client folds it into its read high-water\n * (see {@link CallMessageSchema.minTs}) so later reads preserve\n * read-your-writes / monotonic reads. Absent for actions and error responses.\n */\n ts: z.string().optional(),\n});\n\nexport const SubscriptionUpdateMessageSchema = z.object({\n type: z.literal(\"sub:update\"),\n subId: z.string(),\n data: z.unknown(),\n dataHash: z.string().optional(),\n});\n\n/**\n * Notifies a client that a subscription is no longer tracked on the server\n * and will not receive further updates. Sent when, during a sub-manager\n * recovery cycle, the registration record could not be found (e.g. another\n * app-server instance fully unregistered the sub while we were disconnected).\n * The client may choose to re-subscribe or surface the state to the user.\n */\nexport const SubscriptionGoneMessageSchema = z.object({\n type: z.literal(\"sub:gone\"),\n subId: z.string(),\n reason: z.string().optional(),\n});\n\n/**\n * Server-driven liveness signal. Sent when the channel has been idle for\n * the configured app-ping interval (default 15s) and again after every\n * outbound message. Pure no-op for the client: receiving any message\n * (including this one) resets the client's inactivity watchdog. The client\n * does NOT reply.\n */\nexport const ServerPingMessageSchema = z.object({\n type: z.literal(\"ping\"),\n});\n\nexport const ServerMessageSchema = z.union([\n ConnectedMessageSchema,\n AuthSuccessMessageSchema,\n AuthFailedMessageSchema,\n ResponseMessageSchema,\n SubscriptionUpdateMessageSchema,\n SubscriptionGoneMessageSchema,\n ServerPingMessageSchema,\n]);\n\nexport type ConnectedMessage = z.infer<typeof ConnectedMessageSchema>;\nexport type AuthSuccessMessage = z.infer<typeof AuthSuccessMessageSchema>;\nexport type AuthFailedMessage = z.infer<typeof AuthFailedMessageSchema>;\nexport type ResponseMessage = z.infer<typeof ResponseMessageSchema>;\nexport type SubscriptionUpdateMessage = z.infer<typeof SubscriptionUpdateMessageSchema>;\nexport type SubscriptionGoneMessage = z.infer<typeof SubscriptionGoneMessageSchema>;\nexport type ServerPingMessage = z.infer<typeof ServerPingMessageSchema>;\nexport type ServerMessage = z.infer<typeof ServerMessageSchema>;\n\nexport class AuthenticationError extends Error {\n constructor(\n message: string,\n public extra: Record<string, unknown> = {},\n ) {\n super(message);\n }\n}\n\n/**\n * Helper type to check if a procedure is internal.\n */\nexport type IsInternal<T> = T extends { internal: infer I } ? (I extends true ? true : false) : false;\n\nexport interface Context {\n requestId: string;\n /** Authenticated user info */\n user: {\n userId: string;\n };\n}\n"],"mappings":";;AAYA,MAAa,oBAAoB,EAAE,OAAO;CACxC,MAAM,EAAE,QAAQ,MAAM;CACtB,MAAM,EAAE,OAAO,EAAE,OAAO,GAAG,EAAE,IAAI,CAAC,CAAC,CAAC,SAAS;AAC/C,CAAC;AAED,MAAa,oBAAoB,EAAE,OAAO;CACxC,MAAM,EAAE,QAAQ,MAAM;CACtB,IAAI,EAAE,OAAO;CACb,WAAW,EAAE,OAAO;CACpB,OAAO,EAAE,QAAQ;;;;;;;;;;CAUjB,OAAO,EAAE,OAAO,CAAC,CAAC,SAAS;AAC7B,CAAC;AAED,MAAa,yBAAyB,EAAE,OAAO;CAC7C,MAAM,EAAE,QAAQ,WAAW;CAC3B,IAAI,EAAE,OAAO;CACb,WAAW,EAAE,OAAO;CACpB,OAAO,EAAE,QAAQ;AACnB,CAAC;AAED,MAAa,2BAA2B,EAAE,OAAO;CAC/C,MAAM,EAAE,QAAQ,aAAa;CAC7B,OAAO,EAAE,OAAO;AAClB,CAAC;AAED,MAAa,sBAAsB,EAAE,MAAM;CACzC;CACA;CACA;CACA;AACF,CAAC;AAWD,MAAa,yBAAyB,EAAE,OAAO;CAC7C,MAAM,EAAE,QAAQ,WAAW;CAC3B,WAAW,EAAE,OAAO;AACtB,CAAC;AAED,MAAa,2BAA2B,EAAE,OAAO;CAC/C,MAAM,EAAE,QAAQ,cAAc;CAC9B,QAAQ,EAAE,OAAO;CACjB,OAAO,EAAE,OAAO,CAAC,CAAC,SAAS;AAC7B,CAAC;AAED,MAAa,0BAA0B,EAAE,OAAO;CAC9C,MAAM,EAAE,QAAQ,aAAa;CAC7B,SAAS,EAAE,OAAO;CAClB,OAAO,EAAE,OAAO,EAAE,OAAO,GAAG,EAAE,QAAQ,CAAC,CAAC,CAAC,SAAS;AACpD,CAAC;AAED,MAAa,wBAAwB,EAAE,OAAO;CAC5C,MAAM,EAAE,QAAQ,UAAU;CAC1B,IAAI,EAAE,OAAO;CACb,SAAS,EAAE,QAAQ;CACnB,MAAM,EAAE,QAAQ,CAAC,CAAC,SAAS;CAC3B,OAAO,EAAE,OAAO,CAAC,CAAC,SAAS;CAC3B,MAAM,EAAE,OAAO,CAAC,CAAC,SAAS;;;;;;;;;CAS1B,OAAO,EAAE,OAAO,CAAC,CAAC,SAAS;;;;;;;;CAQ3B,IAAI,EAAE,OAAO,CAAC,CAAC,SAAS;AAC1B,CAAC;AAED,MAAa,kCAAkC,EAAE,OAAO;CACtD,MAAM,EAAE,QAAQ,YAAY;CAC5B,OAAO,EAAE,OAAO;CAChB,MAAM,EAAE,QAAQ;CAChB,UAAU,EAAE,OAAO,CAAC,CAAC,SAAS;AAChC,CAAC;;;;;;;;AASD,MAAa,gCAAgC,EAAE,OAAO;CACpD,MAAM,EAAE,QAAQ,UAAU;CAC1B,OAAO,EAAE,OAAO;CAChB,QAAQ,EAAE,OAAO,CAAC,CAAC,SAAS;AAC9B,CAAC;;;;;;;;AASD,MAAa,0BAA0B,EAAE,OAAO,EAC9C,MAAM,EAAE,QAAQ,MAAM,EACxB,CAAC;AAED,MAAa,sBAAsB,EAAE,MAAM;CACzC;CACA;CACA;CACA;CACA;CACA;CACA;AACF,CAAC;AAWD,IAAa,sBAAb,cAAyC,MAAM;CAGpC;CAFT,YACE,SACA,QAAwC,CAAC,GACzC;EACA,MAAM,OAAO;EAFN,KAAA,QAAA;CAGT;AACF"}
@@ -0,0 +1,132 @@
1
+ import z from "zod";
2
+ //#region src/db/types_client_rpc.ts
3
+ const AuthMessageSchema = z.object({
4
+ type: z.literal("auth"),
5
+ data: z.record(z.string(), z.any()).optional()
6
+ });
7
+ const CallMessageSchema = z.object({
8
+ type: z.literal("call"),
9
+ id: z.string(),
10
+ procedure: z.string(),
11
+ input: z.unknown(),
12
+ /**
13
+ * Client read high-water: the largest commit/snapshot ts the client has
14
+ * already observed (from a prior mutation's commitTs or a query's returned
15
+ * `ts`). Decimal string (bigint on the wire). The server routes a query's
16
+ * reads to the replica ONLY when the replica has applied at least this ts —
17
+ * giving read-your-writes + monotonic reads without a signed token. Omitted
18
+ * until the client has observed anything. The server clamps it to its own
19
+ * primary latest, so a client can't pin the primary forever with a bogus value.
20
+ */
21
+ minTs: z.string().optional()
22
+ });
23
+ const SubscribeMessageSchema = z.object({
24
+ type: z.literal("subscribe"),
25
+ id: z.string(),
26
+ procedure: z.string(),
27
+ input: z.unknown()
28
+ });
29
+ const UnsubscribeMessageSchema = z.object({
30
+ type: z.literal("unsubscribe"),
31
+ subId: z.string()
32
+ });
33
+ const ClientMessageSchema = z.union([
34
+ AuthMessageSchema,
35
+ CallMessageSchema,
36
+ SubscribeMessageSchema,
37
+ UnsubscribeMessageSchema
38
+ ]);
39
+ const ConnectedMessageSchema = z.object({
40
+ type: z.literal("connected"),
41
+ sessionId: z.string()
42
+ });
43
+ const AuthSuccessMessageSchema = z.object({
44
+ type: z.literal("auth:success"),
45
+ userId: z.string(),
46
+ extra: z.object().optional()
47
+ });
48
+ const AuthFailedMessageSchema = z.object({
49
+ type: z.literal("auth:failed"),
50
+ message: z.string(),
51
+ extra: z.record(z.string(), z.unknown()).optional()
52
+ });
53
+ const ResponseMessageSchema = z.object({
54
+ type: z.literal("response"),
55
+ id: z.string(),
56
+ success: z.boolean(),
57
+ data: z.unknown().optional(),
58
+ error: z.string().optional(),
59
+ code: z.string().optional(),
60
+ /**
61
+ * Populated by the server only for a successful `subscribe` response.
62
+ * Carries the canonical, server-computed subscription id so the client
63
+ * can route subsequent `sub:update` / `sub:gone` messages without
64
+ * recomputing the hash locally (which would require the wasm runtime
65
+ * and would not reflect server-side per-procedure `queryIdentity`
66
+ * overrides).
67
+ */
68
+ subId: z.string().optional(),
69
+ /**
70
+ * Observed commit/snapshot ts for a successful `call`. Decimal string
71
+ * (bigint on the wire). For a query it's the snapshot the reads ran at; for a
72
+ * mutation it's the commitTs. The client folds it into its read high-water
73
+ * (see {@link CallMessageSchema.minTs}) so later reads preserve
74
+ * read-your-writes / monotonic reads. Absent for actions and error responses.
75
+ */
76
+ ts: z.string().optional()
77
+ });
78
+ const SubscriptionUpdateMessageSchema = z.object({
79
+ type: z.literal("sub:update"),
80
+ subId: z.string(),
81
+ data: z.unknown(),
82
+ dataHash: z.string().optional(),
83
+ /**
84
+ * The primary snapshot ts this update's data reflects. Decimal string (bigint
85
+ * on the wire). The client folds it into its read high-water so a later
86
+ * one-shot query can't route to a replica that is behind what the live view
87
+ * already showed (cross-subscription monotonic reads). Present on server-side
88
+ * recomputes; omitted when the update is served straight from cache (no ts
89
+ * handy) — the client simply doesn't advance for those.
90
+ */
91
+ ts: z.string().optional()
92
+ });
93
+ /**
94
+ * Notifies a client that a subscription is no longer tracked on the server
95
+ * and will not receive further updates. Sent when, during a sub-manager
96
+ * recovery cycle, the registration record could not be found (e.g. another
97
+ * app-server instance fully unregistered the sub while we were disconnected).
98
+ * The client may choose to re-subscribe or surface the state to the user.
99
+ */
100
+ const SubscriptionGoneMessageSchema = z.object({
101
+ type: z.literal("sub:gone"),
102
+ subId: z.string(),
103
+ reason: z.string().optional()
104
+ });
105
+ /**
106
+ * Server-driven liveness signal. Sent when the channel has been idle for
107
+ * the configured app-ping interval (default 15s) and again after every
108
+ * outbound message. Pure no-op for the client: receiving any message
109
+ * (including this one) resets the client's inactivity watchdog. The client
110
+ * does NOT reply.
111
+ */
112
+ const ServerPingMessageSchema = z.object({ type: z.literal("ping") });
113
+ const ServerMessageSchema = z.union([
114
+ ConnectedMessageSchema,
115
+ AuthSuccessMessageSchema,
116
+ AuthFailedMessageSchema,
117
+ ResponseMessageSchema,
118
+ SubscriptionUpdateMessageSchema,
119
+ SubscriptionGoneMessageSchema,
120
+ ServerPingMessageSchema
121
+ ]);
122
+ var AuthenticationError = class extends Error {
123
+ extra;
124
+ constructor(message, extra = {}) {
125
+ super(message);
126
+ this.extra = extra;
127
+ }
128
+ };
129
+ //#endregion
130
+ export { CallMessageSchema as a, ResponseMessageSchema as c, SubscribeMessageSchema as d, SubscriptionGoneMessageSchema as f, AuthenticationError as i, ServerMessageSchema as l, UnsubscribeMessageSchema as m, AuthMessageSchema as n, ClientMessageSchema as o, SubscriptionUpdateMessageSchema as p, AuthSuccessMessageSchema as r, ConnectedMessageSchema as s, AuthFailedMessageSchema as t, ServerPingMessageSchema as u };
131
+
132
+ //# sourceMappingURL=types_client_rpc-ByGwoRCL.js.map