@keepkit/core 0.28.5 → 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,7 @@ 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
86
  }
87
87
  function isKeepCollectionMembership(value) {
88
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));
@@ -143,29 +143,37 @@ var ScopedStorageAdapter = class {
143
143
  const scoped = items.map((item) => ({ ...item, ...this.scope ? { scope: this.scope } : {} }));
144
144
  const ids = new Set(scoped.map((item) => item.id));
145
145
  const next = current.filter((item) => !ids.has(item.id) || !isSameKeepScope(item.scope, this.scope));
146
- await writeAll(this.base, [...next, ...scoped]);
146
+ await writeAll(this.base, [...next, ...scoped], this.scope);
147
147
  }
148
148
  async remove(id) {
149
149
  const current = await this.base.getAll();
150
150
  await writeAll(
151
151
  this.base,
152
- 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
153
154
  );
155
+ await this.removeMembershipsForItems([id]);
154
156
  }
155
157
  async removeMany(ids) {
156
158
  const idSet = new Set(ids);
157
159
  const current = await this.base.getAll();
158
160
  await writeAll(
159
161
  this.base,
160
- 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
161
164
  );
165
+ await this.removeMembershipsForItems([...idSet]);
162
166
  }
163
167
  async clear() {
164
168
  const current = await this.base.getAll();
169
+ const removedIds = current.filter((item) => isSameKeepScope(item.scope, this.scope)).map((item) => item.id);
165
170
  await writeAll(
166
171
  this.base,
167
- current.filter((item) => !isSameKeepScope(item.scope, this.scope))
172
+ current.filter((item) => !isSameKeepScope(item.scope, this.scope)),
173
+ this.scope
168
174
  );
175
+ if (removedIds.length > 0) await this.removeMembershipsForItems(removedIds);
176
+ await this.clearMembershipsForScope();
169
177
  }
170
178
  async merge(items) {
171
179
  await this.setMany(items);
@@ -174,6 +182,25 @@ var ScopedStorageAdapter = class {
174
182
  subscribe(listener) {
175
183
  return this.base.subscribe?.(listener) ?? (() => void 0);
176
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
+ }
177
204
  };
178
205
  function createScopedStorageAdapter(base, scope) {
179
206
  return new ScopedStorageAdapter(base, scope);
@@ -193,32 +220,38 @@ var ScopedSyncQueueAdapter = class {
193
220
  const ids = new Set(scoped.map((operation) => operation.operationId));
194
221
  await this.base.setMany([
195
222
  ...current.filter(
196
- (operation) => !ids.has(operation.operationId) && !isSameKeepScope(operation.scope, this.scope)
223
+ (operation) => !ids.has(operation.operationId) || !isSameKeepScope(operation.scope, this.scope)
197
224
  ),
198
225
  ...scoped
199
226
  ]);
200
227
  }
201
- remove(operationIds) {
202
- 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);
203
232
  }
204
- clear() {
205
- return this.base.getAll().then(
206
- (operations) => this.base.setMany(operations.filter((operation) => !isSameKeepScope(operation.scope, this.scope)))
207
- );
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);
208
236
  }
209
237
  };
210
- 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
+ }
211
246
  if (base.setMany) {
212
247
  await base.setMany(items);
213
248
  return;
214
249
  }
215
- const existing = await base.getAll();
216
- const ids = new Set(items.map((item) => item.id));
217
- for (const item of existing) {
218
- if (!ids.has(item.id)) await base.remove(item.id);
219
- }
220
250
  for (const item of items) await base.set(item);
221
251
  }
252
+ function itemStorageKey(item) {
253
+ return JSON.stringify([item.scope?.tenantId ?? null, item.scope?.userId ?? null, item.id]);
254
+ }
222
255
 
223
256
  // src/storage/sync.ts
224
257
  var DEFAULT_SYNC_QUEUE_KEY = "keepkit:sync-queue";
@@ -381,11 +414,12 @@ var SyncStorageAdapter = class {
381
414
  };
