@supalive/core 1.11.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 (228) 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-DDDAcnJj.d.ts +2208 -0
  15. package/dist/index-DDDAcnJj.d.ts.map +1 -0
  16. package/dist/index-DZhReQWB.d.ts +2288 -0
  17. package/dist/index-DZhReQWB.d.ts.map +1 -0
  18. package/dist/index-DlG8HDCO.d.ts +2215 -0
  19. package/dist/index-DlG8HDCO.d.ts.map +1 -0
  20. package/dist/index-Wgi_dGzj.d.ts +2215 -0
  21. package/dist/index-Wgi_dGzj.d.ts.map +1 -0
  22. package/dist/index-j3YTMNlF.d.ts +2223 -0
  23. package/dist/index-j3YTMNlF.d.ts.map +1 -0
  24. package/dist/mysql-BBbilD4I.js +614 -0
  25. package/dist/mysql-BBbilD4I.js.map +1 -0
  26. package/dist/mysql-BHJ2mrwZ.d.ts +113 -0
  27. package/dist/mysql-BHJ2mrwZ.d.ts.map +1 -0
  28. package/dist/mysql-BQbHJ_rK.js +617 -0
  29. package/dist/mysql-BQbHJ_rK.js.map +1 -0
  30. package/dist/mysql-BXoAON7c.d.ts +113 -0
  31. package/dist/mysql-BXoAON7c.d.ts.map +1 -0
  32. package/dist/mysql-BgfCUYGt.d.ts +113 -0
  33. package/dist/mysql-BgfCUYGt.d.ts.map +1 -0
  34. package/dist/mysql-Br7MqUXg.js +620 -0
  35. package/dist/mysql-Br7MqUXg.js.map +1 -0
  36. package/dist/mysql-BrueWtO9.d.ts +113 -0
  37. package/dist/mysql-BrueWtO9.d.ts.map +1 -0
  38. package/dist/mysql-CCo--z6n.d.ts +113 -0
  39. package/dist/mysql-CCo--z6n.d.ts.map +1 -0
  40. package/dist/mysql-CQcbcZB8.d.ts +113 -0
  41. package/dist/mysql-CQcbcZB8.d.ts.map +1 -0
  42. package/dist/mysql-CbgCY89y.d.ts +113 -0
  43. package/dist/mysql-CbgCY89y.d.ts.map +1 -0
  44. package/dist/mysql-CqD1ZnNY.js +619 -0
  45. package/dist/mysql-CqD1ZnNY.js.map +1 -0
  46. package/dist/mysql-D3Db2q3_.js +619 -0
  47. package/dist/mysql-D3Db2q3_.js.map +1 -0
  48. package/dist/mysql-DBrhTmYm.js +619 -0
  49. package/dist/mysql-DBrhTmYm.js.map +1 -0
  50. package/dist/mysql-DF8eJO-v.d.ts +113 -0
  51. package/dist/mysql-DF8eJO-v.d.ts.map +1 -0
  52. package/dist/mysql-DX7YA7IS.d.ts +113 -0
  53. package/dist/mysql-DX7YA7IS.d.ts.map +1 -0
  54. package/dist/mysql-DZ0_Rsjc.js +614 -0
  55. package/dist/mysql-DZ0_Rsjc.js.map +1 -0
  56. package/dist/mysql-Dko1MJqQ.js +619 -0
  57. package/dist/mysql-Dko1MJqQ.js.map +1 -0
  58. package/dist/mysql-F831ycnw.d.ts +113 -0
  59. package/dist/mysql-F831ycnw.d.ts.map +1 -0
  60. package/dist/mysql-TLPG62tv.js +618 -0
  61. package/dist/mysql-TLPG62tv.js.map +1 -0
  62. package/dist/mysql-XUkTkefP.js +610 -0
  63. package/dist/mysql-XUkTkefP.js.map +1 -0
  64. package/dist/mysql-e-ii74yG.js +613 -0
  65. package/dist/mysql-e-ii74yG.js.map +1 -0
  66. package/dist/overlap-checker-BBkqRdcs.js +259 -0
  67. package/dist/overlap-checker-BBkqRdcs.js.map +1 -0
  68. package/dist/overlap-checker-BdNoCPtA.js +259 -0
  69. package/dist/overlap-checker-BdNoCPtA.js.map +1 -0
  70. package/dist/overlap-checker-DHUJe-Yy.js +259 -0
  71. package/dist/overlap-checker-DHUJe-Yy.js.map +1 -0
  72. package/dist/overlap-checker-DWeQLpSD.js +259 -0
  73. package/dist/overlap-checker-DWeQLpSD.js.map +1 -0
  74. package/dist/overlap-checker-Kz7B_jsp.js +259 -0
  75. package/dist/overlap-checker-Kz7B_jsp.js.map +1 -0
  76. package/dist/postgres-7hJuQX4Y.d.ts +117 -0
  77. package/dist/postgres-7hJuQX4Y.d.ts.map +1 -0
  78. package/dist/postgres-B4H78IkW.js +864 -0
  79. package/dist/postgres-B4H78IkW.js.map +1 -0
  80. package/dist/postgres-B4h8D5U2.d.ts +117 -0
  81. package/dist/postgres-B4h8D5U2.d.ts.map +1 -0
  82. package/dist/postgres-BXFPamkP.js +864 -0
  83. package/dist/postgres-BXFPamkP.js.map +1 -0
  84. package/dist/postgres-C7CHroi9.js +865 -0
  85. package/dist/postgres-C7CHroi9.js.map +1 -0
  86. package/dist/postgres-CoR3TUMd.js +865 -0
  87. package/dist/postgres-CoR3TUMd.js.map +1 -0
  88. package/dist/postgres-D-RsG12a.d.ts +117 -0
  89. package/dist/postgres-D-RsG12a.d.ts.map +1 -0
  90. package/dist/postgres-D5Z1S3J9.d.ts +117 -0
  91. package/dist/postgres-D5Z1S3J9.d.ts.map +1 -0
  92. package/dist/postgres-DHs89iWT.js +864 -0
  93. package/dist/postgres-DHs89iWT.js.map +1 -0
  94. package/dist/postgres-DvBIRnYA.d.ts +117 -0
  95. package/dist/postgres-DvBIRnYA.d.ts.map +1 -0
  96. package/dist/postgres-EXjuHMUi.d.ts +117 -0
  97. package/dist/postgres-EXjuHMUi.d.ts.map +1 -0
  98. package/dist/postgres-Eo-lpUiX.d.ts +117 -0
  99. package/dist/postgres-Eo-lpUiX.d.ts.map +1 -0
  100. package/dist/postgres-VLAGIFLR.d.ts +117 -0
  101. package/dist/postgres-VLAGIFLR.d.ts.map +1 -0
  102. package/dist/postgres-hrQmDPUp.d.ts +117 -0
  103. package/dist/postgres-hrQmDPUp.d.ts.map +1 -0
  104. package/dist/postgres-pXrP9Fx0.d.ts +117 -0
  105. package/dist/postgres-pXrP9Fx0.d.ts.map +1 -0
  106. package/dist/query-Bpw8wO6L.js +730 -0
  107. package/dist/query-Bpw8wO6L.js.map +1 -0
  108. package/dist/query-BuOs9w-U.js +730 -0
  109. package/dist/query-BuOs9w-U.js.map +1 -0
  110. package/dist/query-Xw2hi6TB.js +730 -0
  111. package/dist/query-Xw2hi6TB.js.map +1 -0
  112. package/dist/read-routing-BUcQAwnd.js +62 -0
  113. package/dist/read-routing-BUcQAwnd.js.map +1 -0
  114. package/dist/read-routing-BXflWzQI.js +61 -0
  115. package/dist/read-routing-BXflWzQI.js.map +1 -0
  116. package/dist/read-routing-CcsLp0ow.js +61 -0
  117. package/dist/read-routing-CcsLp0ow.js.map +1 -0
  118. package/dist/realtime_db-B70vqtRZ.js +272 -0
  119. package/dist/realtime_db-B70vqtRZ.js.map +1 -0
  120. package/dist/realtime_db-BMysrWym.js +312 -0
  121. package/dist/realtime_db-BMysrWym.js.map +1 -0
  122. package/dist/realtime_db-Bi7S938D.js +312 -0
  123. package/dist/realtime_db-Bi7S938D.js.map +1 -0
  124. package/dist/realtime_db-CVze3gM0.js +312 -0
  125. package/dist/realtime_db-CVze3gM0.js.map +1 -0
  126. package/dist/realtime_db-DMipkUmD.js +312 -0
  127. package/dist/realtime_db-DMipkUmD.js.map +1 -0
  128. package/dist/realtime_db-XWzuq2JS.js +272 -0
  129. package/dist/realtime_db-XWzuq2JS.js.map +1 -0
  130. package/dist/realtime_db-e3TztUUT.js +272 -0
  131. package/dist/realtime_db-e3TztUUT.js.map +1 -0
  132. package/dist/router-Kgrlci8X.js +337 -0
  133. package/dist/router-Kgrlci8X.js.map +1 -0
  134. package/dist/router-g8qSQb8m.js +314 -0
  135. package/dist/router-g8qSQb8m.js.map +1 -0
  136. package/dist/schema-CWdfWuIj.js +105 -0
  137. package/dist/schema-CWdfWuIj.js.map +1 -0
  138. package/dist/schema-DLue3K2s.js +134 -0
  139. package/dist/schema-DLue3K2s.js.map +1 -0
  140. package/dist/schema-DfowI0ip.js +134 -0
  141. package/dist/schema-DfowI0ip.js.map +1 -0
  142. package/dist/schema-fKldWzwc.js +134 -0
  143. package/dist/schema-fKldWzwc.js.map +1 -0
  144. package/dist/src/client/index.d.ts +1 -1
  145. package/dist/src/client/index.js +69 -29
  146. package/dist/src/client/index.js.map +1 -1
  147. package/dist/src/exports/mysql.d.ts +1 -1
  148. package/dist/src/exports/mysql.js +1 -1
  149. package/dist/src/exports/postgres.d.ts +1 -1
  150. package/dist/src/exports/postgres.js +1 -1
  151. package/dist/src/exports/procedure.d.ts +1 -1
  152. package/dist/src/exports/procedure.js +5 -5
  153. package/dist/src/exports/schema-sql.d.ts +18 -1
  154. package/dist/src/exports/schema-sql.d.ts.map +1 -1
  155. package/dist/src/exports/schema-sql.js +91 -15
  156. package/dist/src/exports/schema-sql.js.map +1 -1
  157. package/dist/src/exports/server.d.ts +11 -4
  158. package/dist/src/exports/server.d.ts.map +1 -1
  159. package/dist/src/exports/server.js +136 -59
  160. package/dist/src/exports/server.js.map +1 -1
  161. package/dist/src/exports/sub-manager-worker-entry.js +1 -1
  162. package/dist/src/exports/types.d.ts +2 -2
  163. package/dist/src/exports/types.js +4 -4
  164. package/dist/sub-worker-dispatch-CO9nVdSG.js +813 -0
  165. package/dist/sub-worker-dispatch-CO9nVdSG.js.map +1 -0
  166. package/dist/sub-worker-dispatch-CQBknEL7.js +813 -0
  167. package/dist/sub-worker-dispatch-CQBknEL7.js.map +1 -0
  168. package/dist/sub-worker-dispatch-CQHeit6D.js +813 -0
  169. package/dist/sub-worker-dispatch-CQHeit6D.js.map +1 -0
  170. package/dist/sub-worker-dispatch-Cl-XnBwq.js +810 -0
  171. package/dist/sub-worker-dispatch-Cl-XnBwq.js.map +1 -0
  172. package/dist/sub-worker-dispatch-CmEW10it.js +813 -0
  173. package/dist/sub-worker-dispatch-CmEW10it.js.map +1 -0
  174. package/dist/sub-worker-dispatch-CunxuOPT.js +823 -0
  175. package/dist/sub-worker-dispatch-CunxuOPT.js.map +1 -0
  176. package/dist/sub-worker-dispatch-CvvFm40r.js +817 -0
  177. package/dist/sub-worker-dispatch-CvvFm40r.js.map +1 -0
  178. package/dist/sub-worker-dispatch-D15UHo1g.js +813 -0
  179. package/dist/sub-worker-dispatch-D15UHo1g.js.map +1 -0
  180. package/dist/sub-worker-dispatch-D8DY56ry.js +817 -0
  181. package/dist/sub-worker-dispatch-D8DY56ry.js.map +1 -0
  182. package/dist/sub-worker-dispatch-DJgWxtSQ.js +823 -0
  183. package/dist/sub-worker-dispatch-DJgWxtSQ.js.map +1 -0
  184. package/dist/sub-worker-dispatch-Dplqeubd.js +813 -0
  185. package/dist/sub-worker-dispatch-Dplqeubd.js.map +1 -0
  186. package/dist/sub-worker-dispatch-dXjUmSMN.js +813 -0
  187. package/dist/sub-worker-dispatch-dXjUmSMN.js.map +1 -0
  188. package/dist/table-id-BJdmctEQ.js +91 -0
  189. package/dist/table-id-BJdmctEQ.js.map +1 -0
  190. package/dist/table-id-DyrJBKAQ.js +105 -0
  191. package/dist/table-id-DyrJBKAQ.js.map +1 -0
  192. package/dist/table-id-x9y1eHUr.js +91 -0
  193. package/dist/table-id-x9y1eHUr.js.map +1 -0
  194. package/dist/types_client_rpc-BGMwPJst.js +123 -0
  195. package/dist/types_client_rpc-BGMwPJst.js.map +1 -0
  196. package/dist/types_client_rpc-ByGwoRCL.js +132 -0
  197. package/dist/types_client_rpc-ByGwoRCL.js.map +1 -0
  198. package/dist/types_db-BsR02Sph.js +171 -0
  199. package/dist/types_db-BsR02Sph.js.map +1 -0
  200. package/dist/types_db-CgGlOMmr.js +187 -0
  201. package/dist/types_db-CgGlOMmr.js.map +1 -0
  202. package/dist/types_db-CmxFWgU3.js +171 -0
  203. package/dist/types_db-CmxFWgU3.js.map +1 -0
  204. package/dist/types_db-OUou3o2Z.js +172 -0
  205. package/dist/types_db-OUou3o2Z.js.map +1 -0
  206. package/dist/types_server-B3z8hi-Z.d.ts +523 -0
  207. package/dist/types_server-B3z8hi-Z.d.ts.map +1 -0
  208. package/dist/types_server-BF5Y0s__.d.ts +521 -0
  209. package/dist/types_server-BF5Y0s__.d.ts.map +1 -0
  210. package/dist/types_server-BXBvz4T8.d.ts +523 -0
  211. package/dist/types_server-BXBvz4T8.d.ts.map +1 -0
  212. package/dist/types_server-BhC19R2a.js +27 -0
  213. package/dist/types_server-BhC19R2a.js.map +1 -0
  214. package/dist/types_server-CClUPZn1.d.ts +521 -0
  215. package/dist/types_server-CClUPZn1.d.ts.map +1 -0
  216. package/dist/types_server-CnYTSPhM.d.ts +523 -0
  217. package/dist/types_server-CnYTSPhM.d.ts.map +1 -0
  218. package/dist/types_server-DHpGUHB_.d.ts +521 -0
  219. package/dist/types_server-DHpGUHB_.d.ts.map +1 -0
  220. package/dist/types_server-Ded1u3bL.d.ts +521 -0
  221. package/dist/types_server-Ded1u3bL.d.ts.map +1 -0
  222. package/dist/types_server-fons7nmo.d.ts +523 -0
  223. package/dist/types_server-fons7nmo.d.ts.map +1 -0
  224. package/dist/types_server-okvBG4v_.d.ts +521 -0
  225. package/dist/types_server-okvBG4v_.d.ts.map +1 -0
  226. package/dist/types_server-pD8DiI7s.d.ts +521 -0
  227. package/dist/types_server-pD8DiI7s.d.ts.map +1 -0
  228. package/package.json +1 -1
