@keepkit/core 0.28.4 → 0.28.6

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.
@@ -82,7 +82,10 @@ function isKeepItemArray(value) {
82
82
  return Array.isArray(value) && value.every(isKeepItem);
83
83
  }
84
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));
85
+ return isRecord(value) && typeof value.id === "string" && value.id.trim().length > 0 && typeof value.name === "string" && value.name.trim().length > 0 && (value.revision === void 0 || typeof value.revision === "string") && (value.updatedAt === void 0 || typeof value.updatedAt === "number" && Number.isFinite(value.updatedAt)) && (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));
86
89
  }
87
90
  function isRecord(value) {
88
91
  return typeof value === "object" && value !== null;
@@ -116,6 +119,17 @@ var ScopedStorageAdapter = class {
116
119
  this.setCollection = (collection) => setCollection({ ...collection, ...this.scope ? { scope: this.scope } : {} });
117
120
  this.removeCollection = (id) => removeCollection(id, this.scope);
118
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
+ }
119
133
  }
120
134
  async getAll() {
121
135
  const items = await this.base.getAll();
@@ -129,29 +143,37 @@ var ScopedStorageAdapter = class {
129
143
  const scoped = items.map((item) => ({ ...item, ...this.scope ? { scope: this.scope } : {} }));
130
144
  const ids = new Set(scoped.map((item) => item.id));
131
145
  const next = current.filter((item) => !ids.has(item.id) || !isSameKeepScope(item.scope, this.scope));
132
- await writeAll(this.base, [...next, ...scoped]);
146
+ await writeAll(this.base, [...next, ...scoped], this.scope);
133
147
  }
134
148
  async remove(id) {
135
149
  const current = await this.base.getAll();
136
150
  await writeAll(
137
151
  this.base,
138
- current.filter((item) => item.id !== id || !isSameKeepScope(item.scope, this.scope))
152
+ current.filter((item) => item.id !== id || !isSameKeepScope(item.scope, this.scope)),
153
+ this.scope
139
154
  );
155
+ await this.removeMembershipsForItems([id]);
140
156
  }
141
157
  async removeMany(ids) {
142
158
  const idSet = new Set(ids);
143
159
  const current = await this.base.getAll();
144
160
  await writeAll(
145
161
  this.base,
146
- current.filter((item) => !idSet.has(item.id) || !isSameKeepScope(item.scope, this.scope))
162
+ current.filter((item) => !idSet.has(item.id) || !isSameKeepScope(item.scope, this.scope)),
163
+ this.scope
147
164
  );
165
+ await this.removeMembershipsForItems([...idSet]);
148
166
  }
149
167
  async clear() {
150
168
  const current = await this.base.getAll();
169
+ const removedIds = current.filter((item) => isSameKeepScope(item.scope, this.scope)).map((item) => item.id);
151
170
  await writeAll(
152
171
  this.base,
153
- current.filter((item) => !isSameKeepScope(item.scope, this.scope))
172
+ current.filter((item) => !isSameKeepScope(item.scope, this.scope)),
173
+ this.scope
154
174
  );
175
+ if (removedIds.length > 0) await this.removeMembershipsForItems(removedIds);
176
+ await this.clearMembershipsForScope();
155
177
  }
156
178
  async merge(items) {
157
179
  await this.setMany(items);
@@ -160,6 +182,25 @@ var ScopedStorageAdapter = class {
160
182
  subscribe(listener) {
161
183
  return this.base.subscribe?.(listener) ?? (() => void 0);
162
184
  }
185
+ async removeMembershipsForItems(ids) {
186
+ if (!this.base.getCollectionMemberships || !this.base.removeCollectionMembership || ids.length === 0) return;
187
+ const removedIds = new Set(ids);
188
+ const memberships = await this.base.getCollectionMemberships();
189
+ for (const membership of memberships) {
190
+ if (removedIds.has(membership.itemId) && isSameKeepScope(membership.scope, this.scope)) {
191
+ await this.base.removeCollectionMembership(membership.collectionId, membership.itemId, this.scope);
192
+ }
193
+ }
194
+ }
195
+ async clearMembershipsForScope() {
196
+ if (!this.base.getCollectionMemberships || !this.base.removeCollectionMembership) return;
197
+ const memberships = await this.base.getCollectionMemberships();
198
+ for (const membership of memberships) {
199
+ if (isSameKeepScope(membership.scope, this.scope)) {
200
+ await this.base.removeCollectionMembership(membership.collectionId, membership.itemId, this.scope);
201
+ }
202
+ }
203
+ }
163
204
  };
164
205
  function createScopedStorageAdapter(base, scope) {
165
206
  return new ScopedStorageAdapter(base, scope);
@@ -179,32 +220,38 @@ var ScopedSyncQueueAdapter = class {
179
220
  const ids = new Set(scoped.map((operation) => operation.operationId));
180
221
  await this.base.setMany([
181
222
  ...current.filter(
182
- (operation) => !ids.has(operation.operationId) && !isSameKeepScope(operation.scope, this.scope)
223
+ (operation) => !ids.has(operation.operationId) || !isSameKeepScope(operation.scope, this.scope)
183
224
  ),
184
225
  ...scoped
185
226
  ]);
186
227
  }
187
- remove(operationIds) {
188
- return this.base.remove(operationIds);
228
+ async remove(operationIds) {
229
+ const ids = new Set(operationIds);
230
+ const scopedIds = (await this.base.getAll()).filter((operation) => ids.has(operation.operationId) && isSameKeepScope(operation.scope, this.scope)).map((operation) => operation.operationId);
231
+ await this.base.remove(scopedIds);
189
232
  }
190
- clear() {
191
- return this.base.getAll().then(
192
- (operations) => this.base.setMany(operations.filter((operation) => !isSameKeepScope(operation.scope, this.scope)))
193
- );
233
+ async clear() {
234
+ const operationIds = (await this.base.getAll()).filter((operation) => isSameKeepScope(operation.scope, this.scope)).map((operation) => operation.operationId);
235
+ await this.base.remove(operationIds);
194
236
  }
195
237
  };
196
- async function writeAll(base, items) {
238
+ async function writeAll(base, items, scope) {
239
+ const existing = await base.getAll();
240
+ const desiredItems = new Set(items.filter((item) => isSameKeepScope(item.scope, scope)).map(itemStorageKey));
241
+ const removedIds = existing.filter((item) => isSameKeepScope(item.scope, scope) && !desiredItems.has(itemStorageKey(item))).map((item) => item.id);
242
+ if (removedIds.length > 0) {
243
+ if (base.removeMany) await base.removeMany(removedIds, scope);
244
+ else await Promise.all(removedIds.map((id) => base.remove(id, scope)));
245
+ }
197
246
  if (base.setMany) {
198
247
  await base.setMany(items);
199
248
  return;
200
249
  }
201
- const existing = await base.getAll();
202
- const ids = new Set(items.map((item) => item.id));
203
- for (const item of existing) {
204
- if (!ids.has(item.id)) await base.remove(item.id);
205
- }
206
250
  for (const item of items) await base.set(item);
207
251
  }
252
+ function itemStorageKey(item) {
253
+ return JSON.stringify([item.scope?.tenantId ?? null, item.scope?.userId ?? null, item.id]);
254
+ }
208
255
 
209
256
  // src/storage/sync.ts
210
257
  var DEFAULT_SYNC_QUEUE_KEY = "keepkit:sync-queue";
@@ -225,7 +272,7 @@ var LocalStorageSyncQueueAdapter = class {
225
272
  } catch (cause) {
226
273
  throw Object.assign(new Error("KeepKit sync queue contains invalid JSON."), { cause });
227
274
  }
228
- if (!Array.isArray(value) || !value.every(isSyncOperation)) {
275
+ if (!Array.isArray(value) || !value.every(isSyncQueueOperation)) {
229
276
  throw new Error("KeepKit sync queue contains invalid operations.");
230
277
  }
231
278
  return value;
@@ -255,7 +302,7 @@ var IndexedDBSyncQueueAdapter = class {
255
302
  if (!database) return [];
256
303
  const transaction = database.transaction(this.storeName, "readonly");
257
304
  const value = await requestToPromise(transaction.objectStore(this.storeName).getAll());
258
- if (!Array.isArray(value) || !value.every(isSyncOperation)) {
305
+ if (!Array.isArray(value) || !value.every(isSyncQueueOperation)) {
259
306
  throw new Error("KeepKit sync queue contains invalid operations.");
260
307
  }
261
308
  return value;
@@ -367,11 +414,12 @@ var SyncStorageAdapter = class {
367
414
  };
368
415
  this.local = options.local;
369
416
  this.remote = options.remote;
370
- this.queue = options.queue ?? createDefaultQueue(options);
417
+ this.scope = getSyncScope(options);
418
+ const queue = options.queue ?? createDefaultQueue(options);
419
+ this.queue = this.scope ? new ScopedSyncQueueAdapter(queue, this.scope) : queue;
371
420
  this.clientId = options.clientId ?? createId();
372
421
  this.now = options.now ?? Date.now;
373
422
  this.resolveConflict = options.resolveConflict;
374
- this.scope = getSyncScope(options);
375
423
  this.maxRetries = Math.max(0, options.maxRetries ?? 3);
376
424
  this.retryDelayMs = Math.max(0, options.retryDelayMs ?? 0);
377
425
  this.retryBackoff = Math.max(1, options.retryBackoff ?? 2);
@@ -385,8 +433,93 @@ var SyncStorageAdapter = class {
385
433
  const scope = this.scope;
386
434
  return scope ? collections.filter((collection) => sameScope(collection.scope, scope)) : collections;
387
435
  };
388
- this.setCollection = (collection) => setCollection({ ...collection, ...this.scope ? { scope: this.scope } : {} });
389
- this.removeCollection = (id) => removeCollection(id, this.scope);
436
+ this.setCollection = async (collection) => {
437
+ const scopedCollection = { ...collection, ...this.scope ? { scope: this.scope } : {} };
438
+ if (this.remote.pushCollection) {
439
+ const current = (await this.getCollections?.())?.find((entry) => entry.id === scopedCollection.id);
440
+ const operation = this.createCollectionOperation(
441
+ "upsert",
442
+ scopedCollection.id,
443
+ scopedCollection,
444
+ current?.revision
445
+ );
446
+ await this.enqueueBeforeLocalWrite(operation);
447
+ try {
448
+ await setCollection(scopedCollection);
449
+ } catch (cause) {
450
+ await this.removeQueued(operation.operationId);
451
+ throw cause;
452
+ }
453
+ this.notifyDataListeners();
454
+ this.setPendingState();
455
+ return;
456
+ }
457
+ await setCollection(scopedCollection);
458
+ this.notifyDataListeners();
459
+ };
460
+ this.removeCollection = async (id) => {
461
+ const memberships = await this.getCollectionMemberships?.() ?? [];
462
+ const membershipOperations = this.remote.pushMembership ? memberships.filter((membership) => membership.collectionId === id).map((membership) => this.createMembershipOperation("remove", membership.collectionId, membership.itemId)) : [];
463
+ if (this.remote.pushCollection) {
464
+ const current = (await this.getCollections?.())?.find((collection) => collection.id === id);
465
+ const operation = this.createCollectionOperation("remove", id, void 0, current?.revision);
466
+ await this.enqueueManyBeforeLocalWrite([...membershipOperations, operation]);
467
+ try {
468
+ await removeCollection(id, this.scope);
469
+ } catch (cause) {
470
+ await this.removeQueued([operation.operationId, ...membershipOperations.map((entry) => entry.operationId)]);
471
+ throw cause;
472
+ }
473
+ this.notifyDataListeners();
474
+ this.setPendingState();
475
+ return;
476
+ }
477
+ await removeCollection(id, this.scope);
478
+ this.notifyDataListeners();
479
+ };
480
+ }
481
+ if (this.local.getCollectionMemberships && this.local.setCollectionMembership && this.local.removeCollectionMembership) {
482
+ const getMemberships = this.local.getCollectionMemberships.bind(this.local);
483
+ const setMembership = this.local.setCollectionMembership.bind(this.local);
484
+ const removeMembership = this.local.removeCollectionMembership.bind(this.local);
485
+ this.getCollectionMemberships = async () => {
486
+ const memberships = await getMemberships();
487
+ const scope = this.scope;
488
+ return scope ? memberships.filter((membership) => sameScope(membership.scope, scope)) : memberships;
489
+ };
490
+ this.setCollectionMembership = async (membership) => {
491
+ const scoped = { ...membership, ...this.scope ? { scope: this.scope } : {} };
492
+ if (this.remote.pushMembership) {
493
+ const operation = this.createMembershipOperation("upsert", scoped.collectionId, scoped.itemId, scoped);
494
+ await this.enqueueBeforeLocalWrite(operation);
495
+ try {
496
+ await setMembership(scoped);
497
+ } catch (cause) {
498
+ await this.removeQueued(operation.operationId);
499
+ throw cause;
500
+ }
501
+ this.setPendingState();
502
+ } else {
503
+ await setMembership(scoped);
504
+ }
505
+ this.notifyDataListeners();
506
+ };
507
+ this.removeCollectionMembership = async (collectionId, itemId) => {
508
+ if (this.remote.pushMembership) {
509
+ const operation = this.createMembershipOperation("remove", collectionId, itemId);
510
+ await this.enqueueBeforeLocalWrite(operation);
511
+ try {
512
+ await removeMembership(collectionId, itemId, this.scope);
513
+ } catch (cause) {
514
+ await this.removeQueued(operation.operationId);
515
+ throw cause;
516
+ }
517
+ this.setPendingState();
518
+ } else {
519
+ await removeMembership(collectionId, itemId, this.scope);
520
+ }
521
+ this.notifyDataListeners();
522
+ };
390
523
  }
391
524
  if (typeof window !== "undefined") {
392
525
  this.onlineHandler = () => void this.flushSync();
@@ -428,11 +561,13 @@ var SyncStorageAdapter = class {
428
561
  }
429
562
  async remove(id) {
430
563
  const operation = this.createOperation("remove", id);
431
- await this.enqueueBeforeLocalWrite(operation);
564
+ const memberships = await this.getCollectionMemberships?.() ?? [];
565
+ const membershipOperations = this.remote.pushMembership ? memberships.filter((membership) => membership.itemId === id).map((membership) => this.createMembershipOperation("remove", membership.collectionId, membership.itemId)) : [];
566
+ await this.enqueueManyBeforeLocalWrite([...membershipOperations, operation]);
432
567
  try {
433
- await this.local.remove(id);
568
+ await this.local.remove(id, this.scope);
434
569
  } catch (cause) {
435
- await this.removeQueued(operation.operationId);
570
+ await this.removeQueued([operation.operationId, ...membershipOperations.map((entry) => entry.operationId)]);
436
571
  throw cause;
437
572
  }
438
573
  this.notifyDataListeners();
@@ -440,11 +575,15 @@ var SyncStorageAdapter = class {
440
575
  }
441
576
  async removeMany(ids) {
442
577
  const operations = [...new Set(ids)].map((id) => this.createOperation("remove", id));
443
- await this.enqueueManyBeforeLocalWrite(operations);
578
+ const idSet = new Set(ids);
579
+ const memberships = await this.getCollectionMemberships?.() ?? [];
580
+ const membershipOperations = this.remote.pushMembership ? memberships.filter((membership) => idSet.has(membership.itemId)).map((membership) => this.createMembershipOperation("remove", membership.collectionId, membership.itemId)) : [];
581
+ await this.enqueueManyBeforeLocalWrite([...membershipOperations, ...operations]);
444
582
  try {
445
- await removeKeepItems(this.local, ids);
583
+ if (this.local.removeMany) await this.local.removeMany(ids, this.scope);
584
+ else await Promise.all(ids.map((id) => this.local.remove(id, this.scope)));
446
585
  } catch (cause) {
447
- await this.removeQueued(operations.map((operation) => operation.operationId));
586
+ await this.removeQueued([...operations, ...membershipOperations].map((operation) => operation.operationId));
448
587
  throw cause;
449
588
  }
450
589
  this.notifyDataListeners();
@@ -453,9 +592,17 @@ var SyncStorageAdapter = class {
453
592
  async clear() {
454
593
  const items = await this.getAll();
455
594
  await this.removeMany(items.map((item) => item.id));
456
- await this.local.clear();
595
+ if (!this.scope) await this.local.clear();
457
596
  }
458
597
  async merge(localItems) {
598
+ if (this.scope) {
599
+ const merged2 = mergeKeepItemLists(
600
+ await this.getAll(),
601
+ localItems.map((item) => this.applyScope(item))
602
+ );
603
+ await this.setMany(localItems);
604
+ return merged2;
605
+ }
459
606
  const merged = this.local.merge ? await this.local.merge(localItems) : mergeKeepItemLists(await this.local.getAll(), localItems);
460
607
  if (!this.local.merge) await persistKeepItems(this.local, merged);
461
608
  await this.setMany(localItems);
@@ -504,6 +651,47 @@ var SyncStorageAdapter = class {
504
651
  });
505
652
  if (resolution !== "remote" && this.queueItems.length > 0) await this.flushSync();
506
653
  }
654
+ async resolveCollectionSyncConflict(id, resolution, collection) {
655
+ await this.loadQueue();
656
+ const conflict = (this.state.collectionConflicts ?? []).find((entry) => entry.operation.id === id);
657
+ if (!conflict) return;
658
+ if (!this.local.getCollections || !this.local.setCollection || !this.local.removeCollection) {
659
+ throw new Error("KeepKit collection storage is unavailable.");
660
+ }
661
+ if (resolution === "remote") {
662
+ if (conflict.deleted) await this.local.removeCollection(id, this.scope);
663
+ else {
664
+ if (!conflict.collection) throw new Error("Remote collection conflict is missing its selected definition.");
665
+ await this.local.setCollection({
666
+ ...conflict.collection,
667
+ ...conflict.revision ? { revision: conflict.revision } : {},
668
+ ...this.scope ? { scope: this.scope } : {}
669
+ });
670
+ }
671
+ await this.removeQueued(conflict.operation.operationId);
672
+ this.updateState({
673
+ collectionConflicts: (this.state.collectionConflicts ?? []).filter((entry) => entry.operation.id !== id)
674
+ });
675
+ return;
676
+ }
677
+ if (collection && collection.id !== id) {
678
+ throw new TypeError("KeepKit collection conflict resolution must use the requested collection ID.");
679
+ }
680
+ const chosen = collection ?? (await this.getCollections?.())?.find((entry) => entry.id === id);
681
+ const retry = {
682
+ ...conflict.operation,
683
+ operationId: `${this.clientId}:${this.now()}:${createId()}`,
684
+ ...conflict.operation.type === "upsert" && chosen ? { collection: { ...chosen, ...this.scope ? { scope: this.scope } : {} } } : {},
685
+ createdAt: this.now(),
686
+ baseRevision: conflict.revision,
687
+ attempts: 0
688
+ };
689
+ await this.replaceQueued(conflict.operation, retry);
690
+ this.updateState({
691
+ collectionConflicts: (this.state.collectionConflicts ?? []).filter((entry) => entry.operation.id !== id)
692
+ });
693
+ await this.flushSync();
694
+ }
507
695
  dispose() {
508
696
  this.disposed = true;
509
697
  if (this.onlineHandler) window.removeEventListener("online", this.onlineHandler);
@@ -522,6 +710,38 @@ var SyncStorageAdapter = class {
522
710
  for (const operation of [...this.queueItems]) {
523
711
  if (this.disposed) return;
524
712
  try {
713
+ if (isCollectionSyncOperation(operation)) {
714
+ if (!this.remote.pushCollection) throw new Error("Remote sync driver does not support collection sync.");
715
+ const result2 = await this.pushCollectionWithRetry(operation);
716
+ if (result2?.type === "conflict" && result2.resolution === "manual") {
717
+ const conflict = createCollectionSyncConflict(operation, result2);
718
+ this.updateState({
719
+ status: "conflict",
720
+ collectionConflicts: [
721
+ ...(this.state.collectionConflicts ?? []).filter((entry) => entry.operation.id !== operation.id),
722
+ conflict
723
+ ]
724
+ });
725
+ continue;
726
+ }
727
+ await this.applyCollectionPushResult(operation, result2);
728
+ await this.removeQueued(operation.operationId);
729
+ this.updateState({
730
+ status: this.queueItems.length > 0 ? "syncing" : "synced",
731
+ lastSyncedAt: this.now()
732
+ });
733
+ continue;
734
+ }
735
+ if (isMembershipSyncOperation(operation)) {
736
+ if (!this.remote.pushMembership) throw new Error("Remote sync driver does not support membership sync.");
737
+ await this.pushMembershipWithRetry(operation);
738
+ await this.removeQueued(operation.operationId);
739
+ this.updateState({
740
+ status: this.queueItems.length > 0 ? "syncing" : "synced",
741
+ lastSyncedAt: this.now()
742
+ });
743
+ continue;
744
+ }
525
745
  const result = await this.pushWithRetry(operation);
526
746
  if (result.type === "conflict") {
527
747
  const local = operation.item;
@@ -586,6 +806,31 @@ var SyncStorageAdapter = class {
586
806
  ...this.scope ? { scope: this.scope } : {}
587
807
  };
588
808
  }
809
+ createCollectionOperation(type, id, collection, baseRevision) {
810
+ return {
811
+ operationId: `${this.clientId}:${this.now()}:${createId()}`,
812
+ type,
813
+ entity: "collection",
814
+ id,
815
+ ...collection ? { collection } : {},
816
+ createdAt: this.now(),
817
+ ...baseRevision ? { baseRevision } : {},
818
+ ...this.scope ? { scope: this.scope } : {}
819
+ };
820
+ }
821
+ createMembershipOperation(type, collectionId, itemId, membership) {
822
+ return {
823
+ operationId: `${this.clientId}:${this.now()}:${createId()}`,
824
+ type,
825
+ entity: "membership",
826
+ id: getMembershipId(collectionId, itemId),
827
+ collectionId,
828
+ itemId,
829
+ ...membership ? { membership } : {},
830
+ createdAt: this.now(),
831
+ ...this.scope ? { scope: this.scope } : {}
832
+ };
833
+ }
589
834
  applyScope(item) {
590
835
  return this.scope ? { ...item, scope: this.scope } : item;
591
836
  }
@@ -602,6 +847,61 @@ var SyncStorageAdapter = class {
602
847
  }
603
848
  }
604
849
  }
850
+ async pushCollectionWithRetry(operation) {
851
+ let attempt = 0;
852
+ while (true) {
853
+ try {
854
+ return await this.remote.pushCollection?.({ ...operation, attempts: attempt });
855
+ } catch (error) {
856
+ if (isKeepSyncAuthError(error) || attempt >= this.maxRetries) throw error;
857
+ attempt += 1;
858
+ const delay = this.retryDelayMs * this.retryBackoff ** (attempt - 1);
859
+ if (delay > 0) await wait(delay);
860
+ }
861
+ }
862
+ }
863
+ async applyCollectionPushResult(operation, result) {
864
+ if (!this.local.getCollections || !this.local.setCollection || !this.local.removeCollection) return;
865
+ if (!result) return;
866
+ const outcome = result.type === "conflict" ? result.resolution : "remote";
867
+ if (outcome === "manual") return;
868
+ const deleted = outcome === "local" ? operation.type === "remove" : result.deleted ?? (operation.type === "remove" && outcome === "remote");
869
+ if (deleted) {
870
+ await this.local.removeCollection(operation.id, this.scope);
871
+ } else if (result.type === "synced" || outcome === "remote" || outcome === "local") {
872
+ const current = (outcome === "local" ? operation.collection : result.collection) ?? (await this.getCollections?.())?.find((entry) => entry.id === operation.id);
873
+ if (!current) return;
874
+ await this.local.setCollection({
875
+ ...current,
876
+ ...result.revision ? { revision: result.revision } : current.revision ? { revision: current.revision } : {},
877
+ ...this.scope ? { scope: this.scope } : {}
878
+ });
879
+ }
880
+ if (result.type === "conflict") {
881
+ const conflict = createCollectionSyncConflict(operation, result);
882
+ this.updateState({
883
+ status: "conflict",
884
+ collectionConflicts: [
885
+ ...(this.state.collectionConflicts ?? []).filter((entry) => entry.operation.id !== operation.id),
886
+ conflict
887
+ ]
888
+ });
889
+ }
890
+ }
891
+ async pushMembershipWithRetry(operation) {
892
+ let attempt = 0;
893
+ while (true) {
894
+ try {
895
+ await this.remote.pushMembership?.({ ...operation, attempts: attempt });
896
+ return;
897
+ } catch (error) {
898
+ if (isKeepSyncAuthError(error) || attempt >= this.maxRetries) throw error;
899
+ attempt += 1;
900
+ const delay = this.retryDelayMs * this.retryBackoff ** (attempt - 1);
901
+ if (delay > 0) await wait(delay);
902
+ }
903
+ }
904
+ }
605
905
  async enqueueBeforeLocalWrite(operation) {
606
906
  await this.enqueueManyBeforeLocalWrite([operation]);
607
907
  }
@@ -610,7 +910,8 @@ var SyncStorageAdapter = class {
610
910
  const next = [...this.queueItems];
611
911
  for (const operation of operations) {
612
912
  for (let index = next.length - 1; index >= 0; index -= 1) {
613
- if (next[index]?.id !== operation.id) continue;
913
+ const queued = next[index];
914
+ if (!queued || getQueueIdentity(queued) !== getQueueIdentity(operation)) continue;
614
915
  next.splice(index, 1);
615
916
  }
616
917
  next.push(operation);
@@ -670,30 +971,79 @@ var SyncStorageAdapter = class {
670
971
  });
671
972
  }
672
973
  async pullRemote() {
673
- if (!this.remote.pull) return true;
974
+ if (!this.remote.pull && !this.remote.pullCollections && !this.remote.pullMemberships) return true;
674
975
  try {
675
- const remoteItems = await this.remote.pull();
676
- const pendingIds = new Set(this.queueItems.map((operation) => operation.id));
677
- const localItems = await this.getAll();
678
- const localById = new Map(localItems.map((item) => [item.id, item]));
679
- const remoteById = /* @__PURE__ */ new Map();
680
- for (const item of remoteItems) {
681
- if (this.scope && item.scope && !sameScope(item.scope, this.scope)) continue;
682
- if (pendingIds.has(item.id)) continue;
683
- const previous = remoteById.get(item.id);
684
- remoteById.set(item.id, previous ? mergeKeepItemLists([item], [previous])[0] ?? item : item);
976
+ if (this.remote.pull) {
977
+ const remoteItems = await this.remote.pull();
978
+ const pendingIds = new Set(
979
+ this.queueItems.filter((operation) => operation.entity !== "collection").map((operation) => operation.id)
980
+ );
981
+ const localItems = await this.getAll();
982
+ const localById = new Map(localItems.map((item) => [item.id, item]));
983
+ const remoteById = /* @__PURE__ */ new Map();
984
+ for (const item of remoteItems) {
985
+ if (this.scope && item.scope && !sameScope(item.scope, this.scope)) continue;
986
+ if (pendingIds.has(item.id)) continue;
987
+ const previous = remoteById.get(item.id);
988
+ remoteById.set(item.id, previous ? mergeKeepItemLists([item], [previous])[0] ?? item : item);
989
+ }
990
+ const incoming = [...remoteById.values()].flatMap((item) => {
991
+ const current = localById.get(item.id);
992
+ if (current && item.updatedAt < current.updatedAt && (item.lastOpenedAt ?? 0) <= (current.lastOpenedAt ?? 0)) {
993
+ return [];
994
+ }
995
+ const merged = current ? mergeKeepItemLists([item], [current])[0] ?? item : item;
996
+ return [this.applyScope(merged)];
997
+ });
998
+ if (incoming.length > 0) {
999
+ await persistKeepItems(this.local, incoming);
1000
+ this.notifyDataListeners();
1001
+ }
685
1002
  }
686
- const incoming = [...remoteById.values()].flatMap((item) => {
687
- const current = localById.get(item.id);
688
- if (current && item.updatedAt < current.updatedAt && (item.lastOpenedAt ?? 0) <= (current.lastOpenedAt ?? 0)) {
689
- return [];
1003
+ if (this.remote.pullCollections && this.local.getCollections && this.local.setCollection && this.local.removeCollection) {
1004
+ const remoteCollections = await this.remote.pullCollections();
1005
+ const localCollections = await this.local.getCollections();
1006
+ const pendingIds = new Set(this.queueItems.filter(isCollectionSyncOperation).map((operation) => operation.id));
1007
+ const remoteById = new Map(
1008
+ remoteCollections.filter((collection) => !this.scope || !collection.scope || sameScope(collection.scope, this.scope)).map((collection) => [collection.id, collection])
1009
+ );
1010
+ for (const collection of remoteById.values()) {
1011
+ if (pendingIds.has(collection.id)) continue;
1012
+ await this.local.setCollection({ ...collection, ...this.scope ? { scope: this.scope } : {} });
690
1013
  }
691
- const merged = current ? mergeKeepItemLists([item], [current])[0] ?? item : item;
692
- return [this.applyScope(merged)];
693
- });
694
- if (incoming.length === 0) return true;
695
- await persistKeepItems(this.local, incoming);
696
- this.notifyDataListeners();
1014
+ for (const collection of localCollections) {
1015
+ if (pendingIds.has(collection.id) || remoteById.has(collection.id)) continue;
1016
+ if (this.scope && !sameScope(collection.scope, this.scope)) continue;
1017
+ await this.local.removeCollection(collection.id, this.scope);
1018
+ }
1019
+ this.notifyDataListeners();
1020
+ }
1021
+ if (this.remote.pullMemberships && this.getCollections && this.local.getCollectionMemberships && this.local.setCollectionMembership && this.local.removeCollectionMembership) {
1022
+ const remoteMemberships = await this.remote.pullMemberships();
1023
+ const localMemberships = await this.local.getCollectionMemberships();
1024
+ const pendingIds = new Set(this.queueItems.filter(isMembershipSyncOperation).map((operation) => operation.id));
1025
+ const itemIds = new Set((await this.getAll()).map((item) => item.id));
1026
+ const collectionIds = new Set((await this.getCollections()).map((collection) => collection.id));
1027
+ const remoteById = new Map(
1028
+ remoteMemberships.filter((membership) => {
1029
+ if (this.scope && membership.scope && !sameScope(membership.scope, this.scope)) return false;
1030
+ if (!itemIds.has(membership.itemId)) return false;
1031
+ return collectionIds.has(membership.collectionId);
1032
+ }).map((membership) => [getMembershipId(membership.collectionId, membership.itemId), membership])
1033
+ );
1034
+ for (const membership of remoteById.values()) {
1035
+ const id = getMembershipId(membership.collectionId, membership.itemId);
1036
+ if (pendingIds.has(id)) continue;
1037
+ await this.local.setCollectionMembership({ ...membership, ...this.scope ? { scope: this.scope } : {} });
1038
+ }
1039
+ for (const membership of localMemberships) {
1040
+ const id = getMembershipId(membership.collectionId, membership.itemId);
1041
+ if (pendingIds.has(id) || remoteById.has(id)) continue;
1042
+ if (this.scope && !sameScope(membership.scope, this.scope)) continue;
1043
+ await this.local.removeCollectionMembership(membership.collectionId, membership.itemId, this.scope);
1044
+ }
1045
+ this.notifyDataListeners();
1046
+ }
697
1047
  return true;
698
1048
  } catch (error) {
699
1049
  this.updateState({ status: "error", error });
@@ -714,7 +1064,33 @@ var SyncStorageAdapter = class {
714
1064
  };
715
1065
  function isSyncOperation(value) {
716
1066
  if (!isRecord(value)) return false;
717
- return typeof value.operationId === "string" && (value.type === "upsert" || value.type === "remove") && typeof value.id === "string" && typeof value.createdAt === "number";
1067
+ 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));
1068
+ }
1069
+ function isCollectionSyncOperation(value) {
1070
+ if (!isRecord(value)) return false;
1071
+ 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));
1072
+ }
1073
+ function isMembershipSyncOperation(value) {
1074
+ if (!isRecord(value)) return false;
1075
+ return typeof value.operationId === "string" && value.entity === "membership" && (value.type === "upsert" || value.type === "remove") && typeof value.id === "string" && typeof value.collectionId === "string" && typeof value.itemId === "string" && typeof value.createdAt === "number" && (value.membership === void 0 || isKeepCollectionMembership(value.membership)) && (value.type !== "upsert" || isKeepCollectionMembership(value.membership));
1076
+ }
1077
+ function isSyncQueueOperation(value) {
1078
+ return isSyncOperation(value) || isCollectionSyncOperation(value) || isMembershipSyncOperation(value);
1079
+ }
1080
+ function getQueueIdentity(operation) {
1081
+ return `${operation.entity ?? "item"}:${operation.id}`;
1082
+ }
1083
+ function getMembershipId(collectionId, itemId) {
1084
+ return `${collectionId}\0${itemId}`;
1085
+ }
1086
+ function createCollectionSyncConflict(operation, result) {
1087
+ const details = {
1088
+ operation,
1089
+ resolution: result.resolution,
1090
+ ...result.revision ? { revision: result.revision } : {}
1091
+ };
1092
+ if (result.deleted) return { ...details, deleted: true };
1093
+ return { ...details, deleted: false, collection: result.collection };
718
1094
  }
719
1095
  function createId() {
720
1096
  if (typeof crypto !== "undefined" && "randomUUID" in crypto) return crypto.randomUUID();
@@ -797,6 +1173,7 @@ var FallbackStorageAdapter = class {
797
1173
  this.migrateFallbackOnEmpty = options.migrateFallbackOnEmpty ?? false;
798
1174
  this.mirrorWrites = options.mirrorWrites ?? false;
799
1175
  this.storageKey = options.fallback.storageKey ?? options.primary.storageKey;
1176
+ this.scope = options.primary.scope ?? options.fallback.scope;
800
1177
  if (this.primary.getCollections && this.primary.setCollection && this.primary.removeCollection && this.fallback.getCollections && this.fallback.setCollection && this.fallback.removeCollection) {
801
1178
  const fallbackGetCollections = this.fallback.getCollections.bind(this.fallback);
802
1179
  this.getCollections = async () => {
@@ -820,6 +1197,22 @@ var FallbackStorageAdapter = class {
820
1197
  return adapter.removeCollection(id, scope);
821
1198
  });
822
1199
  }
1200
+ if (this.primary.getCollectionMemberships && this.primary.setCollectionMembership && this.primary.removeCollectionMembership && this.fallback.getCollectionMemberships && this.fallback.setCollectionMembership && this.fallback.removeCollectionMembership) {
1201
+ const fallbackGetMemberships = this.fallback.getCollectionMemberships.bind(this.fallback);
1202
+ this.getCollectionMemberships = async () => {
1203
+ const memberships = await this.execute(
1204
+ (adapter) => adapter.getCollectionMemberships?.() ?? Promise.resolve([])
1205
+ );
1206
+ if (this.active !== "primary" || !this.migrateFallbackOnEmpty || memberships.length > 0) return memberships;
1207
+ const fallbackMemberships = await fallbackGetMemberships();
1208
+ for (const membership of fallbackMemberships) await this.setCollectionMembership?.(membership);
1209
+ return fallbackMemberships;
1210
+ };
1211
+ this.setCollectionMembership = (membership) => this.executeWrite((adapter) => adapter.setCollectionMembership?.(membership) ?? Promise.resolve());
1212
+ this.removeCollectionMembership = (collectionId, itemId, scope) => this.executeWrite(
1213
+ (adapter) => adapter.removeCollectionMembership?.(collectionId, itemId, scope) ?? Promise.resolve()
1214
+ );
1215
+ }
823
1216
  }
824
1217
  get isUsingFallback() {
825
1218
  return this.active === "fallback";
@@ -835,12 +1228,12 @@ var FallbackStorageAdapter = class {
835
1228
  (adapter) => adapter.setMany ? adapter.setMany(items) : Promise.all(items.map((item) => adapter.set(item))).then(() => void 0)
836
1229
  );
837
1230
  }
838
- remove(id) {
839
- return this.executeWrite((adapter) => adapter.remove(id));
1231
+ remove(id, scope) {
1232
+ return this.executeWrite((adapter) => adapter.remove(id, scope));
840
1233
  }
841
- removeMany(ids) {
1234
+ removeMany(ids, scope) {
842
1235
  return this.executeWrite(
843
- (adapter) => adapter.removeMany ? adapter.removeMany(ids) : Promise.all(ids.map((id) => adapter.remove(id))).then(() => void 0)
1236
+ (adapter) => adapter.removeMany ? adapter.removeMany(ids, scope) : Promise.all(ids.map((id) => adapter.remove(id, scope))).then(() => void 0)
844
1237
  );
845
1238
  }
846
1239
  clear() {
@@ -947,6 +1340,9 @@ function createStorageAdapter(options) {
947
1340
  const getCollections = options.getCollections;
948
1341
  const setCollection = options.setCollection;
949
1342
  const removeCollection = options.removeCollection;
1343
+ const getCollectionMemberships = options.getCollectionMemberships;
1344
+ const setCollectionMembership = options.setCollectionMembership;
1345
+ const removeCollectionMembership = options.removeCollectionMembership;
950
1346
  return {
951
1347
  getAll: async () => options.getAll(),
952
1348
  ...getCollections && setCollection && removeCollection ? {
@@ -954,10 +1350,15 @@ function createStorageAdapter(options) {
954
1350
  setCollection: async (collection) => setCollection(collection),
955
1351
  removeCollection: async (id, scope) => removeCollection(id, scope)
956
1352
  } : {},
1353
+ ...getCollectionMemberships && setCollectionMembership && removeCollectionMembership ? {
1354
+ getCollectionMemberships: async () => getCollectionMemberships(),
1355
+ setCollectionMembership: async (membership) => setCollectionMembership(membership),
1356
+ removeCollectionMembership: async (collectionId, itemId, scope) => removeCollectionMembership(collectionId, itemId, scope)
1357
+ } : {},
957
1358
  set: async (item) => options.set(item),
958
1359
  ...setMany ? { setMany: async (items) => setMany(items) } : {},
959
- remove: async (id) => options.remove(id),
960
- ...removeMany ? { removeMany: async (ids) => removeMany(ids) } : {},
1360
+ remove: async (id, scope) => options.remove(id, scope),
1361
+ ...removeMany ? { removeMany: async (ids, scope) => removeMany(ids, scope) } : {},
961
1362
  clear: async () => options.clear(),
962
1363
  ...merge ? { merge: async (items) => merge(items) } : {},
963
1364
  ...subscribe ? {
@@ -1040,30 +1441,84 @@ var LocalStorageAdapter = class {
1040
1441
  current.filter((entry) => entry.id !== id || !isSameKeepScope(entry.scope, scope)),
1041
1442
  "remove"
1042
1443
  );
1444
+ const memberships = await this.getCollectionMemberships();
1445
+ await this.writeMemberships(
1446
+ memberships.filter((membership) => membership.collectionId !== id || !isSameKeepScope(membership.scope, scope)),
1447
+ "remove"
1448
+ );
1449
+ }
1450
+ async getCollectionMemberships() {
1451
+ if (!this.storage) return [];
1452
+ const storageKey = `${this.storageKey}:memberships`;
1453
+ let raw;
1454
+ try {
1455
+ raw = this.storage.getItem(storageKey);
1456
+ } catch (cause) {
1457
+ throw new KeepStorageAccessError({ operation: "getAll", storageKey, cause });
1458
+ }
1459
+ if (!raw) return [];
1460
+ try {
1461
+ const value = JSON.parse(raw);
1462
+ if (!Array.isArray(value) || !value.every(isKeepCollectionMembership)) {
1463
+ throw new KeepStorageParseError({ operation: "getAll", storageKey });
1464
+ }
1465
+ return value;
1466
+ } catch (cause) {
1467
+ if (cause instanceof KeepStorageParseError) throw cause;
1468
+ throw new KeepStorageParseError({ operation: "getAll", storageKey, cause });
1469
+ }
1470
+ }
1471
+ async setCollectionMembership(membership) {
1472
+ if (!isKeepCollectionMembership(membership)) throw new Error("Collection membership is invalid.");
1473
+ const current = await this.getCollectionMemberships();
1474
+ const sameMembership = (entry) => entry.collectionId === membership.collectionId && entry.itemId === membership.itemId && isSameKeepScope(entry.scope, membership.scope);
1475
+ await this.writeMemberships([...current.filter((entry) => !sameMembership(entry)), membership], "set");
1476
+ }
1477
+ async removeCollectionMembership(collectionId, itemId, scope) {
1478
+ const current = await this.getCollectionMemberships();
1479
+ await this.writeMemberships(
1480
+ current.filter(
1481
+ (entry) => entry.collectionId !== collectionId || entry.itemId !== itemId || !isSameKeepScope(entry.scope, scope)
1482
+ ),
1483
+ "remove"
1484
+ );
1043
1485
  }
1044
1486
  async set(item) {
1045
1487
  await this.setMany([item]);
1046
1488
  }
1047
1489
  async setMany(items) {
1048
1490
  const current = await this.getAll();
1049
- const byId = new Map(current.map((item) => [item.id, item]));
1050
- for (const item of items) byId.set(item.id, item);
1051
- this.write([...byId.values()], "set");
1491
+ const byScopeAndId = new Map(current.map((item) => [getItemStorageKey(item), item]));
1492
+ for (const item of items) byScopeAndId.set(getItemStorageKey(item), item);
1493
+ this.write([...byScopeAndId.values()], "set");
1052
1494
  }
1053
- async remove(id) {
1054
- await this.removeMany([id]);
1495
+ async remove(id, scope) {
1496
+ await this.removeMany([id], scope);
1055
1497
  }
1056
- async removeMany(ids) {
1498
+ async removeMany(ids, scope) {
1057
1499
  const idSet = new Set(ids);
1058
1500
  const items = await this.getAll();
1059
1501
  this.write(
1060
- items.filter((item) => !idSet.has(item.id)),
1502
+ items.filter((item) => !idSet.has(item.id) || scope !== void 0 && !isSameKeepScope(item.scope, scope)),
1061
1503
  "remove"
1062
1504
  );
1505
+ const memberships = await this.getCollectionMemberships();
1506
+ const membershipIds = memberships.filter(
1507
+ (membership) => idSet.has(membership.itemId) && (scope === void 0 || isSameKeepScope(membership.scope, scope))
1508
+ );
1509
+ for (const membership of membershipIds) {
1510
+ await this.removeCollectionMembership(membership.collectionId, membership.itemId, membership.scope);
1511
+ }
1063
1512
  }
1064
1513
  async clear() {
1065
1514
  if (!this.storage) return;
1066
1515
  try {
1516
+ const items = await this.getAll();
1517
+ if (items.length > 0) await this.removeMany(items.map((item) => item.id));
1518
+ const memberships = await this.getCollectionMemberships();
1519
+ for (const membership of memberships) {
1520
+ await this.removeCollectionMembership(membership.collectionId, membership.itemId, membership.scope);
1521
+ }
1067
1522
  this.storage.removeItem(this.storageKey);
1068
1523
  } catch (cause) {
1069
1524
  throw new KeepStorageAccessError({
@@ -1081,7 +1536,8 @@ var LocalStorageAdapter = class {
1081
1536
  subscribe(listener) {
1082
1537
  if (typeof window === "undefined") return () => void 0;
1083
1538
  const handleStorage = (event) => {
1084
- if (event.key !== null && event.key !== this.storageKey && event.key !== `${this.storageKey}:collections`) return;
1539
+ if (event.key !== null && event.key !== this.storageKey && event.key !== `${this.storageKey}:collections` && event.key !== `${this.storageKey}:memberships`)
1540
+ return;
1085
1541
  listener();
1086
1542
  };
1087
1543
  window.addEventListener("storage", handleStorage);
@@ -1116,6 +1572,16 @@ var LocalStorageAdapter = class {
1116
1572
  throw new KeepStorageAccessError({ operation, storageKey, cause });
1117
1573
  }
1118
1574
  }
1575
+ async writeMemberships(memberships, operation) {
1576
+ if (!this.storage) return;
1577
+ const storageKey = `${this.storageKey}:memberships`;
1578
+ try {
1579
+ this.storage.setItem(storageKey, JSON.stringify(memberships));
1580
+ } catch (cause) {
1581
+ if (isQuotaExceededError(cause)) throw new KeepStorageQuotaError({ operation, storageKey, cause });
1582
+ throw new KeepStorageAccessError({ operation, storageKey, cause });
1583
+ }
1584
+ }
1119
1585
  };
1120
1586
  var IndexedDBAdapter = class {
1121
1587
  constructor(options = {}) {
@@ -1134,7 +1600,7 @@ var IndexedDBAdapter = class {
1134
1600
  if (!isKeepItemArray(value)) {
1135
1601
  throw new KeepStorageParseError({ operation: "getAll", storageKey: this.storageKey });
1136
1602
  }
1137
- return value;
1603
+ return value.map(decodeIndexedDBItem);
1138
1604
  } catch (cause) {
1139
1605
  if (cause instanceof KeepStorageParseError) throw cause;
1140
1606
  throw new KeepStorageAccessError({ operation: "getAll", storageKey: this.storageKey, cause });
@@ -1165,6 +1631,38 @@ var IndexedDBAdapter = class {
1165
1631
  }
1166
1632
  async removeCollection(id, scope) {
1167
1633
  await this.writeCollection("remove", (store) => store.delete(collectionKey(id, scope)));
1634
+ const memberships = await this.getCollectionMemberships();
1635
+ for (const membership of memberships) {
1636
+ if (membership.collectionId === id && isSameKeepScope(membership.scope, scope)) {
1637
+ await this.removeCollectionMembership(id, membership.itemId, scope);
1638
+ }
1639
+ }
1640
+ }
1641
+ async getCollectionMemberships() {
1642
+ const database = await this.openCollections("getAll");
1643
+ if (!database) return [];
1644
+ const storageKey = `${this.storageKey}:memberships`;
1645
+ try {
1646
+ const transaction = database.transaction("memberships", "readonly");
1647
+ const value = await requestToPromise2(transaction.objectStore("memberships").getAll());
1648
+ if (!Array.isArray(value) || !value.every((entry) => isRecord(entry) && typeof entry.key === "string" && isKeepCollectionMembership(entry))) {
1649
+ throw new KeepStorageParseError({ operation: "getAll", storageKey });
1650
+ }
1651
+ return value.map(({ key: _key, ...membership }) => membership);
1652
+ } catch (cause) {
1653
+ if (cause instanceof KeepStorageParseError) throw cause;
1654
+ throw new KeepStorageAccessError({ operation: "getAll", storageKey, cause });
1655
+ }
1656
+ }
1657
+ async setCollectionMembership(membership) {
1658
+ if (!isKeepCollectionMembership(membership)) throw new Error("Collection membership is invalid.");
1659
+ await this.writeCollectionMembership(
1660
+ "set",
1661
+ (store) => store.put({ ...membership, key: membershipKey(membership.collectionId, membership.itemId, membership.scope) })
1662
+ );
1663
+ }
1664
+ async removeCollectionMembership(collectionId, itemId, scope) {
1665
+ await this.writeCollectionMembership("remove", (store) => store.delete(membershipKey(collectionId, itemId, scope)));
1168
1666
  }
1169
1667
  async set(item) {
1170
1668
  return this.setMany([item]);
@@ -1173,9 +1671,12 @@ var IndexedDBAdapter = class {
1173
1671
  const database = await this.open("set");
1174
1672
  if (!database) return;
1175
1673
  try {
1674
+ const identities = new Set(items.map(getItemIdentity));
1675
+ const previous = (await this.getAll()).filter((item) => identities.has(getItemIdentity(item)));
1176
1676
  const transaction = database.transaction(this.storeName, "readwrite");
1177
1677
  const objectStore = transaction.objectStore(this.storeName);
1178
- for (const item of items) objectStore.put(item);
1678
+ for (const item of previous) objectStore.delete(getStoredIndexedDBItemId(item));
1679
+ for (const item of items) objectStore.put(encodeIndexedDBItem(item));
1179
1680
  await transactionToPromise2(transaction);
1180
1681
  this.notifySubscribers();
1181
1682
  } catch (cause) {
@@ -1185,17 +1686,25 @@ var IndexedDBAdapter = class {
1185
1686
  throw new KeepStorageAccessError({ operation: "set", storageKey: this.storageKey, cause });
1186
1687
  }
1187
1688
  }
1188
- async remove(id) {
1189
- return this.removeMany([id]);
1689
+ async remove(id, scope) {
1690
+ return this.removeMany([id], scope);
1190
1691
  }
1191
- async removeMany(ids) {
1692
+ async removeMany(ids, scope) {
1192
1693
  const database = await this.open("remove");
1193
1694
  if (!database) return;
1194
1695
  try {
1696
+ const idSet = new Set(ids);
1697
+ const removableIds = (await this.getAll()).filter((item) => idSet.has(item.id) && (scope === void 0 || isSameKeepScope(item.scope, scope))).map(getStoredIndexedDBItemId);
1195
1698
  const transaction = database.transaction(this.storeName, "readwrite");
1196
1699
  const objectStore = transaction.objectStore(this.storeName);
1197
- for (const id of new Set(ids)) objectStore.delete(id);
1700
+ for (const id of removableIds) objectStore.delete(id);
1198
1701
  await transactionToPromise2(transaction);
1702
+ const memberships = await this.getCollectionMemberships();
1703
+ for (const membership of memberships) {
1704
+ if (idSet.has(membership.itemId) && (scope === void 0 || isSameKeepScope(membership.scope, scope))) {
1705
+ await this.removeCollectionMembership(membership.collectionId, membership.itemId, membership.scope);
1706
+ }
1707
+ }
1199
1708
  this.notifySubscribers();
1200
1709
  } catch (cause) {
1201
1710
  throw new KeepStorageAccessError({ operation: "remove", storageKey: this.storageKey, cause });
@@ -1205,6 +1714,12 @@ var IndexedDBAdapter = class {
1205
1714
  const database = await this.open("clear");
1206
1715
  if (!database) return;
1207
1716
  try {
1717
+ const items = await this.getAll();
1718
+ if (items.length > 0) await this.removeMany(items.map((item) => item.id));
1719
+ const memberships = await this.getCollectionMemberships();
1720
+ for (const membership of memberships) {
1721
+ await this.removeCollectionMembership(membership.collectionId, membership.itemId, membership.scope);
1722
+ }
1208
1723
  const transaction = database.transaction(this.storeName, "readwrite");
1209
1724
  transaction.objectStore(this.storeName).clear();
1210
1725
  await transactionToPromise2(transaction);
@@ -1249,6 +1764,20 @@ var IndexedDBAdapter = class {
1249
1764
  throw new KeepStorageAccessError({ operation, storageKey, cause });
1250
1765
  }
1251
1766
  }
1767
+ async writeCollectionMembership(operation, write) {
1768
+ const database = await this.openCollections(operation);
1769
+ if (!database) return;
1770
+ const storageKey = `${this.storageKey}:memberships`;
1771
+ try {
1772
+ const transaction = database.transaction("memberships", "readwrite");
1773
+ write(transaction.objectStore("memberships"));
1774
+ await transactionToPromise2(transaction);
1775
+ this.notifySubscribers();
1776
+ } catch (cause) {
1777
+ if (isQuotaExceededError(cause)) throw new KeepStorageQuotaError({ operation, storageKey, cause });
1778
+ throw new KeepStorageAccessError({ operation, storageKey, cause });
1779
+ }
1780
+ }
1252
1781
  openCollections(operation) {
1253
1782
  if (!this.indexedDB) return Promise.resolve(void 0);
1254
1783
  const storageKey = `${this.storageKey}:collections`;
@@ -1256,7 +1785,7 @@ var IndexedDBAdapter = class {
1256
1785
  this.collectionDatabasePromise = new Promise((resolve, reject) => {
1257
1786
  let request;
1258
1787
  try {
1259
- request = this.indexedDB?.open(storageKey, 1);
1788
+ request = this.indexedDB?.open(storageKey, 2);
1260
1789
  } catch (cause) {
1261
1790
  reject(cause);
1262
1791
  return;
@@ -1265,6 +1794,9 @@ var IndexedDBAdapter = class {
1265
1794
  if (!request.result.objectStoreNames.contains("collections")) {
1266
1795
  request.result.createObjectStore("collections", { keyPath: "key" });
1267
1796
  }
1797
+ if (!request.result.objectStoreNames.contains("memberships")) {
1798
+ request.result.createObjectStore("memberships", { keyPath: "key" });
1799
+ }
1268
1800
  };
1269
1801
  request.onsuccess = () => resolve(request.result);
1270
1802
  request.onerror = () => reject(request.error);
@@ -1304,9 +1836,42 @@ var IndexedDBAdapter = class {
1304
1836
  });
1305
1837
  }
1306
1838
  };
1839
+ var indexedDBStoredItemId = /* @__PURE__ */ Symbol("keepkit.indexeddb-stored-item-id");
1840
+ function encodeIndexedDBItem(item) {
1841
+ if (!item.scope) return item;
1842
+ return {
1843
+ ...item,
1844
+ id: createScopedIndexedDBItemId(item),
1845
+ __keepkitOriginalId: item.id
1846
+ };
1847
+ }
1848
+ function decodeIndexedDBItem(item) {
1849
+ const originalId = item.__keepkitOriginalId;
1850
+ const isEncoded = typeof originalId === "string" && item.scope !== void 0 && item.id === createScopedIndexedDBItemId({ ...item, id: originalId });
1851
+ const { __keepkitOriginalId: _encodedOriginalId, ...record } = item;
1852
+ const decoded = isEncoded ? { ...record, id: originalId } : item;
1853
+ Object.defineProperty(decoded, indexedDBStoredItemId, { value: item.id });
1854
+ return decoded;
1855
+ }
1856
+ function getStoredIndexedDBItemId(item) {
1857
+ return item[indexedDBStoredItemId] ?? createScopedIndexedDBItemId(item);
1858
+ }
1859
+ function createScopedIndexedDBItemId(item) {
1860
+ if (!item.scope) return item.id;
1861
+ return `\0keepkit-scope:${JSON.stringify([item.scope.tenantId ?? null, item.scope.userId ?? null, item.id])}`;
1862
+ }
1863
+ function getItemIdentity(item) {
1864
+ return JSON.stringify([item.scope?.tenantId ?? null, item.scope?.userId ?? null, item.id]);
1865
+ }
1307
1866
  function collectionKey(id, scope) {
1308
1867
  return JSON.stringify([scope?.tenantId ?? null, scope?.userId ?? null, id]);
1309
1868
  }
1869
+ function getItemStorageKey(item) {
1870
+ return JSON.stringify([item.scope?.tenantId ?? null, item.scope?.userId ?? null, item.id]);
1871
+ }
1872
+ function membershipKey(collectionId, itemId, scope) {
1873
+ return JSON.stringify([scope?.tenantId ?? null, scope?.userId ?? null, collectionId, itemId]);
1874
+ }
1310
1875
  function getBrowserStorage2() {
1311
1876
  if (typeof window === "undefined") return void 0;
1312
1877
  try {
@@ -1352,6 +1917,8 @@ export {
1352
1917
  removeKeepItems,
1353
1918
  mergeKeepItemLists,
1354
1919
  isKeepItem,
1920
+ isKeepCollectionDefinition,
1921
+ isKeepCollectionMembership,
1355
1922
  isRecord,
1356
1923
  getKeepScopeKey,
1357
1924
  isSameKeepScope,
@@ -1374,4 +1941,4 @@ export {
1374
1941
  LocalStorageAdapter,
1375
1942
  IndexedDBAdapter
1376
1943
  };
1377
- //# sourceMappingURL=chunk-2TENUU7B.js.map
1944
+ //# sourceMappingURL=chunk-QRPQYE7K.js.map