@spooky-sync/core 0.0.1-canary.131 → 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 +8 -20
- package/dist/index.js +14 -35
- package/dist/types.d.ts +10 -10
- package/package.json +3 -3
- package/src/modules/data/data.hydration.test.ts +13 -21
- package/src/modules/data/index.ts +0 -11
- package/src/sp00ky.init-query.test.ts +16 -18
- package/src/sp00ky.ts +16 -40
- package/src/types.ts +10 -10
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 (
|
|
1307
|
-
*
|
|
1308
|
-
* down-event. Never rejects — both halves catch and
|
|
1309
|
-
* returned promise can't produce an unhandled
|
|
1310
|
-
*
|
|
1311
|
-
*
|
|
1312
|
-
*
|
|
1313
|
-
*
|
|
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
|
@@ -2795,16 +2795,6 @@ var DataModule = class {
|
|
|
2795
2795
|
return null;
|
|
2796
2796
|
}
|
|
2797
2797
|
}
|
|
2798
|
-
/**
|
|
2799
|
-
* True when this query's preload marker exists and is younger than
|
|
2800
|
-
* `maxAgeMs`. Used by instant-hydrate to skip its one-shot fetch for rows a
|
|
2801
|
-
* recent `preload()` already persisted (the register lifecycle re-syncs them
|
|
2802
|
-
* authoritatively). Missing or unreadable markers are treated as stale.
|
|
2803
|
-
*/
|
|
2804
|
-
async isPreloadFresh(hashKey, maxAgeMs) {
|
|
2805
|
-
const marker = await this.getPreloadMarker(hashKey);
|
|
2806
|
-
return marker !== null && Date.now() - marker.fetchedAt <= maxAgeMs;
|
|
2807
|
-
}
|
|
2808
2798
|
/** Stamp the preload freshness marker after a successful snapshot fetch. */
|
|
2809
2799
|
async writePreloadMarker(hash, rowCount) {
|
|
2810
2800
|
await this.local.upsert("_00_preload", new RecordId("_00_preload", hash), {
|
|
@@ -5141,8 +5131,8 @@ function parseBackendInfo(raw) {
|
|
|
5141
5131
|
|
|
5142
5132
|
//#endregion
|
|
5143
5133
|
//#region src/modules/devtools/index.ts
|
|
5144
|
-
const CORE_VERSION = "0.0.1-canary.
|
|
5145
|
-
const WASM_VERSION = "0.0.1-canary.
|
|
5134
|
+
const CORE_VERSION = "0.0.1-canary.132";
|
|
5135
|
+
const WASM_VERSION = "0.0.1-canary.132";
|
|
5146
5136
|
const SURREAL_VERSION = "3.0.3";
|
|
5147
5137
|
var DevToolsService = class {
|
|
5148
5138
|
eventsHistory = [];
|
|
@@ -7077,7 +7067,6 @@ var BucketHandle = class {
|
|
|
7077
7067
|
* callback switches to the user's bucket (cache + outbox intact).
|
|
7078
7068
|
*/
|
|
7079
7069
|
const LAST_BUCKET_KEY = "sp00ky:last_bucket";
|
|
7080
|
-
const HYDRATE_PRELOAD_FRESH = "1h";
|
|
7081
7070
|
function readBootBucketHint() {
|
|
7082
7071
|
try {
|
|
7083
7072
|
return typeof localStorage !== "undefined" ? localStorage.getItem(LAST_BUCKET_KEY) : null;
|
|
@@ -7432,30 +7421,20 @@ var Sp00kyClient = class {
|
|
|
7432
7421
|
return hash;
|
|
7433
7422
|
}
|
|
7434
7423
|
/**
|
|
7435
|
-
* Background tail of {@link initQuery}: instant-hydrate (
|
|
7436
|
-
*
|
|
7437
|
-
* down-event. Never rejects — both halves catch and
|
|
7438
|
-
* returned promise can't produce an unhandled
|
|
7439
|
-
*
|
|
7440
|
-
*
|
|
7441
|
-
*
|
|
7442
|
-
*
|
|
7443
|
-
* pure duplicate round trip. "Fresh" = preloaded this session
|
|
7444
|
-
* (`preloadedHashes`) or a `_00_preload` marker younger than
|
|
7445
|
-
* HYDRATE_PRELOAD_FRESH. Both are per-bucket (the Set is cleared on bucket
|
|
7446
|
-
* switch, the marker table lives in the bucket), so neither can claim
|
|
7447
|
-
* 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.
|
|
7448
7432
|
*/
|
|
7449
7433
|
async finishQueryInit(hash, q, params) {
|
|
7450
|
-
if (this.config.instantHydrate
|
|
7451
|
-
|
|
7452
|
-
|
|
7453
|
-
|
|
7454
|
-
if (epoch === this.local.epoch) await this.dataModule.applyHydration(hash, rows ?? []);
|
|
7455
|
-
} else this.logger.debug({
|
|
7456
|
-
hash,
|
|
7457
|
-
Category: "sp00ky-client::Sp00kyClient::instantHydrate"
|
|
7458
|
-
}, "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 ?? []);
|
|
7459
7438
|
} catch (err) {
|
|
7460
7439
|
if (err instanceof StaleEpochError) this.logger.debug({
|
|
7461
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
|
-
*
|
|
454
|
-
* result yet,
|
|
455
|
-
*
|
|
456
|
-
*
|
|
457
|
-
* versions so the registration's `syncRecords` skips re-pulling
|
|
458
|
-
* bodies.
|
|
459
|
-
* from the local cache immediately
|
|
460
|
-
*
|
|
461
|
-
*
|
|
462
|
-
*
|
|
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.
|
|
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.
|
|
63
|
-
"@spooky-sync/ssp-wasm": "0.0.1-canary.
|
|
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`
|
|
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
|
|
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.
|
|
122
|
-
|
|
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('
|
|
130
|
-
const dm = makeDm(async () => ({ fetchedAt:
|
|
131
|
-
expect(await dm.
|
|
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('
|
|
140
|
-
|
|
141
|
-
|
|
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('
|
|
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.
|
|
140
|
+
expect(await dm.getPreloadMarker('h')).toBeNull();
|
|
149
141
|
});
|
|
150
142
|
});
|
|
@@ -890,17 +890,6 @@ export class DataModule<S extends SchemaStructure> {
|
|
|
890
890
|
}
|
|
891
891
|
}
|
|
892
892
|
|
|
893
|
-
/**
|
|
894
|
-
* True when this query's preload marker exists and is younger than
|
|
895
|
-
* `maxAgeMs`. Used by instant-hydrate to skip its one-shot fetch for rows a
|
|
896
|
-
* recent `preload()` already persisted (the register lifecycle re-syncs them
|
|
897
|
-
* authoritatively). Missing or unreadable markers are treated as stale.
|
|
898
|
-
*/
|
|
899
|
-
async isPreloadFresh(hashKey: string, maxAgeMs: number): Promise<boolean> {
|
|
900
|
-
const marker = await this.getPreloadMarker(hashKey);
|
|
901
|
-
return marker !== null && Date.now() - marker.fetchedAt <= maxAgeMs;
|
|
902
|
-
}
|
|
903
|
-
|
|
904
893
|
/** Stamp the preload freshness marker after a successful snapshot fetch. */
|
|
905
894
|
async writePreloadMarker(hash: string, rowCount: number): Promise<void> {
|
|
906
895
|
// RecordId, not a bare string: the SurrealDB engine binds the id verbatim,
|
|
@@ -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
|
|
9
|
-
// down-event run in a background chain that (a) keeps hydrate
|
|
10
|
-
// before the enqueue, (b)
|
|
11
|
-
//
|
|
12
|
-
// mounts of the
|
|
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('
|
|
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
|
|
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 (
|
|
742
|
-
*
|
|
743
|
-
* down-event. Never rejects — both halves catch and
|
|
744
|
-
* returned promise can't produce an unhandled
|
|
745
|
-
*
|
|
746
|
-
*
|
|
747
|
-
*
|
|
748
|
-
*
|
|
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
|
|
749
|
+
if (this.config.instantHydrate === true && this.dataModule.isCold(hash)) {
|
|
761
750
|
try {
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
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
|
-
*
|
|
167
|
-
* result yet,
|
|
168
|
-
*
|
|
169
|
-
*
|
|
170
|
-
* versions so the registration's `syncRecords` skips re-pulling
|
|
171
|
-
* bodies.
|
|
172
|
-
* from the local cache immediately
|
|
173
|
-
*
|
|
174
|
-
*
|
|
175
|
-
*
|
|
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
|
/**
|