@@ -1,13 +1,14 @@
1
- import { n as getContextRegistry } from "../../router-CiWYeWUR.js";
2
- import { n as stableStringify, r as supaliveStringify, t as groupByToMap } from "../../helper-wGbzz6zo.js";
3
- import { r as evaluateCacheFreshness } from "../../overlap-checker-BoWNco2y.js";
4
- import { i as AuthenticationError, o as ClientMessageSchema } from "../../types_client_rpc-CIsjQQGG.js";
5
- import { n as RemoteSubscriptionMessageSchema } from "../../types_server-Dk6o_B7N.js";
6
- import { n as sleep } from "../../realtime_db-DiYQbfeA.js";
7
- import { C as appInstanceId, D as serializeMapValue, E as logger, S as RetentionWatermark, T as logLevelOf, _ as UnregisterSubscriptionResultSchema, a as bootstrapDbTypes, b as UpdateSubscriptionReadSetResultSchema, c as initCore, d as InvalidateWritesetParamsSchema, f as InvalidateWritesetResultSchema, g as UnregisterSubscriptionParamsSchema, h as RegisterSubscriptionResultSchema, i as DB_QUERY_TIMEOUT_MS, l as initDatabase, m as RegisterSubscriptionParamsSchema, n as SubscriptionWorker, o as createDatabase, p as RegisterSubscriptionBatchResultSchema, r as CORE_VERSION, s as initCacheLayer, t as dispatchWorkerMessage, u as CacheLayer, v as UnregisterSubscriptionsResultSchema, w as isProduction, x as RETENTION_WATERMARK_CHANNEL, y as UpdateSubscriptionReadSetParamsSchema } from "../../sub-worker-dispatch-DohrszOw.js";
1
+ import { n as getContextRegistry, u as primaryOnlyConn } from "../../router-Kgrlci8X.js";
2
+ import { n as stableStringify, r as supaliveStringify, t as groupByToMap } from "../../helper-zdJT5FUc.js";
3
+ import { r as evaluateCacheFreshness } from "../../overlap-checker-Kz7B_jsp.js";
4
+ import { i as AuthenticationError, o as ClientMessageSchema } from "../../types_client_rpc-ByGwoRCL.js";
5
+ import { n as RemoteSubscriptionMessageSchema } from "../../types_server-BhC19R2a.js";
6
+ import { n as sleep } from "../../realtime_db-CVze3gM0.js";
7
+ import { n as resolveReadRouting, t as makeConnRouting } from "../../read-routing-BUcQAwnd.js";
8
+ import { C as appInstanceId, D as serializeMapValue, E as logger, S as RetentionWatermark, T as logLevelOf, _ as UnregisterSubscriptionResultSchema, a as bootstrapDbTypes, b as UpdateSubscriptionReadSetResultSchema, c as initCore, d as InvalidateWritesetParamsSchema, f as InvalidateWritesetResultSchema, g as UnregisterSubscriptionParamsSchema, h as RegisterSubscriptionResultSchema, i as DB_QUERY_TIMEOUT_MS, l as initDatabase, m as RegisterSubscriptionParamsSchema, n as SubscriptionWorker, o as createDatabase, p as RegisterSubscriptionBatchResultSchema, r as CORE_VERSION, s as initCacheLayer, t as dispatchWorkerMessage, u as CacheLayer, v as UnregisterSubscriptionsResultSchema, w as isProduction, x as RETENTION_WATERMARK_CHANNEL, y as UpdateSubscriptionReadSetParamsSchema } from "../../sub-worker-dispatch-DJgWxtSQ.js";
9
+ import { v4 } from "uuid";
8
10
  import { createServer } from "http";
