@keepkit/core 0.28.5 → 0.28.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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-5XSSH3TO.js";
23
24
  import {
24
25
  KeepSchemaValidationError,
25
26
  parseKeepMeta,
@@ -53,7 +54,7 @@ import {
53
54
  isKeepSyncAuthError,
54
55
  isSameKeepScope,
55
56
  normalizeKeepTags
56
- } from "./chunk-NZLAW4LY.js";
57
+ } from "./chunk-QRPQYE7K.js";
57
58
 
58
59
  // src/features/items/activity.ts
59
60
  var DEFAULT_KEEP_HISTORY_KEY = "keepkit:view-history";
@@ -61,6 +62,7 @@ var DEFAULT_KEEP_VIEWING_RECORDS_KEY = "keepkit:viewing-records";
61
62
  var DEFAULT_KEEP_HISTORY_LIMIT = 100;
62
63
  var LocalStorageKeepHistoryStorage = class {
63
64
  constructor(options = {}) {
65
+ this.writes = Promise.resolve();
64
66
  this.key = options.key ?? DEFAULT_KEEP_HISTORY_KEY;
65
67
  this.limitKey = `${this.key}:limit`;
66
68
  this.storage = options.storage ?? getBrowserStorage();
@@ -84,32 +86,49 @@ var LocalStorageKeepHistoryStorage = class {
84
86
  }
85
87
  async setLimit(limit) {
86
88
  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));
89
+ await this.withWriteLock(async () => {
90
+ this.writeRaw(this.limitKey, JSON.stringify(normalized), "history-limit");
91
+ const entries = await this.getAll();
92
+ await this.writeEntries(entries.slice(0, normalized));
93
+ });
90
94
  }
91
- async record(itemId, viewedAt = this.now()) {
95
+ async record(itemId, viewedAt = this.now(), context) {
92
96
  const normalizedId = normalizeId(itemId);
93
97
  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);
98
+ return this.withWriteLock(async () => {
99
+ const entries = await this.getAll();
100
+ const previous = entries.find((entry) => entry.itemId === normalizedId);
101
+ const isNewer = viewedAt >= (previous?.lastViewedAt ?? -1);
102
+ const nextContext = isNewer ? normalizeHistoryContext(context) : previous?.context;
103
+ const next = [
104
+ {
105
+ itemId: normalizedId,
106
+ lastViewedAt: Math.max(previous?.lastViewedAt ?? 0, viewedAt),
107
+ ...nextContext ? { context: nextContext } : {}
108
+ },
109
+ ...entries.filter((entry) => entry.itemId !== normalizedId)
110
+ ].sort((left, right) => right.lastViewedAt - left.lastViewedAt).slice(0, await this.getLimit());
111
+ await this.writeEntries(next);
112
+ return cloneHistoryEntries(next);
113
+ });
102
114
  }
103
115
  async remove(itemId) {
104
116
  const normalizedId = normalizeId(itemId);
105
- await this.writeEntries((await this.getAll()).filter((entry) => entry.itemId !== normalizedId));
117
+ await this.withWriteLock(
118
+ async () => this.writeEntries((await this.getAll()).filter((entry) => entry.itemId !== normalizedId))
119
+ );
106
120
  }
107
121
  async clear() {
108
- this.writeRaw(this.key, "[]", "history");
122
+ await this.withWriteLock(async () => this.writeRaw(this.key, "[]", "history"));
109
123
  }
110
124
  async writeEntries(entries) {
111
125
  this.writeRaw(this.key, JSON.stringify(entries), "history");
112
126
  }
127
+ withWriteLock(operation) {
128
+ const next = this.writes.then(operation);
129
+ this.writes = next.catch(() => void 0);
130
+ return next;
131
+ }
113
132
  async read(key, validate, label) {
114
133
  const raw = this.readRaw(key, label);
115
134
  if (raw === null) return [];
@@ -125,8 +144,10 @@ var LocalStorageKeepHistoryStorage = class {
125
144
  }
126
145
  }
