@minnowdb/core 0.2.1 → 0.4.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 (270) hide show
  1. package/README.md +23 -32
  2. package/dist/block-format/block.d.ts +32 -13
  3. package/dist/block-format/block.js +179 -55
  4. package/dist/block-format/checksum.d.ts +2 -1
  5. package/dist/block-format/checksum.js +5 -2
  6. package/dist/block-format/codecs.d.ts +7 -2
  7. package/dist/block-format/codecs.js +53 -12
  8. package/dist/block-format/column.d.ts +3 -2
  9. package/dist/block-format/column.js +96 -30
  10. package/dist/block-format/index.d.ts +2 -2
  11. package/dist/block-format/index.js +2 -2
  12. package/dist/block-format/physical.d.ts +8 -2
  13. package/dist/block-format/physical.js +14 -6
  14. package/dist/block-format/types.d.ts +7 -3
  15. package/dist/block-format/types.js +0 -1
  16. package/dist/block-format/unicode.d.ts +11 -0
  17. package/dist/block-format/unicode.js +46 -0
  18. package/dist/date-value.d.ts +15 -0
  19. package/dist/date-value.js +64 -0
  20. package/dist/engine/artifact-cache.d.ts +2 -1
  21. package/dist/engine/artifact-cache.js +5 -1
  22. package/dist/engine/batch.d.ts +11 -3
  23. package/dist/engine/batch.js +17 -7
  24. package/dist/engine/buffered-writer.d.ts +2 -1
  25. package/dist/engine/buffered-writer.js +34 -9
  26. package/dist/engine/cache-limits.d.ts +25 -0
  27. package/dist/engine/cache-limits.js +25 -0
  28. package/dist/engine/catalog.d.ts +24 -5
  29. package/dist/engine/catalog.js +34 -7
  30. package/dist/engine/client.d.ts +29 -7
  31. package/dist/engine/client.js +216 -40
  32. package/dist/engine/database.d.ts +224 -39
  33. package/dist/engine/database.js +7475 -1539
  34. package/dist/engine/defaults.d.ts +4 -8
  35. package/dist/engine/defaults.js +46 -22
  36. package/dist/engine/errors.d.ts +19 -1
  37. package/dist/engine/errors.js +32 -2
  38. package/dist/engine/fts.d.ts +0 -6
  39. package/dist/engine/fts.js +41 -14
  40. package/dist/engine/group-index.d.ts +0 -1
  41. package/dist/engine/group-index.js +6 -11
  42. package/dist/engine/index.d.ts +16 -10
  43. package/dist/engine/index.js +12 -9
  44. package/dist/engine/join-index.d.ts +0 -1
  45. package/dist/engine/join-index.js +2 -2
  46. package/dist/engine/keyed-live.d.ts +57 -0
  47. package/dist/engine/keyed-live.js +226 -0
  48. package/dist/engine/live-api.d.ts +4 -0
  49. package/dist/engine/live-api.js +4 -0
  50. package/dist/engine/live.d.ts +43 -26
  51. package/dist/engine/live.js +504 -126
  52. package/dist/engine/memory.d.ts +2 -1
  53. package/dist/engine/memory.js +3 -2
  54. package/dist/engine/optimizer.d.ts +0 -1
  55. package/dist/engine/optimizer.js +262 -27
  56. package/dist/engine/query-api.d.ts +3 -0
  57. package/dist/engine/query-api.js +3 -0
  58. package/dist/engine/query-cache.d.ts +1 -2
  59. package/dist/engine/query-cache.js +7 -5
  60. package/dist/engine/query.d.ts +134 -29
  61. package/dist/engine/query.js +1668 -372
  62. package/dist/engine/result-wire.d.ts +0 -1
  63. package/dist/engine/result-wire.js +2 -2
  64. package/dist/engine/schema-wire.d.ts +6 -3
  65. package/dist/engine/schema-wire.js +40 -34
  66. package/dist/engine/schema.d.ts +156 -80
  67. package/dist/engine/schema.js +570 -108
  68. package/dist/engine/sort-keys.d.ts +4 -4
  69. package/dist/engine/sort-keys.js +44 -29
  70. package/dist/engine/sql-domains.d.ts +31 -0
  71. package/dist/engine/sql-domains.js +585 -0
  72. package/dist/engine/sql-driver.d.ts +19 -0
  73. package/dist/engine/sql-driver.js +1 -0
  74. package/dist/engine/sql-json.d.ts +7 -9
  75. package/dist/engine/sql-json.js +75 -15
  76. package/dist/engine/sql-semantics.d.ts +8 -3
  77. package/dist/engine/sql-semantics.js +458 -30
  78. package/dist/engine/typed-live.d.ts +67 -0
  79. package/dist/engine/typed-live.js +349 -0
  80. package/dist/engine/vector.d.ts +12 -2
  81. package/dist/engine/vector.js +635 -119
  82. package/dist/engine/worker-host.d.ts +9 -2
  83. package/dist/engine/worker-host.js +620 -152
  84. package/dist/engine/worker.d.ts +0 -1
  85. package/dist/engine/worker.js +3 -3
  86. package/dist/engine/write-block-planner.d.ts +18 -0
  87. package/dist/engine/write-block-planner.js +112 -0
  88. package/dist/index.d.ts +0 -1
  89. package/dist/index.js +0 -1
  90. package/dist/plan/index.d.ts +3 -4
  91. package/dist/plan/index.js +3 -4
  92. package/dist/storage/index.d.ts +1 -1
  93. package/dist/storage/index.js +1 -1
  94. package/dist/storage/indexeddb.d.ts +99 -51
  95. package/dist/storage/indexeddb.js +13448 -2660
  96. package/dist/storage/memory.d.ts +64 -57
  97. package/dist/storage/memory.js +1042 -128
  98. package/dist/storage/opfs/files.d.ts +18 -7
  99. package/dist/storage/opfs/files.js +115 -28
  100. package/dist/storage/opfs/index.d.ts +1 -1
  101. package/dist/storage/opfs/index.js +1 -1
  102. package/dist/storage/opfs/leader.d.ts +409 -75
  103. package/dist/storage/opfs/leader.js +4621 -650
  104. package/dist/storage/opfs/rpc.d.ts +15 -3
  105. package/dist/storage/opfs/rpc.js +224 -2
  106. package/dist/storage/opfs/snapshot-ledger.d.ts +40 -0
  107. package/dist/storage/opfs/snapshot-ledger.js +281 -0
  108. package/dist/storage/opfs/store.d.ts +33 -99
  109. package/dist/storage/opfs/store.js +453 -364
  110. package/dist/storage/persistence.d.ts +37 -0
  111. package/dist/storage/persistence.js +78 -0
  112. package/dist/storage/snapshot-stream.d.ts +24 -0
  113. package/dist/storage/snapshot-stream.js +897 -0
  114. package/dist/storage/snapshot.d.ts +9 -85
  115. package/dist/storage/snapshot.js +33 -265
  116. package/dist/storage/toolkit/extents.d.ts +41 -7
  117. package/dist/storage/toolkit/extents.js +402 -39
  118. package/dist/storage/toolkit/index.d.ts +4 -5
  119. package/dist/storage/toolkit/index.js +28 -4
  120. package/dist/storage/toolkit/record-core.d.ts +182 -55
  121. package/dist/storage/toolkit/record-core.js +6158 -1331
  122. package/dist/storage/toolkit/sync-file.d.ts +10 -1
  123. package/dist/storage/toolkit/sync-file.js +50 -2
  124. package/dist/storage/toolkit/wal.d.ts +23 -5
  125. package/dist/storage/toolkit/wal.js +89 -27
  126. package/dist/storage/toolkit/wire.d.ts +15 -2
  127. package/dist/storage/toolkit/wire.js +199 -10
  128. package/dist/storage/types.d.ts +1439 -204
  129. package/dist/storage/types.js +1745 -141
  130. package/dist/testing/block-store-conformance.d.ts +0 -1
  131. package/dist/testing/block-store-conformance.js +1009 -119
  132. package/dist/testing/index.d.ts +46 -26
  133. package/dist/testing/index.js +112 -57
  134. package/dist/testing/opfs-shim.d.ts +13 -3
  135. package/dist/testing/opfs-shim.js +40 -6
  136. package/dist/testing/simulator.d.ts +97 -0
  137. package/dist/testing/simulator.js +591 -0
  138. package/dist/testing/sqllogictest.d.ts +89 -0
  139. package/dist/testing/sqllogictest.js +434 -0
  140. package/dist/transactions/index.d.ts +92 -16
  141. package/dist/transactions/index.js +1074 -236
  142. package/dist/worker-protocol/index.d.ts +3 -2
  143. package/dist/worker-protocol/index.js +2 -5
  144. package/package.json +53 -3
  145. package/postgres-feature-profile.json +223 -0
  146. package/sql-feature-matrix.json +172 -252
  147. package/dist/block-format/block.d.ts.map +0 -1
  148. package/dist/block-format/block.js.map +0 -1
  149. package/dist/block-format/checksum.d.ts.map +0 -1
  150. package/dist/block-format/checksum.js.map +0 -1
  151. package/dist/block-format/codecs.d.ts.map +0 -1
  152. package/dist/block-format/codecs.js.map +0 -1
  153. package/dist/block-format/column.d.ts.map +0 -1
  154. package/dist/block-format/column.js.map +0 -1
  155. package/dist/block-format/index.d.ts.map +0 -1
  156. package/dist/block-format/index.js.map +0 -1
  157. package/dist/block-format/physical.d.ts.map +0 -1
  158. package/dist/block-format/physical.js.map +0 -1
  159. package/dist/block-format/types.d.ts.map +0 -1
  160. package/dist/block-format/types.js.map +0 -1
  161. package/dist/engine/artifact-cache.d.ts.map +0 -1
  162. package/dist/engine/artifact-cache.js.map +0 -1
  163. package/dist/engine/batch.d.ts.map +0 -1
  164. package/dist/engine/batch.js.map +0 -1
  165. package/dist/engine/buffered-writer.d.ts.map +0 -1
  166. package/dist/engine/buffered-writer.js.map +0 -1
  167. package/dist/engine/catalog.d.ts.map +0 -1
  168. package/dist/engine/catalog.js.map +0 -1
  169. package/dist/engine/client.d.ts.map +0 -1
  170. package/dist/engine/client.js.map +0 -1
  171. package/dist/engine/coordinator.d.ts +0 -17
  172. package/dist/engine/coordinator.d.ts.map +0 -1
  173. package/dist/engine/coordinator.js +0 -60
  174. package/dist/engine/coordinator.js.map +0 -1
  175. package/dist/engine/database.d.ts.map +0 -1
  176. package/dist/engine/database.js.map +0 -1
  177. package/dist/engine/defaults.d.ts.map +0 -1
  178. package/dist/engine/defaults.js.map +0 -1
  179. package/dist/engine/errors.d.ts.map +0 -1
  180. package/dist/engine/errors.js.map +0 -1
  181. package/dist/engine/fts.d.ts.map +0 -1
  182. package/dist/engine/fts.js.map +0 -1
  183. package/dist/engine/group-index.d.ts.map +0 -1
  184. package/dist/engine/group-index.js.map +0 -1
  185. package/dist/engine/index.d.ts.map +0 -1
  186. package/dist/engine/index.js.map +0 -1
  187. package/dist/engine/join-index.d.ts.map +0 -1
  188. package/dist/engine/join-index.js.map +0 -1
  189. package/dist/engine/live.d.ts.map +0 -1
  190. package/dist/engine/live.js.map +0 -1
  191. package/dist/engine/memory.d.ts.map +0 -1
  192. package/dist/engine/memory.js.map +0 -1
  193. package/dist/engine/optimizer.d.ts.map +0 -1
  194. package/dist/engine/optimizer.js.map +0 -1
  195. package/dist/engine/query-cache.d.ts.map +0 -1
  196. package/dist/engine/query-cache.js.map +0 -1
  197. package/dist/engine/query.d.ts.map +0 -1
  198. package/dist/engine/query.js.map +0 -1
  199. package/dist/engine/result-wire.d.ts.map +0 -1
  200. package/dist/engine/result-wire.js.map +0 -1
  201. package/dist/engine/schema-wire.d.ts.map +0 -1
  202. package/dist/engine/schema-wire.js.map +0 -1
  203. package/dist/engine/schema.d.ts.map +0 -1
  204. package/dist/engine/schema.js.map +0 -1
  205. package/dist/engine/sort-keys.d.ts.map +0 -1
  206. package/dist/engine/sort-keys.js.map +0 -1
  207. package/dist/engine/sql-json.d.ts.map +0 -1
  208. package/dist/engine/sql-json.js.map +0 -1
  209. package/dist/engine/sql-semantics.d.ts.map +0 -1
  210. package/dist/engine/sql-semantics.js.map +0 -1
  211. package/dist/engine/vector.d.ts.map +0 -1
  212. package/dist/engine/vector.js.map +0 -1
  213. package/dist/engine/worker-host.d.ts.map +0 -1
  214. package/dist/engine/worker-host.js.map +0 -1
  215. package/dist/engine/worker.d.ts.map +0 -1
  216. package/dist/engine/worker.js.map +0 -1
  217. package/dist/index.d.ts.map +0 -1
  218. package/dist/index.js.map +0 -1
  219. package/dist/plan/index.d.ts.map +0 -1
  220. package/dist/plan/index.js.map +0 -1
  221. package/dist/storage/fixture-shape.d.ts +0 -42
  222. package/dist/storage/fixture-shape.d.ts.map +0 -1
  223. package/dist/storage/fixture-shape.js +0 -146
  224. package/dist/storage/fixture-shape.js.map +0 -1
  225. package/dist/storage/index.d.ts.map +0 -1
  226. package/dist/storage/index.js.map +0 -1
  227. package/dist/storage/indexeddb.d.ts.map +0 -1
  228. package/dist/storage/indexeddb.js.map +0 -1
  229. package/dist/storage/memory.d.ts.map +0 -1
  230. package/dist/storage/memory.js.map +0 -1
  231. package/dist/storage/opfs/files.d.ts.map +0 -1
  232. package/dist/storage/opfs/files.js.map +0 -1
  233. package/dist/storage/opfs/index.d.ts.map +0 -1
  234. package/dist/storage/opfs/index.js.map +0 -1
  235. package/dist/storage/opfs/leader.d.ts.map +0 -1
  236. package/dist/storage/opfs/leader.js.map +0 -1
  237. package/dist/storage/opfs/rpc.d.ts.map +0 -1
  238. package/dist/storage/opfs/rpc.js.map +0 -1
  239. package/dist/storage/opfs/store.d.ts.map +0 -1
  240. package/dist/storage/opfs/store.js.map +0 -1
  241. package/dist/storage/snapshot.d.ts.map +0 -1
  242. package/dist/storage/snapshot.js.map +0 -1
  243. package/dist/storage/toolkit/extents.d.ts.map +0 -1
  244. package/dist/storage/toolkit/extents.js.map +0 -1
  245. package/dist/storage/toolkit/index.d.ts.map +0 -1
  246. package/dist/storage/toolkit/index.js.map +0 -1
  247. package/dist/storage/toolkit/record-core.d.ts.map +0 -1
  248. package/dist/storage/toolkit/record-core.js.map +0 -1
  249. package/dist/storage/toolkit/sync-file.d.ts.map +0 -1
  250. package/dist/storage/toolkit/sync-file.js.map +0 -1
  251. package/dist/storage/toolkit/wal.d.ts.map +0 -1
  252. package/dist/storage/toolkit/wal.js.map +0 -1
  253. package/dist/storage/toolkit/wire.d.ts.map +0 -1
  254. package/dist/storage/toolkit/wire.js.map +0 -1
  255. package/dist/storage/types.d.ts.map +0 -1
  256. package/dist/storage/types.js.map +0 -1
  257. package/dist/testing/block-store-conformance.d.ts.map +0 -1
  258. package/dist/testing/block-store-conformance.js.map +0 -1
  259. package/dist/testing/index.d.ts.map +0 -1
  260. package/dist/testing/index.js.map +0 -1
  261. package/dist/testing/opfs-shim.d.ts.map +0 -1
  262. package/dist/testing/opfs-shim.js.map +0 -1
  263. package/dist/testing/seeds.d.ts +0 -11
  264. package/dist/testing/seeds.d.ts.map +0 -1
  265. package/dist/testing/seeds.js +0 -50
  266. package/dist/testing/seeds.js.map +0 -1
  267. package/dist/transactions/index.d.ts.map +0 -1
  268. package/dist/transactions/index.js.map +0 -1
  269. package/dist/worker-protocol/index.d.ts.map +0 -1
  270. package/dist/worker-protocol/index.js.map +0 -1