9
11
  import { WebSocket, WebSocketServer } from "ws";
10
- import { v4 } from "uuid";
11
12
  import { ExponentialBackoff, handleAll, retry } from "cockatiel";
12
13
  import { sha1 } from "hash-wasm";
13
14
  import { Client, Server } from "rpc-websockets";
@@ -78,43 +79,65 @@ async function generateCacheKey(queryName, input, queryIdentity) {
78
79
  *
79
80
  * Flow (mirrors `handleSubscribe` / worker `register`):
80
81
  * 1. Resolve `queryIdentity` → `cacheKey` (identical segmentation to subs).
81
- * 2. Freshness check via {@link evaluateCacheFreshness}: cached metadata +
82
- * the retention watermark + a between-ts commit-log scan.
83
- * 3. Fresh → return the cached data.
84
- * Stale/miss recompute via `queryInternal` and write BOTH the data and
85
- * the metadata (readSet + snapshotTs) back, so a later one-shot or a fresh
86
- * subscription can reuse it.
82
+ * 2. Pick the read connection ONCE (replica when it's caught up to the client's
83
+ * `clientMinTs`, else primary) and use it for BOTH the freshness scan and
84
+ * the row reads, so a cache HIT offloads the primary too — not just a miss.
85
+ * 3. Freshness check via {@link evaluateCacheFreshness} on that connection:
86
+ * cached metadata + the retention watermark + a between-ts commit-log scan.
87
+ * 4. Fresh → return the cached data.
88
+ * Stale/miss → recompute via `queryInternalOn` (on the chosen connection)
89
+ * and write BOTH the data and the metadata (readSet + snapshotTs) back, so
90
+ * a later one-shot or a fresh subscription can reuse it.
87
91
  *
88
92
  * The retention guard is what makes an untracked one-shot safe: nothing holds
89
93
  * the prune watermark down for it, so an entry whose snapshot has fallen below
90
94
  * the oldest retained commit log is treated as a miss (its freshness scan would
91
95
  * be incomplete).
92
96
  *
93
- * `lastSnapshotTs` is the freshness upper bound — the latest snapshot ts as of
94
- * this call. The caller loads it (via `db.getLatestSnapshotTimestamp()`) and
95
- * passes it in, exactly like the subscribe path loads it before `register`, so
96
- * the snapshot read lives at the caller seam rather than being hidden here.
97
+ * `lastSnapshotTs` is the primary freshness upper bound — the latest snapshot ts
98
+ * as of this call. The caller loads it (via `db.getLatestSnapshotTimestamp()`)
99
+ * and passes it in, exactly like the subscribe path loads it before `register`,
100
+ * so the snapshot read lives at the caller seam rather than being hidden here.
101
+ *
102
+ * `clientMinTs` is the caller's read high-water (the largest commit/snapshot ts
103
+ * it has already observed). It gates replica routing: a query only runs on the
104
+ * replica when the replica has applied at least `clientMinTs` — otherwise reads
105
+ * (and the freshness scan) stay on the primary, preserving read-your-writes and
106
+ * monotonic reads. It is clamped to `lastSnapshotTs` here, so a client can at
107
+ * most force the primary (never a value beyond it). Returns the data plus the
108
+ * `ts` the result reflects, which the caller echoes so the client can advance
109
+ * its high-water.
97
110
  */
