@minnowdb/core 0.7.10 → 0.9.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 (75) hide show
  1. package/dist/engine/artifact-cache.js +3 -2
  2. package/dist/engine/buffered-writer.js +12 -2
  3. package/dist/engine/client.d.ts +9 -0
  4. package/dist/engine/client.js +112 -20
  5. package/dist/engine/database.d.ts +5 -3
  6. package/dist/engine/database.js +2199 -1675
  7. package/dist/engine/errors.d.ts +21 -2
  8. package/dist/engine/errors.js +30 -1
  9. package/dist/engine/index.d.ts +1 -0
  10. package/dist/engine/index.js +4 -0
  11. package/dist/engine/live-accept.js +13 -0
  12. package/dist/engine/live-aggregate.js +264 -0
  13. package/dist/engine/live-patch.d.ts +21 -0
  14. package/dist/engine/live-patch.js +31 -0
  15. package/dist/engine/live.d.ts +14 -0
  16. package/dist/engine/live.js +146 -142
  17. package/dist/engine/optimizer.js +11 -6
  18. package/dist/engine/query-cache.js +19 -15
  19. package/dist/engine/query-generations.js +61 -0
  20. package/dist/engine/query-identity.js +41 -0
  21. package/dist/engine/query.d.ts +7 -13
  22. package/dist/engine/query.js +298 -435
  23. package/dist/engine/result-state.d.ts +7 -0
  24. package/dist/engine/result-state.js +15 -0
  25. package/dist/engine/sql-domains.js +121 -0
  26. package/dist/engine/sql-semantics.js +7 -4
  27. package/dist/engine/typed-live.js +28 -20
  28. package/dist/engine/vector.d.ts +4 -0
  29. package/dist/engine/vector.js +14 -14
  30. package/dist/engine/windows.d.ts +12 -0
  31. package/dist/engine/windows.js +387 -0
  32. package/dist/engine/worker-server.d.ts +1 -1
  33. package/dist/engine/worker-server.js +28 -15
  34. package/dist/engine/write-coordinator.js +54 -0
  35. package/dist/plan/model.d.ts +1 -1
  36. package/dist/storage/indexeddb.js +134 -89
  37. package/dist/storage/opfs/index.d.ts +1 -1
  38. package/dist/storage/opfs/index.js +3 -1
  39. package/dist/storage/opfs/leader.js +20 -9
  40. package/dist/storage/opfs/rpc.js +3 -1
  41. package/dist/storage/opfs/store.d.ts +2 -0
  42. package/dist/storage/opfs/store.js +119 -43
  43. package/dist/storage/toolkit/record-core.js +2 -1
  44. package/dist/storage/types.d.ts +14 -0
  45. package/dist/storage/types.js +21 -0
  46. package/dist/transactions/index.d.ts +3 -0
  47. package/dist/transactions/index.js +4 -1
  48. package/dist/worker-protocol/index.d.ts +1 -1
  49. package/dist/worker-protocol/index.js +1 -1
  50. package/package.json +2 -2
  51. package/postgres-feature-profile.json +6 -1
  52. package/sql-feature-matrix.json +20 -27
  53. package/dist/date-value.d.ts +0 -20
  54. package/dist/engine/artifact-cache.d.ts +0 -29
  55. package/dist/engine/byte-estimates.d.ts +0 -11
  56. package/dist/engine/cancellation.d.ts +0 -2
  57. package/dist/engine/defaults.d.ts +0 -29
  58. package/dist/engine/group-index.d.ts +0 -33
  59. package/dist/engine/join-index.d.ts +0 -10
  60. package/dist/engine/live-equal.d.ts +0 -7
  61. package/dist/engine/point-read.d.ts +0 -59
  62. package/dist/engine/query-cache.d.ts +0 -21
  63. package/dist/engine/result-wire.d.ts +0 -70
  64. package/dist/engine/sort-keys.d.ts +0 -73
  65. package/dist/engine/sql-domains.d.ts +0 -93
  66. package/dist/engine/sql-functions.d.ts +0 -11
  67. package/dist/engine/sql-json.d.ts +0 -40
  68. package/dist/engine/sql-semantics.d.ts +0 -66
  69. package/dist/engine/worker-store-indexeddb.d.ts +0 -2
  70. package/dist/engine/worker-store-memory.d.ts +0 -2
  71. package/dist/engine/worker-store-opfs.d.ts +0 -2
  72. package/dist/engine/write-block-planner.d.ts +0 -19
  73. package/dist/storage/opfs/leader.d.ts +0 -460
  74. package/dist/storage/opfs/rpc.d.ts +0 -82
  75. package/dist/storage/opfs/snapshot-ledger.d.ts +0 -41
