@abloatai/humans 0.52.0 → 0.54.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.
- package/dist/local/Database.js +19 -2
- package/dist/local/InstanceCache.d.ts +9 -0
- package/dist/local/InstanceCache.js +9 -0
- package/dist/local/SyncClient.d.ts +23 -9
- package/dist/local/SyncClient.js +42 -34
- package/dist/local/client/createInternalComponents.js +4 -0
- package/dist/local/client/createModelProxy.js +20 -2
- package/dist/local/rowWatermarks.d.ts +40 -0
- package/dist/local/rowWatermarks.js +53 -0
- package/dist/local/sync/OnDemandLoader.d.ts +22 -13
- package/dist/local/sync/OnDemandLoader.js +57 -72
- package/dist/local/sync/bootstrapApply.d.ts +2 -4
- package/dist/plugin.d.ts +7 -0
- package/package.json +2 -2
- package/src/local/Database.ts +22 -2
- package/src/local/InstanceCache.ts +10 -0
- package/src/local/SyncClient.ts +79 -38
- package/src/local/client/createInternalComponents.ts +4 -0
- package/src/local/client/createModelProxy.ts +24 -3
- package/src/local/rowWatermarks.ts +54 -0
- package/src/local/sync/OnDemandLoader.ts +98 -69
- package/src/local/sync/bootstrapApply.ts +2 -1
- package/src/plugin.ts +7 -0
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The log position each pooled row is known to reflect — the client-side
|
|
3
|
+
* companion of the server's per-row watermark (`ModelListEvidence.stamp`).
|
|
4
|
+
*
|
|
5
|
+
* A row's copy in the pool moves through four doors, and every one of them
|
|
6
|
+
* names the log position it delivers: the ordered delta stream (the delta's
|
|
7
|
+
* id), the acknowledgement of this client's own commit (`lastSyncId`), a
|
|
8
|
+
* bootstrap snapshot (its `lastSyncId`), and a server read (the row's evidence
|
|
9
|
+
* stamp). Recording that position per row is what lets a later snapshot be
|
|
10
|
+
* judged. A snapshot taken at position P cannot carry anything the log did not
|
|
11
|
+
* hold at P, so when the pooled copy already reflects a position beyond P the
|
|
12
|
+
* snapshot is stale for that row and is left unapplied. Deltas repair every
|
|
13
|
+
* peer change a skipped snapshot would have carried; nothing repairs a
|
|
14
|
+
* snapshot that regresses this client's own confirmed write, because own
|
|
15
|
+
* echoes are suppressed on apply — which is why the rule errs toward keeping
|
|
16
|
+
* the resident copy.
|
|
17
|
+
*
|
|
18
|
+
* The row's `updatedAt` is not consulted. It is an application field the
|
|
19
|
+
* server never stamps and the client fabricates when a row arrives without one,
|
|
20
|
+
* so it orders nothing; the log does.
|
|
21
|
+
*
|
|
22
|
+
* Positions are `sync_deltas` ids, the same space as {@link LogPosition}. Zero
|
|
23
|
+
* and `undefined` mean "no evidence" and never advance a row.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
export class RowWatermarks {
|
|
27
|
+
readonly #positions = new WeakMap<object, number>();
|
|
28
|
+
|
|
29
|
+
/** Record that `row`'s pooled copy reflects the log at least through `position`. */
|
|
30
|
+
advance(row: object, position: number | undefined): void {
|
|
31
|
+
if (position === undefined || !(position > 0)) return;
|
|
32
|
+
const known = this.#positions.get(row);
|
|
33
|
+
if (known === undefined || position > known) this.#positions.set(row, position);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** The highest log position `row` is known to reflect, if the client has any evidence. */
|
|
37
|
+
of(row: object): number | undefined {
|
|
38
|
+
return this.#positions.get(row);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Whether the pooled copy of `row` is known to be ahead of a snapshot that
|
|
43
|
+
* reflects the log through `snapshotPosition`. `snapshotPosition` is a lower
|
|
44
|
+
* bound: the position the snapshot provably includes (a row's evidence stamp,
|
|
45
|
+
* a bootstrap's `lastSyncId`, or the client's own read floor at the moment
|
|
46
|
+
* the read was issued — the server had at least that much when it answered).
|
|
47
|
+
* A snapshot with no known position is never judged stale.
|
|
48
|
+
*/
|
|
49
|
+
isAheadOf(row: object, snapshotPosition: number | undefined): boolean {
|
|
50
|
+
if (snapshotPosition === undefined) return false;
|
|
51
|
+
const known = this.#positions.get(row);
|
|
52
|
+
return known !== undefined && known > snapshotPosition;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -19,6 +19,13 @@
|
|
|
19
19
|
* loaded models) or the live delta stream (pushed over the WebSocket). It only
|
|
20
20
|
* fills the gap for lazily loaded models read by id or filter after the engine
|
|
21
21
|
* is ready.
|
|
22
|
+
*
|
|
23
|
+
* A network answer is a snapshot, unordered against that stream: it may leave
|
|
24
|
+
* before a write and return after it. Each returned row therefore meets the
|
|
25
|
+
* pool by log position — the position the row provably reflects against the
|
|
26
|
+
* position the pooled copy is already known to hold ({@link RowWatermarks}) —
|
|
27
|
+
* never by wall-clock `updatedAt`, which the server does not stamp and which
|
|
28
|
+
* orders nothing.
|
|
22
29
|
*/
|
|
23
30
|
|
|
24
31
|
import type { InstanceCache } from '../InstanceCache.js';
|
|
@@ -30,12 +37,18 @@ import type { ModelRegistry, RegisteredModelClass } from '../ModelRegistry.js';
|
|
|
30
37
|
import type { RuntimeContext } from '../RuntimeContext.js';
|
|
31
38
|
import { postQuery } from '../query/client.js';
|
|
32
39
|
import type { RecoveryClass } from '@abloatai/transaction/errorCodes';
|
|
33
|
-
import type { LoadWhere, Query, WhereClause, WhereOp
|
|
40
|
+
import type { LoadWhere, Query, WhereClause, WhereOp } from '../query/types.js';
|
|
41
|
+
import { normalizeWhere } from '@abloatai/transaction/resources/where';
|
|
34
42
|
import type { Schema } from '@abloatai/transaction/schema/schema';
|
|
43
|
+
import type { LogPositionPort } from '../logPosition.js';
|
|
35
44
|
|
|
36
45
|
export interface OnDemandLoaderOptions {
|
|
37
46
|
readonly objectPool: InstanceCache;
|
|
38
|
-
|
|
47
|
+
/**
|
|
48
|
+
* The local tier reads and writes rows through a model's store, so store
|
|
49
|
+
* lookup is the whole of the loader's dependency on the database.
|
|
50
|
+
*/
|
|
51
|
+
readonly database: Pick<Database, 'getStore'>;
|
|
39
52
|
readonly registry: ModelRegistry;
|
|
40
53
|
readonly schema: Schema;
|
|
41
54
|
/** Bootstrap base URL (without trailing slash), e.g. `https://api.example.com/api`. */
|
|
@@ -49,6 +62,13 @@ export interface OnDemandLoaderOptions {
|
|
|
49
62
|
readonly getCapabilityToken?: () => string | null;
|
|
50
63
|
/** The owning client's runtime. Defaults to the module-global bridge. */
|
|
51
64
|
readonly runtime?: RuntimeContext;
|
|
65
|
+
/**
|
|
66
|
+
* The client's position in the log. Read at the moment a query is issued:
|
|
67
|
+
* the server holds at least that much when it answers, so it is the position
|
|
68
|
+
* every returned row provably reflects — the bound a resident row is judged
|
|
69
|
+
* against before a snapshot may overwrite it (see {@link RowWatermarks}).
|
|
70
|
+
*/
|
|
71
|
+
readonly position: Pick<LogPositionPort, 'readFloor'>;
|
|
52
72
|
}
|
|
53
73
|
|
|
54
74
|
export interface FetchOptions<T> {
|
|
@@ -96,28 +116,39 @@ interface SchemaModelDef {
|
|
|
96
116
|
>;
|
|
97
117
|
}
|
|
98
118
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
119
|
+
/**
|
|
120
|
+
* Where a row being hydrated came from, and what that says about how it may
|
|
121
|
+
* meet a row already in the pool.
|
|
122
|
+
*
|
|
123
|
+
* A network snapshot is unordered against the delta stream: it may have been
|
|
124
|
+
* issued before a write and answered after it. It carries the position it
|
|
125
|
+
* provably reflects, and a resident row known to be beyond that position is
|
|
126
|
+
* left as it is. A local read (IndexedDB) exists to fill pool misses; the pool
|
|
127
|
+
* is never behind local storage for a row it already holds, so a resident row
|
|
128
|
+
* is kept untouched.
|
|
129
|
+
*/
|
|
130
|
+
type HydrationOrigin =
|
|
131
|
+
| { readonly kind: 'network'; readonly position: number | undefined }
|
|
132
|
+
| { readonly kind: 'local' };
|
|
133
|
+
|
|
134
|
+
const LOCAL: HydrationOrigin = { kind: 'local' };
|
|
109
135
|
|
|
110
136
|
/**
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
137
|
+
* The position a returned row provably reflects: the greater of its own
|
|
138
|
+
* evidence stamp (the row's watermark, which lags for a row that has not
|
|
139
|
+
* changed in a while) and the client's read floor when the query was issued
|
|
140
|
+
* (which the server had already passed when it answered). Both are lower
|
|
141
|
+
* bounds; the tighter one judges. `undefined` when neither says anything.
|
|
116
142
|
*/
|
|
117
|
-
function
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
143
|
+
function snapshotPosition(
|
|
144
|
+
raw: unknown,
|
|
145
|
+
evidenceById: ReadonlyMap<string, number>,
|
|
146
|
+
readFloorAtIssue: number,
|
|
147
|
+
): number | undefined {
|
|
148
|
+
const id = raw && typeof raw === 'object' ? (raw as { id?: unknown }).id : undefined;
|
|
149
|
+
const stamp = typeof id === 'string' ? (evidenceById.get(id) ?? 0) : 0;
|
|
150
|
+
const position = Math.max(stamp, readFloorAtIssue);
|
|
151
|
+
return position > 0 ? position : undefined;
|
|
121
152
|
}
|
|
122
153
|
|
|
123
154
|
export class OnDemandLoader {
|
|
@@ -308,7 +339,7 @@ export class OnDemandLoader {
|
|
|
308
339
|
if (local.length === 0) {
|
|
309
340
|
const fromIdb = await scanIdb(this.opts.database, typename, clauses);
|
|
310
341
|
const idbModels = fromIdb
|
|
311
|
-
.map((raw) => this.hydrateOne(raw, typename))
|
|
342
|
+
.map((raw) => this.hydrateOne(raw, LOCAL, typename))
|
|
312
343
|
.filter((m): m is Model => m !== null);
|
|
313
344
|
if (idbModels.length > 0) {
|
|
314
345
|
this.opts.objectPool.addBatch(idbModels, ModelScope.live);
|
|
@@ -345,18 +376,29 @@ export class OnDemandLoader {
|
|
|
345
376
|
): Promise<Model[]> {
|
|
346
377
|
const network = await this.queryNetwork(modelName, clauses, options);
|
|
347
378
|
const networkRows = network.rows;
|
|
379
|
+
const evidenceById = new Map(network.evidence.map((entry) => [entry.id, entry.stamp]));
|
|
348
380
|
const networkModels = networkRows
|
|
349
381
|
// Strict: a row the server returned whose type name this client never
|
|
350
382
|
// registered is a genuine schema collision (the pushed schema differs
|
|
351
383
|
// from the local one). Throw here, naming the cause, rather than silently
|
|
352
384
|
// dropping the row and failing downstream as `entity_not_found`.
|
|
353
|
-
.map((raw) =>
|
|
385
|
+
.map((raw) =>
|
|
386
|
+
this.hydrateOne(
|
|
387
|
+
raw,
|
|
388
|
+
{ kind: 'network', position: snapshotPosition(raw, evidenceById, network.position) },
|
|
389
|
+
typename,
|
|
390
|
+
{ strict: true },
|
|
391
|
+
),
|
|
392
|
+
)
|
|
354
393
|
.filter((m): m is Model => m !== null);
|
|
355
394
|
|
|
356
|
-
const evidenceById = new Map(network.evidence.map((entry) => [entry.id, entry.stamp]));
|
|
357
395
|
for (const model of networkModels) {
|
|
358
396
|
const stamp = evidenceById.get(model.id);
|
|
359
|
-
if (stamp
|
|
397
|
+
if (stamp === undefined) continue;
|
|
398
|
+
// The read's evidence, kept for the premise a guarded write may cite;
|
|
399
|
+
// and the position the pooled row now reflects, for freshness.
|
|
400
|
+
this.readEvidence.set(model, stamp);
|
|
401
|
+
this.opts.objectPool.watermarks.advance(model, stamp);
|
|
360
402
|
}
|
|
361
403
|
|
|
362
404
|
if (networkModels.length > 0) {
|
|
@@ -437,7 +479,7 @@ export class OnDemandLoader {
|
|
|
437
479
|
|
|
438
480
|
const rows = await this.readChildrenLocal(targetTypename, foreignKey, missing);
|
|
439
481
|
const models = rows
|
|
440
|
-
.map((raw) => this.hydrateOne(this.stampTypename(raw, targetTypename), targetTypename))
|
|
482
|
+
.map((raw) => this.hydrateOne(this.stampTypename(raw, targetTypename), LOCAL, targetTypename))
|
|
441
483
|
.filter((m): m is Model => m !== null);
|
|
442
484
|
if (models.length > 0) {
|
|
443
485
|
this.opts.objectPool.addBatch(models, ModelScope.live);
|
|
@@ -492,6 +534,7 @@ export class OnDemandLoader {
|
|
|
492
534
|
|
|
493
535
|
private hydrateOne(
|
|
494
536
|
raw: unknown,
|
|
537
|
+
origin: HydrationOrigin,
|
|
495
538
|
typename?: string,
|
|
496
539
|
opts?: { strict?: boolean },
|
|
497
540
|
): Model | null {
|
|
@@ -502,26 +545,27 @@ export class OnDemandLoader {
|
|
|
502
545
|
// Keep the existing instance alive when a query refreshes it. A query
|
|
503
546
|
// can carry fresher server state after a missed delta, but unlike the
|
|
504
547
|
// ordered delta stream it can also finish late with an older snapshot;
|
|
505
|
-
// the
|
|
548
|
+
// the origin decides which before anything is applied.
|
|
506
549
|
const existing = this.opts.objectPool.get(obj.id);
|
|
507
550
|
if (existing) {
|
|
551
|
+
if (origin.kind === 'local') return existing;
|
|
552
|
+
// A request that began before an optimistic write can return afterward
|
|
553
|
+
// with the old row; applying it would visibly snap the live model
|
|
554
|
+
// back, and the matching authoritative delta cannot repair it because
|
|
555
|
+
// own echoes are suppressed. The pool knows the position the row
|
|
556
|
+
// already reflects; a snapshot from before it is left unapplied.
|
|
557
|
+
if (this.opts.objectPool.watermarks.isAheadOf(existing, origin.position)) return existing;
|
|
558
|
+
|
|
508
559
|
const stamped = this.stampTypename(obj, typename) as Record<string, unknown>;
|
|
509
|
-
//
|
|
510
|
-
//
|
|
511
|
-
// it here would visibly snap the live model back, and the matching
|
|
512
|
-
// authoritative delta cannot repair it because own echoes are
|
|
513
|
-
// intentionally suppressed. Keep a newer resident row intact.
|
|
514
|
-
if (snapshotDoesNotAdvanceModel(stamped, existing)) return existing;
|
|
515
|
-
|
|
516
|
-
// If the source has no comparable timestamp, retain pending local
|
|
517
|
-
// fields while accepting unrelated server fields. This is the same
|
|
518
|
-
// local-first merge contract used by SyncClient's delta resolver.
|
|
560
|
+
// Retain pending local fields while accepting the server's others —
|
|
561
|
+
// the same local-first merge contract SyncClient's delta resolver uses.
|
|
519
562
|
const localChanges = existing.getChanges();
|
|
520
563
|
existing.updateFromData(
|
|
521
564
|
Object.keys(localChanges).length > 0
|
|
522
565
|
? { ...stamped, ...localChanges, updatedAt: existing.updatedAt }
|
|
523
566
|
: stamped,
|
|
524
567
|
);
|
|
568
|
+
this.opts.objectPool.watermarks.advance(existing, origin.position);
|
|
525
569
|
return existing;
|
|
526
570
|
}
|
|
527
571
|
return null;
|
|
@@ -574,6 +618,8 @@ export class OnDemandLoader {
|
|
|
574
618
|
): Promise<{
|
|
575
619
|
rows: unknown[];
|
|
576
620
|
evidence: readonly { id: string; stamp: number }[];
|
|
621
|
+
/** The client's read floor when the query was issued — what every returned row provably reflects. */
|
|
622
|
+
position: number;
|
|
577
623
|
}> {
|
|
578
624
|
const typename = this.resolveTypename(modelName);
|
|
579
625
|
const orderEntries = options?.orderBy ? Object.entries(options.orderBy) : [];
|
|
@@ -592,6 +638,9 @@ export class OnDemandLoader {
|
|
|
592
638
|
? { related: options.expand }
|
|
593
639
|
: {}),
|
|
594
640
|
};
|
|
641
|
+
// Read before the request leaves: the server holds at least this much of
|
|
642
|
+
// the log when it answers, so it is the position the response reflects.
|
|
643
|
+
const position = this.opts.position.readFloor;
|
|
595
644
|
const result = await postQuery(
|
|
596
645
|
{
|
|
597
646
|
baseUrl: this.opts.baseUrl,
|
|
@@ -620,9 +669,9 @@ export class OnDemandLoader {
|
|
|
620
669
|
// own typed pool, then leave the nested arrays in place on the
|
|
621
670
|
// primary row.
|
|
622
671
|
if (options?.expand && options.expand.length > 0) {
|
|
623
|
-
this.hydrateExpanded(modelName, normalized, options.expand);
|
|
672
|
+
this.hydrateExpanded(modelName, normalized, options.expand, position);
|
|
624
673
|
}
|
|
625
|
-
return { rows: normalized, evidence };
|
|
674
|
+
return { rows: normalized, evidence, position };
|
|
626
675
|
}
|
|
627
676
|
|
|
628
677
|
/**
|
|
@@ -636,8 +685,12 @@ export class OnDemandLoader {
|
|
|
636
685
|
parentModelName: string,
|
|
637
686
|
rows: unknown[],
|
|
638
687
|
relationNames: readonly string[],
|
|
688
|
+
position: number,
|
|
639
689
|
): void {
|
|
640
690
|
const parentDef = this.getModelDef(parentModelName);
|
|
691
|
+
// Nested rows carry no evidence of their own; the read floor at issue
|
|
692
|
+
// time is what they provably reflect. A floor of zero says nothing.
|
|
693
|
+
const origin: HydrationOrigin = { kind: 'network', position: position > 0 ? position : undefined };
|
|
641
694
|
|
|
642
695
|
for (const row of rows) {
|
|
643
696
|
if (!row || typeof row !== 'object') continue;
|
|
@@ -655,7 +708,7 @@ export class OnDemandLoader {
|
|
|
655
708
|
for (const item of items) {
|
|
656
709
|
const stamped = this.stampTypename(item, targetTypename);
|
|
657
710
|
stampedItems.push(stamped);
|
|
658
|
-
const m = this.hydrateOne(stamped);
|
|
711
|
+
const m = this.hydrateOne(stamped, origin);
|
|
659
712
|
if (m) models.push(m);
|
|
660
713
|
}
|
|
661
714
|
if (models.length > 0) {
|
|
@@ -756,7 +809,7 @@ function scanPool(
|
|
|
756
809
|
}
|
|
757
810
|
|
|
758
811
|
async function scanIdb(
|
|
759
|
-
database: Database,
|
|
812
|
+
database: Pick<Database, 'getStore'>,
|
|
760
813
|
modelName: string,
|
|
761
814
|
clauses: readonly WhereClause[],
|
|
762
815
|
): Promise<unknown[]> {
|
|
@@ -805,34 +858,10 @@ async function scanIdb(
|
|
|
805
858
|
}
|
|
806
859
|
}
|
|
807
860
|
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
*
|
|
813
|
-
* Detection: an array whose first element is itself an array is treated
|
|
814
|
-
* as tuple form. Object form is the fallback.
|
|
815
|
-
*
|
|
816
|
-
* Exported so callers can pre-normalize (e.g., for tests, or to inspect
|
|
817
|
-
* the canonical clauses before passing them to `load`/`subscribe`).
|
|
818
|
-
*/
|
|
819
|
-
export function normalizeWhere(where: unknown): readonly WhereClause[] {
|
|
820
|
-
if (where == null) return [];
|
|
821
|
-
if (Array.isArray(where)) {
|
|
822
|
-
// Tuple form — assumed to already use server-side column names.
|
|
823
|
-
return where as readonly WhereClause[];
|
|
824
|
-
}
|
|
825
|
-
if (typeof where === 'object') {
|
|
826
|
-
const obj = where as Record<string, unknown>;
|
|
827
|
-
return Object.entries(obj).map(([key, value]) => {
|
|
828
|
-
if (Array.isArray(value)) {
|
|
829
|
-
return [key, 'IN', value as readonly WherePrimitive[]] as WhereClause;
|
|
830
|
-
}
|
|
831
|
-
return [key, value as WherePrimitive] as WhereClause;
|
|
832
|
-
});
|
|
833
|
-
}
|
|
834
|
-
return [];
|
|
835
|
-
}
|
|
861
|
+
// `normalizeWhere` lives with the grammar it produces, so both transports read
|
|
862
|
+
// the same one; re-exported here for callers that pre-normalize (tests, or
|
|
863
|
+
// inspecting the canonical clauses before `load`/`subscribe`).
|
|
864
|
+
export { normalizeWhere };
|
|
836
865
|
|
|
837
866
|
/** Equality-only subset of clauses, keyed by column. Used by IDB fast paths. */
|
|
838
867
|
function extractEqClauses(clauses: readonly WhereClause[]): Record<string, unknown> {
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
import { globalRuntime } from '../context.js';
|
|
18
18
|
import type { RuntimeContext } from '../RuntimeContext.js';
|
|
19
19
|
import type { BootstrapResult } from '../Database.js';
|
|
20
|
+
import type { BootstrapSnapshot } from '../SyncClient.js';
|
|
20
21
|
import type { SyncDelta } from './SyncWebSocket.js';
|
|
21
22
|
|
|
22
23
|
/** Counts describing what applying a bootstrap changed in the pool: entities
|
|
@@ -43,7 +44,7 @@ export interface PoolContext {
|
|
|
43
44
|
applyDeltaBatchToPool(results: NonNullable<BootstrapResult['deltaResults']>): void;
|
|
44
45
|
/** Writes bootstrap data into the pool: creates models, heals partial rows, upserts, and removes stale local copies the server no longer reports. */
|
|
45
46
|
applyBootstrapDataToPool(
|
|
46
|
-
bootstrapData:
|
|
47
|
+
bootstrapData: BootstrapSnapshot,
|
|
47
48
|
protectedIds?: ReadonlySet<string>,
|
|
48
49
|
): { added: number; updated: number; removed: number; skipped: number; healed: number };
|
|
49
50
|
/** Pool size — for the completion log line. */
|
package/src/plugin.ts
CHANGED
|
@@ -94,6 +94,13 @@ export interface AppliedChange {
|
|
|
94
94
|
* no client transaction behind them.
|
|
95
95
|
*/
|
|
96
96
|
transactionId?: string;
|
|
97
|
+
/**
|
|
98
|
+
* The log position of the delta this change answers — its `sync_deltas` id.
|
|
99
|
+
* The apply stage records it per row so a later snapshot can be judged
|
|
100
|
+
* against what the row already reflects. Absent when the source carried no
|
|
101
|
+
* position.
|
|
102
|
+
*/
|
|
103
|
+
syncId?: number;
|
|
97
104
|
}
|
|
98
105
|
|
|
99
106
|
/**
|