@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 CHANGED
@@ -7,7 +7,8 @@
7
7
  Reactアプリケーション向けの、ヘッドレスで非同期処理を前提とした保存・コレクション用プリミティブです。
8
8
 
9
9
  ```tsx
10
- import { KeepButton, KeepProvider, LocalStorageAdapter } from "@keepkit/core";
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 { KeepButton, KeepProvider, LocalStorageAdapter } from "@keepkit/core";
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 ?? (getBrowserIndexedDB() ? new IndexedDBSyncQueueAdapter({
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-H4A322SZ.js.map
968
+ //# sourceMappingURL=chunk-X4UVKTBK.js.map