@keepkit/core 0.28.3 → 0.28.5

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.
@@ -81,6 +81,12 @@ function isKeepItem(value) {
81
81
  function isKeepItemArray(value) {
82
82
  return Array.isArray(value) && value.every(isKeepItem);
83
83
  }
84
+ function isKeepCollectionDefinition(value) {
85
+ return isRecord(value) && typeof value.id === "string" && value.id.trim().length > 0 && typeof value.name === "string" && value.name.trim().length > 0 && (value.scope === void 0 || isSyncScope(value.scope));
86
+ }
87
+ function isKeepCollectionMembership(value) {
88
+ return isRecord(value) && typeof value.collectionId === "string" && value.collectionId.trim().length > 0 && typeof value.itemId === "string" && value.itemId.trim().length > 0 && typeof value.order === "number" && Number.isInteger(value.order) && Number.isFinite(value.order) && value.order >= 0 && (value.scope === void 0 || isSyncScope(value.scope));
89
+ }
84
90
  function isRecord(value) {
85
91
  return typeof value === "object" && value !== null;
86
92
  }
@@ -102,6 +108,28 @@ var ScopedStorageAdapter = class {
102
108
  this.scope = scope;
103
109
  const scopeKey = getKeepScopeKey(scope);
104
110
  this.storageKey = base.storageKey ? base.storageKey.endsWith(scopeKey) && scopeKey ? base.storageKey : `${base.storageKey}${scopeKey}` : void 0;
111
+ if (base.getCollections && base.setCollection && base.removeCollection) {
112
+ const getCollections = base.getCollections.bind(base);
113
+ const setCollection = base.setCollection.bind(base);
114
+ const removeCollection = base.removeCollection.bind(base);
115
+ this.getCollections = async () => {
116
+ const collections = await getCollections();
117
+ return this.scope ? collections.filter((collection) => isSameKeepScope(collection.scope, this.scope)) : collections;
118
+ };
119
+ this.setCollection = (collection) => setCollection({ ...collection, ...this.scope ? { scope: this.scope } : {} });
120
+ this.removeCollection = (id) => removeCollection(id, this.scope);
121
+ }
122
+ if (base.getCollectionMemberships && base.setCollectionMembership && base.removeCollectionMembership) {
123
+ const getMemberships = base.getCollectionMemberships.bind(base);
124
+ const setMembership = base.setCollectionMembership.bind(base);
125
+ const removeMembership = base.removeCollectionMembership.bind(base);
126
+ this.getCollectionMemberships = async () => {
127
+ const memberships = await getMemberships();
128
+ return this.scope ? memberships.filter((membership) => isSameKeepScope(membership.scope, this.scope)) : memberships;
129
+ };
130
+ this.setCollectionMembership = (membership) => setMembership({ ...membership, ...this.scope ? { scope: this.scope } : {} });
131
+ this.removeCollectionMembership = (collectionId, itemId) => removeMembership(collectionId, itemId, this.scope);
132
+ }
105
133
  }
106
134
  async getAll() {
107
135
  const items = await this.base.getAll();
@@ -211,7 +239,7 @@ var LocalStorageSyncQueueAdapter = class {
211
239
  } catch (cause) {
212
240
  throw Object.assign(new Error("KeepKit sync queue contains invalid JSON."), { cause });
213
241
  }
214
- if (!Array.isArray(value) || !value.every(isSyncOperation)) {
242
+ if (!Array.isArray(value) || !value.every(isSyncQueueOperation)) {
215
243
  throw new Error("KeepKit sync queue contains invalid operations.");
216
244
  }
217
245
  return value;
@@ -241,7 +269,7 @@ var IndexedDBSyncQueueAdapter = class {
241
269
  if (!database) return [];
242
270
  const transaction = database.transaction(this.storeName, "readonly");
243
271
  const value = await requestToPromise(transaction.objectStore(this.storeName).getAll());
244
- if (!Array.isArray(value) || !value.every(isSyncOperation)) {
272
+ if (!Array.isArray(value) || !value.every(isSyncQueueOperation)) {
245
273
  throw new Error("KeepKit sync queue contains invalid operations.");
246
274
  }
247
275
  return value;
@@ -362,6 +390,69 @@ var SyncStorageAdapter = class {
362
390
  this.retryDelayMs = Math.max(0, options.retryDelayMs ?? 0);
363
391
  this.retryBackoff = Math.max(1, options.retryBackoff ?? 2);
364
392
  this.storageKey = this.local.storageKey;
393
+ if (this.local.getCollections && this.local.setCollection && this.local.removeCollection) {
394
+ const getCollections = this.local.getCollections.bind(this.local);
395
+ const setCollection = this.local.setCollection.bind(this.local);
396
+ const removeCollection = this.local.removeCollection.bind(this.local);
397
+ this.getCollections = async () => {
398
+ const collections = await getCollections();
399
+ const scope = this.scope;
400
+ return scope ? collections.filter((collection) => sameScope(collection.scope, scope)) : collections;
401
+ };
402
+ this.setCollection = async (collection) => {
403
+ const scopedCollection = { ...collection, ...this.scope ? { scope: this.scope } : {} };
404
+ if (this.remote.pushCollection) {
405
+ const operation = this.createCollectionOperation("upsert", scopedCollection.id, scopedCollection);
406
+ await this.enqueueBeforeLocalWrite(operation);
407
+ try {
408
+ await setCollection(scopedCollection);
409
+ } catch (cause) {
410
+ await this.removeQueued(operation.operationId);
411
+ throw cause;
412
+ }
413
+ this.notifyDataListeners();
414
+ this.setPendingState();
415
+ return;
416
+ }
417
+ await setCollection(scopedCollection);
418
+ this.notifyDataListeners();
419
+ };
420
+ this.removeCollection = async (id) => {
421
+ if (this.remote.pushCollection) {
422
+ const operation = this.createCollectionOperation("remove", id);
423
+ await this.enqueueBeforeLocalWrite(operation);
424
+ try {
425
+ await removeCollection(id, this.scope);
426
+ } catch (cause) {
427
+ await this.removeQueued(operation.operationId);
428
+ throw cause;
429
+ }
430
+ this.notifyDataListeners();
431
+ this.setPendingState();
432
+ return;
433
+ }
434
+ await removeCollection(id, this.scope);
435
+ this.notifyDataListeners();
436
+ };
437
+ }
438
+ if (this.local.getCollectionMemberships && this.local.setCollectionMembership && this.local.removeCollectionMembership) {
439
+ const getMemberships = this.local.getCollectionMemberships.bind(this.local);
440
+ const setMembership = this.local.setCollectionMembership.bind(this.local);
441
+ const removeMembership = this.local.removeCollectionMembership.bind(this.local);
442
+ this.getCollectionMemberships = async () => {
443
+ const memberships = await getMemberships();
444
+ const scope = this.scope;
445
+ return scope ? memberships.filter((membership) => sameScope(membership.scope, scope)) : memberships;
446
+ };
447
+ this.setCollectionMembership = async (membership) => {
448
+ await setMembership({ ...membership, ...this.scope ? { scope: this.scope } : {} });
449
+ this.notifyDataListeners();
450
+ };
451
+ this.removeCollectionMembership = async (collectionId, itemId) => {
452
+ await removeMembership(collectionId, itemId, this.scope);
453
+ this.notifyDataListeners();
454
+ };
455
+ }
365
456
  if (typeof window !== "undefined") {
366
457
  this.onlineHandler = () => void this.flushSync();
367
458
  window.addEventListener("online", this.onlineHandler);
@@ -496,6 +587,16 @@ var SyncStorageAdapter = class {
496
587
  for (const operation of [...this.queueItems]) {
497
588
  if (this.disposed) return;
498
589
  try {
590
+ if (isCollectionSyncOperation(operation)) {
591
+ if (!this.remote.pushCollection) throw new Error("Remote sync driver does not support collection sync.");
592
+ await this.pushCollectionWithRetry(operation);
593
+ await this.removeQueued(operation.operationId);
594
+ this.updateState({
595
+ status: this.queueItems.length > 0 ? "syncing" : "synced",
596
+ lastSyncedAt: this.now()
597
+ });
598
+ continue;
599
+ }
499
600
  const result = await this.pushWithRetry(operation);
500
601
  if (result.type === "conflict") {
501
602
  const local = operation.item;
@@ -560,6 +661,17 @@ var SyncStorageAdapter = class {
560
661
  ...this.scope ? { scope: this.scope } : {}
561
662
  };
562
663
  }
664
+ createCollectionOperation(type, id, collection) {
665
+ return {
666
+ operationId: `${this.clientId}:${this.now()}:${createId()}`,
667
+ type,
668
+ entity: "collection",
669
+ id,
670
+ ...collection ? { collection } : {},
671
+ createdAt: this.now(),
672
+ ...this.scope ? { scope: this.scope } : {}
673
+ };
674
+ }
563
675
  applyScope(item) {
564
676
  return this.scope ? { ...item, scope: this.scope } : item;
565
677
  }
@@ -576,6 +688,20 @@ var SyncStorageAdapter = class {
576
688
  }
577
689
  }
578
690
  }
691
+ async pushCollectionWithRetry(operation) {
692
+ let attempt = 0;
693
+ while (true) {
694
+ try {
695
+ await this.remote.pushCollection?.({ ...operation, attempts: attempt });
696
+ return;
697
+ } catch (error) {
698
+ if (isKeepSyncAuthError(error) || attempt >= this.maxRetries) throw error;
699
+ attempt += 1;
700
+ const delay = this.retryDelayMs * this.retryBackoff ** (attempt - 1);
701
+ if (delay > 0) await wait(delay);
702
+ }
703
+ }
704
+ }
579
705
  async enqueueBeforeLocalWrite(operation) {
580
706
  await this.enqueueManyBeforeLocalWrite([operation]);
581
707
  }
@@ -584,7 +710,8 @@ var SyncStorageAdapter = class {
584
710
  const next = [...this.queueItems];
585
711
  for (const operation of operations) {
586
712
  for (let index = next.length - 1; index >= 0; index -= 1) {
587
- if (next[index]?.id !== operation.id) continue;
713
+ const queued = next[index];
714
+ if (!queued || getQueueIdentity(queued) !== getQueueIdentity(operation)) continue;
588
715
  next.splice(index, 1);
589
716
  }
590
717
  next.push(operation);
@@ -644,30 +771,53 @@ var SyncStorageAdapter = class {
644
771
  });
645
772
  }
646
773
  async pullRemote() {
647
- if (!this.remote.pull) return true;
774
+ if (!this.remote.pull && !this.remote.pullCollections) return true;
648
775
  try {
649
- const remoteItems = await this.remote.pull();
650
- const pendingIds = new Set(this.queueItems.map((operation) => operation.id));
651
- const localItems = await this.getAll();
652
- const localById = new Map(localItems.map((item) => [item.id, item]));
653
- const remoteById = /* @__PURE__ */ new Map();
654
- for (const item of remoteItems) {
655
- if (this.scope && item.scope && !sameScope(item.scope, this.scope)) continue;
656
- if (pendingIds.has(item.id)) continue;
657
- const previous = remoteById.get(item.id);
658
- remoteById.set(item.id, previous ? mergeKeepItemLists([item], [previous])[0] ?? item : item);
776
+ if (this.remote.pull) {
777
+ const remoteItems = await this.remote.pull();
778
+ const pendingIds = new Set(
779
+ this.queueItems.filter((operation) => operation.entity !== "collection").map((operation) => operation.id)
780
+ );
781
+ const localItems = await this.getAll();
782
+ const localById = new Map(localItems.map((item) => [item.id, item]));
783
+ const remoteById = /* @__PURE__ */ new Map();
784
+ for (const item of remoteItems) {
785
+ if (this.scope && item.scope && !sameScope(item.scope, this.scope)) continue;
786
+ if (pendingIds.has(item.id)) continue;
787
+ const previous = remoteById.get(item.id);
788
+ remoteById.set(item.id, previous ? mergeKeepItemLists([item], [previous])[0] ?? item : item);
789
+ }
790
+ const incoming = [...remoteById.values()].flatMap((item) => {
791
+ const current = localById.get(item.id);
792
+ if (current && item.updatedAt < current.updatedAt && (item.lastOpenedAt ?? 0) <= (current.lastOpenedAt ?? 0)) {
793
+ return [];
794
+ }
795
+ const merged = current ? mergeKeepItemLists([item], [current])[0] ?? item : item;
796
+ return [this.applyScope(merged)];
797
+ });
798
+ if (incoming.length > 0) {
799
+ await persistKeepItems(this.local, incoming);
800
+ this.notifyDataListeners();
801
+ }
659
802
  }
660
- const incoming = [...remoteById.values()].flatMap((item) => {
661
- const current = localById.get(item.id);
662
- if (current && item.updatedAt < current.updatedAt && (item.lastOpenedAt ?? 0) <= (current.lastOpenedAt ?? 0)) {
663
- return [];
803
+ if (this.remote.pullCollections && this.local.getCollections && this.local.setCollection && this.local.removeCollection) {
804
+ const remoteCollections = await this.remote.pullCollections();
805
+ const localCollections = await this.local.getCollections();
806
+ const pendingIds = new Set(this.queueItems.filter(isCollectionSyncOperation).map((operation) => operation.id));
807
+ const remoteById = new Map(
808
+ remoteCollections.filter((collection) => !this.scope || sameScope(collection.scope, this.scope)).map((collection) => [collection.id, collection])
809
+ );
810
+ for (const collection of remoteById.values()) {
811
+ if (pendingIds.has(collection.id)) continue;
812
+ await this.local.setCollection({ ...collection, ...this.scope ? { scope: this.scope } : {} });
664
813
  }
665
- const merged = current ? mergeKeepItemLists([item], [current])[0] ?? item : item;
666
- return [this.applyScope(merged)];
667
- });
668
- if (incoming.length === 0) return true;
669
- await persistKeepItems(this.local, incoming);
670
- this.notifyDataListeners();
814
+ for (const collection of localCollections) {
815
+ if (pendingIds.has(collection.id) || remoteById.has(collection.id)) continue;
816
+ if (this.scope && !sameScope(collection.scope, this.scope)) continue;
817
+ await this.local.removeCollection(collection.id, this.scope);
818
+ }
819
+ this.notifyDataListeners();
820
+ }
671
821
  return true;
672
822
  } catch (error) {
673
823
  this.updateState({ status: "error", error });
@@ -688,7 +838,17 @@ var SyncStorageAdapter = class {
688
838
  };
689
839
  function isSyncOperation(value) {
690
840
  if (!isRecord(value)) return false;
691
- return typeof value.operationId === "string" && (value.type === "upsert" || value.type === "remove") && typeof value.id === "string" && typeof value.createdAt === "number";
841
+ return typeof value.operationId === "string" && (value.type === "upsert" || value.type === "remove") && typeof value.id === "string" && typeof value.createdAt === "number" && (value.entity === void 0 || value.entity === "item" || value.entity === "collection") && (value.collection === void 0 || isKeepCollectionDefinition(value.collection)) && (value.entity !== "collection" || value.type !== "upsert" || isKeepCollectionDefinition(value.collection));
842
+ }
843
+ function isCollectionSyncOperation(value) {
844
+ if (!isRecord(value)) return false;
845
+ return typeof value.operationId === "string" && value.entity === "collection" && (value.type === "upsert" || value.type === "remove") && typeof value.id === "string" && typeof value.createdAt === "number" && (value.collection === void 0 || isKeepCollectionDefinition(value.collection));
846
+ }
847
+ function isSyncQueueOperation(value) {
848
+ return isSyncOperation(value) || isCollectionSyncOperation(value);
849
+ }
850
+ function getQueueIdentity(operation) {
851
+ return `${operation.entity === "collection" ? "collection" : "item"}:${operation.id}`;
692
852
  }
693
853
  function createId() {
694
854
  if (typeof crypto !== "undefined" && "randomUUID" in crypto) return crypto.randomUUID();
@@ -771,6 +931,45 @@ var FallbackStorageAdapter = class {
771
931
  this.migrateFallbackOnEmpty = options.migrateFallbackOnEmpty ?? false;
772
932
  this.mirrorWrites = options.mirrorWrites ?? false;
773
933
  this.storageKey = options.fallback.storageKey ?? options.primary.storageKey;
934
+ if (this.primary.getCollections && this.primary.setCollection && this.primary.removeCollection && this.fallback.getCollections && this.fallback.setCollection && this.fallback.removeCollection) {
935
+ const fallbackGetCollections = this.fallback.getCollections.bind(this.fallback);
936
+ this.getCollections = async () => {
937
+ const collections = await this.execute((adapter) => {
938
+ if (!adapter.getCollections) throw new Error("Collection storage is unavailable.");
939
+ return adapter.getCollections();
940
+ });
941
+ if (this.active === "primary" && this.migrateFallbackOnEmpty && collections.length === 0) {
942
+ const fallbackCollections = await fallbackGetCollections();
943
+ for (const collection of fallbackCollections) await this.setCollection?.(collection);
944
+ return fallbackCollections;
945
+ }
946
+ return collections;
947
+ };
948
+ this.setCollection = (collection) => this.executeWrite((adapter) => {
949
+ if (!adapter.setCollection) throw new Error("Collection storage is unavailable.");
950
+ return adapter.setCollection(collection);
951
+ });
952
+ this.removeCollection = (id, scope) => this.executeWrite((adapter) => {
953
+ if (!adapter.removeCollection) throw new Error("Collection storage is unavailable.");
954
+ return adapter.removeCollection(id, scope);
955
+ });
956
+ }
957
+ if (this.primary.getCollectionMemberships && this.primary.setCollectionMembership && this.primary.removeCollectionMembership && this.fallback.getCollectionMemberships && this.fallback.setCollectionMembership && this.fallback.removeCollectionMembership) {
958
+ const fallbackGetMemberships = this.fallback.getCollectionMemberships.bind(this.fallback);
959
+ this.getCollectionMemberships = async () => {
960
+ const memberships = await this.execute(
961
+ (adapter) => adapter.getCollectionMemberships?.() ?? Promise.resolve([])
962
+ );
963
+ if (this.active !== "primary" || !this.migrateFallbackOnEmpty || memberships.length > 0) return memberships;
964
+ const fallbackMemberships = await fallbackGetMemberships();
965
+ for (const membership of fallbackMemberships) await this.setCollectionMembership?.(membership);
966
+ return fallbackMemberships;
967
+ };
968
+ this.setCollectionMembership = (membership) => this.executeWrite((adapter) => adapter.setCollectionMembership?.(membership) ?? Promise.resolve());
969
+ this.removeCollectionMembership = (collectionId, itemId, scope) => this.executeWrite(
970
+ (adapter) => adapter.removeCollectionMembership?.(collectionId, itemId, scope) ?? Promise.resolve()
971
+ );
972
+ }
774
973
  }
775
974
  get isUsingFallback() {
776
975
  return this.active === "fallback";
@@ -895,8 +1094,24 @@ function createStorageAdapter(options) {
895
1094
  const subscribe = options.subscribe;
896
1095
  const setMany = options.setMany;
897
1096
  const removeMany = options.removeMany;
1097
+ const getCollections = options.getCollections;
1098
+ const setCollection = options.setCollection;
1099
+ const removeCollection = options.removeCollection;
1100
+ const getCollectionMemberships = options.getCollectionMemberships;
1101
+ const setCollectionMembership = options.setCollectionMembership;
1102
+ const removeCollectionMembership = options.removeCollectionMembership;
898
1103
  return {
899
1104
  getAll: async () => options.getAll(),
1105
+ ...getCollections && setCollection && removeCollection ? {
1106
+ getCollections: async () => getCollections(),
1107
+ setCollection: async (collection) => setCollection(collection),
1108
+ removeCollection: async (id, scope) => removeCollection(id, scope)
1109
+ } : {},
1110
+ ...getCollectionMemberships && setCollectionMembership && removeCollectionMembership ? {
1111
+ getCollectionMemberships: async () => getCollectionMemberships(),
1112
+ setCollectionMembership: async (membership) => setCollectionMembership(membership),
1113
+ removeCollectionMembership: async (collectionId, itemId, scope) => removeCollectionMembership(collectionId, itemId, scope)
1114
+ } : {},
900
1115
  set: async (item) => options.set(item),
901
1116
  ...setMany ? { setMany: async (items) => setMany(items) } : {},
902
1117
  remove: async (id) => options.remove(id),
@@ -945,6 +1160,86 @@ var LocalStorageAdapter = class {
945
1160
  });
946
1161
  }
947
1162
  }
1163
+ async getCollections() {
1164
+ if (!this.storage) return [];
1165
+ const storageKey = `${this.storageKey}:collections`;
1166
+ let raw;
1167
+ try {
1168
+ raw = this.storage.getItem(storageKey);
1169
+ } catch (cause) {
1170
+ throw new KeepStorageAccessError({ operation: "getAll", storageKey, cause });
1171
+ }
1172
+ if (!raw) return [];
1173
+ try {
1174
+ const value = JSON.parse(raw);
1175
+ if (!Array.isArray(value) || !value.every(isKeepCollectionDefinition)) {
1176
+ throw new KeepStorageParseError({ operation: "getAll", storageKey });
1177
+ }
1178
+ return value;
1179
+ } catch (cause) {
1180
+ if (cause instanceof KeepStorageParseError) throw cause;
1181
+ throw new KeepStorageParseError({ operation: "getAll", storageKey, cause });
1182
+ }
1183
+ }
1184
+ async setCollection(collection) {
1185
+ if (!isKeepCollectionDefinition(collection)) throw new Error("Collection id and name are required.");
1186
+ const current = await this.getCollections();
1187
+ await this.writeCollections(
1188
+ [
1189
+ ...current.filter((entry) => entry.id !== collection.id || !isSameKeepScope(entry.scope, collection.scope)),
1190
+ collection
1191
+ ],
1192
+ "set"
1193
+ );
1194
+ }
1195
+ async removeCollection(id, scope) {
1196
+ const current = await this.getCollections();
1197
+ await this.writeCollections(
1198
+ current.filter((entry) => entry.id !== id || !isSameKeepScope(entry.scope, scope)),
1199
+ "remove"
1200
+ );
1201
+ const memberships = await this.getCollectionMemberships();
1202
+ await this.writeMemberships(
1203
+ memberships.filter((membership) => membership.collectionId !== id || !isSameKeepScope(membership.scope, scope)),
1204
+ "remove"
1205
+ );
1206
+ }
1207
+ async getCollectionMemberships() {
1208
+ if (!this.storage) return [];
1209
+ const storageKey = `${this.storageKey}:memberships`;
1210
+ let raw;
1211
+ try {
1212
+ raw = this.storage.getItem(storageKey);
1213
+ } catch (cause) {
1214
+ throw new KeepStorageAccessError({ operation: "getAll", storageKey, cause });
1215
+ }
1216
+ if (!raw) return [];
1217
+ try {
1218
+ const value = JSON.parse(raw);
1219
+ if (!Array.isArray(value) || !value.every(isKeepCollectionMembership)) {
1220
+ throw new KeepStorageParseError({ operation: "getAll", storageKey });
1221
+ }
1222
+ return value;
1223
+ } catch (cause) {
1224
+ if (cause instanceof KeepStorageParseError) throw cause;
1225
+ throw new KeepStorageParseError({ operation: "getAll", storageKey, cause });
1226
+ }
1227
+ }
1228
+ async setCollectionMembership(membership) {
1229
+ if (!isKeepCollectionMembership(membership)) throw new Error("Collection membership is invalid.");
1230
+ const current = await this.getCollectionMemberships();
1231
+ const sameMembership = (entry) => entry.collectionId === membership.collectionId && entry.itemId === membership.itemId && isSameKeepScope(entry.scope, membership.scope);
1232
+ await this.writeMemberships([...current.filter((entry) => !sameMembership(entry)), membership], "set");
1233
+ }
1234
+ async removeCollectionMembership(collectionId, itemId, scope) {
1235
+ const current = await this.getCollectionMemberships();
1236
+ await this.writeMemberships(
1237
+ current.filter(
1238
+ (entry) => entry.collectionId !== collectionId || entry.itemId !== itemId || !isSameKeepScope(entry.scope, scope)
1239
+ ),
1240
+ "remove"
1241
+ );
1242
+ }
948
1243
  async set(item) {
949
1244
  await this.setMany([item]);
950
1245
  }
@@ -985,7 +1280,8 @@ var LocalStorageAdapter = class {
985
1280
  subscribe(listener) {
986
1281
  if (typeof window === "undefined") return () => void 0;
987
1282
  const handleStorage = (event) => {
988
- if (event.key !== null && event.key !== this.storageKey) return;
1283
+ if (event.key !== null && event.key !== this.storageKey && event.key !== `${this.storageKey}:collections` && event.key !== `${this.storageKey}:memberships`)
1284
+ return;
989
1285
  listener();
990
1286
  };
991
1287
  window.addEventListener("storage", handleStorage);
@@ -1010,6 +1306,26 @@ var LocalStorageAdapter = class {
1010
1306
  });
1011
1307
  }
1012
1308
  }
1309
+ async writeCollections(collections, operation) {
1310
+ if (!this.storage) return;
1311
+ const storageKey = `${this.storageKey}:collections`;
1312
+ try {
1313
+ this.storage.setItem(storageKey, JSON.stringify(collections));
1314
+ } catch (cause) {
1315
+ if (isQuotaExceededError(cause)) throw new KeepStorageQuotaError({ operation, storageKey, cause });
1316
+ throw new KeepStorageAccessError({ operation, storageKey, cause });
1317
+ }
1318
+ }
1319
+ async writeMemberships(memberships, operation) {
1320
+ if (!this.storage) return;
1321
+ const storageKey = `${this.storageKey}:memberships`;
1322
+ try {
1323
+ this.storage.setItem(storageKey, JSON.stringify(memberships));
1324
+ } catch (cause) {
1325
+ if (isQuotaExceededError(cause)) throw new KeepStorageQuotaError({ operation, storageKey, cause });
1326
+ throw new KeepStorageAccessError({ operation, storageKey, cause });
1327
+ }
1328
+ }
1013
1329
  };
1014
1330
  var IndexedDBAdapter = class {
1015
1331
  constructor(options = {}) {
@@ -1034,6 +1350,64 @@ var IndexedDBAdapter = class {
1034
1350
  throw new KeepStorageAccessError({ operation: "getAll", storageKey: this.storageKey, cause });
1035
1351
  }
1036
1352
  }
1353
+ async getCollections() {
1354
+ const database = await this.openCollections("getAll");
1355
+ if (!database) return [];
1356
+ const storageKey = `${this.storageKey}:collections`;
1357
+ try {
1358
+ const transaction = database.transaction("collections", "readonly");
1359
+ const value = await requestToPromise2(transaction.objectStore("collections").getAll());
1360
+ if (!Array.isArray(value) || !value.every((entry) => isRecord(entry) && typeof entry.key === "string" && isKeepCollectionDefinition(entry))) {
1361
+ throw new KeepStorageParseError({ operation: "getAll", storageKey });
1362
+ }
1363
+ return value.map(({ key: _key, ...collection }) => collection);
1364
+ } catch (cause) {
1365
+ if (cause instanceof KeepStorageParseError) throw cause;
1366
+ throw new KeepStorageAccessError({ operation: "getAll", storageKey, cause });
1367
+ }
1368
+ }
1369
+ async setCollection(collection) {
1370
+ if (!isKeepCollectionDefinition(collection)) throw new Error("Collection id and name are required.");
1371
+ await this.writeCollection(
1372
+ "set",
1373
+ (store) => store.put({ ...collection, key: collectionKey(collection.id, collection.scope) })
1374
+ );
1375
+ }
1376
+ async removeCollection(id, scope) {
1377
+ await this.writeCollection("remove", (store) => store.delete(collectionKey(id, scope)));
1378
+ const memberships = await this.getCollectionMemberships();
1379
+ for (const membership of memberships) {
1380
+ if (membership.collectionId === id && isSameKeepScope(membership.scope, scope)) {
1381
+ await this.removeCollectionMembership(id, membership.itemId, scope);
1382
+ }
1383
+ }
1384
+ }
1385
+ async getCollectionMemberships() {
1386
+ const database = await this.openCollections("getAll");
1387
+ if (!database) return [];
1388
+ const storageKey = `${this.storageKey}:memberships`;
1389
+ try {
1390
+ const transaction = database.transaction("memberships", "readonly");
1391
+ const value = await requestToPromise2(transaction.objectStore("memberships").getAll());
1392
+ if (!Array.isArray(value) || !value.every((entry) => isRecord(entry) && typeof entry.key === "string" && isKeepCollectionMembership(entry))) {
1393
+ throw new KeepStorageParseError({ operation: "getAll", storageKey });
1394
+ }
1395
+ return value.map(({ key: _key, ...membership }) => membership);
1396
+ } catch (cause) {
1397
+ if (cause instanceof KeepStorageParseError) throw cause;
1398
+ throw new KeepStorageAccessError({ operation: "getAll", storageKey, cause });
1399
+ }
1400
+ }
1401
+ async setCollectionMembership(membership) {
1402
+ if (!isKeepCollectionMembership(membership)) throw new Error("Collection membership is invalid.");
1403
+ await this.writeCollectionMembership(
1404
+ "set",
1405
+ (store) => store.put({ ...membership, key: membershipKey(membership.collectionId, membership.itemId, membership.scope) })
1406
+ );
1407
+ }
1408
+ async removeCollectionMembership(collectionId, itemId, scope) {
1409
+ await this.writeCollectionMembership("remove", (store) => store.delete(membershipKey(collectionId, itemId, scope)));
1410
+ }
1037
1411
  async set(item) {
1038
1412
  return this.setMany([item]);
1039
1413
  }
@@ -1103,6 +1477,64 @@ var IndexedDBAdapter = class {
1103
1477
  channel.postMessage({ type: "keepkit:changed" });
1104
1478
  channel.close();
1105
1479
  }
1480
+ async writeCollection(operation, write) {
1481
+ const database = await this.openCollections(operation);
1482
+ if (!database) return;
1483
+ const storageKey = `${this.storageKey}:collections`;
1484
+ try {
1485
+ const transaction = database.transaction("collections", "readwrite");
1486
+ write(transaction.objectStore("collections"));
1487
+ await transactionToPromise2(transaction);
1488
+ this.notifySubscribers();
1489
+ } catch (cause) {
1490
+ if (isQuotaExceededError(cause)) throw new KeepStorageQuotaError({ operation, storageKey, cause });
1491
+ throw new KeepStorageAccessError({ operation, storageKey, cause });
1492
+ }
1493
+ }
1494
+ async writeCollectionMembership(operation, write) {
1495
+ const database = await this.openCollections(operation);
1496
+ if (!database) return;
1497
+ const storageKey = `${this.storageKey}:memberships`;
1498
+ try {
1499
+ const transaction = database.transaction("memberships", "readwrite");
1500
+ write(transaction.objectStore("memberships"));
1501
+ await transactionToPromise2(transaction);
1502
+ this.notifySubscribers();
1503
+ } catch (cause) {
1504
+ if (isQuotaExceededError(cause)) throw new KeepStorageQuotaError({ operation, storageKey, cause });
1505
+ throw new KeepStorageAccessError({ operation, storageKey, cause });
1506
+ }
1507
+ }
1508
+ openCollections(operation) {
1509
+ if (!this.indexedDB) return Promise.resolve(void 0);
1510
+ const storageKey = `${this.storageKey}:collections`;
1511
+ if (!this.collectionDatabasePromise) {
1512
+ this.collectionDatabasePromise = new Promise((resolve, reject) => {
1513
+ let request;
1514
+ try {
1515
+ request = this.indexedDB?.open(storageKey, 2);
1516
+ } catch (cause) {
1517
+ reject(cause);
1518
+ return;
1519
+ }
1520
+ request.onupgradeneeded = () => {
1521
+ if (!request.result.objectStoreNames.contains("collections")) {
1522
+ request.result.createObjectStore("collections", { keyPath: "key" });
1523
+ }
1524
+ if (!request.result.objectStoreNames.contains("memberships")) {
1525
+ request.result.createObjectStore("memberships", { keyPath: "key" });
1526
+ }
1527
+ };
1528
+ request.onsuccess = () => resolve(request.result);
1529
+ request.onerror = () => reject(request.error);
1530
+ request.onblocked = () => reject(request.error ?? new Error("IndexedDB collection storage open was blocked."));
1531
+ });
1532
+ }
1533
+ return this.collectionDatabasePromise.catch((cause) => {
1534
+ this.collectionDatabasePromise = void 0;
1535
+ throw new KeepStorageAccessError({ operation, storageKey, cause });
1536
+ });
1537
+ }
1106
1538
  open(operation) {
1107
1539
  if (!this.indexedDB) return Promise.resolve(void 0);
1108
1540
  if (!this.databasePromise) {
@@ -1131,6 +1563,12 @@ var IndexedDBAdapter = class {
1131
1563
  });
1132
1564
  }
1133
1565
  };
1566
+ function collectionKey(id, scope) {
1567
+ return JSON.stringify([scope?.tenantId ?? null, scope?.userId ?? null, id]);
1568
+ }
1569
+ function membershipKey(collectionId, itemId, scope) {
1570
+ return JSON.stringify([scope?.tenantId ?? null, scope?.userId ?? null, collectionId, itemId]);
1571
+ }
1134
1572
  function getBrowserStorage2() {
1135
1573
  if (typeof window === "undefined") return void 0;
1136
1574
  try {
@@ -1176,6 +1614,8 @@ export {
1176
1614
  removeKeepItems,
1177
1615
  mergeKeepItemLists,
1178
1616
  isKeepItem,
1617
+ isKeepCollectionDefinition,
1618
+ isKeepCollectionMembership,
1179
1619
  isRecord,
1180
1620
  getKeepScopeKey,
1181
1621
  isSameKeepScope,
@@ -1198,4 +1638,4 @@ export {
1198
1638
  LocalStorageAdapter,
1199
1639
  IndexedDBAdapter
1200
1640
  };
1201
- //# sourceMappingURL=chunk-MZFZQIM5.js.map
1641
+ //# sourceMappingURL=chunk-NZLAW4LY.js.map