@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
package/README.md
CHANGED
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
Reactアプリケーション向けの、ヘッドレスで非同期処理を前提とした保存・コレクション用プリミティブです。
|
|
8
8
|
|
|
9
9
|
```tsx
|
|
10
|
-
import {
|
|
10
|
+
import { LocalStorageAdapter } from "@keepkit/core/storage";
|
|
11
|
+
import { KeepButton, KeepProvider } from "@keepkit/core/react";
|
|
11
12
|
|
|
12
13
|
const storage = new LocalStorageAdapter({ key: "my-app:items" });
|
|
13
14
|
|
|
@@ -26,6 +27,8 @@ const storage = new LocalStorageAdapter({ key: "my-app:items" });
|
|
|
26
27
|
|
|
27
28
|
`exportItems(adapter)` / `importItems(adapter, json, { mode: "replace" | "merge" })` でversion付きJSONバックアップを扱えます。結果には `imported` / `failed` 件数が含まれます。
|
|
28
29
|
|
|
30
|
+
`KeepButton` の標準ARIAラベルは `savedAriaLabel` / `unsavedAriaLabel` または `getAriaLabel` で多言語化できます。`useKeepItem(id).refreshMetadata` と `isKeepItemMetadataStale` はメタデータの再取得に使えます。`useKeepList().revalidate` または `useKeepContext().revalidateItems` は削除・非公開・期限切れの対象を検出します。検出した状態を整理する場合は `removeStatuses` を明示してください。
|
|
31
|
+
|
|
29
32
|
大規模アプリでは `SyncStorageAdapter` を使ってローカル保存とリモート同期を分離できます。`IndexedDBSyncQueueAdapter`(既定)または `LocalStorageSyncQueueAdapter` に操作を永続化し、オンライン復帰時に `flushSync()` で再送します。`KeepProvider` の `syncState` で `pending` / `syncing` / `conflict` / `error` を取得できます。
|
|
30
33
|
|
|
31
34
|
```tsx
|
|
@@ -49,6 +52,10 @@ const storage = new SyncStorageAdapter({
|
|
|
49
52
|
|
|
50
53
|
`useKeepList` は `savedBetween`、`search: { query, mode: "and" | "or" }`、`filterFn`、`tagCounts` をサポートします。TanStack Query や SWR のキャッシュ連携には、依存関係を core に追加しない `createKeepInvalidationPlugin` を利用できます。
|
|
51
54
|
|
|
55
|
+
サーバーで取得した一覧を初期表示に使う場合は `KeepProvider initialItems={itemsFromServer}` を指定できます。クライアント adapter の初回読み込み中も同じスナップショットを描画し、refresh 完了後は adapter の値に更新されます。Reactを使わない環境やRSCのサーバー側コードでは `@keepkit/core/core`、React binding は `@keepkit/core/react`、ストレージは `@keepkit/core/storage`、スキーマ処理は `@keepkit/core/schema` からimportできます。
|
|
56
|
+
|
|
57
|
+
ブラウザの制限環境では `createBrowserStorageAdapter` が IndexedDB を優先し、アクセスできない場合は `LocalStorageAdapter` に切り替えます。自前の主/代替 adapter を組み合わせる場合は `FallbackStorageAdapter` を利用できます。`useKeepShortcut({ key: "k", modifier: "meta", id, itemPayload })` は編集可能なフィールドを除外して保存操作をキーボードに割り当てます。`asChild` の `KeepButton` には `aria-pressed`、`aria-disabled`、button role、Space/Enter 操作が自動で付与されます。
|
|
58
|
+
|
|
52
59
|
詳細な使い方と開発コマンドは [ルートREADME](../../README.md) を、現在の変更内容は [リリースノート](../../RELEASE_NOTES.md) を参照してください。
|
|
53
60
|
|
|
54
61
|
## English
|
|
@@ -56,7 +63,8 @@ const storage = new SyncStorageAdapter({
|
|
|
56
63
|
Headless, async-first save-and-collect primitives for React applications.
|
|
57
64
|
|
|
58
65
|
```tsx
|
|
59
|
-
import {
|
|
66
|
+
import { LocalStorageAdapter } from "@keepkit/core/storage";
|
|
67
|
+
import { KeepButton, KeepProvider } from "@keepkit/core/react";
|
|
60
68
|
|
|
61
69
|
const storage = new LocalStorageAdapter({ key: "my-app:items" });
|
|
62
70
|
|
|
@@ -75,8 +83,14 @@ Use `useKeepItem(id, payload)` for saving, toggling, removing, and updating note
|
|
|
75
83
|
|
|
76
84
|
Use `exportItems(adapter)` and `importItems(adapter, json, { mode: "replace" | "merge" })` for versioned JSON backups. Results include imported and failed counts.
|
|
77
85
|
|
|
86
|
+
Localize the default `KeepButton` ARIA label with `savedAriaLabel` / `unsavedAriaLabel` or `getAriaLabel`. `useKeepItem(id).refreshMetadata` and `isKeepItemMetadataStale` fetch current metadata, while `useKeepList().revalidate` or `useKeepContext().revalidateItems` detects deleted, private, and expired targets. Pass `removeStatuses` explicitly when detected records should be cleaned up.
|
|
87
|
+
|
|
78
88
|
For larger applications, wrap a local adapter with `SyncStorageAdapter` to persist a durable offline queue and flush it with `flushSync()` when connectivity returns. Use `syncState` from `KeepProvider` for `pending`, `syncing`, `conflict`, and `error` feedback. `KeepProvider` also accepts a Zod-like `parse`, `safeParse`, or Standard Schema validator through `schema`; invalid stored records can be rejected or dropped with `invalidItemPolicy`.
|
|
79
89
|
|
|
80
90
|
`useKeepList` supports `savedBetween`, tokenized `search` with `and` / `or` modes, `filterFn`, and `tagCounts`. Use `createKeepInvalidationPlugin` to connect TanStack Query, SWR, or another cache without adding framework dependencies to core.
|
|
81
91
|
|
|
92
|
+
Pass a server-loaded list as `KeepProvider initialItems={itemsFromServer}` when the initial client render should use the same snapshot while storage hydrates. The first refresh then updates the state from the adapter. Framework-neutral and RSC server code can import `@keepkit/core/core`; React bindings, storage, and schema helpers are also available from `@keepkit/core/react`, `@keepkit/core/storage`, and `@keepkit/core/schema` respectively.
|
|
93
|
+
|
|
94
|
+
In restricted browser environments, `createBrowserStorageAdapter` prefers IndexedDB and switches to `LocalStorageAdapter` when IndexedDB cannot be accessed. Use `FallbackStorageAdapter` to compose your own primary and fallback adapters. `useKeepShortcut({ key: "k", modifier: "meta", id, itemPayload })` binds a save action while ignoring editable fields. `KeepButton asChild` automatically supplies `aria-pressed`, `aria-disabled`, a button role, and Space/Enter activation.
|
|
95
|
+
|
|
82
96
|
See the [root README](../../README.md) for detailed usage and development commands, and the [release notes](../../RELEASE_NOTES.md) for the current changes.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
//# sourceMappingURL=chunk-5QSZP6MT.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
// src/query.ts
|
|
2
|
+
function queryKeepItems(source, options = {}) {
|
|
3
|
+
const filtered = source.filter((item) => {
|
|
4
|
+
const [from, to] = options.savedBetween ?? [];
|
|
5
|
+
const savedAt = item.savedAt;
|
|
6
|
+
const lowerBound = from === void 0 ? void 0 : toTimestamp(from);
|
|
7
|
+
const upperBound = to === void 0 ? void 0 : toTimestamp(to);
|
|
8
|
+
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);
|
|
9
|
+
});
|
|
10
|
+
const tagCounts = getTagCounts(filtered);
|
|
11
|
+
const sortBy = options.sortBy ?? options.sort?.by;
|
|
12
|
+
const direction = (options.order ?? options.sort?.direction) === "asc" ? 1 : -1;
|
|
13
|
+
const sorted = sortBy ? [...filtered].sort((a, b) => (a[sortBy] - b[sortBy]) * direction) : filtered;
|
|
14
|
+
const offset = Math.max(0, options.offset ?? 0);
|
|
15
|
+
const items = options.limit === void 0 ? sorted.slice(offset) : sorted.slice(offset, offset + Math.max(0, options.limit));
|
|
16
|
+
return { items, totalCount: sorted.length, tagCounts };
|
|
17
|
+
}
|
|
18
|
+
function getTagCounts(items) {
|
|
19
|
+
const counts = {};
|
|
20
|
+
for (const item of items) {
|
|
21
|
+
for (const tag of item.tags ?? []) counts[tag] = (counts[tag] ?? 0) + 1;
|
|
22
|
+
}
|
|
23
|
+
return Object.fromEntries(Object.entries(counts).sort(([a], [b]) => a.localeCompare(b)));
|
|
24
|
+
}
|
|
25
|
+
function matchesSearch(item, searchQuery, search) {
|
|
26
|
+
const query = search?.query ?? searchQuery;
|
|
27
|
+
if (!query?.trim()) return true;
|
|
28
|
+
const fields = search?.fields ?? ["note", "meta", "tags"];
|
|
29
|
+
const values = fields.map((field) => {
|
|
30
|
+
if (field === "note") return item.note ?? "";
|
|
31
|
+
if (field === "tags") return (item.tags ?? []).join(" ");
|
|
32
|
+
try {
|
|
33
|
+
return JSON.stringify(item.meta) ?? "";
|
|
34
|
+
} catch {
|
|
35
|
+
return String(item.meta);
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
const text = values.join(" ").toLocaleLowerCase();
|
|
39
|
+
if (!search) return text.includes(query.trim().toLocaleLowerCase());
|
|
40
|
+
const normalized = query.trim().toLocaleLowerCase();
|
|
41
|
+
const needles = search.tokenize === false ? [normalized] : normalized.split(/\s+/).filter(Boolean);
|
|
42
|
+
const matches = needles.map((needle) => text.includes(needle));
|
|
43
|
+
return search.mode === "or" ? matches.some(Boolean) : matches.every(Boolean);
|
|
44
|
+
}
|
|
45
|
+
function toTimestamp(value) {
|
|
46
|
+
return value instanceof Date ? value.getTime() : value;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// src/revalidation.ts
|
|
50
|
+
function isKeepItemMetadataStale(item, maxAgeMs, now = Date.now) {
|
|
51
|
+
return item.metaUpdatedAt === void 0 || now() - item.metaUpdatedAt >= maxAgeMs;
|
|
52
|
+
}
|
|
53
|
+
async function revalidateKeepItems(source, revalidator, options = {}) {
|
|
54
|
+
const removeStatuses = new Set(options.removeStatuses ?? []);
|
|
55
|
+
const now = options.now ?? Date.now;
|
|
56
|
+
const items = [];
|
|
57
|
+
const updatedItems = [];
|
|
58
|
+
const removedIds = [];
|
|
59
|
+
const results = [];
|
|
60
|
+
for (const item of source) {
|
|
61
|
+
const rawResult = await revalidator(item);
|
|
62
|
+
const result = typeof rawResult === "string" ? { status: rawResult } : rawResult;
|
|
63
|
+
if (result.status === "available") {
|
|
64
|
+
const timestamp = now();
|
|
65
|
+
const updated = result.meta === void 0 ? item : { ...item, meta: result.meta, metaUpdatedAt: timestamp, updatedAt: timestamp };
|
|
66
|
+
const didUpdate = updated !== item;
|
|
67
|
+
items.push(updated);
|
|
68
|
+
if (didUpdate) updatedItems.push(updated);
|
|
69
|
+
results.push({ item: updated, status: "available", updated: didUpdate });
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
const shouldRemove = removeStatuses.has(result.status);
|
|
73
|
+
if (shouldRemove) removedIds.push(item.id);
|
|
74
|
+
else items.push(item);
|
|
75
|
+
results.push({ item, status: result.status, reason: result.reason, updated: false });
|
|
76
|
+
}
|
|
77
|
+
return {
|
|
78
|
+
items,
|
|
79
|
+
checked: source.length,
|
|
80
|
+
updated: updatedItems.length,
|
|
81
|
+
removed: removedIds.length,
|
|
82
|
+
updatedItems,
|
|
83
|
+
removedIds,
|
|
84
|
+
results
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
async function reconcileKeepItems(storage, revalidator, options = {}) {
|
|
88
|
+
const source = await storage.getAll();
|
|
89
|
+
const summary = await revalidateKeepItems(source, revalidator, options);
|
|
90
|
+
if (summary.updatedItems.length > 0) await persistItems(storage, summary.updatedItems);
|
|
91
|
+
if (summary.removedIds.length > 0) await removeItems(storage, summary.removedIds);
|
|
92
|
+
return summary;
|
|
93
|
+
}
|
|
94
|
+
async function persistItems(storage, items) {
|
|
95
|
+
if (storage.setMany) {
|
|
96
|
+
await storage.setMany(items);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
for (const item of items) await storage.set(item);
|
|
100
|
+
}
|
|
101
|
+
async function removeItems(storage, ids) {
|
|
102
|
+
if (storage.removeMany) {
|
|
103
|
+
await storage.removeMany(ids);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
for (const id of ids) await storage.remove(id);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// src/store.ts
|
|
110
|
+
var KeepStore = class {
|
|
111
|
+
constructor(initialState) {
|
|
112
|
+
this.listeners = /* @__PURE__ */ new Set();
|
|
113
|
+
this.getSnapshot = () => this.state;
|
|
114
|
+
this.subscribe = (listener) => {
|
|
115
|
+
this.listeners.add(listener);
|
|
116
|
+
return () => this.listeners.delete(listener);
|
|
117
|
+
};
|
|
118
|
+
this.state = initialState;
|
|
119
|
+
}
|
|
120
|
+
setState(next) {
|
|
121
|
+
let changed = false;
|
|
122
|
+
for (const key of Object.keys(next)) {
|
|
123
|
+
if (!Object.is(this.state[key], next[key])) {
|
|
124
|
+
changed = true;
|
|
125
|
+
break;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
if (!changed) return;
|
|
129
|
+
this.state = { ...this.state, ...next };
|
|
130
|
+
for (const listener of this.listeners) listener();
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
export {
|
|
135
|
+
queryKeepItems,
|
|
136
|
+
getTagCounts,
|
|
137
|
+
isKeepItemMetadataStale,
|
|
138
|
+
revalidateKeepItems,
|
|
139
|
+
reconcileKeepItems,
|
|
140
|
+
KeepStore
|
|
141
|
+
};
|
|
142
|
+
//# sourceMappingURL=chunk-J7NKIOXV.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/query.ts","../src/revalidation.ts","../src/store.ts"],"sourcesContent":["import type { KeepItem } from \"./types\";\n\nexport type KeepListOptions<TMeta = Record<string, unknown>> = {\n targetType?: string;\n tag?: string;\n tags?: string[];\n sort?: {\n by: \"savedAt\" | \"updatedAt\";\n direction?: \"asc\" | \"desc\";\n };\n searchQuery?: string;\n search?: {\n query: string;\n mode?: \"and\" | \"or\";\n tokenize?: boolean;\n fields?: Array<\"note\" | \"meta\" | \"tags\">;\n };\n sortBy?: \"savedAt\" | \"updatedAt\";\n order?: \"asc\" | \"desc\";\n limit?: number;\n offset?: number;\n filter?: (item: KeepItem<TMeta>) => boolean;\n filterFn?: (item: KeepItem<TMeta>) => boolean;\n savedBetween?: readonly [Date | number, Date | number];\n};\n\nexport type QueryKeepItemsResult<TMeta = Record<string, unknown>> = {\n items: KeepItem<TMeta>[];\n totalCount: number;\n tagCounts: Record<string, number>;\n};\n\n/** Apply the same filtering and pagination rules as useKeepList without React. */\nexport function queryKeepItems<TMeta = Record<string, unknown>>(\n source: KeepItem<TMeta>[],\n options: KeepListOptions<TMeta> = {},\n): QueryKeepItemsResult<TMeta> {\n const filtered = source.filter((item) => {\n const [from, to] = options.savedBetween ?? [];\n const savedAt = item.savedAt;\n const lowerBound = from === undefined ? undefined : toTimestamp(from);\n const upperBound = to === undefined ? undefined : toTimestamp(to);\n return (\n (options.targetType === undefined || item.targetType === options.targetType) &&\n (options.tag === undefined || item.tags?.includes(options.tag) === true) &&\n (options.tags === undefined || options.tags.every((tag) => item.tags?.includes(tag))) &&\n (lowerBound === undefined || savedAt >= lowerBound) &&\n (upperBound === undefined || savedAt <= upperBound) &&\n matchesSearch(item, options.searchQuery, options.search) &&\n (options.filter?.(item) ?? true) &&\n (options.filterFn?.(item) ?? true)\n );\n });\n const tagCounts = getTagCounts(filtered);\n const sortBy = options.sortBy ?? options.sort?.by;\n const direction = (options.order ?? options.sort?.direction) === \"asc\" ? 1 : -1;\n const sorted = sortBy ? [...filtered].sort((a, b) => (a[sortBy] - b[sortBy]) * direction) : filtered;\n const offset = Math.max(0, options.offset ?? 0);\n const items =\n options.limit === undefined ? sorted.slice(offset) : sorted.slice(offset, offset + Math.max(0, options.limit));\n return { items, totalCount: sorted.length, tagCounts };\n}\n\nexport function getTagCounts<TMeta = Record<string, unknown>>(items: KeepItem<TMeta>[]): Record<string, number> {\n const counts: Record<string, number> = {};\n for (const item of items) {\n for (const tag of item.tags ?? []) counts[tag] = (counts[tag] ?? 0) + 1;\n }\n return Object.fromEntries(Object.entries(counts).sort(([a], [b]) => a.localeCompare(b)));\n}\n\nfunction matchesSearch<TMeta>(\n item: KeepItem<TMeta>,\n searchQuery?: string,\n search?: KeepListOptions<TMeta>[\"search\"],\n): boolean {\n const query = search?.query ?? searchQuery;\n if (!query?.trim()) return true;\n const fields = search?.fields ?? [\"note\", \"meta\", \"tags\"];\n const values = fields.map((field) => {\n if (field === \"note\") return item.note ?? \"\";\n if (field === \"tags\") return (item.tags ?? []).join(\" \");\n try {\n return JSON.stringify(item.meta) ?? \"\";\n } catch {\n return String(item.meta);\n }\n });\n const text = values.join(\" \").toLocaleLowerCase();\n if (!search) return text.includes(query.trim().toLocaleLowerCase());\n const normalized = query.trim().toLocaleLowerCase();\n const needles = search.tokenize === false ? [normalized] : normalized.split(/\\s+/).filter(Boolean);\n const matches = needles.map((needle) => text.includes(needle));\n return search.mode === \"or\" ? matches.some(Boolean) : matches.every(Boolean);\n}\n\nfunction toTimestamp(value: Date | number): number {\n return value instanceof Date ? value.getTime() : value;\n}\n","import type { KeepItem, StorageAdapter } from \"./types\";\n\nexport type KeepItemStatus = \"available\" | \"deleted\" | \"private\" | \"expired\" | \"unknown\";\n\nexport type KeepItemRevalidationResult<TMeta = Record<string, unknown>> =\n | { status: \"available\"; meta?: TMeta }\n | { status: Exclude<KeepItemStatus, \"available\">; reason?: string };\n\nexport type KeepItemRevalidator<TMeta = Record<string, unknown>> = (\n item: KeepItem<TMeta>,\n) =>\n | KeepItemRevalidationResult<TMeta>\n | KeepItemRevalidationResult<TMeta>[\"status\"]\n | Promise<KeepItemRevalidationResult<TMeta> | KeepItemRevalidationResult<TMeta>[\"status\"]>;\n\nexport type KeepItemMetadataRefresher<TMeta = Record<string, unknown>> = (\n item: KeepItem<TMeta>,\n) => TMeta | Promise<TMeta>;\n\n/** Return whether source metadata should be fetched again based on its age. */\nexport function isKeepItemMetadataStale<TMeta>(\n item: KeepItem<TMeta>,\n maxAgeMs: number,\n now: () => number = Date.now,\n): boolean {\n return item.metaUpdatedAt === undefined || now() - item.metaUpdatedAt >= maxAgeMs;\n}\n\nexport type KeepItemRevalidationRecord<TMeta = Record<string, unknown>> = {\n item: KeepItem<TMeta>;\n status: KeepItemStatus;\n reason?: string;\n updated: boolean;\n};\n\nexport type RevalidateKeepItemsOptions = {\n /** Statuses that should be removed after they are detected. Detection is the default. */\n removeStatuses?: Array<Exclude<KeepItemStatus, \"available\">>;\n now?: () => number;\n};\n\nexport type KeepItemRevalidationSummary<TMeta = Record<string, unknown>> = {\n items: KeepItem<TMeta>[];\n checked: number;\n updated: number;\n removed: number;\n updatedItems: KeepItem<TMeta>[];\n removedIds: string[];\n results: KeepItemRevalidationRecord<TMeta>[];\n};\n\n/** Revalidate saved items without coupling the checker to a network client. */\nexport async function revalidateKeepItems<TMeta = Record<string, unknown>>(\n source: KeepItem<TMeta>[],\n revalidator: KeepItemRevalidator<TMeta>,\n options: RevalidateKeepItemsOptions = {},\n): Promise<KeepItemRevalidationSummary<TMeta>> {\n const removeStatuses = new Set(options.removeStatuses ?? []);\n const now = options.now ?? Date.now;\n const items: KeepItem<TMeta>[] = [];\n const updatedItems: KeepItem<TMeta>[] = [];\n const removedIds: string[] = [];\n const results: KeepItemRevalidationRecord<TMeta>[] = [];\n\n for (const item of source) {\n const rawResult = await revalidator(item);\n const result: KeepItemRevalidationResult<TMeta> = typeof rawResult === \"string\" ? { status: rawResult } : rawResult;\n if (result.status === \"available\") {\n const timestamp = now();\n const updated =\n result.meta === undefined\n ? item\n : { ...item, meta: result.meta, metaUpdatedAt: timestamp, updatedAt: timestamp };\n const didUpdate = updated !== item;\n items.push(updated);\n if (didUpdate) updatedItems.push(updated);\n results.push({ item: updated, status: \"available\", updated: didUpdate });\n continue;\n }\n\n const shouldRemove = removeStatuses.has(result.status);\n if (shouldRemove) removedIds.push(item.id);\n else items.push(item);\n results.push({ item, status: result.status, reason: result.reason, updated: false });\n }\n\n return {\n items,\n checked: source.length,\n updated: updatedItems.length,\n removed: removedIds.length,\n updatedItems,\n removedIds,\n results,\n };\n}\n\n/** Revalidate and persist saved items for framework-neutral applications. */\nexport async function reconcileKeepItems<TMeta = Record<string, unknown>>(\n storage: StorageAdapter<TMeta>,\n revalidator: KeepItemRevalidator<TMeta>,\n options: RevalidateKeepItemsOptions = {},\n): Promise<KeepItemRevalidationSummary<TMeta>> {\n const source = await storage.getAll();\n const summary = await revalidateKeepItems(source, revalidator, options);\n if (summary.updatedItems.length > 0) await persistItems(storage, summary.updatedItems);\n if (summary.removedIds.length > 0) await removeItems(storage, summary.removedIds);\n return summary;\n}\n\nasync function persistItems<TMeta>(storage: StorageAdapter<TMeta>, items: KeepItem<TMeta>[]): Promise<void> {\n if (storage.setMany) {\n await storage.setMany(items);\n return;\n }\n for (const item of items) await storage.set(item);\n}\n\nasync function removeItems<TMeta>(storage: StorageAdapter<TMeta>, ids: string[]): Promise<void> {\n if (storage.removeMany) {\n await storage.removeMany(ids);\n return;\n }\n for (const id of ids) await storage.remove(id);\n}\n","import type {\n KeepItemMetadataRefresher,\n KeepItemRevalidationSummary,\n KeepItemRevalidator,\n RevalidateKeepItemsOptions,\n} from \"./revalidation\";\nimport type { KeepItem } from \"./types\";\n\nexport type KeepStoreState<TMeta = Record<string, unknown>> = {\n items: KeepItem<TMeta>[];\n isLoading: boolean;\n isHydrated: boolean;\n isMutating: boolean;\n error: unknown | null;\n};\n\nexport type KeepStoreActions<TMeta = Record<string, unknown>> = {\n saveItem: (item: KeepItem<TMeta>) => Promise<void>;\n updateNote: (id: string, note?: string) => Promise<void>;\n updateTags: (id: string, tags?: string[]) => Promise<void>;\n updateTagsBatch: (ids: string[], tags?: string[]) => Promise<void>;\n addTagsBatch: (ids: string[], tags: string[]) => Promise<void>;\n removeTagsBatch: (ids: string[], tags: string[]) => Promise<void>;\n removeItem: (id: string) => Promise<void>;\n removeItems: (ids: string[]) => Promise<void>;\n clear: () => Promise<void>;\n refresh: () => Promise<void>;\n refreshItemMetadata: (id: string, refresh: KeepItemMetadataRefresher<TMeta>) => Promise<void>;\n revalidateItems: (\n revalidator: KeepItemRevalidator<TMeta>,\n options?: RevalidateKeepItemsOptions,\n ) => Promise<KeepItemRevalidationSummary<TMeta>>;\n};\n\nexport class KeepStore<TMeta = Record<string, unknown>> {\n private state: KeepStoreState<TMeta>;\n private readonly listeners = new Set<() => void>();\n\n constructor(initialState: KeepStoreState<TMeta>) {\n this.state = initialState;\n }\n\n getSnapshot = (): KeepStoreState<TMeta> => this.state;\n\n subscribe = (listener: () => void): (() => void) => {\n this.listeners.add(listener);\n return () => this.listeners.delete(listener);\n };\n\n setState(next: Partial<KeepStoreState<TMeta>>): void {\n let changed = false;\n for (const key of Object.keys(next) as Array<keyof KeepStoreState<TMeta>>) {\n if (!Object.is(this.state[key], next[key])) {\n changed = true;\n break;\n }\n }\n if (!changed) return;\n this.state = { ...this.state, ...next };\n for (const listener of this.listeners) listener();\n }\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// src/schema.ts
|
|
2
|
+
var KeepSchemaValidationError = class extends Error {
|
|
3
|
+
constructor(message, options = {}) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.name = "KeepSchemaValidationError";
|
|
6
|
+
this.cause = options.cause;
|
|
7
|
+
this.itemId = options.itemId;
|
|
8
|
+
}
|
|
9
|
+
};
|
|
10
|
+
async function parseKeepMeta(schema, value) {
|
|
11
|
+
try {
|
|
12
|
+
if ("parse" in schema) return await schema.parse(value);
|
|
13
|
+
if ("safeParse" in schema) {
|
|
14
|
+
const result2 = await schema.safeParse(value);
|
|
15
|
+
if (result2.success) return result2.data;
|
|
16
|
+
throw new KeepSchemaValidationError("KeepKit metadata did not match the configured schema.", {
|
|
17
|
+
cause: result2.error
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
const result = await schema["~standard"].validate(value);
|
|
21
|
+
if (!result.issues && "value" in result) return result.value;
|
|
22
|
+
throw new KeepSchemaValidationError("KeepKit metadata did not match the configured schema.", {
|
|
23
|
+
cause: result.issues
|
|
24
|
+
});
|
|
25
|
+
} catch (cause) {
|
|
26
|
+
if (cause instanceof KeepSchemaValidationError) throw cause;
|
|
27
|
+
throw new KeepSchemaValidationError("KeepKit metadata did not match the configured schema.", {
|
|
28
|
+
cause
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
async function validateKeepItem(item, schema) {
|
|
33
|
+
return { ...item, meta: await parseKeepMeta(schema, item.meta) };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export {
|
|
37
|
+
KeepSchemaValidationError,
|
|
38
|
+
parseKeepMeta,
|
|
39
|
+
validateKeepItem
|
|
40
|
+
};
|
|
41
|
+
//# sourceMappingURL=chunk-THZ3ACR2.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/schema.ts"],"sourcesContent":["import type { KeepItem, KeepSchema } from \"./types\";\n\nexport class KeepSchemaValidationError extends Error {\n readonly cause?: unknown;\n readonly itemId?: string;\n\n constructor(message: string, options: { cause?: unknown; itemId?: string } = {}) {\n super(message);\n this.name = \"KeepSchemaValidationError\";\n this.cause = options.cause;\n this.itemId = options.itemId;\n }\n}\n\n/** Parse metadata with a Zod-like, safeParse-like, or Standard Schema parser. */\nexport async function parseKeepMeta<T>(schema: KeepSchema<T>, value: unknown): Promise<T> {\n try {\n if (\"parse\" in schema) return await schema.parse(value);\n\n if (\"safeParse\" in schema) {\n const result = await schema.safeParse(value);\n if (result.success) return result.data;\n throw new KeepSchemaValidationError(\"KeepKit metadata did not match the configured schema.\", {\n cause: result.error,\n });\n }\n\n const result = await schema[\"~standard\"].validate(value);\n if (!result.issues && \"value\" in result) return result.value as T;\n throw new KeepSchemaValidationError(\"KeepKit metadata did not match the configured schema.\", {\n cause: result.issues,\n });\n } catch (cause) {\n if (cause instanceof KeepSchemaValidationError) throw cause;\n throw new KeepSchemaValidationError(\"KeepKit metadata did not match the configured schema.\", {\n cause,\n });\n }\n}\n\nexport async function validateKeepItem<TMeta>(\n item: KeepItem<unknown>,\n schema: KeepSchema<TMeta>,\n): Promise<KeepItem<TMeta>> {\n return { ...item, meta: await parseKeepMeta(schema, item.meta) };\n}\n"],"mappings":";AAEO,IAAM,4BAAN,cAAwC,MAAM;AAAA,EAInD,YAAY,SAAiB,UAAgD,CAAC,GAAG;AAC/E,UAAM,OAAO;AACb,SAAK,OAAO;AACZ,SAAK,QAAQ,QAAQ;AACrB,SAAK,SAAS,QAAQ;AAAA,EACxB;AACF;AAGA,eAAsB,cAAiB,QAAuB,OAA4B;AACxF,MAAI;AACF,QAAI,WAAW,OAAQ,QAAO,MAAM,OAAO,MAAM,KAAK;AAEtD,QAAI,eAAe,QAAQ;AACzB,YAAMA,UAAS,MAAM,OAAO,UAAU,KAAK;AAC3C,UAAIA,QAAO,QAAS,QAAOA,QAAO;AAClC,YAAM,IAAI,0BAA0B,yDAAyD;AAAA,QAC3F,OAAOA,QAAO;AAAA,MAChB,CAAC;AAAA,IACH;AAEA,UAAM,SAAS,MAAM,OAAO,WAAW,EAAE,SAAS,KAAK;AACvD,QAAI,CAAC,OAAO,UAAU,WAAW,OAAQ,QAAO,OAAO;AACvD,UAAM,IAAI,0BAA0B,yDAAyD;AAAA,MAC3F,OAAO,OAAO;AAAA,IAChB,CAAC;AAAA,EACH,SAAS,OAAO;AACd,QAAI,iBAAiB,0BAA2B,OAAM;AACtD,UAAM,IAAI,0BAA0B,yDAAyD;AAAA,MAC3F;AAAA,IACF,CAAC;AAAA,EACH;AACF;AAEA,eAAsB,iBACpB,MACA,QAC0B;AAC1B,SAAO,EAAE,GAAG,MAAM,MAAM,MAAM,cAAc,QAAQ,KAAK,IAAI,EAAE;AACjE;","names":["result"]}
|
|
@@ -136,6 +136,39 @@ var IndexedDBSyncQueueAdapter = class {
|
|
|
136
136
|
});
|
|
137
137
|
}
|
|
138
138
|
};
|
|
139
|
+
var FallbackSyncQueueAdapter = class {
|
|
140
|
+
constructor(options) {
|
|
141
|
+
this.active = "primary";
|
|
142
|
+
this.primary = options.primary;
|
|
143
|
+
this.fallback = options.fallback;
|
|
144
|
+
this.shouldFallback = options.shouldFallback ?? (() => true);
|
|
145
|
+
}
|
|
146
|
+
get isUsingFallback() {
|
|
147
|
+
return this.active === "fallback";
|
|
148
|
+
}
|
|
149
|
+
getAll() {
|
|
150
|
+
return this.execute((adapter) => adapter.getAll());
|
|
151
|
+
}
|
|
152
|
+
setMany(operations) {
|
|
153
|
+
return this.execute((adapter) => adapter.setMany(operations));
|
|
154
|
+
}
|
|
155
|
+
remove(operationIds) {
|
|
156
|
+
return this.execute((adapter) => adapter.remove(operationIds));
|
|
157
|
+
}
|
|
158
|
+
clear() {
|
|
159
|
+
return this.execute((adapter) => adapter.clear());
|
|
160
|
+
}
|
|
161
|
+
async execute(operation) {
|
|
162
|
+
const adapter = this.active === "primary" ? this.primary : this.fallback;
|
|
163
|
+
try {
|
|
164
|
+
return await operation(adapter);
|
|
165
|
+
} catch (error) {
|
|
166
|
+
if (this.active !== "primary" || !this.shouldFallback(error)) throw error;
|
|
167
|
+
this.active = "fallback";
|
|
168
|
+
return operation(this.fallback);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
};
|
|
139
172
|
var SyncStorageAdapter = class {
|
|
140
173
|
constructor(options) {
|
|
141
174
|
this.listeners = /* @__PURE__ */ new Set();
|
|
@@ -158,11 +191,7 @@ var SyncStorageAdapter = class {
|
|
|
158
191
|
};
|
|
159
192
|
this.local = options.local;
|
|
160
193
|
this.remote = options.remote;
|
|
161
|
-
this.queue = options.queue ?? (
|
|
162
|
-
databaseName: options.queueDatabaseName
|
|
163
|
-
}) : new LocalStorageSyncQueueAdapter({
|
|
164
|
-
key: options.queueKey ?? `${DEFAULT_SYNC_QUEUE_KEY}:${options.local.storageKey ?? "default"}`
|
|
165
|
-
}));
|
|
194
|
+
this.queue = options.queue ?? createDefaultQueue(options);
|
|
166
195
|
this.clientId = options.clientId ?? createId();
|
|
167
196
|
this.now = options.now ?? Date.now;
|
|
168
197
|
this.resolveConflict = options.resolveConflict;
|
|
@@ -171,6 +200,7 @@ var SyncStorageAdapter = class {
|
|
|
171
200
|
this.onlineHandler = () => void this.flushSync();
|
|
172
201
|
window.addEventListener("online", this.onlineHandler);
|
|
173
202
|
}
|
|
203
|
+
void this.resumeQueue();
|
|
174
204
|
}
|
|
175
205
|
getAll() {
|
|
176
206
|
return this.local.getAll();
|
|
@@ -331,10 +361,23 @@ var SyncStorageAdapter = class {
|
|
|
331
361
|
this.queueLoadPromise = this.queue.getAll().then((items) => {
|
|
332
362
|
this.queueItems = items;
|
|
333
363
|
this.queueLoaded = true;
|
|
364
|
+
}).catch((error) => {
|
|
365
|
+
this.queueLoadPromise = void 0;
|
|
366
|
+
throw error;
|
|
334
367
|
});
|
|
335
368
|
}
|
|
336
369
|
await this.queueLoadPromise;
|
|
337
370
|
}
|
|
371
|
+
async resumeQueue() {
|
|
372
|
+
try {
|
|
373
|
+
await this.loadQueue();
|
|
374
|
+
if (this.queueItems.length === 0) return;
|
|
375
|
+
this.updateState({ status: "pending", pendingCount: this.queueItems.length });
|
|
376
|
+
if (isBrowserOnline()) await this.flushSync();
|
|
377
|
+
} catch (error) {
|
|
378
|
+
this.updateState({ status: "error", error });
|
|
379
|
+
}
|
|
380
|
+
}
|
|
338
381
|
async persistQueue(next) {
|
|
339
382
|
const previousIds = new Set(this.queueItems.map((operation) => operation.operationId));
|
|
340
383
|
const nextIds = new Set(next.map((operation) => operation.operationId));
|
|
@@ -352,9 +395,7 @@ var SyncStorageAdapter = class {
|
|
|
352
395
|
}
|
|
353
396
|
async replaceQueued(previous, next) {
|
|
354
397
|
await this.persistQueue(
|
|
355
|
-
this.queueItems.map(
|
|
356
|
-
(operation) => operation.operationId === previous.operationId ? next : operation
|
|
357
|
-
)
|
|
398
|
+
this.queueItems.map((operation) => operation.operationId === previous.operationId ? next : operation)
|
|
358
399
|
);
|
|
359
400
|
this.setPendingState();
|
|
360
401
|
}
|
|
@@ -432,6 +473,22 @@ function getBrowserIndexedDB() {
|
|
|
432
473
|
if (typeof indexedDB === "undefined") return void 0;
|
|
433
474
|
return indexedDB;
|
|
434
475
|
}
|
|
476
|
+
function isBrowserOnline() {
|
|
477
|
+
return typeof navigator === "undefined" || navigator.onLine !== false;
|
|
478
|
+
}
|
|
479
|
+
function createDefaultQueue(options) {
|
|
480
|
+
const queueKey = options.queueKey ?? `${DEFAULT_SYNC_QUEUE_KEY}:${options.local.storageKey ?? "default"}`;
|
|
481
|
+
const fallback = new LocalStorageSyncQueueAdapter({ key: queueKey });
|
|
482
|
+
const indexedDB2 = getBrowserIndexedDB();
|
|
483
|
+
if (!indexedDB2) return fallback;
|
|
484
|
+
return new FallbackSyncQueueAdapter({
|
|
485
|
+
primary: new IndexedDBSyncQueueAdapter({
|
|
486
|
+
databaseName: options.queueDatabaseName,
|
|
487
|
+
indexedDB: indexedDB2
|
|
488
|
+
}),
|
|
489
|
+
fallback
|
|
490
|
+
});
|
|
491
|
+
}
|
|
435
492
|
function requestToPromise(request) {
|
|
436
493
|
return new Promise((resolve, reject) => {
|
|
437
494
|
request.onsuccess = () => resolve(request.result);
|
|
@@ -450,6 +507,134 @@ function transactionToPromise(transaction) {
|
|
|
450
507
|
var DEFAULT_STORAGE_KEY = "keepkit:items";
|
|
451
508
|
var DEFAULT_INDEXEDDB_DATABASE = "keepkit";
|
|
452
509
|
var DEFAULT_INDEXEDDB_STORE = "items";
|
|
510
|
+
var FallbackStorageAdapter = class {
|
|
511
|
+
constructor(options) {
|
|
512
|
+
this.active = "primary";
|
|
513
|
+
this.listeners = /* @__PURE__ */ new Set();
|
|
514
|
+
this.primary = options.primary;
|
|
515
|
+
this.fallback = options.fallback;
|
|
516
|
+
this.shouldFallback = options.shouldFallback ?? isRecoverableStorageError;
|
|
517
|
+
this.onFallback = options.onFallback;
|
|
518
|
+
this.migrateFallbackOnEmpty = options.migrateFallbackOnEmpty ?? false;
|
|
519
|
+
this.mirrorWrites = options.mirrorWrites ?? false;
|
|
520
|
+
this.storageKey = options.fallback.storageKey ?? options.primary.storageKey;
|
|
521
|
+
}
|
|
522
|
+
get isUsingFallback() {
|
|
523
|
+
return this.active === "fallback";
|
|
524
|
+
}
|
|
525
|
+
getAll() {
|
|
526
|
+
return this.readAll();
|
|
527
|
+
}
|
|
528
|
+
set(item) {
|
|
529
|
+
return this.executeWrite((adapter) => adapter.set(item));
|
|
530
|
+
}
|
|
531
|
+
setMany(items) {
|
|
532
|
+
return this.executeWrite(
|
|
533
|
+
(adapter) => adapter.setMany ? adapter.setMany(items) : Promise.all(items.map((item) => adapter.set(item))).then(() => void 0)
|
|
534
|
+
);
|
|
535
|
+
}
|
|
536
|
+
remove(id) {
|
|
537
|
+
return this.executeWrite((adapter) => adapter.remove(id));
|
|
538
|
+
}
|
|
539
|
+
removeMany(ids) {
|
|
540
|
+
return this.executeWrite(
|
|
541
|
+
(adapter) => adapter.removeMany ? adapter.removeMany(ids) : Promise.all(ids.map((id) => adapter.remove(id))).then(() => void 0)
|
|
542
|
+
);
|
|
543
|
+
}
|
|
544
|
+
clear() {
|
|
545
|
+
return this.executeWrite((adapter) => adapter.clear());
|
|
546
|
+
}
|
|
547
|
+
merge(localItems) {
|
|
548
|
+
return this.execute(async (adapter) => {
|
|
549
|
+
const merged = adapter.merge ? await adapter.merge(localItems) : await mergeItems(adapter, localItems);
|
|
550
|
+
if (this.active === "primary" && this.mirrorWrites) {
|
|
551
|
+
try {
|
|
552
|
+
if (this.fallback.setMany) await this.fallback.setMany(merged);
|
|
553
|
+
else for (const item of merged) await this.fallback.set(item);
|
|
554
|
+
} catch {
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
return merged;
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
subscribe(listener) {
|
|
561
|
+
this.listeners.add(listener);
|
|
562
|
+
const notify = (source) => {
|
|
563
|
+
if (source === this.active) listener();
|
|
564
|
+
};
|
|
565
|
+
const unsubscribePrimary = this.primary.subscribe?.(() => notify("primary"));
|
|
566
|
+
const unsubscribeFallback = this.fallback.subscribe?.(() => notify("fallback"));
|
|
567
|
+
return () => {
|
|
568
|
+
this.listeners.delete(listener);
|
|
569
|
+
unsubscribePrimary?.();
|
|
570
|
+
unsubscribeFallback?.();
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
async readAll() {
|
|
574
|
+
if (this.active === "fallback") return this.fallback.getAll();
|
|
575
|
+
let items;
|
|
576
|
+
try {
|
|
577
|
+
items = await this.primary.getAll();
|
|
578
|
+
} catch (error) {
|
|
579
|
+
if (!this.shouldFallback(error)) throw error;
|
|
580
|
+
this.activateFallback(error);
|
|
581
|
+
return this.fallback.getAll();
|
|
582
|
+
}
|
|
583
|
+
if (!this.migrateFallbackOnEmpty || items.length > 0) return items;
|
|
584
|
+
const fallbackItems = await this.fallback.getAll();
|
|
585
|
+
if (fallbackItems.length === 0) return items;
|
|
586
|
+
try {
|
|
587
|
+
if (this.primary.setMany) await this.primary.setMany(fallbackItems);
|
|
588
|
+
else for (const item of fallbackItems) await this.primary.set(item);
|
|
589
|
+
return fallbackItems;
|
|
590
|
+
} catch (error) {
|
|
591
|
+
if (!this.shouldFallback(error)) throw error;
|
|
592
|
+
this.activateFallback(error);
|
|
593
|
+
return fallbackItems;
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
activateFallback(error) {
|
|
597
|
+
this.active = "fallback";
|
|
598
|
+
this.onFallback?.(error);
|
|
599
|
+
for (const listener of this.listeners) listener();
|
|
600
|
+
}
|
|
601
|
+
async executeWrite(operation) {
|
|
602
|
+
const result = await this.execute(operation);
|
|
603
|
+
if (this.active === "primary" && this.mirrorWrites) {
|
|
604
|
+
try {
|
|
605
|
+
await operation(this.fallback);
|
|
606
|
+
} catch {
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
return result;
|
|
610
|
+
}
|
|
611
|
+
async execute(operation) {
|
|
612
|
+
const adapter = this.active === "primary" ? this.primary : this.fallback;
|
|
613
|
+
try {
|
|
614
|
+
return await operation(adapter);
|
|
615
|
+
} catch (error) {
|
|
616
|
+
if (this.active !== "primary" || !this.shouldFallback(error)) throw error;
|
|
617
|
+
this.activateFallback(error);
|
|
618
|
+
return operation(this.fallback);
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
};
|
|
622
|
+
function createBrowserStorageAdapter(options = {}) {
|
|
623
|
+
const browserIndexedDB = options.indexedDB ?? getBrowserIndexedDB2();
|
|
624
|
+
const fallback = new LocalStorageAdapter({ key: options.key, storage: options.storage });
|
|
625
|
+
if (!browserIndexedDB) return fallback;
|
|
626
|
+
return new FallbackStorageAdapter({
|
|
627
|
+
primary: new IndexedDBAdapter({
|
|
628
|
+
databaseName: options.databaseName,
|
|
629
|
+
storeName: options.storeName,
|
|
630
|
+
version: options.version,
|
|
631
|
+
indexedDB: browserIndexedDB
|
|
632
|
+
}),
|
|
633
|
+
fallback,
|
|
634
|
+
migrateFallbackOnEmpty: true,
|
|
635
|
+
mirrorWrites: true
|
|
636
|
+
});
|
|
637
|
+
}
|
|
453
638
|
function createStorageAdapter(options) {
|
|
454
639
|
const merge = options.merge;
|
|
455
640
|
const subscribe = options.subscribe;
|
|
@@ -469,6 +654,18 @@ function createStorageAdapter(options) {
|
|
|
469
654
|
...options.storageKey ? { storageKey: options.storageKey } : {}
|
|
470
655
|
};
|
|
471
656
|
}
|
|
657
|
+
async function mergeItems(adapter, localItems) {
|
|
658
|
+
const remoteItems = await adapter.getAll();
|
|
659
|
+
const byId = new Map(remoteItems.map((item) => [item.id, item]));
|
|
660
|
+
for (const item of localItems) {
|
|
661
|
+
const current = byId.get(item.id);
|
|
662
|
+
if (!current || item.updatedAt > current.updatedAt) byId.set(item.id, item);
|
|
663
|
+
}
|
|
664
|
+
const merged = [...byId.values()].sort((a, b) => b.updatedAt - a.updatedAt);
|
|
665
|
+
if (adapter.setMany) await adapter.setMany(merged);
|
|
666
|
+
else for (const item of merged) await adapter.set(item);
|
|
667
|
+
return merged;
|
|
668
|
+
}
|
|
472
669
|
var LocalStorageAdapter = class {
|
|
473
670
|
constructor(options = {}) {
|
|
474
671
|
this.storageKey = options.key ?? DEFAULT_STORAGE_KEY;
|
|
@@ -592,9 +789,7 @@ var IndexedDBAdapter = class {
|
|
|
592
789
|
if (!database) return [];
|
|
593
790
|
try {
|
|
594
791
|
const transaction = database.transaction(this.storeName, "readonly");
|
|
595
|
-
const value = await requestToPromise2(
|
|
596
|
-
transaction.objectStore(this.storeName).getAll()
|
|
597
|
-
);
|
|
792
|
+
const value = await requestToPromise2(transaction.objectStore(this.storeName).getAll());
|
|
598
793
|
if (!isKeepItemArray(value)) {
|
|
599
794
|
throw new KeepStorageParseError({ operation: "getAll", storageKey: this.storageKey });
|
|
600
795
|
}
|
|
@@ -657,8 +852,7 @@ var IndexedDBAdapter = class {
|
|
|
657
852
|
const byId = new Map(remoteItems.map((item) => [item.id, item]));
|
|
658
853
|
for (const localItem of localItems) {
|
|
659
854
|
const remoteItem = byId.get(localItem.id);
|
|
660
|
-
if (!remoteItem || localItem.updatedAt > remoteItem.updatedAt)
|
|
661
|
-
byId.set(localItem.id, localItem);
|
|
855
|
+
if (!remoteItem || localItem.updatedAt > remoteItem.updatedAt) byId.set(localItem.id, localItem);
|
|
662
856
|
}
|
|
663
857
|
const merged = [...byId.values()].sort((a, b) => b.updatedAt - a.updatedAt);
|
|
664
858
|
await this.setMany(merged);
|
|
@@ -710,7 +904,7 @@ var IndexedDBAdapter = class {
|
|
|
710
904
|
};
|
|
711
905
|
function isKeepItemArray(value) {
|
|
712
906
|
return Array.isArray(value) && value.every(
|
|
713
|
-
(item) => isRecord2(item) && typeof item.id === "string" && typeof item.savedAt === "number" && Number.isFinite(item.savedAt) && typeof item.updatedAt === "number" && Number.isFinite(item.updatedAt) && "meta" in item && (item.targetType === void 0 || typeof item.targetType === "string") && (item.note === void 0 || typeof item.note === "string") && (item.schemaVersion === void 0 || typeof item.schemaVersion === "number" && Number.isFinite(item.schemaVersion)) && (item.revision === void 0 || typeof item.revision === "string") && (item.tags === void 0 || Array.isArray(item.tags) && item.tags.every((tag) => typeof tag === "string"))
|
|
907
|
+
(item) => isRecord2(item) && typeof item.id === "string" && typeof item.savedAt === "number" && Number.isFinite(item.savedAt) && typeof item.updatedAt === "number" && Number.isFinite(item.updatedAt) && "meta" in item && (item.targetType === void 0 || typeof item.targetType === "string") && (item.note === void 0 || typeof item.note === "string") && (item.schemaVersion === void 0 || typeof item.schemaVersion === "number" && Number.isFinite(item.schemaVersion)) && (item.revision === void 0 || typeof item.revision === "string") && (item.metaUpdatedAt === void 0 || typeof item.metaUpdatedAt === "number" && Number.isFinite(item.metaUpdatedAt)) && (item.tags === void 0 || Array.isArray(item.tags) && item.tags.every((tag) => typeof tag === "string"))
|
|
714
908
|
);
|
|
715
909
|
}
|
|
716
910
|
function isRecord2(value) {
|
|
@@ -728,6 +922,9 @@ function getBrowserIndexedDB2() {
|
|
|
728
922
|
if (typeof indexedDB === "undefined") return void 0;
|
|
729
923
|
return indexedDB;
|
|
730
924
|
}
|
|
925
|
+
function isRecoverableStorageError(error) {
|
|
926
|
+
return error instanceof KeepStorageAccessError || error instanceof KeepStorageQuotaError;
|
|
927
|
+
}
|
|
731
928
|
function requestToPromise2(request) {
|
|
732
929
|
return new Promise((resolve, reject) => {
|
|
733
930
|
request.onsuccess = () => resolve(request.result);
|
|
@@ -757,12 +954,15 @@ export {
|
|
|
757
954
|
DEFAULT_SYNC_QUEUE_STORE,
|
|
758
955
|
LocalStorageSyncQueueAdapter,
|
|
759
956
|
IndexedDBSyncQueueAdapter,
|
|
957
|
+
FallbackSyncQueueAdapter,
|
|
760
958
|
SyncStorageAdapter,
|
|
761
959
|
DEFAULT_STORAGE_KEY,
|
|
762
960
|
DEFAULT_INDEXEDDB_DATABASE,
|
|
763
961
|
DEFAULT_INDEXEDDB_STORE,
|
|
962
|
+
FallbackStorageAdapter,
|
|
963
|
+
createBrowserStorageAdapter,
|
|
764
964
|
createStorageAdapter,
|
|
765
965
|
LocalStorageAdapter,
|
|
766
966
|
IndexedDBAdapter
|
|
767
967
|
};
|
|
768
|
-
//# sourceMappingURL=chunk-
|
|
968
|
+
//# sourceMappingURL=chunk-X4UVKTBK.js.map
|