@keepkit/core 0.28.5 → 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 +66 -4
- package/dist/{chunk-AUNFZU3A.js → chunk-EMOPFB5D.js} +174 -43
- package/dist/chunk-EMOPFB5D.js.map +1 -0
- package/dist/{chunk-NZLAW4LY.js → chunk-SLH4DFYQ.js} +382 -70
- package/dist/chunk-SLH4DFYQ.js.map +1 -0
- package/dist/core.d.ts +35 -10
- package/dist/core.js +252 -92
- 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 +18 -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
package/dist/core.js
CHANGED
|
@@ -13,13 +13,14 @@ import {
|
|
|
13
13
|
isKeepItemMetadataStale,
|
|
14
14
|
mergeKeepItems,
|
|
15
15
|
migrateKeepItems,
|
|
16
|
+
migrateLegacyCollectionMemberships,
|
|
16
17
|
moveKeepItem,
|
|
17
18
|
orderKeepItems,
|
|
18
19
|
queryKeepItems,
|
|
19
20
|
reconcileKeepItems,
|
|
20
21
|
reorderKeepItems,
|
|
21
22
|
revalidateKeepItems
|
|
22
|
-
} from "./chunk-
|
|
23
|
+
} from "./chunk-EMOPFB5D.js";
|
|
23
24
|
import {
|
|
24
25
|
KeepSchemaValidationError,
|
|
25
26
|
parseKeepMeta,
|
|
@@ -53,7 +54,23 @@ import {
|
|
|
53
54
|
isKeepSyncAuthError,
|
|
54
55
|
isSameKeepScope,
|
|
55
56
|
normalizeKeepTags
|
|
56
|
-
} 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
|
+
}
|
|
57
74
|
|
|
58
75
|
// src/features/items/activity.ts
|
|
59
76
|
var DEFAULT_KEEP_HISTORY_KEY = "keepkit:view-history";
|
|
@@ -84,32 +101,52 @@ var LocalStorageKeepHistoryStorage = class {
|
|
|
84
101
|
}
|
|
85
102
|
async setLimit(limit) {
|
|
86
103
|
const normalized = normalizeLimit(limit);
|
|
87
|
-
this.
|
|
88
|
-
|
|
89
|
-
|
|
104
|
+
await this.withWriteLock(async () => {
|
|
105
|
+
this.writeRaw(this.limitKey, JSON.stringify(normalized), "history-limit");
|
|
106
|
+
const entries = await this.getAll();
|
|
107
|
+
await this.writeEntries(entries.slice(0, normalized));
|
|
108
|
+
});
|
|
90
109
|
}
|
|
91
|
-
async record(itemId, viewedAt = this.now()) {
|
|
110
|
+
async record(itemId, viewedAt = this.now(), context) {
|
|
92
111
|
const normalizedId = normalizeId(itemId);
|
|
93
112
|
assertTimestamp(viewedAt);
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
113
|
+
return this.withWriteLock(async () => {
|
|
114
|
+
const entries = await this.getAll();
|
|
115
|
+
const previous = entries.find((entry) => entry.itemId === normalizedId);
|
|
116
|
+
const isNewer = viewedAt >= (previous?.lastViewedAt ?? -1);
|
|
117
|
+
const nextContext = isNewer ? normalizeHistoryContext(context) : previous?.context;
|
|
118
|
+
const next = [
|
|
119
|
+
{
|
|
120
|
+
itemId: normalizedId,
|
|
121
|
+
lastViewedAt: Math.max(previous?.lastViewedAt ?? 0, viewedAt),
|
|
122
|
+
...nextContext ? { context: nextContext } : {}
|
|
123
|
+
},
|
|
124
|
+
...entries.filter((entry) => entry.itemId !== normalizedId)
|
|
125
|
+
].sort((left, right) => right.lastViewedAt - left.lastViewedAt).slice(0, await this.getLimit());
|
|
126
|
+
await this.writeEntries(next);
|
|
127
|
+
return cloneHistoryEntries(next);
|
|
128
|
+
});
|
|
102
129
|
}
|
|
103
130
|
async remove(itemId) {
|
|
104
131
|
const normalizedId = normalizeId(itemId);
|
|
105
|
-
await this.
|
|
132
|
+
await this.withWriteLock(
|
|
133
|
+
async () => this.writeEntries((await this.getAll()).filter((entry) => entry.itemId !== normalizedId))
|
|
134
|
+
);
|
|
106
135
|
}
|
|
107
136
|
async clear() {
|
|
108
|
-
this.writeRaw(this.key, "[]", "history");
|
|
137
|
+
await this.withWriteLock(async () => this.writeRaw(this.key, "[]", "history"));
|
|
109
138
|
}
|
|
110
139
|
async writeEntries(entries) {
|
|
111
140
|
this.writeRaw(this.key, JSON.stringify(entries), "history");
|
|
112
141
|
}
|
|
142
|
+
withWriteLock(operation) {
|
|
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
|
+
);
|
|
149
|
+
}
|
|
113
150
|
async read(key, validate, label) {
|
|
114
151
|
const raw = this.readRaw(key, label);
|
|
115
152
|
if (raw === null) return [];
|
|
@@ -125,8 +162,10 @@ var LocalStorageKeepHistoryStorage = class {
|
|
|
125
162
|
}
|
|
126
163
|
}
|
|
127
164
|
writeRaw(key, value, label) {
|
|
165
|
+
if (!this.storage)
|
|
166
|
+
throw new KeepActivityStorageError(`KeepKit cannot persist ${label} data without browser storage.`, key);
|
|
128
167
|
try {
|
|
129
|
-
this.storage
|
|
168
|
+
this.storage.setItem(key, value);
|
|
130
169
|
} catch (cause) {
|
|
131
170
|
throw new KeepActivityStorageError(`KeepKit could not write ${label} data.`, key, cause);
|
|
132
171
|
}
|
|
@@ -145,29 +184,51 @@ var LocalStorageKeepViewingRecordStorage = class {
|
|
|
145
184
|
return filtered.sort((left, right) => right.viewedAt - left.viewedAt).map((record) => ({ ...record }));
|
|
146
185
|
}
|
|
147
186
|
async set(record) {
|
|
148
|
-
|
|
149
|
-
const records = await this.readRecords();
|
|
150
|
-
this.writeRecords([...records.filter((entry) => entry.id !== normalized.id), normalized]);
|
|
187
|
+
await this.saveRecord(record);
|
|
151
188
|
}
|
|
152
189
|
async add(itemId, options = {}) {
|
|
190
|
+
const timestamp = this.now();
|
|
153
191
|
const record = {
|
|
154
192
|
id: this.createId(),
|
|
155
193
|
itemId: normalizeId(itemId),
|
|
156
|
-
viewedAt: options.viewedAt ??
|
|
194
|
+
viewedAt: options.viewedAt ?? timestamp,
|
|
195
|
+
createdAt: timestamp,
|
|
196
|
+
updatedAt: timestamp,
|
|
157
197
|
...options.note?.trim() ? { note: options.note.trim() } : {}
|
|
158
198
|
};
|
|
159
|
-
|
|
160
|
-
|
|
199
|
+
return this.saveRecord(record);
|
|
200
|
+
}
|
|
201
|
+
async saveRecord(record) {
|
|
202
|
+
const normalized = normalizeViewingRecord(record);
|
|
203
|
+
return this.withWriteLock(async () => {
|
|
204
|
+
const records = await this.readRecords();
|
|
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
|
+
}
|
|
209
|
+
const now = this.now();
|
|
210
|
+
const next = {
|
|
211
|
+
...normalized,
|
|
212
|
+
createdAt: previous?.createdAt ?? normalized.createdAt ?? now,
|
|
213
|
+
updatedAt: Math.max(now, (previous?.updatedAt ?? -1) + 1, normalized.updatedAt ?? -1)
|
|
214
|
+
};
|
|
215
|
+
this.writeRecords([...records.filter((entry) => entry.id !== normalized.id), next]);
|
|
216
|
+
return { ...next };
|
|
217
|
+
});
|
|
161
218
|
}
|
|
162
219
|
async remove(id) {
|
|
163
|
-
|
|
220
|
+
await this.withWriteLock(
|
|
221
|
+
async () => this.writeRecords((await this.readRecords()).filter((record) => record.id !== id))
|
|
222
|
+
);
|
|
164
223
|
}
|
|
165
224
|
async removeForItem(itemId) {
|
|
166
225
|
const normalizedId = normalizeId(itemId);
|
|
167
|
-
|
|
226
|
+
await this.withWriteLock(
|
|
227
|
+
async () => this.writeRecords((await this.readRecords()).filter((record) => record.itemId !== normalizedId))
|
|
228
|
+
);
|
|
168
229
|
}
|
|
169
230
|
async clear() {
|
|
170
|
-
this.writeRecords([]);
|
|
231
|
+
await this.withWriteLock(async () => this.writeRecords([]));
|
|
171
232
|
}
|
|
172
233
|
async readRecords() {
|
|
173
234
|
const raw = this.readRaw();
|
|
@@ -186,12 +247,26 @@ var LocalStorageKeepViewingRecordStorage = class {
|
|
|
186
247
|
}
|
|
187
248
|
}
|
|
188
249
|
writeRecords(records) {
|
|
250
|
+
if (!this.storage) {
|
|
251
|
+
throw new KeepActivityStorageError("KeepKit cannot persist viewing records without browser storage.", this.key);
|
|
252
|
+
}
|
|
189
253
|
try {
|
|
190
|
-
this.storage
|
|
254
|
+
this.storage.setItem(this.key, JSON.stringify(records));
|
|
191
255
|
} catch (cause) {
|
|
192
256
|
throw new KeepActivityStorageError("KeepKit could not write viewing records.", this.key, cause);
|
|
193
257
|
}
|
|
194
258
|
}
|
|
259
|
+
withWriteLock(operation) {
|
|
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
|
+
);
|
|
269
|
+
}
|
|
195
270
|
};
|
|
196
271
|
var KeepActivityStorageError = class extends Error {
|
|
197
272
|
constructor(message, storageKey, cause) {
|
|
@@ -203,20 +278,22 @@ var KeepActivityStorageError = class extends Error {
|
|
|
203
278
|
};
|
|
204
279
|
function isHistoryEntryArray(value) {
|
|
205
280
|
return Array.isArray(value) && value.every(
|
|
206
|
-
(entry) => isRecord(entry) && typeof entry.itemId === "string" && entry.itemId.length > 0 && typeof entry.lastViewedAt === "number" && Number.isFinite(entry.lastViewedAt)
|
|
281
|
+
(entry) => isRecord(entry) && typeof entry.itemId === "string" && entry.itemId.length > 0 && typeof entry.lastViewedAt === "number" && Number.isFinite(entry.lastViewedAt) && (entry.context === void 0 || isHistoryContext(entry.context))
|
|
207
282
|
);
|
|
208
283
|
}
|
|
209
284
|
function isViewingRecordArray(value) {
|
|
210
285
|
return Array.isArray(value) && value.every(isViewingRecord);
|
|
211
286
|
}
|
|
212
287
|
function isViewingRecord(value) {
|
|
213
|
-
return isRecord(value) && typeof value.id === "string" && value.id.length > 0 && typeof value.itemId === "string" && value.itemId.length > 0 && typeof value.viewedAt === "number" && Number.isFinite(value.viewedAt) && value.viewedAt >= 0 && (value.note === void 0 || typeof value.note === "string");
|
|
288
|
+
return isRecord(value) && typeof value.id === "string" && value.id.length > 0 && typeof value.itemId === "string" && value.itemId.length > 0 && typeof value.viewedAt === "number" && Number.isFinite(value.viewedAt) && value.viewedAt >= 0 && (value.createdAt === void 0 || typeof value.createdAt === "number" && Number.isFinite(value.createdAt)) && (value.updatedAt === void 0 || typeof value.updatedAt === "number" && Number.isFinite(value.updatedAt)) && (value.note === void 0 || typeof value.note === "string");
|
|
214
289
|
}
|
|
215
290
|
function normalizeViewingRecord(record) {
|
|
216
291
|
const normalized = {
|
|
217
292
|
id: normalizeId(record.id),
|
|
218
293
|
itemId: normalizeId(record.itemId),
|
|
219
294
|
viewedAt: record.viewedAt,
|
|
295
|
+
...record.createdAt !== void 0 ? { createdAt: record.createdAt } : {},
|
|
296
|
+
...record.updatedAt !== void 0 ? { updatedAt: record.updatedAt } : {},
|
|
220
297
|
...record.note?.trim() ? { note: record.note.trim() } : {}
|
|
221
298
|
};
|
|
222
299
|
assertTimestamp(normalized.viewedAt);
|
|
@@ -235,7 +312,15 @@ function assertTimestamp(value) {
|
|
|
235
312
|
if (!Number.isFinite(value) || value < 0) throw new TypeError("KeepKit timestamps must be non-negative numbers.");
|
|
236
313
|
}
|
|
237
314
|
function cloneHistoryEntries(entries) {
|
|
238
|
-
return entries.map((entry) => ({ ...entry }));
|
|
315
|
+
return entries.map((entry) => ({ ...entry, ...entry.context ? { context: { ...entry.context } } : {} }));
|
|
316
|
+
}
|
|
317
|
+
function normalizeHistoryContext(context) {
|
|
318
|
+
if (!context) return void 0;
|
|
319
|
+
const entries = Object.entries(context).filter(([key, value]) => key.trim() && typeof value === "string");
|
|
320
|
+
return entries.length > 0 ? Object.fromEntries(entries) : void 0;
|
|
321
|
+
}
|
|
322
|
+
function isHistoryContext(value) {
|
|
323
|
+
return isRecord(value) && Object.values(value).every((entry) => typeof entry === "string");
|
|
239
324
|
}
|
|
240
325
|
function parseJson(raw, key, label) {
|
|
241
326
|
try {
|
|
@@ -261,6 +346,7 @@ function createRecordId() {
|
|
|
261
346
|
}
|
|
262
347
|
|
|
263
348
|
// src/features/items/collections.ts
|
|
349
|
+
var collectionMutationQueues = /* @__PURE__ */ new WeakMap();
|
|
264
350
|
async function getKeepCollectionItems(storage, collectionId) {
|
|
265
351
|
const memberships = await requireMemberships(storage);
|
|
266
352
|
const items = await storage.getAll();
|
|
@@ -271,48 +357,62 @@ async function getKeepCollectionItems(storage, collectionId) {
|
|
|
271
357
|
});
|
|
272
358
|
}
|
|
273
359
|
async function addKeepItemToCollection(storage, collectionId, itemId, order) {
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
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;
|
|
286
379
|
});
|
|
287
|
-
const reordered = collectionMemberships.map((membership, index) => ({ ...membership, order: index }));
|
|
288
|
-
for (const membership of reordered) await storage.setCollectionMembership?.(membership);
|
|
289
|
-
const added = reordered.find((membership) => membership.itemId === itemId);
|
|
290
|
-
if (!added) throw new Error("KeepKit failed to add the collection membership.");
|
|
291
|
-
return added;
|
|
292
380
|
}
|
|
293
381
|
async function removeKeepItemFromCollection(storage, collectionId, itemId) {
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
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
|
+
});
|
|
298
388
|
}
|
|
299
389
|
async function reorderKeepCollectionItems(storage, collectionId, itemIds) {
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
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
|
+
});
|
|
316
416
|
}
|
|
317
417
|
async function requireMemberships(storage) {
|
|
318
418
|
if (!storage.getCollectionMemberships || !storage.setCollectionMembership || !storage.removeCollectionMembership) {
|
|
@@ -383,41 +483,69 @@ var LocalStorageKeepProgressStorage = class {
|
|
|
383
483
|
return record;
|
|
384
484
|
}
|
|
385
485
|
async saveCourse(courseId, currentItemId) {
|
|
386
|
-
const
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
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
|
+
});
|
|
394
502
|
}
|
|
395
503
|
async saveItem(itemId, progress) {
|
|
396
|
-
const
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
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
|
+
});
|
|
404
524
|
}
|
|
405
525
|
async set(record) {
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
526
|
+
await this.withWriteLock(async () => {
|
|
527
|
+
const normalized = normalizeProgressRecord(record);
|
|
528
|
+
const records = await this.getAll();
|
|
529
|
+
const identity = getProgressIdentity(normalized);
|
|
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]);
|
|
533
|
+
});
|
|
410
534
|
}
|
|
411
535
|
async resetCourse(courseId) {
|
|
412
536
|
const id = normalizeId2(courseId, "course");
|
|
413
|
-
|
|
537
|
+
await this.withWriteLock(
|
|
538
|
+
async () => this.writeRecords((await this.getAll()).filter((entry) => entry.kind !== "course" || entry.courseId !== id))
|
|
539
|
+
);
|
|
414
540
|
}
|
|
415
541
|
async resetItem(itemId) {
|
|
416
542
|
const id = normalizeId2(itemId, "item");
|
|
417
|
-
|
|
543
|
+
await this.withWriteLock(
|
|
544
|
+
async () => this.writeRecords((await this.getAll()).filter((entry) => entry.kind !== "item" || entry.itemId !== id))
|
|
545
|
+
);
|
|
418
546
|
}
|
|
419
547
|
async clear() {
|
|
420
|
-
this.writeRecords([]);
|
|
548
|
+
await this.withWriteLock(async () => this.writeRecords([]));
|
|
421
549
|
}
|
|
422
550
|
readRaw() {
|
|
423
551
|
try {
|
|
@@ -427,12 +555,23 @@ var LocalStorageKeepProgressStorage = class {
|
|
|
427
555
|
}
|
|
428
556
|
}
|
|
429
557
|
writeRecords(records) {
|
|
558
|
+
if (!this.storage) {
|
|
559
|
+
throw new KeepProgressStorageError("KeepKit cannot persist progress without browser storage.", this.key);
|
|
560
|
+
}
|
|
430
561
|
try {
|
|
431
|
-
this.storage
|
|
562
|
+
this.storage.setItem(this.key, JSON.stringify(records));
|
|
432
563
|
} catch (cause) {
|
|
433
564
|
throw new KeepProgressStorageError("KeepKit could not write progress data.", this.key, cause);
|
|
434
565
|
}
|
|
435
566
|
}
|
|
567
|
+
withWriteLock(operation) {
|
|
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
|
+
);
|
|
574
|
+
}
|
|
436
575
|
};
|
|
437
576
|
function isProgressCompatible(record, compatibility) {
|
|
438
577
|
if (record.language !== void 0 && record.language !== compatibility?.language) return false;
|
|
@@ -440,6 +579,8 @@ function isProgressCompatible(record, compatibility) {
|
|
|
440
579
|
if (compatibility?.language !== void 0 && record.language !== compatibility.language) return false;
|
|
441
580
|
if (compatibility?.contentVersion !== void 0 && record.contentVersion !== compatibility.contentVersion)
|
|
442
581
|
return false;
|
|
582
|
+
if (record.audioId !== void 0 && record.audioId !== compatibility?.audioId) return false;
|
|
583
|
+
if (compatibility?.audioId !== void 0 && record.audioId !== compatibility.audioId) return false;
|
|
443
584
|
return true;
|
|
444
585
|
}
|
|
445
586
|
var KeepProgressStorageError = class extends Error {
|
|
@@ -457,7 +598,7 @@ function isProgressRecord(value) {
|
|
|
457
598
|
return typeof value.courseId === "string" && value.courseId.length > 0 && (value.currentItemId === void 0 || typeof value.currentItemId === "string");
|
|
458
599
|
}
|
|
459
600
|
if (value.kind !== "item") return false;
|
|
460
|
-
return typeof value.itemId === "string" && value.itemId.length > 0 && (value.audioPositionMs === void 0 || typeof value.audioPositionMs === "number" && Number.isFinite(value.audioPositionMs) && value.audioPositionMs >= 0) && (value.readingPosition === void 0 || typeof value.readingPosition === "number" && Number.isFinite(value.readingPosition) && value.readingPosition >= 0) && (value.language === void 0 || typeof value.language === "string") && (value.contentVersion === void 0 || typeof value.contentVersion === "string");
|
|
601
|
+
return typeof value.itemId === "string" && value.itemId.length > 0 && (value.audioPositionMs === void 0 || typeof value.audioPositionMs === "number" && Number.isFinite(value.audioPositionMs) && value.audioPositionMs >= 0) && (value.readingPosition === void 0 || typeof value.readingPosition === "number" && Number.isFinite(value.readingPosition) && value.readingPosition >= 0 || typeof value.readingPosition === "string" && value.readingPosition.trim().length > 0) && (value.audioId === void 0 || typeof value.audioId === "string" && value.audioId.length > 0) && (value.language === void 0 || typeof value.language === "string") && (value.contentVersion === void 0 || typeof value.contentVersion === "string");
|
|
461
602
|
}
|
|
462
603
|
function normalizeProgressRecord(record) {
|
|
463
604
|
if (!isProgressRecord(record)) throw new TypeError("KeepKit progress record is invalid.");
|
|
@@ -474,6 +615,7 @@ function normalizeProgressRecord(record) {
|
|
|
474
615
|
itemId: normalizeId2(record.itemId, "item"),
|
|
475
616
|
...record.audioPositionMs !== void 0 ? { audioPositionMs: record.audioPositionMs } : {},
|
|
476
617
|
...record.readingPosition !== void 0 ? { readingPosition: record.readingPosition } : {},
|
|
618
|
+
...record.audioId !== void 0 ? { audioId: record.audioId } : {},
|
|
477
619
|
...record.language !== void 0 ? { language: record.language } : {},
|
|
478
620
|
...record.contentVersion !== void 0 ? { contentVersion: record.contentVersion } : {},
|
|
479
621
|
updatedAt: record.updatedAt
|
|
@@ -969,6 +1111,7 @@ var AuthenticatedSyncStorageController = class {
|
|
|
969
1111
|
function createAuthenticatedRemote(options, scope) {
|
|
970
1112
|
const pull = options.transport.pull;
|
|
971
1113
|
const pullCollections = options.transport.pullCollections;
|
|
1114
|
+
const pullMemberships = options.transport.pullMemberships;
|
|
972
1115
|
return {
|
|
973
1116
|
push: async (operation) => {
|
|
974
1117
|
try {
|
|
@@ -1001,6 +1144,22 @@ function createAuthenticatedRemote(options, scope) {
|
|
|
1001
1144
|
} catch (cause) {
|
|
1002
1145
|
return handleAuthFailure(cause, options, { scope });
|
|
1003
1146
|
}
|
|
1147
|
+
} : void 0,
|
|
1148
|
+
pushMembership: options.transport.pushMembership ? async (membershipOperation) => {
|
|
1149
|
+
try {
|
|
1150
|
+
const token = await options.getAuthToken();
|
|
1151
|
+
await options.transport.pushMembership?.(membershipOperation, { token, scope, membershipOperation });
|
|
1152
|
+
} catch (cause) {
|
|
1153
|
+
return handleAuthFailure(cause, options, { membershipOperation, scope });
|
|
1154
|
+
}
|
|
1155
|
+
} : void 0,
|
|
1156
|
+
pullMemberships: pullMemberships ? async () => {
|
|
1157
|
+
try {
|
|
1158
|
+
const token = await options.getAuthToken();
|
|
1159
|
+
return await pullMemberships({ token, scope });
|
|
1160
|
+
} catch (cause) {
|
|
1161
|
+
return handleAuthFailure(cause, options, { scope });
|
|
1162
|
+
}
|
|
1004
1163
|
} : void 0
|
|
1005
1164
|
};
|
|
1006
1165
|
}
|
|
@@ -1084,6 +1243,7 @@ export {
|
|
|
1084
1243
|
mergeKeepItems,
|
|
1085
1244
|
mergeKeepListQueryFromUrl,
|
|
1086
1245
|
migrateKeepItems,
|
|
1246
|
+
migrateLegacyCollectionMemberships,
|
|
1087
1247
|
moveKeepItem,
|
|
1088
1248
|
normalizeKeepTags,
|
|
1089
1249
|
orderKeepItems,
|