98
- async function runCachedQuery(deps, procedure, procedureName, input, serverCtx, lastSnapshotTs) {
111
+ async function runCachedQuery(deps, procedure, procedureName, input, serverCtx, lastSnapshotTs, clientMinTs) {
99
112
  const queryIdentity = resolveQueryIdentity(procedure, serverCtx, input, deps.getUserId);
100
113
  const cacheKey = await generateCacheKey(procedureName, stableStringify(input), queryIdentity);
114
+ const routing = await resolveReadRouting(deps.db, lastSnapshotTs, clientMinTs ?? 0n);
115
+ const readTs = routing.readTs;
101
116
  const metadata = await deps.cache.getQueryCacheMetaData(cacheKey);
102
- if ((await evaluateCacheFreshness(deps.db.impl, metadata, lastSnapshotTs, deps.getMinRetainedTs())).fresh) {
117
+ if ((await evaluateCacheFreshness(routing.readImpl, metadata, readTs, deps.getMinRetainedTs())).fresh) {
103
118
  const cached = await deps.cache.getQueryCacheFor(cacheKey);
104
119
  if (cached && metadata && cached.version === metadata.version) {
105
- if (lastSnapshotTs > metadata.lastSnapshotTs) deps.cache.advanceQueryCacheMetadata(cacheKey, {
106
- lastSnapshotTs,
120
+ if (readTs > metadata.lastSnapshotTs) deps.cache.advanceQueryCacheMetadata(cacheKey, {
121
+ lastSnapshotTs: readTs,
107
122
  version: metadata.version,
108
123
  readSet: metadata.readSet
109
124
  }).catch((err) => logger.error(err, "one-shot horizon advance failed"));
110
- return cached.data;
125
+ const servedTs = readTs > metadata.lastSnapshotTs ? readTs : metadata.lastSnapshotTs;
126
+ return {
127
+ data: cached.data,
128
+ ts: servedTs
129
+ };
111
130
  }
112
131
  }
113
- const result = await deps.db.queryInternal(async (ctx) => {
132
+ const result = await deps.db.queryInternalOn({
133
+ impl: routing.readImpl,
134
+ beginTs: readTs
135
+ }, async (ctx) => {
114
136
  const fn = procedure.fn;
115
137
  return fn({
116
138
  db: ctx,
117
- serverCtx
139
+ serverCtx,
140
+ ...makeConnRouting(ctx, routing)
118
141
  }, input);
119
142
  });
120
143
  await deps.cache.setQueryCacheAndMetadataFor(cacheKey, result.data, {
@@ -122,7 +145,10 @@ async function runCachedQuery(deps, procedure, procedureName, input, serverCtx,
122
145
  version: String(result.ts),
123
146
  readSet: result.readSet
124
147
  }).catch((err) => logger.error(err, "one-shot query cache write failed"));
125
- return result.data;
148
+ return {
149
+ data: result.data,
150
+ ts: result.ts
151
+ };
126
152
  }
127
153
  //#endregion
128
154
  //#region src/server/sub-manager-client.ts
@@ -595,11 +621,13 @@ var HttpRpcHandler = class {
595
621
  const rawBody = await readBody(req);
596
622
  let name;
597
623
  let rawInput;
624
+ let minTs;
598
625
  try {
599
626
  const envelope = rawBody.trim().length > 0 ? JSON.parse(rawBody) : {};
600
627
  if (!envelope || typeof envelope !== "object" || typeof envelope.name !== "string") throw new Error("missing procedure name");
601
628
  name = envelope.name;
602
629
  rawInput = envelope.input ?? {};
630
+ minTs = typeof envelope.minTs === "string" ? parseMinTs$1(envelope.minTs) : void 0;
603
631
  } catch {
604
632
  return this.send(res, 400, {
605
633
  success: false,
@@ -631,10 +659,11 @@ var HttpRpcHandler = class {
631
659
  });
632
660
  }
633
661
  try {
634
- const data = await this.runProcedure(procedure, input, auth.ctx);
662
+ const { data, ts } = await this.runProcedure(procedure, input, auth.ctx, minTs);
635
663
  return this.send(res, 200, {
636
664
  success: true,
637
- data
665
+ data,
666
+ ts: ts?.toString()
638
667
  });
639
668
  } catch (err) {
640
669
  logger$3.error(err, `RPC "${name}" failed`);
@@ -645,25 +674,32 @@ var HttpRpcHandler = class {
645
674
  });
646
675
  }
647
676
  }
648
- async runProcedure(procedure, input, serverCtx) {
677
+ async runProcedure(procedure, input, serverCtx, minTs) {
649
678
  if (procedure.type === "query") {
650
679
  const lastSnapshotTs = await this.deps.db.impl.getLatestSnapshotTimestamp();
651
- return runCachedQuery(this.deps.oneShotDeps, procedure, procedure.name, input, serverCtx, lastSnapshotTs);
680
+ return runCachedQuery(this.deps.oneShotDeps, procedure, procedure.name, input, serverCtx, lastSnapshotTs, minTs);
681
+ }
682
+ if (procedure.type === "mutation") {
683
+ const res = await this.deps.db.mutation(async (ctx) => {
684
+ const fn = procedure.fn;
685
+ return fn({
686
+ db: ctx,
687
+ serverCtx,
688
+ ...primaryOnlyConn
689
+ }, input);
690
+ });
691
+ return {
692
+ data: res.result,
693
+ ts: res.commitTs
694
+ };
652
695
  }
653
- if (procedure.type === "mutation") return (await this.deps.db.mutation(async (ctx) => {
654
- const fn = procedure.fn;
655
- return fn({
656
- db: ctx,
657
- serverCtx
658
- }, input);
659
- })).result;
660
696
  const fn = procedure.fn;
661
- return fn({
697
+ return { data: await fn({
662
698
  db: this.deps.db,
663
699
  serverCtx,
664
700
  storage: this.deps.storage,
665
701
  scheduler: this.deps.scheduler
666
- }, input);
702
+ }, input) };
667
703
  }
668
704
  async resolveAuth(req) {
669
705
  const data = (this.rpc.auth ?? defaultHttpAuth)(req);
@@ -711,6 +747,16 @@ var HttpRpcHandler = class {
711
747
  res.end(supaliveStringify(body));
712
748
  }
713
749
  };
750
+ /** Parse a wire `minTs` (decimal bigint string) to a bigint, or undefined when
751
+ * absent/malformed — a bad value falls back to primary-safe routing. */
752
+ function parseMinTs$1(minTs) {
753
+ if (minTs === void 0) return void 0;
754
+ try {
755
+ return BigInt(minTs);
756
+ } catch {
757
+ return;
758
+ }
759
+ }
714
760
  function normalizeRpcPath(p) {
715
761
  let s = p.startsWith("/") ? p : `/${p}`;
716
762
  if (s.length > 1 && s.endsWith("/")) s = s.slice(0, -1);
@@ -1127,7 +1173,7 @@ var SupaliveWebSocketServer = class {
1127
1173
  await this.handleAuth(session, msg.data);
1128
1174
  break;
1129
1175
  case "call":
1130
- await this.handleCall(session, msg.id, msg.procedure, msg.input);
1176
+ await this.handleCall(session, msg.id, msg.procedure, msg.input, parseMinTs(msg.minTs));
1131
1177
  break;
1132
1178
  case "subscribe":
1133
1179
  await this.handleSubscribe(session, msg.id, msg.procedure, msg.input);
@@ -1206,7 +1252,8 @@ var SupaliveWebSocketServer = class {
1206
1252
  const result = await this.db.mutation(async (ctx) => {
1207
1253
  return fn({
1208
1254
  db: ctx,
1209
- serverCtx: session.serverCtx
1255
+ serverCtx: session.serverCtx,
1256
+ ...primaryOnlyConn
1210
1257
  }, op.validatedInput);
1211
1258
  });
1212
1259
  op.resolve(result);
@@ -1218,7 +1265,7 @@ var SupaliveWebSocketServer = class {
1218
1265
  session.mutationInFlight = false;
1219
1266
  }
1220
1267
  }
1221
- async handleCall(session, id, procedureName, input) {
1268
+ async handleCall(session, id, procedureName, input, minTs) {
1222
1269
  if (!this.router) {
1223
1270
  this.sendError(session, id, "Router not registered", "SERVER_ERROR");
1224
1271
  return;
@@ -1232,12 +1279,13 @@ var SupaliveWebSocketServer = class {
1232
1279
  const validatedInput = procedure.inputSchema.parse(input);
1233
1280
  if (procedure.type === "query") {
1234
1281
  const lastSnapshotTs = await this.db.impl.getLatestSnapshotTimestamp();
1235
- const result = await runCachedQuery(this.oneShotDeps, procedure, procedureName, validatedInput, session.serverCtx, lastSnapshotTs);
1282
+ const { data, ts } = await runCachedQuery(this.oneShotDeps, procedure, procedureName, validatedInput, session.serverCtx, lastSnapshotTs, minTs);
1236
1283
  this.sendMessage(session, {
1237
1284
  type: "response",
1238
1285
  id,
1239
1286
  success: true,
1240
- data: result
1287
+ data,
1288
+ ts: ts.toString()
1241
1289
  });
1242
1290
  } else if (procedure.type === "mutation") {
1243
1291
  if (session.mutationQueue.length >= MAX_MUTATION_QUEUE_BUFFER) {
@@ -1249,7 +1297,8 @@ var SupaliveWebSocketServer = class {
1249
1297
  type: "response",
1250
1298
  id,
1251
1299
  success: true,
1252
- data: mutationResult.result
1300
+ data: mutationResult.result,
1301
+ ts: mutationResult.commitTs.toString()
1253
1302
  });
1254
1303
  } else if (procedure.type === "action") {
1255
1304
  if (session.actionInFlight >= MAX_ACTION_CONCURRENCY) {
@@ -1301,14 +1350,18 @@ var SupaliveWebSocketServer = class {
1301
1350
  let data;
1302
1351
  let dataHash;
1303
1352
  let liveResult = null;
1353
+ let servedTs;
1304
1354
  session.subIds.add(subId);
1305
1355
  if (existingServerSub) {
1306
1356
  existingServerSub.subscribedTimes += 1;
1307
1357
  const cached = await this.cache.getQueryCacheFor(existingServerSub.cacheKey);
1308
- if (cached) data = cached.data;
1309
- else {
1358
+ if (cached) {
1359
+ data = cached.data;
1360
+ servedTs = cached.lastSnapshotTs;
1361
+ } else {
1310
1362
  liveResult = await this.executeLiveQuery(procedureName, validatedInput, session.serverCtx);
1311
1363
  data = liveResult.data;
1364
+ servedTs = liveResult.ts;
1312
1365
  }
1313
1366
  } else {
1314
1367
  const lastSnapshotTs = await this.db.impl.getLatestSnapshotTimestamp();
@@ -1321,11 +1374,19 @@ var SupaliveWebSocketServer = class {
1321
1374
  instanceName: this.instanceId,
1322
1375
  lastSnapshotTs
1323
1376
  });
1324
- const cached = await this.cache.getQueryCacheFor(subManagerResult.cacheKey);
1325
- if (cached && !subManagerResult.recompute && cached.version === subManagerResult.version) data = cached.data;
1326
- else {
1377
+ let rebuild = true;
1378
+ if (!subManagerResult.recompute) {
1379
+ const cached = await this.cache.getQueryCacheFor(subManagerResult.cacheKey);
1380
+ if (cached && cached.version === subManagerResult.version) {
1381
+ data = cached.data;
1382
+ servedTs = cached.lastSnapshotTs;
1383
+ rebuild = false;
1384
+ }
1385
+ }
1386
+ if (rebuild) {
1327
1387
  liveResult = await this.executeLiveQuery(procedureName, validatedInput, session.serverCtx);
1328
1388
  data = liveResult.data;
1389
+ servedTs = liveResult.ts;
1329
1390
  }
1330
1391
  this.serverSubscriptions.set(subId, {
1331
1392
  subId,
@@ -1341,7 +1402,7 @@ var SupaliveWebSocketServer = class {
1341
1402
  lastSnapshotTs: liveResult.ts,
1342
1403
  readSet: liveResult.readSet,
1343
1404
  instanceName: this.instanceId
1344
- }), this.cache.setQueryCacheFor(cacheKey, data, String(liveResult.ts))]);
1405
+ }), this.cache.setQueryCacheFor(cacheKey, data, liveResult.ts)]);
1345
1406
  if (!updateResult.tracked) logger$2.warn(`updateSubscriptionReadSet returned tracked:false immediately after register for ${subId} — possible sub-manager restart mid-subscribe`);
1346
1407
  }
1347
1408
  dataHash = await getHashOf(data);
@@ -1355,7 +1416,8 @@ var SupaliveWebSocketServer = class {
1355
1416
  type: "sub:update",
1356
1417
  subId,
1357
1418
  data,
1358
- dataHash
1419
+ dataHash,
1420
+ ts: servedTs.toString()
1359
1421
  });
1360
1422
  logger$2.info(`Subscribed: ${subId} for session ${session.id}`);
1361
1423
  } catch (err) {
@@ -1588,18 +1650,20 @@ var SupaliveWebSocketServer = class {
1588
1650
  lastSnapshotTs: liveResult.ts,
1589
1651
  readSet: liveResult.readSet,
1590
1652
  instanceName: this.instanceId
1591
- }), this.cache.setQueryCacheFor(affected.cacheKey, liveResult.data, String(liveResult.ts))]);
1653
+ }), this.cache.setQueryCacheFor(affected.cacheKey, liveResult.data, liveResult.ts)]);
1592
1654
  if (!updateResult.tracked) {
1593
1655
  this.dropSubscriptionLocally(affected.subId, "no longer tracked by sub-manager");
1594
1656
  return;
1595
1657
  }
1658
+ const ts = liveResult.ts.toString();
1596
1659
  for (const [, session] of this.sessions) if (session.subIds.has(affected.subId)) this.sendMessage(session, {
1597
1660
  type: "sub:update",
1598
1661
  subId: affected.subId,
1599
1662
  data: liveResult.data,
1600
- dataHash
1663
+ dataHash,
1664
+ ts
1601
1665
  });
1602
- if (affected.notifyInstances.length > 0) await this.publishSubscriptionUpdate(affected.subId, liveResult.data, dataHash, affected.notifyInstances);
1666
+ if (affected.notifyInstances.length > 0) await this.publishSubscriptionUpdate(affected.subId, liveResult.data, dataHash, affected.notifyInstances, ts);
1603
1667
  logger$2.debug(`Re-executed affected query for subId: ${affected.subId}`);
1604
1668
  } catch (err) {
1605
1669
  logger$2.error(err, `Failed to re-execute query for subId ${affected.subId}`);
@@ -1612,7 +1676,8 @@ var SupaliveWebSocketServer = class {
1612
1676
  const fn = procedure.fn;
1613
1677
  return fn({
1614
1678
  db: ctx,
1615
- serverCtx
1679
+ serverCtx,
1680
+ ...primaryOnlyConn
1616
1681
  }, input);
1617
1682
  });
1618
1683
  }
@@ -1724,7 +1789,8 @@ var SupaliveWebSocketServer = class {
1724
1789
  type: "sub:update",
1725
1790
  subId: parsed.subId,
1726
1791
  data: parsed.data,
1727
- dataHash: parsed.dataHash
1792
+ dataHash: parsed.dataHash,
1793
+ ts: parsed.ts
1728
1794
  });
1729
1795
  }
1730
1796
  return;
@@ -1745,13 +1811,14 @@ var SupaliveWebSocketServer = class {
1745
1811
  notifyInstances: req.notifyInstances
1746
1812
  });
