@keepkit/core 0.1.0 → 0.3.0
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 +16 -2
- package/dist/chunk-5QSZP6MT.js +1 -0
- package/dist/chunk-5QSZP6MT.js.map +1 -0
- package/dist/chunk-J7NKIOXV.js +142 -0
- package/dist/chunk-J7NKIOXV.js.map +1 -0
- package/dist/chunk-THZ3ACR2.js +41 -0
- package/dist/chunk-THZ3ACR2.js.map +1 -0
- package/dist/{chunk-H4A322SZ.js → chunk-X4UVKTBK.js} +215 -15
- package/dist/chunk-X4UVKTBK.js.map +1 -0
- package/dist/core.d.ts +66 -0
- package/dist/core.js +220 -0
- package/dist/core.js.map +1 -0
- package/dist/react.d.ts +159 -0
- package/dist/{index.js → react.js} +220 -364
- package/dist/react.js.map +1 -0
- package/dist/schema.d.ts +15 -0
- package/dist/schema.js +11 -0
- package/dist/schema.js.map +1 -0
- package/dist/storage.d.ts +227 -1
- package/dist/storage.js +8 -1
- package/dist/store-DyjKY0s4.d.ts +102 -0
- package/dist/types-BRfvVnCA.d.ts +164 -0
- package/package.json +13 -8
- package/dist/chunk-H4A322SZ.js.map +0 -1
- package/dist/index.d.ts +0 -224
- package/dist/index.js.map +0 -1
- package/dist/storage-DPIKGno6.d.ts +0 -316
|
@@ -1,190 +1,18 @@
|
|
|
1
|
+
"use client";
|
|
1
2
|
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
KeepStorageQuotaError,
|
|
14
|
-
LocalStorageAdapter,
|
|
15
|
-
LocalStorageSyncQueueAdapter,
|
|
16
|
-
SyncStorageAdapter,
|
|
17
|
-
createStorageAdapter,
|
|
3
|
+
KeepStore,
|
|
4
|
+
isKeepItemMetadataStale,
|
|
5
|
+
queryKeepItems,
|
|
6
|
+
revalidateKeepItems
|
|
7
|
+
} from "./chunk-J7NKIOXV.js";
|
|
8
|
+
import {
|
|
9
|
+
parseKeepMeta
|
|
10
|
+
} from "./chunk-THZ3ACR2.js";
|
|
11
|
+
import "./chunk-5QSZP6MT.js";
|
|
12
|
+
import {
|
|
13
|
+
createBrowserStorageAdapter,
|
|
18
14
|
normalizeKeepTags
|
|
19
|
-
} from "./chunk-
|
|
20
|
-
|
|
21
|
-
// src/migration.ts
|
|
22
|
-
async function mergeKeepItems(localItems, target) {
|
|
23
|
-
if (target.merge) return target.merge(localItems);
|
|
24
|
-
const remoteItems = await target.getAll();
|
|
25
|
-
const byId = new Map(remoteItems.map((item) => [item.id, item]));
|
|
26
|
-
for (const localItem of localItems) {
|
|
27
|
-
const remoteItem = byId.get(localItem.id);
|
|
28
|
-
if (!remoteItem || localItem.updatedAt > remoteItem.updatedAt) {
|
|
29
|
-
byId.set(localItem.id, localItem);
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
const merged = [...byId.values()].sort((a, b) => b.updatedAt - a.updatedAt);
|
|
33
|
-
await Promise.all(merged.map((item) => target.set(item)));
|
|
34
|
-
return merged;
|
|
35
|
-
}
|
|
36
|
-
async function migrateKeepItems(source, target) {
|
|
37
|
-
const localItems = await source.getAll();
|
|
38
|
-
const merged = await mergeKeepItems(localItems, target);
|
|
39
|
-
await source.clear();
|
|
40
|
-
return merged;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
// src/schema.ts
|
|
44
|
-
var KeepSchemaValidationError = class extends Error {
|
|
45
|
-
constructor(message, options = {}) {
|
|
46
|
-
super(message);
|
|
47
|
-
this.name = "KeepSchemaValidationError";
|
|
48
|
-
this.cause = options.cause;
|
|
49
|
-
this.itemId = options.itemId;
|
|
50
|
-
}
|
|
51
|
-
};
|
|
52
|
-
async function parseKeepMeta(schema, value) {
|
|
53
|
-
try {
|
|
54
|
-
if ("parse" in schema) return await schema.parse(value);
|
|
55
|
-
if ("safeParse" in schema) {
|
|
56
|
-
const result2 = await schema.safeParse(value);
|
|
57
|
-
if (result2.success) return result2.data;
|
|
58
|
-
throw new KeepSchemaValidationError("KeepKit metadata did not match the configured schema.", {
|
|
59
|
-
cause: result2.error
|
|
60
|
-
});
|
|
61
|
-
}
|
|
62
|
-
const result = await schema["~standard"].validate(value);
|
|
63
|
-
if (!result.issues && "value" in result) return result.value;
|
|
64
|
-
throw new KeepSchemaValidationError("KeepKit metadata did not match the configured schema.", {
|
|
65
|
-
cause: result.issues
|
|
66
|
-
});
|
|
67
|
-
} catch (cause) {
|
|
68
|
-
if (cause instanceof KeepSchemaValidationError) throw cause;
|
|
69
|
-
throw new KeepSchemaValidationError("KeepKit metadata did not match the configured schema.", {
|
|
70
|
-
cause
|
|
71
|
-
});
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
async function validateKeepItem(item, schema) {
|
|
75
|
-
return { ...item, meta: await parseKeepMeta(schema, item.meta) };
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
// src/backup.ts
|
|
79
|
-
var KEEP_BACKUP_FORMAT = "keepkit";
|
|
80
|
-
var KEEP_BACKUP_VERSION = 1;
|
|
81
|
-
var KeepBackupParseError = class extends Error {
|
|
82
|
-
constructor(message, options) {
|
|
83
|
-
super(message);
|
|
84
|
-
this.name = "KeepBackupParseError";
|
|
85
|
-
if (options?.cause !== void 0) this.cause = options.cause;
|
|
86
|
-
}
|
|
87
|
-
};
|
|
88
|
-
var KeepBackupImportError = class extends Error {
|
|
89
|
-
constructor(message, options) {
|
|
90
|
-
super(message);
|
|
91
|
-
this.name = "KeepBackupImportError";
|
|
92
|
-
this.mode = options.mode;
|
|
93
|
-
this.imported = options.imported;
|
|
94
|
-
this.failed = options.failed;
|
|
95
|
-
if (options.cause !== void 0) this.cause = options.cause;
|
|
96
|
-
}
|
|
97
|
-
};
|
|
98
|
-
async function exportItems(adapter) {
|
|
99
|
-
const backup = {
|
|
100
|
-
format: KEEP_BACKUP_FORMAT,
|
|
101
|
-
version: KEEP_BACKUP_VERSION,
|
|
102
|
-
exportedAt: Date.now(),
|
|
103
|
-
items: await adapter.getAll()
|
|
104
|
-
};
|
|
105
|
-
return JSON.stringify(backup, null, 2);
|
|
106
|
-
}
|
|
107
|
-
async function importItems(adapter, data, options = {}) {
|
|
108
|
-
const backup = parseBackup(data);
|
|
109
|
-
const mode = options.mode ?? "merge";
|
|
110
|
-
const validItems = [];
|
|
111
|
-
let failed = 0;
|
|
112
|
-
for (const item of backup.items) {
|
|
113
|
-
if (!options.schema) {
|
|
114
|
-
validItems.push(item);
|
|
115
|
-
continue;
|
|
116
|
-
}
|
|
117
|
-
try {
|
|
118
|
-
validItems.push(await validateKeepItem(item, options.schema));
|
|
119
|
-
} catch (cause) {
|
|
120
|
-
options.onInvalidItem?.(cause, item);
|
|
121
|
-
if ((options.invalidItemPolicy ?? "error") === "drop") {
|
|
122
|
-
failed += 1;
|
|
123
|
-
continue;
|
|
124
|
-
}
|
|
125
|
-
throw cause;
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
let items;
|
|
129
|
-
if (mode === "merge") {
|
|
130
|
-
try {
|
|
131
|
-
items = await mergeKeepItems(validItems, adapter);
|
|
132
|
-
} catch (cause) {
|
|
133
|
-
throw new KeepBackupImportError("KeepKit could not merge the backup.", {
|
|
134
|
-
mode,
|
|
135
|
-
imported: 0,
|
|
136
|
-
failed: validItems.length + failed,
|
|
137
|
-
cause
|
|
138
|
-
});
|
|
139
|
-
}
|
|
140
|
-
} else {
|
|
141
|
-
let imported = 0;
|
|
142
|
-
try {
|
|
143
|
-
await adapter.clear();
|
|
144
|
-
for (const item of validItems) {
|
|
145
|
-
await adapter.set(item);
|
|
146
|
-
imported += 1;
|
|
147
|
-
}
|
|
148
|
-
items = await adapter.getAll();
|
|
149
|
-
} catch (cause) {
|
|
150
|
-
throw new KeepBackupImportError("KeepKit could not replace the stored items.", {
|
|
151
|
-
mode,
|
|
152
|
-
imported,
|
|
153
|
-
failed: validItems.length + failed - imported,
|
|
154
|
-
cause
|
|
155
|
-
});
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
return { mode, imported: validItems.length, failed, total: items.length, items };
|
|
159
|
-
}
|
|
160
|
-
function parseBackup(data) {
|
|
161
|
-
let value = data;
|
|
162
|
-
if (typeof data === "string") {
|
|
163
|
-
try {
|
|
164
|
-
value = JSON.parse(data);
|
|
165
|
-
} catch (cause) {
|
|
166
|
-
throw new KeepBackupParseError("KeepKit backup is not valid JSON.", { cause });
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
if (!isRecord(value)) throw new KeepBackupParseError("KeepKit backup must be an object.");
|
|
170
|
-
if (value.format !== KEEP_BACKUP_FORMAT || value.version !== KEEP_BACKUP_VERSION) {
|
|
171
|
-
throw new KeepBackupParseError("KeepKit backup format or version is unsupported.");
|
|
172
|
-
}
|
|
173
|
-
if (typeof value.exportedAt !== "number" || !Number.isFinite(value.exportedAt)) {
|
|
174
|
-
throw new KeepBackupParseError("KeepKit backup has an invalid export timestamp.");
|
|
175
|
-
}
|
|
176
|
-
if (!Array.isArray(value.items) || !value.items.every(isKeepItem)) {
|
|
177
|
-
throw new KeepBackupParseError("KeepKit backup contains invalid items.");
|
|
178
|
-
}
|
|
179
|
-
return value;
|
|
180
|
-
}
|
|
181
|
-
function isKeepItem(value) {
|
|
182
|
-
if (!isRecord(value)) return false;
|
|
183
|
-
return typeof value.id === "string" && typeof value.savedAt === "number" && Number.isFinite(value.savedAt) && typeof value.updatedAt === "number" && Number.isFinite(value.updatedAt) && "meta" in value && (value.targetType === void 0 || typeof value.targetType === "string") && (value.note === void 0 || typeof value.note === "string") && (value.schemaVersion === void 0 || typeof value.schemaVersion === "number" && Number.isFinite(value.schemaVersion)) && (value.revision === void 0 || typeof value.revision === "string") && (value.tags === void 0 || Array.isArray(value.tags) && value.tags.every((tag) => typeof tag === "string"));
|
|
184
|
-
}
|
|
185
|
-
function isRecord(value) {
|
|
186
|
-
return typeof value === "object" && value !== null;
|
|
187
|
-
}
|
|
15
|
+
} from "./chunk-X4UVKTBK.js";
|
|
188
16
|
|
|
189
17
|
// src/hooks/useKeepItem.ts
|
|
190
18
|
import { useCallback as useCallback3 } from "react";
|
|
@@ -199,39 +27,13 @@ import {
|
|
|
199
27
|
useRef,
|
|
200
28
|
useSyncExternalStore
|
|
201
29
|
} from "react";
|
|
202
|
-
|
|
203
|
-
// src/store.ts
|
|
204
|
-
var KeepStore = class {
|
|
205
|
-
constructor(initialState) {
|
|
206
|
-
this.listeners = /* @__PURE__ */ new Set();
|
|
207
|
-
this.getSnapshot = () => this.state;
|
|
208
|
-
this.subscribe = (listener) => {
|
|
209
|
-
this.listeners.add(listener);
|
|
210
|
-
return () => this.listeners.delete(listener);
|
|
211
|
-
};
|
|
212
|
-
this.state = initialState;
|
|
213
|
-
}
|
|
214
|
-
setState(next) {
|
|
215
|
-
let changed = false;
|
|
216
|
-
for (const key of Object.keys(next)) {
|
|
217
|
-
if (!Object.is(this.state[key], next[key])) {
|
|
218
|
-
changed = true;
|
|
219
|
-
break;
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
if (!changed) return;
|
|
223
|
-
this.state = { ...this.state, ...next };
|
|
224
|
-
for (const listener of this.listeners) listener();
|
|
225
|
-
}
|
|
226
|
-
};
|
|
227
|
-
|
|
228
|
-
// src/KeepProvider.tsx
|
|
229
30
|
import { jsx } from "react/jsx-runtime";
|
|
230
|
-
var defaultStorage =
|
|
31
|
+
var defaultStorage = createBrowserStorageAdapter();
|
|
231
32
|
var KeepContext = createContext(null);
|
|
232
33
|
var KeepStoreContext = createContext(null);
|
|
233
34
|
function KeepProvider({
|
|
234
35
|
storage = defaultStorage,
|
|
36
|
+
initialItems,
|
|
235
37
|
onSave,
|
|
236
38
|
onRemove,
|
|
237
39
|
onNoteUpdate,
|
|
@@ -249,7 +51,7 @@ function KeepProvider({
|
|
|
249
51
|
const storeRef = useRef(null);
|
|
250
52
|
if (!storeRef.current) {
|
|
251
53
|
storeRef.current = new KeepStore({
|
|
252
|
-
items: [],
|
|
54
|
+
items: initialItems ? [...initialItems] : [],
|
|
253
55
|
isLoading: true,
|
|
254
56
|
isHydrated: false,
|
|
255
57
|
isMutating: false,
|
|
@@ -276,10 +78,7 @@ function KeepProvider({
|
|
|
276
78
|
const pendingRefreshesRef = useRef(0);
|
|
277
79
|
const pendingMutationsRef = useRef(0);
|
|
278
80
|
const syncStorage = isSyncCapableStorage(storage) ? storage : void 0;
|
|
279
|
-
const getSyncState = useCallback(
|
|
280
|
-
() => syncStorage?.getSyncState() ?? IDLE_SYNC_STATE,
|
|
281
|
-
[syncStorage]
|
|
282
|
-
);
|
|
81
|
+
const getSyncState = useCallback(() => syncStorage?.getSyncState() ?? IDLE_SYNC_STATE, [syncStorage]);
|
|
283
82
|
const subscribeSync = useCallback(
|
|
284
83
|
(listener) => syncStorage?.subscribeSync(listener) ?? (() => void 0),
|
|
285
84
|
[syncStorage]
|
|
@@ -300,19 +99,13 @@ function KeepProvider({
|
|
|
300
99
|
},
|
|
301
100
|
[store]
|
|
302
101
|
);
|
|
303
|
-
const runBeforePlugins = useCallback(
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
);
|
|
310
|
-
const runAfterPlugins = useCallback(
|
|
311
|
-
async (context) => {
|
|
312
|
-
for (const plugin of pluginsRef.current) await plugin.after?.(context);
|
|
313
|
-
},
|
|
314
|
-
[]
|
|
315
|
-
);
|
|
102
|
+
const runBeforePlugins = useCallback(async (context) => {
|
|
103
|
+
for (const plugin of pluginsRef.current) await plugin.before?.(context);
|
|
104
|
+
return context;
|
|
105
|
+
}, []);
|
|
106
|
+
const runAfterPlugins = useCallback(async (context) => {
|
|
107
|
+
for (const plugin of pluginsRef.current) await plugin.after?.(context);
|
|
108
|
+
}, []);
|
|
316
109
|
const enqueueOperation = useCallback((operation) => {
|
|
317
110
|
const run = operationTailRef.current.then(operation, operation);
|
|
318
111
|
operationTailRef.current = run.then(
|
|
@@ -434,10 +227,9 @@ function KeepProvider({
|
|
|
434
227
|
throw cause;
|
|
435
228
|
}
|
|
436
229
|
await runMutation("save", normalizedItem.id, (previous) => ({
|
|
437
|
-
next: [
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
].sort((a, b) => b.updatedAt - a.updatedAt),
|
|
230
|
+
next: [...previous.filter((current) => current.id !== normalizedItem.id), normalizedItem].sort(
|
|
231
|
+
(a, b) => b.updatedAt - a.updatedAt
|
|
232
|
+
),
|
|
441
233
|
persist: () => storage.set(normalizedItem),
|
|
442
234
|
onSuccess: () => handlersRef.current.onSave?.(normalizedItem),
|
|
443
235
|
pluginContext: { action: "save", id: normalizedItem.id, item: normalizedItem }
|
|
@@ -533,9 +325,7 @@ function KeepProvider({
|
|
|
533
325
|
const idSet = new Set(ids);
|
|
534
326
|
const currentItems = itemsRef.current.filter((item) => idSet.has(item.id));
|
|
535
327
|
await Promise.all(
|
|
536
|
-
currentItems.map(
|
|
537
|
-
(item) => updateTags(item.id, normalizeKeepTags([...item.tags ?? [], ...additions]))
|
|
538
|
-
)
|
|
328
|
+
currentItems.map((item) => updateTags(item.id, normalizeKeepTags([...item.tags ?? [], ...additions])))
|
|
539
329
|
);
|
|
540
330
|
},
|
|
541
331
|
[updateTags]
|
|
@@ -547,10 +337,7 @@ function KeepProvider({
|
|
|
547
337
|
const currentItems = itemsRef.current.filter((item) => idSet.has(item.id));
|
|
548
338
|
await Promise.all(
|
|
549
339
|
currentItems.map(
|
|
550
|
-
(item) => updateTags(
|
|
551
|
-
item.id,
|
|
552
|
-
normalizeKeepTags((item.tags ?? []).filter((tag) => !removals.has(tag)))
|
|
553
|
-
)
|
|
340
|
+
(item) => updateTags(item.id, normalizeKeepTags((item.tags ?? []).filter((tag) => !removals.has(tag))))
|
|
554
341
|
)
|
|
555
342
|
);
|
|
556
343
|
},
|
|
@@ -614,10 +401,65 @@ function KeepProvider({
|
|
|
614
401
|
})),
|
|
615
402
|
[runMutation, storage]
|
|
616
403
|
);
|
|
617
|
-
const
|
|
618
|
-
(
|
|
619
|
-
|
|
404
|
+
const revalidateItems = useCallback(
|
|
405
|
+
async (revalidator, options = {}) => {
|
|
406
|
+
pendingMutationsRef.current += 1;
|
|
407
|
+
store.setState({ isMutating: true });
|
|
408
|
+
const run = enqueueOperation(async () => {
|
|
409
|
+
const previous = itemsRef.current;
|
|
410
|
+
let persistenceStarted = false;
|
|
411
|
+
try {
|
|
412
|
+
const summary = await revalidateKeepItems(previous, revalidator, options);
|
|
413
|
+
const pluginContext = { action: "revalidate", items: summary.updatedItems };
|
|
414
|
+
await runBeforePlugins(pluginContext);
|
|
415
|
+
if (summary.updatedItems.length > 0) {
|
|
416
|
+
persistenceStarted = true;
|
|
417
|
+
if (storage.setMany) await storage.setMany(summary.updatedItems);
|
|
418
|
+
else for (const item of summary.updatedItems) await storage.set(item);
|
|
419
|
+
}
|
|
420
|
+
if (summary.removedIds.length > 0) {
|
|
421
|
+
persistenceStarted = true;
|
|
422
|
+
if (storage.removeMany) await storage.removeMany(summary.removedIds);
|
|
423
|
+
else for (const id of summary.removedIds) await storage.remove(id);
|
|
424
|
+
}
|
|
425
|
+
setItems(summary.items);
|
|
426
|
+
store.setState({ error: null });
|
|
427
|
+
const removedIdSet = new Set(summary.removedIds);
|
|
428
|
+
for (const result of summary.results) {
|
|
429
|
+
if (removedIdSet.has(result.item.id)) handlersRef.current.onRemove?.(result.item);
|
|
430
|
+
}
|
|
431
|
+
await runAfterPlugins(pluginContext);
|
|
432
|
+
void Promise.resolve(handlersRef.current.onChange?.({ ...pluginContext, phase: "local" })).catch(
|
|
433
|
+
(cause) => reportError(cause, { action: "revalidate" })
|
|
434
|
+
);
|
|
435
|
+
return summary;
|
|
436
|
+
} catch (cause) {
|
|
437
|
+
if (persistenceStarted) await restoreItems(storage, previous);
|
|
438
|
+
setItems(previous);
|
|
439
|
+
reportError(cause, { action: "revalidate" });
|
|
440
|
+
throw cause;
|
|
441
|
+
}
|
|
442
|
+
});
|
|
443
|
+
return run.finally(() => {
|
|
444
|
+
pendingMutationsRef.current -= 1;
|
|
445
|
+
if (pendingMutationsRef.current === 0) store.setState({ isMutating: false });
|
|
446
|
+
});
|
|
447
|
+
},
|
|
448
|
+
[enqueueOperation, reportError, runAfterPlugins, runBeforePlugins, setItems, storage, store]
|
|
449
|
+
);
|
|
450
|
+
const refreshItemMetadata = useCallback(
|
|
451
|
+
async (id, refresh2) => {
|
|
452
|
+
if (!itemsRef.current.some((item) => item.id === id)) {
|
|
453
|
+
throw new Error(`Cannot refresh metadata for missing item "${id}".`);
|
|
454
|
+
}
|
|
455
|
+
await revalidateItems(async (item) => {
|
|
456
|
+
if (item.id !== id) return "available";
|
|
457
|
+
return { status: "available", meta: await refresh2(item) };
|
|
458
|
+
});
|
|
459
|
+
},
|
|
460
|
+
[revalidateItems]
|
|
620
461
|
);
|
|
462
|
+
const flushSync = useCallback(() => syncStorage ? syncStorage.flushSync() : Promise.resolve(), [syncStorage]);
|
|
621
463
|
const value = useMemo(
|
|
622
464
|
() => ({
|
|
623
465
|
items,
|
|
@@ -636,7 +478,9 @@ function KeepProvider({
|
|
|
636
478
|
removeItems,
|
|
637
479
|
clear,
|
|
638
480
|
refresh,
|
|
639
|
-
flushSync
|
|
481
|
+
flushSync,
|
|
482
|
+
refreshItemMetadata,
|
|
483
|
+
revalidateItems
|
|
640
484
|
}),
|
|
641
485
|
[
|
|
642
486
|
clear,
|
|
@@ -655,7 +499,9 @@ function KeepProvider({
|
|
|
655
499
|
updateTagsBatch,
|
|
656
500
|
addTagsBatch,
|
|
657
501
|
removeTagsBatch,
|
|
658
|
-
removeItems
|
|
502
|
+
removeItems,
|
|
503
|
+
refreshItemMetadata,
|
|
504
|
+
revalidateItems
|
|
659
505
|
]
|
|
660
506
|
);
|
|
661
507
|
const actions = useMemo(
|
|
@@ -669,7 +515,9 @@ function KeepProvider({
|
|
|
669
515
|
removeItem,
|
|
670
516
|
removeItems,
|
|
671
517
|
clear,
|
|
672
|
-
refresh
|
|
518
|
+
refresh,
|
|
519
|
+
refreshItemMetadata,
|
|
520
|
+
revalidateItems
|
|
673
521
|
}),
|
|
674
522
|
[
|
|
675
523
|
addTagsBatch,
|
|
@@ -681,7 +529,9 @@ function KeepProvider({
|
|
|
681
529
|
saveItem,
|
|
682
530
|
updateNote,
|
|
683
531
|
updateTags,
|
|
684
|
-
updateTagsBatch
|
|
532
|
+
updateTagsBatch,
|
|
533
|
+
refreshItemMetadata,
|
|
534
|
+
revalidateItems
|
|
685
535
|
]
|
|
686
536
|
);
|
|
687
537
|
const storeAccess = useMemo(() => ({ store, actions }), [actions, store]);
|
|
@@ -698,6 +548,16 @@ function isSyncCapableStorage(storage) {
|
|
|
698
548
|
async function parseKeepMetaItem(item, schema) {
|
|
699
549
|
return { ...item, meta: await parseKeepMeta(schema, item.meta) };
|
|
700
550
|
}
|
|
551
|
+
async function restoreItems(storage, items) {
|
|
552
|
+
try {
|
|
553
|
+
if (storage.setMany) {
|
|
554
|
+
await storage.setMany(items);
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
for (const item of items) await storage.set(item);
|
|
558
|
+
} catch {
|
|
559
|
+
}
|
|
560
|
+
}
|
|
701
561
|
function useKeepContext() {
|
|
702
562
|
const context = useContext(KeepContext);
|
|
703
563
|
if (!context) throw new Error("Keep hooks must be used inside a KeepProvider");
|
|
@@ -759,6 +619,10 @@ function useKeepItem(id, itemPayload) {
|
|
|
759
619
|
const toggle = useCallback3(() => item ? remove() : save(), [item, remove, save]);
|
|
760
620
|
const updateNote = useCallback3((note) => actions.updateNote(id, note), [actions, id]);
|
|
761
621
|
const updateTags = useCallback3((tags) => actions.updateTags(id, tags), [actions, id]);
|
|
622
|
+
const refreshMetadata = useCallback3(
|
|
623
|
+
(refresh) => actions.refreshItemMetadata(id, refresh),
|
|
624
|
+
[actions, id]
|
|
625
|
+
);
|
|
762
626
|
return {
|
|
763
627
|
item,
|
|
764
628
|
isSaved: Boolean(item),
|
|
@@ -769,7 +633,8 @@ function useKeepItem(id, itemPayload) {
|
|
|
769
633
|
remove,
|
|
770
634
|
toggle,
|
|
771
635
|
updateNote,
|
|
772
|
-
updateTags
|
|
636
|
+
updateTags,
|
|
637
|
+
refreshMetadata
|
|
773
638
|
};
|
|
774
639
|
}
|
|
775
640
|
|
|
@@ -873,14 +738,8 @@ function useKeepList(options = {}) {
|
|
|
873
738
|
(ids, tags2) => actions.updateTagsBatch(ids, tags2),
|
|
874
739
|
[actions]
|
|
875
740
|
);
|
|
876
|
-
const addTagsBatch = useCallback4(
|
|
877
|
-
|
|
878
|
-
[actions]
|
|
879
|
-
);
|
|
880
|
-
const removeTagsBatch = useCallback4(
|
|
881
|
-
(ids, tags2) => actions.removeTagsBatch(ids, tags2),
|
|
882
|
-
[actions]
|
|
883
|
-
);
|
|
741
|
+
const addTagsBatch = useCallback4((ids, tags2) => actions.addTagsBatch(ids, tags2), [actions]);
|
|
742
|
+
const removeTagsBatch = useCallback4((ids, tags2) => actions.removeTagsBatch(ids, tags2), [actions]);
|
|
884
743
|
return {
|
|
885
744
|
items,
|
|
886
745
|
totalCount,
|
|
@@ -896,55 +755,10 @@ function useKeepList(options = {}) {
|
|
|
896
755
|
addTagsBatch,
|
|
897
756
|
removeTagsBatch,
|
|
898
757
|
clear: actions.clear,
|
|
899
|
-
refresh: actions.refresh
|
|
758
|
+
refresh: actions.refresh,
|
|
759
|
+
revalidate: actions.revalidateItems
|
|
900
760
|
};
|
|
901
761
|
}
|
|
902
|
-
function queryKeepItems(source, options = {}) {
|
|
903
|
-
const filtered = source.filter((item) => {
|
|
904
|
-
const [from, to] = options.savedBetween ?? [];
|
|
905
|
-
const savedAt = item.savedAt;
|
|
906
|
-
const lowerBound = from === void 0 ? void 0 : toTimestamp(from);
|
|
907
|
-
const upperBound = to === void 0 ? void 0 : toTimestamp(to);
|
|
908
|
-
return (options.targetType === void 0 || item.targetType === options.targetType) && (options.tag === void 0 || item.tags?.includes(options.tag) === true) && (options.tags === void 0 || options.tags.every((tag) => item.tags?.includes(tag))) && (lowerBound === void 0 || savedAt >= lowerBound) && (upperBound === void 0 || savedAt <= upperBound) && matchesSearch(item, options.searchQuery, options.search) && (options.filter?.(item) ?? true) && (options.filterFn?.(item) ?? true);
|
|
909
|
-
});
|
|
910
|
-
const tagCounts = getTagCounts(filtered);
|
|
911
|
-
const sortBy = options.sortBy ?? options.sort?.by;
|
|
912
|
-
const direction = (options.order ?? options.sort?.direction) === "asc" ? 1 : -1;
|
|
913
|
-
const sorted = sortBy ? [...filtered].sort((a, b) => (a[sortBy] - b[sortBy]) * direction) : filtered;
|
|
914
|
-
const offset = Math.max(0, options.offset ?? 0);
|
|
915
|
-
const items = options.limit === void 0 ? sorted.slice(offset) : sorted.slice(offset, offset + Math.max(0, options.limit));
|
|
916
|
-
return { items, totalCount: sorted.length, tagCounts };
|
|
917
|
-
}
|
|
918
|
-
function getTagCounts(items) {
|
|
919
|
-
const counts = {};
|
|
920
|
-
for (const item of items) {
|
|
921
|
-
for (const tag of item.tags ?? []) counts[tag] = (counts[tag] ?? 0) + 1;
|
|
922
|
-
}
|
|
923
|
-
return Object.fromEntries(Object.entries(counts).sort(([a], [b]) => a.localeCompare(b)));
|
|
924
|
-
}
|
|
925
|
-
function matchesSearch(item, searchQuery, search) {
|
|
926
|
-
const query = search?.query ?? searchQuery;
|
|
927
|
-
if (!query?.trim()) return true;
|
|
928
|
-
const fields = search?.fields ?? ["note", "meta", "tags"];
|
|
929
|
-
const values = fields.map((field) => {
|
|
930
|
-
if (field === "note") return item.note ?? "";
|
|
931
|
-
if (field === "tags") return (item.tags ?? []).join(" ");
|
|
932
|
-
try {
|
|
933
|
-
return JSON.stringify(item.meta) ?? "";
|
|
934
|
-
} catch {
|
|
935
|
-
return String(item.meta);
|
|
936
|
-
}
|
|
937
|
-
});
|
|
938
|
-
const text = values.join(" ").toLocaleLowerCase();
|
|
939
|
-
if (!search) return text.includes(query.trim().toLocaleLowerCase());
|
|
940
|
-
const normalized = query.trim().toLocaleLowerCase();
|
|
941
|
-
const needles = search.tokenize === false ? [normalized] : normalized.split(/\s+/).filter(Boolean);
|
|
942
|
-
const matches = needles.map((needle) => text.includes(needle));
|
|
943
|
-
return search.mode === "or" ? matches.some(Boolean) : matches.every(Boolean);
|
|
944
|
-
}
|
|
945
|
-
function toTimestamp(value) {
|
|
946
|
-
return value instanceof Date ? value.getTime() : value;
|
|
947
|
-
}
|
|
948
762
|
function sameItems(left, right) {
|
|
949
763
|
return left.length === right.length && left.every((item, index) => item === right[index]);
|
|
950
764
|
}
|
|
@@ -957,6 +771,62 @@ function sameCounts(left, right) {
|
|
|
957
771
|
});
|
|
958
772
|
}
|
|
959
773
|
|
|
774
|
+
// src/hooks/useKeepShortcut.ts
|
|
775
|
+
import { useEffect as useEffect2 } from "react";
|
|
776
|
+
function useKeepShortcut(options) {
|
|
777
|
+
const item = useKeepItem(options.id ?? "", options.itemPayload);
|
|
778
|
+
const {
|
|
779
|
+
action = "toggle",
|
|
780
|
+
allowInEditable = false,
|
|
781
|
+
enabled = true,
|
|
782
|
+
key,
|
|
783
|
+
modifier,
|
|
784
|
+
onError,
|
|
785
|
+
onTrigger,
|
|
786
|
+
preventDefault = true
|
|
787
|
+
} = options;
|
|
788
|
+
useEffect2(() => {
|
|
789
|
+
if (!enabled) return;
|
|
790
|
+
const handleKeyDown = (event) => {
|
|
791
|
+
if (!allowInEditable && isEditableTarget(event.target)) return;
|
|
792
|
+
if (!matchesShortcut(event, key, modifier)) return;
|
|
793
|
+
if (preventDefault) event.preventDefault();
|
|
794
|
+
const run = onTrigger ? onTrigger(event) : options.id ? action === "save" ? item.save() : action === "remove" ? item.remove() : item.toggle() : void 0;
|
|
795
|
+
if (run) void Promise.resolve(run).catch((error) => onError?.(error));
|
|
796
|
+
};
|
|
797
|
+
window.addEventListener("keydown", handleKeyDown);
|
|
798
|
+
return () => window.removeEventListener("keydown", handleKeyDown);
|
|
799
|
+
}, [
|
|
800
|
+
action,
|
|
801
|
+
allowInEditable,
|
|
802
|
+
enabled,
|
|
803
|
+
item.remove,
|
|
804
|
+
item.save,
|
|
805
|
+
item.toggle,
|
|
806
|
+
key,
|
|
807
|
+
modifier,
|
|
808
|
+
onError,
|
|
809
|
+
onTrigger,
|
|
810
|
+
options.id,
|
|
811
|
+
preventDefault
|
|
812
|
+
]);
|
|
813
|
+
}
|
|
814
|
+
function matchesShortcut(event, key, modifier) {
|
|
815
|
+
if (event.key.toLocaleLowerCase() !== key.toLocaleLowerCase()) return false;
|
|
816
|
+
const modifiers = {
|
|
817
|
+
meta: event.metaKey,
|
|
818
|
+
ctrl: event.ctrlKey,
|
|
819
|
+
alt: event.altKey,
|
|
820
|
+
shift: event.shiftKey
|
|
821
|
+
};
|
|
822
|
+
if (modifier ? !modifiers[modifier] : Object.values(modifiers).some(Boolean)) return false;
|
|
823
|
+
return Object.entries(modifiers).every(([name, pressed]) => name === modifier || !pressed);
|
|
824
|
+
}
|
|
825
|
+
function isEditableTarget(target) {
|
|
826
|
+
if (!(target instanceof HTMLElement)) return false;
|
|
827
|
+
return target.isContentEditable || target.tagName === "INPUT" || target.tagName === "TEXTAREA" || target.tagName === "SELECT";
|
|
828
|
+
}
|
|
829
|
+
|
|
960
830
|
// src/KeepButton.tsx
|
|
961
831
|
import {
|
|
962
832
|
Children,
|
|
@@ -969,6 +839,9 @@ function KeepButton({
|
|
|
969
839
|
children,
|
|
970
840
|
savedLabel = "Saved",
|
|
971
841
|
unsavedLabel = "Save",
|
|
842
|
+
savedAriaLabel,
|
|
843
|
+
unsavedAriaLabel,
|
|
844
|
+
getAriaLabel,
|
|
972
845
|
asChild = false,
|
|
973
846
|
onToggleError,
|
|
974
847
|
onClick,
|
|
@@ -984,6 +857,7 @@ function KeepButton({
|
|
|
984
857
|
const { isSaved, toggle } = state;
|
|
985
858
|
const isDisabled = disabled ?? state.isMutating;
|
|
986
859
|
async function handleClick(event) {
|
|
860
|
+
if (isDisabled) return;
|
|
987
861
|
if (asChild) {
|
|
988
862
|
onClick?.(event);
|
|
989
863
|
} else {
|
|
@@ -1000,33 +874,53 @@ function KeepButton({
|
|
|
1000
874
|
}
|
|
1001
875
|
const content = typeof children === "function" ? children(state) : children ?? (isSaved ? savedLabel : unsavedLabel);
|
|
1002
876
|
function handleElementClick(event) {
|
|
877
|
+
if (isDisabled) return;
|
|
1003
878
|
if (asChild && isValidElement(content)) {
|
|
1004
879
|
content.props.onClick?.(event);
|
|
1005
880
|
}
|
|
1006
881
|
if (!event.defaultPrevented) void handleClick(event);
|
|
1007
882
|
}
|
|
883
|
+
function handleKeyDown(event) {
|
|
884
|
+
if (asChild && isValidElement(content)) {
|
|
885
|
+
content.props.onKeyDown?.(event);
|
|
886
|
+
}
|
|
887
|
+
buttonProps.onKeyDown?.(event);
|
|
888
|
+
if (!event.defaultPrevented && !isDisabled && asChild && (event.key === "Enter" || event.key === " ")) {
|
|
889
|
+
void handleClick(event);
|
|
890
|
+
event.preventDefault();
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
const child = asChild ? Children.only(content) : void 0;
|
|
894
|
+
if (asChild && !isValidElement(child)) {
|
|
895
|
+
throw new Error("KeepButton with asChild requires a single React element child.");
|
|
896
|
+
}
|
|
1008
897
|
const commonProps = {
|
|
1009
898
|
...buttonProps,
|
|
1010
899
|
"aria-pressed": isSaved,
|
|
1011
|
-
"aria-label": ("aria-label" in buttonProps ? buttonProps["aria-label"] : void 0) ?? (isSaved ?
|
|
1012
|
-
|
|
1013
|
-
|
|
900
|
+
"aria-label": ("aria-label" in buttonProps ? buttonProps["aria-label"] : void 0) ?? getAriaLabel?.(state) ?? (isSaved ? savedAriaLabel : unsavedAriaLabel) ?? getAccessibleLabel(isSaved, item, asChild),
|
|
901
|
+
...asChild ? {
|
|
902
|
+
"aria-disabled": isDisabled,
|
|
903
|
+
role: buttonProps.role ?? "button",
|
|
904
|
+
tabIndex: isDisabled ? -1 : buttonProps.tabIndex ?? 0
|
|
905
|
+
} : { disabled: isDisabled },
|
|
906
|
+
onClick: handleElementClick,
|
|
907
|
+
onKeyDown: handleKeyDown
|
|
1014
908
|
};
|
|
1015
909
|
if (asChild) {
|
|
1016
|
-
const child = Children.only(content);
|
|
1017
|
-
if (!isValidElement(child)) {
|
|
1018
|
-
throw new Error("KeepButton with asChild requires a single React element child.");
|
|
1019
|
-
}
|
|
1020
910
|
return cloneElement(child, commonProps);
|
|
1021
911
|
}
|
|
1022
|
-
return /* @__PURE__ */ jsx2(
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
912
|
+
return /* @__PURE__ */ jsx2("button", { ...commonProps, type: "type" in buttonProps ? buttonProps.type ?? "button" : "button", children: content });
|
|
913
|
+
}
|
|
914
|
+
function getAccessibleLabel(isSaved, item, asChild) {
|
|
915
|
+
if (!asChild) return isSaved ? "Remove saved item" : "Save item";
|
|
916
|
+
const title = getMetaTitle(item.meta);
|
|
917
|
+
const subject = title ? `${item.targetType ?? "item"}: ${title}` : item.targetType ?? "item";
|
|
918
|
+
return `${isSaved ? "Remove" : "Save"} ${subject}`;
|
|
919
|
+
}
|
|
920
|
+
function getMetaTitle(meta) {
|
|
921
|
+
if (typeof meta !== "object" || meta === null || !("title" in meta)) return void 0;
|
|
922
|
+
const title = meta.title;
|
|
923
|
+
return typeof title === "string" && title.trim() ? title.trim() : void 0;
|
|
1030
924
|
}
|
|
1031
925
|
|
|
1032
926
|
// src/createKeepKit.tsx
|
|
@@ -1037,57 +931,19 @@ function createKeepKit(options = {}) {
|
|
|
1037
931
|
KeepButton: (props) => /* @__PURE__ */ jsx3(KeepButton, { ...props }),
|
|
1038
932
|
useKeepContext: () => useKeepContext(),
|
|
1039
933
|
useKeepItem: (id, itemPayload) => useKeepItem(id, itemPayload),
|
|
1040
|
-
useKeepList: (options2) => useKeepList(options2)
|
|
1041
|
-
|
|
1042
|
-
}
|
|
1043
|
-
|
|
1044
|
-
// src/integrations.ts
|
|
1045
|
-
function createKeepInvalidationPlugin(options) {
|
|
1046
|
-
return {
|
|
1047
|
-
name: options.name ?? "keepkit-cache-invalidation",
|
|
1048
|
-
after: async (context) => {
|
|
1049
|
-
const keys = typeof options.queryKeys === "function" ? options.queryKeys(context) : [options.queryKeys];
|
|
1050
|
-
await Promise.all(keys.map((queryKey) => options.invalidate(queryKey, context)));
|
|
1051
|
-
}
|
|
934
|
+
useKeepList: (options2) => useKeepList(options2),
|
|
935
|
+
useKeepShortcut: (shortcutOptions) => useKeepShortcut(shortcutOptions)
|
|
1052
936
|
};
|
|
1053
937
|
}
|
|
1054
938
|
export {
|
|
1055
|
-
DEFAULT_INDEXEDDB_DATABASE,
|
|
1056
|
-
DEFAULT_INDEXEDDB_STORE,
|
|
1057
|
-
DEFAULT_STORAGE_KEY,
|
|
1058
|
-
DEFAULT_SYNC_QUEUE_DATABASE,
|
|
1059
|
-
DEFAULT_SYNC_QUEUE_KEY,
|
|
1060
|
-
DEFAULT_SYNC_QUEUE_STORE,
|
|
1061
|
-
IndexedDBAdapter,
|
|
1062
|
-
IndexedDBSyncQueueAdapter,
|
|
1063
|
-
KEEP_BACKUP_FORMAT,
|
|
1064
|
-
KEEP_BACKUP_VERSION,
|
|
1065
|
-
KeepBackupImportError,
|
|
1066
|
-
KeepBackupParseError,
|
|
1067
939
|
KeepButton,
|
|
1068
940
|
KeepProvider,
|
|
1069
|
-
KeepSchemaValidationError,
|
|
1070
|
-
KeepStorageAccessError,
|
|
1071
|
-
KeepStorageError,
|
|
1072
|
-
KeepStorageParseError,
|
|
1073
|
-
KeepStorageQuotaError,
|
|
1074
|
-
LocalStorageAdapter,
|
|
1075
|
-
LocalStorageSyncQueueAdapter,
|
|
1076
|
-
SyncStorageAdapter,
|
|
1077
|
-
createKeepInvalidationPlugin,
|
|
1078
941
|
createKeepKit,
|
|
1079
|
-
|
|
1080
|
-
exportItems,
|
|
1081
|
-
getTagCounts,
|
|
1082
|
-
importItems,
|
|
1083
|
-
mergeKeepItems,
|
|
1084
|
-
migrateKeepItems,
|
|
1085
|
-
normalizeKeepTags,
|
|
1086
|
-
parseKeepMeta,
|
|
1087
|
-
queryKeepItems,
|
|
942
|
+
isKeepItemMetadataStale,
|
|
1088
943
|
useKeepContext,
|
|
1089
944
|
useKeepItem,
|
|
1090
945
|
useKeepList,
|
|
1091
|
-
|
|
946
|
+
useKeepShortcut,
|
|
947
|
+
useKeepStore
|
|
1092
948
|
};
|
|
1093
|
-
//# sourceMappingURL=
|
|
949
|
+
//# sourceMappingURL=react.js.map
|