@syncular/client 0.0.6-219 → 0.0.6-223
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 +26 -20
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +42 -7
- 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 +199 -26
- package/dist/engine/SyncEngine.js.map +1 -1
- package/dist/engine/types.d.ts +61 -3
- package/dist/engine/types.d.ts.map +1 -1
- package/dist/errors.d.ts +18 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +31 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.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 +732 -234
- 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 +79 -29
- package/src/engine/SyncEngine.test.ts +238 -0
- package/src/engine/SyncEngine.ts +257 -40
- package/src/engine/types.ts +81 -1
- package/src/errors.ts +59 -0
- package/src/index.ts +1 -0
- package/src/pull-engine.test.ts +422 -7
- package/src/pull-engine.ts +906 -276
- package/src/sync-loop.ts +52 -3
- package/src/sync.ts +6 -9
package/src/client.ts
CHANGED
|
@@ -26,6 +26,9 @@ import type {
|
|
|
26
26
|
SubscriptionProgress,
|
|
27
27
|
SyncAwaitBootstrapOptions,
|
|
28
28
|
SyncAwaitPhaseOptions,
|
|
29
|
+
SyncBootstrapStatus,
|
|
30
|
+
SyncBootstrapStatusOptions,
|
|
31
|
+
SyncClientSubscription,
|
|
29
32
|
SyncDiagnostics,
|
|
30
33
|
SyncEngineState,
|
|
31
34
|
SyncInspectorOptions,
|
|
@@ -35,6 +38,7 @@ import type {
|
|
|
35
38
|
SyncResetOptions,
|
|
36
39
|
SyncResetResult,
|
|
37
40
|
SyncResult,
|
|
41
|
+
SyncTraceEvent,
|
|
38
42
|
TransportHealth,
|
|
39
43
|
} from './engine/types';
|
|
40
44
|
import type { ClientHandlerCollection } from './handlers/collection';
|
|
@@ -74,16 +78,14 @@ export interface ClientOptions<DB extends SyncClientDb> {
|
|
|
74
78
|
actorId: string;
|
|
75
79
|
|
|
76
80
|
/** Subscriptions to sync */
|
|
77
|
-
subscriptions:
|
|
78
|
-
id: string;
|
|
79
|
-
table: string;
|
|
80
|
-
scopes?: Record<string, string | string[]>;
|
|
81
|
-
params?: Record<string, unknown>;
|
|
82
|
-
}>;
|
|
81
|
+
subscriptions: SyncClientSubscription[];
|
|
83
82
|
|
|
84
83
|
/** Optional: Sync plugins */
|
|
85
84
|
plugins?: SyncClientPlugin[];
|
|
86
85
|
|
|
86
|
+
/** Optional: Emit structured pull/apply tracing to client events and inspector snapshots. */
|
|
87
|
+
traceEnabled?: boolean;
|
|
88
|
+
|
|
87
89
|
/** Optional: Enable realtime transport mode */
|
|
88
90
|
realtimeEnabled?: boolean;
|
|
89
91
|
|
|
@@ -145,7 +147,17 @@ export interface ClientState {
|
|
|
145
147
|
/** Last successful sync timestamp */
|
|
146
148
|
lastSyncAt: number | null;
|
|
147
149
|
/** Current error if any */
|
|
148
|
-
error: {
|
|
150
|
+
error: {
|
|
151
|
+
code: string;
|
|
152
|
+
message: string;
|
|
153
|
+
stage?: string;
|
|
154
|
+
retryable?: boolean;
|
|
155
|
+
httpStatus?: number;
|
|
156
|
+
subscriptionId?: string;
|
|
157
|
+
chunkId?: string;
|
|
158
|
+
table?: string;
|
|
159
|
+
stateId?: string;
|
|
160
|
+
} | null;
|
|
149
161
|
/** Outbox statistics */
|
|
150
162
|
outbox: OutboxStats;
|
|
151
163
|
}
|
|
@@ -175,6 +187,7 @@ export interface MigrationInfo {
|
|
|
175
187
|
type ClientEventType =
|
|
176
188
|
| 'sync:start'
|
|
177
189
|
| 'sync:complete'
|
|
190
|
+
| 'sync:trace'
|
|
178
191
|
| 'sync:live'
|
|
179
192
|
| 'sync:error'
|
|
180
193
|
| 'push:result'
|
|
@@ -193,8 +206,19 @@ type ClientEventType =
|
|
|
193
206
|
type ClientEventPayloads = {
|
|
194
207
|
'sync:start': { timestamp: number };
|
|
195
208
|
'sync:complete': SyncResult;
|
|
209
|
+
'sync:trace': SyncTraceEvent;
|
|
196
210
|
'sync:live': { timestamp: number };
|
|
197
|
-
'sync:error': {
|
|
211
|
+
'sync:error': {
|
|
212
|
+
code: string;
|
|
213
|
+
message: string;
|
|
214
|
+
stage?: string;
|
|
215
|
+
retryable?: boolean;
|
|
216
|
+
httpStatus?: number;
|
|
217
|
+
subscriptionId?: string;
|
|
218
|
+
chunkId?: string;
|
|
219
|
+
table?: string;
|
|
220
|
+
stateId?: string;
|
|
221
|
+
};
|
|
198
222
|
'push:result': PushResultInfo;
|
|
199
223
|
'bootstrap:start': {
|
|
200
224
|
timestamp: number;
|
|
@@ -361,8 +385,11 @@ export class Client<DB extends SyncClientDb = SyncClientDb> {
|
|
|
361
385
|
table: s.table,
|
|
362
386
|
scopes: s.scopes ?? {},
|
|
363
387
|
params: s.params ?? {},
|
|
388
|
+
bootstrapState: s.bootstrapState,
|
|
389
|
+
bootstrapPhase: s.bootstrapPhase,
|
|
364
390
|
})),
|
|
365
391
|
plugins: this.options.plugins,
|
|
392
|
+
traceEnabled: this.options.traceEnabled,
|
|
366
393
|
realtimeEnabled: this.options.realtimeEnabled,
|
|
367
394
|
pollIntervalMs: this.options.pollIntervalMs,
|
|
368
395
|
dedupeRows: this.options.dedupeRows,
|
|
@@ -423,22 +450,14 @@ export class Client<DB extends SyncClientDb = SyncClientDb> {
|
|
|
423
450
|
/**
|
|
424
451
|
* Update subscriptions.
|
|
425
452
|
*/
|
|
426
|
-
updateSubscriptions(
|
|
427
|
-
subscriptions: Array<{
|
|
428
|
-
id: string;
|
|
429
|
-
table: string;
|
|
430
|
-
scopes?: Record<string, string | string[]>;
|
|
431
|
-
params?: Record<string, unknown>;
|
|
432
|
-
}>
|
|
433
|
-
): void {
|
|
453
|
+
updateSubscriptions(subscriptions: SyncClientSubscription[]): void {
|
|
434
454
|
this.options.subscriptions = subscriptions;
|
|
435
455
|
if (this.engine) {
|
|
436
456
|
this.engine.updateSubscriptions(
|
|
437
|
-
subscriptions.map((
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
params: s.params ?? {},
|
|
457
|
+
subscriptions.map((subscription) => ({
|
|
458
|
+
...subscription,
|
|
459
|
+
scopes: subscription.scopes ?? {},
|
|
460
|
+
params: subscription.params ?? {},
|
|
442
461
|
}))
|
|
443
462
|
);
|
|
444
463
|
}
|
|
@@ -447,17 +466,14 @@ export class Client<DB extends SyncClientDb = SyncClientDb> {
|
|
|
447
466
|
/**
|
|
448
467
|
* Get current subscriptions.
|
|
449
468
|
*/
|
|
450
|
-
getSubscriptions():
|
|
451
|
-
id: string;
|
|
452
|
-
table: string;
|
|
453
|
-
scopes: Record<string, string | string[]>;
|
|
454
|
-
params: Record<string, unknown>;
|
|
455
|
-
}> {
|
|
469
|
+
getSubscriptions(): SyncClientSubscription[] {
|
|
456
470
|
return this.options.subscriptions.map((s) => ({
|
|
457
471
|
id: s.id,
|
|
458
472
|
table: s.table,
|
|
459
473
|
scopes: s.scopes ?? {},
|
|
460
474
|
params: s.params ?? {},
|
|
475
|
+
bootstrapState: s.bootstrapState,
|
|
476
|
+
bootstrapPhase: s.bootstrapPhase,
|
|
461
477
|
}));
|
|
462
478
|
}
|
|
463
479
|
|
|
@@ -502,7 +518,17 @@ export class Client<DB extends SyncClientDb = SyncClientDb> {
|
|
|
502
518
|
connectionState: engineState.connectionState,
|
|
503
519
|
lastSyncAt: engineState.lastSyncAt,
|
|
504
520
|
error: engineState.error
|
|
505
|
-
? {
|
|
521
|
+
? {
|
|
522
|
+
code: engineState.error.code,
|
|
523
|
+
message: engineState.error.message,
|
|
524
|
+
stage: engineState.error.stage,
|
|
525
|
+
retryable: engineState.error.retryable,
|
|
526
|
+
httpStatus: engineState.error.httpStatus,
|
|
527
|
+
subscriptionId: engineState.error.subscriptionId,
|
|
528
|
+
chunkId: engineState.error.chunkId,
|
|
529
|
+
table: engineState.error.table,
|
|
530
|
+
stateId: engineState.error.stateId,
|
|
531
|
+
}
|
|
506
532
|
: null,
|
|
507
533
|
outbox: this.outboxStats,
|
|
508
534
|
};
|
|
@@ -524,6 +550,16 @@ export class Client<DB extends SyncClientDb = SyncClientDb> {
|
|
|
524
550
|
return this.engine.getProgress();
|
|
525
551
|
}
|
|
526
552
|
|
|
553
|
+
/**
|
|
554
|
+
* Get bootstrap readiness for the configured blocking phase or a selected subset.
|
|
555
|
+
*/
|
|
556
|
+
async getBootstrapStatus(
|
|
557
|
+
options?: SyncBootstrapStatusOptions
|
|
558
|
+
): Promise<SyncBootstrapStatus | null> {
|
|
559
|
+
if (!this.engine) return null;
|
|
560
|
+
return this.engine.getBootstrapStatus(options);
|
|
561
|
+
}
|
|
562
|
+
|
|
527
563
|
/**
|
|
528
564
|
* Get a diagnostics snapshot for support/debug flows.
|
|
529
565
|
*/
|
|
@@ -957,12 +993,26 @@ export class Client<DB extends SyncClientDb = SyncClientDb> {
|
|
|
957
993
|
});
|
|
958
994
|
});
|
|
959
995
|
|
|
996
|
+
this.engine.on('sync:trace', (payload) => {
|
|
997
|
+
this.emit('sync:trace', payload);
|
|
998
|
+
});
|
|
999
|
+
|
|
960
1000
|
this.engine.on('sync:live', (payload) => {
|
|
961
1001
|
this.emit('sync:live', payload);
|
|
962
1002
|
});
|
|
963
1003
|
|
|
964
1004
|
this.engine.on('sync:error', (error) => {
|
|
965
|
-
this.emit('sync:error', {
|
|
1005
|
+
this.emit('sync:error', {
|
|
1006
|
+
code: error.code,
|
|
1007
|
+
message: error.message,
|
|
1008
|
+
stage: error.stage,
|
|
1009
|
+
retryable: error.retryable,
|
|
1010
|
+
httpStatus: error.httpStatus,
|
|
1011
|
+
subscriptionId: error.subscriptionId,
|
|
1012
|
+
chunkId: error.chunkId,
|
|
1013
|
+
table: error.table,
|
|
1014
|
+
stateId: error.stateId,
|
|
1015
|
+
});
|
|
966
1016
|
});
|
|
967
1017
|
|
|
968
1018
|
this.engine.on('push:result', (payload) => {
|
|
@@ -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
|
{
|
|
@@ -681,11 +745,185 @@ describe('SyncEngine WS inline apply', () => {
|
|
|
681
745
|
|
|
682
746
|
const state = engine.getState();
|
|
683
747
|
expect(state.error?.code).toBe('SNAPSHOT_CHUNK_NOT_FOUND');
|
|
748
|
+
expect(state.error?.stage).toBe('pull');
|
|
684
749
|
expect(state.error?.retryable).toBe(false);
|
|
685
750
|
expect(state.retryCount).toBe(1);
|
|
686
751
|
expect(state.isRetrying).toBe(false);
|
|
687
752
|
});
|
|
688
753
|
|
|
754
|
+
it('classifies gzip decode failures with chunk metadata', async () => {
|
|
755
|
+
const invalidCompressed = new Uint8Array(
|
|
756
|
+
gzipSync(new TextEncoder().encode('truncated-gzip')).subarray(0, 8)
|
|
757
|
+
);
|
|
758
|
+
const transport: SyncTransport = {
|
|
759
|
+
capabilities: {
|
|
760
|
+
snapshotChunkReadMode: 'bytes',
|
|
761
|
+
preferMaterializedSnapshots: true,
|
|
762
|
+
},
|
|
763
|
+
async sync() {
|
|
764
|
+
return {
|
|
765
|
+
pull: {
|
|
766
|
+
ok: true,
|
|
767
|
+
subscriptions: [
|
|
768
|
+
{
|
|
769
|
+
id: 'sub-1',
|
|
770
|
+
status: 'active',
|
|
771
|
+
scopes: {},
|
|
772
|
+
bootstrap: true,
|
|
773
|
+
bootstrapState: null,
|
|
774
|
+
nextCursor: 1,
|
|
775
|
+
commits: [],
|
|
776
|
+
snapshots: [
|
|
777
|
+
{
|
|
778
|
+
table: 'tasks',
|
|
779
|
+
rows: [],
|
|
780
|
+
chunks: [
|
|
781
|
+
{
|
|
782
|
+
id: 'chunk-1',
|
|
783
|
+
byteLength: invalidCompressed.length,
|
|
784
|
+
sha256: '',
|
|
785
|
+
encoding: 'json-row-frame-v1',
|
|
786
|
+
compression: 'gzip',
|
|
787
|
+
},
|
|
788
|
+
],
|
|
789
|
+
isFirstPage: true,
|
|
790
|
+
isLastPage: true,
|
|
791
|
+
},
|
|
792
|
+
],
|
|
793
|
+
},
|
|
794
|
+
],
|
|
795
|
+
},
|
|
796
|
+
};
|
|
797
|
+
},
|
|
798
|
+
async fetchSnapshotChunk() {
|
|
799
|
+
return invalidCompressed;
|
|
800
|
+
},
|
|
801
|
+
};
|
|
802
|
+
|
|
803
|
+
const handlers: ClientHandlerCollection<TestDb> = [
|
|
804
|
+
{
|
|
805
|
+
table: 'tasks',
|
|
806
|
+
async applySnapshot() {},
|
|
807
|
+
async clearAll() {},
|
|
808
|
+
async applyChange() {},
|
|
809
|
+
},
|
|
810
|
+
];
|
|
811
|
+
|
|
812
|
+
const engine = new SyncEngine<TestDb>({
|
|
813
|
+
db,
|
|
814
|
+
transport,
|
|
815
|
+
handlers,
|
|
816
|
+
actorId: 'u1',
|
|
817
|
+
clientId: 'client-gzip-failure',
|
|
818
|
+
subscriptions: [
|
|
819
|
+
{
|
|
820
|
+
id: 'sub-1',
|
|
821
|
+
table: 'tasks',
|
|
822
|
+
scopes: {},
|
|
823
|
+
},
|
|
824
|
+
],
|
|
825
|
+
stateId: 'default',
|
|
826
|
+
pollIntervalMs: 60_000,
|
|
827
|
+
maxRetries: 1,
|
|
828
|
+
});
|
|
829
|
+
|
|
830
|
+
await engine.start();
|
|
831
|
+
engine.stop();
|
|
832
|
+
|
|
833
|
+
const state = engine.getState();
|
|
834
|
+
expect(state.error?.code).toBe('SNAPSHOT_GZIP_DECODE_FAILED');
|
|
835
|
+
expect(state.error?.stage).toBe('snapshot-gzip-decode');
|
|
836
|
+
expect(state.error?.subscriptionId).toBe('sub-1');
|
|
837
|
+
expect(state.error?.chunkId).toBe('chunk-1');
|
|
838
|
+
expect(state.error?.table).toBe('tasks');
|
|
839
|
+
expect(state.error?.retryable).toBe(false);
|
|
840
|
+
});
|
|
841
|
+
|
|
842
|
+
it('classifies snapshot apply failures with stage metadata', async () => {
|
|
843
|
+
const rows = [{ id: 't2', title: 'new', server_version: 1 }];
|
|
844
|
+
const encoded = encodeSnapshotRows(rows);
|
|
845
|
+
const compressed = new Uint8Array(gzipSync(encoded));
|
|
846
|
+
const transport: SyncTransport = {
|
|
847
|
+
async sync() {
|
|
848
|
+
return {
|
|
849
|
+
pull: {
|
|
850
|
+
ok: true,
|
|
851
|
+
subscriptions: [
|
|
852
|
+
{
|
|
853
|
+
id: 'sub-1',
|
|
854
|
+
status: 'active',
|
|
855
|
+
scopes: {},
|
|
856
|
+
bootstrap: true,
|
|
857
|
+
bootstrapState: null,
|
|
858
|
+
nextCursor: 1,
|
|
859
|
+
commits: [],
|
|
860
|
+
snapshots: [
|
|
861
|
+
{
|
|
862
|
+
table: 'tasks',
|
|
863
|
+
rows: [],
|
|
864
|
+
chunks: [
|
|
865
|
+
{
|
|
866
|
+
id: 'chunk-1',
|
|
867
|
+
byteLength: compressed.length,
|
|
868
|
+
sha256: '',
|
|
869
|
+
encoding: 'json-row-frame-v1',
|
|
870
|
+
compression: 'gzip',
|
|
871
|
+
},
|
|
872
|
+
],
|
|
873
|
+
isFirstPage: true,
|
|
874
|
+
isLastPage: true,
|
|
875
|
+
},
|
|
876
|
+
],
|
|
877
|
+
},
|
|
878
|
+
],
|
|
879
|
+
},
|
|
880
|
+
};
|
|
881
|
+
},
|
|
882
|
+
async fetchSnapshotChunk() {
|
|
883
|
+
return compressed;
|
|
884
|
+
},
|
|
885
|
+
};
|
|
886
|
+
|
|
887
|
+
const handlers: ClientHandlerCollection<TestDb> = [
|
|
888
|
+
{
|
|
889
|
+
table: 'tasks',
|
|
890
|
+
async applySnapshot() {
|
|
891
|
+
throw new Error('forced snapshot apply failure');
|
|
892
|
+
},
|
|
893
|
+
async clearAll() {},
|
|
894
|
+
async applyChange() {},
|
|
895
|
+
},
|
|
896
|
+
];
|
|
897
|
+
|
|
898
|
+
const engine = new SyncEngine<TestDb>({
|
|
899
|
+
db,
|
|
900
|
+
transport,
|
|
901
|
+
handlers,
|
|
902
|
+
actorId: 'u1',
|
|
903
|
+
clientId: 'client-apply-failure',
|
|
904
|
+
subscriptions: [
|
|
905
|
+
{
|
|
906
|
+
id: 'sub-1',
|
|
907
|
+
table: 'tasks',
|
|
908
|
+
scopes: {},
|
|
909
|
+
},
|
|
910
|
+
],
|
|
911
|
+
stateId: 'default',
|
|
912
|
+
pollIntervalMs: 60_000,
|
|
913
|
+
maxRetries: 1,
|
|
914
|
+
});
|
|
915
|
+
|
|
916
|
+
await engine.start();
|
|
917
|
+
engine.stop();
|
|
918
|
+
|
|
919
|
+
const state = engine.getState();
|
|
920
|
+
expect(state.error?.code).toBe('SNAPSHOT_APPLY_FAILED');
|
|
921
|
+
expect(state.error?.stage).toBe('snapshot-apply');
|
|
922
|
+
expect(state.error?.subscriptionId).toBe('sub-1');
|
|
923
|
+
expect(state.error?.table).toBe('tasks');
|
|
924
|
+
expect(state.error?.retryable).toBe(false);
|
|
925
|
+
});
|
|
926
|
+
|
|
689
927
|
it('skips outbox and conflict refresh after a read-only successful sync', async () => {
|
|
690
928
|
const handlers: ClientHandlerCollection<TestDb> = [
|
|
691
929
|
{
|