@@ -1,3 +1,8 @@
1
+ import { createLiveQueryPatch } from "./live-patch.js";
2
+ import { acceptLiveExecution } from "./live-accept.js";
3
+ import { copyQueryResult, queryResultRetainedBytes, sameQueryRow } from "./query-cache.js";
4
+ import { encodeQueryIdentity } from "./query-identity.js";
5
+ import { OpfsCoordinationError } from "../storage/types.js";
1
6
  import { LiveQueryLimitError } from "./errors.js";
2
7
  const DEFAULT_LIVE_QUERY_MAX_GROUPS = 256;
3
8
  const DEFAULT_LIVE_QUERY_MAX_SUBSCRIPTIONS = 1024;
@@ -6,12 +11,6 @@ const MAX_LIVE_QUERY_SUBSCRIPTIONS = 16384;
6
11
  const MAX_LIVE_QUERY_SETS_PER_DATABASE = 256;
7
12
  const LIVE_QUERY_EXECUTION_CONCURRENCY = 8;
8
13
  import { LiveQueryLimitError as LiveQueryLimitError2 } from "./errors.js";
9
- function sameQueryValue(left, right) {
10
- if (left instanceof Date || right instanceof Date) {
11
- return left instanceof Date && right instanceof Date && Object.is(dateMilliseconds(left), dateMilliseconds(right));
12
- }
13
- return Object.is(left, right);
14
- }
15
14
  function sameResult(left, right) {
16
15
  if (left.columns.length !== right.columns.length || left.rows.length !== right.rows.length) {
17
16
  return false;
@@ -29,75 +28,11 @@ function sameResult(left, right) {
29
28
  const rightRow = right.rows[rowIndex];
30
29
  if (leftRow === void 0 || rightRow === void 0)
31
30
  return false;
32
- if (leftRow === rightRow)
33
- continue;
34
- for (const column of columns) {
35
- if (!sameQueryValue(leftRow[column] ?? null, rightRow[column] ?? null))
36
- return false;
37
- }
31
+ if (!sameQueryRow(leftRow, rightRow, columns))
32
+ return false;
38
33
  }
39
34
  return true;
40
35
  }
41
- function cloneRow(row, columns) {
42
- const cloned = {};
43
- for (const column of columns) {
44
- const value = row[column] ?? null;
45
- const copy = value instanceof Date ? copyDate(value) : value;
46
- if (column === "__proto__") {
47
- Object.defineProperty(cloned, column, {
48
- value: copy,
49
- enumerable: true,
50
- configurable: true,
51
- writable: true
52
- });
53
- } else
54
- cloned[column] = copy;
55
- }
56
- return cloned;
57
- }
58
- function cloneResult(result) {
59
- const columns = [...result.columns];
60
- return {
61
- columns,
62
- columnDomains: structuredClone(result.columnDomains),
63
- rows: result.rows.map((row) => cloneRow(row, columns))
64
- };
65
- }
66
- function encodeQueryIdentity(value, ancestors = /* @__PURE__ */ new Set()) {
67
- if (value === null)
68
- return "z";
69
- if (typeof value === "undefined")
70
- return "u";
71
- if (typeof value === "boolean")
72
- return value ? "b1" : "b0";
73
- if (typeof value === "number") {
74
- if (Number.isNaN(value))
75
- return "nNaN;";
76
- if (Object.is(value, -0))
77
- return "n-0;";
78
- return `n${String(value)};`;
79
- }
80
- if (typeof value === "string")
81
- return `s${String(value.length)}:${value}`;
82
- if (value instanceof Date)
83
- return `d${String(dateMilliseconds(value))};`;
84
- if (typeof value !== "object") {
85
- throw new TypeError(`Unsupported live-query identity value: ${typeof value}`);
86
- }
87
- if (ancestors.has(value))
88
- throw new TypeError("Live-query identity contains a cycle");
89
- ancestors.add(value);
90
- let encoded;
91
- if (Array.isArray(value)) {
92
- encoded = `a${String(value.length)}[${value.map((item) => encodeQueryIdentity(item, ancestors)).join("")}]`;
93
- } else {
94
- const record = value;
95
- const keys = Object.keys(record).sort();
96
- encoded = `o${String(keys.length)}{${keys.map((key) => `${encodeQueryIdentity(key, ancestors)}${encodeQueryIdentity(record[key], ancestors)}`).join("")}}`;
97
- }
98
- ancestors.delete(value);
99
- return encoded;
100
- }
101
36
  function queryKey(query) {
102
37
  return encodeQueryIdentity(typeof query === "string" ? ["sql", query] : query.kind === "sql-query" ? ["sql-query", query.sql, query.params] : ["typed-query", query.plan]);
103
38
  }
@@ -134,13 +69,19 @@ function groupExecutes(group) {
134
69
  }
135
70
  return false;
136
71
  }
