@spooky-sync/core 0.0.1-canary.130 → 0.0.1-canary.132

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.
package/dist/index.d.ts CHANGED
@@ -522,13 +522,6 @@ declare class DataModule<S extends SchemaStructure> {
522
522
  fetchedAt: number;
523
523
  rowCount: number;
524
524
  } | null>;
525
- /**
526
- * True when this query's preload marker exists and is younger than
527
- * `maxAgeMs`. Used by instant-hydrate to skip its one-shot fetch for rows a
528
- * recent `preload()` already persisted (the register lifecycle re-syncs them
529
- * authoritatively). Missing or unreadable markers are treated as stale.
530
- */
531
- isPreloadFresh(hashKey: string, maxAgeMs: number): Promise<boolean>;
532
525
  /** Stamp the preload freshness marker after a successful snapshot fetch. */
533
526
  writePreloadMarker(hash: string, rowCount: number): Promise<void>;
534
527
  /** True while ≥1 live subscriber is watching this query (refcount guard). */
@@ -1303,19 +1296,14 @@ declare class Sp00kyClient<S extends SchemaStructure> {
1303
1296
  query<Table extends TableNames<S>>(table: Table, options: QueryOptions<TableModel<GetTable<S, Table>>, false>, ttl?: QueryTimeToLive): QueryBuilder<S, Table, Sp00kyQueryResultPromise>;
1304
1297
  private initQuery;
1305
1298
  /**
1306
- * Background tail of {@link initQuery}: instant-hydrate (when the query is
1307
- * cold AND wasn't freshly preloaded) followed by enqueuing the `register`
1308
- * down-event. Never rejects — both halves catch and log, so `void`-ing the
1309
- * returned promise can't produce an unhandled rejection.
1310
- *
1311
- * The fresh-preload skip: rows a recent `preload()` persisted are already in
1312
- * the local cache and were part of the first paint; the register lifecycle
1313
- * re-syncs them authoritatively, so the one-shot hydrate fetch would be a
1314
- * pure duplicate round trip. "Fresh" = preloaded this session
1315
- * (`preloadedHashes`) or a `_00_preload` marker younger than
1316
- * HYDRATE_PRELOAD_FRESH. Both are per-bucket (the Set is cleared on bucket
1317
- * switch, the marker table lives in the bucket), so neither can claim
1318
- * freshness across auth contexts.
1299
+ * Background tail of {@link initQuery}: instant-hydrate (opt-in via
1300
+ * `config.instantHydrate`, and only when the query is cold) followed by
1301
+ * enqueuing the `register` down-event. Never rejects — both halves catch and
1302
+ * log, so `void`-ing the returned promise can't produce an unhandled
1303
+ * rejection. By default (hydrate off) the register lifecycle is the single
1304
+ * freshness path; the one-shot fetch is an optimization apps enable
1305
+ * explicitly, and it runs regardless of preload state cache-first delivery
1306
+ * never depends on WHY rows are cached.
1319
1307
  */
1320
1308
  private finishQueryInit;
1321
1309
  /**
package/dist/index.js CHANGED
@@ -1283,25 +1283,36 @@ var SurrealCacheEngine = class extends LocalDatabaseService {
1283
1283
  }
1284
1284
  return result;
1285
1285
  }
1286
- async getById(_table, id) {
1287
- const [row] = await this.query("SELECT * FROM ONLY $__id;", { __id: id });
1288
- return row ?? null;
1286
+ /**
1287
+ * Coerce the contract's `Id` (a RecordId, a stable `table:id` string, or a
1288
+ * bare id + the verb's `table` param) to a real RecordId. SurrealDB binds
1289
+ * `$__id` verbatim: a plain string makes `FROM ONLY $__id` "select" the
1290
+ * string itself (a truthy non-row) and `UPSERT $__id` an InternalError, so
1291
+ * string ids silently broke every id-verb on this engine.
1292
+ */
1293
+ toRecordId(table, id) {
1294
+ if (typeof id !== "string") return id;
1295
+ return new RecordId(table, id.startsWith(`${table}:`) ? id.slice(table.length + 1) : id);
1296
+ }
1297
+ async getById(table, id) {
1298
+ const [row] = await this.query("SELECT * FROM ONLY $__id;", { __id: this.toRecordId(table, id) });
1299
+ return row && typeof row === "object" ? row : null;
1289
1300
  }
1290
- async upsert(_table, id, data, mode) {
1301
+ async upsert(table, id, data, mode) {
1291
1302
  const sql = mode === "merge" ? surql.upsertMerge("__id", "__data") : surql.upsert("__id", "__data");
1292
1303
  await this.query(surql.seal(sql), {
1293
- __id: id,
1304
+ __id: this.toRecordId(table, id),
1294
1305
  __data: data
1295
1306
  });
1296
1307
  }
1297
- async patch(_table, id, patches) {
1308
+ async patch(table, id, patches) {
1298
1309
  await this.query(surql.seal("UPDATE ONLY $__id PATCH $__patches"), {
1299
- __id: id,
1310
+ __id: this.toRecordId(table, id),
1300
1311
  __patches: patches
1301
1312
  });
1302
1313
  }
1303
- async delete(_table, id) {
1304
- await this.query(surql.seal(surql.delete("__id")), { __id: id });
1314
+ async delete(table, id) {
1315
+ await this.query(surql.seal(surql.delete("__id")), { __id: this.toRecordId(table, id) });
1305
1316
  }
1306
1317
  /**
1307
1318
  * Serialized (not strictly atomic) transaction: verbs run in order on the
@@ -2774,8 +2785,8 @@ var DataModule = class {
2774
2785
  */
2775
2786
  async getPreloadMarker(hash) {
2776
2787
  try {
2777
- const row = await this.local.getById("_00_preload", hash);
2778
- if (!row) return null;
2788
+ const row = await this.local.getById("_00_preload", new RecordId("_00_preload", hash));
2789
+ if (!row || typeof row !== "object") return null;
2779
2790
  return {
2780
2791
  fetchedAt: Number(row.fetchedAt) || 0,
2781
2792
  rowCount: Number(row.rowCount) || 0
@@ -2784,19 +2795,9 @@ var DataModule = class {
2784
2795
  return null;
2785
2796
  }
2786
2797
  }
2787
- /**
2788
- * True when this query's preload marker exists and is younger than
2789
- * `maxAgeMs`. Used by instant-hydrate to skip its one-shot fetch for rows a
2790
- * recent `preload()` already persisted (the register lifecycle re-syncs them
2791
- * authoritatively). Missing or unreadable markers are treated as stale.
2792
- */
2793
- async isPreloadFresh(hashKey, maxAgeMs) {
2794
- const marker = await this.getPreloadMarker(hashKey);
2795
- return marker !== null && Date.now() - marker.fetchedAt <= maxAgeMs;
2796
- }
2797
2798
  /** Stamp the preload freshness marker after a successful snapshot fetch. */
2798
2799
  async writePreloadMarker(hash, rowCount) {
2799
- await this.local.upsert("_00_preload", hash, {
2800
+ await this.local.upsert("_00_preload", new RecordId("_00_preload", hash), {
2800
2801
  fetchedAt: Date.now(),
2801
2802
  rowCount
2802
2803
  }, "replace");
@@ -5130,8 +5131,8 @@ function parseBackendInfo(raw) {
5130
5131
 
5131
5132
  //#endregion
5132
5133
  //#region src/modules/devtools/index.ts
5133
- const CORE_VERSION = "0.0.1-canary.130";
5134
- const WASM_VERSION = "0.0.1-canary.130";
5134
+ const CORE_VERSION = "0.0.1-canary.132";
5135
+ const WASM_VERSION = "0.0.1-canary.132";
5135
5136
  const SURREAL_VERSION = "3.0.3";
5136
5137
  var DevToolsService = class {
5137
5138
  eventsHistory = [];
@@ -7066,7 +7067,6 @@ var BucketHandle = class {
7066
7067
  * callback switches to the user's bucket (cache + outbox intact).
7067
7068
  */
7068
7069
  const LAST_BUCKET_KEY = "sp00ky:last_bucket";
7069
- const HYDRATE_PRELOAD_FRESH = "1h";
7070
7070
  function readBootBucketHint() {
7071
7071
  try {
7072
7072
  return typeof localStorage !== "undefined" ? localStorage.getItem(LAST_BUCKET_KEY) : null;
@@ -7421,30 +7421,20 @@ var Sp00kyClient = class {
7421
7421
  return hash;
7422
7422
  }
7423
7423
  /**
7424
- * Background tail of {@link initQuery}: instant-hydrate (when the query is
7425
- * cold AND wasn't freshly preloaded) followed by enqueuing the `register`
7426
- * down-event. Never rejects — both halves catch and log, so `void`-ing the
7427
- * returned promise can't produce an unhandled rejection.
7428
- *
7429
- * The fresh-preload skip: rows a recent `preload()` persisted are already in
7430
- * the local cache and were part of the first paint; the register lifecycle
7431
- * re-syncs them authoritatively, so the one-shot hydrate fetch would be a
7432
- * pure duplicate round trip. "Fresh" = preloaded this session
7433
- * (`preloadedHashes`) or a `_00_preload` marker younger than
7434
- * HYDRATE_PRELOAD_FRESH. Both are per-bucket (the Set is cleared on bucket
7435
- * switch, the marker table lives in the bucket), so neither can claim
7436
- * freshness across auth contexts.
7424
+ * Background tail of {@link initQuery}: instant-hydrate (opt-in via
7425
+ * `config.instantHydrate`, and only when the query is cold) followed by
7426
+ * enqueuing the `register` down-event. Never rejects — both halves catch and
7427
+ * log, so `void`-ing the returned promise can't produce an unhandled
7428
+ * rejection. By default (hydrate off) the register lifecycle is the single
7429
+ * freshness path; the one-shot fetch is an optimization apps enable
7430
+ * explicitly, and it runs regardless of preload state cache-first delivery
7431
+ * never depends on WHY rows are cached.
7437
7432
  */
7438
7433
  async finishQueryInit(hash, q, params) {
7439
- if (this.config.instantHydrate !== false && this.dataModule.isCold(hash)) try {
7440
- if (!(this.preloadedHashes.has(q.hash) || await this.dataModule.isPreloadFresh(String(q.hash), parseDuration(HYDRATE_PRELOAD_FRESH)))) {
7441
- const epoch = this.local.epoch;
7442
- const [rows] = await this.remote.query(q.selectQuery.query, params);
7443
- if (epoch === this.local.epoch) await this.dataModule.applyHydration(hash, rows ?? []);
7444
- } else this.logger.debug({
7445
- hash,
7446
- Category: "sp00ky-client::Sp00kyClient::instantHydrate"
7447
- }, "Skipping instant hydrate; preload marker fresh");
7434
+ if (this.config.instantHydrate === true && this.dataModule.isCold(hash)) try {
7435
+ const epoch = this.local.epoch;
7436
+ const [rows] = await this.remote.query(q.selectQuery.query, params);
7437
+ if (epoch === this.local.epoch) await this.dataModule.applyHydration(hash, rows ?? []);
7448
7438
  } catch (err) {
7449
7439
  if (err instanceof StaleEpochError) this.logger.debug({
7450
7440
  hash,
package/dist/types.d.ts CHANGED
@@ -450,16 +450,16 @@ interface Sp00kyConfig<S extends SchemaStructure> {
450
450
  */
451
451
  refSyncIntervalMs?: number;
452
452
  /**
453
- * Instant-hydrate cold queries: when a query is registered with no server
454
- * result yet, run its surql directly on the remote (one-shot) and display
455
- * the result as soon as it lands, while the full realtime registration
456
- * proceeds in the background. The hydrated rows are ingested with their
457
- * versions so the registration's `syncRecords` skips re-pulling unchanged
458
- * bodies. The fetch runs OFF the paint path — `useQuery` resolves and paints
459
- * from the local cache immediately, and hydrate only fills in what's
460
- * missing. Skipped entirely when the query was `preload()`ed recently (its
461
- * rows are already local and fresh; the registration re-syncs them), so a
462
- * preloaded screen costs zero duplicate fetches. Defaults to `true`.
453
+ * OPT-IN instant-hydrate for cold queries: when enabled and a query is
454
+ * registered with no server result yet, its surql also runs directly on the
455
+ * remote (one-shot, in the background, OFF the paint path) so rows can land
456
+ * before the full register lifecycle completes. Hydrated rows carry their
457
+ * `_00_rv` versions so the registration's `syncRecords` skips re-pulling
458
+ * unchanged bodies. Regardless of this flag, `useQuery` always resolves and
459
+ * paints from the local cache immediately however the rows got there
460
+ * (preload, prior sync). Default `false`: the register lifecycle
461
+ * (`fn::query::register` `_00_list_ref` record sync) is the single
462
+ * freshness path and no duplicate one-shot fetches are made.
463
463
  */
464
464
  instantHydrate?: boolean;
465
465
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spooky-sync/core",
3
- "version": "0.0.1-canary.130",
3
+ "version": "0.0.1-canary.132",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -59,8 +59,8 @@
59
59
  }
60
60
  },
61
61
  "dependencies": {
62
- "@spooky-sync/query-builder": "0.0.1-canary.130",
63
- "@spooky-sync/ssp-wasm": "0.0.1-canary.130",
62
+ "@spooky-sync/query-builder": "0.0.1-canary.132",
63
+ "@spooky-sync/ssp-wasm": "0.0.1-canary.132",
64
64
  "@sqlite.org/sqlite-wasm": "3.53.0-build1",
65
65
  "@surrealdb/wasm": "^3.0.3",
66
66
  "fast-json-patch": "^3.1.1",
@@ -4,11 +4,9 @@ import { DataModule } from './index';
4
4
  import type { QueryState } from '../../types';
5
5
 
6
6
  /**
7
- * Tests for instant-hydrate's DataModule half: `applyHydration` (run-once,
7
+ * Tests for instant-hydrate's DataModule half: `applyHydration` run-once,
8
8
  * remoteArray priming, subscriber notify, and the epoch guard added when the
9
- * hydrate fetch moved off the paint path into a background chain) and
10
- * `isPreloadFresh` (the marker check that lets a freshly-preloaded query skip
11
- * the duplicate one-shot fetch entirely).
9
+ * hydrate fetch moved off the paint path into a background chain.
12
10
  */
13
11
 
14
12
  function makeLogger(): any {
@@ -118,33 +116,27 @@ describe('DataModule.applyHydration', () => {
118
116
  });
119
117
  });
120
118
 
121
- describe('DataModule.isPreloadFresh', () => {
122
- const maxAgeMs = 60_000;
123
-
124
- function makeDm(getById: (table: string, id: string) => Promise<any>) {
119
+ describe('DataModule.getPreloadMarker', () => {
120
+ function makeDm(getById: (table: string, id: unknown) => Promise<any>) {
125
121
  const local: any = { getById };
126
122
  return new DataModule({} as any, local, schema, makeLogger(), 100);
127
123
  }
128
124
 
129
- it('true for a marker younger than maxAgeMs', async () => {
130
- const dm = makeDm(async () => ({ fetchedAt: Date.now() - 1_000, rowCount: 5 }));
131
- expect(await dm.isPreloadFresh('123', maxAgeMs)).toBe(true);
132
- });
133
-
134
- it('false for a marker older than maxAgeMs', async () => {
135
- const dm = makeDm(async () => ({ fetchedAt: Date.now() - maxAgeMs - 1, rowCount: 5 }));
136
- expect(await dm.isPreloadFresh('123', maxAgeMs)).toBe(false);
125
+ it('returns the marker fields for a real row', async () => {
126
+ const dm = makeDm(async () => ({ fetchedAt: 123, rowCount: 5 }));
127
+ expect(await dm.getPreloadMarker('h')).toEqual({ fetchedAt: 123, rowCount: 5 });
137
128
  });
138
129
 
139
- it('false when no marker exists', async () => {
140
- const dm = makeDm(async () => null);
141
- expect(await dm.isPreloadFresh('123', maxAgeMs)).toBe(false);
130
+ it('returns null on a miss and on a non-object echo (SurrealDB string quirk)', async () => {
131
+ expect(await makeDm(async () => null).getPreloadMarker('h')).toBeNull();
132
+ // `FROM ONLY <string>` used to echo the id string back — must not read as warm.
133
+ expect(await makeDm(async () => 'h' as any).getPreloadMarker('h')).toBeNull();
142
134
  });
143
135
 
144
- it('false when the marker read throws (treated as cold)', async () => {
136
+ it('returns null when the read throws', async () => {
145
137
  const dm = makeDm(async () => {
146
138
  throw new Error('boom');
147
139
  });
148
- expect(await dm.isPreloadFresh('123', maxAgeMs)).toBe(false);
140
+ expect(await dm.getPreloadMarker('h')).toBeNull();
149
141
  });
150
142
  });
@@ -876,8 +876,11 @@ export class DataModule<S extends SchemaStructure> {
876
876
  hash: string
877
877
  ): Promise<{ fetchedAt: number; rowCount: number } | null> {
878
878
  try {
879
- const row = await this.local.getById('_00_preload', hash);
880
- if (!row) return null;
879
+ // Pass a real RecordId: a bare string id hits the SurrealDB engine's
880
+ // `FROM ONLY $__id` as a plain string, which "selects" the string itself
881
+ // (a truthy non-row) instead of the record — misread as a warm marker.
882
+ const row = await this.local.getById('_00_preload', new RecordId('_00_preload', hash));
883
+ if (!row || typeof row !== 'object') return null;
881
884
  return {
882
885
  fetchedAt: Number((row as any).fetchedAt) || 0,
883
886
  rowCount: Number((row as any).rowCount) || 0,
@@ -887,22 +890,14 @@ export class DataModule<S extends SchemaStructure> {
887
890
  }
888
891
  }
889
892
 
890
- /**
891
- * True when this query's preload marker exists and is younger than
892
- * `maxAgeMs`. Used by instant-hydrate to skip its one-shot fetch for rows a
893
- * recent `preload()` already persisted (the register lifecycle re-syncs them
894
- * authoritatively). Missing or unreadable markers are treated as stale.
895
- */
896
- async isPreloadFresh(hashKey: string, maxAgeMs: number): Promise<boolean> {
897
- const marker = await this.getPreloadMarker(hashKey);
898
- return marker !== null && Date.now() - marker.fetchedAt <= maxAgeMs;
899
- }
900
-
901
893
  /** Stamp the preload freshness marker after a successful snapshot fetch. */
902
894
  async writePreloadMarker(hash: string, rowCount: number): Promise<void> {
895
+ // RecordId, not a bare string: the SurrealDB engine binds the id verbatim,
896
+ // and `UPSERT <string>` is an InternalError — the marker silently never
897
+ // landed (the write is awaited inside preload's best-effort catch).
903
898
  await this.local.upsert(
904
899
  '_00_preload',
905
- hash,
900
+ new RecordId('_00_preload', hash),
906
901
  { fetchedAt: Date.now(), rowCount },
907
902
  'replace'
908
903
  );
@@ -15,6 +15,7 @@ import type {
15
15
  } from './cache-engine';
16
16
  import { stableKey } from './relation-resolver';
17
17
  import { surql } from '../../utils/surql';
18
+ import { RecordId } from 'surrealdb';
18
19
 
19
20
  /**
20
21
  * Default local cache backend: the in-browser SurrealDB-WASM store. Implemented
@@ -83,25 +84,41 @@ export class SurrealCacheEngine extends LocalDatabaseService implements LocalCac
83
84
  return result;
84
85
  }
85
86
 
86
- async getById(_table: string, id: Id): Promise<Row | null> {
87
- const [row] = await this.query<[Row | null]>('SELECT * FROM ONLY $__id;', { __id: id });
88
- return row ?? null;
87
+ /**
88
+ * Coerce the contract's `Id` (a RecordId, a stable `table:id` string, or a
89
+ * bare id + the verb's `table` param) to a real RecordId. SurrealDB binds
90
+ * `$__id` verbatim: a plain string makes `FROM ONLY $__id` "select" the
91
+ * string itself (a truthy non-row) and `UPSERT $__id` an InternalError, so
92
+ * string ids silently broke every id-verb on this engine.
93
+ */
94
+ private toRecordId(table: string, id: Id): unknown {
95
+ if (typeof id !== 'string') return id;
96
+ const raw = id.startsWith(`${table}:`) ? id.slice(table.length + 1) : id;
97
+ return new RecordId(table, raw);
98
+ }
99
+
100
+ async getById(table: string, id: Id): Promise<Row | null> {
101
+ const [row] = await this.query<[Row | null]>('SELECT * FROM ONLY $__id;', {
102
+ __id: this.toRecordId(table, id),
103
+ });
104
+ // `FROM ONLY <non-record>` echoes the value back; only a real row counts.
105
+ return row && typeof row === 'object' ? row : null;
89
106
  }
90
107
 
91
- async upsert(_table: string, id: Id, data: Row, mode: 'replace' | 'merge'): Promise<void> {
108
+ async upsert(table: string, id: Id, data: Row, mode: 'replace' | 'merge'): Promise<void> {
92
109
  const sql = mode === 'merge' ? surql.upsertMerge('__id', '__data') : surql.upsert('__id', '__data');
93
- await this.query(surql.seal(sql), { __id: id, __data: data });
110
+ await this.query(surql.seal(sql), { __id: this.toRecordId(table, id), __data: data });
94
111
  }
95
112
 
96
- async patch(_table: string, id: Id, patches: unknown[]): Promise<void> {
113
+ async patch(table: string, id: Id, patches: unknown[]): Promise<void> {
97
114
  await this.query(surql.seal('UPDATE ONLY $__id PATCH $__patches'), {
98
- __id: id,
115
+ __id: this.toRecordId(table, id),
99
116
  __patches: patches,
100
117
  });
101
118
  }
102
119
 
103
- async delete(_table: string, id: Id): Promise<void> {
104
- await this.query(surql.seal(surql.delete('__id')), { __id: id });
120
+ async delete(table: string, id: Id): Promise<void> {
121
+ await this.query(surql.seal(surql.delete('__id')), { __id: this.toRecordId(table, id) });
105
122
  }
106
123
 
107
124
  /**
@@ -5,11 +5,12 @@ import { Sp00kyClient } from './sp00ky';
5
5
 
6
6
  // Guards for the local-first paint contract of `Sp00kyClient.initQuery`:
7
7
  // the hash must resolve (and the hook paint from local cache) without any
8
- // network on the awaited path, while instant-hydrate + the `register`
9
- // down-event run in a background chain that (a) keeps hydrate strictly
10
- // before the enqueue, (b) skips the duplicate one-shot fetch for freshly
11
- // preloaded queries, (c) never rejects, and (d) is shared by concurrent
12
- // mounts of the same query.
8
+ // network on the awaited path, while the opt-in instant-hydrate + the
9
+ // `register` down-event run in a background chain that (a) keeps hydrate
10
+ // strictly before the enqueue, (b) hydrates only when `instantHydrate:
11
+ // true` (default off the register lifecycle is the single freshness
12
+ // path), (c) never rejects, and (d) is shared by concurrent mounts of the
13
+ // same query.
13
14
  //
14
15
  // Structural half: like sp00ky.auth-order.test.ts, a regex over the source
15
16
  // catches the ordering regressions a runtime mock can't cheaply cover
@@ -73,7 +74,6 @@ describe('Sp00kyClient.finishQueryInit behavior', () => {
73
74
  let calls: string[];
74
75
  let enqueued: any[];
75
76
  let epoch: number;
76
- let preloadFresh: boolean;
77
77
  let cold: boolean;
78
78
  let remoteImpl: () => Promise<any>;
79
79
 
@@ -81,7 +81,6 @@ describe('Sp00kyClient.finishQueryInit behavior', () => {
81
81
  calls = [];
82
82
  enqueued = [];
83
83
  epoch = 1;
84
- preloadFresh = false;
85
84
  cold = true;
86
85
  remoteImpl = async () => {
87
86
  calls.push('fetch');
@@ -112,7 +111,6 @@ describe('Sp00kyClient.finishQueryInit behavior', () => {
112
111
  },
113
112
  dataModule: {
114
113
  isCold: () => cold,
115
- isPreloadFresh: async () => preloadFresh,
116
114
  applyHydration: async () => {
117
115
  calls.push('hydrate');
118
116
  },
@@ -121,22 +119,16 @@ describe('Sp00kyClient.finishQueryInit behavior', () => {
121
119
  });
122
120
  });
123
121
 
124
- it('cold path: fetch → hydrate → enqueue, in order', async () => {
122
+ it('cold path with hydrate enabled: fetch → hydrate → enqueue, in order', async () => {
125
123
  await client.finishQueryInit(hash, q, {});
126
124
  expect(calls).toEqual(['fetch', 'hydrate', 'enqueue']);
127
125
  expect(enqueued).toEqual([{ type: 'register', payload: { hash } }]);
128
126
  });
129
127
 
130
- it('session-preloaded query skips the fetch but still enqueues register', async () => {
128
+ it('a preloaded query still hydrates when enabled cache-first never depends on WHY rows are cached', async () => {
131
129
  client.preloadedHashes.add(q.hash);
132
130
  await client.finishQueryInit(hash, q, {});
133
- expect(calls).toEqual(['enqueue']);
134
- });
135
-
136
- it('fresh preload marker skips the fetch but still enqueues register', async () => {
137
- preloadFresh = true;
138
- await client.finishQueryInit(hash, q, {});
139
- expect(calls).toEqual(['enqueue']);
131
+ expect(calls).toEqual(['fetch', 'hydrate', 'enqueue']);
140
132
  });
141
133
 
142
134
  it('warm query (not cold) goes straight to enqueue', async () => {
@@ -164,7 +156,13 @@ describe('Sp00kyClient.finishQueryInit behavior', () => {
164
156
  expect(calls).toEqual(['fetch', 'enqueue']);
165
157
  });
166
158
 
167
- it('instantHydrate: false disables the hydrate fetch entirely', async () => {
159
+ it('default (instantHydrate unset) does not hydrate register lifecycle is the only freshness path', async () => {
160
+ delete client.config.instantHydrate;
161
+ await client.finishQueryInit(hash, q, {});
162
+ expect(calls).toEqual(['enqueue']);
163
+ });
164
+
165
+ it('instantHydrate: false does not hydrate', async () => {
168
166
  client.config.instantHydrate = false;
169
167
  await client.finishQueryInit(hash, q, {});
170
168
  expect(calls).toEqual(['enqueue']);
package/src/sp00ky.ts CHANGED
@@ -103,12 +103,6 @@ export class BucketHandle {
103
103
  */
104
104
  const LAST_BUCKET_KEY = 'sp00ky:last_bucket';
105
105
 
106
- // Max age of a `_00_preload` marker for which instant-hydrate is skipped —
107
- // a query preloaded this recently already painted from local rows and the
108
- // register lifecycle re-syncs it, so the one-shot hydrate fetch would be a
109
- // duplicate round trip. Matches `preload()`'s default `staleTime`.
110
- const HYDRATE_PRELOAD_FRESH: QueryTimeToLive = '1h';
111
-
112
106
  function readBootBucketHint(): string | null {
113
107
  try {
114
108
  return typeof localStorage !== 'undefined' ? localStorage.getItem(LAST_BUCKET_KEY) : null;
@@ -738,47 +732,29 @@ export class Sp00kyClient<S extends SchemaStructure> {
738
732
  }
739
733
 
740
734
  /**
741
- * Background tail of {@link initQuery}: instant-hydrate (when the query is
742
- * cold AND wasn't freshly preloaded) followed by enqueuing the `register`
743
- * down-event. Never rejects — both halves catch and log, so `void`-ing the
744
- * returned promise can't produce an unhandled rejection.
745
- *
746
- * The fresh-preload skip: rows a recent `preload()` persisted are already in
747
- * the local cache and were part of the first paint; the register lifecycle
748
- * re-syncs them authoritatively, so the one-shot hydrate fetch would be a
749
- * pure duplicate round trip. "Fresh" = preloaded this session
750
- * (`preloadedHashes`) or a `_00_preload` marker younger than
751
- * HYDRATE_PRELOAD_FRESH. Both are per-bucket (the Set is cleared on bucket
752
- * switch, the marker table lives in the bucket), so neither can claim
753
- * freshness across auth contexts.
735
+ * Background tail of {@link initQuery}: instant-hydrate (opt-in via
736
+ * `config.instantHydrate`, and only when the query is cold) followed by
737
+ * enqueuing the `register` down-event. Never rejects — both halves catch and
738
+ * log, so `void`-ing the returned promise can't produce an unhandled
739
+ * rejection. By default (hydrate off) the register lifecycle is the single
740
+ * freshness path; the one-shot fetch is an optimization apps enable
741
+ * explicitly, and it runs regardless of preload state cache-first delivery
742
+ * never depends on WHY rows are cached.
754
743
  */
755
744
  private async finishQueryInit(
756
745
  hash: string,
757
746
  q: InnerQuery<any, any, any>,
758
747
  params: Record<string, any>
759
748
  ): Promise<void> {
760
- if (this.config.instantHydrate !== false && this.dataModule.isCold(hash)) {
749
+ if (this.config.instantHydrate === true && this.dataModule.isCold(hash)) {
761
750
  try {
762
- const preloadFresh =
763
- this.preloadedHashes.has(q.hash) ||
764
- (await this.dataModule.isPreloadFresh(
765
- String(q.hash),
766
- parseDuration(HYDRATE_PRELOAD_FRESH)
767
- ));
768
- if (!preloadFresh) {
769
- // Fence against bucket switches: rows fetched under the previous
770
- // auth context must not hydrate the new bucket's query state — the
771
- // rebind's re-registration refills it from the right context.
772
- const epoch = this.local.epoch;
773
- const [rows] = await this.remote.query<[RecordWithId[]]>(q.selectQuery.query, params);
774
- if (epoch === this.local.epoch) {
775
- await this.dataModule.applyHydration(hash, rows ?? []);
776
- }
777
- } else {
778
- this.logger.debug(
779
- { hash, Category: 'sp00ky-client::Sp00kyClient::instantHydrate' },
780
- 'Skipping instant hydrate; preload marker fresh'
781
- );
751
+ // Fence against bucket switches: rows fetched under the previous
752
+ // auth context must not hydrate the new bucket's query state — the
753
+ // rebind's re-registration refills it from the right context.
754
+ const epoch = this.local.epoch;
755
+ const [rows] = await this.remote.query<[RecordWithId[]]>(q.selectQuery.query, params);
756
+ if (epoch === this.local.epoch) {
757
+ await this.dataModule.applyHydration(hash, rows ?? []);
782
758
  }
783
759
  } catch (err) {
784
760
  if (err instanceof StaleEpochError) {
package/src/types.ts CHANGED
@@ -163,16 +163,16 @@ export interface Sp00kyConfig<S extends SchemaStructure> {
163
163
  */
164
164
  refSyncIntervalMs?: number;
165
165
  /**
166
- * Instant-hydrate cold queries: when a query is registered with no server
167
- * result yet, run its surql directly on the remote (one-shot) and display
168
- * the result as soon as it lands, while the full realtime registration
169
- * proceeds in the background. The hydrated rows are ingested with their
170
- * versions so the registration's `syncRecords` skips re-pulling unchanged
171
- * bodies. The fetch runs OFF the paint path — `useQuery` resolves and paints
172
- * from the local cache immediately, and hydrate only fills in what's
173
- * missing. Skipped entirely when the query was `preload()`ed recently (its
174
- * rows are already local and fresh; the registration re-syncs them), so a
175
- * preloaded screen costs zero duplicate fetches. Defaults to `true`.
166
+ * OPT-IN instant-hydrate for cold queries: when enabled and a query is
167
+ * registered with no server result yet, its surql also runs directly on the
168
+ * remote (one-shot, in the background, OFF the paint path) so rows can land
169
+ * before the full register lifecycle completes. Hydrated rows carry their
170
+ * `_00_rv` versions so the registration's `syncRecords` skips re-pulling
171
+ * unchanged bodies. Regardless of this flag, `useQuery` always resolves and
172
+ * paints from the local cache immediately however the rows got there
173
+ * (preload, prior sync). Default `false`: the register lifecycle
174
+ * (`fn::query::register` `_00_list_ref` record sync) is the single
175
+ * freshness path and no duplicate one-shot fetches are made.
176
176
  */
177
177
  instantHydrate?: boolean;
178
178
  /**