@syncular/client 0.0.6-185 → 0.0.6-201
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.d.ts +14 -71
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +81 -406
- package/dist/client.js.map +1 -1
- package/dist/create-client.d.ts +0 -2
- package/dist/create-client.d.ts.map +1 -1
- package/dist/create-client.js +2 -3
- package/dist/create-client.js.map +1 -1
- package/dist/engine/SyncEngine.d.ts +1 -0
- package/dist/engine/SyncEngine.d.ts.map +1 -1
- package/dist/engine/SyncEngine.js +21 -6
- package/dist/engine/SyncEngine.js.map +1 -1
- package/dist/handlers/create-handler.d.ts +5 -0
- package/dist/handlers/create-handler.d.ts.map +1 -1
- package/dist/handlers/create-handler.js +123 -4
- package/dist/handlers/create-handler.js.map +1 -1
- package/dist/handlers/types.d.ts +7 -0
- package/dist/handlers/types.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +0 -1
- package/dist/index.js.map +1 -1
- package/dist/internal/blob-schema.d.ts +32 -0
- package/dist/internal/blob-schema.d.ts.map +1 -0
- package/dist/internal/blob-schema.js +2 -0
- package/dist/internal/blob-schema.js.map +1 -0
- package/dist/mutations.d.ts.map +1 -1
- package/dist/mutations.js +15 -6
- package/dist/mutations.js.map +1 -1
- package/dist/plugins/incrementing-version.d.ts.map +1 -1
- package/dist/plugins/incrementing-version.js +20 -8
- package/dist/plugins/incrementing-version.js.map +1 -1
- package/dist/plugins/types.d.ts +26 -1
- package/dist/plugins/types.d.ts.map +1 -1
- package/dist/plugins/types.js.map +1 -1
- package/dist/pull-engine.d.ts +8 -2
- package/dist/pull-engine.d.ts.map +1 -1
- package/dist/pull-engine.js +150 -26
- package/dist/pull-engine.js.map +1 -1
- package/dist/push-engine.d.ts.map +1 -1
- package/dist/push-engine.js +21 -5
- package/dist/push-engine.js.map +1 -1
- package/dist/schema.d.ts +2 -2
- package/dist/schema.d.ts.map +1 -1
- package/dist/sync-loop.d.ts +3 -1
- package/dist/sync-loop.d.ts.map +1 -1
- package/dist/sync-loop.js +382 -139
- package/dist/sync-loop.js.map +1 -1
- package/package.json +76 -3
- package/src/client.test.ts +72 -155
- package/src/client.ts +113 -572
- package/src/create-client.ts +1 -6
- package/src/engine/SyncEngine.test.ts +90 -0
- package/src/engine/SyncEngine.ts +29 -9
- package/src/handlers/create-handler.ts +197 -4
- package/src/handlers/types.ts +11 -0
- package/src/index.ts +1 -2
- package/src/internal/blob-schema.ts +40 -0
- package/src/mutations.ts +17 -6
- package/src/plugins/incrementing-version.ts +36 -7
- package/src/plugins/types.ts +42 -0
- package/src/pull-engine.test.ts +494 -0
- package/src/pull-engine.ts +193 -29
- package/src/push-engine.ts +31 -5
- package/src/schema.ts +2 -2
- package/src/sync-loop.ts +538 -145
- package/dist/blobs/index.d.ts +0 -6
- package/dist/blobs/index.d.ts.map +0 -1
- package/dist/blobs/index.js +0 -6
- package/dist/blobs/index.js.map +0 -1
- package/dist/blobs/migrate.d.ts +0 -14
- package/dist/blobs/migrate.d.ts.map +0 -1
- package/dist/blobs/migrate.js +0 -59
- package/dist/blobs/migrate.js.map +0 -1
- package/dist/blobs/types.d.ts +0 -62
- package/dist/blobs/types.d.ts.map +0 -1
- package/dist/blobs/types.js +0 -5
- package/dist/blobs/types.js.map +0 -1
- package/src/blobs/index.ts +0 -6
- package/src/blobs/migrate.ts +0 -67
- package/src/blobs/types.ts +0 -84
package/dist/client.js
CHANGED
|
@@ -3,14 +3,12 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Single entry point for offline-first sync with:
|
|
5
5
|
* - Built-in mutations API
|
|
6
|
-
* - Optional blob support
|
|
7
6
|
* - Automatic migrations
|
|
8
7
|
* - Event-driven state management
|
|
9
8
|
* - Conflict handling with events
|
|
10
9
|
*/
|
|
11
10
|
import { countSyncMetric } from '@syncular/core';
|
|
12
11
|
import { sql } from 'kysely';
|
|
13
|
-
import { ensureClientBlobSchema } from './blobs/migrate.js';
|
|
14
12
|
import { SyncEngine } from './engine/SyncEngine.js';
|
|
15
13
|
import { ensureClientSyncSchema } from './migrate.js';
|
|
16
14
|
import { createMutationsApi, createOutboxCommit, } from './mutations.js';
|
|
@@ -49,6 +47,7 @@ export class Client {
|
|
|
49
47
|
engine = null;
|
|
50
48
|
started = false;
|
|
51
49
|
destroyed = false;
|
|
50
|
+
pluginFeatures = new Map();
|
|
52
51
|
eventListeners = new Map();
|
|
53
52
|
outboxStats = {
|
|
54
53
|
pending: 0,
|
|
@@ -59,13 +58,11 @@ export class Client {
|
|
|
59
58
|
};
|
|
60
59
|
/** Mutations API (always available) */
|
|
61
60
|
mutations;
|
|
62
|
-
/** Blob client (only available if blobStorage configured) */
|
|
63
|
-
blobs;
|
|
64
61
|
constructor(options) {
|
|
65
62
|
const plugins = withDefaultClientPlugins(options.plugins);
|
|
66
63
|
this.options = { ...options, plugins };
|
|
67
64
|
// Create mutations API
|
|
68
|
-
const
|
|
65
|
+
const baseCommitFn = createOutboxCommit({
|
|
69
66
|
db: this.options.db,
|
|
70
67
|
idColumn: this.options.idColumn ?? 'id',
|
|
71
68
|
versionColumn: this.options.versionColumn ?? 'server_version',
|
|
@@ -76,11 +73,13 @@ export class Client {
|
|
|
76
73
|
actorId: this.options.actorId,
|
|
77
74
|
clientId: this.options.clientId,
|
|
78
75
|
});
|
|
76
|
+
const commitFn = async (fn, options) => {
|
|
77
|
+
const outcome = await baseCommitFn(fn, options);
|
|
78
|
+
this.scheduleLocalSyncAfterMutation();
|
|
79
|
+
return outcome;
|
|
80
|
+
};
|
|
79
81
|
this.mutations = createMutationsApi(commitFn);
|
|
80
|
-
|
|
81
|
-
if (options.blobStorage && options.transport.blobs) {
|
|
82
|
-
this.blobs = this.createBlobClient(options.blobStorage, options.transport);
|
|
83
|
-
}
|
|
82
|
+
this.setupPluginFeatures();
|
|
84
83
|
}
|
|
85
84
|
// ===========================================================================
|
|
86
85
|
// Identity Getters
|
|
@@ -113,9 +112,7 @@ export class Client {
|
|
|
113
112
|
}
|
|
114
113
|
// Run migrations
|
|
115
114
|
await ensureClientSyncSchema(this.options.db);
|
|
116
|
-
|
|
117
|
-
await ensureClientBlobSchema(this.options.db);
|
|
118
|
-
}
|
|
115
|
+
await this.runPluginMigrations();
|
|
119
116
|
// Create and start engine
|
|
120
117
|
this.engine = new SyncEngine({
|
|
121
118
|
db: this.options.db,
|
|
@@ -155,6 +152,10 @@ export class Client {
|
|
|
155
152
|
*/
|
|
156
153
|
destroy() {
|
|
157
154
|
this.engine?.destroy();
|
|
155
|
+
const ctx = this.createPluginLifecycleContext();
|
|
156
|
+
for (const plugin of this.options.plugins ?? []) {
|
|
157
|
+
plugin.destroy?.(ctx);
|
|
158
|
+
}
|
|
158
159
|
this.eventListeners.clear();
|
|
159
160
|
this.destroyed = true;
|
|
160
161
|
}
|
|
@@ -358,6 +359,62 @@ export class Client {
|
|
|
358
359
|
}
|
|
359
360
|
}
|
|
360
361
|
}
|
|
362
|
+
setupPluginFeatures() {
|
|
363
|
+
const ctx = this.createPluginLifecycleContext();
|
|
364
|
+
for (const plugin of this.options.plugins ?? []) {
|
|
365
|
+
plugin.setup?.(ctx);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
async runPluginMigrations() {
|
|
369
|
+
const ctx = this.createPluginLifecycleContext();
|
|
370
|
+
for (const plugin of this.options.plugins ?? []) {
|
|
371
|
+
if (plugin.migrate) {
|
|
372
|
+
await plugin.migrate(ctx);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
createPluginLifecycleContext() {
|
|
377
|
+
return {
|
|
378
|
+
actorId: this.options.actorId,
|
|
379
|
+
clientId: this.options.clientId,
|
|
380
|
+
db: this.options.db,
|
|
381
|
+
transport: this.options.transport,
|
|
382
|
+
defineFeature: (name, value) => {
|
|
383
|
+
this.defineFeature(name, value);
|
|
384
|
+
},
|
|
385
|
+
emit: (event, payload) => {
|
|
386
|
+
this.emitPluginEvent(event, payload);
|
|
387
|
+
},
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
defineFeature(name, value) {
|
|
391
|
+
if (this.pluginFeatures.has(name)) {
|
|
392
|
+
throw new Error(`Client feature "${name}" is already registered`);
|
|
393
|
+
}
|
|
394
|
+
this.pluginFeatures.set(name, value);
|
|
395
|
+
Object.defineProperty(this, name, {
|
|
396
|
+
configurable: false,
|
|
397
|
+
enumerable: true,
|
|
398
|
+
value,
|
|
399
|
+
writable: false,
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
emitPluginEvent(event, payload) {
|
|
403
|
+
this.emit(event, payload);
|
|
404
|
+
}
|
|
405
|
+
scheduleLocalSyncAfterMutation() {
|
|
406
|
+
if (!this.started || !this.engine || this.destroyed) {
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
const engineState = this.engine.getState();
|
|
410
|
+
if (engineState.isRetrying ||
|
|
411
|
+
engineState.connectionState === 'disconnected') {
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
void this.engine.sync({ trigger: 'local' }).catch((error) => {
|
|
415
|
+
console.error('[Client] Unexpected background sync failure after local mutation:', error);
|
|
416
|
+
});
|
|
417
|
+
}
|
|
361
418
|
// ===========================================================================
|
|
362
419
|
// Conflicts
|
|
363
420
|
// ===========================================================================
|
|
@@ -473,68 +530,46 @@ export class Client {
|
|
|
473
530
|
* Get migration info.
|
|
474
531
|
*/
|
|
475
532
|
async getMigrationInfo() {
|
|
476
|
-
// Check if sync tables exist
|
|
477
533
|
let syncMigrated = false;
|
|
478
534
|
try {
|
|
479
|
-
await
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
535
|
+
await sql `
|
|
536
|
+
select 1
|
|
537
|
+
from ${sql.table('sync_outbox_commits')}
|
|
538
|
+
limit 1
|
|
539
|
+
`.execute(this.options.db);
|
|
484
540
|
syncMigrated = true;
|
|
485
541
|
}
|
|
486
542
|
catch {
|
|
487
543
|
syncMigrated = false;
|
|
488
544
|
}
|
|
489
|
-
|
|
490
|
-
let blobsMigrated = false;
|
|
491
|
-
try {
|
|
492
|
-
await this.options.db
|
|
493
|
-
.selectFrom('sync_blob_cache')
|
|
494
|
-
.selectAll()
|
|
495
|
-
.limit(1)
|
|
496
|
-
.execute();
|
|
497
|
-
blobsMigrated = true;
|
|
498
|
-
}
|
|
499
|
-
catch {
|
|
500
|
-
blobsMigrated = false;
|
|
501
|
-
}
|
|
502
|
-
return { syncMigrated, blobsMigrated };
|
|
545
|
+
return { syncMigrated };
|
|
503
546
|
}
|
|
504
547
|
/**
|
|
505
548
|
* Static: Check if migrations are needed.
|
|
506
549
|
*/
|
|
507
550
|
static async checkMigrations(db) {
|
|
508
551
|
let syncMigrated = false;
|
|
509
|
-
let blobsMigrated = false;
|
|
510
552
|
try {
|
|
511
|
-
await
|
|
553
|
+
await sql `
|
|
554
|
+
select 1
|
|
555
|
+
from ${sql.table('sync_outbox_commits')}
|
|
556
|
+
limit 1
|
|
557
|
+
`.execute(db);
|
|
512
558
|
syncMigrated = true;
|
|
513
559
|
}
|
|
514
560
|
catch {
|
|
515
561
|
syncMigrated = false;
|
|
516
562
|
}
|
|
517
|
-
try {
|
|
518
|
-
await db.selectFrom('sync_blob_cache').selectAll().limit(1).execute();
|
|
519
|
-
blobsMigrated = true;
|
|
520
|
-
}
|
|
521
|
-
catch {
|
|
522
|
-
blobsMigrated = false;
|
|
523
|
-
}
|
|
524
563
|
return {
|
|
525
564
|
needsMigration: !syncMigrated,
|
|
526
565
|
syncMigrated,
|
|
527
|
-
blobsMigrated,
|
|
528
566
|
};
|
|
529
567
|
}
|
|
530
568
|
/**
|
|
531
569
|
* Static: Run migrations.
|
|
532
570
|
*/
|
|
533
|
-
static async migrate(db
|
|
571
|
+
static async migrate(db) {
|
|
534
572
|
await ensureClientSyncSchema(db);
|
|
535
|
-
if (options?.blobs) {
|
|
536
|
-
await ensureClientBlobSchema(db);
|
|
537
|
-
}
|
|
538
573
|
}
|
|
539
574
|
// ===========================================================================
|
|
540
575
|
// Private Helpers
|
|
@@ -633,365 +668,5 @@ export class Client {
|
|
|
633
668
|
createdAt: info.createdAt,
|
|
634
669
|
};
|
|
635
670
|
}
|
|
636
|
-
createBlobClient(storage, transport) {
|
|
637
|
-
const db = this.options.db;
|
|
638
|
-
const blobs = transport.blobs;
|
|
639
|
-
const staleUploadingTimeoutMs = 30_000;
|
|
640
|
-
const maxUploadRetries = 3;
|
|
641
|
-
return {
|
|
642
|
-
async store(data, options) {
|
|
643
|
-
const bytes = await toUint8Array(data);
|
|
644
|
-
const mimeType = data instanceof Blob
|
|
645
|
-
? data.type
|
|
646
|
-
: (options?.mimeType ?? 'application/octet-stream');
|
|
647
|
-
// Compute hash
|
|
648
|
-
const hashHex = await computeSha256Hex(bytes);
|
|
649
|
-
const hash = `sha256:${hashHex}`;
|
|
650
|
-
// Store locally
|
|
651
|
-
await storage.write(hash, bytes);
|
|
652
|
-
// Store metadata
|
|
653
|
-
const now = Date.now();
|
|
654
|
-
await sql `
|
|
655
|
-
insert into ${sql.table('sync_blob_cache')} (
|
|
656
|
-
${sql.join([
|
|
657
|
-
sql.ref('hash'),
|
|
658
|
-
sql.ref('size'),
|
|
659
|
-
sql.ref('mime_type'),
|
|
660
|
-
sql.ref('cached_at'),
|
|
661
|
-
sql.ref('last_accessed_at'),
|
|
662
|
-
sql.ref('encrypted'),
|
|
663
|
-
sql.ref('key_id'),
|
|
664
|
-
sql.ref('body'),
|
|
665
|
-
])}
|
|
666
|
-
) values (
|
|
667
|
-
${sql.join([
|
|
668
|
-
sql.val(hash),
|
|
669
|
-
sql.val(bytes.length),
|
|
670
|
-
sql.val(mimeType),
|
|
671
|
-
sql.val(now),
|
|
672
|
-
sql.val(now),
|
|
673
|
-
sql.val(0),
|
|
674
|
-
sql.val(null),
|
|
675
|
-
sql.val(bytes),
|
|
676
|
-
])}
|
|
677
|
-
)
|
|
678
|
-
on conflict (${sql.ref('hash')}) do nothing
|
|
679
|
-
`.execute(db);
|
|
680
|
-
// Queue for upload or upload immediately
|
|
681
|
-
if (options?.immediate) {
|
|
682
|
-
// Initiate upload
|
|
683
|
-
const initResult = await blobs.initiateUpload({
|
|
684
|
-
hash,
|
|
685
|
-
size: bytes.length,
|
|
686
|
-
mimeType,
|
|
687
|
-
});
|
|
688
|
-
if (!initResult.exists && initResult.uploadUrl) {
|
|
689
|
-
// Upload to presigned URL
|
|
690
|
-
const uploadResponse = await fetch(initResult.uploadUrl, {
|
|
691
|
-
method: initResult.uploadMethod ?? 'PUT',
|
|
692
|
-
body: bytes.buffer,
|
|
693
|
-
headers: initResult.uploadHeaders,
|
|
694
|
-
});
|
|
695
|
-
if (!uploadResponse.ok) {
|
|
696
|
-
throw new Error(`Upload failed: ${uploadResponse.statusText}`);
|
|
697
|
-
}
|
|
698
|
-
// Complete upload
|
|
699
|
-
await blobs.completeUpload(hash);
|
|
700
|
-
}
|
|
701
|
-
}
|
|
702
|
-
else {
|
|
703
|
-
// Queue for later upload
|
|
704
|
-
await sql `
|
|
705
|
-
insert into ${sql.table('sync_blob_outbox')} (
|
|
706
|
-
${sql.join([
|
|
707
|
-
sql.ref('hash'),
|
|
708
|
-
sql.ref('size'),
|
|
709
|
-
sql.ref('mime_type'),
|
|
710
|
-
sql.ref('status'),
|
|
711
|
-
sql.ref('created_at'),
|
|
712
|
-
sql.ref('updated_at'),
|
|
713
|
-
sql.ref('attempt_count'),
|
|
714
|
-
sql.ref('error'),
|
|
715
|
-
sql.ref('encrypted'),
|
|
716
|
-
sql.ref('key_id'),
|
|
717
|
-
sql.ref('body'),
|
|
718
|
-
])}
|
|
719
|
-
) values (
|
|
720
|
-
${sql.join([
|
|
721
|
-
sql.val(hash),
|
|
722
|
-
sql.val(bytes.length),
|
|
723
|
-
sql.val(mimeType),
|
|
724
|
-
sql.val('pending'),
|
|
725
|
-
sql.val(now),
|
|
726
|
-
sql.val(now),
|
|
727
|
-
sql.val(0),
|
|
728
|
-
sql.val(null),
|
|
729
|
-
sql.val(0),
|
|
730
|
-
sql.val(null),
|
|
731
|
-
sql.val(bytes),
|
|
732
|
-
])}
|
|
733
|
-
)
|
|
734
|
-
on conflict (${sql.ref('hash')}) do nothing
|
|
735
|
-
`.execute(db);
|
|
736
|
-
}
|
|
737
|
-
return {
|
|
738
|
-
hash,
|
|
739
|
-
size: bytes.length,
|
|
740
|
-
mimeType,
|
|
741
|
-
};
|
|
742
|
-
},
|
|
743
|
-
async retrieve(ref) {
|
|
744
|
-
// Check local storage first
|
|
745
|
-
const local = await storage.read(ref.hash);
|
|
746
|
-
if (local) {
|
|
747
|
-
// Update access time
|
|
748
|
-
await sql `
|
|
749
|
-
update ${sql.table('sync_blob_cache')}
|
|
750
|
-
set ${sql.ref('last_accessed_at')} = ${sql.val(Date.now())}
|
|
751
|
-
where ${sql.ref('hash')} = ${sql.val(ref.hash)}
|
|
752
|
-
`.execute(db);
|
|
753
|
-
return local;
|
|
754
|
-
}
|
|
755
|
-
// Fetch from server
|
|
756
|
-
const { url } = await blobs.getDownloadUrl(ref.hash);
|
|
757
|
-
const response = await fetch(url);
|
|
758
|
-
if (!response.ok) {
|
|
759
|
-
throw new Error(`Download failed: ${response.statusText}`);
|
|
760
|
-
}
|
|
761
|
-
const bytes = new Uint8Array(await response.arrayBuffer());
|
|
762
|
-
// Cache locally
|
|
763
|
-
await storage.write(ref.hash, bytes);
|
|
764
|
-
const now = Date.now();
|
|
765
|
-
await sql `
|
|
766
|
-
insert into ${sql.table('sync_blob_cache')} (
|
|
767
|
-
${sql.join([
|
|
768
|
-
sql.ref('hash'),
|
|
769
|
-
sql.ref('size'),
|
|
770
|
-
sql.ref('mime_type'),
|
|
771
|
-
sql.ref('cached_at'),
|
|
772
|
-
sql.ref('last_accessed_at'),
|
|
773
|
-
sql.ref('encrypted'),
|
|
774
|
-
sql.ref('key_id'),
|
|
775
|
-
sql.ref('body'),
|
|
776
|
-
])}
|
|
777
|
-
) values (
|
|
778
|
-
${sql.join([
|
|
779
|
-
sql.val(ref.hash),
|
|
780
|
-
sql.val(bytes.length),
|
|
781
|
-
sql.val(ref.mimeType),
|
|
782
|
-
sql.val(now),
|
|
783
|
-
sql.val(now),
|
|
784
|
-
sql.val(0),
|
|
785
|
-
sql.val(null),
|
|
786
|
-
sql.val(bytes),
|
|
787
|
-
])}
|
|
788
|
-
)
|
|
789
|
-
on conflict (${sql.ref('hash')}) do nothing
|
|
790
|
-
`.execute(db);
|
|
791
|
-
return bytes;
|
|
792
|
-
},
|
|
793
|
-
async isLocal(hash) {
|
|
794
|
-
return storage.exists(hash);
|
|
795
|
-
},
|
|
796
|
-
async preload(refs) {
|
|
797
|
-
await Promise.all(refs.map((ref) => this.retrieve(ref)));
|
|
798
|
-
},
|
|
799
|
-
async processUploadQueue() {
|
|
800
|
-
let uploaded = 0;
|
|
801
|
-
let failed = 0;
|
|
802
|
-
const now = Date.now();
|
|
803
|
-
const staleThreshold = now - staleUploadingTimeoutMs;
|
|
804
|
-
await sql `
|
|
805
|
-
update ${sql.table('sync_blob_outbox')}
|
|
806
|
-
set
|
|
807
|
-
${sql.ref('status')} = ${sql.val('failed')},
|
|
808
|
-
${sql.ref('attempt_count')} = ${sql.ref('attempt_count')} + ${sql.val(1)},
|
|
809
|
-
${sql.ref('error')} = ${sql.val('Upload timed out while in uploading state')},
|
|
810
|
-
${sql.ref('updated_at')} = ${sql.val(now)}
|
|
811
|
-
where ${sql.ref('status')} = ${sql.val('uploading')}
|
|
812
|
-
and ${sql.ref('updated_at')} < ${sql.val(staleThreshold)}
|
|
813
|
-
and ${sql.ref('attempt_count')} + ${sql.val(1)} >= ${sql.val(maxUploadRetries)}
|
|
814
|
-
`.execute(db);
|
|
815
|
-
await sql `
|
|
816
|
-
update ${sql.table('sync_blob_outbox')}
|
|
817
|
-
set
|
|
818
|
-
${sql.ref('status')} = ${sql.val('pending')},
|
|
819
|
-
${sql.ref('attempt_count')} = ${sql.ref('attempt_count')} + ${sql.val(1)},
|
|
820
|
-
${sql.ref('error')} = ${sql.val('Upload timed out while in uploading state; retrying')},
|
|
821
|
-
${sql.ref('updated_at')} = ${sql.val(now)}
|
|
822
|
-
where ${sql.ref('status')} = ${sql.val('uploading')}
|
|
823
|
-
and ${sql.ref('updated_at')} < ${sql.val(staleThreshold)}
|
|
824
|
-
and ${sql.ref('attempt_count')} + ${sql.val(1)} < ${sql.val(maxUploadRetries)}
|
|
825
|
-
`.execute(db);
|
|
826
|
-
const pendingResult = await sql `
|
|
827
|
-
select
|
|
828
|
-
${sql.ref('hash')},
|
|
829
|
-
${sql.ref('size')},
|
|
830
|
-
${sql.ref('mime_type')},
|
|
831
|
-
${sql.ref('body')},
|
|
832
|
-
${sql.ref('attempt_count')}
|
|
833
|
-
from ${sql.table('sync_blob_outbox')}
|
|
834
|
-
where ${sql.ref('status')} = ${sql.val('pending')}
|
|
835
|
-
and ${sql.ref('attempt_count')} < ${sql.val(maxUploadRetries)}
|
|
836
|
-
limit ${sql.val(10)}
|
|
837
|
-
`.execute(db);
|
|
838
|
-
const pending = pendingResult.rows;
|
|
839
|
-
for (const item of pending) {
|
|
840
|
-
const nextAttemptCount = item.attempt_count + 1;
|
|
841
|
-
try {
|
|
842
|
-
// Mark as uploading
|
|
843
|
-
await sql `
|
|
844
|
-
update ${sql.table('sync_blob_outbox')}
|
|
845
|
-
set
|
|
846
|
-
${sql.ref('status')} = ${sql.val('uploading')},
|
|
847
|
-
${sql.ref('attempt_count')} = ${sql.val(nextAttemptCount)},
|
|
848
|
-
${sql.ref('error')} = ${sql.val(null)},
|
|
849
|
-
${sql.ref('updated_at')} = ${sql.val(Date.now())}
|
|
850
|
-
where ${sql.ref('hash')} = ${sql.val(item.hash)}
|
|
851
|
-
and ${sql.ref('status')} = ${sql.val('pending')}
|
|
852
|
-
`.execute(db);
|
|
853
|
-
// Initiate upload
|
|
854
|
-
const initResult = await blobs.initiateUpload({
|
|
855
|
-
hash: item.hash,
|
|
856
|
-
size: item.size,
|
|
857
|
-
mimeType: item.mime_type,
|
|
858
|
-
});
|
|
859
|
-
if (!initResult.exists && initResult.uploadUrl && item.body) {
|
|
860
|
-
const uploadBody = new ArrayBuffer(item.body.byteLength);
|
|
861
|
-
new Uint8Array(uploadBody).set(item.body);
|
|
862
|
-
// Upload
|
|
863
|
-
const uploadResponse = await fetch(initResult.uploadUrl, {
|
|
864
|
-
method: initResult.uploadMethod ?? 'PUT',
|
|
865
|
-
body: uploadBody,
|
|
866
|
-
headers: initResult.uploadHeaders,
|
|
867
|
-
});
|
|
868
|
-
if (!uploadResponse.ok) {
|
|
869
|
-
throw new Error(`Upload failed: ${uploadResponse.statusText}`);
|
|
870
|
-
}
|
|
871
|
-
// Complete
|
|
872
|
-
const completeResult = await blobs.completeUpload(item.hash);
|
|
873
|
-
if (!completeResult.ok) {
|
|
874
|
-
throw new Error(completeResult.error ?? 'Failed to complete blob upload');
|
|
875
|
-
}
|
|
876
|
-
}
|
|
877
|
-
// Mark as complete
|
|
878
|
-
await sql `
|
|
879
|
-
delete from ${sql.table('sync_blob_outbox')}
|
|
880
|
-
where ${sql.ref('hash')} = ${sql.val(item.hash)}
|
|
881
|
-
`.execute(db);
|
|
882
|
-
uploaded++;
|
|
883
|
-
}
|
|
884
|
-
catch (err) {
|
|
885
|
-
const nextStatus = nextAttemptCount >= maxUploadRetries ? 'failed' : 'pending';
|
|
886
|
-
await sql `
|
|
887
|
-
update ${sql.table('sync_blob_outbox')}
|
|
888
|
-
set
|
|
889
|
-
${sql.ref('status')} = ${sql.val(nextStatus)},
|
|
890
|
-
${sql.ref('error')} = ${sql.val(err instanceof Error ? err.message : 'Unknown error')},
|
|
891
|
-
${sql.ref('updated_at')} = ${sql.val(Date.now())}
|
|
892
|
-
where ${sql.ref('hash')} = ${sql.val(item.hash)}
|
|
893
|
-
`.execute(db);
|
|
894
|
-
if (nextStatus === 'failed') {
|
|
895
|
-
failed++;
|
|
896
|
-
}
|
|
897
|
-
}
|
|
898
|
-
}
|
|
899
|
-
return { uploaded, failed };
|
|
900
|
-
},
|
|
901
|
-
async getUploadQueueStats() {
|
|
902
|
-
const rowsResult = await sql `
|
|
903
|
-
select
|
|
904
|
-
${sql.ref('status')} as status,
|
|
905
|
-
count(${sql.ref('hash')}) as count
|
|
906
|
-
from ${sql.table('sync_blob_outbox')}
|
|
907
|
-
group by ${sql.ref('status')}
|
|
908
|
-
`.execute(db);
|
|
909
|
-
const stats = { pending: 0, uploading: 0, failed: 0 };
|
|
910
|
-
for (const row of rowsResult.rows) {
|
|
911
|
-
if (row.status === 'pending')
|
|
912
|
-
stats.pending = Number(row.count);
|
|
913
|
-
if (row.status === 'uploading')
|
|
914
|
-
stats.uploading = Number(row.count);
|
|
915
|
-
if (row.status === 'failed')
|
|
916
|
-
stats.failed = Number(row.count);
|
|
917
|
-
}
|
|
918
|
-
return stats;
|
|
919
|
-
},
|
|
920
|
-
async getCacheStats() {
|
|
921
|
-
const result = await sql `
|
|
922
|
-
select
|
|
923
|
-
count(${sql.ref('hash')}) as count,
|
|
924
|
-
sum(${sql.ref('size')}) as totalBytes
|
|
925
|
-
from ${sql.table('sync_blob_cache')}
|
|
926
|
-
`.execute(db);
|
|
927
|
-
const row = result.rows[0];
|
|
928
|
-
return {
|
|
929
|
-
count: Number(row?.count ?? 0),
|
|
930
|
-
totalBytes: Number(row?.totalBytes ?? 0),
|
|
931
|
-
};
|
|
932
|
-
},
|
|
933
|
-
async pruneCache(maxBytes) {
|
|
934
|
-
if (!maxBytes)
|
|
935
|
-
return 0;
|
|
936
|
-
// Get current size
|
|
937
|
-
const stats = await this.getCacheStats();
|
|
938
|
-
if (stats.totalBytes <= maxBytes)
|
|
939
|
-
return 0;
|
|
940
|
-
// Get oldest entries to delete
|
|
941
|
-
const toFree = stats.totalBytes - maxBytes;
|
|
942
|
-
let freed = 0;
|
|
943
|
-
const oldEntriesResult = await sql `
|
|
944
|
-
select ${sql.ref('hash')}, ${sql.ref('size')}
|
|
945
|
-
from ${sql.table('sync_blob_cache')}
|
|
946
|
-
order by ${sql.ref('last_accessed_at')} asc
|
|
947
|
-
`.execute(db);
|
|
948
|
-
const oldEntries = oldEntriesResult.rows;
|
|
949
|
-
for (const entry of oldEntries) {
|
|
950
|
-
if (freed >= toFree)
|
|
951
|
-
break;
|
|
952
|
-
await storage.delete(entry.hash);
|
|
953
|
-
await sql `
|
|
954
|
-
delete from ${sql.table('sync_blob_cache')}
|
|
955
|
-
where ${sql.ref('hash')} = ${sql.val(entry.hash)}
|
|
956
|
-
`.execute(db);
|
|
957
|
-
freed += entry.size;
|
|
958
|
-
}
|
|
959
|
-
return freed;
|
|
960
|
-
},
|
|
961
|
-
async clearCache() {
|
|
962
|
-
if (storage.clear) {
|
|
963
|
-
await storage.clear();
|
|
964
|
-
}
|
|
965
|
-
else {
|
|
966
|
-
// Delete each entry individually
|
|
967
|
-
const entriesResult = await sql `
|
|
968
|
-
select ${sql.ref('hash')}
|
|
969
|
-
from ${sql.table('sync_blob_cache')}
|
|
970
|
-
`.execute(db);
|
|
971
|
-
for (const entry of entriesResult.rows) {
|
|
972
|
-
await storage.delete(entry.hash);
|
|
973
|
-
}
|
|
974
|
-
}
|
|
975
|
-
await sql `delete from ${sql.table('sync_blob_cache')}`.execute(db);
|
|
976
|
-
},
|
|
977
|
-
};
|
|
978
|
-
}
|
|
979
|
-
}
|
|
980
|
-
// ============================================================================
|
|
981
|
-
// Helpers
|
|
982
|
-
// ============================================================================
|
|
983
|
-
async function toUint8Array(data) {
|
|
984
|
-
if (data instanceof Uint8Array) {
|
|
985
|
-
return data;
|
|
986
|
-
}
|
|
987
|
-
const buffer = await data.arrayBuffer();
|
|
988
|
-
return new Uint8Array(buffer);
|
|
989
|
-
}
|
|
990
|
-
async function computeSha256Hex(data) {
|
|
991
|
-
const hashBuffer = await crypto.subtle.digest('SHA-256', data.buffer);
|
|
992
|
-
const hashArray = new Uint8Array(hashBuffer);
|
|
993
|
-
return Array.from(hashArray)
|
|
994
|
-
.map((b) => b.toString(16).padStart(2, '0'))
|
|
995
|
-
.join('');
|
|
996
671
|
}
|
|
997
672
|
//# sourceMappingURL=client.js.map
|