@syncular/client 0.37.0 → 0.39.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/client.js +66 -17
- package/package.json +3 -3
- package/src/client.ts +68 -17
package/dist/client.js
CHANGED
|
@@ -3443,6 +3443,50 @@ export class SyncClient {
|
|
|
3443
3443
|
let section;
|
|
3444
3444
|
let errorFrame;
|
|
3445
3445
|
let deltaCursor = -1;
|
|
3446
|
+
// §5.4/§5.5: issue every segment download of this response before the
|
|
3447
|
+
// apply loop reaches the first one, so a paged bootstrap overlaps its page
|
|
3448
|
+
// fetches instead of paying one blocking round trip per page. Eligibility
|
|
3449
|
+
// mirrors the loop: a descriptor outside an applied section, inside a
|
|
3450
|
+
// skipped delta section, or from an unconfigured downloader is never
|
|
3451
|
+
// fetched, and a prefetched failure is only observed where the loop awaits
|
|
3452
|
+
// it. A promise left over because an earlier frame ended apply is handled
|
|
3453
|
+
// at creation, never unhandled.
|
|
3454
|
+
const prefetched = new Map();
|
|
3455
|
+
let reporting;
|
|
3456
|
+
{
|
|
3457
|
+
let openSub;
|
|
3458
|
+
for (const frame of message.frames) {
|
|
3459
|
+
if (frame.type === 'SUB_START') {
|
|
3460
|
+
const sub = subsById.get(frame.id);
|
|
3461
|
+
openSub =
|
|
3462
|
+
sub === undefined ||
|
|
3463
|
+
(mode === 'delta' &&
|
|
3464
|
+
(sub.status !== 'active' || sub.bootstrapState !== undefined))
|
|
3465
|
+
? undefined
|
|
3466
|
+
: sub;
|
|
3467
|
+
}
|
|
3468
|
+
else if (frame.type === 'SUB_END') {
|
|
3469
|
+
openSub = undefined;
|
|
3470
|
+
}
|
|
3471
|
+
else if (frame.type === 'ERROR') {
|
|
3472
|
+
break; // the round fails at this frame: later refs are not fetched
|
|
3473
|
+
}
|
|
3474
|
+
else if (frame.type === 'SEGMENT_REF' &&
|
|
3475
|
+
openSub !== undefined &&
|
|
3476
|
+
this.#config.segments !== undefined &&
|
|
3477
|
+
!(frame.mediaType === 'sqlite' &&
|
|
3478
|
+
(this.#acceptMask() & ACCEPT_SQLITE) === 0)) {
|
|
3479
|
+
const pending = this.#downloadSegment(frame, openSub, (bytes) => {
|
|
3480
|
+
// Only the descriptor the loop is applying reports bytes; a
|
|
3481
|
+
// sibling still in flight would overwrite its progress.
|
|
3482
|
+
if (reporting === frame)
|
|
3483
|
+
this.#progress.update({ bytesReceived: bytes });
|
|
3484
|
+
});
|
|
3485
|
+
pending.catch(() => { });
|
|
3486
|
+
prefetched.set(frame, pending);
|
|
3487
|
+
}
|
|
3488
|
+
}
|
|
3489
|
+
}
|
|
3446
3490
|
// Each durable observer transaction emits its own revisioned batch.
|
|
3447
3491
|
// Async decrypt/download work happens outside SQLite transactions.
|
|
3448
3492
|
this.#beginDiagnosticsDeferral();
|
|
@@ -3533,9 +3577,27 @@ export class SyncClient {
|
|
|
3533
3577
|
(this.#acceptMask() & ACCEPT_SQLITE) === 0) {
|
|
3534
3578
|
throw invalidRequest('SEGMENT_REF mediaType sqlite was not advertised in accept (§4.2)');
|
|
3535
3579
|
}
|
|
3536
|
-
|
|
3537
|
-
|
|
3538
|
-
|
|
3580
|
+
let bytes;
|
|
3581
|
+
if (frame.type === 'SEGMENT_REF') {
|
|
3582
|
+
this.#progress.update({
|
|
3583
|
+
phase: 'download',
|
|
3584
|
+
subscriptionId: section.sub.id,
|
|
3585
|
+
table: frame.table,
|
|
3586
|
+
segmentId: frame.segmentId,
|
|
3587
|
+
bytesReceived: 0,
|
|
3588
|
+
bytesTotal: frame.byteLength,
|
|
3589
|
+
rowsProcessed: 0,
|
|
3590
|
+
rowsTotal: frame.rowCount,
|
|
3591
|
+
});
|
|
3592
|
+
const prefetch = prefetched.get(frame);
|
|
3593
|
+
if (prefetch !== undefined)
|
|
3594
|
+
reporting = frame;
|
|
3595
|
+
bytes = await (prefetch ??
|
|
3596
|
+
this.#downloadSegment(frame, section.sub, (bytesReceived) => this.#progress.update({ bytesReceived })));
|
|
3597
|
+
}
|
|
3598
|
+
else {
|
|
3599
|
+
bytes = frame.payload;
|
|
3600
|
+
}
|
|
3539
3601
|
if (frame.type === 'SEGMENT_REF')
|
|
3540
3602
|
this.#progress.update({ phase: 'import' });
|
|
3541
3603
|
if (frame.type === 'SEGMENT_REF' && frame.mediaType === 'sqlite') {
|
|
@@ -3896,24 +3958,11 @@ export class SyncClient {
|
|
|
3896
3958
|
throw error;
|
|
3897
3959
|
}
|
|
3898
3960
|
}
|
|
3899
|
-
async #downloadSegment(frame, sub) {
|
|
3961
|
+
async #downloadSegment(frame, sub, onProgress) {
|
|
3900
3962
|
const downloader = this.#config.segments;
|
|
3901
3963
|
if (downloader === undefined) {
|
|
3902
3964
|
throw invalidRequest('received SEGMENT_REF but no segment downloader is configured');
|
|
3903
3965
|
}
|
|
3904
|
-
this.#progress.update({
|
|
3905
|
-
phase: 'download',
|
|
3906
|
-
subscriptionId: sub.id,
|
|
3907
|
-
table: frame.table,
|
|
3908
|
-
segmentId: frame.segmentId,
|
|
3909
|
-
bytesReceived: 0,
|
|
3910
|
-
bytesTotal: frame.byteLength,
|
|
3911
|
-
rowsProcessed: 0,
|
|
3912
|
-
rowsTotal: frame.rowCount,
|
|
3913
|
-
});
|
|
3914
|
-
const onProgress = (bytesReceived) => {
|
|
3915
|
-
this.#progress.update({ bytesReceived });
|
|
3916
|
-
};
|
|
3917
3966
|
let bytes;
|
|
3918
3967
|
if (frame.url !== undefined) {
|
|
3919
3968
|
// §5.4: a url-carrying descriptor MUST be fetched from that URL —
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@syncular/client",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.39.0",
|
|
4
4
|
"description": "Syncular TypeScript client core — offline-first sync over SQLite (WASM/OPFS, Bun, Node)",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Benjamin Kniffler",
|
|
@@ -97,10 +97,10 @@
|
|
|
97
97
|
},
|
|
98
98
|
"dependencies": {
|
|
99
99
|
"@sqlite.org/sqlite-wasm": "^3.53.0-build1",
|
|
100
|
-
"@syncular/core": "0.
|
|
100
|
+
"@syncular/core": "0.39.0"
|
|
101
101
|
},
|
|
102
102
|
"devDependencies": {
|
|
103
|
-
"@syncular/server": "0.
|
|
103
|
+
"@syncular/server": "0.39.0",
|
|
104
104
|
"playwright": "1.63.0",
|
|
105
105
|
"@electric-sql/pglite": "^0.5.4"
|
|
106
106
|
}
|
package/src/client.ts
CHANGED
|
@@ -4832,6 +4832,52 @@ export class SyncClient {
|
|
|
4832
4832
|
let errorFrame: ClientSyncError | undefined;
|
|
4833
4833
|
let deltaCursor = -1;
|
|
4834
4834
|
|
|
4835
|
+
// §5.4/§5.5: issue every segment download of this response before the
|
|
4836
|
+
// apply loop reaches the first one, so a paged bootstrap overlaps its page
|
|
4837
|
+
// fetches instead of paying one blocking round trip per page. Eligibility
|
|
4838
|
+
// mirrors the loop: a descriptor outside an applied section, inside a
|
|
4839
|
+
// skipped delta section, or from an unconfigured downloader is never
|
|
4840
|
+
// fetched, and a prefetched failure is only observed where the loop awaits
|
|
4841
|
+
// it. A promise left over because an earlier frame ended apply is handled
|
|
4842
|
+
// at creation, never unhandled.
|
|
4843
|
+
const prefetched = new Map<SegmentRefFrame, Promise<Uint8Array>>();
|
|
4844
|
+
let reporting: SegmentRefFrame | undefined;
|
|
4845
|
+
{
|
|
4846
|
+
let openSub: SubscriptionRecord | undefined;
|
|
4847
|
+
for (const frame of message.frames) {
|
|
4848
|
+
if (frame.type === 'SUB_START') {
|
|
4849
|
+
const sub = subsById.get(frame.id);
|
|
4850
|
+
openSub =
|
|
4851
|
+
sub === undefined ||
|
|
4852
|
+
(mode === 'delta' &&
|
|
4853
|
+
(sub.status !== 'active' || sub.bootstrapState !== undefined))
|
|
4854
|
+
? undefined
|
|
4855
|
+
: sub;
|
|
4856
|
+
} else if (frame.type === 'SUB_END') {
|
|
4857
|
+
openSub = undefined;
|
|
4858
|
+
} else if (frame.type === 'ERROR') {
|
|
4859
|
+
break; // the round fails at this frame: later refs are not fetched
|
|
4860
|
+
} else if (
|
|
4861
|
+
frame.type === 'SEGMENT_REF' &&
|
|
4862
|
+
openSub !== undefined &&
|
|
4863
|
+
this.#config.segments !== undefined &&
|
|
4864
|
+
!(
|
|
4865
|
+
frame.mediaType === 'sqlite' &&
|
|
4866
|
+
(this.#acceptMask() & ACCEPT_SQLITE) === 0
|
|
4867
|
+
)
|
|
4868
|
+
) {
|
|
4869
|
+
const pending = this.#downloadSegment(frame, openSub, (bytes) => {
|
|
4870
|
+
// Only the descriptor the loop is applying reports bytes; a
|
|
4871
|
+
// sibling still in flight would overwrite its progress.
|
|
4872
|
+
if (reporting === frame)
|
|
4873
|
+
this.#progress.update({ bytesReceived: bytes });
|
|
4874
|
+
});
|
|
4875
|
+
pending.catch(() => {});
|
|
4876
|
+
prefetched.set(frame, pending);
|
|
4877
|
+
}
|
|
4878
|
+
}
|
|
4879
|
+
}
|
|
4880
|
+
|
|
4835
4881
|
// Each durable observer transaction emits its own revisioned batch.
|
|
4836
4882
|
// Async decrypt/download work happens outside SQLite transactions.
|
|
4837
4883
|
this.#beginDiagnosticsDeferral();
|
|
@@ -4949,10 +4995,27 @@ export class SyncClient {
|
|
|
4949
4995
|
'SEGMENT_REF mediaType sqlite was not advertised in accept (§4.2)',
|
|
4950
4996
|
);
|
|
4951
4997
|
}
|
|
4952
|
-
|
|
4953
|
-
|
|
4954
|
-
|
|
4955
|
-
:
|
|
4998
|
+
let bytes: Uint8Array;
|
|
4999
|
+
if (frame.type === 'SEGMENT_REF') {
|
|
5000
|
+
this.#progress.update({
|
|
5001
|
+
phase: 'download',
|
|
5002
|
+
subscriptionId: section.sub.id,
|
|
5003
|
+
table: frame.table,
|
|
5004
|
+
segmentId: frame.segmentId,
|
|
5005
|
+
bytesReceived: 0,
|
|
5006
|
+
bytesTotal: frame.byteLength,
|
|
5007
|
+
rowsProcessed: 0,
|
|
5008
|
+
rowsTotal: frame.rowCount,
|
|
5009
|
+
});
|
|
5010
|
+
const prefetch = prefetched.get(frame);
|
|
5011
|
+
if (prefetch !== undefined) reporting = frame;
|
|
5012
|
+
bytes = await (prefetch ??
|
|
5013
|
+
this.#downloadSegment(frame, section.sub, (bytesReceived) =>
|
|
5014
|
+
this.#progress.update({ bytesReceived }),
|
|
5015
|
+
));
|
|
5016
|
+
} else {
|
|
5017
|
+
bytes = frame.payload;
|
|
5018
|
+
}
|
|
4956
5019
|
if (frame.type === 'SEGMENT_REF')
|
|
4957
5020
|
this.#progress.update({ phase: 'import' });
|
|
4958
5021
|
if (frame.type === 'SEGMENT_REF' && frame.mediaType === 'sqlite') {
|
|
@@ -5458,6 +5521,7 @@ export class SyncClient {
|
|
|
5458
5521
|
async #downloadSegment(
|
|
5459
5522
|
frame: SegmentRefFrame,
|
|
5460
5523
|
sub: SubscriptionRecord,
|
|
5524
|
+
onProgress: (bytesReceived: number) => void,
|
|
5461
5525
|
): Promise<Uint8Array> {
|
|
5462
5526
|
const downloader = this.#config.segments;
|
|
5463
5527
|
if (downloader === undefined) {
|
|
@@ -5465,19 +5529,6 @@ export class SyncClient {
|
|
|
5465
5529
|
'received SEGMENT_REF but no segment downloader is configured',
|
|
5466
5530
|
);
|
|
5467
5531
|
}
|
|
5468
|
-
this.#progress.update({
|
|
5469
|
-
phase: 'download',
|
|
5470
|
-
subscriptionId: sub.id,
|
|
5471
|
-
table: frame.table,
|
|
5472
|
-
segmentId: frame.segmentId,
|
|
5473
|
-
bytesReceived: 0,
|
|
5474
|
-
bytesTotal: frame.byteLength,
|
|
5475
|
-
rowsProcessed: 0,
|
|
5476
|
-
rowsTotal: frame.rowCount,
|
|
5477
|
-
});
|
|
5478
|
-
const onProgress = (bytesReceived: number): void => {
|
|
5479
|
-
this.#progress.update({ bytesReceived });
|
|
5480
|
-
};
|
|
5481
5532
|
let bytes: Uint8Array;
|
|
5482
5533
|
if (frame.url !== undefined) {
|
|
5483
5534
|
// §5.4: a url-carrying descriptor MUST be fetched from that URL —
|