@minnowdb/core 0.3.0 → 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 (276) hide show
  1. package/README.md +4 -2
  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 +0 -1
  21. package/dist/engine/artifact-cache.js +0 -1
  22. package/dist/engine/batch.d.ts +5 -2
  23. package/dist/engine/batch.js +0 -1
  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 +5 -6
  29. package/dist/engine/catalog.js +5 -5
  30. package/dist/engine/client.d.ts +27 -7
  31. package/dist/engine/client.js +197 -31
  32. package/dist/engine/database.d.ts +161 -38
  33. package/dist/engine/database.js +4320 -1523
  34. package/dist/engine/defaults.d.ts +0 -1
  35. package/dist/engine/defaults.js +2 -4
  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 +39 -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 -11
  43. package/dist/engine/index.js +12 -10
  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 +499 -129
  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 +4 -4
  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 +0 -1
  59. package/dist/engine/query-cache.js +4 -4
  60. package/dist/engine/query.d.ts +4 -7
  61. package/dist/engine/query.js +348 -155
  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 +1 -2
  65. package/dist/engine/schema-wire.js +0 -1
  66. package/dist/engine/schema.d.ts +3 -8
  67. package/dist/engine/schema.js +41 -24
  68. package/dist/engine/sort-keys.d.ts +0 -1
  69. package/dist/engine/sort-keys.js +2 -2
  70. package/dist/engine/sql-domains.d.ts +6 -1
  71. package/dist/engine/sql-domains.js +209 -45
  72. package/dist/engine/sql-driver.d.ts +2 -2
  73. package/dist/engine/sql-driver.js +0 -1
  74. package/dist/engine/sql-json.d.ts +0 -1
  75. package/dist/engine/sql-json.js +53 -7
  76. package/dist/engine/sql-semantics.d.ts +8 -5
  77. package/dist/engine/sql-semantics.js +427 -65
  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 +7 -1
  81. package/dist/engine/vector.js +202 -58
  82. package/dist/engine/worker-host.d.ts +8 -2
  83. package/dist/engine/worker-host.js +616 -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 +0 -1
  91. package/dist/plan/index.js +0 -1
  92. package/dist/storage/index.d.ts +1 -1
  93. package/dist/storage/index.js +1 -1
  94. package/dist/storage/indexeddb.d.ts +74 -65
  95. package/dist/storage/indexeddb.js +12301 -2076
  96. package/dist/storage/memory.d.ts +56 -66
  97. package/dist/storage/memory.js +1004 -139
  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 +403 -91
  103. package/dist/storage/opfs/leader.js +4306 -602
  104. package/dist/storage/opfs/rpc.d.ts +15 -3
  105. package/dist/storage/opfs/rpc.js +223 -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 +26 -121
  109. package/dist/storage/opfs/store.js +436 -388
  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 -99
  115. package/dist/storage/snapshot.js +33 -309
  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 +3 -4
  119. package/dist/storage/toolkit/index.js +27 -3
  120. package/dist/storage/toolkit/record-core.d.ts +174 -61
  121. package/dist/storage/toolkit/record-core.js +5401 -849
  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 +4 -6
  127. package/dist/storage/toolkit/wire.js +80 -25
  128. package/dist/storage/types.d.ts +1271 -218
  129. package/dist/storage/types.js +1471 -138
  130. package/dist/testing/block-store-conformance.d.ts +0 -1
  131. package/dist/testing/block-store-conformance.js +877 -138
  132. package/dist/testing/index.d.ts +42 -31
  133. package/dist/testing/index.js +100 -68
  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 +0 -1
  137. package/dist/testing/simulator.js +43 -12
  138. package/dist/testing/sqllogictest.d.ts +0 -1
  139. package/dist/testing/sqllogictest.js +0 -1
  140. package/dist/transactions/index.d.ts +81 -20
  141. package/dist/transactions/index.js +1050 -268
  142. package/dist/worker-protocol/index.d.ts +3 -2
  143. package/dist/worker-protocol/index.js +2 -5
  144. package/package.json +49 -1
  145. package/dist/block-format/block.d.ts.map +0 -1
  146. package/dist/block-format/block.js.map +0 -1
  147. package/dist/block-format/checksum.d.ts.map +0 -1
  148. package/dist/block-format/checksum.js.map +0 -1
  149. package/dist/block-format/codecs.d.ts.map +0 -1
  150. package/dist/block-format/codecs.js.map +0 -1
  151. package/dist/block-format/column.d.ts.map +0 -1
  152. package/dist/block-format/column.js.map +0 -1
  153. package/dist/block-format/index.d.ts.map +0 -1
  154. package/dist/block-format/index.js.map +0 -1
  155. package/dist/block-format/physical.d.ts.map +0 -1
  156. package/dist/block-format/physical.js.map +0 -1
  157. package/dist/block-format/types.d.ts.map +0 -1
  158. package/dist/block-format/types.js.map +0 -1
  159. package/dist/engine/artifact-cache.d.ts.map +0 -1
  160. package/dist/engine/artifact-cache.js.map +0 -1
  161. package/dist/engine/batch.d.ts.map +0 -1
  162. package/dist/engine/batch.js.map +0 -1
  163. package/dist/engine/buffered-writer.d.ts.map +0 -1
  164. package/dist/engine/buffered-writer.js.map +0 -1
  165. package/dist/engine/catalog.d.ts.map +0 -1
  166. package/dist/engine/catalog.js.map +0 -1
  167. package/dist/engine/client.d.ts.map +0 -1
  168. package/dist/engine/client.js.map +0 -1
  169. package/dist/engine/coordinator.d.ts +0 -17
  170. package/dist/engine/coordinator.d.ts.map +0 -1
  171. package/dist/engine/coordinator.js +0 -60
  172. package/dist/engine/coordinator.js.map +0 -1
  173. package/dist/engine/database.d.ts.map +0 -1
  174. package/dist/engine/database.js.map +0 -1
  175. package/dist/engine/defaults.d.ts.map +0 -1
  176. package/dist/engine/defaults.js.map +0 -1
  177. package/dist/engine/errors.d.ts.map +0 -1
  178. package/dist/engine/errors.js.map +0 -1
  179. package/dist/engine/fts.d.ts.map +0 -1
  180. package/dist/engine/fts.js.map +0 -1
  181. package/dist/engine/group-index.d.ts.map +0 -1
  182. package/dist/engine/group-index.js.map +0 -1
  183. package/dist/engine/index.d.ts.map +0 -1
  184. package/dist/engine/index.js.map +0 -1
  185. package/dist/engine/join-index.d.ts.map +0 -1
  186. package/dist/engine/join-index.js.map +0 -1
  187. package/dist/engine/live.d.ts.map +0 -1
  188. package/dist/engine/live.js.map +0 -1
  189. package/dist/engine/memory.d.ts.map +0 -1
  190. package/dist/engine/memory.js.map +0 -1
  191. package/dist/engine/optimizer.d.ts.map +0 -1
  192. package/dist/engine/optimizer.js.map +0 -1
  193. package/dist/engine/query-cache.d.ts.map +0 -1
  194. package/dist/engine/query-cache.js.map +0 -1
  195. package/dist/engine/query.d.ts.map +0 -1
  196. package/dist/engine/query.js.map +0 -1
  197. package/dist/engine/result-wire.d.ts.map +0 -1
  198. package/dist/engine/result-wire.js.map +0 -1
  199. package/dist/engine/schema-wire.d.ts.map +0 -1
  200. package/dist/engine/schema-wire.js.map +0 -1
  201. package/dist/engine/schema.d.ts.map +0 -1
  202. package/dist/engine/schema.js.map +0 -1
  203. package/dist/engine/sort-keys.d.ts.map +0 -1
  204. package/dist/engine/sort-keys.js.map +0 -1
  205. package/dist/engine/sql-domains.d.ts.map +0 -1
  206. package/dist/engine/sql-domains.js.map +0 -1
  207. package/dist/engine/sql-driver.d.ts.map +0 -1
  208. package/dist/engine/sql-driver.js.map +0 -1
  209. package/dist/engine/sql-json.d.ts.map +0 -1
  210. package/dist/engine/sql-json.js.map +0 -1
  211. package/dist/engine/sql-semantics.d.ts.map +0 -1
  212. package/dist/engine/sql-semantics.js.map +0 -1
  213. package/dist/engine/vector.d.ts.map +0 -1
  214. package/dist/engine/vector.js.map +0 -1
  215. package/dist/engine/worker-host.d.ts.map +0 -1
  216. package/dist/engine/worker-host.js.map +0 -1
  217. package/dist/engine/worker.d.ts.map +0 -1
  218. package/dist/engine/worker.js.map +0 -1
  219. package/dist/index.d.ts.map +0 -1
  220. package/dist/index.js.map +0 -1
  221. package/dist/plan/index.d.ts.map +0 -1
  222. package/dist/plan/index.js.map +0 -1
  223. package/dist/storage/fixture-shape.d.ts +0 -42
  224. package/dist/storage/fixture-shape.d.ts.map +0 -1
  225. package/dist/storage/fixture-shape.js +0 -146
  226. package/dist/storage/fixture-shape.js.map +0 -1
  227. package/dist/storage/index.d.ts.map +0 -1
  228. package/dist/storage/index.js.map +0 -1
  229. package/dist/storage/indexeddb.d.ts.map +0 -1
  230. package/dist/storage/indexeddb.js.map +0 -1
  231. package/dist/storage/memory.d.ts.map +0 -1
  232. package/dist/storage/memory.js.map +0 -1
  233. package/dist/storage/opfs/files.d.ts.map +0 -1
  234. package/dist/storage/opfs/files.js.map +0 -1
  235. package/dist/storage/opfs/index.d.ts.map +0 -1
  236. package/dist/storage/opfs/index.js.map +0 -1
  237. package/dist/storage/opfs/leader.d.ts.map +0 -1
  238. package/dist/storage/opfs/leader.js.map +0 -1
  239. package/dist/storage/opfs/rpc.d.ts.map +0 -1
  240. package/dist/storage/opfs/rpc.js.map +0 -1
  241. package/dist/storage/opfs/store.d.ts.map +0 -1
  242. package/dist/storage/opfs/store.js.map +0 -1
  243. package/dist/storage/snapshot.d.ts.map +0 -1
  244. package/dist/storage/snapshot.js.map +0 -1
  245. package/dist/storage/toolkit/extents.d.ts.map +0 -1
  246. package/dist/storage/toolkit/extents.js.map +0 -1
  247. package/dist/storage/toolkit/index.d.ts.map +0 -1
  248. package/dist/storage/toolkit/index.js.map +0 -1
  249. package/dist/storage/toolkit/record-core.d.ts.map +0 -1
  250. package/dist/storage/toolkit/record-core.js.map +0 -1
  251. package/dist/storage/toolkit/sync-file.d.ts.map +0 -1
  252. package/dist/storage/toolkit/sync-file.js.map +0 -1
  253. package/dist/storage/toolkit/wal.d.ts.map +0 -1
  254. package/dist/storage/toolkit/wal.js.map +0 -1
  255. package/dist/storage/toolkit/wire.d.ts.map +0 -1
  256. package/dist/storage/toolkit/wire.js.map +0 -1
  257. package/dist/storage/types.d.ts.map +0 -1
  258. package/dist/storage/types.js.map +0 -1
  259. package/dist/testing/block-store-conformance.d.ts.map +0 -1
  260. package/dist/testing/block-store-conformance.js.map +0 -1
  261. package/dist/testing/index.d.ts.map +0 -1
  262. package/dist/testing/index.js.map +0 -1
  263. package/dist/testing/opfs-shim.d.ts.map +0 -1
  264. package/dist/testing/opfs-shim.js.map +0 -1
  265. package/dist/testing/seeds.d.ts +0 -11
  266. package/dist/testing/seeds.d.ts.map +0 -1
  267. package/dist/testing/seeds.js +0 -50
  268. package/dist/testing/seeds.js.map +0 -1
  269. package/dist/testing/simulator.d.ts.map +0 -1
  270. package/dist/testing/simulator.js.map +0 -1
  271. package/dist/testing/sqllogictest.d.ts.map +0 -1
  272. package/dist/testing/sqllogictest.js.map +0 -1
  273. package/dist/transactions/index.d.ts.map +0 -1
  274. package/dist/transactions/index.js.map +0 -1
  275. package/dist/worker-protocol/index.d.ts.map +0 -1
  276. 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;