382
415
  this.local = options.local;
383
416
  this.remote = options.remote;
384
- 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;
385
420
  this.clientId = options.clientId ?? createId();
386
421
  this.now = options.now ?? Date.now;
387
422
  this.resolveConflict = options.resolveConflict;
388
- this.scope = getSyncScope(options);
389
423
  this.maxRetries = Math.max(0, options.maxRetries ?? 3);
390
424
  this.retryDelayMs = Math.max(0, options.retryDelayMs ?? 0);
391
425
  this.retryBackoff = Math.max(1, options.retryBackoff ?? 2);
@@ -402,7 +436,13 @@ var SyncStorageAdapter = class {
402
436
  this.setCollection = async (collection) => {
403
437
  const scopedCollection = { ...collection, ...this.scope ? { scope: this.scope } : {} };
404
438
  if (this.remote.pushCollection) {
405
- const operation = this.createCollectionOperation("upsert", scopedCollection.id, scopedCollection);
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
+ );
406
446
  await this.enqueueBeforeLocalWrite(operation);
407
447
  try {
408
448
  await setCollection(scopedCollection);
@@ -418,13 +458,16 @@ var SyncStorageAdapter = class {
418
458
  this.notifyDataListeners();
419
459
  };
420
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)) : [];
421
463
  if (this.remote.pushCollection) {
422
- const operation = this.createCollectionOperation("remove", id);
423
- await this.enqueueBeforeLocalWrite(operation);
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]);
424
467
  try {
425
468
  await removeCollection(id, this.scope);
426
469
  } catch (cause) {
427
- await this.removeQueued(operation.operationId);
470
+ await this.removeQueued([operation.operationId, ...membershipOperations.map((entry) => entry.operationId)]);
428
471
  throw cause;
429
472
  }
430
473
  this.notifyDataListeners();
@@ -445,11 +488,36 @@ var SyncStorageAdapter = class {
445
488
  return scope ? memberships.filter((membership) => sameScope(membership.scope, scope)) : memberships;
446
489
  };
447
490
  this.setCollectionMembership = async (membership) => {
448
- await setMembership({ ...membership, ...this.scope ? { scope: this.scope } : {} });
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
+ }
449
505
  this.notifyDataListeners();
450
506
  };
451
507
  this.removeCollectionMembership = async (collectionId, itemId) => {
452
- await removeMembership(collectionId, itemId, this.scope);
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
+ }
453
521
  this.notifyDataListeners();
454
522
  };
455
523
  }
@@ -493,11 +561,13 @@ var SyncStorageAdapter = class {
493
561
  }
494
562
  async remove(id) {
495
563
  const operation = this.createOperation("remove", id);
496
- 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]);
497
567
  try {
498
- await this.local.remove(id);
568
+ await this.local.remove(id, this.scope);
499
569
  } catch (cause) {
500
- await this.removeQueued(operation.operationId);
570
+ await this.removeQueued([operation.operationId, ...membershipOperations.map((entry) => entry.operationId)]);
501
571
  throw cause;
502
572
  }
503
573
  this.notifyDataListeners();
@@ -505,11 +575,15 @@ var SyncStorageAdapter = class {
505
575
  }
506
576
  async removeMany(ids) {
507
577
  const operations = [...new Set(ids)].map((id) => this.createOperation("remove", id));
508
- 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]);
509
582
  try {
510
- 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)));
511
585
  } catch (cause) {
512
- await this.removeQueued(operations.map((operation) => operation.operationId));
586
+ await this.removeQueued([...operations, ...membershipOperations].map((operation) => operation.operationId));
513
587
  throw cause;
514
588
  }
515
589
  this.notifyDataListeners();
@@ -518,9 +592,17 @@ var SyncStorageAdapter = class {
518
592
  async clear() {
519
593
  const items = await this.getAll();
520
594
  await this.removeMany(items.map((item) => item.id));
521
- await this.local.clear();
595
+ if (!this.scope) await this.local.clear();
522
596
  }
523
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
+ }
524
606
  const merged = this.local.merge ? await this.local.merge(localItems) : mergeKeepItemLists(await this.local.getAll(), localItems);