127
146
  writeRaw(key, value, label) {
147
+ if (!this.storage)
148
+ throw new KeepActivityStorageError(`KeepKit cannot persist ${label} data without browser storage.`, key);
128
149
  try {
129
- this.storage?.setItem(key, value);
150
+ this.storage.setItem(key, value);
130
151
  } catch (cause) {
131
152
  throw new KeepActivityStorageError(`KeepKit could not write ${label} data.`, key, cause);
132
153
  }
@@ -134,6 +155,7 @@ var LocalStorageKeepHistoryStorage = class {
134
155
  };
135
156
  var LocalStorageKeepViewingRecordStorage = class {
136
157
  constructor(options = {}) {
158
+ this.writes = Promise.resolve();
137
159
  this.key = options.key ?? DEFAULT_KEEP_VIEWING_RECORDS_KEY;
138
160
  this.storage = options.storage ?? getBrowserStorage();
139
161
  this.now = options.now ?? Date.now;
@@ -145,29 +167,48 @@ var LocalStorageKeepViewingRecordStorage = class {
145
167
  return filtered.sort((left, right) => right.viewedAt - left.viewedAt).map((record) => ({ ...record }));
146
168
  }
147
169
  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]);
170
+ await this.saveRecord(record);
151
171
  }
152
172
  async add(itemId, options = {}) {
173
+ const timestamp = this.now();
153
174
  const record = {
154
175
  id: this.createId(),
155
176
  itemId: normalizeId(itemId),
156
- viewedAt: options.viewedAt ?? this.now(),
177
+ viewedAt: options.viewedAt ?? timestamp,
178
+ createdAt: timestamp,
179
+ updatedAt: timestamp,
157
180
  ...options.note?.trim() ? { note: options.note.trim() } : {}
158
181
  };
159
- await this.set(record);
160
- return { ...record };
182
+ return this.saveRecord(record);
183
+ }
184
+ async saveRecord(record) {
185
+ const normalized = normalizeViewingRecord(record);
186
+ return this.withWriteLock(async () => {
187
+ const records = await this.readRecords();
188
+ const previous = records.find((entry) => entry.id === normalized.id);
189
+ const now = this.now();
190
+ const next = {
191
+ ...normalized,
192
+ createdAt: previous?.createdAt ?? normalized.createdAt ?? now,
193
+ updatedAt: now
194
+ };
195
+ this.writeRecords([...records.filter((entry) => entry.id !== normalized.id), next]);
196
+ return { ...next };
197
+ });
161
198
  }
162
199
  async remove(id) {
163
- this.writeRecords((await this.readRecords()).filter((record) => record.id !== id));
200
+ await this.withWriteLock(
201
+ async () => this.writeRecords((await this.readRecords()).filter((record) => record.id !== id))
202
+ );
164
203
  }
165
204
  async removeForItem(itemId) {
166
205
  const normalizedId = normalizeId(itemId);
167
- this.writeRecords((await this.readRecords()).filter((record) => record.itemId !== normalizedId));
206
+ await this.withWriteLock(
207
+ async () => this.writeRecords((await this.readRecords()).filter((record) => record.itemId !== normalizedId))
208
+ );
168
209
  }
169
210
  async clear() {
170
- this.writeRecords([]);
211
+ await this.withWriteLock(async () => this.writeRecords([]));
171
212
  }
172
213
  async readRecords() {
173
214
  const raw = this.readRaw();
@@ -186,12 +227,20 @@ var LocalStorageKeepViewingRecordStorage = class {
186
227
  }
187
228
  }
188
229
  writeRecords(records) {
230
+ if (!this.storage) {
231
+ throw new KeepActivityStorageError("KeepKit cannot persist viewing records without browser storage.", this.key);
232
+ }
189
233
  try {
190
- this.storage?.setItem(this.key, JSON.stringify(records));
234
+ this.storage.setItem(this.key, JSON.stringify(records));
191
235
  } catch (cause) {
192
236
  throw new KeepActivityStorageError("KeepKit could not write viewing records.", this.key, cause);
193
237
  }
194
238
  }
239
+ withWriteLock(operation) {
240
+ const next = this.writes.then(operation);
241
+ this.writes = next.catch(() => void 0);
242
+ return next;
243
+ }
195
244
  };
196
245
  var KeepActivityStorageError = class extends Error {
197
246
  constructor(message, storageKey, cause) {
@@ -203,20 +252,22 @@ var KeepActivityStorageError = class extends Error {
203
252
  };
204
253
  function isHistoryEntryArray(value) {
205
254
  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)
255
+ (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
256
  );
208
257
  }
209
258
  function isViewingRecordArray(value) {
210
259
  return Array.isArray(value) && value.every(isViewingRecord);
211
260
  }
212
261
  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");
262
+ 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
263
  }
