@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/README.md +88 -6
- package/dist/{chunk-Y4U5DEGG.js → chunk-5XSSH3TO.js} +212 -28
- package/dist/chunk-5XSSH3TO.js.map +1 -0
- package/dist/{chunk-2TENUU7B.js → chunk-QRPQYE7K.js} +643 -76
- package/dist/chunk-QRPQYE7K.js.map +1 -0
- package/dist/core.d.ts +192 -7
- package/dist/core.js +565 -7
- 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-Dtkxpnof.d.ts → scope-1bxGd68Y.d.ts} +9 -4
- package/dist/storage.d.ts +42 -18
- package/dist/storage.js +1 -1
- package/dist/{store-45yaDeER.d.ts → store-98vWM3tV.d.ts} +31 -4
- package/dist/{types-C_U5Q44o.d.ts → types-GTCMFt5T.d.ts} +83 -5
- package/package.json +1 -1
- package/dist/chunk-2TENUU7B.js.map +0 -1
- package/dist/chunk-Y4U5DEGG.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-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-
|
|
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 (!
|
|
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 (!
|
|
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 (!
|
|
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 (!
|
|
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
|
|
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,
|