@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.
- package/README.md +88 -6
- package/dist/{chunk-Y4U5DEGG.js → chunk-5XSSH3TO.js} +212 -28
- package/dist/chunk-5XSSH3TO.js.map +1 -0
- package/dist/{chunk-2TENUU7B.js → chunk-QRPQYE7K.js} +643 -76
- package/dist/chunk-QRPQYE7K.js.map +1 -0
- package/dist/core.d.ts +192 -7
- package/dist/core.js +565 -7
- 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-Dtkxpnof.d.ts → scope-1bxGd68Y.d.ts} +9 -4
- package/dist/storage.d.ts +42 -18
- package/dist/storage.js +1 -1
- package/dist/{store-45yaDeER.d.ts → store-98vWM3tV.d.ts} +31 -4
- package/dist/{types-C_U5Q44o.d.ts → types-GTCMFt5T.d.ts} +83 -5
- package/package.json +1 -1
- package/dist/chunk-2TENUU7B.js.map +0 -1
- package/dist/chunk-Y4U5DEGG.js.map +0 -1
|
@@ -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)
|
|
223
|
+
(operation) => !ids.has(operation.operationId) || !isSameKeepScope(operation.scope, this.scope)
|
|
183
224
|
),
|
|
184
225
|
...scoped
|
|
185
226
|
]);
|
|
186
227
|
}
|
|
187
|
-
remove(operationIds) {
|
|
188
|
-
|
|
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
|
-
|
|
192
|
-
|
|
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(
|
|
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(
|
|
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.
|
|
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) =>
|
|
389
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
const
|
|
684
|
-
|
|
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
|
-
|
|
687
|
-
const
|
|
688
|
-
|
|
689
|
-
|
|
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
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
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
|
|
1050
|
-
for (const item of items)
|
|
1051
|
-
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");
|
|
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`)
|
|
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
|
|
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
|
|
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,
|
|
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-
|
|
1944
|
+
//# sourceMappingURL=chunk-QRPQYE7K.js.map
|