1747
1813
  }
1748
- async publishSubscriptionUpdate(subId, data, dataHash, notifyInstances) {
1814
+ async publishSubscriptionUpdate(subId, data, dataHash, notifyInstances, ts) {
1749
1815
  const payload = supaliveStringify({
1750
1816
  type: "update",
1751
1817
  subId,
1752
1818
  data,
1753
1819
  dataHash,
1754
- originInstance: this.instanceId
1820
+ originInstance: this.instanceId,
1821
+ ts
1755
1822
  });
1756
1823
  const publishPromises = notifyInstances.map(async (instance) => {
1757
1824
  if (instance === this.instanceId) return;
@@ -1765,6 +1832,16 @@ var SupaliveWebSocketServer = class {
1765
1832
  await Promise.all(publishPromises);
1766
1833
  }
1767
1834
  };
1835
+ /** Parse a wire `minTs` (decimal bigint string) to a bigint, or undefined when
1836
+ * absent/malformed — a bad value simply falls back to primary-safe routing. */
1837
+ function parseMinTs(minTs) {
1838
+ if (minTs === void 0) return void 0;
1839
+ try {
1840
+ return BigInt(minTs);
1841
+ } catch {
1842
+ return;
1843
+ }
1844
+ }
1768
1845
  /** Normalize a job path to a leading-slash, no-trailing-slash prefix. */
1769
1846
  function normalizeJobPath(p) {
1770
1847
  let s = p.startsWith("/") ? p : `/${p}`;