@syncular/client 0.0.6-168 → 0.0.6-177

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.
@@ -149,6 +149,142 @@ describe('applyPullResponse chunk streaming', () => {
149
149
  expect(streamFetchCount).toBe(1);
150
150
  });
151
151
 
152
+ it('materializes chunked bootstrap snapshots for afterPull plugins via streaming transport', async () => {
153
+ const firstRows = Array.from({ length: 1200 }, (_, index) => ({
154
+ id: `${index + 1}`,
155
+ name: `Item ${index + 1}`,
156
+ }));
157
+ const secondRows = Array.from({ length: 1200 }, (_, index) => ({
158
+ id: `${index + 1201}`,
159
+ name: `Item ${index + 1201}`,
160
+ }));
161
+ const firstChunk = new Uint8Array(gzipSync(encodeSnapshotRows(firstRows)));
162
+ const secondChunk = new Uint8Array(
163
+ gzipSync(encodeSnapshotRows(secondRows))
164
+ );
165
+
166
+ let streamFetchCount = 0;
167
+ let activeStreamFetches = 0;
168
+ let maxConcurrentStreamFetches = 0;
169
+ const transport: SyncTransport = {
170
+ async sync() {
171
+ return {};
172
+ },
173
+ async fetchSnapshotChunk() {
174
+ throw new Error('fetchSnapshotChunk should not be used');
175
+ },
176
+ async fetchSnapshotChunkStream({ chunkId }) {
177
+ streamFetchCount += 1;
178
+ activeStreamFetches += 1;
179
+ maxConcurrentStreamFetches = Math.max(
180
+ maxConcurrentStreamFetches,
181
+ activeStreamFetches
182
+ );
183
+ await new Promise((resolve) => setTimeout(resolve, 5));
184
+ activeStreamFetches -= 1;
185
+
186
+ if (chunkId === 'chunk-1') {
187
+ return createStreamFromBytes(firstChunk, 193);
188
+ }
189
+ if (chunkId === 'chunk-2') {
190
+ return createStreamFromBytes(secondChunk, 211);
191
+ }
192
+ throw new Error(`Unexpected chunk id: ${chunkId}`);
193
+ },
194
+ };
195
+
196
+ const handlers: ClientHandlerCollection<TestDb> = [
197
+ createClientHandler({
198
+ table: 'items',
199
+ scopes: ['items:{id}'],
200
+ }),
201
+ ];
202
+
203
+ let pluginSawRows = 0;
204
+ const options = {
205
+ clientId: 'client-1',
206
+ subscriptions: [
207
+ {
208
+ id: 'items-sub',
209
+ table: 'items',
210
+ scopes: {},
211
+ },
212
+ ],
213
+ stateId: 'default',
214
+ plugins: [
215
+ {
216
+ name: 'after-pull-observer',
217
+ async afterPull(_ctx, { response }) {
218
+ pluginSawRows =
219
+ response.subscriptions[0]?.snapshots?.[0]?.rows.length ?? 0;
220
+ return response;
221
+ },
222
+ },
223
+ ],
224
+ };
225
+
226
+ const pullState = await buildPullRequest(db, options);
227
+
228
+ const response: SyncPullResponse = {
229
+ ok: true,
230
+ subscriptions: [
231
+ {
232
+ id: 'items-sub',
233
+ status: 'active',
234
+ scopes: {},
235
+ bootstrap: true,
236
+ bootstrapState: null,
237
+ nextCursor: 2,
238
+ commits: [],
239
+ snapshots: [
240
+ {
241
+ table: 'items',
242
+ rows: [],
243
+ chunks: [
244
+ {
245
+ id: 'chunk-1',
246
+ byteLength: firstChunk.length,
247
+ sha256: '',
248
+ encoding: 'json-row-frame-v1',
249
+ compression: 'gzip',
250
+ },
251
+ {
252
+ id: 'chunk-2',
253
+ byteLength: secondChunk.length,
254
+ sha256: '',
255
+ encoding: 'json-row-frame-v1',
256
+ compression: 'gzip',
257
+ },
258
+ ],
259
+ isFirstPage: true,
260
+ isLastPage: true,
261
+ },
262
+ ],
263
+ },
264
+ ],
265
+ };
266
+
267
+ await applyPullResponse(
268
+ db,
269
+ transport,
270
+ handlers,
271
+ options,
272
+ pullState,
273
+ response
274
+ );
275
+
276
+ const countResult = await sql<{ count: number }>`
277
+ select count(*) as count
278
+ from ${sql.table('items')}
279
+ `.execute(db);
280
+ expect(Number(countResult.rows[0]?.count ?? 0)).toBe(
281
+ firstRows.length + secondRows.length
282
+ );
283
+ expect(pluginSawRows).toBe(firstRows.length + secondRows.length);
284
+ expect(streamFetchCount).toBe(2);
285
+ expect(maxConcurrentStreamFetches).toBe(1);
286
+ });
287
+
152
288
  it('rolls back partial chunked bootstrap when a later chunk fails', async () => {
153
289
  const firstRows = Array.from({ length: 1500 }, (_, index) => ({
154
290
  id: `${index + 1}`,
@@ -12,7 +12,6 @@ import type {
12
12
  SyncSubscriptionRequest,
13
13
  SyncTransport,
14
14
  } from '@syncular/core';
15
- import { decodeSnapshotRows } from '@syncular/core';
16
15
  import { type Kysely, sql, type Transaction } from 'kysely';
17
16
  import {
18
17
  type ClientHandlerCollection,
@@ -29,7 +28,6 @@ import type { SyncClientDb, SyncSubscriptionStateTable } from './schema';
29
28
  // of the same objects during pull operations
30
29
  const jsonCache = new WeakMap<object, string>();
31
30
  const jsonCacheStats = { hits: 0, misses: 0 };
32
- const SNAPSHOT_CHUNK_CONCURRENCY = 8;
33
31
  const SNAPSHOT_APPLY_BATCH_ROWS = 500;
34
32
  const SNAPSHOT_ROW_FRAME_MAGIC = new Uint8Array([0x53, 0x52, 0x46, 0x31]); // "SRF1"
35
33
  const FRAME_LENGTH_BYTES = 4;
@@ -96,24 +94,6 @@ function toOwnedUint8Array(chunk: Uint8Array): Uint8Array<ArrayBuffer> {
96
94
  return bytes;
97
95
  }
98
96
 
99
- async function streamToBytes(
100
- stream: ReadableStream<Uint8Array>
101
- ): Promise<Uint8Array> {
102
- const reader = stream.getReader();
103
- try {
104
- const chunks: Uint8Array[] = [];
105
- while (true) {
106
- const { done, value } = await reader.read();
107
- if (done) break;
108
- if (!value || value.length === 0) continue;
109
- chunks.push(value);
110
- }
111
- return concatBytes(chunks);
112
- } finally {
113
- reader.releaseLock();
114
- }
115
- }
116
-
117
97
  async function maybeGunzipStream(
118
98
  stream: ReadableStream<Uint8Array>
119
99
  ): Promise<ReadableStream<Uint8Array>> {
@@ -165,14 +145,6 @@ async function maybeGunzipStream(
165
145
  );
166
146
  }
167
147
 
168
- async function maybeGunzip(bytes: Uint8Array): Promise<Uint8Array> {
169
- if (!isGzipBytes(bytes)) return bytes;
170
- const decompressedStream = await maybeGunzipStream(
171
- bytesToReadableStream(bytes)
172
- );
173
- return streamToBytes(decompressedStream);
174
- }
175
-
176
148
  async function* decodeSnapshotRowStreamBatches(
177
149
  stream: ReadableStream<Uint8Array>,
178
150
  batchSize: number
@@ -356,29 +328,62 @@ async function readAllBytesFromStream(
356
328
  return bytes;
357
329
  }
358
330
 
359
- async function mapWithConcurrency<T, U>(
360
- items: readonly T[],
361
- concurrency: number,
362
- mapper: (item: T, index: number) => Promise<U>
363
- ): Promise<U[]> {
364
- if (items.length === 0) return [];
365
-
366
- const workerCount = Math.max(1, Math.min(concurrency, items.length));
367
- const results = new Array<U>(items.length);
368
- let nextIndex = 0;
369
-
370
- async function worker(): Promise<void> {
371
- while (nextIndex < items.length) {
372
- const index = nextIndex;
373
- nextIndex += 1;
374
- const item = items[index];
375
- if (item === undefined) continue;
376
- results[index] = await mapper(item, index);
331
+ async function materializeSnapshotChunkRows(
332
+ transport: SyncTransport,
333
+ request: {
334
+ chunkId: string;
335
+ scopeValues?: ScopeValues;
336
+ },
337
+ expectedHash: string | undefined,
338
+ sha256Override?: (bytes: Uint8Array) => Promise<string>
339
+ ): Promise<unknown[]> {
340
+ const rawStream = await fetchSnapshotChunkStream(transport, request);
341
+ const decodedStream = await maybeGunzipStream(rawStream);
342
+ let rowStream = decodedStream;
343
+ let chunkHashPromise: Promise<string> | null = null;
344
+
345
+ if (expectedHash) {
346
+ const [hashStream, streamForRows] = decodedStream.tee();
347
+ rowStream = streamForRows;
348
+ chunkHashPromise = readAllBytesFromStream(hashStream).then((bytes) =>
349
+ computeSha256Hex(bytes, sha256Override)
350
+ );
351
+ }
352
+
353
+ const rows: unknown[] = [];
354
+ let materializeError: unknown = null;
355
+
356
+ try {
357
+ for await (const batch of decodeSnapshotRowStreamBatches(
358
+ rowStream,
359
+ SNAPSHOT_APPLY_BATCH_ROWS
360
+ )) {
361
+ rows.push(...batch);
362
+ }
363
+ } catch (error) {
364
+ materializeError = error;
365
+ }
366
+
367
+ if (chunkHashPromise) {
368
+ try {
369
+ const actualHash = await chunkHashPromise;
370
+ if (!materializeError && actualHash !== expectedHash) {
371
+ materializeError = new Error(
372
+ `Snapshot chunk integrity check failed: expected sha256 ${expectedHash}, got ${actualHash}`
373
+ );
374
+ }
375
+ } catch (hashError) {
376
+ if (!materializeError) {
377
+ materializeError = hashError;
378
+ }
377
379
  }
378
380
  }
379
381
 
380
- await Promise.all(Array.from({ length: workerCount }, () => worker()));
381
- return results;
382
+ if (materializeError) {
383
+ throw materializeError;
384
+ }
385
+
386
+ return rows;
382
387
  }
383
388
 
384
389
  async function materializeChunkedSnapshots(
@@ -386,69 +391,48 @@ async function materializeChunkedSnapshots(
386
391
  response: SyncPullResponse,
387
392
  sha256Override?: (bytes: Uint8Array) => Promise<string>
388
393
  ): Promise<SyncPullResponse> {
389
- const chunkCache = new Map<string, Promise<Uint8Array>>();
390
-
391
- const subscriptions = await Promise.all(
392
- response.subscriptions.map(async (sub) => {
393
- if (!sub.bootstrap) return sub;
394
- if (!sub.snapshots || sub.snapshots.length === 0) return sub;
395
-
396
- const snapshots = await mapWithConcurrency(
397
- sub.snapshots,
398
- SNAPSHOT_CHUNK_CONCURRENCY,
399
- async (snapshot) => {
400
- const chunks = snapshot.chunks ?? [];
401
- if (chunks.length === 0) {
402
- return snapshot;
403
- }
394
+ const subscriptions: SyncPullResponse['subscriptions'] = [];
404
395
 
405
- const parsedRowsByChunk = await mapWithConcurrency(
406
- chunks,
407
- SNAPSHOT_CHUNK_CONCURRENCY,
408
- async (chunk) => {
409
- const promise =
410
- chunkCache.get(chunk.id) ??
411
- transport.fetchSnapshotChunk({
412
- chunkId: chunk.id,
413
- scopeValues: sub.scopes,
414
- });
415
- chunkCache.set(chunk.id, promise);
416
-
417
- const raw = await promise;
418
- const bytes = await maybeGunzip(raw);
419
-
420
- // Verify chunk integrity using sha256 hash
421
- if (chunk.sha256) {
422
- const actualHash = await computeSha256Hex(
423
- bytes,
424
- sha256Override
425
- );
426
- if (actualHash !== chunk.sha256) {
427
- throw new Error(
428
- `Snapshot chunk integrity check failed: expected sha256 ${chunk.sha256}, got ${actualHash}`
429
- );
430
- }
431
- }
432
-
433
- return decodeSnapshotRows(bytes);
434
- }
435
- );
396
+ for (const sub of response.subscriptions) {
397
+ if (!sub.bootstrap || !sub.snapshots || sub.snapshots.length === 0) {
398
+ subscriptions.push(sub);
399
+ continue;
400
+ }
436
401
 
437
- const rows: unknown[] = [];
438
- for (const parsedRows of parsedRowsByChunk) {
439
- rows.push(...parsedRows);
440
- }
402
+ const snapshots: SyncPullSubscriptionResponse['snapshots'] = [];
403
+ for (const snapshot of sub.snapshots) {
404
+ const chunks = snapshot.chunks ?? [];
405
+ if (chunks.length === 0) {
406
+ snapshots.push(snapshot);
407
+ continue;
408
+ }
441
409
 
442
- return { ...snapshot, rows, chunks: undefined };
443
- }
444
- );
410
+ const rows: unknown[] = [];
411
+ for (const chunk of chunks) {
412
+ const chunkRows = await materializeSnapshotChunkRows(
413
+ transport,
414
+ {
415
+ chunkId: chunk.id,
416
+ scopeValues: sub.scopes,
417
+ },
418
+ chunk.sha256,
419
+ sha256Override
420
+ );
421
+ rows.push(...chunkRows);
422
+ }
445
423
 
446
- return { ...sub, snapshots };
447
- })
448
- );
424
+ snapshots.push({
425
+ ...snapshot,
426
+ rows,
427
+ chunks: undefined,
428
+ });
429
+ }
449
430
 
450
- // Clear chunk cache after processing to prevent memory accumulation
451
- chunkCache.clear();
431
+ subscriptions.push({
432
+ ...sub,
433
+ snapshots,
434
+ });
435
+ }
452
436
 
453
437
  return { ...response, subscriptions };
454
438
  }
@@ -0,0 +1,277 @@
1
+ /**
2
+ * Public query exports used by packages that consume @syncular/client.
3
+ *
4
+ * This wrapper keeps query-builder tracking isolated per chain so branching a
5
+ * base Kysely builder does not leak joined tables into sibling branches.
6
+ */
7
+
8
+ import type { Kysely } from 'kysely';
9
+ import type { FingerprintCollector } from './query/FingerprintCollector';
10
+ import {
11
+ computeRowFingerprint,
12
+ computeValueFingerprint,
13
+ hasKeyField,
14
+ type MutationTimestampSource,
15
+ } from './query/fingerprint';
16
+ import type { SyncClientDb } from './schema';
17
+
18
+ export { FingerprintCollector } from './query/FingerprintCollector';
19
+ export {
20
+ canFingerprint,
21
+ computeFingerprint,
22
+ } from './query/fingerprint';
23
+
24
+ export type FingerprintMode = 'auto' | 'value';
25
+
26
+ type TrackedSelectFrom<DB> = Kysely<DB>['selectFrom'];
27
+ type SelectFromArgs<DB> = Parameters<Kysely<DB>['selectFrom']>;
28
+ type SelectFromResult<DB> = ReturnType<Kysely<DB>['selectFrom']>;
29
+
30
+ type ExecutableQuery = {
31
+ execute: () => Promise<unknown>;
32
+ executeTakeFirst: () => Promise<unknown>;
33
+ executeTakeFirstOrThrow: () => Promise<unknown>;
34
+ };
35
+
36
+ const JOIN_METHODS = new Set([
37
+ 'innerJoin',
38
+ 'leftJoin',
39
+ 'rightJoin',
40
+ 'fullJoin',
41
+ 'crossJoin',
42
+ 'innerJoinLateral',
43
+ 'leftJoinLateral',
44
+ 'crossJoinLateral',
45
+ ]);
46
+
47
+ function isRecord(value: unknown): value is Record<string, unknown> {
48
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
49
+ }
50
+
51
+ function isExecutableQuery(value: unknown): value is ExecutableQuery {
52
+ if (!isRecord(value)) return false;
53
+ return (
54
+ typeof Reflect.get(value, 'execute') === 'function' &&
55
+ typeof Reflect.get(value, 'executeTakeFirst') === 'function' &&
56
+ typeof Reflect.get(value, 'executeTakeFirstOrThrow') === 'function'
57
+ );
58
+ }
59
+
60
+ function extractTrackedTableNames(value: unknown): string[] {
61
+ if (typeof value === 'string') {
62
+ const normalized = value.trim();
63
+ if (normalized.length === 0) return [];
64
+
65
+ const aliasIndex = normalized.search(/\s+as\s+/i);
66
+ const withoutAlias =
67
+ aliasIndex >= 0 ? normalized.slice(0, aliasIndex) : normalized;
68
+ const firstToken = withoutAlias.split(/\s+/)[0] ?? '';
69
+ return firstToken.length > 0 ? [firstToken] : [];
70
+ }
71
+
72
+ if (Array.isArray(value)) {
73
+ return value.flatMap((entry) => extractTrackedTableNames(entry));
74
+ }
75
+
76
+ return [];
77
+ }
78
+
79
+ function addFingerprint(args: {
80
+ rows: unknown;
81
+ primaryTable: string | null;
82
+ trackedTables: ReadonlySet<string>;
83
+ collector: FingerprintCollector;
84
+ engine: MutationTimestampSource;
85
+ keyField: string;
86
+ fingerprintMode: FingerprintMode;
87
+ }): void {
88
+ const {
89
+ rows,
90
+ primaryTable,
91
+ trackedTables,
92
+ collector,
93
+ engine,
94
+ keyField,
95
+ fingerprintMode,
96
+ } = args;
97
+
98
+ const fingerprintScope =
99
+ trackedTables.size > 0
100
+ ? Array.from(trackedTables).sort().join('+')
101
+ : (primaryTable ?? 'query');
102
+
103
+ if (
104
+ fingerprintMode === 'auto' &&
105
+ primaryTable &&
106
+ trackedTables.size === 1 &&
107
+ Array.isArray(rows) &&
108
+ hasKeyField(rows, keyField)
109
+ ) {
110
+ collector.add(computeRowFingerprint(rows, primaryTable, engine, keyField));
111
+ return;
112
+ }
113
+
114
+ collector.add(computeValueFingerprint(fingerprintScope, rows));
115
+ }
116
+
117
+ function addTrackedTablesToScopeCollector(
118
+ scopeCollector: Set<string>,
119
+ trackedTables: ReadonlySet<string>
120
+ ): void {
121
+ for (const trackedTable of trackedTables) {
122
+ scopeCollector.add(trackedTable);
123
+ }
124
+ }
125
+
126
+ function createExecuteProxy<B extends ExecutableQuery>(
127
+ builder: B,
128
+ primaryTable: string | null,
129
+ trackedTables: ReadonlySet<string>,
130
+ scopeCollector: Set<string>,
131
+ collector: FingerprintCollector,
132
+ engine: MutationTimestampSource,
133
+ keyField: string,
134
+ fingerprintMode: FingerprintMode
135
+ ): B {
136
+ return new Proxy(builder, {
137
+ get(target, prop, receiver) {
138
+ if (prop === 'execute') {
139
+ return async () => {
140
+ const rows = await target.execute();
141
+ addTrackedTablesToScopeCollector(scopeCollector, trackedTables);
142
+ addFingerprint({
143
+ rows,
144
+ primaryTable,
145
+ trackedTables,
146
+ collector,
147
+ engine,
148
+ keyField,
149
+ fingerprintMode,
150
+ });
151
+ return rows;
152
+ };
153
+ }
154
+
155
+ if (prop === 'executeTakeFirst') {
156
+ return async () => {
157
+ const row = await target.executeTakeFirst();
158
+ addTrackedTablesToScopeCollector(scopeCollector, trackedTables);
159
+ addFingerprint({
160
+ rows: row,
161
+ primaryTable,
162
+ trackedTables,
163
+ collector,
164
+ engine,
165
+ keyField,
166
+ fingerprintMode,
167
+ });
168
+ return row;
169
+ };
170
+ }
171
+
172
+ if (prop === 'executeTakeFirstOrThrow') {
173
+ return async () => {
174
+ const row = await target.executeTakeFirstOrThrow();
175
+ addTrackedTablesToScopeCollector(scopeCollector, trackedTables);
176
+ addFingerprint({
177
+ rows: row,
178
+ primaryTable,
179
+ trackedTables,
180
+ collector,
181
+ engine,
182
+ keyField,
183
+ fingerprintMode,
184
+ });
185
+ return row;
186
+ };
187
+ }
188
+
189
+ const value = Reflect.get(target, prop, receiver);
190
+ if (typeof value !== 'function') {
191
+ return value;
192
+ }
193
+
194
+ return (...args: unknown[]) => {
195
+ const nextTrackedTables = new Set(trackedTables);
196
+
197
+ if (
198
+ typeof prop === 'string' &&
199
+ JOIN_METHODS.has(prop) &&
200
+ args.length > 0
201
+ ) {
202
+ for (const tableName of extractTrackedTableNames(args[0])) {
203
+ nextTrackedTables.add(tableName);
204
+ }
205
+ }
206
+
207
+ const result = Reflect.apply(value, target, args);
208
+ if (!isExecutableQuery(result)) {
209
+ return result;
210
+ }
211
+
212
+ return createExecuteProxy(
213
+ result,
214
+ primaryTable,
215
+ nextTrackedTables,
216
+ scopeCollector,
217
+ collector,
218
+ engine,
219
+ keyField,
220
+ fingerprintMode
221
+ );
222
+ };
223
+ },
224
+ });
225
+ }
226
+
227
+ function createTrackedSelectFrom<DB extends SyncClientDb>(
228
+ db: Kysely<DB>,
229
+ scopeCollector: Set<string>,
230
+ fingerprintCollector: FingerprintCollector,
231
+ engine: MutationTimestampSource,
232
+ keyField = 'id',
233
+ fingerprintMode: FingerprintMode = 'auto'
234
+ ): TrackedSelectFrom<DB> {
235
+ const selectFrom = (...args: SelectFromArgs<DB>) => {
236
+ const trackedTables = new Set<string>(extractTrackedTableNames(args[0]));
237
+ const primaryTable = Array.from(trackedTables)[0] ?? null;
238
+ const builder = db.selectFrom(...args);
239
+
240
+ return createExecuteProxy(
241
+ builder,
242
+ primaryTable,
243
+ trackedTables,
244
+ scopeCollector,
245
+ fingerprintCollector,
246
+ engine,
247
+ keyField,
248
+ fingerprintMode
249
+ ) as SelectFromResult<DB>;
250
+ };
251
+
252
+ return selectFrom as TrackedSelectFrom<DB>;
253
+ }
254
+
255
+ export interface QueryContext<DB extends SyncClientDb = SyncClientDb> {
256
+ selectFrom: TrackedSelectFrom<DB>;
257
+ }
258
+
259
+ export function createQueryContext<DB extends SyncClientDb>(
260
+ db: Kysely<DB>,
261
+ scopeCollector: Set<string>,
262
+ fingerprintCollector: FingerprintCollector,
263
+ engine: MutationTimestampSource,
264
+ keyField = 'id',
265
+ fingerprintMode: FingerprintMode = 'auto'
266
+ ): QueryContext<DB> {
267
+ return {
268
+ selectFrom: createTrackedSelectFrom(
269
+ db,
270
+ scopeCollector,
271
+ fingerprintCollector,
272
+ engine,
273
+ keyField,
274
+ fingerprintMode
275
+ ),
276
+ };
277
+ }
@@ -1,33 +0,0 @@
1
- /**
2
- * @syncular/client - Query Context
3
- *
4
- * Provides a query context with tracked selectFrom for scope tracking
5
- * and automatic fingerprint generation.
6
- */
7
- import type { Kysely } from 'kysely';
8
- import type { SyncClientDb } from '../schema';
9
- import type { FingerprintCollector } from './FingerprintCollector';
10
- import type { MutationTimestampSource } from './fingerprint';
11
- import { createTrackedSelectFrom } from './tracked-select';
12
- export type TrackedSelectFrom<DB> = ReturnType<typeof createTrackedSelectFrom<DB>>;
13
- /**
14
- * Query context provided to query functions.
15
- *
16
- * Only `selectFrom` is exposed to ensure proper scope tracking and fingerprinting.
17
- * If you need raw database access, use the db directly outside the query function.
18
- */
19
- export interface QueryContext<DB extends SyncClientDb = SyncClientDb> {
20
- /**
21
- * Wrapped selectFrom that:
22
- * 1. Registers table as watched scope
23
- * 2. Intercepts .execute() to auto-detect fingerprinting mode:
24
- * - Result has keyField (default: 'id')? -> row-level fingerprinting
25
- * - No keyField? -> value-based fingerprinting (for aggregates)
26
- */
27
- selectFrom: TrackedSelectFrom<DB>;
28
- }
29
- /**
30
- * Create a query context with tracked selectFrom.
31
- */
32
- export declare function createQueryContext<DB extends SyncClientDb>(db: Kysely<DB>, scopeCollector: Set<string>, fingerprintCollector: FingerprintCollector, engine: MutationTimestampSource, keyField?: string): QueryContext<DB>;
33
- //# sourceMappingURL=QueryContext.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"QueryContext.d.ts","sourceRoot":"","sources":["../../src/query/QueryContext.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AACrC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAC9C,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AACnE,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,eAAe,CAAC;AAC7D,OAAO,EAAE,uBAAuB,EAAE,MAAM,kBAAkB,CAAC;AAE3D,MAAM,MAAM,iBAAiB,CAAC,EAAE,IAAI,UAAU,CAC5C,OAAO,uBAAuB,CAAC,EAAE,CAAC,CACnC,CAAC;AAEF;;;;;GAKG;AACH,MAAM,WAAW,YAAY,CAAC,EAAE,SAAS,YAAY,GAAG,YAAY;IAClE;;;;;;OAMG;IACH,UAAU,EAAE,iBAAiB,CAAC,EAAE,CAAC,CAAC;CACnC;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,EAAE,SAAS,YAAY,EACxD,EAAE,EAAE,MAAM,CAAC,EAAE,CAAC,EACd,cAAc,EAAE,GAAG,CAAC,MAAM,CAAC,EAC3B,oBAAoB,EAAE,oBAAoB,EAC1C,MAAM,EAAE,uBAAuB,EAC/B,QAAQ,SAAO,GACd,YAAY,CAAC,EAAE,CAAC,CAUlB"}