@keepkit/core 0.28.6 → 0.28.7
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 +10 -10
- package/dist/{chunk-5XSSH3TO.js → chunk-EMOPFB5D.js} +5 -3
- package/dist/chunk-EMOPFB5D.js.map +1 -0
- package/dist/{chunk-QRPQYE7K.js → chunk-SLH4DFYQ.js} +23 -14
- package/dist/chunk-SLH4DFYQ.js.map +1 -0
- package/dist/core.d.ts +0 -3
- package/dist/core.js +134 -70
- package/dist/core.js.map +1 -1
- package/dist/react.js +2 -2
- package/dist/storage.d.ts +1 -0
- package/dist/storage.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-5XSSH3TO.js.map +0 -1
- package/dist/chunk-QRPQYE7K.js.map +0 -1
package/dist/core.js
CHANGED
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
reconcileKeepItems,
|
|
21
21
|
reorderKeepItems,
|
|
22
22
|
revalidateKeepItems
|
|
23
|
-
} from "./chunk-
|
|
23
|
+
} from "./chunk-EMOPFB5D.js";
|
|
24
24
|
import {
|
|
25
25
|
KeepSchemaValidationError,
|
|
26
26
|
parseKeepMeta,
|
|
@@ -54,7 +54,23 @@ import {
|
|
|
54
54
|
isKeepSyncAuthError,
|
|
55
55
|
isSameKeepScope,
|
|
56
56
|
normalizeKeepTags
|
|
57
|
-
} from "./chunk-
|
|
57
|
+
} from "./chunk-SLH4DFYQ.js";
|
|
58
|
+
|
|
59
|
+
// src/features/persistence/write-lock.ts
|
|
60
|
+
var writeQueues = /* @__PURE__ */ new Map();
|
|
61
|
+
function withLocalStorageWriteLock(key, operation, lockUnavailableError) {
|
|
62
|
+
const lockName = `keepkit:local-storage:${key}`;
|
|
63
|
+
if (typeof navigator !== "undefined" && navigator.locks) {
|
|
64
|
+
return navigator.locks.request(lockName, operation);
|
|
65
|
+
}
|
|
66
|
+
if (typeof window !== "undefined") return Promise.reject(lockUnavailableError);
|
|
67
|
+
const previous = writeQueues.get(lockName) ?? Promise.resolve();
|
|
68
|
+
const next = previous.catch(() => void 0).then(operation);
|
|
69
|
+
writeQueues.set(lockName, next);
|
|
70
|
+
return next.finally(() => {
|
|
71
|
+
if (writeQueues.get(lockName) === next) writeQueues.delete(lockName);
|
|
72
|
+
});
|
|
73
|
+
}
|
|
58
74
|
|
|
59
75
|
// src/features/items/activity.ts
|
|
60
76
|
var DEFAULT_KEEP_HISTORY_KEY = "keepkit:view-history";
|
|
@@ -62,7 +78,6 @@ var DEFAULT_KEEP_VIEWING_RECORDS_KEY = "keepkit:viewing-records";
|
|
|
62
78
|
var DEFAULT_KEEP_HISTORY_LIMIT = 100;
|
|
63
79
|
var LocalStorageKeepHistoryStorage = class {
|
|
64
80
|
constructor(options = {}) {
|
|
65
|
-
this.writes = Promise.resolve();
|
|
66
81
|
this.key = options.key ?? DEFAULT_KEEP_HISTORY_KEY;
|
|
67
82
|
this.limitKey = `${this.key}:limit`;
|
|
68
83
|
this.storage = options.storage ?? getBrowserStorage();
|
|
@@ -125,9 +140,12 @@ var LocalStorageKeepHistoryStorage = class {
|
|
|
125
140
|
this.writeRaw(this.key, JSON.stringify(entries), "history");
|
|
126
141
|
}
|
|
127
142
|
withWriteLock(operation) {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
143
|
+
if (!this.storage) return operation();
|
|
144
|
+
return withLocalStorageWriteLock(
|
|
145
|
+
this.key,
|
|
146
|
+
operation,
|
|
147
|
+
new KeepActivityStorageError("KeepKit needs the Web Locks API to safely update history across tabs.", this.key)
|
|
148
|
+
);
|
|
131
149
|
}
|
|
132
150
|
async read(key, validate, label) {
|
|
133
151
|
const raw = this.readRaw(key, label);
|
|
@@ -155,7 +173,6 @@ var LocalStorageKeepHistoryStorage = class {
|
|
|
155
173
|
};
|
|
156
174
|
var LocalStorageKeepViewingRecordStorage = class {
|
|
157
175
|
constructor(options = {}) {
|
|
158
|
-
this.writes = Promise.resolve();
|
|
159
176
|
this.key = options.key ?? DEFAULT_KEEP_VIEWING_RECORDS_KEY;
|
|
160
177
|
this.storage = options.storage ?? getBrowserStorage();
|
|
161
178
|
this.now = options.now ?? Date.now;
|
|
@@ -186,11 +203,14 @@ var LocalStorageKeepViewingRecordStorage = class {
|
|
|
186
203
|
return this.withWriteLock(async () => {
|
|
187
204
|
const records = await this.readRecords();
|
|
188
205
|
const previous = records.find((entry) => entry.id === normalized.id);
|
|
206
|
+
if (previous?.updatedAt !== void 0 && normalized.updatedAt !== void 0 && previous.updatedAt > normalized.updatedAt) {
|
|
207
|
+
return { ...previous };
|
|
208
|
+
}
|
|
189
209
|
const now = this.now();
|
|
190
210
|
const next = {
|
|
191
211
|
...normalized,
|
|
192
212
|
createdAt: previous?.createdAt ?? normalized.createdAt ?? now,
|
|
193
|
-
updatedAt: now
|
|
213
|
+
updatedAt: Math.max(now, (previous?.updatedAt ?? -1) + 1, normalized.updatedAt ?? -1)
|
|
194
214
|
};
|
|
195
215
|
this.writeRecords([...records.filter((entry) => entry.id !== normalized.id), next]);
|
|
196
216
|
return { ...next };
|
|
@@ -237,9 +257,15 @@ var LocalStorageKeepViewingRecordStorage = class {
|
|
|
237
257
|
}
|
|
238
258
|
}
|
|
239
259
|
withWriteLock(operation) {
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
260
|
+
if (!this.storage) return operation();
|
|
261
|
+
return withLocalStorageWriteLock(
|
|
262
|
+
this.key,
|
|
263
|
+
operation,
|
|
264
|
+
new KeepActivityStorageError(
|
|
265
|
+
"KeepKit needs the Web Locks API to safely update viewing records across tabs.",
|
|
266
|
+
this.key
|
|
267
|
+
)
|
|
268
|
+
);
|
|
243
269
|
}
|
|
244
270
|
};
|
|
245
271
|
var KeepActivityStorageError = class extends Error {
|
|
@@ -320,6 +346,7 @@ function createRecordId() {
|
|
|
320
346
|
}
|
|
321
347
|
|
|
322
348
|
// src/features/items/collections.ts
|
|
349
|
+
var collectionMutationQueues = /* @__PURE__ */ new WeakMap();
|
|
323
350
|
async function getKeepCollectionItems(storage, collectionId) {
|
|
324
351
|
const memberships = await requireMemberships(storage);
|
|
325
352
|
const items = await storage.getAll();
|
|
@@ -330,48 +357,62 @@ async function getKeepCollectionItems(storage, collectionId) {
|
|
|
330
357
|
});
|
|
331
358
|
}
|
|
332
359
|
async function addKeepItemToCollection(storage, collectionId, itemId, order) {
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
360
|
+
return withCollectionMutationLock(storage, async () => {
|
|
361
|
+
if (!(await storage.getAll()).some((item) => item.id === itemId)) {
|
|
362
|
+
throw new Error(`KeepKit cannot add missing item "${itemId}" to a collection.`);
|
|
363
|
+
}
|
|
364
|
+
const collectionMemberships = (await requireMemberships(storage)).filter((membership) => membership.collectionId === collectionId).filter((membership) => membership.itemId !== itemId).sort((left, right) => left.order - right.order);
|
|
365
|
+
const insertionIndex = order ?? collectionMemberships.length;
|
|
366
|
+
if (!Number.isInteger(insertionIndex) || insertionIndex < 0) {
|
|
367
|
+
throw new RangeError("Collection order must be a non-negative integer.");
|
|
368
|
+
}
|
|
369
|
+
collectionMemberships.splice(Math.min(insertionIndex, collectionMemberships.length), 0, {
|
|
370
|
+
collectionId,
|
|
371
|
+
itemId,
|
|
372
|
+
order: insertionIndex
|
|
373
|
+
});
|
|
374
|
+
const reordered = collectionMemberships.map((membership, index) => ({ ...membership, order: index }));
|
|
375
|
+
for (const membership of reordered) await storage.setCollectionMembership?.(membership);
|
|
376
|
+
const added = reordered.find((membership) => membership.itemId === itemId);
|
|
377
|
+
if (!added) throw new Error("KeepKit failed to add the collection membership.");
|
|
378
|
+
return added;
|
|
345
379
|
});
|
|
346
|
-
const reordered = collectionMemberships.map((membership, index) => ({ ...membership, order: index }));
|
|
347
|
-
for (const membership of reordered) await storage.setCollectionMembership?.(membership);
|
|
348
|
-
const added = reordered.find((membership) => membership.itemId === itemId);
|
|
349
|
-
if (!added) throw new Error("KeepKit failed to add the collection membership.");
|
|
350
|
-
return added;
|
|
351
380
|
}
|
|
352
381
|
async function removeKeepItemFromCollection(storage, collectionId, itemId) {
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
382
|
+
await withCollectionMutationLock(storage, async () => {
|
|
383
|
+
const memberships = await requireMemberships(storage);
|
|
384
|
+
await storage.removeCollectionMembership?.(collectionId, itemId);
|
|
385
|
+
const remaining = memberships.filter((membership) => membership.collectionId === collectionId && membership.itemId !== itemId).sort((left, right) => left.order - right.order).map((membership, order) => ({ ...membership, order }));
|
|
386
|
+
for (const membership of remaining) await storage.setCollectionMembership?.(membership);
|
|
387
|
+
});
|
|
357
388
|
}
|
|
358
389
|
async function reorderKeepCollectionItems(storage, collectionId, itemIds) {
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
390
|
+
return withCollectionMutationLock(storage, async () => {
|
|
391
|
+
const allMemberships = await requireMemberships(storage);
|
|
392
|
+
const current = allMemberships.filter((membership) => membership.collectionId === collectionId).sort((left, right) => left.order - right.order);
|
|
393
|
+
const currentIds = new Set(current.map((membership) => membership.itemId));
|
|
394
|
+
const orderedIds = [...itemIds.filter((id, index) => currentIds.has(id) && itemIds.indexOf(id) === index)];
|
|
395
|
+
for (const membership of current) {
|
|
396
|
+
if (!orderedIds.includes(membership.itemId)) orderedIds.push(membership.itemId);
|
|
397
|
+
}
|
|
398
|
+
const currentByItemId = new Map(current.map((membership) => [membership.itemId, membership]));
|
|
399
|
+
const reordered = orderedIds.map((itemId, order) => ({
|
|
400
|
+
...currentByItemId.get(itemId) ?? {},
|
|
401
|
+
collectionId,
|
|
402
|
+
itemId,
|
|
403
|
+
order
|
|
404
|
+
}));
|
|
405
|
+
for (const membership of reordered) await storage.setCollectionMembership?.(membership);
|
|
406
|
+
return reordered;
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
function withCollectionMutationLock(storage, operation) {
|
|
410
|
+
const previous = collectionMutationQueues.get(storage) ?? Promise.resolve();
|
|
411
|
+
const next = previous.catch(() => void 0).then(operation);
|
|
412
|
+
collectionMutationQueues.set(storage, next);
|
|
413
|
+
return next.finally(() => {
|
|
414
|
+
if (collectionMutationQueues.get(storage) === next) collectionMutationQueues.delete(storage);
|
|
415
|
+
});
|
|
375
416
|
}
|
|
376
417
|
async function requireMemberships(storage) {
|
|
377
418
|
if (!storage.getCollectionMemberships || !storage.setCollectionMembership || !storage.removeCollectionMembership) {
|
|
@@ -411,7 +452,6 @@ var createKeepKitSetup = createKeepKitPreset;
|
|
|
411
452
|
var DEFAULT_KEEP_PROGRESS_KEY = "keepkit:progress";
|
|
412
453
|
var LocalStorageKeepProgressStorage = class {
|
|
413
454
|
constructor(options = {}) {
|
|
414
|
-
this.writes = Promise.resolve();
|
|
415
455
|
this.key = options.key ?? DEFAULT_KEEP_PROGRESS_KEY;
|
|
416
456
|
this.storage = options.storage ?? getBrowserStorage2();
|
|
417
457
|
this.now = options.now ?? Date.now;
|
|
@@ -443,32 +483,53 @@ var LocalStorageKeepProgressStorage = class {
|
|
|
443
483
|
return record;
|
|
444
484
|
}
|
|
445
485
|
async saveCourse(courseId, currentItemId) {
|
|
446
|
-
const
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
486
|
+
const id = normalizeId2(courseId, "course");
|
|
487
|
+
return this.withWriteLock(async () => {
|
|
488
|
+
const records = await this.getAll();
|
|
489
|
+
const previous = records.find((entry) => entry.kind === "course" && entry.courseId === id);
|
|
490
|
+
const record = {
|
|
491
|
+
kind: "course",
|
|
492
|
+
courseId: id,
|
|
493
|
+
...currentItemId?.trim() ? { currentItemId: currentItemId.trim() } : {},
|
|
494
|
+
updatedAt: Math.max(this.now(), (previous?.updatedAt ?? -1) + 1)
|
|
495
|
+
};
|
|
496
|
+
this.writeRecords([
|
|
497
|
+
...records.filter((entry) => getProgressIdentity(entry) !== getProgressIdentity(record)),
|
|
498
|
+
record
|
|
499
|
+
]);
|
|
500
|
+
return record;
|
|
501
|
+
});
|
|
454
502
|
}
|
|
455
503
|
async saveItem(itemId, progress) {
|
|
456
|
-
const
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
504
|
+
const id = normalizeId2(itemId, "item");
|
|
505
|
+
return this.withWriteLock(async () => {
|
|
506
|
+
const records = await this.getAll();
|
|
507
|
+
const previous = records.find((entry) => entry.kind === "item" && entry.itemId === id);
|
|
508
|
+
const record = {
|
|
509
|
+
...previous,
|
|
510
|
+
...progress,
|
|
511
|
+
kind: "item",
|
|
512
|
+
itemId: id,
|
|
513
|
+
...progress.audioId !== void 0 ? progress.audioId.trim() ? { audioId: progress.audioId.trim() } : { audioId: void 0 } : {},
|
|
514
|
+
updatedAt: Math.max(this.now(), (previous?.updatedAt ?? -1) + 1)
|
|
515
|
+
};
|
|
516
|
+
const normalized = normalizeProgressRecord(record);
|
|
517
|
+
if (normalized.kind !== "item") throw new TypeError("KeepKit progress record is invalid.");
|
|
518
|
+
this.writeRecords([
|
|
519
|
+
...records.filter((entry) => getProgressIdentity(entry) !== getProgressIdentity(normalized)),
|
|
520
|
+
normalized
|
|
521
|
+
]);
|
|
522
|
+
return normalized;
|
|
523
|
+
});
|
|
465
524
|
}
|
|
466
525
|
async set(record) {
|
|
467
526
|
await this.withWriteLock(async () => {
|
|
468
527
|
const normalized = normalizeProgressRecord(record);
|
|
469
528
|
const records = await this.getAll();
|
|
470
529
|
const identity = getProgressIdentity(normalized);
|
|
471
|
-
|
|
530
|
+
const previous = records.find((entry) => getProgressIdentity(entry) === identity);
|
|
531
|
+
const resolved = previous && previous.updatedAt > normalized.updatedAt ? previous : normalized;
|
|
532
|
+
this.writeRecords([...records.filter((entry) => getProgressIdentity(entry) !== identity), resolved]);
|
|
472
533
|
});
|
|
473
534
|
}
|
|
474
535
|
async resetCourse(courseId) {
|
|
@@ -504,9 +565,12 @@ var LocalStorageKeepProgressStorage = class {
|
|
|
504
565
|
}
|
|
505
566
|
}
|
|
506
567
|
withWriteLock(operation) {
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
568
|
+
if (!this.storage) return operation();
|
|
569
|
+
return withLocalStorageWriteLock(
|
|
570
|
+
this.key,
|
|
571
|
+
operation,
|
|
572
|
+
new KeepProgressStorageError("KeepKit needs the Web Locks API to safely update progress across tabs.", this.key)
|
|
573
|
+
);
|
|
510
574
|
}
|
|
511
575
|
};
|
|
512
576
|
function isProgressCompatible(record, compatibility) {
|