@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.
- package/README.md +66 -4
- package/dist/{chunk-AUNFZU3A.js → chunk-5XSSH3TO.js} +172 -43
- package/dist/chunk-5XSSH3TO.js.map +1 -0
- package/dist/{chunk-NZLAW4LY.js → chunk-QRPQYE7K.js} +363 -60
- package/dist/chunk-QRPQYE7K.js.map +1 -0
- package/dist/core.d.ts +38 -10
- package/dist/core.js +135 -39
- package/dist/core.js.map +1 -1
- package/dist/react.d.ts +5 -5
- package/dist/react.js +2 -2
- package/dist/schema.d.ts +1 -1
- package/dist/{scope-BteyH-b9.d.ts → scope-1bxGd68Y.d.ts} +4 -2
- package/dist/storage.d.ts +17 -12
- package/dist/storage.js +1 -1
- package/dist/{store-DVP3QyVp.d.ts → store-98vWM3tV.d.ts} +20 -2
- package/dist/{types-SKIL2p2c.d.ts → types-GTCMFt5T.d.ts} +52 -5
- package/package.json +1 -1
- package/dist/chunk-AUNFZU3A.js.map +0 -1
- package/dist/chunk-NZLAW4LY.js.map +0 -1
|
@@ -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)
|
|
223
|
+
(operation) => !ids.has(operation.operationId) || !isSameKeepScope(operation.scope, this.scope)
|
|
197
224
|
),
|
|
198
225
|
...scoped
|
|
199
226
|
]);
|
|
200
227
|
}
|
|
201
|
-
remove(operationIds) {
|
|
202
|
-
|
|
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
|
-
|
|
206
|
-
|
|
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.
|
|
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
|
|
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
|
|
423
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
1249
|
-
for (const item of items)
|
|
1250
|
-
this.write([...
|
|
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
|
|
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
|
|
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-
|
|
1944
|
+
//# sourceMappingURL=chunk-QRPQYE7K.js.map
|