@@ -105,56 +245,239 @@ export class LiveQuerySet {
105
245
  if (this.#closed)
106
246
  throw new Error("Live query set is closed");
107
247
  }
108
- /** Registers a query, delivers its initial result, and re-runs it when its tables change. */
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. */
109
255
  async subscribe(query, options) {
110
256
  this.#throwIfClosed();
111
- const dependencies = await this.#host.dependencyTableIds(query);
112
- this.#throwIfClosed();
113
- const version = await this.#host.currentVersion();
114
- this.#throwIfClosed();
115
- const subscription = {
116
- query,
117
- dependencies,
118
- options,
119
- digest: undefined,
120
- seenVersion: version,
121
- closed: false,
122
- };
123
- if (this.#lastSeenVersion === null || (version !== null && version < this.#lastSeenVersion)) {
124
- this.#lastSeenVersion = version;
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;
125
290
  }
126
- const result = await this.#host.execute(query);
127
- // close() can run while any host call above is suspended. Never register or notify a
128
- // subscription after its parent set has completed.
291
+ return this.#subscriptionHandle(group, subscriber);
292
+ }
293
+ /** Observes invalidation without executing the statement inside the set. */
294
+ async observe(query, options) {
129
295
  this.#throwIfClosed();
130
- subscription.digest = digestResult(result);
131
- this.#subscriptions.add(subscription);
296
+ this.#reserveSubscription();
297
+ let group;
298
+ let subscriber;
132
299
  try {
133
- 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
+ }
134
314
  }
135
315
  catch (error) {
136
- // A throwing initial handler would leak an unclosable subscription; unregister first.
137
- subscription.closed = true;
138
- 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
+ }
139
324
  throw error;
140
325
  }
