@syncular/client 0.18.0 → 0.20.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/README.md +112 -19
- package/dist/apply.d.ts +2 -0
- package/dist/apply.js +32 -8
- package/dist/blob.d.ts +12 -39
- package/dist/blob.js +63 -134
- package/dist/client.d.ts +6 -3
- package/dist/client.js +95 -58
- package/dist/http.d.ts +0 -8
- package/dist/http.js +36 -3
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/outbox.js +3 -0
- package/dist/progress.d.ts +24 -0
- package/dist/progress.js +42 -0
- package/dist/schema.js +3 -0
- package/dist/transport.d.ts +2 -1
- package/dist/wasm-database.js +41 -1
- package/dist/worker-entry.d.ts +2 -0
- package/dist/worker-entry.js +34 -7
- package/dist/worker-host.d.ts +3 -0
- package/dist/worker-host.js +13 -1
- package/dist/worker-protocol.d.ts +4 -0
- package/package.json +4 -3
- package/src/apply.ts +39 -13
- package/src/blob.ts +86 -157
- package/src/client.ts +110 -69
- package/src/http.ts +41 -3
- package/src/index.ts +6 -0
- package/src/outbox.ts +3 -0
- package/src/progress.ts +62 -0
- package/src/schema.ts +5 -0
- package/src/transport.ts +5 -1
- package/src/wasm-database.ts +79 -1
- package/src/worker-entry.ts +38 -7
- package/src/worker-host.ts +17 -1
- package/src/worker-protocol.ts +2 -0
package/src/worker-entry.ts
CHANGED
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
withClientDiagnosticsHost,
|
|
21
21
|
} from './diagnostics';
|
|
22
22
|
import { encryptionConfigFromKeyring } from './encryption';
|
|
23
|
-
import { ClientSyncError } from './errors';
|
|
23
|
+
import { ClientSyncError, STORAGE_BUSY_CODE } from './errors';
|
|
24
24
|
import {
|
|
25
25
|
httpBlobTransport,
|
|
26
26
|
httpSegmentDownloader,
|
|
@@ -51,6 +51,8 @@ export interface SyncWorkerOverrides {
|
|
|
51
51
|
readonly openDatabase?: (
|
|
52
52
|
config: WorkerInitConfig,
|
|
53
53
|
) => Promise<ClientDatabase> | ClientDatabase;
|
|
54
|
+
/** Retry deadline indirection for deterministic worker-host tests. */
|
|
55
|
+
readonly waitForStorageRetry?: (delayMs: number) => Promise<void>;
|
|
54
56
|
readonly createTransport?: (config: WorkerInitConfig) => SyncTransport;
|
|
55
57
|
readonly createSegments?: (
|
|
56
58
|
config: WorkerInitConfig,
|
|
@@ -244,9 +246,12 @@ export function startSyncWorker(overrides: SyncWorkerOverrides = {}): void {
|
|
|
244
246
|
return Object.assign(gateOffline(inner), {
|
|
245
247
|
...(fetchUrl !== undefined
|
|
246
248
|
? {
|
|
247
|
-
fetchUrl: (
|
|
249
|
+
fetchUrl: (
|
|
250
|
+
url: string,
|
|
251
|
+
onProgress?: (bytesReceived: number) => void,
|
|
252
|
+
) => {
|
|
248
253
|
assertOnline();
|
|
249
|
-
return fetchUrl(url);
|
|
254
|
+
return fetchUrl(url, onProgress);
|
|
250
255
|
},
|
|
251
256
|
}
|
|
252
257
|
: {}),
|
|
@@ -262,10 +267,33 @@ export function startSyncWorker(overrides: SyncWorkerOverrides = {}): void {
|
|
|
262
267
|
}
|
|
263
268
|
autoSync = config.autoSync ?? true;
|
|
264
269
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
270
|
+
for (let attempt = 0; ; attempt++) {
|
|
271
|
+
try {
|
|
272
|
+
database =
|
|
273
|
+
overrides.openDatabase !== undefined
|
|
274
|
+
? await overrides.openDatabase(config)
|
|
275
|
+
: await defaultOpenDatabase(config);
|
|
276
|
+
break;
|
|
277
|
+
} catch (error) {
|
|
278
|
+
if (
|
|
279
|
+
config.database.mode !== 'persistent' ||
|
|
280
|
+
!(error instanceof ClientSyncError) ||
|
|
281
|
+
error.code !== STORAGE_BUSY_CODE ||
|
|
282
|
+
!error.retryable ||
|
|
283
|
+
attempt === 6
|
|
284
|
+
) {
|
|
285
|
+
throw error;
|
|
286
|
+
}
|
|
287
|
+
// Retain the leader lease while the previous worker's OPFS handles
|
|
288
|
+
// drain. Only opening is retried; core initialization has not started.
|
|
289
|
+
const delayMs = Math.min(50 * 2 ** attempt, 1000);
|
|
290
|
+
if (overrides.waitForStorageRetry !== undefined) {
|
|
291
|
+
await overrides.waitForStorageRetry(delayMs);
|
|
292
|
+
} else {
|
|
293
|
+
await new Promise<void>((resolve) => setTimeout(resolve, delayMs));
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
269
297
|
const transport = gateOffline(
|
|
270
298
|
overrides.createTransport !== undefined
|
|
271
299
|
? overrides.createTransport(config)
|
|
@@ -336,6 +364,9 @@ export function startSyncWorker(overrides: SyncWorkerOverrides = {}): void {
|
|
|
336
364
|
post({ t: 'event', event: { kind: 'presence', scopeKey } });
|
|
337
365
|
},
|
|
338
366
|
});
|
|
367
|
+
started.onProgress((progress) => {
|
|
368
|
+
if (!closed) post({ t: 'event', event: { kind: 'progress', progress } });
|
|
369
|
+
});
|
|
339
370
|
started.onChange((batch) => {
|
|
340
371
|
if (!closed) post({ t: 'event', event: { kind: 'change', batch } });
|
|
341
372
|
});
|
package/src/worker-host.ts
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ProgressEmitter,
|
|
3
|
+
type SyncProgress,
|
|
4
|
+
type SyncProgressListener,
|
|
5
|
+
} from './progress';
|
|
1
6
|
import type { PromiseMethods } from './client';
|
|
2
7
|
/**
|
|
3
8
|
* Main-thread side of the worker mode and the
|
|
@@ -273,6 +278,7 @@ export class SyncClientHandle implements PromiseMethods<WorkerApi> {
|
|
|
273
278
|
#core: LeaderCore | undefined;
|
|
274
279
|
#follower: FollowerLink | undefined;
|
|
275
280
|
readonly #invalidation: InvalidationEmitter;
|
|
281
|
+
readonly #progress = new ProgressEmitter();
|
|
276
282
|
readonly #changes: ChangeEmitter;
|
|
277
283
|
readonly #presence: Set<(scopeKey: string) => void>;
|
|
278
284
|
readonly #diagnostics: ClientDiagnosticsEmitter;
|
|
@@ -376,7 +382,9 @@ export class SyncClientHandle implements PromiseMethods<WorkerApi> {
|
|
|
376
382
|
|
|
377
383
|
/** @internal — dispatch a worker/relayed event to handle-local listeners. */
|
|
378
384
|
__dispatchEvent(event: SyncWorkerEvent): void {
|
|
379
|
-
if (event.kind === '
|
|
385
|
+
if (event.kind === 'progress') {
|
|
386
|
+
this.#progress.emit(event.progress);
|
|
387
|
+
} else if (event.kind === 'presence') {
|
|
380
388
|
for (const listener of this.#presence) {
|
|
381
389
|
try {
|
|
382
390
|
listener(event.scopeKey);
|
|
@@ -409,6 +417,13 @@ export class SyncClientHandle implements PromiseMethods<WorkerApi> {
|
|
|
409
417
|
return this.#invalidation.on(listener);
|
|
410
418
|
}
|
|
411
419
|
|
|
420
|
+
onProgress(listener: SyncProgressListener): () => void {
|
|
421
|
+
return this.#progress.on(listener);
|
|
422
|
+
}
|
|
423
|
+
progressSnapshot(): SyncProgress | undefined {
|
|
424
|
+
return this.#progress.snapshot();
|
|
425
|
+
}
|
|
426
|
+
|
|
412
427
|
onChange(listener: ClientChangeListener): () => void {
|
|
413
428
|
return this.#changes.on(listener);
|
|
414
429
|
}
|
|
@@ -646,6 +661,7 @@ export class SyncClientHandle implements PromiseMethods<WorkerApi> {
|
|
|
646
661
|
async close(): Promise<void> {
|
|
647
662
|
if (this.#closed) return;
|
|
648
663
|
this.#closed = true;
|
|
664
|
+
this.#progress.clear();
|
|
649
665
|
this.#devtoolsUnregister();
|
|
650
666
|
if (this.#follower !== undefined) {
|
|
651
667
|
this.#follower.close();
|
package/src/worker-protocol.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { SyncProgress } from './progress';
|
|
1
2
|
/**
|
|
2
3
|
* The worker RPC protocol: the whole
|
|
3
4
|
* client core runs in a Web Worker; the UI thread talks to it through
|
|
@@ -206,6 +207,7 @@ export interface WorkerErrorShape {
|
|
|
206
207
|
}
|
|
207
208
|
|
|
208
209
|
export type SyncWorkerEvent =
|
|
210
|
+
| { readonly kind: 'progress'; readonly progress: SyncProgress }
|
|
209
211
|
| {
|
|
210
212
|
readonly kind: 'sync-needed';
|
|
211
213
|
readonly reason: 'startup' | 'hello' | WakeReason;
|