@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.
Files changed (81) hide show
  1. package/dist/client.d.ts +14 -71
  2. package/dist/client.d.ts.map +1 -1
  3. package/dist/client.js +81 -406
  4. package/dist/client.js.map +1 -1
  5. package/dist/create-client.d.ts +0 -2
  6. package/dist/create-client.d.ts.map +1 -1
  7. package/dist/create-client.js +2 -3
  8. package/dist/create-client.js.map +1 -1
  9. package/dist/engine/SyncEngine.d.ts +1 -0
  10. package/dist/engine/SyncEngine.d.ts.map +1 -1
  11. package/dist/engine/SyncEngine.js +21 -6
  12. package/dist/engine/SyncEngine.js.map +1 -1
  13. package/dist/handlers/create-handler.d.ts +5 -0
  14. package/dist/handlers/create-handler.d.ts.map +1 -1
  15. package/dist/handlers/create-handler.js +123 -4
  16. package/dist/handlers/create-handler.js.map +1 -1
  17. package/dist/handlers/types.d.ts +7 -0
  18. package/dist/handlers/types.d.ts.map +1 -1
  19. package/dist/index.d.ts +1 -1
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +0 -1
  22. package/dist/index.js.map +1 -1
  23. package/dist/internal/blob-schema.d.ts +32 -0
  24. package/dist/internal/blob-schema.d.ts.map +1 -0
  25. package/dist/internal/blob-schema.js +2 -0
  26. package/dist/internal/blob-schema.js.map +1 -0
  27. package/dist/mutations.d.ts.map +1 -1
  28. package/dist/mutations.js +15 -6
  29. package/dist/mutations.js.map +1 -1
  30. package/dist/plugins/incrementing-version.d.ts.map +1 -1
  31. package/dist/plugins/incrementing-version.js +20 -8
  32. package/dist/plugins/incrementing-version.js.map +1 -1
  33. package/dist/plugins/types.d.ts +26 -1
  34. package/dist/plugins/types.d.ts.map +1 -1
  35. package/dist/plugins/types.js.map +1 -1
  36. package/dist/pull-engine.d.ts +8 -2
  37. package/dist/pull-engine.d.ts.map +1 -1
  38. package/dist/pull-engine.js +150 -26
  39. package/dist/pull-engine.js.map +1 -1
  40. package/dist/push-engine.d.ts.map +1 -1
  41. package/dist/push-engine.js +21 -5
  42. package/dist/push-engine.js.map +1 -1
  43. package/dist/schema.d.ts +2 -2
  44. package/dist/schema.d.ts.map +1 -1
  45. package/dist/sync-loop.d.ts +3 -1
  46. package/dist/sync-loop.d.ts.map +1 -1
  47. package/dist/sync-loop.js +382 -139
  48. package/dist/sync-loop.js.map +1 -1
  49. package/package.json +76 -3
  50. package/src/client.test.ts +72 -155
  51. package/src/client.ts +113 -572
  52. package/src/create-client.ts +1 -6
  53. package/src/engine/SyncEngine.test.ts +90 -0
  54. package/src/engine/SyncEngine.ts +29 -9
  55. package/src/handlers/create-handler.ts +197 -4
  56. package/src/handlers/types.ts +11 -0
  57. package/src/index.ts +1 -2
  58. package/src/internal/blob-schema.ts +40 -0
  59. package/src/mutations.ts +17 -6
  60. package/src/plugins/incrementing-version.ts +36 -7
  61. package/src/plugins/types.ts +42 -0
  62. package/src/pull-engine.test.ts +494 -0
  63. package/src/pull-engine.ts +193 -29
  64. package/src/push-engine.ts +31 -5
  65. package/src/schema.ts +2 -2
  66. package/src/sync-loop.ts +538 -145
  67. package/dist/blobs/index.d.ts +0 -6
  68. package/dist/blobs/index.d.ts.map +0 -1
  69. package/dist/blobs/index.js +0 -6
  70. package/dist/blobs/index.js.map +0 -1
  71. package/dist/blobs/migrate.d.ts +0 -14
  72. package/dist/blobs/migrate.d.ts.map +0 -1
  73. package/dist/blobs/migrate.js +0 -59
  74. package/dist/blobs/migrate.js.map +0 -1
  75. package/dist/blobs/types.d.ts +0 -62
  76. package/dist/blobs/types.d.ts.map +0 -1
  77. package/dist/blobs/types.js +0 -5
  78. package/dist/blobs/types.js.map +0 -1
  79. package/src/blobs/index.ts +0 -6
  80. package/src/blobs/migrate.ts +0 -67
  81. 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 commitFn = createOutboxCommit({
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
- // Create blob client if storage provided
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
- if (this.options.blobStorage) {
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 this.options.db
480
- .selectFrom('sync_outbox_commits')
481
- .selectAll()
482
- .limit(1)
483
- .execute();
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
- // Check if blob tables exist
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 db.selectFrom('sync_outbox_commits').selectAll().limit(1).execute();
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, options) {
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