@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/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-Y4U5DEGG.js";
23
+ } from "./chunk-5XSSH3TO.js";
23
24
  import {
24
25
  KeepSchemaValidationError,
25
26
  parseKeepMeta,
@@ -53,7 +54,331 @@ import {
53
54
  isKeepSyncAuthError,
54
55
  isSameKeepScope,
55
56
  normalizeKeepTags
56
- } from "./chunk-2TENUU7B.js";
57
+ } from "./chunk-QRPQYE7K.js";
58
+
59
+ // src/features/items/activity.ts
60
+ var DEFAULT_KEEP_HISTORY_KEY = "keepkit:view-history";
61
+ var DEFAULT_KEEP_VIEWING_RECORDS_KEY = "keepkit:viewing-records";
62
+ var DEFAULT_KEEP_HISTORY_LIMIT = 100;
63
+ var LocalStorageKeepHistoryStorage = class {
64
+ constructor(options = {}) {
65
+ this.writes = Promise.resolve();
66
+ this.key = options.key ?? DEFAULT_KEEP_HISTORY_KEY;
67
+ this.limitKey = `${this.key}:limit`;
68
+ this.storage = options.storage ?? getBrowserStorage();
69
+ this.initialLimit = normalizeLimit(options.maxEntries ?? DEFAULT_KEEP_HISTORY_LIMIT);
70
+ this.now = options.now ?? Date.now;
71
+ }
72
+ async getAll() {
73
+ return (await this.read(this.key, isHistoryEntryArray, "history")).sort(
74
+ (left, right) => right.lastViewedAt - left.lastViewedAt
75
+ );
76
+ }
77
+ async getLimit() {
78
+ if (!this.storage) return this.initialLimit;
79
+ const raw = this.readRaw(this.limitKey, "history-limit");
80
+ if (raw === null) return this.initialLimit;
81
+ const parsed = parseJson(raw, this.limitKey, "history-limit");
82
+ if (typeof parsed !== "number" || !Number.isInteger(parsed) || parsed < 1) {
83
+ throw new KeepActivityStorageError("KeepKit history limit is invalid.", this.limitKey);
84
+ }
85
+ return parsed;
86
+ }
87
+ async setLimit(limit) {
88
+ const normalized = normalizeLimit(limit);
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
+ });
94
+ }
95
+ async record(itemId, viewedAt = this.now(), context) {
96
+ const normalizedId = normalizeId(itemId);
97
+ assertTimestamp(viewedAt);
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
+ });
114
+ }
115
+ async remove(itemId) {
116
+ const normalizedId = normalizeId(itemId);
117
+ await this.withWriteLock(
118
+ async () => this.writeEntries((await this.getAll()).filter((entry) => entry.itemId !== normalizedId))
119
+ );
120
+ }
121
+ async clear() {
122
+ await this.withWriteLock(async () => this.writeRaw(this.key, "[]", "history"));
123
+ }
124
+ async writeEntries(entries) {
125
+ this.writeRaw(this.key, JSON.stringify(entries), "history");
126
+ }
127
+ withWriteLock(operation) {
128
+ const next = this.writes.then(operation);
129
+ this.writes = next.catch(() => void 0);
130
+ return next;
131
+ }
132
+ async read(key, validate, label) {
133
+ const raw = this.readRaw(key, label);
134
+ if (raw === null) return [];
135
+ const value = parseJson(raw, key, label);
136
+ if (!validate(value)) throw new KeepActivityStorageError(`KeepKit ${label} data is invalid.`, key);
137
+ return value;
138
+ }
139
+ readRaw(key, label) {
140
+ try {
141
+ return this.storage?.getItem(key) ?? null;
142
+ } catch (cause) {
143
+ throw new KeepActivityStorageError(`KeepKit could not read ${label} data.`, key, cause);
144
+ }
145
+ }
146
+ writeRaw(key, value, label) {
147
+ if (!this.storage)
148
+ throw new KeepActivityStorageError(`KeepKit cannot persist ${label} data without browser storage.`, key);
149
+ try {
150
+ this.storage.setItem(key, value);
151
+ } catch (cause) {
152
+ throw new KeepActivityStorageError(`KeepKit could not write ${label} data.`, key, cause);
153
+ }
154
+ }
155
+ };
156
+ var LocalStorageKeepViewingRecordStorage = class {
157
+ constructor(options = {}) {
158
+ this.writes = Promise.resolve();
159
+ this.key = options.key ?? DEFAULT_KEEP_VIEWING_RECORDS_KEY;
160
+ this.storage = options.storage ?? getBrowserStorage();
161
+ this.now = options.now ?? Date.now;
162
+ this.createId = options.createId ?? createRecordId;
163
+ }
164
+ async getAll(itemId) {
165
+ const records = await this.readRecords();
166
+ const filtered = itemId === void 0 ? records : records.filter((record) => record.itemId === itemId);
167
+ return filtered.sort((left, right) => right.viewedAt - left.viewedAt).map((record) => ({ ...record }));
168
+ }
169
+ async set(record) {
170
+ await this.saveRecord(record);
171
+ }
172
+ async add(itemId, options = {}) {
173
+ const timestamp = this.now();
174
+ const record = {
175
+ id: this.createId(),
176
+ itemId: normalizeId(itemId),
177
+ viewedAt: options.viewedAt ?? timestamp,
178
+ createdAt: timestamp,
179
+ updatedAt: timestamp,
180
+ ...options.note?.trim() ? { note: options.note.trim() } : {}
181
+ };
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
+ });
198
+ }
199
+ async remove(id) {
200
+ await this.withWriteLock(
201
+ async () => this.writeRecords((await this.readRecords()).filter((record) => record.id !== id))
202
+ );
203
+ }
204
+ async removeForItem(itemId) {
205
+ const normalizedId = normalizeId(itemId);
206
+ await this.withWriteLock(
207
+ async () => this.writeRecords((await this.readRecords()).filter((record) => record.itemId !== normalizedId))
208
+ );
209
+ }
210
+ async clear() {
211
+ await this.withWriteLock(async () => this.writeRecords([]));
212
+ }
213
+ async readRecords() {
214
+ const raw = this.readRaw();
215
+ if (raw === null) return [];
216
+ const value = parseJson(raw, this.key, "viewing records");
217
+ if (!isViewingRecordArray(value)) {
218
+ throw new KeepActivityStorageError("KeepKit viewing record data is invalid.", this.key);
219
+ }
220
+ return value;
221
+ }
222
+ readRaw() {
223
+ try {
224
+ return this.storage?.getItem(this.key) ?? null;
225
+ } catch (cause) {
226
+ throw new KeepActivityStorageError("KeepKit could not read viewing records.", this.key, cause);
227
+ }
228
+ }
229
+ writeRecords(records) {
230
+ if (!this.storage) {
231
+ throw new KeepActivityStorageError("KeepKit cannot persist viewing records without browser storage.", this.key);
232
+ }
233
+ try {
234
+ this.storage.setItem(this.key, JSON.stringify(records));
235
+ } catch (cause) {
236
+ throw new KeepActivityStorageError("KeepKit could not write viewing records.", this.key, cause);
237
+ }
238
+ }
239
+ withWriteLock(operation) {
240
+ const next = this.writes.then(operation);
241
+ this.writes = next.catch(() => void 0);
242
+ return next;
243
+ }
244
+ };
245
+ var KeepActivityStorageError = class extends Error {
246
+ constructor(message, storageKey, cause) {
247
+ super(message);
248
+ this.name = "KeepActivityStorageError";
249
+ this.storageKey = storageKey;
250
+ this.cause = cause;
251
+ }
252
+ };
253
+ function isHistoryEntryArray(value) {
254
+ return Array.isArray(value) && value.every(
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))
256
+ );
257
+ }
258
+ function isViewingRecordArray(value) {
259
+ return Array.isArray(value) && value.every(isViewingRecord);
260
+ }
261
+ function isViewingRecord(value) {
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");
263
+ }
264
+ function normalizeViewingRecord(record) {
265
+ const normalized = {
266
+ id: normalizeId(record.id),
267
+ itemId: normalizeId(record.itemId),
268
+ viewedAt: record.viewedAt,
269
+ ...record.createdAt !== void 0 ? { createdAt: record.createdAt } : {},
270
+ ...record.updatedAt !== void 0 ? { updatedAt: record.updatedAt } : {},
271
+ ...record.note?.trim() ? { note: record.note.trim() } : {}
272
+ };
273
+ assertTimestamp(normalized.viewedAt);
274
+ return normalized;
275
+ }
276
+ function normalizeId(value) {
277
+ const normalized = value.trim();
278
+ if (!normalized) throw new TypeError("KeepKit item ids must not be empty.");
279
+ return normalized;
280
+ }
281
+ function normalizeLimit(value) {
282
+ if (!Number.isInteger(value) || value < 1) throw new RangeError("KeepKit history limit must be a positive integer.");
283
+ return value;
284
+ }
285
+ function assertTimestamp(value) {
286
+ if (!Number.isFinite(value) || value < 0) throw new TypeError("KeepKit timestamps must be non-negative numbers.");
287
+ }
288
+ function cloneHistoryEntries(entries) {
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");
298
+ }
299
+ function parseJson(raw, key, label) {
300
+ try {
301
+ return JSON.parse(raw);
302
+ } catch (cause) {
303
+ throw new KeepActivityStorageError(`KeepKit ${label} data is not valid JSON.`, key, cause);
304
+ }
305
+ }
306
+ function isRecord(value) {
307
+ return typeof value === "object" && value !== null && !Array.isArray(value);
308
+ }
309
+ function getBrowserStorage() {
310
+ if (typeof window === "undefined") return void 0;
311
+ try {
312
+ return window.localStorage;
313
+ } catch {
314
+ return void 0;
315
+ }
316
+ }
317
+ function createRecordId() {
318
+ if (typeof crypto !== "undefined" && "randomUUID" in crypto) return crypto.randomUUID();
319
+ return `keepkit-view-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
320
+ }
321
+
322
+ // src/features/items/collections.ts
323
+ async function getKeepCollectionItems(storage, collectionId) {
324
+ const memberships = await requireMemberships(storage);
325
+ const items = await storage.getAll();
326
+ const itemsById = new Map(items.map((item) => [item.id, item]));
327
+ return memberships.filter((membership) => membership.collectionId === collectionId).sort((left, right) => left.order - right.order).flatMap((membership) => {
328
+ const item = itemsById.get(membership.itemId);
329
+ return item ? [item] : [];
330
+ });
331
+ }
332
+ async function addKeepItemToCollection(storage, collectionId, itemId, order) {
333
+ if (!(await storage.getAll()).some((item) => item.id === itemId)) {
334
+ throw new Error(`KeepKit cannot add missing item "${itemId}" to a collection.`);
335
+ }
336
+ const collectionMemberships = (await requireMemberships(storage)).filter((membership) => membership.collectionId === collectionId).filter((membership) => membership.itemId !== itemId).sort((left, right) => left.order - right.order);
337
+ const insertionIndex = order ?? collectionMemberships.length;
338
+ if (!Number.isInteger(insertionIndex) || insertionIndex < 0) {
339
+ throw new RangeError("Collection order must be a non-negative integer.");
340
+ }
341
+ collectionMemberships.splice(Math.min(insertionIndex, collectionMemberships.length), 0, {
342
+ collectionId,
343
+ itemId,
344
+ order: insertionIndex
345
+ });
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
+ }
352
+ async function removeKeepItemFromCollection(storage, collectionId, itemId) {
353
+ const memberships = await requireMemberships(storage);
354
+ await storage.removeCollectionMembership?.(collectionId, itemId);
355
+ const remaining = memberships.filter((membership) => membership.collectionId === collectionId && membership.itemId !== itemId).sort((left, right) => left.order - right.order).map((membership, order) => ({ ...membership, order }));
356
+ for (const membership of remaining) await storage.setCollectionMembership?.(membership);
357
+ }
358
+ async function reorderKeepCollectionItems(storage, collectionId, itemIds) {
359
+ const allMemberships = await requireMemberships(storage);
360
+ const current = allMemberships.filter((membership) => membership.collectionId === collectionId).sort((left, right) => left.order - right.order);
361
+ const currentIds = new Set(current.map((membership) => membership.itemId));
362
+ const orderedIds = [...itemIds.filter((id, index) => currentIds.has(id) && itemIds.indexOf(id) === index)];
363
+ for (const membership of current) {
364
+ if (!orderedIds.includes(membership.itemId)) orderedIds.push(membership.itemId);
365
+ }
366
+ const currentByItemId = new Map(current.map((membership) => [membership.itemId, membership]));
367
+ const reordered = orderedIds.map((itemId, order) => ({
368
+ ...currentByItemId.get(itemId) ?? {},
369
+ collectionId,
370
+ itemId,
371
+ order
372
+ }));
373
+ for (const membership of reordered) await storage.setCollectionMembership?.(membership);
374
+ return reordered;
375
+ }
376
+ async function requireMemberships(storage) {
377
+ if (!storage.getCollectionMemberships || !storage.setCollectionMembership || !storage.removeCollectionMembership) {
378
+ throw new Error("KeepKit storage adapter does not support collection memberships.");
379
+ }
380
+ return storage.getCollectionMemberships();
381
+ }
57
382
 
58
383
  // src/features/items/presets.ts
59
384
  function createKeepKitPreset(options = {}) {
@@ -82,6 +407,176 @@ function createKeepKitPreset(options = {}) {
82
407
  }
83
408
  var createKeepKitSetup = createKeepKitPreset;
84
409
 
410
+ // src/features/items/progress.ts
411
+ var DEFAULT_KEEP_PROGRESS_KEY = "keepkit:progress";
412
+ var LocalStorageKeepProgressStorage = class {
413
+ constructor(options = {}) {
414
+ this.writes = Promise.resolve();
415
+ this.key = options.key ?? DEFAULT_KEEP_PROGRESS_KEY;
416
+ this.storage = options.storage ?? getBrowserStorage2();
417
+ this.now = options.now ?? Date.now;
418
+ }
419
+ async getAll() {
420
+ const raw = this.readRaw();
421
+ if (raw === null) return [];
422
+ let value;
423
+ try {
424
+ value = JSON.parse(raw);
425
+ } catch (cause) {
426
+ throw new KeepProgressStorageError("KeepKit progress data is not valid JSON.", this.key, cause);
427
+ }
428
+ if (!Array.isArray(value) || !value.every(isProgressRecord)) {
429
+ throw new KeepProgressStorageError("KeepKit progress data is invalid.", this.key);
430
+ }
431
+ return value.map((record) => ({ ...record }));
432
+ }
433
+ async getCourse(courseId) {
434
+ const id = normalizeId2(courseId, "course");
435
+ const record = (await this.getAll()).find((entry) => entry.kind === "course" && entry.courseId === id);
436
+ return record?.kind === "course" ? record : void 0;
437
+ }
438
+ async getItem(itemId, compatibility) {
439
+ const id = normalizeId2(itemId, "item");
440
+ const record = (await this.getAll()).find((entry) => entry.kind === "item" && entry.itemId === id);
441
+ if (record?.kind !== "item") return void 0;
442
+ if (!isProgressCompatible(record, compatibility)) return void 0;
443
+ return record;
444
+ }
445
+ async saveCourse(courseId, currentItemId) {
446
+ const record = {
447
+ kind: "course",
448
+ courseId: normalizeId2(courseId, "course"),
449
+ ...currentItemId?.trim() ? { currentItemId: currentItemId.trim() } : {},
450
+ updatedAt: this.now()
451
+ };
452
+ await this.set(record);
453
+ return record;
454
+ }
455
+ async saveItem(itemId, progress) {
456
+ const record = {
457
+ kind: "item",
458
+ itemId: normalizeId2(itemId, "item"),
459
+ ...progress,
460
+ ...progress.audioId?.trim() ? { audioId: progress.audioId.trim() } : {},
461
+ updatedAt: this.now()
462
+ };
463
+ await this.set(record);
464
+ return record;
465
+ }
466
+ async set(record) {
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
+ });
473
+ }
474
+ async resetCourse(courseId) {
475
+ const id = normalizeId2(courseId, "course");
476
+ await this.withWriteLock(
477
+ async () => this.writeRecords((await this.getAll()).filter((entry) => entry.kind !== "course" || entry.courseId !== id))
478
+ );
479
+ }
480
+ async resetItem(itemId) {
481
+ const id = normalizeId2(itemId, "item");
482
+ await this.withWriteLock(
483
+ async () => this.writeRecords((await this.getAll()).filter((entry) => entry.kind !== "item" || entry.itemId !== id))
484
+ );
485
+ }
486
+ async clear() {
487
+ await this.withWriteLock(async () => this.writeRecords([]));
488
+ }
489
+ readRaw() {
490
+ try {
491
+ return this.storage?.getItem(this.key) ?? null;
492
+ } catch (cause) {
493
+ throw new KeepProgressStorageError("KeepKit could not read progress data.", this.key, cause);
494
+ }
495
+ }
496
+ writeRecords(records) {
497
+ if (!this.storage) {
498
+ throw new KeepProgressStorageError("KeepKit cannot persist progress without browser storage.", this.key);
499
+ }
500
+ try {
501
+ this.storage.setItem(this.key, JSON.stringify(records));
502
+ } catch (cause) {
503
+ throw new KeepProgressStorageError("KeepKit could not write progress data.", this.key, cause);
504
+ }
505
+ }
506
+ withWriteLock(operation) {
507
+ const next = this.writes.then(operation);
508
+ this.writes = next.catch(() => void 0);
509
+ return next;
510
+ }
511
+ };
512
+ function isProgressCompatible(record, compatibility) {
513
+ if (record.language !== void 0 && record.language !== compatibility?.language) return false;
514
+ if (record.contentVersion !== void 0 && record.contentVersion !== compatibility?.contentVersion) return false;
515
+ if (compatibility?.language !== void 0 && record.language !== compatibility.language) return false;
516
+ if (compatibility?.contentVersion !== void 0 && record.contentVersion !== compatibility.contentVersion)
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;
520
+ return true;
521
+ }
522
+ var KeepProgressStorageError = class extends Error {
523
+ constructor(message, storageKey, cause) {
524
+ super(message);
525
+ this.name = "KeepProgressStorageError";
526
+ this.storageKey = storageKey;
527
+ this.cause = cause;
528
+ }
529
+ };
530
+ function isProgressRecord(value) {
531
+ if (!isRecord2(value) || typeof value.updatedAt !== "number" || !Number.isFinite(value.updatedAt) || value.updatedAt < 0)
532
+ return false;
533
+ if (value.kind === "course") {
534
+ return typeof value.courseId === "string" && value.courseId.length > 0 && (value.currentItemId === void 0 || typeof value.currentItemId === "string");
535
+ }
536
+ if (value.kind !== "item") return false;
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");
538
+ }
539
+ function normalizeProgressRecord(record) {
540
+ if (!isProgressRecord(record)) throw new TypeError("KeepKit progress record is invalid.");
541
+ if (record.kind === "course") {
542
+ return {
543
+ kind: "course",
544
+ courseId: normalizeId2(record.courseId, "course"),
545
+ ...record.currentItemId?.trim() ? { currentItemId: record.currentItemId.trim() } : {},
546
+ updatedAt: record.updatedAt
547
+ };
548
+ }
549
+ return {
550
+ kind: "item",
551
+ itemId: normalizeId2(record.itemId, "item"),
552
+ ...record.audioPositionMs !== void 0 ? { audioPositionMs: record.audioPositionMs } : {},
553
+ ...record.readingPosition !== void 0 ? { readingPosition: record.readingPosition } : {},
554
+ ...record.audioId !== void 0 ? { audioId: record.audioId } : {},
555
+ ...record.language !== void 0 ? { language: record.language } : {},
556
+ ...record.contentVersion !== void 0 ? { contentVersion: record.contentVersion } : {},
557
+ updatedAt: record.updatedAt
558
+ };
559
+ }
560
+ function getProgressIdentity(record) {
561
+ return record.kind === "course" ? `course:${record.courseId}` : `item:${record.itemId}`;
562
+ }
563
+ function normalizeId2(value, kind) {
564
+ const normalized = value.trim();
565
+ if (!normalized) throw new TypeError(`KeepKit ${kind} ids must not be empty.`);
566
+ return normalized;
567
+ }
568
+ function isRecord2(value) {
569
+ return typeof value === "object" && value !== null && !Array.isArray(value);
570
+ }
571
+ function getBrowserStorage2() {
572
+ if (typeof window === "undefined") return void 0;
573
+ try {
574
+ return window.localStorage;
575
+ } catch {
576
+ return void 0;
577
+ }
578
+ }
579
+
85
580
  // src/features/items/saved-views.ts
86
581
  var DEFAULT_SAVED_VIEWS_STORAGE_KEY = "keepkit:saved-views";
87
582
  var LocalStorageKeepSavedViewStorage = class {
@@ -149,13 +644,13 @@ function isKeepListQuery(value) {
149
644
  }
150
645
  if (query.pinnedFirst !== void 0 && typeof query.pinnedFirst !== "boolean") return false;
151
646
  if (query.sort !== void 0) {
152
- if (!isRecord(query.sort) || !["savedAt", "updatedAt", "lastOpenedAt"].includes(String(query.sort.by)))
647
+ if (!isRecord3(query.sort) || !["savedAt", "updatedAt", "lastOpenedAt"].includes(String(query.sort.by)))
153
648
  return false;
154
649
  if (query.sort.direction !== void 0 && query.sort.direction !== "asc" && query.sort.direction !== "desc")
155
650
  return false;
156
651
  }
157
652
  if (query.search !== void 0) {
158
- if (!isRecord(query.search)) return false;
653
+ if (!isRecord3(query.search)) return false;
159
654
  if (query.search.query !== void 0 && typeof query.search.query !== "string") return false;
160
655
  if (query.search.mode !== void 0 && query.search.mode !== "and" && query.search.mode !== "or") return false;
161
656
  if (query.search.tokenize !== void 0 && typeof query.search.tokenize !== "boolean") return false;
@@ -164,7 +659,7 @@ function isKeepListQuery(value) {
164
659
  }
165
660
  }
166
661
  if (query.activity !== void 0) {
167
- if (!isRecord(query.activity)) return false;
662
+ if (!isRecord3(query.activity)) return false;
168
663
  if (query.activity.opened !== void 0 && query.activity.opened !== "ever" && query.activity.opened !== "never") {
169
664
  return false;
170
665
  }
@@ -178,7 +673,7 @@ function isKeepListQuery(value) {
178
673
  return false;
179
674
  }
180
675
  if (query.pagination !== void 0) {
181
- if (!isRecord(query.pagination) || [query.pagination.page, query.pagination.pageSize].some(isInvalidNumber))
676
+ if (!isRecord3(query.pagination) || [query.pagination.page, query.pagination.pageSize].some(isInvalidNumber))
182
677
  return false;
183
678
  }
184
679
  const organization = query.organization;
@@ -193,7 +688,7 @@ function isKeepListQuery(value) {
193
688
  }
194
689
  return true;
195
690
  }
196
- function isRecord(value) {
691
+ function isRecord3(value) {
197
692
  return typeof value === "object" && value !== null && !Array.isArray(value);
198
693
  }
199
694
  function isInvalidNumber(value) {
@@ -411,6 +906,20 @@ var AuthenticatedSyncStorageController = class {
411
906
  await this.current.removeCollection?.(id);
412
907
  };
413
908
  }
909
+ if (this.current.getCollectionMemberships && this.current.setCollectionMembership && this.current.removeCollectionMembership) {
910
+ this.getCollectionMemberships = async () => {
911
+ await this.ensureScope();
912
+ return this.current.getCollectionMemberships?.() ?? [];
913
+ };
914
+ this.setCollectionMembership = async (membership) => {
915
+ await this.ensureScope();
916
+ await this.current.setCollectionMembership?.(membership);
917
+ };
918
+ this.removeCollectionMembership = async (collectionId, itemId) => {
919
+ await this.ensureScope();
920
+ await this.current.removeCollectionMembership?.(collectionId, itemId);
921
+ };
922
+ }
414
923
  }
415
924
  get storageKey() {
416
925
  return this.current.storageKey;
@@ -537,6 +1046,8 @@ var AuthenticatedSyncStorageController = class {
537
1046
  };
538
1047
  function createAuthenticatedRemote(options, scope) {
539
1048
  const pull = options.transport.pull;
1049
+ const pullCollections = options.transport.pullCollections;
1050
+ const pullMemberships = options.transport.pullMemberships;
540
1051
  return {
541
1052
  push: async (operation) => {
542
1053
  try {
@@ -553,6 +1064,38 @@ function createAuthenticatedRemote(options, scope) {
553
1064
  } catch (cause) {
554
1065
  return handleAuthFailure(cause, options, { scope });
555
1066
  }
1067
+ } : void 0,
1068
+ pushCollection: options.transport.pushCollection ? async (collectionOperation) => {
1069
+ try {
1070
+ const token = await options.getAuthToken();
1071
+ await options.transport.pushCollection?.(collectionOperation, { token, scope, collectionOperation });
1072
+ } catch (cause) {
1073
+ return handleAuthFailure(cause, options, { collectionOperation, scope });
1074
+ }
1075
+ } : void 0,
1076
+ pullCollections: pullCollections ? async () => {
1077
+ try {
1078
+ const token = await options.getAuthToken();
1079
+ return await pullCollections({ token, scope });
1080
+ } catch (cause) {
1081
+ return handleAuthFailure(cause, options, { scope });
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
+ }
556
1099
  } : void 0
557
1100
  };
558
1101
  }
@@ -575,7 +1118,11 @@ function getAuthStatus(error) {
575
1118
  export {
576
1119
  DEFAULT_INDEXEDDB_DATABASE,
577
1120
  DEFAULT_INDEXEDDB_STORE,
1121
+ DEFAULT_KEEP_HISTORY_KEY,
1122
+ DEFAULT_KEEP_HISTORY_LIMIT,
1123
+ DEFAULT_KEEP_PROGRESS_KEY,
578
1124
  DEFAULT_KEEP_URL_PARAMS,
1125
+ DEFAULT_KEEP_VIEWING_RECORDS_KEY,
579
1126
  DEFAULT_SAVED_VIEWS_STORAGE_KEY,
580
1127
  DEFAULT_STORAGE_KEY,
581
1128
  DEFAULT_SYNC_QUEUE_DATABASE,
@@ -587,8 +1134,10 @@ export {
587
1134
  IndexedDBSyncQueueAdapter,
588
1135
  KEEP_BACKUP_FORMAT,
589
1136
  KEEP_BACKUP_VERSION,
1137
+ KeepActivityStorageError,
590
1138
  KeepBackupImportError,
591
1139
  KeepBackupParseError,
1140
+ KeepProgressStorageError,
592
1141
  KeepSchemaValidationError,
593
1142
  KeepStorageAccessError,
594
1143
  KeepStorageError,
@@ -597,11 +1146,15 @@ export {
597
1146
  KeepStore,
598
1147
  KeepSyncAuthError,
599
1148
  LocalStorageAdapter,
1149
+ LocalStorageKeepHistoryStorage,
1150
+ LocalStorageKeepProgressStorage,
600
1151
  LocalStorageKeepSavedViewStorage,
1152
+ LocalStorageKeepViewingRecordStorage,
601
1153
  LocalStorageSyncQueueAdapter,
602
1154
  ScopedStorageAdapter,
603
1155
  ScopedSyncQueueAdapter,
604
1156
  SyncStorageAdapter,
1157
+ addKeepItemToCollection,
605
1158
  createAuthenticatedSyncKit,
606
1159
  createBrowserStorageAdapter,
607
1160
  createInboxQuery,
@@ -614,22 +1167,27 @@ export {
614
1167
  decodeKeepListQuery,
615
1168
  encodeKeepListQuery,
616
1169
  exportItems,
1170
+ getKeepCollectionItems,
617
1171
  getKeepNavigationState,
618
1172
  getKeepScopeKey,
619
1173
  getTagCounts,
620
1174
  importItems,
621
1175
  isKeepItemMetadataStale,
622
1176
  isKeepSyncAuthError,
1177
+ isProgressCompatible,
623
1178
  isSameKeepScope,
624
1179
  mergeKeepItems,
625
1180
  mergeKeepListQueryFromUrl,
626
1181
  migrateKeepItems,
1182
+ migrateLegacyCollectionMemberships,
627
1183
  moveKeepItem,
628
1184
  normalizeKeepTags,
629
1185
  orderKeepItems,
630
1186
  parseKeepMeta,
631
1187
  queryKeepItems,
632
1188
  reconcileKeepItems,
1189
+ removeKeepItemFromCollection,
1190
+ reorderKeepCollectionItems,
633
1191
  reorderKeepItems,
634
1192
  revalidateKeepItems,
635
1193
  serializeKeepListQuery,