@@ -1,13 +1,22 @@
1
- /** Companion to LiveQuerySet#stillOpen for the per-subscription flag. */
2
- function isSubscriptionClosed(subscription) {
3
- return subscription.closed;
1
+ export const DEFAULT_LIVE_QUERY_MAX_GROUPS = 256;
2
+ export const DEFAULT_LIVE_QUERY_MAX_SUBSCRIPTIONS = 1_024;
3
+ export const MAX_LIVE_QUERY_GROUPS = 4_096;
4
+ export const MAX_LIVE_QUERY_SUBSCRIPTIONS = 16_384;
5
+ export const MAX_LIVE_QUERY_SETS_PER_DATABASE = 256;
6
+ export class LiveQueryLimitError extends Error {
7
+ resource;
8
+ limit;
9
+ name = "LiveQueryLimitError";
10
+ constructor(resource, limit) {
11
+ super(resource === "set"
12
+ ? `A database cannot retain more than ${String(limit)} live-query sets`
13
+ : `A live-query set cannot retain more than ${String(limit)} ${resource} records`);
14
+ this.resource = resource;
15
+ this.limit = limit;
16
+ }
4
17
  }
5
18
  const digestScratch = new DataView(new ArrayBuffer(8));
6
- /**
7
- * A 32-bit FNV-1a digest of a result, mixed from typed values: a number's eight bytes, a
8
- * string's char codes, a tag per type and per null. Building a string per cell and hashing
9
- * that used to cost several times the query it digested.
10
- */
19
+ /** Fast inequality filter. Equal digests are always followed by exact comparison. */
11
20
  function digestResult(result) {
12
21
  let hash = 0x811c9dc5;
13
22
  const mixByte = (byte) => {
@@ -18,9 +27,9 @@ function digestResult(result) {
18
27
  for (let index = 0; index < 8; index += 1)
19
28
  mixByte(digestScratch.getUint8(index));
20
29
  };
21
- const mixString = (text) => {
22
- for (let index = 0; index < text.length; index += 1) {
23
- const code = text.charCodeAt(index);
30
+ const mixString = (value) => {
31
+ for (let index = 0; index < value.length; index += 1) {
32
+ const code = value.charCodeAt(index);
24
33
  mixByte(code & 0xff);
25
34
  mixByte(code >>> 8);
26
35
  }
@@ -45,20 +54,142 @@ function digestResult(result) {
45
54
  mixByte(value ? 4 : 5);
46
55
  else {
47
56
  mixByte(6);
48
- mixNumber(value.getTime());
57
+ mixNumber(dateMilliseconds(value));
49
58
  }
50
59
  }
51
60
  mixByte(0xfe);
52
61
  }
53
62
  return hash;
54
63
  }
64
+ function sameQueryValue(left, right) {
65
+ if (left instanceof Date || right instanceof Date) {
66
+ return (left instanceof Date &&
67
+ right instanceof Date &&
68
+ Object.is(dateMilliseconds(left), dateMilliseconds(right)));
69
+ }
70
+ return Object.is(left, right);
71
+ }
72
+ function sameResult(left, right) {
73
+ if (left.columns.length !== right.columns.length || left.rows.length !== right.rows.length) {
74
+ return false;
75
+ }
76
+ for (let index = 0; index < left.columns.length; index += 1) {
77
+ if (left.columns[index] !== right.columns[index])
78
+ return false;
79
+ }
80
+ for (let rowIndex = 0; rowIndex < left.rows.length; rowIndex += 1) {
81
+ const leftRow = left.rows[rowIndex];
82
+ const rightRow = right.rows[rowIndex];
83
+ if (leftRow === undefined || rightRow === undefined)
84
+ return false;
85
+ for (const column of left.columns) {
86
+ if (!sameQueryValue(leftRow[column] ?? null, rightRow[column] ?? null))
87
+ return false;
88
+ }
89
+ }
90
+ return true;
91
+ }
92
+ function cloneRow(row, columns) {
93
+ const cloned = {};
94
+ for (const column of columns) {
95
+ const value = row[column] ?? null;
96
+ const copy = value instanceof Date ? copyDate(value) : value;
97
+ if (column === "__proto__") {
98
+ Object.defineProperty(cloned, column, {
99
+ value: copy,
100
+ enumerable: true,
101
+ configurable: true,
102
+ writable: true,
103
+ });
104
+ }
105
+ else
106
+ cloned[column] = copy;
107
+ }
108
+ return cloned;
109
+ }
110
+ function cloneResult(result) {
111
+ const columns = [...result.columns];
112
+ return { columns, rows: result.rows.map((row) => cloneRow(row, columns)) };
113
+ }
114
+ /** Type-tagged, length-delimited structural identity. Unlike JSON, it preserves Date vs string,
115
+ * -0, non-finite numbers, and undefined fields, so deduplication cannot merge distinct plans. */
116
+ function encodeQueryIdentity(value, ancestors = new Set()) {
117
+ if (value === null)
118
+ return "z";
119
+ if (typeof value === "undefined")
120
+ return "u";
121
+ if (typeof value === "boolean")
122
+ return value ? "b1" : "b0";
123
+ if (typeof value === "number") {
124
+ if (Number.isNaN(value))
125
+ return "nNaN;";
126
+ if (Object.is(value, -0))
127
+ return "n-0;";
128
+ return `n${String(value)};`;
129
+ }
130
+ if (typeof value === "string")
131
+ return `s${String(value.length)}:${value}`;
132
+ if (value instanceof Date)
133
+ return `d${String(dateMilliseconds(value))};`;
134
+ if (typeof value !== "object") {
135
+ throw new TypeError(`Unsupported live-query identity value: ${typeof value}`);
136
+ }
137
+ if (ancestors.has(value))
138
+ throw new TypeError("Live-query identity contains a cycle");
139
+ ancestors.add(value);
140
+ let encoded;
141
+ if (Array.isArray(value)) {
142
+ encoded = `a${String(value.length)}[${value
143
+ .map((item) => encodeQueryIdentity(item, ancestors))
144
+ .join("")}]`;
145
+ }
146
+ else {
147
+ const record = value;
148
+ const keys = Object.keys(record).sort();
149
+ encoded = `o${String(keys.length)}{${keys
150
+ .map((key) => `${encodeQueryIdentity(key, ancestors)}${encodeQueryIdentity(record[key], ancestors)}`)
151
+ .join("")}}`;
152
+ }
153
+ ancestors.delete(value);
154
+ return encoded;
155
+ }
156
+ function queryKey(query) {
157
+ return encodeQueryIdentity(typeof query === "string"
158
+ ? ["sql", query]
159
+ : query.kind === "sql-query"
160
+ ? ["sql-query", query.sql, query.params]
161
+ : ["typed-query", query.plan]);
162
+ }
163
+ function sameProbe(left, right) {
164
+ return left.manifestVersion === right.manifestVersion && left.schemaEpoch === right.schemaEpoch;
165
+ }
166
+ function versionOrdinal(version) {
167
+ return version ?? -1;
168
+ }
169
+ function boundedLiveLimit(value, maximum, label) {
170
+ if (!Number.isSafeInteger(value) || value < 1 || value > maximum) {
171
+ throw new RangeError(`Live query ${label} limit must be between 1 and ${String(maximum)}`);
172
+ }
173
+ return value;
174
+ }
175
+ /** Structural changes have their own counter so ordinary commits and physical-index state churn
176
+ * do not force dependency resolution. A manifest reset is still widened: it can only result from
177
+ * replacing or rebuilding the durable database state. */
178
+ function catalogChangedBetween(previous, current) {
179
+ return (versionOrdinal(current.manifestVersion) < versionOrdinal(previous.manifestVersion) ||
180
+ current.schemaEpoch !== previous.schemaEpoch);
181
+ }
55
182
  export class LiveQuerySet {
56
183
  #host;
57
184
  #channel;
58
185
  #channelListener = () => {
59
186
  this.#hint();
60
187
  };
61
- #subscriptions = new Set();
188
+ #groups = new Map();
189
+ #opening = new Map();
190
+ #groupsByTable = new Map();
191
+ #maxGroups;
192
+ #maxSubscriptions;
62
193
  #stats = {
63
194
  hints: 0,
64
195
  versionChecks: 0,
@@ -67,16 +198,25 @@ export class LiveQuerySet {
67
198
  rerunsAvoided: 0,
68
199
  zoneSkips: 0,
69
200
  notificationsSuppressed: 0,
201
+ invalidations: 0,
202
+ sharedExecutions: 0,
70
203
  lastSweepMs: 0,
71
204
  };
72
- #lastSeenVersion = null;
205
+ #lastProbe;
73
206
  #sweepChain = Promise.resolve();
74
207
  #sweepQueued = false;
75
208
  #pollTimer;
76
209
  #closed = false;
210
+ #subscriptionCount = 0;
77
211
  #ownsChannel;
78
212
  #onClosed;
79
213
  constructor(host, options = {}) {
214
+ if (options.pollIntervalMs !== undefined &&
215
+ (!Number.isSafeInteger(options.pollIntervalMs) || options.pollIntervalMs <= 0)) {
216
+ throw new RangeError("Live query poll interval must be a positive whole number");
217
+ }
218
+ this.#maxGroups = boundedLiveLimit(options.maxGroups ?? DEFAULT_LIVE_QUERY_MAX_GROUPS, MAX_LIVE_QUERY_GROUPS, "group");
219
+ this.#maxSubscriptions = boundedLiveLimit(options.maxSubscriptions ?? DEFAULT_LIVE_QUERY_MAX_SUBSCRIPTIONS, MAX_LIVE_QUERY_SUBSCRIPTIONS, "subscription");
80
220
  this.#host = host;
81
221
  if (options.channel !== undefined) {
82
222
  this.#channel = options.channel;
@@ -101,52 +241,243 @@ export class LiveQuerySet {
101
241
  get stats() {
102
242
  return { ...this.#stats };
103
243
  }
104
- /** Registers a query, delivers its initial result, and re-runs it when its tables change. */
105
- async subscribe(query, options) {
244
+ #throwIfClosed() {
106
245
  if (this.#closed)
107
246
  throw new Error("Live query set is closed");
108
- const dependencies = await this.#host.dependencyTableIds(query);
109
- const version = await this.#host.currentVersion();
110
- const subscription = {
111
- query,
112
- dependencies,
113
- options,
114
- digest: undefined,
115
- seenVersion: version,
116
- closed: false,
117
- };
118
- if (this.#lastSeenVersion === null || (version !== null && version < this.#lastSeenVersion)) {
119
- this.#lastSeenVersion = version;
247
+ }
248
+ #reserveSubscription() {
249
+ if (this.#subscriptionCount >= this.#maxSubscriptions) {
250
+ throw new LiveQueryLimitError("subscription", this.#maxSubscriptions);
251
+ }
252
+ this.#subscriptionCount += 1;
253
+ }
254
+ /** Registers a query, delivers its current result, and shares work with equal statements. */
255
+ async subscribe(query, options) {
256
+ this.#throwIfClosed();
257
+ this.#reserveSubscription();
258
+ let group;
259
+ let subscriber;
260
+ try {
261
+ group = await this.#getOrOpenGroup(query);
262
+ this.#throwIfClosed();
263
+ subscriber = {
264
+ kind: "result",
265
+ options,
266
+ delivered: false,
267
+ closed: false,
268
+ };
269
+ group.subscribers.add(subscriber);
270
+ // Reconcile before exposing a shared cached result to a late subscriber.
271
+ await this.refresh();
272
+ this.#throwIfClosed();
273
+ if (!subscriber.delivered) {
274
+ const result = group.result ?? (await this.#executeGroup(group)).result;
275
+ this.#throwIfClosed();
276
+ if (!subscriber.closed)
277
+ this.#deliverResult(subscriber, result);
278
+ }
279
+ }
280
+ catch (error) {
281
+ if (group !== undefined && subscriber !== undefined) {
282
+ this.#removeSubscriber(group, subscriber, false);
283
+ }
284
+ else {
285
+ this.#subscriptionCount -= 1;
286
+ if (group !== undefined)
287
+ this.#removeEmptyGroup(group);
288
+ }
289
+ throw error;
120
290
  }
121
- const result = await this.#host.execute(query);
122
- subscription.digest = digestResult(result);
123
- this.#subscriptions.add(subscription);
291
+ return this.#subscriptionHandle(group, subscriber);
292
+ }
293
+ /** Observes invalidation without executing the statement inside the set. */
294
+ async observe(query, options) {
295
+ this.#throwIfClosed();
296
+ this.#reserveSubscription();
297
+ let group;
298
+ let subscriber;
124
299
  try {
125
- options.onChange(result);
300
+ group = await this.#getOrOpenGroup(query);
301
+ this.#throwIfClosed();
302
+ subscriber = {
303
+ kind: "observer",
304
+ options,
305
+ delivered: false,
306
+ closed: false,
307
+ };
308
+ group.subscribers.add(subscriber);
309
+ await this.refresh();
310
+ this.#throwIfClosed();
311
+ if (!subscriber.delivered) {
312
+ this.#deliverInvalidation(subscriber, { ...group.seenProbe, initial: true });
313
+ }
126
314
  }
127
315
  catch (error) {
128
- // A throwing initial handler would leak an unclosable subscription; unregister first.
129
- subscription.closed = true;
130
- this.#subscriptions.delete(subscription);
316
+ if (group !== undefined && subscriber !== undefined) {
317
+ this.#removeSubscriber(group, subscriber, false);
318
+ }
319
+ else {
320
+ this.#subscriptionCount -= 1;
321
+ if (group !== undefined)
322
+ this.#removeEmptyGroup(group);
323
+ }
131
324
  throw error;
132
325
  }
326
+ return this.#subscriptionHandle(group, subscriber);
327
+ }
328
+ #subscriptionHandle(group, subscriber) {
133
329
  return {
134
- dependencyTableIds: [...dependencies],
330
+ dependencyTableIds: [...group.dependencies],
135
331
  close: () => {
136
- if (subscription.closed)
137
- return;
138
- subscription.closed = true;
139
- this.#subscriptions.delete(subscription);
140
- subscription.options.onComplete?.();
332
+ this.#removeSubscriber(group, subscriber, true);
141
333
  },
142
334
  };
143
335
  }
336
+ #removeSubscriber(group, subscriber, complete) {
337
+ if (subscriber.closed)
338
+ return;
339
+ subscriber.closed = true;
340
+ this.#subscriptionCount -= 1;
341
+ group.subscribers.delete(subscriber);
342
+ if (complete)
343
+ subscriber.options.onComplete?.();
344
+ this.#removeEmptyGroup(group);
345
+ }
346
+ #removeEmptyGroup(group) {
347
+ if (group.subscribers.size !== 0)
348
+ return;
349
+ if (this.#groups.get(group.key) !== group)
350
+ return;
351
+ this.#groups.delete(group.key);
352
+ this.#unindexGroup(group, group.dependencies);
353
+ }
354
+ async #getOrOpenGroup(query) {
355
+ const key = queryKey(query);
356
+ const existing = this.#groups.get(key);
357
+ if (existing !== undefined) {
358
+ this.#stats.sharedExecutions += 1;
359
+ return existing;
360
+ }
361
+ const opening = this.#opening.get(key);
362
+ if (opening !== undefined) {
363
+ this.#stats.sharedExecutions += 1;
364
+ return opening;
365
+ }
366
+ if (this.#groups.size + this.#opening.size >= this.#maxGroups) {
367
+ throw new LiveQueryLimitError("group", this.#maxGroups);
368
+ }
369
+ const created = this.#openGroup(key, query);
370
+ this.#opening.set(key, created);
371
+ try {
372
+ return await created;
373
+ }
374
+ finally {
375
+ this.#opening.delete(key);
376
+ }
377
+ }
378
+ async #openGroup(key, query) {
379
+ const { dependencies, probe: after } = await this.#stableDependencies(query);
380
+ const group = {
381
+ key,
382
+ query,
383
+ dependencies,
384
+ subscribers: new Set(),
385
+ seenProbe: after,
386
+ result: undefined,
387
+ digest: undefined,
388
+ execution: undefined,
389
+ };
390
+ this.#groups.set(key, group);
391
+ this.#indexGroup(group, dependencies);
392
+ if (this.#lastProbe === undefined ||
393
+ versionOrdinal(after.manifestVersion) < versionOrdinal(this.#lastProbe.manifestVersion)) {
394
+ this.#lastProbe = after;
395
+ }
396
+ return group;
397
+ }
398
+ #indexGroup(group, dependencies) {
399
+ for (const tableId of dependencies) {
400
+ let groups = this.#groupsByTable.get(tableId);
401
+ if (groups === undefined) {
402
+ groups = new Set();
403
+ this.#groupsByTable.set(tableId, groups);
404
+ }
405
+ groups.add(group);
406
+ }
407
+ }
408
+ #unindexGroup(group, dependencies) {
409
+ for (const tableId of dependencies) {
410
+ const groups = this.#groupsByTable.get(tableId);
411
+ groups?.delete(group);
412
+ if (groups?.size === 0)
413
+ this.#groupsByTable.delete(tableId);
414
+ }
415
+ }
416
+ async #refreshDependencies(group) {
417
+ const { dependencies: next } = await this.#stableDependencies(group.query);
418
+ if (!this.#stillOpen() || !this.#groups.has(group.key))
419
+ return;
420
+ this.#unindexGroup(group, group.dependencies);
421
+ group.dependencies = next;
422
+ this.#indexGroup(group, next);
423
+ }
424
+ /** Resolve a view/table set inside a catalog-stable probe window or refuse stale indexing. */
425
+ async #stableDependencies(query) {
426
+ let before = await this.#host.currentProbe();
427
+ this.#throwIfClosed();
428
+ for (let attempt = 0; attempt < 8; attempt += 1) {
429
+ const dependencies = await this.#host.dependencyTableIds(query);
430
+ this.#throwIfClosed();
431
+ const after = await this.#host.currentProbe();
432
+ this.#throwIfClosed();
433
+ if (!catalogChangedBetween(before, after))
434
+ return { dependencies, probe: after };
435
+ before = after;
436
+ }
437
+ throw new Error("Catalog kept changing while live-query dependencies were resolved");
438
+ }
439
+ async #executeGroup(group) {
440
+ if (group.execution !== undefined) {
441
+ this.#stats.sharedExecutions += 1;
442
+ return group.execution;
443
+ }
444
+ const execution = (async () => {
445
+ const executed = await this.#host.execute(group.query);
446
+ const digest = digestResult(executed);
447
+ const previous = group.result;
448
+ const changed = previous === undefined || group.digest !== digest || !sameResult(previous, executed);
449
+ const retained = cloneResult(executed);
450
+ group.result = retained;
451
+ group.digest = digest;
452
+ return { result: retained, changed };
453
+ })();
454
+ group.execution = execution;
455
+ try {
456
+ return await execution;
457
+ }
458
+ finally {
459
+ if (group.execution === execution)
460
+ group.execution = undefined;
461
+ }
462
+ }
463
+ #deliverResult(subscriber, result) {
464
+ if (subscriber.closed)
465
+ return;
466
+ subscriber.delivered = true;
467
+ subscriber.options.onChange(cloneResult(result));
468
+ }
469
+ #deliverInvalidation(subscriber, invalidation) {
470
+ if (subscriber.closed)
471
+ return;
472
+ subscriber.delivered = true;
473
+ subscriber.options.onInvalidate(invalidation);
474
+ }
144
475
  /** Called by the owning database after each local write commit; also hints other tabs. */
145
476
  notifyLocalCommit() {
146
477
  this.#channel?.postMessage("minnow-commit");
147
478
  this.#hint();
148
479
  }
149
- /** Runs one authoritative version check and selective re-execution sweep. */
480
+ /** Runs one authoritative probe and selective re-execution sweep. */
150
481
  async refresh() {
151
482
  this.#hint();
152
483
  await this.#sweepChain;
@@ -160,14 +491,19 @@ export class LiveQuerySet {
160
491
  this.#channel?.close?.();
161
492
  if (this.#pollTimer !== undefined)
162
493
  clearInterval(this.#pollTimer);
163
- const subscriptions = [...this.#subscriptions];
164
- this.#subscriptions.clear();
165
- // Wake every remaining subscriber so iterators and UI bindings end instead of hanging.
166
- for (const subscription of subscriptions) {
167
- if (subscription.closed)
168
- continue;
169
- subscription.closed = true;
170
- subscription.options.onComplete?.();
494
+ const groups = [...this.#groups.values()];
495
+ this.#groups.clear();
496
+ this.#groupsByTable.clear();
497
+ for (const group of groups) {
498
+ const subscribers = [...group.subscribers];
499
+ group.subscribers.clear();
500
+ for (const subscriber of subscribers) {
501
+ if (subscriber.closed)
502
+ continue;
503
+ subscriber.closed = true;
504
+ this.#subscriptionCount -= 1;
505
+ subscriber.options.onComplete?.();
506
+ }
171
507
  }
172
508
  this.#onClosed?.();
173
509
  }
@@ -184,107 +520,152 @@ export class LiveQuerySet {
184
520
  await this.#sweep();
185
521
  }
186
522
  catch (error) {
187
- for (const subscription of this.#subscriptions) {
188
- subscription.options.onError?.(error);
189
- }
523
+ for (const group of this.#groups.values())
524
+ this.#notifyGroupError(group, error);
190
525
  }
191
526
  });
192
527
  }
193
- /**
194
- * Re-reads the closed flags through calls. `close()` runs while a sweep is suspended at an
195
- * await, but control-flow analysis assumes flags cannot change across one, so a direct read
196
- * would be narrowed to its pre-await value and the guard silently dropped.
197
- */
198
528
  #stillOpen() {
199
529
  return !this.#closed;
200
530
  }
201
531
  async #sweep() {
202
- if (this.#closed || this.#subscriptions.size === 0)
532
+ if (this.#closed || this.#groups.size === 0)
203
533
  return;
204
534
  this.#stats.versionChecks += 1;
205
- const current = await this.#host.currentVersion();
206
- if (!this.#stillOpen() || current === null)
535
+ const current = await this.#host.currentProbe();
536
+ if (!this.#stillOpen())
207
537
  return;
208
- const anyLagging = [...this.#subscriptions].some((subscription) => !subscription.closed &&
209
- (subscription.seenVersion === null || subscription.seenVersion < current));
210
- if (current === this.#lastSeenVersion && !anyLagging)
538
+ const last = this.#lastProbe ?? current;
539
+ const anyLagging = [...this.#groups.values()].some((group) => !sameProbe(group.seenProbe, current));
540
+ if (sameProbe(last, current) && !anyLagging)
211
541
  return;
212
542
  const started = performance.now();
213
543
  this.#stats.sweeps += 1;
214
- // The changed-table window starts at the set-level version, so it only gates subscriptions
215
- // that were up to date there. A lagging subscription's window starts earlier at its own
216
- // seenVersion, so it re-runs unconditionally — the digest suppresses no-op notifications.
217
- const changed = current === this.#lastSeenVersion
218
- ? new Set()
219
- : await this.#changedTablesSince(this.#lastSeenVersion, current);
220
- if (!this.#stillOpen())
221
- return;
222
- for (const subscription of [...this.#subscriptions]) {
223
- if (subscription.closed)
544
+ const windowCache = new Map();
545
+ const changedSince = (after) => {
546
+ const key = versionOrdinal(after);
547
+ let pending = windowCache.get(key);
548
+ if (pending === undefined) {
549
+ pending = this.#changedTablesSince(after, current.manifestVersion);
550
+ windowCache.set(key, pending);
551
+ }
552
+ return pending;
553
+ };
554
+ for (const group of [...this.#groups.values()]) {
555
+ if (!this.#stillOpen())
556
+ return;
557
+ if (group.subscribers.size === 0 || sameProbe(group.seenProbe, current))
224
558
  continue;
225
- if (subscription.seenVersion !== null && subscription.seenVersion >= current)
559
+ const prior = group.seenProbe;
560
+ const catalogChanged = catalogChangedBetween(prior, current);
561
+ if (catalogChanged) {
562
+ try {
563
+ await this.#refreshDependencies(group);
564
+ }
565
+ catch (error) {
566
+ this.#notifyGroupError(group, error);
567
+ continue;
568
+ }
569
+ }
570
+ let relevant = [];
571
+ let affected = catalogChanged;
572
+ if (!affected && prior.manifestVersion !== current.manifestVersion) {
573
+ const changed = await changedSince(prior.manifestVersion);
574
+ if (!this.#stillOpen())
575
+ return;
576
+ if (changed === "all")
577
+ affected = true;
578
+ else {
579
+ relevant = [...group.dependencies].filter((tableId) => changed.has(tableId));
580
+ affected = relevant.length > 0;
581
+ }
582
+ }
583
+ if (!affected) {
584
+ this.#stats.rerunsAvoided += 1;
585
+ group.seenProbe = current;
226
586
  continue;
227
- if (subscription.seenVersion === this.#lastSeenVersion) {
228
- const affected = changed === "all" ||
229
- [...subscription.dependencies].some((tableId) => changed.has(tableId));
230
- if (!affected) {
587
+ }
588
+ if (!catalogChanged &&
589
+ relevant.length > 0 &&
590
+ current.manifestVersion !== null &&
591
+ this.#host.changeCanAffect !== undefined) {
592
+ let canAffect;
593
+ try {
594
+ canAffect = await this.#host.changeCanAffect(group.query, relevant, prior.manifestVersion, current.manifestVersion);
595
+ }
596
+ catch {
597
+ canAffect = true;
598
+ }
599
+ if (!this.#stillOpen())
600
+ return;
601
+ if (!canAffect) {
231
602
  this.#stats.rerunsAvoided += 1;
232
- subscription.seenVersion = current;
603
+ this.#stats.zoneSkips += 1;
604
+ group.seenProbe = current;
233
605
  continue;
234
606
  }
235
- if (changed !== "all" && this.#host.changeCanAffect !== undefined) {
236
- const relevant = [...subscription.dependencies].filter((tableId) => changed.has(tableId));
237
- let canAffect;
238
- try {
239
- canAffect = await this.#host.changeCanAffect(subscription.query, relevant, this.#lastSeenVersion, current);
240
- }
241
- catch {
242
- canAffect = true;
243
- }
244
- if (!this.#stillOpen())
245
- return;
246
- if (isSubscriptionClosed(subscription))
607
+ }
608
+ const resultSubscribers = [...group.subscribers].filter((subscriber) => subscriber.kind === "result" && !subscriber.closed);
609
+ const observers = [...group.subscribers].filter((subscriber) => subscriber.kind === "observer" && !subscriber.closed);
610
+ try {
611
+ let execution;
612
+ if (resultSubscribers.length > 0) {
613
+ // An initial delivery may be executing against the pre-sweep snapshot. Waiting for it
614
+ // here can deadlock callers that intentionally hold that execute while awaiting this
615
+ // refresh. Leave the group dirty; once the initial delivery lands, the next refresh
616
+ // re-runs it against `current`.
617
+ if (group.result === undefined && group.execution !== undefined)
247
618
  continue;
248
- if (!canAffect) {
249
- this.#stats.rerunsAvoided += 1;
250
- this.#stats.zoneSkips += 1;
251
- subscription.seenVersion = current;
619
+ this.#stats.reruns += 1;
620
+ execution = await this.#executeGroup(group);
621
+ if (!this.#stillOpen() || !this.#groups.has(group.key))
252
622
  continue;
623
+ }
624
+ for (const observer of observers) {
625
+ try {
626
+ this.#deliverInvalidation(observer, { ...current, initial: false });
627
+ this.#stats.invalidations += 1;
628
+ }
629
+ catch (error) {
630
+ observer.options.onError?.(error);
253
631
  }
254
632
  }
255
- }
256
- this.#stats.reruns += 1;
257
- try {
258
- const result = await this.#host.execute(subscription.query);
259
- // The subscription (or the set) may have closed during the execute; a late onChange
260
- // after onComplete would break the subscriber contract.
261
- if (!this.#stillOpen() || isSubscriptionClosed(subscription))
262
- continue;
263
- subscription.seenVersion = current;
264
- const digest = digestResult(result);
265
- if (digest === subscription.digest) {
266
- this.#stats.notificationsSuppressed += 1;
267
- continue;
633
+ if (execution !== undefined) {
634
+ if (execution.changed) {
635
+ for (const subscriber of resultSubscribers) {
636
+ try {
637
+ this.#deliverResult(subscriber, execution.result);
638
+ }
639
+ catch (error) {
640
+ subscriber.options.onError?.(error);
641
+ }
642
+ }
643
+ }
644
+ else {
645
+ this.#stats.notificationsSuppressed += resultSubscribers.length;
646
+ }
268
647
  }
269
- subscription.digest = digest;
270
- subscription.options.onChange(result);
648
+ group.seenProbe = current;
271
649
  }
272
650
  catch (error) {
273
- if (!this.#stillOpen() || isSubscriptionClosed(subscription))
274
- continue;
275
- subscription.seenVersion = current;
276
- subscription.options.onError?.(error);
651
+ // Do not advance seenProbe. A refresh with no newer commit retries the dirty group.
652
+ this.#notifyGroupError(group, error);
277
653
  }
278
654
  }
279
- this.#lastSeenVersion = current;
655
+ this.#lastProbe = current;
280
656
  this.#stats.lastSweepMs = performance.now() - started;
281
657
  }
282
- /**
283
- * Unions the persisted change sets of every manifest in (after, until], reading bounded pages.
284
- * A missing manifest version or a manifest without a change set widens to "all" — correctness
285
- * over selectivity.
286
- */
658
+ #notifyGroupError(group, error) {
659
+ for (const subscriber of group.subscribers) {
660
+ if (subscriber.closed)
661
+ continue;
662
+ subscriber.options.onError?.(error);
663
+ }
664
+ }
665
+ /** Unions persisted table change sets in (after, until], widening on any history gap. */
287
666
  async #changedTablesSince(after, until) {
667
+ if (until === null || after === until)
668
+ return new Set();
288
669
  const changed = new Set();
289
670
  let cursor = after;
290
671
  let expected = (after ?? -1) + 1;
@@ -296,18 +677,15 @@ export class LiveQuerySet {
296
677
  if (manifest.version !== expected)
297
678
  return "all";
298
679
  expected += 1;
299
- if (manifest.changedTableIds === undefined)
300
- return "all";
301
680
  for (const tableId of manifest.changedTableIds)
302
681
  changed.add(tableId);
303
682
  if (manifest.version === until)
304
683
  return changed;
305
684
  }
306
- if (page.nextCursor === null) {
685
+ if (page.nextCursor === null)
307
686
  return expected > until ? changed : "all";
308
- }
309
687
  cursor = page.nextCursor;
310
688
  }
311
689
  }
312
690
  }
313
- //# sourceMappingURL=live.js.map
691
+ import { copyDate, dateMilliseconds } from "../date-value.js";