326
+ return this.#subscriptionHandle(group, subscriber);
327
+ }
328
+ #subscriptionHandle(group, subscriber) {
141
329
  return {
142
- dependencyTableIds: [...dependencies],
330
+ dependencyTableIds: [...group.dependencies],
143
331
  close: () => {
144
- if (subscription.closed)
145
- return;
146
- subscription.closed = true;
147
- this.#subscriptions.delete(subscription);
148
- subscription.options.onComplete?.();
332
+ this.#removeSubscriber(group, subscriber, true);
149
333
  },
150
334
  };
151
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
+ }
152
475
  /** Called by the owning database after each local write commit; also hints other tabs. */
153
476
  notifyLocalCommit() {
154
477
  this.#channel?.postMessage("minnow-commit");
155
478
  this.#hint();
156
479
  }
157
- /** Runs one authoritative version check and selective re-execution sweep. */
480
+ /** Runs one authoritative probe and selective re-execution sweep. */
158
481
  async refresh() {
159
482
  this.#hint();
160
483
  await this.#sweepChain;
@@ -168,14 +491,19 @@ export class LiveQuerySet {
168
491
  this.#channel?.close?.();
169
492
  if (this.#pollTimer !== undefined)
170
493
  clearInterval(this.#pollTimer);
171
- const subscriptions = [...this.#subscriptions];
172
- this.#subscriptions.clear();
173
- // Wake every remaining subscriber so iterators and UI bindings end instead of hanging.
174
- for (const subscription of subscriptions) {
175
- if (subscription.closed)
176
- continue;
177
- subscription.closed = true;
178
- 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
+ }
179
507
  }
180
508
  this.#onClosed?.();
181
509
  }
@@ -192,107 +520,152 @@ export class LiveQuerySet {
192
520
  await this.#sweep();
193
521
  }
194
522
  catch (error) {
195
- for (const subscription of this.#subscriptions) {
196
- subscription.options.onError?.(error);
197
- }
523
+ for (const group of this.#groups.values())
524
+ this.#notifyGroupError(group, error);
198
525
  }
199
526
  });
200
527
  }
201
- /**
202
- * Re-reads the closed flags through calls. `close()` runs while a sweep is suspended at an
203
- * await, but control-flow analysis assumes flags cannot change across one, so a direct read
204
- * would be narrowed to its pre-await value and the guard silently dropped.
205
- */
206
528
  #stillOpen() {
207
529
  return !this.#closed;
208
530
  }
