@syncular/client 0.0.6-219 → 0.0.6-221
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/README.md +10 -1
- package/dist/client.d.ts +12 -20
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +20 -5
- package/dist/client.js.map +1 -1
- package/dist/engine/SyncEngine.d.ts +4 -3
- package/dist/engine/SyncEngine.d.ts.map +1 -1
- package/dist/engine/SyncEngine.js +84 -14
- package/dist/engine/SyncEngine.js.map +1 -1
- package/dist/engine/types.d.ts +53 -2
- package/dist/engine/types.d.ts.map +1 -1
- package/dist/pull-engine.d.ts +6 -2
- package/dist/pull-engine.d.ts.map +1 -1
- package/dist/pull-engine.js +564 -220
- package/dist/pull-engine.js.map +1 -1
- package/dist/sync-loop.d.ts +5 -3
- package/dist/sync-loop.d.ts.map +1 -1
- package/dist/sync-loop.js +30 -0
- package/dist/sync-loop.js.map +1 -1
- package/dist/sync.d.ts +4 -3
- package/dist/sync.d.ts.map +1 -1
- package/dist/sync.js +1 -0
- package/dist/sync.js.map +1 -1
- package/package.json +3 -3
- package/src/client.ts +35 -25
- package/src/engine/SyncEngine.test.ts +64 -0
- package/src/engine/SyncEngine.ts +117 -24
- package/src/engine/types.ts +70 -1
- package/src/pull-engine.test.ts +118 -0
- package/src/pull-engine.ts +679 -261
- package/src/sync-loop.ts +52 -3
- package/src/sync.ts +6 -9
|
@@ -225,6 +225,70 @@ describe('SyncEngine WS inline apply', () => {
|
|
|
225
225
|
expect(snapshot.diagnostics).toBeDefined();
|
|
226
226
|
});
|
|
227
227
|
|
|
228
|
+
it('emits structured pull/apply trace events when tracing is enabled', async () => {
|
|
229
|
+
const handlers: ClientHandlerCollection<TestDb> = [
|
|
230
|
+
{
|
|
231
|
+
table: 'tasks',
|
|
232
|
+
async applySnapshot() {},
|
|
233
|
+
async clearAll() {},
|
|
234
|
+
async applyChange() {},
|
|
235
|
+
},
|
|
236
|
+
];
|
|
237
|
+
|
|
238
|
+
const transport: SyncTransport = {
|
|
239
|
+
async sync() {
|
|
240
|
+
return {
|
|
241
|
+
pull: {
|
|
242
|
+
ok: true,
|
|
243
|
+
subscriptions: [
|
|
244
|
+
{
|
|
245
|
+
id: 'sub-1',
|
|
246
|
+
status: 'active',
|
|
247
|
+
table: 'tasks',
|
|
248
|
+
scopes: {},
|
|
249
|
+
bootstrap: false,
|
|
250
|
+
commits: [],
|
|
251
|
+
snapshots: [],
|
|
252
|
+
nextCursor: 0,
|
|
253
|
+
},
|
|
254
|
+
],
|
|
255
|
+
},
|
|
256
|
+
};
|
|
257
|
+
},
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
const engine = new SyncEngine<TestDb>({
|
|
261
|
+
db,
|
|
262
|
+
transport,
|
|
263
|
+
handlers,
|
|
264
|
+
actorId: 'u1',
|
|
265
|
+
clientId: 'client-trace',
|
|
266
|
+
subscriptions: [{ id: 'sub-1', table: 'tasks', scopes: {} }],
|
|
267
|
+
stateId: 'default',
|
|
268
|
+
traceEnabled: true,
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
const stages: string[] = [];
|
|
272
|
+
engine.on('sync:trace', (payload) => {
|
|
273
|
+
stages.push(payload.stage);
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
await engine.start();
|
|
277
|
+
|
|
278
|
+
expect(stages).toContain('pull:start');
|
|
279
|
+
expect(stages).toContain('pull:response');
|
|
280
|
+
expect(stages).toContain('apply:transaction:start');
|
|
281
|
+
expect(stages).toContain('apply:transaction:complete');
|
|
282
|
+
expect(stages).toContain('apply:subscription:start');
|
|
283
|
+
expect(stages).toContain('apply:subscription:complete');
|
|
284
|
+
|
|
285
|
+
const snapshot = await engine.getInspectorSnapshot({ eventLimit: 50 });
|
|
286
|
+
const traceEvents = snapshot.recentEvents.filter(
|
|
287
|
+
(event) => event.event === 'sync:trace'
|
|
288
|
+
);
|
|
289
|
+
expect(traceEvents.length).toBeGreaterThan(0);
|
|
290
|
+
});
|
|
291
|
+
|
|
228
292
|
it('coalesces rapid data:change emissions when debounce is configured', async () => {
|
|
229
293
|
const handlers: ClientHandlerCollection<TestDb> = [
|
|
230
294
|
{
|
package/src/engine/SyncEngine.ts
CHANGED
|
@@ -14,7 +14,6 @@ import {
|
|
|
14
14
|
type SyncOperation,
|
|
15
15
|
type SyncPullResponse,
|
|
16
16
|
type SyncPullSubscriptionResponse,
|
|
17
|
-
type SyncSubscriptionRequest,
|
|
18
17
|
SyncTransportError,
|
|
19
18
|
startSyncSpan,
|
|
20
19
|
} from '@syncular/core';
|
|
@@ -53,6 +52,7 @@ import type {
|
|
|
53
52
|
SyncBootstrapStatus,
|
|
54
53
|
SyncBootstrapStatusOptions,
|
|
55
54
|
SyncBootstrapSubscriptionPhase,
|
|
55
|
+
SyncClientSubscription,
|
|
56
56
|
SyncConnectionState,
|
|
57
57
|
SyncDiagnostics,
|
|
58
58
|
SyncEngineConfig,
|
|
@@ -273,6 +273,11 @@ function defaultSelectorEquality<T>(left: T, right: T): boolean {
|
|
|
273
273
|
return Object.is(left, right);
|
|
274
274
|
}
|
|
275
275
|
|
|
276
|
+
function normalizeBootstrapPhase(value: number | undefined): number {
|
|
277
|
+
if (value === undefined) return 0;
|
|
278
|
+
return Number.isFinite(value) ? Math.max(0, Math.trunc(value)) : 0;
|
|
279
|
+
}
|
|
280
|
+
|
|
276
281
|
function areMetadataRecordsEqual(
|
|
277
282
|
left: Record<string, unknown> | undefined,
|
|
278
283
|
right: Record<string, unknown> | undefined
|
|
@@ -736,30 +741,100 @@ export class SyncEngine<DB extends SyncClientDb = SyncClientDb> {
|
|
|
736
741
|
completedAt: progress?.completedAt,
|
|
737
742
|
lastErrorCode: progress?.lastErrorCode,
|
|
738
743
|
lastErrorMessage: progress?.lastErrorMessage,
|
|
744
|
+
bootstrapPhase: normalizeBootstrapPhase(configuredSub?.bootstrapPhase),
|
|
739
745
|
};
|
|
740
746
|
});
|
|
741
747
|
|
|
742
748
|
const expectedSubscriptions = subscriptions.filter((sub) => sub.expected);
|
|
743
|
-
const
|
|
749
|
+
const activePhase =
|
|
750
|
+
expectedSubscriptions
|
|
751
|
+
.filter((sub) => !sub.ready)
|
|
752
|
+
.reduce<number | null>(
|
|
753
|
+
(lowest, sub) =>
|
|
754
|
+
lowest === null || sub.bootstrapPhase < lowest
|
|
755
|
+
? sub.bootstrapPhase
|
|
756
|
+
: lowest,
|
|
757
|
+
null
|
|
758
|
+
) ?? null;
|
|
759
|
+
const selectedMaxPhase =
|
|
760
|
+
options.maxPhase ??
|
|
761
|
+
(explicitIdSet.size > 0
|
|
762
|
+
? 'all'
|
|
763
|
+
: expectedSubscriptions.length > 0
|
|
764
|
+
? expectedSubscriptions.reduce(
|
|
765
|
+
(lowest, sub) => Math.min(lowest, sub.bootstrapPhase),
|
|
766
|
+
Number.POSITIVE_INFINITY
|
|
767
|
+
)
|
|
768
|
+
: 0);
|
|
769
|
+
const blockingSubscriptions = expectedSubscriptions.filter((sub) =>
|
|
770
|
+
selectedMaxPhase === 'all' ? true : sub.bootstrapPhase <= selectedMaxPhase
|
|
771
|
+
);
|
|
772
|
+
const readySubscriptionIds = blockingSubscriptions
|
|
744
773
|
.filter((sub) => sub.ready)
|
|
745
774
|
.map((sub) => sub.id);
|
|
746
|
-
const pendingSubscriptionIds =
|
|
775
|
+
const pendingSubscriptionIds = blockingSubscriptions
|
|
747
776
|
.filter((sub) => !sub.ready)
|
|
748
777
|
.map((sub) => sub.id);
|
|
749
778
|
const channelPhase = this.resolveChannelPhase(
|
|
750
779
|
filteredStates.map((sub) => this.mapSubscriptionToProgress(sub))
|
|
751
780
|
);
|
|
752
781
|
const progressPercent =
|
|
753
|
-
|
|
782
|
+
blockingSubscriptions.length === 0
|
|
754
783
|
? pendingSubscriptionIds.length === 0
|
|
755
784
|
? 100
|
|
756
785
|
: 0
|
|
757
786
|
: Math.round(
|
|
758
|
-
|
|
787
|
+
blockingSubscriptions.reduce(
|
|
759
788
|
(sum, sub) => sum + sub.progressPercent,
|
|
760
789
|
0
|
|
761
|
-
) /
|
|
790
|
+
) / blockingSubscriptions.length
|
|
762
791
|
);
|
|
792
|
+
const phases = Array.from(
|
|
793
|
+
expectedSubscriptions.reduce(
|
|
794
|
+
(acc, sub) => {
|
|
795
|
+
const current = acc.get(sub.bootstrapPhase) ?? {
|
|
796
|
+
phase: sub.bootstrapPhase,
|
|
797
|
+
expectedSubscriptionIds: [] as string[],
|
|
798
|
+
readySubscriptionIds: [] as string[],
|
|
799
|
+
pendingSubscriptionIds: [] as string[],
|
|
800
|
+
progressPercent: 0,
|
|
801
|
+
};
|
|
802
|
+
current.expectedSubscriptionIds.push(sub.id);
|
|
803
|
+
if (sub.ready) {
|
|
804
|
+
current.readySubscriptionIds.push(sub.id);
|
|
805
|
+
} else {
|
|
806
|
+
current.pendingSubscriptionIds.push(sub.id);
|
|
807
|
+
}
|
|
808
|
+
current.progressPercent += sub.progressPercent;
|
|
809
|
+
acc.set(sub.bootstrapPhase, current);
|
|
810
|
+
return acc;
|
|
811
|
+
},
|
|
812
|
+
new Map<
|
|
813
|
+
number,
|
|
814
|
+
{
|
|
815
|
+
phase: number;
|
|
816
|
+
expectedSubscriptionIds: string[];
|
|
817
|
+
readySubscriptionIds: string[];
|
|
818
|
+
pendingSubscriptionIds: string[];
|
|
819
|
+
progressPercent: number;
|
|
820
|
+
}
|
|
821
|
+
>()
|
|
822
|
+
)
|
|
823
|
+
)
|
|
824
|
+
.sort(([left], [right]) => left - right)
|
|
825
|
+
.map(([, phase]) => ({
|
|
826
|
+
phase: phase.phase,
|
|
827
|
+
expectedSubscriptionIds: phase.expectedSubscriptionIds,
|
|
828
|
+
readySubscriptionIds: phase.readySubscriptionIds,
|
|
829
|
+
pendingSubscriptionIds: phase.pendingSubscriptionIds,
|
|
830
|
+
isReady: phase.pendingSubscriptionIds.length === 0,
|
|
831
|
+
progressPercent:
|
|
832
|
+
phase.expectedSubscriptionIds.length === 0
|
|
833
|
+
? 100
|
|
834
|
+
: Math.round(
|
|
835
|
+
phase.progressPercent / phase.expectedSubscriptionIds.length
|
|
836
|
+
),
|
|
837
|
+
}));
|
|
763
838
|
|
|
764
839
|
return {
|
|
765
840
|
stateId,
|
|
@@ -767,10 +842,13 @@ export class SyncEngine<DB extends SyncClientDb = SyncClientDb> {
|
|
|
767
842
|
progressPercent,
|
|
768
843
|
isBootstrapping: pendingSubscriptionIds.length > 0,
|
|
769
844
|
isReady: pendingSubscriptionIds.length === 0,
|
|
770
|
-
expectedSubscriptionIds:
|
|
845
|
+
expectedSubscriptionIds: blockingSubscriptions.map((sub) => sub.id),
|
|
771
846
|
readySubscriptionIds,
|
|
772
847
|
pendingSubscriptionIds,
|
|
773
848
|
subscriptions,
|
|
849
|
+
activePhase,
|
|
850
|
+
selectedMaxPhase,
|
|
851
|
+
phases,
|
|
774
852
|
};
|
|
775
853
|
}
|
|
776
854
|
|
|
@@ -826,20 +904,25 @@ export class SyncEngine<DB extends SyncClientDb = SyncClientDb> {
|
|
|
826
904
|
const deadline = Date.now() + timeoutMs;
|
|
827
905
|
|
|
828
906
|
while (true) {
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
);
|
|
907
|
+
if (options.subscriptionId !== undefined) {
|
|
908
|
+
const state = await this.getSubscriptionState(options.subscriptionId, {
|
|
909
|
+
stateId,
|
|
910
|
+
});
|
|
911
|
+
const hasPendingBootstrap =
|
|
912
|
+
state?.status === 'active' && state.bootstrapState !== null;
|
|
836
913
|
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
914
|
+
if (!hasPendingBootstrap) {
|
|
915
|
+
return this.getProgress();
|
|
916
|
+
}
|
|
917
|
+
} else {
|
|
918
|
+
const bootstrap = await this.getBootstrapStatus({
|
|
919
|
+
stateId,
|
|
920
|
+
maxPhase: options.maxPhase,
|
|
921
|
+
});
|
|
840
922
|
|
|
841
|
-
|
|
842
|
-
|
|
923
|
+
if (bootstrap.isReady) {
|
|
924
|
+
return this.getProgress();
|
|
925
|
+
}
|
|
843
926
|
}
|
|
844
927
|
|
|
845
928
|
if (this.state.error) {
|
|
@@ -1668,6 +1751,18 @@ export class SyncEngine<DB extends SyncClientDb = SyncClientDb> {
|
|
|
1668
1751
|
}
|
|
1669
1752
|
}
|
|
1670
1753
|
|
|
1754
|
+
private shouldTrace(): boolean {
|
|
1755
|
+
return (
|
|
1756
|
+
this.config.traceEnabled === true ||
|
|
1757
|
+
(this.listeners.get('sync:trace')?.size ?? 0) > 0
|
|
1758
|
+
);
|
|
1759
|
+
}
|
|
1760
|
+
|
|
1761
|
+
private emitTrace(payload: SyncEventPayloads['sync:trace']): void {
|
|
1762
|
+
if (!this.shouldTrace()) return;
|
|
1763
|
+
this.emit('sync:trace', payload);
|
|
1764
|
+
}
|
|
1765
|
+
|
|
1671
1766
|
private updateState(partial: Partial<SyncEngineState>): void {
|
|
1672
1767
|
const nextState = { ...this.state, ...partial };
|
|
1673
1768
|
const unchanged =
|
|
@@ -1953,14 +2048,14 @@ export class SyncEngine<DB extends SyncClientDb = SyncClientDb> {
|
|
|
1953
2048
|
clientId: this.config.clientId!,
|
|
1954
2049
|
actorId: this.config.actorId ?? undefined,
|
|
1955
2050
|
plugins: this.config.plugins,
|
|
1956
|
-
subscriptions: this.config
|
|
1957
|
-
.subscriptions as SyncSubscriptionRequest[],
|
|
2051
|
+
subscriptions: this.config.subscriptions,
|
|
1958
2052
|
limitCommits: this.config.limitCommits,
|
|
1959
2053
|
limitSnapshotRows: this.config.limitSnapshotRows,
|
|
1960
2054
|
maxSnapshotPages: this.config.maxSnapshotPages,
|
|
1961
2055
|
dedupeRows: this.config.dedupeRows,
|
|
1962
2056
|
stateId: this.config.stateId,
|
|
1963
2057
|
sha256: this.config.sha256,
|
|
2058
|
+
onTrace: (event) => this.emitTrace(event),
|
|
1964
2059
|
trigger,
|
|
1965
2060
|
allowSkipPullOnLocalWsPush:
|
|
1966
2061
|
this.state.transportMode === 'realtime',
|
|
@@ -2987,9 +3082,7 @@ export class SyncEngine<DB extends SyncClientDb = SyncClientDb> {
|
|
|
2987
3082
|
/**
|
|
2988
3083
|
* Update subscriptions dynamically
|
|
2989
3084
|
*/
|
|
2990
|
-
updateSubscriptions(
|
|
2991
|
-
subscriptions: Array<Omit<SyncSubscriptionRequest, 'cursor'>>
|
|
2992
|
-
): void {
|
|
3085
|
+
updateSubscriptions(subscriptions: SyncClientSubscription[]): void {
|
|
2993
3086
|
this.config.subscriptions = subscriptions;
|
|
2994
3087
|
// Trigger a sync to apply new subscriptions
|
|
2995
3088
|
this.triggerSyncInBackground(undefined, 'subscription update');
|
package/src/engine/types.ts
CHANGED
|
@@ -107,6 +107,20 @@ export type SyncBootstrapSubscriptionPhase =
|
|
|
107
107
|
| SubscriptionProgressPhase
|
|
108
108
|
| 'pending';
|
|
109
109
|
|
|
110
|
+
export interface SyncClientSubscription
|
|
111
|
+
extends Omit<SyncSubscriptionRequest, 'cursor'> {
|
|
112
|
+
/**
|
|
113
|
+
* Local-only bootstrap phase for staged startup.
|
|
114
|
+
*
|
|
115
|
+
* Lower phases bootstrap first. Higher phases are deferred until all lower
|
|
116
|
+
* phases are ready, but once a higher-phase subscription is already ready it
|
|
117
|
+
* continues to participate in normal pull requests.
|
|
118
|
+
*
|
|
119
|
+
* Defaults to `0`.
|
|
120
|
+
*/
|
|
121
|
+
bootstrapPhase?: number;
|
|
122
|
+
}
|
|
123
|
+
|
|
110
124
|
export interface SyncBootstrapSubscriptionStatus {
|
|
111
125
|
stateId: string;
|
|
112
126
|
id: string;
|
|
@@ -122,6 +136,16 @@ export interface SyncBootstrapSubscriptionStatus {
|
|
|
122
136
|
completedAt?: number;
|
|
123
137
|
lastErrorCode?: string;
|
|
124
138
|
lastErrorMessage?: string;
|
|
139
|
+
bootstrapPhase: number;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export interface SyncBootstrapPhaseStatus {
|
|
143
|
+
phase: number;
|
|
144
|
+
expectedSubscriptionIds: string[];
|
|
145
|
+
readySubscriptionIds: string[];
|
|
146
|
+
pendingSubscriptionIds: string[];
|
|
147
|
+
isReady: boolean;
|
|
148
|
+
progressPercent: number;
|
|
125
149
|
}
|
|
126
150
|
|
|
127
151
|
export interface SyncBootstrapStatus {
|
|
@@ -134,11 +158,51 @@ export interface SyncBootstrapStatus {
|
|
|
134
158
|
readySubscriptionIds: string[];
|
|
135
159
|
pendingSubscriptionIds: string[];
|
|
136
160
|
subscriptions: SyncBootstrapSubscriptionStatus[];
|
|
161
|
+
activePhase: number | null;
|
|
162
|
+
selectedMaxPhase: number | 'all';
|
|
163
|
+
phases: SyncBootstrapPhaseStatus[];
|
|
137
164
|
}
|
|
138
165
|
|
|
139
166
|
export interface SyncBootstrapStatusOptions {
|
|
140
167
|
stateId?: string;
|
|
141
168
|
subscriptionIds?: string[];
|
|
169
|
+
maxPhase?: number | 'all';
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export type SyncTraceStage =
|
|
173
|
+
| 'pull:start'
|
|
174
|
+
| 'pull:response'
|
|
175
|
+
| 'pull:error'
|
|
176
|
+
| 'apply:transaction:start'
|
|
177
|
+
| 'apply:transaction:complete'
|
|
178
|
+
| 'apply:transaction:error'
|
|
179
|
+
| 'apply:subscription:start'
|
|
180
|
+
| 'apply:subscription:complete'
|
|
181
|
+
| 'apply:subscription:error'
|
|
182
|
+
| 'apply:chunk-materialize:start'
|
|
183
|
+
| 'apply:chunk-materialize:complete'
|
|
184
|
+
| 'apply:chunk-materialize:error';
|
|
185
|
+
|
|
186
|
+
export interface SyncTraceEvent {
|
|
187
|
+
stage: SyncTraceStage;
|
|
188
|
+
timestamp: number;
|
|
189
|
+
stateId?: string;
|
|
190
|
+
subscriptionId?: string;
|
|
191
|
+
table?: string;
|
|
192
|
+
bootstrap?: boolean;
|
|
193
|
+
activeBootstrapPhase?: number | null;
|
|
194
|
+
transactionMode?: 'single-transaction' | 'per-subscription';
|
|
195
|
+
subscriptionIds?: string[];
|
|
196
|
+
subscriptionCount?: number;
|
|
197
|
+
commitCount?: number;
|
|
198
|
+
snapshotCount?: number;
|
|
199
|
+
chunkCount?: number;
|
|
200
|
+
chunkId?: string;
|
|
201
|
+
chunkIndex?: number;
|
|
202
|
+
rowCount?: number;
|
|
203
|
+
nextCursor?: number | null;
|
|
204
|
+
durationMs?: number;
|
|
205
|
+
errorMessage?: string;
|
|
142
206
|
}
|
|
143
207
|
|
|
144
208
|
/**
|
|
@@ -177,6 +241,7 @@ export type SyncEventType =
|
|
|
177
241
|
| 'state:change'
|
|
178
242
|
| 'sync:start'
|
|
179
243
|
| 'sync:complete'
|
|
244
|
+
| 'sync:trace'
|
|
180
245
|
| 'sync:live'
|
|
181
246
|
| 'sync:error'
|
|
182
247
|
| 'push:result'
|
|
@@ -223,6 +288,7 @@ export interface SyncEventPayloads {
|
|
|
223
288
|
pullRounds: number;
|
|
224
289
|
pullResponse: SyncPullResponse;
|
|
225
290
|
};
|
|
291
|
+
'sync:trace': SyncTraceEvent;
|
|
226
292
|
'sync:live': { timestamp: number };
|
|
227
293
|
'sync:error': SyncError;
|
|
228
294
|
'push:result': PushResultInfo;
|
|
@@ -287,7 +353,7 @@ export interface SyncEngineConfig<DB extends SyncClientDb = SyncClientDb> {
|
|
|
287
353
|
/** Stable device/app installation id */
|
|
288
354
|
clientId: string | null | undefined;
|
|
289
355
|
/** Subscriptions for partial sync */
|
|
290
|
-
subscriptions:
|
|
356
|
+
subscriptions: SyncClientSubscription[];
|
|
291
357
|
/** Pull limit (commit count per request) */
|
|
292
358
|
limitCommits?: number;
|
|
293
359
|
/** Bootstrap snapshot rows per page */
|
|
@@ -344,6 +410,8 @@ export interface SyncEngineConfig<DB extends SyncClientDb = SyncClientDb> {
|
|
|
344
410
|
plugins?: SyncClientPlugin[];
|
|
345
411
|
/** Custom SHA-256 hash function (for platforms without crypto.subtle, e.g. React Native) */
|
|
346
412
|
sha256?: (bytes: Uint8Array) => Promise<string>;
|
|
413
|
+
/** Emit structured pull/apply tracing events to the event stream and inspector buffer. */
|
|
414
|
+
traceEnabled?: boolean;
|
|
347
415
|
}
|
|
348
416
|
|
|
349
417
|
/**
|
|
@@ -469,6 +537,7 @@ export interface SyncAwaitBootstrapOptions {
|
|
|
469
537
|
timeoutMs?: number;
|
|
470
538
|
stateId?: string;
|
|
471
539
|
subscriptionId?: string;
|
|
540
|
+
maxPhase?: number | 'all';
|
|
472
541
|
}
|
|
473
542
|
|
|
474
543
|
export interface SyncDiagnostics {
|
package/src/pull-engine.test.ts
CHANGED
|
@@ -1790,3 +1790,121 @@ describe('applyPullResponse chunk streaming', () => {
|
|
|
1790
1790
|
expect(clearedScopes).toEqual([{ project_id: 'p1' }]);
|
|
1791
1791
|
});
|
|
1792
1792
|
});
|
|
1793
|
+
|
|
1794
|
+
describe('buildPullRequest phased bootstrap selection', () => {
|
|
1795
|
+
let db: Kysely<TestDb>;
|
|
1796
|
+
|
|
1797
|
+
beforeEach(async () => {
|
|
1798
|
+
db = createDatabase<TestDb>({
|
|
1799
|
+
dialect: createBunSqliteDialect({ path: ':memory:' }),
|
|
1800
|
+
family: 'sqlite',
|
|
1801
|
+
});
|
|
1802
|
+
await ensureClientSyncSchema(db);
|
|
1803
|
+
});
|
|
1804
|
+
|
|
1805
|
+
afterEach(async () => {
|
|
1806
|
+
await db.destroy();
|
|
1807
|
+
});
|
|
1808
|
+
|
|
1809
|
+
it('requests only the lowest pending bootstrap phase by default', async () => {
|
|
1810
|
+
const pullState = await buildPullRequest(db, {
|
|
1811
|
+
clientId: 'client-1',
|
|
1812
|
+
stateId: 'default',
|
|
1813
|
+
subscriptions: [
|
|
1814
|
+
{ id: 'catalog-meta', table: 'items', bootstrapPhase: 0 },
|
|
1815
|
+
{ id: 'catalog-relations', table: 'scoped_items', bootstrapPhase: 1 },
|
|
1816
|
+
],
|
|
1817
|
+
});
|
|
1818
|
+
|
|
1819
|
+
expect(pullState.request.subscriptions.map((sub) => sub.id)).toEqual([
|
|
1820
|
+
'catalog-meta',
|
|
1821
|
+
]);
|
|
1822
|
+
});
|
|
1823
|
+
|
|
1824
|
+
it('unlocks later phases once earlier phases are ready', async () => {
|
|
1825
|
+
const now = Date.now();
|
|
1826
|
+
|
|
1827
|
+
await db
|
|
1828
|
+
.insertInto('sync_subscription_state')
|
|
1829
|
+
.values({
|
|
1830
|
+
state_id: 'default',
|
|
1831
|
+
subscription_id: 'catalog-meta',
|
|
1832
|
+
table: 'items',
|
|
1833
|
+
scopes_json: '{}',
|
|
1834
|
+
params_json: '{}',
|
|
1835
|
+
cursor: 12,
|
|
1836
|
+
bootstrap_state_json: null,
|
|
1837
|
+
status: 'active',
|
|
1838
|
+
created_at: now,
|
|
1839
|
+
updated_at: now,
|
|
1840
|
+
})
|
|
1841
|
+
.execute();
|
|
1842
|
+
|
|
1843
|
+
const pullState = await buildPullRequest(db, {
|
|
1844
|
+
clientId: 'client-1',
|
|
1845
|
+
stateId: 'default',
|
|
1846
|
+
subscriptions: [
|
|
1847
|
+
{ id: 'catalog-meta', table: 'items', bootstrapPhase: 0 },
|
|
1848
|
+
{ id: 'catalog-relations', table: 'scoped_items', bootstrapPhase: 1 },
|
|
1849
|
+
],
|
|
1850
|
+
});
|
|
1851
|
+
|
|
1852
|
+
expect(pullState.request.subscriptions.map((sub) => sub.id)).toEqual([
|
|
1853
|
+
'catalog-meta',
|
|
1854
|
+
'catalog-relations',
|
|
1855
|
+
]);
|
|
1856
|
+
});
|
|
1857
|
+
|
|
1858
|
+
it('keeps already-ready later phases live while earlier phases rebootstrap', async () => {
|
|
1859
|
+
const now = Date.now();
|
|
1860
|
+
|
|
1861
|
+
await db
|
|
1862
|
+
.insertInto('sync_subscription_state')
|
|
1863
|
+
.values([
|
|
1864
|
+
{
|
|
1865
|
+
state_id: 'default',
|
|
1866
|
+
subscription_id: 'catalog-meta',
|
|
1867
|
+
table: 'items',
|
|
1868
|
+
scopes_json: '{}',
|
|
1869
|
+
params_json: '{}',
|
|
1870
|
+
cursor: -1,
|
|
1871
|
+
bootstrap_state_json: JSON.stringify({
|
|
1872
|
+
asOfCommitSeq: 0,
|
|
1873
|
+
tables: ['items'],
|
|
1874
|
+
tableIndex: 0,
|
|
1875
|
+
rowCursor: null,
|
|
1876
|
+
}),
|
|
1877
|
+
status: 'active',
|
|
1878
|
+
created_at: now,
|
|
1879
|
+
updated_at: now,
|
|
1880
|
+
},
|
|
1881
|
+
{
|
|
1882
|
+
state_id: 'default',
|
|
1883
|
+
subscription_id: 'catalog-relations',
|
|
1884
|
+
table: 'scoped_items',
|
|
1885
|
+
scopes_json: '{}',
|
|
1886
|
+
params_json: '{}',
|
|
1887
|
+
cursor: 42,
|
|
1888
|
+
bootstrap_state_json: null,
|
|
1889
|
+
status: 'active',
|
|
1890
|
+
created_at: now,
|
|
1891
|
+
updated_at: now,
|
|
1892
|
+
},
|
|
1893
|
+
])
|
|
1894
|
+
.execute();
|
|
1895
|
+
|
|
1896
|
+
const pullState = await buildPullRequest(db, {
|
|
1897
|
+
clientId: 'client-1',
|
|
1898
|
+
stateId: 'default',
|
|
1899
|
+
subscriptions: [
|
|
1900
|
+
{ id: 'catalog-meta', table: 'items', bootstrapPhase: 0 },
|
|
1901
|
+
{ id: 'catalog-relations', table: 'scoped_items', bootstrapPhase: 1 },
|
|
1902
|
+
],
|
|
1903
|
+
});
|
|
1904
|
+
|
|
1905
|
+
expect(pullState.request.subscriptions.map((sub) => sub.id)).toEqual([
|
|
1906
|
+
'catalog-meta',
|
|
1907
|
+
'catalog-relations',
|
|
1908
|
+
]);
|
|
1909
|
+
});
|
|
1910
|
+
});
|