137
- function groupMemoizes(group) {
72
+ function groupMemoizes(group, changed = true) {
138
73
  for (const subscriber of group.subscribers) {
139
- if (!subscriber.closed && subscriber.kind === "observer")
74
+ if (!subscriber.closed && subscriber.kind === "observer" && (changed || subscriber.options.suppressUnchanged !== true))
140
75
  return true;
141
76
  }
142
77
  return false;
143
78
  }
79
+ function callLiveCallback(callback) {
80
+ try {
81
+ callback?.();
82
+ } catch {
83
+ }
84
+ }
144
85
  class LiveQuerySet {
145
86
  #host;
146
87
  #channel;
@@ -153,6 +94,7 @@ class LiveQuerySet {
153
94
  #lagging = /* @__PURE__ */ new Set();
154
95
  #maxGroups;
155
96
  #maxSubscriptions;
97
+ #maxRetainedBytes;
156
98
  #sharedResults;
157
99
  #incremental;
158
100
  #stats = {
@@ -167,6 +109,7 @@ class LiveQuerySet {
167
109
  maintained: 0,
168
110
  groupsVisited: 0,
169
111
  retainedRows: 0,
112
+ retainedBytes: 0,
170
113
  sharedExecutions: 0,
171
114
  lastSweepMs: 0
172
115
  };
@@ -176,9 +119,12 @@ class LiveQuerySet {
176
119
  #executionWaiters = [];
177
120
  #sweepChain = Promise.resolve();
178
121
  #sweepQueued = false;
122
+ #retryTimer;
123
+ #retryDelayMs = 100;
179
124
  #pollTimer;
180
125
  #closed = false;
181
126
  #subscriptionCount = 0;
127
+ #retainedBytes = 0;
182
128
  #ownsChannel;
183
129
  #onClosed;
184
130
  constructor(host, options = {}) {
@@ -187,6 +133,9 @@ class LiveQuerySet {
187
133
  }
188
134
  this.#maxGroups = boundedLiveLimit(options.maxGroups ?? DEFAULT_LIVE_QUERY_MAX_GROUPS, MAX_LIVE_QUERY_GROUPS, "group");
189
135
  this.#maxSubscriptions = boundedLiveLimit(options.maxSubscriptions ?? DEFAULT_LIVE_QUERY_MAX_SUBSCRIPTIONS, MAX_LIVE_QUERY_SUBSCRIPTIONS, "subscription");
136
+ this.#maxRetainedBytes = options.maxRetainedBytes ?? 64 * 1024 * 1024;
137
+ if (!Number.isSafeInteger(this.#maxRetainedBytes) || this.#maxRetainedBytes < 0)
138
+ throw new RangeError("Live query retained byte limit must be a non-negative safe integer");
190
139
  this.#sharedResults = options.sharedResults === true;
191
140
  this.#incremental = options.incremental !== false;
192
141
  this.#host = host;
@@ -212,7 +161,7 @@ class LiveQuerySet {
212
161
  let retainedRows = 0;
213
162
  for (const group of this.#groups.values())
214
163
  retainedRows += group.result?.rows.length ?? 0;
215
- return { ...this.#stats, retainedRows };
164
+ return { ...this.#stats, retainedRows, retainedBytes: this.#retainedBytes };
216
165
  }
217
166
  #freshProbe() {
218
167
  let pending = this.#pendingProbe;
@@ -236,72 +185,59 @@ class LiveQuerySet {
236
185
  }
237
186
  this.#subscriptionCount += 1;
238
187
  }
239
- async subscribe(query, options) {
240
- this.#throwIfClosed();
241
- this.#reserveSubscription();
242
- let group;
243
- let subscriber;
244
- try {
245
- const opened = await this.#getOrOpenGroup(query);
246
- group = opened.group;
247
- this.#throwIfClosed();
248
- subscriber = {
249
- kind: "result",
250
- options,
251
- delivered: false,
252
- closed: false,
253
- seenDelivery: -1
254
- };
255
- group.subscribers.add(subscriber);
256
- await this.#settleGroup(group, opened.fresh, true);
257
- this.#throwIfClosed();
258
- if (!subscriber.delivered) {
259
- const result = group.result ?? (await this.#executeGroup(group)).result;
260
- this.#throwIfClosed();
261
- if (!subscriber.closed) {
262
- this.#deliverResult(group, subscriber, result, { ...group.seenProbe, initial: true });
263
- }
264
- }
265
- } catch (error) {
266
- if (group !== void 0 && subscriber !== void 0) {
267
- this.#removeSubscriber(group, subscriber, false);
268
- } else {
269
- this.#subscriptionCount -= 1;
270
- if (group !== void 0)
271
- this.#removeEmptyGroup(group);
272
- }
273
- throw error;
274
- }
275
- return this.#subscriptionHandle(group, subscriber);
188
+ subscribe(query, options) {
189
+ return this.#register(query, {
190
+ kind: "result",
191
+ options,
192
+ delivered: false,
193
+ closed: false,
194
+ seenDelivery: -1
195
+ });
196
+ }
197
+ subscribePatches(query, options) {
198
+ return this.subscribe(query, {
199
+ sharedResults: true,
200
+ onChange: (result, delivery) => options.onPatch(createLiveQueryPatch(result, delivery), delivery),
201
+ ...options.onError === void 0 ? {} : { onError: options.onError.bind(options) },
202
+ ...options.onComplete === void 0 ? {} : { onComplete: options.onComplete.bind(options) }
203
+ });
276
204
  }
277
- async observe(query, options) {
205
+ observe(query, options) {
206
+ return this.#register(query, { kind: "observer", options, delivered: false, closed: false });
207
+ }
208
+ async #register(query, subscriber) {
278
209
  this.#throwIfClosed();
279
210
  this.#reserveSubscription();
280
211
  let group;
281
- let subscriber;
212
+ let installed = false;
282
213
  try {
283
214
  const opened = await this.#getOrOpenGroup(query);
284
215
  group = opened.group;
285
216
  this.#throwIfClosed();
286
- subscriber = {
287
- kind: "observer",
288
- options,
289
- delivered: false,
290
- closed: false
291
- };
292
217
  group.subscribers.add(subscriber);
293
- await this.#settleGroup(group, opened.fresh, options.suppressUnchanged === true);
218
+ group.pendingSubscriptions -= 1;
219
+ installed = true;
220
+ await this.#settleGroup(group, opened.fresh, subscriber.kind === "result" || subscriber.options.suppressUnchanged === true);
294
221
  this.#throwIfClosed();
295
222
  if (!subscriber.delivered) {
296
- this.#deliverInvalidation(subscriber, { ...group.seenProbe, initial: true });
223
+ if (subscriber.kind === "observer")
224
+ this.#deliverInvalidation(subscriber, { ...group.seenProbe, initial: true });
225
+ else {
226
+ const result = group.result ?? (await this.#executeGroup(group)).result;
227
+ this.#throwIfClosed();
228
+ if (!subscriber.closed)
229
+ this.#deliverResult(group, subscriber, result, { ...group.seenProbe, initial: true });
230
+ }
297
231
  }
298
232
  } catch (error) {
299
- if (group !== void 0 && subscriber !== void 0) {
233
+ if (group !== void 0 && installed) {
300
234
  this.#removeSubscriber(group, subscriber, false);
301
235
  } else {
302
236
  this.#subscriptionCount -= 1;
303
- if (group !== void 0)
237
+ if (group !== void 0) {
238
+ group.pendingSubscriptions -= 1;
304
239
  this.#removeEmptyGroup(group);
240
+ }
305
241
  }
306
242
  throw error;
307
243
  }
@@ -337,30 +273,39 @@ class LiveQuerySet {
337
273
  subscriber.closed = true;
338
274
  this.#subscriptionCount -= 1;
339
275
  group.subscribers.delete(subscriber);
340
- if (complete)
341
- subscriber.options.onComplete?.();
342
276
  this.#removeEmptyGroup(group);
277
+ if (complete)
278
+ callLiveCallback(() => subscriber.options.onComplete?.());
343
279
  }
344
280
  #removeEmptyGroup(group) {
345
- if (group.subscribers.size !== 0)
281
+ if (group.subscribers.size !== 0 || group.pendingSubscriptions !== 0)
346
282
  return;
347
283
  if (this.#groups.get(group.key) !== group)
348
284
  return;
349
285
  this.#groups.delete(group.key);
286
+ this.#retainedBytes -= group.retainedBytes;
287
+ group.retainedBytes = 0;
288
+ group.result = void 0;
289
+ group.maintenance = void 0;
350
290
  this.#lagging.delete(group);
351
291
  this.#unindexGroup(group, group.dependencies);
352
292
  }
353
293
  async #getOrOpenGroup(query) {
294
+ if (typeof query !== "string")
295
+ query = structuredClone(query);
354
296
  const key = queryKey(query);
355
297
  const existing = this.#groups.get(key);
356
298
  if (existing !== void 0) {
357
299
  this.#stats.sharedExecutions += 1;
300
+ existing.pendingSubscriptions += 1;
358
301
  return { group: existing, fresh: false };
359
302
  }
360
303
  const opening = this.#opening.get(key);
361
304
  if (opening !== void 0) {
362
305
  this.#stats.sharedExecutions += 1;
363
- return { group: await opening, fresh: false };
306
+ const group = await opening;
307
+ group.pendingSubscriptions += 1;
308
+ return { group, fresh: false };
364
309
  }
365
310
  if (this.#groups.size + this.#opening.size >= this.#maxGroups) {
366
311
  throw new LiveQueryLimitError("group", this.#maxGroups);
@@ -368,7 +313,9 @@ class LiveQuerySet {
368
313
  const created = this.#openGroup(key, query);
369
314
  this.#opening.set(key, created);
370
315
  try {
371
- return { group: await created, fresh: true };
316
+ const group = await created;
317
+ group.pendingSubscriptions += 1;
318
+ return { group, fresh: true };
372
319
  } finally {
373
320
  this.#opening.delete(key);
374
321
  }
@@ -380,8 +327,10 @@ class LiveQuerySet {
380
327
  query,
381
328
  dependencies,
382
329
  subscribers: /* @__PURE__ */ new Set(),
330
+ pendingSubscriptions: 0,
383
331
  seenProbe: after,
384
332
  result: void 0,
333
+ retainedBytes: 0,
385
334
  execution: void 0,
386
335
  deliveries: 0,
387
336
  maintenance: void 0,
@@ -445,14 +394,18 @@ class LiveQuerySet {
445
394
  await this.#acquireExecutionSlot();
446
395
  let executed;
447
396
  let maintenance;
397
+ let retainedBytes;
398
+ let candidate;
448
399
  try {
449
400
  if (this.#incremental && this.#host.executeMaintainable !== void 0 && !group.unmaintainable) {
450
401
  const maintained = await this.#host.executeMaintainable(group.query, context);
451
402
  if (maintained === void 0)
452
403
  group.unmaintainable = true;
453
404
  else {
405
+ candidate = maintained;
454
406
  executed = maintained.result;
455
407
  maintenance = maintained.state;
408
+ retainedBytes = maintained.retainedBytes;
456
409
  }
457
410
  }
458
411
  executed ??= await this.#host.execute(group.query, context);
@@ -461,8 +414,9 @@ class LiveQuerySet {
461
414
  }
462
415
  const previous = group.result;
463
416
  const changed = previous === void 0 || !sameResult(previous, executed);
464
- group.result = executed;
465
- group.maintenance = maintenance;
417
+ this.#retain(group, executed, maintenance, retainedBytes, candidate);
418
+ if (candidate !== void 0 && context !== void 0 && groupMemoizes(group, changed))
419
+ await this.#memoizeGroup(group, executed, context.probe);
466
420
  return { result: executed, changed };
467
421
  })();
468
422
  group.execution = execution;
@@ -473,6 +427,28 @@ class LiveQuerySet {
473
427
  group.execution = void 0;
474
428
  }
475
429
  }
430
+ async #memoizeGroup(group, result, probe) {
431
+ if (this.#closed || this.#groups.get(group.key) !== group)
432
+ return;
433
+ try {
434
+ await this.#host.memoize?.(group.query, result, probe);
435
+ } catch {
436
+ }
437
+ }
438
+ #retain(group, result, state, hint, candidate) {
439
+ if (this.#closed || this.#groups.get(group.key) !== group)
440
+ return;
441
+ const bytes = hint ?? (result === group.result && state === group.maintenance ? group.retainedBytes : queryResultRetainedBytes(result) + result.rows.length * 48);
442
+ const total = this.#retainedBytes - group.retainedBytes + bytes;
443
+ if (!Number.isSafeInteger(bytes) || bytes < 0 || !Number.isSafeInteger(total) || total > this.#maxRetainedBytes)
444
+ throw new LiveQueryLimitError("byte", this.#maxRetainedBytes);
445
+ if (candidate !== void 0)
446
+ acceptLiveExecution(candidate);
447
+ group.result = result;
448
+ group.maintenance = state;
449
+ group.retainedBytes = bytes;
450
+ this.#retainedBytes = total;
451
+ }
476
452
  async #acquireExecutionSlot() {
477
453
  if (this.#executing < LIVE_QUERY_EXECUTION_CONCURRENCY) {
478
454
  this.#executing += 1;
@@ -509,8 +485,9 @@ class LiveQuerySet {
509
485
  verdict.declined = true;
510
486
  return { result: retained, changed: false };
511
487
  }
512
- group.result = maintained.result;
513
- group.maintenance = maintained.state;
488
+ this.#retain(group, maintained.result, maintained.state, maintained.retainedBytes, maintained);
489
+ if (groupMemoizes(group, maintained.changed))
490
+ await this.#memoizeGroup(group, maintained.result, probe);
514
491
  return {
515
492
  result: maintained.result,
516
493
  changed: maintained.changed,
@@ -531,7 +508,7 @@ class LiveQuerySet {
531
508
  return;
532
509
  subscriber.delivered = true;
533
510
  const consecutive = subscriber.seenDelivery === group.deliveries - 1;
534
- subscriber.options.onChange(this.#sharedResults ? result : cloneResult(result), retained !== void 0 && consecutive && !delivery.initial ? { ...delivery, retained } : delivery);
511
+ subscriber.options.onChange(this.#sharedResults || subscriber.options.sharedResults === true ? result : copyQueryResult(result), retained !== void 0 && consecutive && !delivery.initial ? { ...delivery, retained } : delivery);
535
512
  subscriber.seenDelivery = group.deliveries;
536
513
  }
537
514
  #deliverInvalidation(subscriber, invalidation) {
@@ -557,11 +534,17 @@ class LiveQuerySet {
557
534
  this.#channel?.close?.();
558
535
  if (this.#pollTimer !== void 0)
559
536
  clearInterval(this.#pollTimer);
537
+ if (this.#retryTimer !== void 0)
538
+ clearTimeout(this.#retryTimer);
560
539
  const groups = [...this.#groups.values()];
561
540
  this.#groups.clear();
541
+ this.#retainedBytes = 0;
562
542
  this.#groupsByTable.clear();
563
543
  this.#lagging.clear();
564
544
  for (const group of groups) {
545
+ group.result = void 0;
546
+ group.maintenance = void 0;
547
+ group.retainedBytes = 0;
565
548
  const subscribers = [...group.subscribers];
566
549
  group.subscribers.clear();
567
550
  for (const subscriber of subscribers) {
@@ -569,28 +552,46 @@ class LiveQuerySet {
569
552
  continue;
570
553
  subscriber.closed = true;
571
554
  this.#subscriptionCount -= 1;
572
- subscriber.options.onComplete?.();
555
+ callLiveCallback(() => subscriber.options.onComplete?.());
573
556
  }
574
557
  }
575
- this.#onClosed?.();
558
+ callLiveCallback(this.#onClosed);
576
559
  }
577
560
  #hint() {
578
561
  if (this.#closed)
579
562
  return;
580
563
  this.#stats.hints += 1;
564
+ if (this.#retryTimer !== void 0)
565
+ return;
581
566
  if (this.#sweepQueued)
582
567
  return;
583
568
  this.#sweepQueued = true;
584
569
  this.#sweepChain = this.#sweepChain.then(async () => {
585
570
  this.#sweepQueued = false;
571
+ if (this.#retryTimer !== void 0)
572
+ return;
586
573
  try {
587
574
  await this.#sweep();
575
+ this.#retryDelayMs = 100;
588
576
  } catch (error) {
577
+ if (error instanceof OpfsCoordinationError) {
578
+ this.#retrySweep();
579
+ return;
580
+ }
589
581
  for (const group of this.#groups.values())
590
582
  this.#notifyGroupError(group, error);
591
583
  }
592
584
  });
593
585
  }
586
+ #retrySweep() {
587
+ if (this.#closed || this.#groups.size === 0 || this.#retryTimer !== void 0)
588
+ return;
589
+ this.#retryTimer = setTimeout(() => {
590
+ this.#retryTimer = void 0;
591
+ this.#hint();
592
+ }, this.#retryDelayMs);
593
+ this.#retryDelayMs = Math.min(this.#retryDelayMs * 2, 5e3);
594
+ }
594
595
  #stillOpen() {
595
596
  return !this.#closed;
596
597
  }
@@ -668,6 +669,8 @@ class LiveQuerySet {
668
669
  await this.#refreshDependencies(group);
669
670
  } catch (error) {
670
671
  this.#lagging.add(group);
672
+ if (error instanceof OpfsCoordinationError)
673
+ throw error;
671
674
  this.#notifyGroupError(group, error);
672
675
  return;
673
676
  }
@@ -735,29 +738,31 @@ class LiveQuerySet {
735
738
  if (isObserver(subscriber)) {
736
739
  if (execution !== void 0 && !execution.changed && subscriber.options.suppressUnchanged === true) {
737
740
  this.#stats.notificationsSuppressed += 1;
738
- return;
741
+ continue;
739
742
  }
740
743
  try {
741
744
  this.#deliverInvalidation(subscriber, invalidation);
742
745
  this.#stats.invalidations += 1;
743
746
  } catch (error) {
744
- subscriber.options.onError?.(error);
747
+ callLiveCallback(() => subscriber.options.onError?.(error));
745
748
  }
746
749
  } else if (isResultSubscriber(subscriber) && execution !== void 0) {
747
750
  if (!execution.changed) {
748
751
  this.#stats.notificationsSuppressed += 1;
749
- return;
752
+ continue;
750
753
  }
751
754
  try {
752
755
  this.#deliverResult(group, subscriber, execution.result, invalidation, execution.retained);
753
756
  } catch (error) {
754
- subscriber.options.onError?.(error);
757
+ callLiveCallback(() => subscriber.options.onError?.(error));
755
758
  }
756
759
  }
757
760
  }
758
761
  this.#settleProbe(group, current);
759
762
  } catch (error) {
760
763
  this.#lagging.add(group);
764
+ if (error instanceof OpfsCoordinationError)
765
+ throw error;
761
766
  this.#notifyGroupError(group, error);
762
767
  }
763
768
  }
@@ -769,7 +774,7 @@ class LiveQuerySet {
769
774
  for (const subscriber of group.subscribers) {
770
775
  if (subscriber.closed)
771
776
  continue;
772
- subscriber.options.onError?.(error);
777
+ callLiveCallback(() => subscriber.options.onError?.(error));
773
778
  }
774
779
  }
775
780
  async #changedTablesSince(after, until) {
@@ -797,7 +802,6 @@ class LiveQuerySet {
797
802
  }
798
803
  }
799
804
  }
800
- import { copyDate, dateMilliseconds } from "../date-value.js";
801
805
  export {
802
806
  DEFAULT_LIVE_QUERY_MAX_GROUPS,
803
807
  DEFAULT_LIVE_QUERY_MAX_SUBSCRIPTIONS,
@@ -1,3 +1,4 @@
1
+ import { encodeQueryIdentity } from "./query-identity.js";
1
2
  import { dateIsoString, dateMilliseconds } from "../date-value.js";
2
3
  import { crossJoinPlan } from "../plan/model.js";
3
4
  import { blockHasRowWindow, blockHasSubqueries, childExpressions, DUAL_TABLE, expressionAliases, forEachBlockExpression, forEachNestedBlock, hasAggregate, isAggregateCall, isScalarFunctionName, mapBlockExpressions, mapChildExpressions, parseQuantified, scalarFunctionNames, scalarFunctionValue, splitCondition, statementDatetimeNames, volatileScalarFunctionNames, transparentProjectionSource, dateTruncValue, integerQuotient } from "./query.js";
@@ -557,7 +558,7 @@ function propagateJoinKeyConstants(block) {
557
558
  }
558
559
  if (pairs.length === 0)
559
560
  return;
560
- const signature = (predicate) => JSON.stringify(predicate);
561
+ const signature = (predicate) => encodeQueryIdentity(predicate);
561
562
  const present = new Set(block.predicates.map(signature));
562
563
  const implied = [];
563
564
  const constantSide = (predicate) => {
@@ -785,9 +786,9 @@ function decorrelateBlock(block, nextAlias) {
785
786
  if (!expressionHasCorrelatedSubquery(item.expression))
786
787
  continue;
787
788
  if (grouped) {
788
- const groupedExpressions = new Set(block.groupBy.map((group) => JSON.stringify(group)));
789
+ const groupedExpressions = new Set(block.groupBy.map((group) => encodeQueryIdentity(group)));
789
790
  const outerReferences = correlatedOuterReferences(item.expression);
790
- if (containsAggregateCall(item.expression) || outerReferences.some((reference) => !groupedExpressions.has(JSON.stringify({ kind: "column", reference })))) {
791
+ if (containsAggregateCall(item.expression) || outerReferences.some((reference) => !groupedExpressions.has(encodeQueryIdentity({ kind: "column", reference })))) {
791
792
  throw new TypeError("A grouped correlated select-list subquery must reference only GROUP BY columns and cannot be nested inside an aggregate");
792
793
  }
793
794
  }
@@ -2428,7 +2429,11 @@ function foldExpression(expression) {
2428
2429
  if (folded === null || typeof folded === "string" || typeof folded === "boolean" || folded instanceof Date || typeof folded === "number" && Number.isFinite(folded)) {
2429
2430
  const target = foldedArguments[1];
2430
2431
  const targetWord = expression.name === "CAST" && target?.kind === "literal" && typeof target.value === "string" ? target.value : void 0;
2431
- const sqlDomain = targetWord?.startsWith("numeric:") === true ? (() => {
2432
+ const first = literalValues[0];
2433
+ const sqlDomain = expression.name === "ARRAY" ? {
2434
+ kind: "array",
2435
+ element: typeof first === "number" ? "DOUBLE" : typeof first === "boolean" ? "BOOLEAN" : first instanceof Date ? "TIMESTAMP" : "TEXT"
2436
+ } : targetWord?.startsWith("numeric:") === true ? (() => {
2432
2437
  const [, precisionWord = "", scaleWord = ""] = targetWord.split(":");
2433
2438
  return {
2434
2439
  kind: "numeric",
@@ -2565,8 +2570,8 @@ function rewriteForInner(expression, source, derived, singleSource) {
2565
2570
  if (containsAggregateOrWindow(item.expression))
2566
2571
  return void 0;
2567
2572
  if (derived.groupBy.length > 0) {
2568
- const signature = JSON.stringify(item.expression);
2569
- if (!derived.groupBy.some((group) => JSON.stringify(group) === signature)) {
2573
+ const signature = encodeQueryIdentity(item.expression);
2574
+ if (!derived.groupBy.some((group) => encodeQueryIdentity(group) === signature)) {
2570
2575
  return void 0;
2571
2576
  }
2572
2577
  }
@@ -1,25 +1,28 @@
1
+ import { encodeQueryIdentity } from "./query-identity.js";
1
2
  import { copyDate, dateMilliseconds } from "../date-value.js";
2
3
  import { estimateValuesBytes } from "./byte-estimates.js";
3
- import { copyQueryResultExternalization } from "./query.js";
4
+ import { copyQueryResultExternalization } from "./result-state.js";
4
5
  import { defineSqlResultProperty } from "./sql-semantics.js";
5
6
  const RESULT_MEMO_MAX_BYTES = 4 * 1024 * 1024;
7
+ function sameQueryRow(left, right, columns) {
8
+ if (left === right)
9
+ return true;
10
+ for (const column of columns) {
11
+ const a = left[column] ?? null;
12
+ const b = right[column] ?? null;
13
+ if (a instanceof Date || b instanceof Date) {
14
+ if (!(a instanceof Date && b instanceof Date) || !Object.is(dateMilliseconds(a), dateMilliseconds(b)))
15
+ return false;
16
+ } else if (!Object.is(a, b))
17
+ return false;
18
+ }
19
+ return true;
20
+ }
6
21
  function queryResultMemoKey(sql, params) {
7
22
  return JSON.stringify([sql, params.map(encodeParameter)]);
8
23
  }
9
24
  function planMemoKey(plan) {
10
- return JSON.stringify(plan, (_key, value) => {
11
- if (value instanceof Date)
12
- return { $date: dateMilliseconds(value) };
13
- if (typeof value === "bigint")
14
- return { $bigint: value.toString() };
15
- if (typeof value === "number") {
16
- if (Object.is(value, -0))
17
- return { $number: "-0" };
18
- if (!Number.isFinite(value))
19
- return { $number: String(value) };
20
- }
21
- return value;
22
- });
25
+ return encodeQueryIdentity(plan);
23
26
  }
24
27
  function copyQueryResult(result) {
25
28
  const columns = result.columns;
@@ -67,5 +70,6 @@ export {
67
70
  copyQueryResult,
68
71
  planMemoKey,
69
72
  queryResultMemoKey,
70
- queryResultRetainedBytes
73
+ queryResultRetainedBytes,
74
+ sameQueryRow
71
75
  };