@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/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-AUNFZU3A.js";
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-NZLAW4LY.js";
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.writeRaw(this.limitKey, JSON.stringify(normalized), "history-limit");
88
- const entries = await this.getAll();
89
- await this.writeEntries(entries.slice(0, normalized));
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
- const entries = await this.getAll();
95
- const previous = entries.find((entry) => entry.itemId === normalizedId);
96
- const next = [
97
- { itemId: normalizedId, lastViewedAt: Math.max(previous?.lastViewedAt ?? 0, viewedAt) },
98
- ...entries.filter((entry) => entry.itemId !== normalizedId)
99
- ].sort((left, right) => right.lastViewedAt - left.lastViewedAt).slice(0, await this.getLimit());
100
- await this.writeEntries(next);
101
- return cloneHistoryEntries(next);
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.writeEntries((await this.getAll()).filter((entry) => entry.itemId !== normalizedId));
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?.setItem(key, value);
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
- const normalized = normalizeViewingRecord(record);
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 ?? this.now(),
194
+ viewedAt: options.viewedAt ?? timestamp,
195
+ createdAt: timestamp,
196
+ updatedAt: timestamp,
157
197
  ...options.note?.trim() ? { note: options.note.trim() } : {}
158
198
  };
159
- await this.set(record);
160
- return { ...record };
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
- this.writeRecords((await this.readRecords()).filter((record) => record.id !== id));
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
- this.writeRecords((await this.readRecords()).filter((record) => record.itemId !== normalizedId));
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?.setItem(this.key, JSON.stringify(records));
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
- if (!(await storage.getAll()).some((item) => item.id === itemId)) {
275
- throw new Error(`KeepKit cannot add missing item "${itemId}" to a collection.`);
276
- }
277
- const collectionMemberships = (await requireMemberships(storage)).filter((membership) => membership.collectionId === collectionId).filter((membership) => membership.itemId !== itemId).sort((left, right) => left.order - right.order);
278
- const insertionIndex = order ?? collectionMemberships.length;
279
- if (!Number.isInteger(insertionIndex) || insertionIndex < 0) {
280
- throw new RangeError("Collection order must be a non-negative integer.");
281
- }
282
- collectionMemberships.splice(Math.min(insertionIndex, collectionMemberships.length), 0, {
283
- collectionId,
284
- itemId,
285
- order: insertionIndex
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
- const memberships = await requireMemberships(storage);
295
- await storage.removeCollectionMembership?.(collectionId, itemId);
296
- const remaining = memberships.filter((membership) => membership.collectionId === collectionId && membership.itemId !== itemId).sort((left, right) => left.order - right.order).map((membership, order) => ({ ...membership, order }));
297
- for (const membership of remaining) await storage.setCollectionMembership?.(membership);
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
- const allMemberships = await requireMemberships(storage);
301
- const current = allMemberships.filter((membership) => membership.collectionId === collectionId).sort((left, right) => left.order - right.order);
302
- const currentIds = new Set(current.map((membership) => membership.itemId));
303
- const orderedIds = [...itemIds.filter((id, index) => currentIds.has(id) && itemIds.indexOf(id) === index)];
304
- for (const membership of current) {
305
- if (!orderedIds.includes(membership.itemId)) orderedIds.push(membership.itemId);
306
- }
307
- const currentByItemId = new Map(current.map((membership) => [membership.itemId, membership]));
308
- const reordered = orderedIds.map((itemId, order) => ({
309
- ...currentByItemId.get(itemId) ?? {},
310
- collectionId,
311
- itemId,
312
- order
313
- }));
314
- for (const membership of reordered) await storage.setCollectionMembership?.(membership);
315
- return reordered;
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 record = {
387
- kind: "course",
388
- courseId: normalizeId2(courseId, "course"),
389
- ...currentItemId?.trim() ? { currentItemId: currentItemId.trim() } : {},
390
- updatedAt: this.now()
391
- };
392
- await this.set(record);
393
- return record;
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 record = {
397
- kind: "item",
398
- itemId: normalizeId2(itemId, "item"),
399
- ...progress,
400
- updatedAt: this.now()
401
- };
402
- await this.set(record);
403
- return record;
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
- const normalized = normalizeProgressRecord(record);
407
- const records = await this.getAll();
408
- const identity = getProgressIdentity(normalized);
409
- this.writeRecords([...records.filter((entry) => getProgressIdentity(entry) !== identity), normalized]);
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
- this.writeRecords((await this.getAll()).filter((entry) => entry.kind !== "course" || entry.courseId !== id));
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
- this.writeRecords((await this.getAll()).filter((entry) => entry.kind !== "item" || entry.itemId !== id));
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?.setItem(this.key, JSON.stringify(records));
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,