@syncular/client 0.25.1 → 0.25.2

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.
@@ -240,6 +240,27 @@ class ObservationCache {
240
240
  this.#active.clear();
241
241
  }
242
242
  }
243
+ const EMPTY_ROWS = Object.freeze([]);
244
+ const unreadQuerySnapshots = new Map();
245
+ /** One frozen snapshot per phase and availability for entries that have no
246
+ * successful read, so switching between unread entries keeps the snapshot and
247
+ * `rows` identity. */
248
+ function unreadQuerySnapshot(phase, availability) {
249
+ const key = canonicalValue([phase, availability]);
250
+ let snapshot = unreadQuerySnapshots.get(key);
251
+ if (snapshot === undefined) {
252
+ snapshot = Object.freeze({
253
+ rows: EMPTY_ROWS,
254
+ phase,
255
+ revision: undefined,
256
+ error: undefined,
257
+ isRefreshing: false,
258
+ availability,
259
+ });
260
+ unreadQuerySnapshots.set(key, snapshot);
261
+ }
262
+ return snapshot;
263
+ }
243
264
  class QueryEntry {
244
265
  store;
245
266
  spec;
@@ -247,14 +268,9 @@ class QueryEntry {
247
268
  cache;
248
269
  #owner = Symbol('query-window-claim');
249
270
  #listeners = new Set();
250
- #state = {
251
- rows: [],
252
- phase: 'loading',
253
- revision: undefined,
254
- error: undefined,
255
- isRefreshing: false,
256
- availability: { state: 'ready' },
257
- };
271
+ #state = unreadQuerySnapshot('loading', {
272
+ state: 'ready',
273
+ });
258
274
  #delegate;
259
275
  #generation = 0;
260
276
  #scheduled = false;
@@ -351,14 +367,7 @@ class QueryEntry {
351
367
  this.#desiredRevision = 0n;
352
368
  this.#claimError = undefined;
353
369
  this.#claimPending = false;
354
- this.#state = {
355
- rows: [],
356
- phase: 'loading',
357
- revision: undefined,
358
- error: undefined,
359
- isRefreshing: false,
360
- availability: { state: 'ready' },
361
- };
370
+ this.#state = unreadQuerySnapshot('loading', { state: 'ready' });
362
371
  }
363
372
  dispose() {
364
373
  this.#listeners.clear();
@@ -385,7 +394,13 @@ class QueryEntry {
385
394
  canonicalValue(this.#state.availability)) {
386
395
  return;
387
396
  }
388
- this.#state = next;
397
+ this.#state =
398
+ next.revision === undefined &&
399
+ next.rows.length === 0 &&
400
+ next.error === undefined &&
401
+ !next.isRefreshing
402
+ ? unreadQuerySnapshot(next.phase, next.availability)
403
+ : next;
389
404
  for (const listener of this.#listeners)
390
405
  listener();
391
406
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@syncular/client",
3
- "version": "0.25.1",
3
+ "version": "0.25.2",
4
4
  "description": "Syncular TypeScript client core — offline-first sync over SQLite (WASM/OPFS, Bun, Node)",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Benjamin Kniffler",
@@ -89,10 +89,10 @@
89
89
  },
90
90
  "dependencies": {
91
91
  "@sqlite.org/sqlite-wasm": "^3.53.0-build1",
92
- "@syncular/core": "0.25.1"
92
+ "@syncular/core": "0.25.2"
93
93
  },
94
94
  "devDependencies": {
95
- "@syncular/server": "0.25.1",
95
+ "@syncular/server": "0.25.2",
96
96
  "playwright": "1.63.0"
97
97
  }
98
98
  }
@@ -360,17 +360,38 @@ class ObservationCache {
360
360
  }
361
361
  }
362
362
 
363
+ const EMPTY_ROWS: readonly never[] = Object.freeze([]);
364
+ const unreadQuerySnapshots = new Map<string, LiveQueryResult<never>>();
365
+
366
+ /** One frozen snapshot per phase and availability for entries that have no
367
+ * successful read, so switching between unread entries keeps the snapshot and
368
+ * `rows` identity. */
369
+ function unreadQuerySnapshot(
370
+ phase: LiveQueryPhase,
371
+ availability: SyncAvailability,
372
+ ): LiveQueryResult<never> {
373
+ const key = canonicalValue([phase, availability]);
374
+ let snapshot = unreadQuerySnapshots.get(key);
375
+ if (snapshot === undefined) {
376
+ snapshot = Object.freeze({
377
+ rows: EMPTY_ROWS,
378
+ phase,
379
+ revision: undefined,
380
+ error: undefined,
381
+ isRefreshing: false,
382
+ availability,
383
+ });
384
+ unreadQuerySnapshots.set(key, snapshot);
385
+ }
386
+ return snapshot;
387
+ }
388
+
363
389
  class QueryEntry<Row> implements ExternalStoreEntry<LiveQueryResult<Row>> {
364
390
  readonly #owner = Symbol('query-window-claim');
365
391
  readonly #listeners = new Set<() => void>();
366
- #state: LiveQueryResult<Row> = {
367
- rows: [],
368
- phase: 'loading',
369
- revision: undefined,
370
- error: undefined,
371
- isRefreshing: false,
372
- availability: { state: 'ready' },
373
- };
392
+ #state: LiveQueryResult<Row> = unreadQuerySnapshot('loading', {
393
+ state: 'ready',
394
+ });
374
395
  #delegate: QueryEntry<Row> | undefined;
375
396
  #generation = 0;
376
397
  #scheduled = false;
@@ -476,14 +497,7 @@ class QueryEntry<Row> implements ExternalStoreEntry<LiveQueryResult<Row>> {
476
497
  this.#desiredRevision = 0n;
477
498
  this.#claimError = undefined;
478
499
  this.#claimPending = false;
479
- this.#state = {
480
- rows: [],
481
- phase: 'loading',
482
- revision: undefined,
483
- error: undefined,
484
- isRefreshing: false,
485
- availability: { state: 'ready' },
486
- };
500
+ this.#state = unreadQuerySnapshot('loading', { state: 'ready' });
487
501
  }
488
502
 
489
503
  dispose(): void {
@@ -513,7 +527,13 @@ class QueryEntry<Row> implements ExternalStoreEntry<LiveQueryResult<Row>> {
513
527
  ) {
514
528
  return;
515
529
  }
516
- this.#state = next;
530
+ this.#state =
531
+ next.revision === undefined &&
532
+ next.rows.length === 0 &&
533
+ next.error === undefined &&
534
+ !next.isRefreshing
535
+ ? unreadQuerySnapshot(next.phase, next.availability)
536
+ : next;
517
537
  for (const listener of this.#listeners) listener();
518
538
  }
519
539