215
264
  function normalizeViewingRecord(record) {
216
265
  const normalized = {
217
266
  id: normalizeId(record.id),
218
267
  itemId: normalizeId(record.itemId),
219
268
  viewedAt: record.viewedAt,
269
+ ...record.createdAt !== void 0 ? { createdAt: record.createdAt } : {},
270
+ ...record.updatedAt !== void 0 ? { updatedAt: record.updatedAt } : {},
220
271
  ...record.note?.trim() ? { note: record.note.trim() } : {}
221
272
  };
222
273
  assertTimestamp(normalized.viewedAt);
@@ -235,7 +286,15 @@ function assertTimestamp(value) {
235
286
  if (!Number.isFinite(value) || value < 0) throw new TypeError("KeepKit timestamps must be non-negative numbers.");
236
287
  }
237
288
  function cloneHistoryEntries(entries) {
238
- return entries.map((entry) => ({ ...entry }));
289
+ return entries.map((entry) => ({ ...entry, ...entry.context ? { context: { ...entry.context } } : {} }));
290
+ }
291
+ function normalizeHistoryContext(context) {
292
+ if (!context) return void 0;
293
+ const entries = Object.entries(context).filter(([key, value]) => key.trim() && typeof value === "string");
294
+ return entries.length > 0 ? Object.fromEntries(entries) : void 0;
295
+ }
296
+ function isHistoryContext(value) {
297
+ return isRecord(value) && Object.values(value).every((entry) => typeof entry === "string");
239
298
  }
240
299
  function parseJson(raw, key, label) {
241
300
  try {
@@ -352,6 +411,7 @@ var createKeepKitSetup = createKeepKitPreset;
352
411
  var DEFAULT_KEEP_PROGRESS_KEY = "keepkit:progress";
353
412
  var LocalStorageKeepProgressStorage = class {
354
413
  constructor(options = {}) {
414
+ this.writes = Promise.resolve();
355
415
  this.key = options.key ?? DEFAULT_KEEP_PROGRESS_KEY;
356
416
  this.storage = options.storage ?? getBrowserStorage2();
357
417
  this.now = options.now ?? Date.now;
@@ -397,27 +457,34 @@ var LocalStorageKeepProgressStorage = class {
397
457
  kind: "item",
398
458
  itemId: normalizeId2(itemId, "item"),
399
459
  ...progress,
460
+ ...progress.audioId?.trim() ? { audioId: progress.audioId.trim() } : {},
400
461
  updatedAt: this.now()
401
462
  };
402
463
  await this.set(record);
403
464
  return record;
404
465
  }
405
466
  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]);
467
+ await this.withWriteLock(async () => {
468
+ const normalized = normalizeProgressRecord(record);
469
+ const records = await this.getAll();
470
+ const identity = getProgressIdentity(normalized);
471
+ this.writeRecords([...records.filter((entry) => getProgressIdentity(entry) !== identity), normalized]);
472
+ });
410
473
  }
411
474
  async resetCourse(courseId) {
412
475
  const id = normalizeId2(courseId, "course");
413
- this.writeRecords((await this.getAll()).filter((entry) => entry.kind !== "course" || entry.courseId !== id));
476
+ await this.withWriteLock(
477
+ async () => this.writeRecords((await this.getAll()).filter((entry) => entry.kind !== "course" || entry.courseId !== id))
478
+ );
414
479
  }
415
480
  async resetItem(itemId) {
416
481
  const id = normalizeId2(itemId, "item");
417
- this.writeRecords((await this.getAll()).filter((entry) => entry.kind !== "item" || entry.itemId !== id));
482
+ await this.withWriteLock(
483
+ async () => this.writeRecords((await this.getAll()).filter((entry) => entry.kind !== "item" || entry.itemId !== id))
484
+ );
418
485
  }
419
486
  async clear() {
420
- this.writeRecords([]);
487
+ await this.withWriteLock(async () => this.writeRecords([]));
421
488
  }
422
489
  readRaw() {
423
490
  try {
@@ -427,12 +494,20 @@ var LocalStorageKeepProgressStorage = class {
427
494
  }
428
495
  }
429
496
  writeRecords(records) {
497
+ if (!this.storage) {
498
+ throw new KeepProgressStorageError("KeepKit cannot persist progress without browser storage.", this.key);
499
+ }
430
500
  try {
431
- this.storage?.setItem(this.key, JSON.stringify(records));
501
+ this.storage.setItem(this.key, JSON.stringify(records));
432
502
  } catch (cause) {
433
503
  throw new KeepProgressStorageError("KeepKit could not write progress data.", this.key, cause);
434
504
  }
435
505
  }
506
+ withWriteLock(operation) {
507
+ const next = this.writes.then(operation);
508
+ this.writes = next.catch(() => void 0);
509
+ return next;
510
+ }
436
511
  };
437
512
  function isProgressCompatible(record, compatibility) {
438
513
  if (record.language !== void 0 && record.language !== compatibility?.language) return false;
@@ -440,6 +515,8 @@ function isProgressCompatible(record, compatibility) {
440
515
  if (compatibility?.language !== void 0 && record.language !== compatibility.language) return false;
441
516
  if (compatibility?.contentVersion !== void 0 && record.contentVersion !== compatibility.contentVersion)
442
517
  return false;
518
+ if (record.audioId !== void 0 && record.audioId !== compatibility?.audioId) return false;
519
+ if (compatibility?.audioId !== void 0 && record.audioId !== compatibility.audioId) return false;
443
520
  return true;
444
521
  }
445
522
  var KeepProgressStorageError = class extends Error {
@@ -457,7 +534,7 @@ function isProgressRecord(value) {
457
534
  return typeof value.courseId === "string" && value.courseId.length > 0 && (value.currentItemId === void 0 || typeof value.currentItemId === "string");
458
535
  }
459
536
  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");
537
+ 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
538
  }
462
539
  function normalizeProgressRecord(record) {
463
540
  if (!isProgressRecord(record)) throw new TypeError("KeepKit progress record is invalid.");
@@ -474,6 +551,7 @@ function normalizeProgressRecord(record) {
474
551
  itemId: normalizeId2(record.itemId, "item"),
475
552
  ...record.audioPositionMs !== void 0 ? { audioPositionMs: record.audioPositionMs } : {},
476
553
  ...record.readingPosition !== void 0 ? { readingPosition: record.readingPosition } : {},
554
+ ...record.audioId !== void 0 ? { audioId: record.audioId } : {},
477
555
  ...record.language !== void 0 ? { language: record.language } : {},
478
556
  ...record.contentVersion !== void 0 ? { contentVersion: record.contentVersion } : {},
479
557
  updatedAt: record.updatedAt
@@ -969,6 +1047,7 @@ var AuthenticatedSyncStorageController = class {
969
1047
  function createAuthenticatedRemote(options, scope) {
970
1048
  const pull = options.transport.pull;
971
1049
  const pullCollections = options.transport.pullCollections;
1050
+ const pullMemberships = options.transport.pullMemberships;
972
1051
  return {
973
1052
  push: async (operation) => {
974
1053
  try {
@@ -1001,6 +1080,22 @@ function createAuthenticatedRemote(options, scope) {
1001
1080
  } catch (cause) {
1002
1081
  return handleAuthFailure(cause, options, { scope });
1003
1082
  }
1083
+ } : void 0,
1084
+ pushMembership: options.transport.pushMembership ? async (membershipOperation) => {
1085
+ try {
1086
+ const token = await options.getAuthToken();
1087
+ await options.transport.pushMembership?.(membershipOperation, { token, scope, membershipOperation });
1088
+ } catch (cause) {
1089
+ return handleAuthFailure(cause, options, { membershipOperation, scope });
1090
+ }
1091
+ } : void 0,
1092
+ pullMemberships: pullMemberships ? async () => {
1093
+ try {
1094
+ const token = await options.getAuthToken();
1095
+ return await pullMemberships({ token, scope });
1096
+ } catch (cause) {
1097
+ return handleAuthFailure(cause, options, { scope });
1098
+ }
1004
1099
  } : void 0
1005
1100
  };
1006
1101
  }
@@ -1084,6 +1179,7 @@ export {
1084
1179
  mergeKeepItems,
1085
1180
  mergeKeepListQueryFromUrl,
1086
1181
  migrateKeepItems,
1182
+ migrateLegacyCollectionMemberships,
1087
1183
  moveKeepItem,
1088
1184
  normalizeKeepTags,
1089
1185
  orderKeepItems,