@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,259 @@
1
+ import { t as tableIdOf } from "./table-id-DyrJBKAQ.js";
2
+ import { a as utf8Decode, i as bytesEqual } from "./helper-zdJT5FUc.js";
3
+ import { a as jsonPathExtract, i as jsonPathExists, r as jsonContains } from "./query-Xw2hi6TB.js";
4
+ import { C as normalizeIdToBytes } from "./types_db-BsR02Sph.js";
5
+ //#region src/server/overlap-checker.ts
6
+ function mapReadEntryToRaw(entry) {
7
+ if (entry.kind === "point") return {
8
+ kind: "point",
9
+ tableId: tableIdOf(entry.table),
10
+ id: normalizeIdToBytes(entry.id),
11
+ table: entry.table
12
+ };
13
+ else return {
14
+ kind: "range",
15
+ tableId: tableIdOf(entry.table),
16
+ table: entry.table,
17
+ predicate: entry.predicate,
18
+ columns: entry.columns,
19
+ offset: entry.offset,
20
+ orderBy: entry.orderBy,
21
+ limit: entry.limit
22
+ };
23
+ }
24
+ function mapReadSetToRaw(readSet) {
25
+ return readSet.map(mapReadEntryToRaw);
26
+ }
27
+ function mapWriteEntryToRaw(entry) {
28
+ return {
29
+ table: entry.table,
30
+ tableId: entry.tableId,
31
+ id: normalizeIdToBytes(entry.id),
32
+ op: entry.op,
33
+ data: entry.data,
34
+ prevData: entry.prevData
35
+ };
36
+ }
37
+ function mapWriteSetToRaw(writeSet) {
38
+ return writeSet.map(mapWriteEntryToRaw);
39
+ }
40
+ function evaluatePredicateAgainstRow(rowData, predicate) {
41
+ switch (predicate.kind) {
42
+ case "and": {
43
+ let hasUnknown = false;
44
+ for (const child of predicate.children) {
45
+ const r = evaluatePredicateAgainstRow(rowData, child);
46
+ if (r === "no") return "no";
47
+ if (r === "unknown") hasUnknown = true;
48
+ }
49
+ return hasUnknown ? "unknown" : "yes";
50
+ }
51
+ case "or": {
52
+ let hasUnknown = false;
53
+ for (const child of predicate.children) {
54
+ const r = evaluatePredicateAgainstRow(rowData, child);
55
+ if (r === "yes") return "yes";
56
+ if (r === "unknown") hasUnknown = true;
57
+ }
58
+ return hasUnknown ? "unknown" : "no";
59
+ }
60
+ case "leaf": {
61
+ if (!(predicate.column in rowData)) return "unknown";
62
+ const val = rowData[predicate.column];
63
+ switch (predicate.operator) {
64
+ case "=": return val === predicate.value ? "yes" : "no";
65
+ case "!=": return val !== predicate.value ? "yes" : "no";
66
+ case ">": return val > predicate.value ? "yes" : "no";
67
+ case ">=": return val >= predicate.value ? "yes" : "no";
68
+ case "<": return val < predicate.value ? "yes" : "no";
69
+ case "<=": return val <= predicate.value ? "yes" : "no";
70
+ case "between": return val >= predicate.value && val <= predicate.value2 ? "yes" : "no";
71
+ case "in": return Array.isArray(predicate.value) && predicate.value.includes(val) ? "yes" : "no";
72
+ case "like":
73
+ case "contains":
74
+ case "ilike":
75
+ case "icontains": {
76
+ if (typeof val !== "string" || typeof predicate.value !== "string") return "no";
77
+ const haystack = predicate.operator === "ilike" || predicate.operator === "icontains" ? val.toLowerCase() : val;
78
+ const needle = predicate.operator === "ilike" || predicate.operator === "icontains" ? predicate.value.toLowerCase() : predicate.value;
79
+ const pattern = predicate.operator === "contains" || predicate.operator === "icontains" ? `%${needle}%` : needle;
80
+ return new RegExp("^" + pattern.replace(/%/g, ".*").replace(/_/g, ".") + "$").test(haystack) ? "yes" : "no";
81
+ }
82
+ case "jsonContains": {
83
+ const candidate = decodeJsonLeafValue(predicate);
84
+ const target = predicate.path !== void 0 ? jsonPathExtract(val, predicate.path) : val;
85
+ if (target === void 0) return "no";
86
+ return jsonContains(target, candidate) ? "yes" : "no";
87
+ }
88
+ case "jsonContainedBy": {
89
+ const candidate = decodeJsonLeafValue(predicate);
90
+ const target = predicate.path !== void 0 ? jsonPathExtract(val, predicate.path) : val;
91
+ if (target === void 0) return "no";
92
+ return jsonContains(candidate, target) ? "yes" : "no";
93
+ }
94
+ case "jsonHasKey": {
95
+ if (predicate.mode && Array.isArray(predicate.value)) {
96
+ const paths = predicate.value;
97
+ if (paths.length === 0) return "no";
98
+ return (predicate.mode === "all" ? paths.every((p) => jsonPathExists(val, p)) : paths.some((p) => jsonPathExists(val, p))) ? "yes" : "no";
99
+ }
100
+ if (predicate.raw) return jsonPathExists(val, predicate.value) ? "yes" : "no";
101
+ const key = predicate.value;
102
+ if (val === null || typeof val !== "object") return "no";
103
+ if (Array.isArray(val)) return val.includes(key) ? "yes" : "no";
104
+ return key in val ? "yes" : "no";
105
+ }
106
+ case "jsonArrayContains": {
107
+ const candidate = decodeJsonLeafValue(predicate);
108
+ const target = predicate.path !== void 0 ? jsonPathExtract(val, predicate.path) : val;
109
+ if (!Array.isArray(target)) return "no";
110
+ return target.some((el) => jsonContains(el, candidate)) ? "yes" : "no";
111
+ }
112
+ case "isNull": return val === null ? "yes" : "no";
113
+ case "isNotNull": return val !== null ? "yes" : "no";
114
+ default: return "unknown";
115
+ }
116
+ }
117
+ }
118
+ }
119
+ function decodeJsonLeafValue(leaf) {
120
+ if (!leaf.raw) return leaf.value;
121
+ if (typeof leaf.value !== "string") return leaf.value;
122
+ try {
123
+ return JSON.parse(leaf.value);
124
+ } catch {
125
+ return leaf.value;
126
+ }
127
+ }
128
+ function parseLogData(log) {
129
+ if (log._parsedData !== void 0) return log._parsedData;
130
+ const data = log.data.length > 0 ? JSON.parse(utf8Decode(log.data)) : null;
131
+ log._parsedData = data;
132
+ return data;
133
+ }
134
+ function checkLogsAffectReadSet(logs, rawReadSet) {
135
+ for (const log of logs) for (const rawRead of rawReadSet) {
136
+ if (!bytesEqual(log.tableId, rawRead.tableId)) continue;
137
+ if (rawRead.kind === "point" ? checkPointReadOverlap(log, rawRead) : checkRangeReadOverlap(log, rawRead)) return true;
138
+ }
139
+ return false;
140
+ }
141
+ /**
142
+ * Stateless freshness check shared by the subscription worker's `register` and
143
+ * the main server's one-shot query path. Decides whether a query's cached
144
+ * result (described by `metadata`) is still valid at `currentTs`.
145
+ *
146
+ * It is a pure function of Redis (`metadata`, `minRetainedTs`) + the DB
147
+ * commit-log probes (`hasCommitForPoints` for point-only tables,
148
+ * `getCommitLogsBetweenTs` for range tables) + the read set — it holds no
149
+ * worker-local state, so it is safe to run outside the sub-manager.
150
+ *
151
+ * `minRetainedTs` is the retention watermark: the oldest commit-log ts still
152
+ * retained after prune.
153
+ *
154
+ * Two ways a cache entry is deemed stale:
155
+ * 1. **Retention guard** — `metadata.lastSnapshotTs < minRetainedTs` (or the
156
+ * watermark is unknown). The between-ts scan would start below the oldest
157
+ * retained commit log, so it could silently miss pruned commits → we cannot
158
+ * prove freshness and must recompute. This is the case that a one-shot
159
+ * (untracked by any subscription, so nothing holds the retention watermark
160
+ * down) can hit.
161
+ * 2. **Overlap** — a commit in `(lastSnapshotTs, currentTs]` touched the read
162
+ * set.
163
+ */
164
+ async function evaluateCacheFreshness(db, metadata, currentTs, minRetainedTs) {
165
+ if (!metadata) return {
166
+ fresh: false,
167
+ readSet: [],
168
+ rawReadSet: []
169
+ };
170
+ const readSet = metadata.readSet;
171
+ const rawReadSet = mapReadSetToRaw(readSet);
172
+ if (minRetainedTs === null || metadata.lastSnapshotTs < minRetainedTs) return {
173
+ fresh: false,
174
+ readSet,
175
+ rawReadSet
176
+ };
177
+ if (rawReadSet.length === 0) return {
178
+ fresh: true,
179
+ readSet,
180
+ rawReadSet
181
+ };
182
+ const beginTs = metadata.lastSnapshotTs + 1n;
183
+ const endTs = currentTs + 1n;
184
+ const rangeTables = /* @__PURE__ */ new Map();
185
+ for (const r of rawReadSet) if (r.kind === "range") rangeTables.set(r.table, r.tableId);
186
+ const pointProbes = [];
187
+ for (const r of rawReadSet) if (r.kind === "point" && !rangeTables.has(r.table)) pointProbes.push({
188
+ tableId: r.tableId,
189
+ id: r.id
190
+ });
191
+ if (pointProbes.length > 0) {
192
+ if (await db.hasCommitForPoints(beginTs, endTs, pointProbes)) return {
193
+ fresh: false,
194
+ readSet,
195
+ rawReadSet
196
+ };
197
+ }
198
+ if (rangeTables.size > 0) {
199
+ if (checkLogsAffectReadSet(await db.getCommitLogsBetweenTs(beginTs, endTs, [...rangeTables.values()]), rawReadSet)) return {
200
+ fresh: false,
201
+ readSet,
202
+ rawReadSet
203
+ };
204
+ }
205
+ return {
206
+ fresh: true,
207
+ readSet,
208
+ rawReadSet
209
+ };
210
+ }
211
+ function checkPointReadOverlap(log, rawRead) {
212
+ return bytesEqual(log.id, rawRead.id);
213
+ }
214
+ function checkRangeReadOverlap(log, rawRead) {
215
+ const logData = parseLogData(log);
216
+ if (!logData) return true;
217
+ const op = logData.op ?? "update";
218
+ const data = logData.data;
219
+ const prevData = logData.prevData;
220
+ return checkRowWriteAffectsRange(op, prevData, data, rawRead.predicate);
221
+ }
222
+ function checkWriteSetAffectsReadSet(writeSet, rawReadSet) {
223
+ for (const write of writeSet) for (const rawRead of rawReadSet) {
224
+ if (!bytesEqual(write.tableId, rawRead.tableId)) continue;
225
+ if (checkWriteAffectsRawReadEntry(write, rawRead)) return true;
226
+ }
227
+ return false;
228
+ }
229
+ function checkWriteAffectsRawReadEntry(write, rawRead) {
230
+ if (rawRead.kind === "point") return bytesEqual(write.id, rawRead.id);
231
+ return checkRowWriteAffectsRange(write.op, write.prevData, write.data, rawRead.predicate);
232
+ }
233
+ /**
234
+ * Evaluate whether a write affects a range subscription, given the row's
235
+ * prior and post state. The write is considered affecting if EITHER side
236
+ * matched the predicate — that catches:
237
+ * - rows entering the result set (prev "no", post "yes")
238
+ * - rows leaving the result set (prev "yes", post "no")
239
+ * - rows changing while still in the set (prev "yes", post "yes")
240
+ * Skipped only if both sides are a definite "no".
241
+ *
242
+ * When prevData is missing (e.g. inserts, or commit logs predating this
243
+ * change), we fall back to conservative "unknown" on that side.
244
+ */
245
+ function checkRowWriteAffectsRange(op, prevData, data, predicate) {
246
+ const prevVerdict = prevData ? evaluatePredicateAgainstRow(prevData, predicate) : op === "insert" ? "no" : "unknown";
247
+ let postVerdict;
248
+ if (op === "delete") postVerdict = "no";
249
+ else if (data) postVerdict = evaluatePredicateAgainstRow(prevData ? {
250
+ ...prevData,
251
+ ...data
252
+ } : data, predicate);
253
+ else postVerdict = "unknown";
254
+ return prevVerdict !== "no" || postVerdict !== "no";
255
+ }
256
+ //#endregion
257
+ export { mapWriteSetToRaw as a, mapReadSetToRaw as i, checkWriteSetAffectsReadSet as n, evaluateCacheFreshness as r, checkLogsAffectReadSet as t };
258
+
259
+ //# sourceMappingURL=overlap-checker-DHUJe-Yy.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"overlap-checker-DHUJe-Yy.js","names":[],"sources":["../src/server/overlap-checker.ts"],"sourcesContent":["import type {\n ReadEntry, CommitLogEntry,\n Predicate, WriteEntry, WriteOp,\n RawReadEntry, RawPointRead, RawRangeRead, LeafPredicate,\n QueryCacheMetadata,\n} from \"../db/types_db\";\nimport { normalizeIdToBytes } from \"../db/types_db\";\nimport { bytesEqual, utf8Decode } from \"../db/bytes\";\nimport { tableIdOf } from \"../db/table-id\";\nimport { jsonContains, jsonPathExists, jsonPathExtract } from \"../db/query\";\nimport type { Database } from \"../db/db\";\n\nexport interface RawWriteEntry {\n table: string;\n tableId: Uint8Array;\n id: Uint8Array;\n op: WriteOp;\n data?: Record<string, unknown>;\n prevData?: Record<string, unknown>;\n}\n\nexport function mapReadEntryToRaw(entry: ReadEntry): RawReadEntry {\n // tableId is not carried on a stored/wire ReadEntry — it is `utf8(table)` and\n // is reconstructed here (memoized, interned) from the table name. This is the\n // single producer of RawReadEntry, so every consumer (freshness partition,\n // checkLogsAffectReadSet, the invalidator, the commit-time OCC scan) gets the\n // derived bytes regardless of the source.\n if (entry.kind === \"point\") {\n return {\n kind: \"point\",\n tableId: tableIdOf(entry.table),\n id: normalizeIdToBytes(entry.id),\n table: entry.table,\n };\n } else {\n return {\n kind: \"range\",\n tableId: tableIdOf(entry.table),\n table: entry.table,\n predicate: entry.predicate,\n columns: entry.columns,\n offset: entry.offset,\n orderBy: entry.orderBy,\n limit: entry.limit,\n };\n }\n}\n\nexport function mapReadSetToRaw(readSet: ReadEntry[]): RawReadEntry[] {\n return readSet.map(mapReadEntryToRaw);\n}\n\nexport function mapWriteEntryToRaw(entry: WriteEntry): RawWriteEntry {\n return {\n table: entry.table,\n tableId: entry.tableId,\n id: normalizeIdToBytes(entry.id),\n op: entry.op,\n data: entry.data,\n prevData: entry.prevData,\n };\n}\n\nexport function mapWriteSetToRaw(writeSet: WriteEntry[]): RawWriteEntry[] {\n return writeSet.map(mapWriteEntryToRaw);\n}\n\nexport type OverlapVerdict = \"yes\" | \"no\" | \"unknown\";\n\nexport function evaluatePredicateAgainstRow(\n rowData: Record<string, unknown>,\n predicate: Predicate\n): OverlapVerdict {\n switch (predicate.kind) {\n case \"and\": {\n let hasUnknown = false;\n for (const child of predicate.children) {\n const r = evaluatePredicateAgainstRow(rowData, child);\n if (r === \"no\") return \"no\";\n if (r === \"unknown\") hasUnknown = true;\n }\n return hasUnknown ? \"unknown\" : \"yes\";\n }\n case \"or\": {\n let hasUnknown = false;\n for (const child of predicate.children) {\n const r = evaluatePredicateAgainstRow(rowData, child);\n if (r === \"yes\") return \"yes\";\n if (r === \"unknown\") hasUnknown = true;\n }\n return hasUnknown ? \"unknown\" : \"no\";\n }\n case \"leaf\": {\n if (!(predicate.column in rowData)) return \"unknown\";\n const val = rowData[predicate.column];\n switch (predicate.operator) {\n case \"=\": return val === predicate.value ? \"yes\" : \"no\";\n case \"!=\": return val !== predicate.value ? \"yes\" : \"no\";\n case \">\": return (val as number) > (predicate.value as number) ? \"yes\" : \"no\";\n case \">=\": return (val as number) >= (predicate.value as number) ? \"yes\" : \"no\";\n case \"<\": return (val as number) < (predicate.value as number) ? \"yes\" : \"no\";\n case \"<=\": return (val as number) <= (predicate.value as number) ? \"yes\" : \"no\";\n case \"between\":\n return (val as number) >= (predicate.value as number) &&\n (val as number) <= (predicate.value2 as number) ? \"yes\" : \"no\";\n case \"in\":\n return Array.isArray(predicate.value) && (predicate.value as unknown[]).includes(val) ? \"yes\" : \"no\";\n case \"like\":\n case \"contains\":\n case \"ilike\":\n case \"icontains\": {\n if (typeof val !== \"string\" || typeof predicate.value !== \"string\") return \"no\";\n const haystack = (predicate.operator === \"ilike\" || predicate.operator === \"icontains\")\n ? val.toLowerCase() : val;\n const needle = (predicate.operator === \"ilike\" || predicate.operator === \"icontains\")\n ? (predicate.value as string).toLowerCase() : predicate.value as string;\n const pattern = (predicate.operator === \"contains\" || predicate.operator === \"icontains\")\n ? `%${needle}%` : needle;\n const regex = new RegExp(\"^\" + pattern.replace(/%/g, \".*\").replace(/_/g, \".\") + \"$\");\n return regex.test(haystack) ? \"yes\" : \"no\";\n }\n case \"jsonContains\": {\n const candidate = decodeJsonLeafValue(predicate);\n const target = predicate.path !== undefined ? jsonPathExtract(val, predicate.path) : val;\n if (target === undefined) return \"no\";\n return jsonContains(target, candidate) ? \"yes\" : \"no\";\n }\n case \"jsonContainedBy\": {\n const candidate = decodeJsonLeafValue(predicate);\n const target = predicate.path !== undefined ? jsonPathExtract(val, predicate.path) : val;\n if (target === undefined) return \"no\";\n return jsonContains(candidate, target) ? \"yes\" : \"no\";\n }\n case \"jsonHasKey\": {\n if (predicate.mode && Array.isArray(predicate.value)) {\n const paths = predicate.value as string[];\n if (paths.length === 0) return \"no\";\n const verdict = predicate.mode === \"all\"\n ? paths.every(p => jsonPathExists(val, p))\n : paths.some(p => jsonPathExists(val, p));\n return verdict ? \"yes\" : \"no\";\n }\n if (predicate.raw) {\n return jsonPathExists(val, predicate.value as string) ? \"yes\" : \"no\";\n }\n const key = predicate.value as string;\n if (val === null || typeof val !== \"object\") return \"no\";\n if (Array.isArray(val)) return (val as unknown[]).includes(key) ? \"yes\" : \"no\";\n return key in (val as object) ? \"yes\" : \"no\";\n }\n case \"jsonArrayContains\": {\n const candidate = decodeJsonLeafValue(predicate);\n const target = predicate.path !== undefined ? jsonPathExtract(val, predicate.path) : val;\n if (!Array.isArray(target)) return \"no\";\n return (target as unknown[]).some(el => jsonContains(el, candidate)) ? \"yes\" : \"no\";\n }\n case \"isNull\": return val === null ? \"yes\" : \"no\";\n case \"isNotNull\": return val !== null ? \"yes\" : \"no\";\n default: return \"unknown\";\n }\n }\n }\n}\n\nfunction decodeJsonLeafValue(leaf: LeafPredicate): unknown {\n if (!leaf.raw) return leaf.value;\n if (typeof leaf.value !== \"string\") return leaf.value;\n try { return JSON.parse(leaf.value); } catch { return leaf.value; }\n}\n\nfunction parseLogData(log: CommitLogEntry): Record<string, unknown> | null {\n if (log._parsedData !== undefined) {\n return log._parsedData;\n }\n const data = log.data.length > 0\n ? JSON.parse(utf8Decode(log.data)) as Record<string, unknown>\n : null;\n log._parsedData = data;\n return data;\n}\n\n\nexport function checkLogsAffectReadSet(\n logs: CommitLogEntry[],\n rawReadSet: RawReadEntry[]\n): boolean {\n for (const log of logs) {\n for (const rawRead of rawReadSet) {\n if (!bytesEqual(log.tableId, rawRead.tableId)) continue;\n\n const res = rawRead.kind === \"point\" ?\n checkPointReadOverlap(log, rawRead) :\n checkRangeReadOverlap(log, rawRead);\n\n if (res) {\n return true;\n }\n }\n }\n return false;\n}\n\nexport interface CacheFreshness {\n /** true → the cached data for this key is still valid as of `currentTs`. */\n fresh: boolean;\n /** The cached read set (empty when there is no usable metadata). */\n readSet: ReadEntry[];\n rawReadSet: RawReadEntry[];\n}\n\n/**\n * Stateless freshness check shared by the subscription worker's `register` and\n * the main server's one-shot query path. Decides whether a query's cached\n * result (described by `metadata`) is still valid at `currentTs`.\n *\n * It is a pure function of Redis (`metadata`, `minRetainedTs`) + the DB\n * commit-log probes (`hasCommitForPoints` for point-only tables,\n * `getCommitLogsBetweenTs` for range tables) + the read set — it holds no\n * worker-local state, so it is safe to run outside the sub-manager.\n *\n * `minRetainedTs` is the retention watermark: the oldest commit-log ts still\n * retained after prune.\n *\n * Two ways a cache entry is deemed stale:\n * 1. **Retention guard** — `metadata.lastSnapshotTs < minRetainedTs` (or the\n * watermark is unknown). The between-ts scan would start below the oldest\n * retained commit log, so it could silently miss pruned commits → we cannot\n * prove freshness and must recompute. This is the case that a one-shot\n * (untracked by any subscription, so nothing holds the retention watermark\n * down) can hit.\n * 2. **Overlap** — a commit in `(lastSnapshotTs, currentTs]` touched the read\n * set.\n */\nexport async function evaluateCacheFreshness(\n db: Pick<Database, \"getCommitLogsBetweenTs\" | \"hasCommitForPoints\">,\n metadata: QueryCacheMetadata | null,\n currentTs: bigint,\n minRetainedTs: bigint | null,\n): Promise<CacheFreshness> {\n if (!metadata) return { fresh: false, readSet: [], rawReadSet: [] };\n\n const readSet = metadata.readSet;\n const rawReadSet = mapReadSetToRaw(readSet);\n\n if (minRetainedTs === null || metadata.lastSnapshotTs < minRetainedTs) {\n return { fresh: false, readSet, rawReadSet };\n }\n\n // A query that read nothing can't be invalidated by any commit — permanently\n // fresh (until TTL eviction). Skip the commit-log scan entirely.\n if (rawReadSet.length === 0) return { fresh: true, readSet, rawReadSet };\n\n // Window (lastSnapshotTs, currentTs]: commits strictly AFTER the cached\n // snapshot (the data already reflects everything <= lastSnapshotTs) up to and\n // INCLUDING currentTs. The DB scans take a half-open [begin, end), so +1n on\n // both ends models that inclusive upper bound. Missing the upper bound would\n // drop a mutation committed at exactly currentTs — and a one-shot query has no\n // push-invalidation backstop, so it would serve stale.\n const beginTs = metadata.lastSnapshotTs + 1n;\n const endTs = currentTs + 1n;\n\n // Partition the read set by how precisely a commit-log check can be pushed to\n // the DB:\n // • range tables — a commit only matters if the row's prev/post state\n // satisfies the predicate, which only the in-app evaluator can decide, so\n // the row `data` for these tables must be fetched.\n // • point-only tables (carry no range read) — overlap is a pure\n // (table_id, id) key match, answerable by an existence probe with no blob\n // fetch or JSON parse.\n // A point read whose table ALSO carries a range read is subsumed by that\n // table's row fetch (checkLogsAffectReadSet re-checks it against those rows),\n // so it is intentionally excluded from the point probe.\n //\n // Grouping is keyed by the table NAME (`r.table`), not the tableId bytes: a\n // tableId is a fixed 16-byte digest of the name (tableNameToId), fully derived\n // from it — and unlike the byte array (which is freshly deserialized from the\n // Redis metadata here, so has no stable object identity) a string is a free\n // Map/Set key with no hashing or bytesEqual. The map VALUE still carries the\n // tableId bytes, which the SQL probes need.\n const rangeTables = new Map<string, Uint8Array>();\n for (const r of rawReadSet) {\n if (r.kind === \"range\") rangeTables.set(r.table, r.tableId);\n }\n\n const pointProbes: { tableId: Uint8Array; id: Uint8Array }[] = [];\n for (const r of rawReadSet) {\n if (r.kind === \"point\" && !rangeTables.has(r.table)) {\n pointProbes.push({ tableId: r.tableId, id: r.id });\n }\n }\n\n // Point-only tables: one existence probe on exact (table_id, id) pairs — no\n // data blobs fetched, no predicates evaluated.\n if (pointProbes.length > 0) {\n if (await db.hasCommitForPoints(beginTs, endTs, pointProbes)) {\n return { fresh: false, readSet, rawReadSet };\n }\n }\n\n // Range tables: fetch the window's logs for just those tables and evaluate\n // predicates (and any point reads subsumed onto them) in-app.\n if (rangeTables.size > 0) {\n const logs = await db.getCommitLogsBetweenTs(beginTs, endTs, [...rangeTables.values()]);\n if (checkLogsAffectReadSet(logs, rawReadSet)) {\n return { fresh: false, readSet, rawReadSet };\n }\n }\n\n return { fresh: true, readSet, rawReadSet };\n}\n\nfunction checkPointReadOverlap(\n log: CommitLogEntry,\n rawRead: RawPointRead,\n): boolean {\n return bytesEqual(log.id, rawRead.id);\n}\n\nfunction checkRangeReadOverlap(\n log: CommitLogEntry,\n rawRead: RawRangeRead\n): boolean {\n const logData = parseLogData(log);\n\n // No envelope at all → can't reason, must conflict-out.\n if (!logData) { \n return true;\n }\n\n // Commit logs serialize the full WriteEntry (see PgDatabase.insertCommitLogs),\n // so extract the row state from it. Same prev-OR-post check as the\n // in-memory invalidator path.\n const op = (logData.op as WriteOp | undefined) ?? \"update\";\n const data = logData.data as Record<string, unknown> | undefined;\n const prevData = logData.prevData as Record<string, unknown> | undefined;\n\n return checkRowWriteAffectsRange(op, prevData, data, rawRead.predicate);\n}\n\nexport function checkWriteSetAffectsReadSet(\n writeSet: RawWriteEntry[],\n rawReadSet: RawReadEntry[]\n): boolean {\n for (const write of writeSet) {\n for (const rawRead of rawReadSet) {\n if (!bytesEqual(write.tableId, rawRead.tableId)) continue;\n\n if (checkWriteAffectsRawReadEntry(write, rawRead)) {\n return true;\n }\n }\n }\n return false;\n}\n\nfunction checkWriteAffectsRawReadEntry(\n write: RawWriteEntry,\n rawRead: RawReadEntry\n): boolean {\n if (rawRead.kind === \"point\") {\n return bytesEqual(write.id, rawRead.id);\n }\n return checkRowWriteAffectsRange(\n write.op,\n write.prevData,\n write.data,\n rawRead.predicate,\n );\n}\n\n/**\n * Evaluate whether a write affects a range subscription, given the row's\n * prior and post state. The write is considered affecting if EITHER side\n * matched the predicate — that catches:\n * - rows entering the result set (prev \"no\", post \"yes\")\n * - rows leaving the result set (prev \"yes\", post \"no\")\n * - rows changing while still in the set (prev \"yes\", post \"yes\")\n * Skipped only if both sides are a definite \"no\".\n *\n * When prevData is missing (e.g. inserts, or commit logs predating this\n * change), we fall back to conservative \"unknown\" on that side.\n */\nfunction checkRowWriteAffectsRange(\n op: WriteOp,\n prevData: Record<string, unknown> | undefined,\n data: Record<string, unknown> | undefined,\n predicate: RawRangeRead[\"predicate\"],\n): boolean {\n const prevVerdict: OverlapVerdict = prevData\n ? evaluatePredicateAgainstRow(prevData, predicate)\n : (op === \"insert\" ? \"no\" : \"unknown\");\n\n let postVerdict: OverlapVerdict;\n if (op === \"delete\") {\n postVerdict = \"no\";\n } else if (data) {\n // Merge over prevData so unchanged columns carry over — otherwise a\n // partial update (e.g. {title: \"x\"}) would be \"unknown\" on every\n // predicate column it doesn't touch.\n const post = prevData ? { ...prevData, ...data } : data;\n postVerdict = evaluatePredicateAgainstRow(post, predicate);\n } else {\n postVerdict = \"unknown\";\n }\n\n return prevVerdict !== \"no\" || postVerdict !== \"no\";\n}"],"mappings":";;;;;AAqBA,SAAgB,kBAAkB,OAAgC;CAMhE,IAAI,MAAM,SAAS,SACjB,OAAO;EACL,MAAM;EACN,SAAS,UAAU,MAAM,KAAK;EAC9B,IAAI,mBAAmB,MAAM,EAAE;EAC/B,OAAO,MAAM;CACf;MAEA,OAAO;EACL,MAAM;EACN,SAAS,UAAU,MAAM,KAAK;EAC9B,OAAO,MAAM;EACb,WAAW,MAAM;EACjB,SAAS,MAAM;EACf,QAAQ,MAAM;EACd,SAAS,MAAM;EACf,OAAO,MAAM;CACf;AAEJ;AAEA,SAAgB,gBAAgB,SAAsC;CACpE,OAAO,QAAQ,IAAI,iBAAiB;AACtC;AAEA,SAAgB,mBAAmB,OAAkC;CACnE,OAAO;EACL,OAAO,MAAM;EACb,SAAS,MAAM;EACf,IAAI,mBAAmB,MAAM,EAAE;EAC/B,IAAI,MAAM;EACV,MAAM,MAAM;EACZ,UAAU,MAAM;CAClB;AACF;AAEA,SAAgB,iBAAiB,UAAyC;CACxE,OAAO,SAAS,IAAI,kBAAkB;AACxC;AAIA,SAAgB,4BACd,SACA,WACgB;CAChB,QAAQ,UAAU,MAAlB;EACE,KAAK,OAAO;GACV,IAAI,aAAa;GACjB,KAAK,MAAM,SAAS,UAAU,UAAU;IACtC,MAAM,IAAI,4BAA4B,SAAS,KAAK;IACpD,IAAI,MAAM,MAAM,OAAO;IACvB,IAAI,MAAM,WAAW,aAAa;GACpC;GACA,OAAO,aAAa,YAAY;EAClC;EACA,KAAK,MAAM;GACT,IAAI,aAAa;GACjB,KAAK,MAAM,SAAS,UAAU,UAAU;IACtC,MAAM,IAAI,4BAA4B,SAAS,KAAK;IACpD,IAAI,MAAM,OAAO,OAAO;IACxB,IAAI,MAAM,WAAW,aAAa;GACpC;GACA,OAAO,aAAa,YAAY;EAClC;EACA,KAAK,QAAQ;GACX,IAAI,EAAE,UAAU,UAAU,UAAU,OAAO;GAC3C,MAAM,MAAM,QAAQ,UAAU;GAC9B,QAAQ,UAAU,UAAlB;IACE,KAAK,KAAK,OAAO,QAAQ,UAAU,QAAQ,QAAQ;IACnD,KAAK,MAAM,OAAO,QAAQ,UAAU,QAAQ,QAAQ;IACpD,KAAK,KAAK,OAAQ,MAAkB,UAAU,QAAmB,QAAQ;IACzE,KAAK,MAAM,OAAQ,OAAmB,UAAU,QAAmB,QAAQ;IAC3E,KAAK,KAAK,OAAQ,MAAkB,UAAU,QAAmB,QAAQ;IACzE,KAAK,MAAM,OAAQ,OAAmB,UAAU,QAAmB,QAAQ;IAC3E,KAAK,WACH,OAAQ,OAAmB,UAAU,SAClC,OAAmB,UAAU,SAAoB,QAAQ;IAC9D,KAAK,MACH,OAAO,MAAM,QAAQ,UAAU,KAAK,KAAM,UAAU,MAAoB,SAAS,GAAG,IAAI,QAAQ;IAClG,KAAK;IACL,KAAK;IACL,KAAK;IACL,KAAK,aAAa;KAChB,IAAI,OAAO,QAAQ,YAAY,OAAO,UAAU,UAAU,UAAU,OAAO;KAC3E,MAAM,WAAY,UAAU,aAAa,WAAW,UAAU,aAAa,cACvE,IAAI,YAAY,IAAI;KACxB,MAAM,SAAU,UAAU,aAAa,WAAW,UAAU,aAAa,cACpE,UAAU,MAAiB,YAAY,IAAI,UAAU;KAC1D,MAAM,UAAW,UAAU,aAAa,cAAc,UAAU,aAAa,cACzE,IAAI,OAAO,KAAK;KAEpB,OAAO,IADW,OAAO,MAAM,QAAQ,QAAQ,MAAM,IAAI,CAAC,CAAC,QAAQ,MAAM,GAAG,IAAI,GACrE,CAAC,CAAC,KAAK,QAAQ,IAAI,QAAQ;IACxC;IACA,KAAK,gBAAgB;KACnB,MAAM,YAAY,oBAAoB,SAAS;KAC/C,MAAM,SAAS,UAAU,SAAS,KAAA,IAAY,gBAAgB,KAAK,UAAU,IAAI,IAAI;KACrF,IAAI,WAAW,KAAA,GAAW,OAAO;KACjC,OAAO,aAAa,QAAQ,SAAS,IAAI,QAAQ;IACnD;IACA,KAAK,mBAAmB;KACtB,MAAM,YAAY,oBAAoB,SAAS;KAC/C,MAAM,SAAS,UAAU,SAAS,KAAA,IAAY,gBAAgB,KAAK,UAAU,IAAI,IAAI;KACrF,IAAI,WAAW,KAAA,GAAW,OAAO;KACjC,OAAO,aAAa,WAAW,MAAM,IAAI,QAAQ;IACnD;IACA,KAAK,cAAc;KACjB,IAAI,UAAU,QAAQ,MAAM,QAAQ,UAAU,KAAK,GAAG;MACpD,MAAM,QAAQ,UAAU;MACxB,IAAI,MAAM,WAAW,GAAG,OAAO;MAI/B,QAHgB,UAAU,SAAS,QAC/B,MAAM,OAAM,MAAK,eAAe,KAAK,CAAC,CAAC,IACvC,MAAM,MAAK,MAAK,eAAe,KAAK,CAAC,CAAC,KACzB,QAAQ;KAC3B;KACA,IAAI,UAAU,KACZ,OAAO,eAAe,KAAK,UAAU,KAAe,IAAI,QAAQ;KAElE,MAAM,MAAM,UAAU;KACtB,IAAI,QAAQ,QAAQ,OAAO,QAAQ,UAAU,OAAO;KACpD,IAAI,MAAM,QAAQ,GAAG,GAAG,OAAQ,IAAkB,SAAS,GAAG,IAAI,QAAQ;KAC1E,OAAO,OAAQ,MAAiB,QAAQ;IAC1C;IACA,KAAK,qBAAqB;KACxB,MAAM,YAAY,oBAAoB,SAAS;KAC/C,MAAM,SAAS,UAAU,SAAS,KAAA,IAAY,gBAAgB,KAAK,UAAU,IAAI,IAAI;KACrF,IAAI,CAAC,MAAM,QAAQ,MAAM,GAAG,OAAO;KACnC,OAAQ,OAAqB,MAAK,OAAM,aAAa,IAAI,SAAS,CAAC,IAAI,QAAQ;IACjF;IACA,KAAK,UAAU,OAAO,QAAQ,OAAO,QAAQ;IAC7C,KAAK,aAAa,OAAO,QAAQ,OAAO,QAAQ;IAChD,SAAS,OAAO;GAClB;EACF;CACF;AACF;AAEA,SAAS,oBAAoB,MAA8B;CACzD,IAAI,CAAC,KAAK,KAAK,OAAO,KAAK;CAC3B,IAAI,OAAO,KAAK,UAAU,UAAU,OAAO,KAAK;CAChD,IAAI;EAAE,OAAO,KAAK,MAAM,KAAK,KAAK;CAAG,QAAQ;EAAE,OAAO,KAAK;CAAO;AACpE;AAEA,SAAS,aAAa,KAAqD;CACzE,IAAI,IAAI,gBAAgB,KAAA,GACtB,OAAO,IAAI;CAEb,MAAM,OAAO,IAAI,KAAK,SAAS,IAC3B,KAAK,MAAM,WAAW,IAAI,IAAI,CAAC,IAC/B;CACJ,IAAI,cAAc;CAClB,OAAO;AACT;AAGA,SAAgB,uBACd,MACA,YACS;CACT,KAAK,MAAM,OAAO,MAChB,KAAK,MAAM,WAAW,YAAY;EAChC,IAAI,CAAC,WAAW,IAAI,SAAS,QAAQ,OAAO,GAAG;EAM/C,IAJY,QAAQ,SAAS,UAC3B,sBAAsB,KAAK,OAAO,IAClC,sBAAsB,KAAK,OAAO,GAGlC,OAAO;CAEX;CAEF,OAAO;AACT;;;;;;;;;;;;;;;;;;;;;;;;AAiCA,eAAsB,uBACpB,IACA,UACA,WACA,eACyB;CACzB,IAAI,CAAC,UAAU,OAAO;EAAE,OAAO;EAAO,SAAS,CAAC;EAAG,YAAY,CAAC;CAAE;CAElE,MAAM,UAAU,SAAS;CACzB,MAAM,aAAa,gBAAgB,OAAO;CAE1C,IAAI,kBAAkB,QAAQ,SAAS,iBAAiB,eACtD,OAAO;EAAE,OAAO;EAAO;EAAS;CAAW;CAK7C,IAAI,WAAW,WAAW,GAAG,OAAO;EAAE,OAAO;EAAM;EAAS;CAAW;CAQvE,MAAM,UAAU,SAAS,iBAAiB;CAC1C,MAAM,QAAQ,YAAY;CAoB1B,MAAM,8BAAc,IAAI,IAAwB;CAChD,KAAK,MAAM,KAAK,YACd,IAAI,EAAE,SAAS,SAAS,YAAY,IAAI,EAAE,OAAO,EAAE,OAAO;CAG5D,MAAM,cAAyD,CAAC;CAChE,KAAK,MAAM,KAAK,YACd,IAAI,EAAE,SAAS,WAAW,CAAC,YAAY,IAAI,EAAE,KAAK,GAChD,YAAY,KAAK;EAAE,SAAS,EAAE;EAAS,IAAI,EAAE;CAAG,CAAC;CAMrD,IAAI,YAAY,SAAS;MACnB,MAAM,GAAG,mBAAmB,SAAS,OAAO,WAAW,GACzD,OAAO;GAAE,OAAO;GAAO;GAAS;EAAW;CAAA;CAM/C,IAAI,YAAY,OAAO;MAEjB,uBAAuB,MADR,GAAG,uBAAuB,SAAS,OAAO,CAAC,GAAG,YAAY,OAAO,CAAC,CAAC,GACrD,UAAU,GACzC,OAAO;GAAE,OAAO;GAAO;GAAS;EAAW;CAAA;CAI/C,OAAO;EAAE,OAAO;EAAM;EAAS;CAAW;AAC5C;AAEA,SAAS,sBACP,KACA,SACS;CACT,OAAO,WAAW,IAAI,IAAI,QAAQ,EAAE;AACtC;AAEA,SAAS,sBACP,KACA,SACS;CACT,MAAM,UAAU,aAAa,GAAG;CAGhC,IAAI,CAAC,SACH,OAAO;CAMT,MAAM,KAAM,QAAQ,MAA8B;CAClD,MAAM,OAAO,QAAQ;CACrB,MAAM,WAAW,QAAQ;CAEzB,OAAO,0BAA0B,IAAI,UAAU,MAAM,QAAQ,SAAS;AACxE;AAEA,SAAgB,4BACd,UACA,YACS;CACT,KAAK,MAAM,SAAS,UAClB,KAAK,MAAM,WAAW,YAAY;EAChC,IAAI,CAAC,WAAW,MAAM,SAAS,QAAQ,OAAO,GAAG;EAEjD,IAAI,8BAA8B,OAAO,OAAO,GAC9C,OAAO;CAEX;CAEF,OAAO;AACT;AAEA,SAAS,8BACP,OACA,SACS;CACT,IAAI,QAAQ,SAAS,SACnB,OAAO,WAAW,MAAM,IAAI,QAAQ,EAAE;CAExC,OAAO,0BACL,MAAM,IACN,MAAM,UACN,MAAM,MACN,QAAQ,SACV;AACF;;;;;;;;;;;;;AAcA,SAAS,0BACP,IACA,UACA,MACA,WACS;CACT,MAAM,cAA8B,WAChC,4BAA4B,UAAU,SAAS,IAC9C,OAAO,WAAW,OAAO;CAE9B,IAAI;CACJ,IAAI,OAAO,UACT,cAAc;MACT,IAAI,MAKT,cAAc,4BADD,WAAW;EAAE,GAAG;EAAU,GAAG;CAAK,IAAI,MACH,SAAS;MAEzD,cAAc;CAGhB,OAAO,gBAAgB,QAAQ,gBAAgB;AACjD"}
@@ -0,0 +1,259 @@
1
+ import { t as tableIdOf } from "./table-id-DyrJBKAQ.js";
2
+ import { a as utf8Decode, i as bytesEqual } from "./helper-zdJT5FUc.js";
3
+ import { a as jsonPathExtract, i as jsonPathExists, r as jsonContains } from "./query-Xw2hi6TB.js";
4
+ import { C as normalizeIdToBytes } from "./types_db-CgGlOMmr.js";
5
+ //#region src/server/overlap-checker.ts
6
+ function mapReadEntryToRaw(entry) {
7
+ if (entry.kind === "point") return {
8
+ kind: "point",
9
+ tableId: tableIdOf(entry.table),
10
+ id: normalizeIdToBytes(entry.id),
11
+ table: entry.table
12
+ };
13
+ else return {
14
+ kind: "range",
15
+ tableId: tableIdOf(entry.table),
16
+ table: entry.table,
17
+ predicate: entry.predicate,
18
+ columns: entry.columns,
19
+ offset: entry.offset,
20
+ orderBy: entry.orderBy,
21
+ limit: entry.limit
22
+ };
23
+ }
24
+ function mapReadSetToRaw(readSet) {
25
+ return readSet.map(mapReadEntryToRaw);
26
+ }
27
+ function mapWriteEntryToRaw(entry) {
28
+ return {
29
+ table: entry.table,
30
+ tableId: entry.tableId,
31
+ id: normalizeIdToBytes(entry.id),
32
+ op: entry.op,
33
+ data: entry.data,
34
+ prevData: entry.prevData
35
+ };
36
+ }
37
+ function mapWriteSetToRaw(writeSet) {
38
+ return writeSet.map(mapWriteEntryToRaw);
39
+ }
40
+ function evaluatePredicateAgainstRow(rowData, predicate) {
41
+ switch (predicate.kind) {
42
+ case "and": {
43
+ let hasUnknown = false;
44
+ for (const child of predicate.children) {
45
+ const r = evaluatePredicateAgainstRow(rowData, child);
46
+ if (r === "no") return "no";
47
+ if (r === "unknown") hasUnknown = true;
48
+ }
49
+ return hasUnknown ? "unknown" : "yes";
50
+ }
51
+ case "or": {
52
+ let hasUnknown = false;
53
+ for (const child of predicate.children) {
54
+ const r = evaluatePredicateAgainstRow(rowData, child);
55
+ if (r === "yes") return "yes";
56
+ if (r === "unknown") hasUnknown = true;
57
+ }
58
+ return hasUnknown ? "unknown" : "no";
59
+ }
60
+ case "leaf": {
61
+ if (!(predicate.column in rowData)) return "unknown";
62
+ const val = rowData[predicate.column];
63
+ switch (predicate.operator) {
64
+ case "=": return val === predicate.value ? "yes" : "no";
65
+ case "!=": return val !== predicate.value ? "yes" : "no";
66
+ case ">": return val > predicate.value ? "yes" : "no";
67
+ case ">=": return val >= predicate.value ? "yes" : "no";
68
+ case "<": return val < predicate.value ? "yes" : "no";
69
+ case "<=": return val <= predicate.value ? "yes" : "no";
70
+ case "between": return val >= predicate.value && val <= predicate.value2 ? "yes" : "no";
71
+ case "in": return Array.isArray(predicate.value) && predicate.value.includes(val) ? "yes" : "no";
72
+ case "like":
73
+ case "contains":
74
+ case "ilike":
75
+ case "icontains": {
76
+ if (typeof val !== "string" || typeof predicate.value !== "string") return "no";
77
+ const haystack = predicate.operator === "ilike" || predicate.operator === "icontains" ? val.toLowerCase() : val;
78
+ const needle = predicate.operator === "ilike" || predicate.operator === "icontains" ? predicate.value.toLowerCase() : predicate.value;
79
+ const pattern = predicate.operator === "contains" || predicate.operator === "icontains" ? `%${needle}%` : needle;
80
+ return new RegExp("^" + pattern.replace(/%/g, ".*").replace(/_/g, ".") + "$").test(haystack) ? "yes" : "no";
81
+ }
82
+ case "jsonContains": {
83
+ const candidate = decodeJsonLeafValue(predicate);
84
+ const target = predicate.path !== void 0 ? jsonPathExtract(val, predicate.path) : val;
85
+ if (target === void 0) return "no";
86
+ return jsonContains(target, candidate) ? "yes" : "no";
87
+ }
88
+ case "jsonContainedBy": {
89
+ const candidate = decodeJsonLeafValue(predicate);
90
+ const target = predicate.path !== void 0 ? jsonPathExtract(val, predicate.path) : val;
91
+ if (target === void 0) return "no";
92
+ return jsonContains(candidate, target) ? "yes" : "no";
93
+ }
94
+ case "jsonHasKey": {
95
+ if (predicate.mode && Array.isArray(predicate.value)) {
96
+ const paths = predicate.value;
97
+ if (paths.length === 0) return "no";
98
+ return (predicate.mode === "all" ? paths.every((p) => jsonPathExists(val, p)) : paths.some((p) => jsonPathExists(val, p))) ? "yes" : "no";
99
+ }
100
+ if (predicate.raw) return jsonPathExists(val, predicate.value) ? "yes" : "no";
101
+ const key = predicate.value;
102
+ if (val === null || typeof val !== "object") return "no";
103
+ if (Array.isArray(val)) return val.includes(key) ? "yes" : "no";
104
+ return key in val ? "yes" : "no";
105
+ }
106
+ case "jsonArrayContains": {
107
+ const candidate = decodeJsonLeafValue(predicate);
108
+ const target = predicate.path !== void 0 ? jsonPathExtract(val, predicate.path) : val;
109
+ if (!Array.isArray(target)) return "no";
110
+ return target.some((el) => jsonContains(el, candidate)) ? "yes" : "no";
111
+ }
112
+ case "isNull": return val === null ? "yes" : "no";
113
+ case "isNotNull": return val !== null ? "yes" : "no";
114
+ default: return "unknown";
115
+ }
116
+ }
117
+ }
118
+ }
119
+ function decodeJsonLeafValue(leaf) {
120
+ if (!leaf.raw) return leaf.value;
121
+ if (typeof leaf.value !== "string") return leaf.value;
122
+ try {
123
+ return JSON.parse(leaf.value);
124
+ } catch {
125
+ return leaf.value;
126
+ }
127
+ }
128
+ function parseLogData(log) {
129
+ if (log._parsedData !== void 0) return log._parsedData;
130
+ const data = log.data.length > 0 ? JSON.parse(utf8Decode(log.data)) : null;
131
+ log._parsedData = data;
132
+ return data;
133
+ }
134
+ function checkLogsAffectReadSet(logs, rawReadSet) {
135
+ for (const log of logs) for (const rawRead of rawReadSet) {
136
+ if (!bytesEqual(log.tableId, rawRead.tableId)) continue;
137
+ if (rawRead.kind === "point" ? checkPointReadOverlap(log, rawRead) : checkRangeReadOverlap(log, rawRead)) return true;
138
+ }
139
+ return false;
140
+ }
141
+ /**
142
+ * Stateless freshness check shared by the subscription worker's `register` and
143
+ * the main server's one-shot query path. Decides whether a query's cached
144
+ * result (described by `metadata`) is still valid at `currentTs`.
145
+ *
146
+ * It is a pure function of Redis (`metadata`, `minRetainedTs`) + the DB
147
+ * commit-log probes (`hasCommitForPoints` for point-only tables,
148
+ * `getCommitLogsBetweenTs` for range tables) + the read set — it holds no
149
+ * worker-local state, so it is safe to run outside the sub-manager.
150
+ *
151
+ * `minRetainedTs` is the retention watermark: the oldest commit-log ts still
152
+ * retained after prune.
153
+ *
154
+ * Two ways a cache entry is deemed stale:
155
+ * 1. **Retention guard** — `metadata.lastSnapshotTs < minRetainedTs` (or the
156
+ * watermark is unknown). The between-ts scan would start below the oldest
157
+ * retained commit log, so it could silently miss pruned commits → we cannot
158
+ * prove freshness and must recompute. This is the case that a one-shot
159
+ * (untracked by any subscription, so nothing holds the retention watermark
160
+ * down) can hit.
161
+ * 2. **Overlap** — a commit in `(lastSnapshotTs, currentTs]` touched the read
162
+ * set.
163
+ */
164
+ async function evaluateCacheFreshness(db, metadata, currentTs, minRetainedTs) {
165
+ if (!metadata) return {
166
+ fresh: false,
167
+ readSet: [],
168
+ rawReadSet: []
169
+ };
170
+ const readSet = metadata.readSet;
171
+ const rawReadSet = mapReadSetToRaw(readSet);
172
+ if (minRetainedTs === null || metadata.lastSnapshotTs < minRetainedTs) return {
173
+ fresh: false,
174
+ readSet,
175
+ rawReadSet
176
+ };
177
+ if (rawReadSet.length === 0) return {
178
+ fresh: true,
179
+ readSet,
180
+ rawReadSet
181
+ };
182
+ const beginTs = metadata.lastSnapshotTs + 1n;
183
+ const endTs = currentTs + 1n;
184
+ const rangeTables = /* @__PURE__ */ new Map();
185
+ for (const r of rawReadSet) if (r.kind === "range") rangeTables.set(r.table, r.tableId);
186
+ const pointProbes = [];
187
+ for (const r of rawReadSet) if (r.kind === "point" && !rangeTables.has(r.table)) pointProbes.push({
188
+ tableId: r.tableId,
189
+ id: r.id
190
+ });
191
+ if (pointProbes.length > 0) {
192
+ if (await db.hasCommitForPoints(beginTs, endTs, pointProbes)) return {
193
+ fresh: false,
194
+ readSet,
195
+ rawReadSet
196
+ };
197
+ }
198
+ if (rangeTables.size > 0) {
199
+ if (checkLogsAffectReadSet(await db.getCommitLogsBetweenTs(beginTs, endTs, [...rangeTables.values()]), rawReadSet)) return {
200
+ fresh: false,
201
+ readSet,
202
+ rawReadSet
203
+ };
204
+ }
205
+ return {
206
+ fresh: true,
207
+ readSet,
208
+ rawReadSet
209
+ };
210
+ }
211
+ function checkPointReadOverlap(log, rawRead) {
212
+ return bytesEqual(log.id, rawRead.id);
213
+ }
214
+ function checkRangeReadOverlap(log, rawRead) {
215
+ const logData = parseLogData(log);
216
+ if (!logData) return true;
217
+ const op = logData.op ?? "update";
218
+ const data = logData.data;
219
+ const prevData = logData.prevData;
220
+ return checkRowWriteAffectsRange(op, prevData, data, rawRead.predicate);
221
+ }
222
+ function checkWriteSetAffectsReadSet(writeSet, rawReadSet) {
223
+ for (const write of writeSet) for (const rawRead of rawReadSet) {
224
+ if (!bytesEqual(write.tableId, rawRead.tableId)) continue;
225
+ if (checkWriteAffectsRawReadEntry(write, rawRead)) return true;
226
+ }
227
+ return false;
228
+ }
229
+ function checkWriteAffectsRawReadEntry(write, rawRead) {
230
+ if (rawRead.kind === "point") return bytesEqual(write.id, rawRead.id);
231
+ return checkRowWriteAffectsRange(write.op, write.prevData, write.data, rawRead.predicate);
232
+ }
233
+ /**
234
+ * Evaluate whether a write affects a range subscription, given the row's
235
+ * prior and post state. The write is considered affecting if EITHER side
236
+ * matched the predicate — that catches:
237
+ * - rows entering the result set (prev "no", post "yes")
238
+ * - rows leaving the result set (prev "yes", post "no")
239
+ * - rows changing while still in the set (prev "yes", post "yes")
240
+ * Skipped only if both sides are a definite "no".
241
+ *
242
+ * When prevData is missing (e.g. inserts, or commit logs predating this
243
+ * change), we fall back to conservative "unknown" on that side.
244
+ */
245
+ function checkRowWriteAffectsRange(op, prevData, data, predicate) {
246
+ const prevVerdict = prevData ? evaluatePredicateAgainstRow(prevData, predicate) : op === "insert" ? "no" : "unknown";
247
+ let postVerdict;
248
+ if (op === "delete") postVerdict = "no";
249
+ else if (data) postVerdict = evaluatePredicateAgainstRow(prevData ? {
250
+ ...prevData,
251
+ ...data
252
+ } : data, predicate);
253
+ else postVerdict = "unknown";
254
+ return prevVerdict !== "no" || postVerdict !== "no";
255
+ }
256
+ //#endregion
257
+ export { mapWriteSetToRaw as a, mapReadSetToRaw as i, checkWriteSetAffectsReadSet as n, evaluateCacheFreshness as r, checkLogsAffectReadSet as t };
258
+
259
+ //# sourceMappingURL=overlap-checker-DWeQLpSD.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"overlap-checker-DWeQLpSD.js","names":[],"sources":["../src/server/overlap-checker.ts"],"sourcesContent":["import type {\n ReadEntry, CommitLogEntry,\n Predicate, WriteEntry, WriteOp,\n RawReadEntry, RawPointRead, RawRangeRead, LeafPredicate,\n QueryCacheMetadata,\n} from \"../db/types_db\";\nimport { normalizeIdToBytes } from \"../db/types_db\";\nimport { bytesEqual, utf8Decode } from \"../db/bytes\";\nimport { tableIdOf } from \"../db/table-id\";\nimport { jsonContains, jsonPathExists, jsonPathExtract } from \"../db/query\";\nimport type { Database } from \"../db/db\";\n\nexport interface RawWriteEntry {\n table: string;\n tableId: Uint8Array;\n id: Uint8Array;\n op: WriteOp;\n data?: Record<string, unknown>;\n prevData?: Record<string, unknown>;\n}\n\nexport function mapReadEntryToRaw(entry: ReadEntry): RawReadEntry {\n // tableId is not carried on a stored/wire ReadEntry — it is `utf8(table)` and\n // is reconstructed here (memoized, interned) from the table name. This is the\n // single producer of RawReadEntry, so every consumer (freshness partition,\n // checkLogsAffectReadSet, the invalidator, the commit-time OCC scan) gets the\n // derived bytes regardless of the source.\n if (entry.kind === \"point\") {\n return {\n kind: \"point\",\n tableId: tableIdOf(entry.table),\n id: normalizeIdToBytes(entry.id),\n table: entry.table,\n };\n } else {\n return {\n kind: \"range\",\n tableId: tableIdOf(entry.table),\n table: entry.table,\n predicate: entry.predicate,\n columns: entry.columns,\n offset: entry.offset,\n orderBy: entry.orderBy,\n limit: entry.limit,\n };\n }\n}\n\nexport function mapReadSetToRaw(readSet: ReadEntry[]): RawReadEntry[] {\n return readSet.map(mapReadEntryToRaw);\n}\n\nexport function mapWriteEntryToRaw(entry: WriteEntry): RawWriteEntry {\n return {\n table: entry.table,\n tableId: entry.tableId,\n id: normalizeIdToBytes(entry.id),\n op: entry.op,\n data: entry.data,\n prevData: entry.prevData,\n };\n}\n\nexport function mapWriteSetToRaw(writeSet: WriteEntry[]): RawWriteEntry[] {\n return writeSet.map(mapWriteEntryToRaw);\n}\n\nexport type OverlapVerdict = \"yes\" | \"no\" | \"unknown\";\n\nexport function evaluatePredicateAgainstRow(\n rowData: Record<string, unknown>,\n predicate: Predicate\n): OverlapVerdict {\n switch (predicate.kind) {\n case \"and\": {\n let hasUnknown = false;\n for (const child of predicate.children) {\n const r = evaluatePredicateAgainstRow(rowData, child);\n if (r === \"no\") return \"no\";\n if (r === \"unknown\") hasUnknown = true;\n }\n return hasUnknown ? \"unknown\" : \"yes\";\n }\n case \"or\": {\n let hasUnknown = false;\n for (const child of predicate.children) {\n const r = evaluatePredicateAgainstRow(rowData, child);\n if (r === \"yes\") return \"yes\";\n if (r === \"unknown\") hasUnknown = true;\n }\n return hasUnknown ? \"unknown\" : \"no\";\n }\n case \"leaf\": {\n if (!(predicate.column in rowData)) return \"unknown\";\n const val = rowData[predicate.column];\n switch (predicate.operator) {\n case \"=\": return val === predicate.value ? \"yes\" : \"no\";\n case \"!=\": return val !== predicate.value ? \"yes\" : \"no\";\n case \">\": return (val as number) > (predicate.value as number) ? \"yes\" : \"no\";\n case \">=\": return (val as number) >= (predicate.value as number) ? \"yes\" : \"no\";\n case \"<\": return (val as number) < (predicate.value as number) ? \"yes\" : \"no\";\n case \"<=\": return (val as number) <= (predicate.value as number) ? \"yes\" : \"no\";\n case \"between\":\n return (val as number) >= (predicate.value as number) &&\n (val as number) <= (predicate.value2 as number) ? \"yes\" : \"no\";\n case \"in\":\n return Array.isArray(predicate.value) && (predicate.value as unknown[]).includes(val) ? \"yes\" : \"no\";\n case \"like\":\n case \"contains\":\n case \"ilike\":\n case \"icontains\": {\n if (typeof val !== \"string\" || typeof predicate.value !== \"string\") return \"no\";\n const haystack = (predicate.operator === \"ilike\" || predicate.operator === \"icontains\")\n ? val.toLowerCase() : val;\n const needle = (predicate.operator === \"ilike\" || predicate.operator === \"icontains\")\n ? (predicate.value as string).toLowerCase() : predicate.value as string;\n const pattern = (predicate.operator === \"contains\" || predicate.operator === \"icontains\")\n ? `%${needle}%` : needle;\n const regex = new RegExp(\"^\" + pattern.replace(/%/g, \".*\").replace(/_/g, \".\") + \"$\");\n return regex.test(haystack) ? \"yes\" : \"no\";\n }\n case \"jsonContains\": {\n const candidate = decodeJsonLeafValue(predicate);\n const target = predicate.path !== undefined ? jsonPathExtract(val, predicate.path) : val;\n if (target === undefined) return \"no\";\n return jsonContains(target, candidate) ? \"yes\" : \"no\";\n }\n case \"jsonContainedBy\": {\n const candidate = decodeJsonLeafValue(predicate);\n const target = predicate.path !== undefined ? jsonPathExtract(val, predicate.path) : val;\n if (target === undefined) return \"no\";\n return jsonContains(candidate, target) ? \"yes\" : \"no\";\n }\n case \"jsonHasKey\": {\n if (predicate.mode && Array.isArray(predicate.value)) {\n const paths = predicate.value as string[];\n if (paths.length === 0) return \"no\";\n const verdict = predicate.mode === \"all\"\n ? paths.every(p => jsonPathExists(val, p))\n : paths.some(p => jsonPathExists(val, p));\n return verdict ? \"yes\" : \"no\";\n }\n if (predicate.raw) {\n return jsonPathExists(val, predicate.value as string) ? \"yes\" : \"no\";\n }\n const key = predicate.value as string;\n if (val === null || typeof val !== \"object\") return \"no\";\n if (Array.isArray(val)) return (val as unknown[]).includes(key) ? \"yes\" : \"no\";\n return key in (val as object) ? \"yes\" : \"no\";\n }\n case \"jsonArrayContains\": {\n const candidate = decodeJsonLeafValue(predicate);\n const target = predicate.path !== undefined ? jsonPathExtract(val, predicate.path) : val;\n if (!Array.isArray(target)) return \"no\";\n return (target as unknown[]).some(el => jsonContains(el, candidate)) ? \"yes\" : \"no\";\n }\n case \"isNull\": return val === null ? \"yes\" : \"no\";\n case \"isNotNull\": return val !== null ? \"yes\" : \"no\";\n default: return \"unknown\";\n }\n }\n }\n}\n\nfunction decodeJsonLeafValue(leaf: LeafPredicate): unknown {\n if (!leaf.raw) return leaf.value;\n if (typeof leaf.value !== \"string\") return leaf.value;\n try { return JSON.parse(leaf.value); } catch { return leaf.value; }\n}\n\nfunction parseLogData(log: CommitLogEntry): Record<string, unknown> | null {\n if (log._parsedData !== undefined) {\n return log._parsedData;\n }\n const data = log.data.length > 0\n ? JSON.parse(utf8Decode(log.data)) as Record<string, unknown>\n : null;\n log._parsedData = data;\n return data;\n}\n\n\nexport function checkLogsAffectReadSet(\n logs: CommitLogEntry[],\n rawReadSet: RawReadEntry[]\n): boolean {\n for (const log of logs) {\n for (const rawRead of rawReadSet) {\n if (!bytesEqual(log.tableId, rawRead.tableId)) continue;\n\n const res = rawRead.kind === \"point\" ?\n checkPointReadOverlap(log, rawRead) :\n checkRangeReadOverlap(log, rawRead);\n\n if (res) {\n return true;\n }\n }\n }\n return false;\n}\n\nexport interface CacheFreshness {\n /** true → the cached data for this key is still valid as of `currentTs`. */\n fresh: boolean;\n /** The cached read set (empty when there is no usable metadata). */\n readSet: ReadEntry[];\n rawReadSet: RawReadEntry[];\n}\n\n/**\n * Stateless freshness check shared by the subscription worker's `register` and\n * the main server's one-shot query path. Decides whether a query's cached\n * result (described by `metadata`) is still valid at `currentTs`.\n *\n * It is a pure function of Redis (`metadata`, `minRetainedTs`) + the DB\n * commit-log probes (`hasCommitForPoints` for point-only tables,\n * `getCommitLogsBetweenTs` for range tables) + the read set — it holds no\n * worker-local state, so it is safe to run outside the sub-manager.\n *\n * `minRetainedTs` is the retention watermark: the oldest commit-log ts still\n * retained after prune.\n *\n * Two ways a cache entry is deemed stale:\n * 1. **Retention guard** — `metadata.lastSnapshotTs < minRetainedTs` (or the\n * watermark is unknown). The between-ts scan would start below the oldest\n * retained commit log, so it could silently miss pruned commits → we cannot\n * prove freshness and must recompute. This is the case that a one-shot\n * (untracked by any subscription, so nothing holds the retention watermark\n * down) can hit.\n * 2. **Overlap** — a commit in `(lastSnapshotTs, currentTs]` touched the read\n * set.\n */\nexport async function evaluateCacheFreshness(\n db: Pick<Database, \"getCommitLogsBetweenTs\" | \"hasCommitForPoints\">,\n metadata: QueryCacheMetadata | null,\n currentTs: bigint,\n minRetainedTs: bigint | null,\n): Promise<CacheFreshness> {\n if (!metadata) return { fresh: false, readSet: [], rawReadSet: [] };\n\n const readSet = metadata.readSet;\n const rawReadSet = mapReadSetToRaw(readSet);\n\n if (minRetainedTs === null || metadata.lastSnapshotTs < minRetainedTs) {\n return { fresh: false, readSet, rawReadSet };\n }\n\n // A query that read nothing can't be invalidated by any commit — permanently\n // fresh (until TTL eviction). Skip the commit-log scan entirely.\n if (rawReadSet.length === 0) return { fresh: true, readSet, rawReadSet };\n\n // Window (lastSnapshotTs, currentTs]: commits strictly AFTER the cached\n // snapshot (the data already reflects everything <= lastSnapshotTs) up to and\n // INCLUDING currentTs. The DB scans take a half-open [begin, end), so +1n on\n // both ends models that inclusive upper bound. Missing the upper bound would\n // drop a mutation committed at exactly currentTs — and a one-shot query has no\n // push-invalidation backstop, so it would serve stale.\n const beginTs = metadata.lastSnapshotTs + 1n;\n const endTs = currentTs + 1n;\n\n // Partition the read set by how precisely a commit-log check can be pushed to\n // the DB:\n // • range tables — a commit only matters if the row's prev/post state\n // satisfies the predicate, which only the in-app evaluator can decide, so\n // the row `data` for these tables must be fetched.\n // • point-only tables (carry no range read) — overlap is a pure\n // (table_id, id) key match, answerable by an existence probe with no blob\n // fetch or JSON parse.\n // A point read whose table ALSO carries a range read is subsumed by that\n // table's row fetch (checkLogsAffectReadSet re-checks it against those rows),\n // so it is intentionally excluded from the point probe.\n //\n // Grouping is keyed by the table NAME (`r.table`), not the tableId bytes: a\n // tableId is a fixed 16-byte digest of the name (tableNameToId), fully derived\n // from it — and unlike the byte array (which is freshly deserialized from the\n // Redis metadata here, so has no stable object identity) a string is a free\n // Map/Set key with no hashing or bytesEqual. The map VALUE still carries the\n // tableId bytes, which the SQL probes need.\n const rangeTables = new Map<string, Uint8Array>();\n for (const r of rawReadSet) {\n if (r.kind === \"range\") rangeTables.set(r.table, r.tableId);\n }\n\n const pointProbes: { tableId: Uint8Array; id: Uint8Array }[] = [];\n for (const r of rawReadSet) {\n if (r.kind === \"point\" && !rangeTables.has(r.table)) {\n pointProbes.push({ tableId: r.tableId, id: r.id });\n }\n }\n\n // Point-only tables: one existence probe on exact (table_id, id) pairs — no\n // data blobs fetched, no predicates evaluated.\n if (pointProbes.length > 0) {\n if (await db.hasCommitForPoints(beginTs, endTs, pointProbes)) {\n return { fresh: false, readSet, rawReadSet };\n }\n }\n\n // Range tables: fetch the window's logs for just those tables and evaluate\n // predicates (and any point reads subsumed onto them) in-app.\n if (rangeTables.size > 0) {\n const logs = await db.getCommitLogsBetweenTs(beginTs, endTs, [...rangeTables.values()]);\n if (checkLogsAffectReadSet(logs, rawReadSet)) {\n return { fresh: false, readSet, rawReadSet };\n }\n }\n\n return { fresh: true, readSet, rawReadSet };\n}\n\nfunction checkPointReadOverlap(\n log: CommitLogEntry,\n rawRead: RawPointRead,\n): boolean {\n return bytesEqual(log.id, rawRead.id);\n}\n\nfunction checkRangeReadOverlap(\n log: CommitLogEntry,\n rawRead: RawRangeRead\n): boolean {\n const logData = parseLogData(log);\n\n // No envelope at all → can't reason, must conflict-out.\n if (!logData) { \n return true;\n }\n\n // Commit logs serialize the full WriteEntry (see PgDatabase.insertCommitLogs),\n // so extract the row state from it. Same prev-OR-post check as the\n // in-memory invalidator path.\n const op = (logData.op as WriteOp | undefined) ?? \"update\";\n const data = logData.data as Record<string, unknown> | undefined;\n const prevData = logData.prevData as Record<string, unknown> | undefined;\n\n return checkRowWriteAffectsRange(op, prevData, data, rawRead.predicate);\n}\n\nexport function checkWriteSetAffectsReadSet(\n writeSet: RawWriteEntry[],\n rawReadSet: RawReadEntry[]\n): boolean {\n for (const write of writeSet) {\n for (const rawRead of rawReadSet) {\n if (!bytesEqual(write.tableId, rawRead.tableId)) continue;\n\n if (checkWriteAffectsRawReadEntry(write, rawRead)) {\n return true;\n }\n }\n }\n return false;\n}\n\nfunction checkWriteAffectsRawReadEntry(\n write: RawWriteEntry,\n rawRead: RawReadEntry\n): boolean {\n if (rawRead.kind === \"point\") {\n return bytesEqual(write.id, rawRead.id);\n }\n return checkRowWriteAffectsRange(\n write.op,\n write.prevData,\n write.data,\n rawRead.predicate,\n );\n}\n\n/**\n * Evaluate whether a write affects a range subscription, given the row's\n * prior and post state. The write is considered affecting if EITHER side\n * matched the predicate — that catches:\n * - rows entering the result set (prev \"no\", post \"yes\")\n * - rows leaving the result set (prev \"yes\", post \"no\")\n * - rows changing while still in the set (prev \"yes\", post \"yes\")\n * Skipped only if both sides are a definite \"no\".\n *\n * When prevData is missing (e.g. inserts, or commit logs predating this\n * change), we fall back to conservative \"unknown\" on that side.\n */\nfunction checkRowWriteAffectsRange(\n op: WriteOp,\n prevData: Record<string, unknown> | undefined,\n data: Record<string, unknown> | undefined,\n predicate: RawRangeRead[\"predicate\"],\n): boolean {\n const prevVerdict: OverlapVerdict = prevData\n ? evaluatePredicateAgainstRow(prevData, predicate)\n : (op === \"insert\" ? \"no\" : \"unknown\");\n\n let postVerdict: OverlapVerdict;\n if (op === \"delete\") {\n postVerdict = \"no\";\n } else if (data) {\n // Merge over prevData so unchanged columns carry over — otherwise a\n // partial update (e.g. {title: \"x\"}) would be \"unknown\" on every\n // predicate column it doesn't touch.\n const post = prevData ? { ...prevData, ...data } : data;\n postVerdict = evaluatePredicateAgainstRow(post, predicate);\n } else {\n postVerdict = \"unknown\";\n }\n\n return prevVerdict !== \"no\" || postVerdict !== \"no\";\n}"],"mappings":"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"}