525
607
  if (!this.local.merge) await persistKeepItems(this.local, merged);
526
608
  await this.setMany(localItems);
@@ -569,6 +651,47 @@ var SyncStorageAdapter = class {
569
651
  });
570
652
  if (resolution !== "remote" && this.queueItems.length > 0) await this.flushSync();
571
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
+ }
572
695
  dispose() {
573
696
  this.disposed = true;
574
697
  if (this.onlineHandler) window.removeEventListener("online", this.onlineHandler);
@@ -589,7 +712,29 @@ var SyncStorageAdapter = class {
589
712
  try {
590
713
  if (isCollectionSyncOperation(operation)) {
591
714
  if (!this.remote.pushCollection) throw new Error("Remote sync driver does not support collection sync.");
592
- await this.pushCollectionWithRetry(operation);
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);
593
738
  await this.removeQueued(operation.operationId);
594
739
  this.updateState({
595
740
  status: this.queueItems.length > 0 ? "syncing" : "synced",
@@ -661,7 +806,7 @@ var SyncStorageAdapter = class {
661
806
  ...this.scope ? { scope: this.scope } : {}
662
807
  };
663
808
  }
664
- createCollectionOperation(type, id, collection) {
809
+ createCollectionOperation(type, id, collection, baseRevision) {
665
810
  return {
666
811
  operationId: `${this.clientId}:${this.now()}:${createId()}`,
667
812
  type,
@@ -669,6 +814,20 @@ var SyncStorageAdapter = class {
669
814
  id,
670
815
  ...collection ? { collection } : {},
671
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(),
672
831
  ...this.scope ? { scope: this.scope } : {}
673
832
  };
674
833
  }
@@ -692,7 +851,48 @@ var SyncStorageAdapter = class {
692
851
  let attempt = 0;
693
852
  while (true) {
694
853
  try {
695
- await this.remote.pushCollection?.({ ...operation, attempts: attempt });
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 });
696
896
  return;
697
897
  } catch (error) {
698
898
  if (isKeepSyncAuthError(error) || attempt >= this.maxRetries) throw error;
@@ -771,7 +971,7 @@ var SyncStorageAdapter = class {
771
971
  });
772
972
  }
773
973
  async pullRemote() {
774
- if (!this.remote.pull && !this.remote.pullCollections) return true;
974
+ if (!this.remote.pull && !this.remote.pullCollections && !this.remote.pullMemberships) return true;
775
975
  try {
776
976
  if (this.remote.pull) {
777
977
  const remoteItems = await this.remote.pull();
@@ -805,7 +1005,7 @@ var SyncStorageAdapter = class {
805
1005
  const localCollections = await this.local.getCollections();
806
1006
  const pendingIds = new Set(this.queueItems.filter(isCollectionSyncOperation).map((operation) => operation.id));
807
1007
  const remoteById = new Map(
808
- remoteCollections.filter((collection) => !this.scope || sameScope(collection.scope, this.scope)).map((collection) => [collection.id, collection])
1008
+ remoteCollections.filter((collection) => !this.scope || !collection.scope || sameScope(collection.scope, this.scope)).map((collection) => [collection.id, collection])
809
1009
  );
810
1010
  for (const collection of remoteById.values()) {
811
1011
  if (pendingIds.has(collection.id)) continue;
@@ -818,6 +1018,32 @@ var SyncStorageAdapter = class {
818
1018
  }
819
1019
  this.notifyDataListeners();
820
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
+ }
821
1047
  return true;
822
1048
  } catch (error) {
823
1049
  this.updateState({ status: "error", error });
@@ -844,11 +1070,27 @@ function isCollectionSyncOperation(value) {
844
1070
  if (!isRecord(value)) return false;
845
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));
846
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
+ }
847
1077
  function isSyncQueueOperation(value) {
848
- return isSyncOperation(value) || isCollectionSyncOperation(value);
1078
+ return isSyncOperation(value) || isCollectionSyncOperation(value) || isMembershipSyncOperation(value);
849
1079
  }
850
1080
  function getQueueIdentity(operation) {
851
- return `${operation.entity === "collection" ? "collection" : "item"}:${operation.id}`;
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 };
852
1094
  }
853
1095
  function createId() {
854
1096
  if (typeof crypto !== "undefined" && "randomUUID" in crypto) return crypto.randomUUID();
@@ -931,6 +1173,7 @@ var FallbackStorageAdapter = class {
931
1173
  this.migrateFallbackOnEmpty = options.migrateFallbackOnEmpty ?? false;
932
1174
  this.mirrorWrites = options.mirrorWrites ?? false;
933
1175
  this.storageKey = options.fallback.storageKey ?? options.primary.storageKey;
1176
+ this.scope = options.primary.scope ?? options.fallback.scope;
934
1177
  if (this.primary.getCollections && this.primary.setCollection && this.primary.removeCollection && this.fallback.getCollections && this.fallback.setCollection && this.fallback.removeCollection) {
935
1178
  const fallbackGetCollections = this.fallback.getCollections.bind(this.fallback);
936
1179
  this.getCollections = async () => {
@@ -985,12 +1228,12 @@ var FallbackStorageAdapter = class {
985
1228
  (adapter) => adapter.setMany ? adapter.setMany(items) : Promise.all(items.map((item) => adapter.set(item))).then(() => void 0)
986
1229
  );
987
1230
  }
988
- remove(id) {
989
- return this.executeWrite((adapter) => adapter.remove(id));
1231
+ remove(id, scope) {
1232
+ return this.executeWrite((adapter) => adapter.remove(id, scope));
990
1233
  }
991
- removeMany(ids) {
1234
+ removeMany(ids, scope) {
992
1235
  return this.executeWrite(
993
- (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)
994
1237
  );
995
1238
  }
996
1239
  clear() {
@@ -1114,8 +1357,8 @@ function createStorageAdapter(options) {
1114
1357
  } : {},
1115
1358
  set: async (item) => options.set(item),
1116
1359
  ...setMany ? { setMany: async (items) => setMany(items) } : {},
1117
- remove: async (id) => options.remove(id),
1118
- ...removeMany ? { removeMany: async (ids) => removeMany(ids) } : {},
1360
+ remove: async (id, scope) => options.remove(id, scope),
1361
+ ...removeMany ? { removeMany: async (ids, scope) => removeMany(ids, scope) } : {},
1119
1362
  clear: async () => options.clear(),
1120
1363
  ...merge ? { merge: async (items) => merge(items) } : {},
1121
1364
  ...subscribe ? {
@@ -1245,24 +1488,37 @@ var LocalStorageAdapter = class {
1245
1488
  }
1246
1489
  async setMany(items) {
1247
1490
  const current = await this.getAll();
1248
- const byId = new Map(current.map((item) => [item.id, item]));
1249
- for (const item of items) byId.set(item.id, item);
1250
- 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");
1251
1494
  }
1252
- async remove(id) {
1253
- await this.removeMany([id]);
1495
+ async remove(id, scope) {
1496
+ await this.removeMany([id], scope);
1254
1497
  }
1255
- async removeMany(ids) {
1498
+ async removeMany(ids, scope) {
1256
1499
  const idSet = new Set(ids);
1257
1500
  const items = await this.getAll();
1258
1501
  this.write(
1259
- items.filter((item) => !idSet.has(item.id)),
1502
+ items.filter((item) => !idSet.has(item.id) || scope !== void 0 && !isSameKeepScope(item.scope, scope)),
1260
1503
  "remove"
1261
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
+ }
1262
1512
  }
1263
1513
  async clear() {
1264
1514
  if (!this.storage) return;
1265
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
+ }
1266
1522
  this.storage.removeItem(this.storageKey);
1267
1523
  } catch (cause) {
1268
1524
  throw new KeepStorageAccessError({
@@ -1344,7 +1600,7 @@ var IndexedDBAdapter = class {
1344
1600
  if (!isKeepItemArray(value)) {
1345
1601
  throw new KeepStorageParseError({ operation: "getAll", storageKey: this.storageKey });
1346
1602
  }
1347
- return value;
1603
+ return value.map(decodeIndexedDBItem);
1348
1604
  } catch (cause) {
1349
1605
  if (cause instanceof KeepStorageParseError) throw cause;
1350
1606
  throw new KeepStorageAccessError({ operation: "getAll", storageKey: this.storageKey, cause });
@@ -1415,9 +1671,12 @@ var IndexedDBAdapter = class {
1415
1671
  const database = await this.open("set");
1416
1672
  if (!database) return;
1417
1673
  try {
1674
+ const identities = new Set(items.map(getItemIdentity));
1675
+ const previous = (await this.getAll()).filter((item) => identities.has(getItemIdentity(item)));
1418
1676
  const transaction = database.transaction(this.storeName, "readwrite");
1419
1677
  const objectStore = transaction.objectStore(this.storeName);
1420
- 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));
1421
1680
  await transactionToPromise2(transaction);
1422
1681
  this.notifySubscribers();
1423
1682
  } catch (cause) {
@@ -1427,17 +1686,25 @@ var IndexedDBAdapter = class {
1427
1686
  throw new KeepStorageAccessError({ operation: "set", storageKey: this.storageKey, cause });
1428
1687
  }
1429
1688
  }
1430
- async remove(id) {
1431
- return this.removeMany([id]);
1689
+ async remove(id, scope) {
1690
+ return this.removeMany([id], scope);
1432
1691
  }
1433
- async removeMany(ids) {
1692
+ async removeMany(ids, scope) {
1434
1693
  const database = await this.open("remove");
1435
1694
  if (!database) return;
1436
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);
1437
1698
  const transaction = database.transaction(this.storeName, "readwrite");
1438
1699
  const objectStore = transaction.objectStore(this.storeName);
1439
- for (const id of new Set(ids)) objectStore.delete(id);
1700
+ for (const id of removableIds) objectStore.delete(id);
1440
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
+ }
1441
1708
  this.notifySubscribers();
1442
1709
  } catch (cause) {
1443
1710
  throw new KeepStorageAccessError({ operation: "remove", storageKey: this.storageKey, cause });
@@ -1447,6 +1714,12 @@ var IndexedDBAdapter = class {
1447
1714
  const database = await this.open("clear");
1448
1715
  if (!database) return;
1449
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
+ }
1450
1723
  const transaction = database.transaction(this.storeName, "readwrite");
1451
1724
  transaction.objectStore(this.storeName).clear();
1452
1725
  await transactionToPromise2(transaction);
@@ -1563,9 +1836,39 @@ var IndexedDBAdapter = class {
1563
1836
  });
1564
1837
  }
1565
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
+ }
1566
1866
  function collectionKey(id, scope) {
1567
1867
  return JSON.stringify([scope?.tenantId ?? null, scope?.userId ?? null, id]);
1568
1868
  }
1869
+ function getItemStorageKey(item) {
1870
+ return JSON.stringify([item.scope?.tenantId ?? null, item.scope?.userId ?? null, item.id]);
1871
+ }
1569
1872
  function membershipKey(collectionId, itemId, scope) {
1570
1873
  return JSON.stringify([scope?.tenantId ?? null, scope?.userId ?? null, collectionId, itemId]);
1571
1874
  }
@@ -1638,4 +1941,4 @@ export {
1638
1941
  LocalStorageAdapter,
1639
1942
  IndexedDBAdapter
1640
1943
  };
1641
- //# sourceMappingURL=chunk-NZLAW4LY.js.map
1944
+ //# sourceMappingURL=chunk-QRPQYE7K.js.map