209
531
  async #sweep() {
210
- if (this.#closed || this.#subscriptions.size === 0)
532
+ if (this.#closed || this.#groups.size === 0)
211
533
  return;
212
534
  this.#stats.versionChecks += 1;
213
- const current = await this.#host.currentVersion();
214
- if (!this.#stillOpen() || current === null)
535
+ const current = await this.#host.currentProbe();
536
+ if (!this.#stillOpen())
215
537
  return;
216
- const anyLagging = [...this.#subscriptions].some((subscription) => !subscription.closed &&
217
- (subscription.seenVersion === null || subscription.seenVersion < current));
218
- 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)
219
541
  return;
220
542
  const started = performance.now();
221
543
  this.#stats.sweeps += 1;
222
- // The changed-table window starts at the set-level version, so it only gates subscriptions
223
- // that were up to date there. A lagging subscription's window starts earlier at its own
224
- // seenVersion, so it re-runs unconditionally — the digest suppresses no-op notifications.
225
- const changed = current === this.#lastSeenVersion
226
- ? new Set()
227
- : await this.#changedTablesSince(this.#lastSeenVersion, current);
228
- if (!this.#stillOpen())
229
- return;
230
- for (const subscription of [...this.#subscriptions]) {
231
- 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))
232
558
  continue;
233
- 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;
234
586
  continue;
235
- if (subscription.seenVersion === this.#lastSeenVersion) {
236
- const affected = changed === "all" ||
237
- [...subscription.dependencies].some((tableId) => changed.has(tableId));
238
- 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) {
239
602
  this.#stats.rerunsAvoided += 1;
240
- subscription.seenVersion = current;
603
+ this.#stats.zoneSkips += 1;
604
+ group.seenProbe = current;
241
605
  continue;
242
606
  }
243
- if (changed !== "all" && this.#host.changeCanAffect !== undefined) {
244
- const relevant = [...subscription.dependencies].filter((tableId) => changed.has(tableId));
245
- let canAffect;
246
- try {
247
- canAffect = await this.#host.changeCanAffect(subscription.query, relevant, this.#lastSeenVersion, current);
248
- }
249
- catch {
250
- canAffect = true;
251
- }
252
- if (!this.#stillOpen())
253
- return;
254
- 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)
255
618
  continue;
256
- if (!canAffect) {
257
- this.#stats.rerunsAvoided += 1;
258
- this.#stats.zoneSkips += 1;
259
- subscription.seenVersion = current;
619
+ this.#stats.reruns += 1;
620
+ execution = await this.#executeGroup(group);
621
+ if (!this.#stillOpen() || !this.#groups.has(group.key))
260
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);
261
631
  }
262
632
  }
263
- }
264
- this.#stats.reruns += 1;
265
- try {
266
- const result = await this.#host.execute(subscription.query);
267
- // The subscription (or the set) may have closed during the execute; a late onChange
268
- // after onComplete would break the subscriber contract.
269
- if (!this.#stillOpen() || isSubscriptionClosed(subscription))
270
- continue;
271
- subscription.seenVersion = current;
272
- const digest = digestResult(result);
273
- if (digest === subscription.digest) {
274
- this.#stats.notificationsSuppressed += 1;
275
- 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
+ }
276
647
  }
277
- subscription.digest = digest;
278
- subscription.options.onChange(result);
648
+ group.seenProbe = current;
279
649
  }
280
650
  catch (error) {
281
- if (!this.#stillOpen() || isSubscriptionClosed(subscription))
282
- continue;
283
- subscription.seenVersion = current;
284
- subscription.options.onError?.(error);
651
+ // Do not advance seenProbe. A refresh with no newer commit retries the dirty group.
652
+ this.#notifyGroupError(group, error);
285
653
  }
286
654
  }
287
- this.#lastSeenVersion = current;
655
+ this.#lastProbe = current;
288
656
  this.#stats.lastSweepMs = performance.now() - started;
289
657
  }
290
- /**
291
- * Unions the persisted change sets of every manifest in (after, until], reading bounded pages.
292
- * A missing manifest version or a manifest without a change set widens to "all" — correctness
293
- * over selectivity.
294
- */
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. */
295
666
  async #changedTablesSince(after, until) {
667
+ if (until === null || after === until)
668
+ return new Set();
296
669
  const changed = new Set();
297
670
  let cursor = after;
298
671
  let expected = (after ?? -1) + 1;
@@ -304,18 +677,15 @@ export class LiveQuerySet {
304
677
  if (manifest.version !== expected)
305
678
  return "all";
306
679
  expected += 1;
307
- if (manifest.changedTableIds === undefined)
308
- return "all";
309
680
  for (const tableId of manifest.changedTableIds)
310
681
  changed.add(tableId);
311
682
  if (manifest.version === until)
312
683
  return changed;
313
684
  }
314
- if (page.nextCursor === null) {
685
+ if (page.nextCursor === null)
315
686
  return expected > until ? changed : "all";
316
- }
317
687
  cursor = page.nextCursor;
318
688
  }
319
689
  }
320
690
  }
321
- //# sourceMappingURL=live.js.map
691
+ import { copyDate, dateMilliseconds } from "../date-value.js";