@keepkit/core 0.28.6 → 0.28.8

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
@@ -52,7 +52,7 @@ const list = useKeepList({
52
52
 
53
53
  保存対象の公開状態は`KeepItem.status`(`expired`、`removed`、`private`など)と`statusReason`で保持できます。`KeepProvider`の`validateItem` / `resolveItem`を指定すると、引数なしの`revalidateItems()`で検証できます。`revalidateItems`に`removeStatuses`を渡すと検出したアイテムを保存一覧から削除します。`SyncStorageAdapter`は`userId`、`tenantId`、`maxRetries`、`retryDelayMs`、`retryBackoff`に対応し、`retrySync()`で失敗後の同期を再開できます。
54
54
 
55
- v0.28.6では、並行保存・スコープ分離・バックアップ復元・所属同期を補強し、閲覧履歴の文脈、鑑賞記録の作成/更新日時、見出しIDによる読書位置を追加しました。空のコレクション、Inbox、Saved View、利用履歴、Rediscovery query、URL状態codec、認証付き同期も利用できます。
55
+ v0.28.8では個人用リンクライブラリのデモを刷新し、ブラウザー内でのリンク保存、整理、検索、バックアップを試せます。v0.28.8ではスコープをまたぐ結合と並行所属変更を補強し、履歴・鑑賞記録・進行位置の同一キー更新を保護しました。
56
56
 
57
57
  `createAuthenticatedSyncKit`は、リクエストごとの`getAuthToken`、注入可能なpush/pull transport、401/403時の再認証callback、永続オフラインキュー、`setScope`による安全なユーザー/テナント切替を提供します。スコープを切り替えたり対象スコープのキューを削除・全消去しても、他スコープの未送信操作は保持されます。詳細は[`examples/authenticated-sync`](../../examples/authenticated-sync/README.md)を参照してください。
58
58
 
@@ -111,7 +111,7 @@ Use `@keepkit/core/core` for framework-neutral code, `@keepkit/core/react` for R
111
111
 
112
112
  `KeepItem.status` and `statusReason` preserve source availability such as `expired`, `removed`, and `private`. Configure `KeepProvider` with `validateItem` / `resolveItem` to make `revalidateItems()` use those hooks by default. Pass `removeStatuses` to remove detected items from storage. `SyncStorageAdapter` supports scoped queues with `userId` and `tenantId`, configurable retries/backoff, and explicit `retrySync()` recovery.
113
113
 
114
- v0.28.6 strengthens concurrent persistence, scope isolation, backup restore, and membership sync, and adds history context, viewing-record timestamps, and heading-ID reading positions. Empty collections, Inbox, Saved Views, activity tracking, Rediscovery queries, URL state codecs, user/tenant isolation, and authenticated sync are also available.
114
+ v0.28.8 refreshes the personal link library demo for browser-local saving, organization, search, and backups. v0.28.8 strengthened scope-aware merging, concurrent membership changes, and same-key updates to history, viewing records, and progress.
115
115
 
116
116
  `createAuthenticatedSyncKit` provides a per-request `getAuthToken`, injectable push/pull transport, 401/403 reauthentication callbacks, persistent offline queues, and `setScope` for safe user or tenant changes. Scope changes and scoped queue removal or clearing preserve pending operations for other scopes. See [`examples/authenticated-sync`](../../examples/authenticated-sync/README.md) for a recipe.
117
117
 
@@ -129,13 +129,13 @@ The standard browser adapters persist explicit collection definitions and course
129
129
 
130
130
  標準ブラウザーストレージは、空のコレクション、変更した名前、コース別の所属と順序をアイテムとは別に永続化します。独自の`StorageAdapter`では`getCollections`、`setCollection`、`removeCollection`と`getCollectionMemberships`、`setCollectionMembership`、`removeCollectionMembership`を実装すると保存できます。`clear()`は保存アイテムのみを消去し、JSONバックアップv2はコレクションと所属も含みます。リモート同期は任意のコレクション用transportを追加すると利用できます。
131
131
 
132
- ### v0.28.6の保存・復元補完
132
+ ### v0.28.8の保存・復元補完
133
133
 
134
- 同じストレージインスタンス内の履歴・鑑賞記録・進行位置の書き込みは直列化されます。複数タブや別インスタンス間の排他は行いません。ブラウザー保存先が利用できない場合、書き込みは`KeepActivityStorageError`または`KeepProgressStorageError`で失敗します。SSRでは初期化できますが、保存操作はブラウザー側で呼び出してください。
134
+ 同じlocalStorageキーへの履歴・鑑賞記録・進行位置の書き込みは、Web Locks APIが使えるブラウザーではタブ・インスタンス間で直列化されます。同一realm内の複数インスタンスもキーごとに直列化します。`saveItem()`は同じガイドの既存進行情報を読み、指定フィールドだけ更新して他の位置情報を保ちます。鑑賞記録・進行レコードの`set()`で古い`updatedAt`の値が届いた場合は、新しい保存値を残します。Web Locks APIがないブラウザーでは、競合上書きを避けるため書き込み前に`KeepActivityStorageError`または`KeepProgressStorageError`で失敗します。ブラウザー保存先がない場合も同じエラー型で失敗します。SSRでは初期化できますが、保存操作はブラウザー側で呼び出してください。
135
135
 
136
- 既存ストレージを開いた後、コース別所属を読む機能を使う前に`migrateLegacyCollectionMemberships(storage)`を実行して旧単一所属データを移行します。既存所属を上書きせず、所属保存後に旧`collectionId`を消去するため、利用者が所属を外した後の再実行で復活しません。途中でadapterの書き込みが失敗した場合はエラーを返し、再実行しても所属は重複しません。対象scopeに定義名のないIDは戻り値の`missingCollectionIds`で確認できます。項目削除時はlocalStorage、IndexedDB、スコープadapterが所属も消します。
136
+ 既存ストレージを開いた後、コース別所属を読む機能を使う前に`migrateLegacyCollectionMemberships(storage)`を実行して旧単一所属データを移行します。既存所属を上書きせず、所属保存後に旧`collectionId`を消去するため、利用者が所属を外した後の再実行で復活しません。途中でadapterの書き込みが失敗した場合はエラーを返し、再実行しても所属は重複しません。対象scopeに定義名のないIDは戻り値の`missingCollectionIds`で確認できます。項目削除時はlocalStorage、IndexedDB、スコープadapterが所属も消します。同じadapterへの`addKeepItemToCollection()`・`removeKeepItemFromCollection()`・`reorderKeepCollectionItems()`は、所属の読み取りから順序の再採番・書き込みまで直列化されます。
137
137
 
138
- バックアップには保存アイテム・コレクション定義・所属のみが含まれ、閲覧履歴・鑑賞記録・進行位置は含まれません。`importItems()`の置換モードは空のバックアップでも対象範囲を空にします。v2バックアップと戻り値の`scopes`は含まれる利用者・テナント範囲を示し、`includedData`はデータ種別を示します。`imported`、`failed`、`total`は保存アイテムの件数で、`applied`は項目・定義・所属の適用数を個別に示します。スコープ付きadapterは別scopeを含むbackupを変更前に拒否します。v1の`collectionId`は名前を復元できないため`missingCollectionIds`で通知します。途中失敗は`KeepBackupImportError.failedStage`と`applied`で確認します。置換にrollbackはありません。merge時の同ID名は既定でバックアップ側を採用し、`collectionNameConflict: "existing"`を指定すると既存名を維持します。
138
+ バックアップには保存アイテム・コレクション定義・所属のみが含まれ、閲覧履歴・鑑賞記録・進行位置は含まれません。`importItems()`の置換モードは空のバックアップでも対象範囲を空にします。v2バックアップと戻り値の`scopes`は含まれる利用者・テナント範囲を示し、`includedData`はデータ種別を示します。`imported`、`failed`、`total`は保存アイテムの件数で、`applied`は項目・定義・所属の適用数を個別に示します。スコープ付きadapterは別scopeを含むbackupを変更前に拒否します。v1の`collectionId`は名前を復元できないため`missingCollectionIds`で通知します。途中失敗は`KeepBackupImportError.failedStage`と`applied`で確認します。置換にrollbackはありません。merge時の同ID名は既定でバックアップ側を採用し、`collectionNameConflict: "existing"`を指定すると既存名を維持します。共有adapterでは項目・コレクションのIDと`tenantId`・`userId`で識別するため、別scopeの同IDデータは別に保たれます。scope未指定と`{}`は同じ扱いで、空文字列の`userId`または`tenantId`は明示値として別scopeです。複数scopeのバックアップは共有adapterへ復元でき、スコープ付きadapterは別scopeを変更前に拒否します。
139
139
 
140
140
  履歴の文脈には`record(itemId, viewedAt, { language: "ja" })`で言語などを保存でき、`getAll()`で履歴と一緒に取得できます。鑑賞記録は`viewedAt`と`createdAt` / `updatedAt`を分けます。日時がない旧記録は最初に更新した時刻を作成時刻として補います。読書位置は数値offsetと安定した見出しID文字列を受け付けます。`getItem()`には必要な音声ID・言語・コンテンツ版を渡して互換位置だけを取得します。
141
141
 
@@ -177,13 +177,13 @@ const record = { id: "viewing-1", itemId: "guide-1", viewedAt: Date.now(), note:
177
177
  await viewings.set(record); // Reuse this stable ID when retrying the write.
178
178
  ```
179
179
 
180
- ### Persistence and restore improvements in v0.28.6
180
+ ### Persistence and restore improvements in v0.28.8
181
181
 
182
- Writes to history, viewing records, and progress are serialized within one storage instance, but separate tabs or instances are not locked. Writes fail with `KeepActivityStorageError` or `KeepProgressStorageError` when browser storage is unavailable. Construction is SSR-safe; call persistent writes in the browser.
182
+ Writes to history, viewing records, and progress using the same localStorage key are serialized across tabs and instances when the browser provides the Web Locks API. Separate instances in one realm are also serialized by key. `saveItem()` reads existing progress for the same guide and updates only the supplied fields, preserving other positions. For viewing records and progress, `set()` does not replace a newer record with a value whose `updatedAt` is older. In a browser without Web Locks, writes fail before mutation with `KeepActivityStorageError` or `KeepProgressStorageError` to avoid silently overwriting concurrent data. The same error types report unavailable browser storage. Construction is SSR-safe; call persistent writes in the browser.
183
183
 
184
- After opening existing storage and before reading course-specific memberships, run `migrateLegacyCollectionMemberships(storage)`. Existing memberships are never overwritten, and the legacy `collectionId` is cleared after migration so a later run will not recreate a membership the user removed. Adapter write errors are returned; rerunning after a partial failure is safe and does not duplicate memberships. IDs with no same-scope definition name are listed in `missingCollectionIds`. Removing a saved item also removes its memberships in localStorage, IndexedDB, and scoped adapters.
184
+ After opening existing storage and before reading course-specific memberships, run `migrateLegacyCollectionMemberships(storage)`. Existing memberships are never overwritten, and the legacy `collectionId` is cleared after migration so a later run will not recreate a membership the user removed. Adapter write errors are returned; rerunning after a partial failure is safe and does not duplicate memberships. IDs with no same-scope definition name are listed in `missingCollectionIds`. Removing a saved item also removes its memberships in localStorage, IndexedDB, and scoped adapters. `addKeepItemToCollection()`, `removeKeepItemFromCollection()`, and `reorderKeepCollectionItems()` serialize their read, reindex, and write steps when called concurrently with the same adapter instance.
185
185
 
186
- Backups include saved items, collection definitions, and memberships. They exclude history, viewing records, and progress. Replace imports clear the target scope even for an empty backup. Version 2 backups and results expose represented `scopes`; scoped adapters reject data from a different scope before mutation. `imported`, `failed`, and `total` count saved items; `applied` reports items, definitions, and memberships separately. `includedData` identifies data types, and v1 collection IDs whose names cannot be restored are returned in `missingCollectionIds`. `KeepBackupImportError.failedStage` and `applied` describe partial application. Replace has no rollback. Merge keeps the backup name for a same-ID collision by default; choose `collectionNameConflict: "existing"` to retain the stored name.
186
+ Backups include saved items, collection definitions, and memberships. They exclude history, viewing records, and progress. Replace imports clear the target scope even for an empty backup. Version 2 backups and results expose represented `scopes`; scoped adapters reject data from a different scope before mutation. `imported`, `failed`, and `total` count saved items; `applied` reports items, definitions, and memberships separately. `includedData` identifies data types, and v1 collection IDs whose names cannot be restored are returned in `missingCollectionIds`. `KeepBackupImportError.failedStage` and `applied` describe partial application. Replace has no rollback. Merge keeps the backup name for a same-ID collision by default; choose `collectionNameConflict: "existing"` to retain the stored name. Shared adapters identify items and collections by ID plus `tenantId` and `userId`, keeping same-ID records in different scopes independent. An omitted scope and `{}` are equivalent; an empty-string `userId` or `tenantId` is an explicit, distinct scope. Shared adapters accept multi-scope backups, while scoped adapters reject another scope before mutation.
187
187
 
188
188
  History context can be recorded with `record(itemId, viewedAt, { language: "ja" })` and retrieved with each entry from `getAll()`. Viewing records distinguish the self-reported `viewedAt` from `createdAt` / `updatedAt`. A legacy record without these timestamps receives its first-update time as its creation timestamp. Reading positions accept numeric offsets and stable heading ID strings; pass expected audio ID, language, and content version to `getItem()` to reject incompatible positions.
189
189
 
@@ -9,7 +9,7 @@ import {
9
9
  mergeKeepItemLists,
10
10
  persistKeepItems,
11
11
  removeKeepItems
12
- } from "./chunk-QRPQYE7K.js";
12
+ } from "./chunk-SLH4DFYQ.js";
13
13
 
14
14
  // src/features/items/navigation.ts
15
15
  function orderKeepItems(items) {
@@ -348,7 +348,9 @@ async function restoreCollections(adapter, collections, mode, conflict, applied)
348
348
  for (const collection of existing) await adapter.removeCollection(collection.id, collection.scope);
349
349
  }
350
350
  for (const collection of collections) {
351
- const current = existing.find((entry) => entry.id === collection.id);
351
+ const current = existing.find(
352
+ (entry) => entry.id === collection.id && sameScope(entry.scope ?? {}, collection.scope)
353
+ );
352
354
  const value = mode === "merge" && conflict === "existing" && current ? { ...collection, name: current.name } : collection;
353
355
  await adapter.setCollection(value);
354
356
  applied.collections += 1;
@@ -589,4 +591,4 @@ export {
589
591
  reconcileKeepItems,
590
592
  KeepStore
591
593
  };
592
- //# sourceMappingURL=chunk-5XSSH3TO.js.map
594
+ //# sourceMappingURL=chunk-EMOPFB5D.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/features/items/navigation.ts","../src/features/persistence/migration.ts","../src/features/persistence/backup.ts","../src/features/items/query.ts","../src/features/items/revalidation.ts","../src/features/store/store.ts"],"sourcesContent":["import type { KeepItem } from \"./types\";\n\nexport type KeepNavigationState<TMeta = Record<string, unknown>> = {\n items: KeepItem<TMeta>[];\n currentIndex: number;\n currentPosition: number | null;\n currentItem: KeepItem<TMeta> | null;\n hasNext: boolean;\n hasPrev: boolean;\n nextItem: KeepItem<TMeta> | null;\n prevItem: KeepItem<TMeta> | null;\n};\n\n/** Return items in their persisted custom order, leaving legacy items stable. */\nexport function orderKeepItems<TMeta>(items: readonly KeepItem<TMeta>[]): KeepItem<TMeta>[] {\n if (!items.some((item) => item.order !== undefined)) return [...items];\n return items\n .map((item, index) => ({ item, index }))\n .sort((left, right) => {\n const leftOrder = left.item.order;\n const rightOrder = right.item.order;\n if (leftOrder === undefined && rightOrder !== undefined) return 1;\n if (leftOrder !== undefined && rightOrder === undefined) return -1;\n if (leftOrder !== undefined && rightOrder !== undefined && leftOrder !== rightOrder) {\n return leftOrder - rightOrder;\n }\n return left.index - right.index;\n })\n .map(({ item }) => item);\n}\n\n/** Derive the previous/current/next view for a list and an item id or index. */\nexport function getKeepNavigationState<TMeta = Record<string, unknown>>(\n source: readonly KeepItem<TMeta>[],\n current?: string | number,\n): KeepNavigationState<TMeta> {\n const items = orderKeepItems(source);\n const currentIndex =\n typeof current === \"number\"\n ? Number.isInteger(current) && current >= 0 && current < items.length\n ? current\n : -1\n : current === undefined\n ? -1\n : items.findIndex((item) => item.id === current);\n const currentItem = currentIndex >= 0 ? (items[currentIndex] ?? null) : null;\n return {\n items,\n currentIndex,\n currentPosition: currentIndex >= 0 ? currentIndex + 1 : null,\n currentItem,\n hasNext: currentIndex >= 0 && currentIndex < items.length - 1,\n hasPrev: currentIndex > 0,\n nextItem: currentIndex >= 0 ? (items[currentIndex + 1] ?? null) : null,\n prevItem: currentIndex > 0 ? (items[currentIndex - 1] ?? null) : null,\n };\n}\n\n/** Apply a partial or complete id order and assign stable zero-based positions. */\nexport function reorderKeepItems<TMeta = Record<string, unknown>>(\n source: readonly KeepItem<TMeta>[],\n orderedIds: readonly string[],\n): KeepItem<TMeta>[] {\n const byId = new Map(source.map((item) => [item.id, item]));\n const seen = new Set<string>();\n for (const id of orderedIds) {\n if (!byId.has(id)) throw new Error(`Cannot reorder unknown Keep item \"${id}\".`);\n if (seen.has(id)) throw new Error(`Cannot reorder Keep items with duplicate id \"${id}\".`);\n seen.add(id);\n }\n const current = orderKeepItems(source);\n const sequence = [...orderedIds, ...current.map((item) => item.id).filter((id) => !seen.has(id))];\n return sequence.map((id, order) => {\n const item = byId.get(id);\n if (!item) throw new Error(`Cannot reorder unknown Keep item \"${id}\".`);\n return { ...item, order };\n });\n}\n\n/** Move one item within the current custom/legacy order. */\nexport function moveKeepItem<TMeta = Record<string, unknown>>(\n source: readonly KeepItem<TMeta>[],\n id: string,\n targetIndex: number,\n): KeepItem<TMeta>[] {\n const items = orderKeepItems(source);\n const currentIndex = items.findIndex((item) => item.id === id);\n if (currentIndex < 0) throw new Error(`Cannot move unknown Keep item \"${id}\".`);\n if (!Number.isInteger(targetIndex)) throw new Error(\"Keep item targetIndex must be an integer.\");\n const nextIndex = Math.min(Math.max(0, targetIndex), items.length - 1);\n const [item] = items.splice(currentIndex, 1);\n items.splice(nextIndex, 0, item);\n return reorderKeepItems(\n items,\n items.map((entry) => entry.id),\n );\n}\n","import type { KeepCollectionMembership, KeepItem, StorageAdapter, SyncScope } from \"../items/types\";\nimport { mergeKeepItemLists } from \"./helpers\";\n\n/** Merge anonymous local items into a signed-in or remote adapter. */\nexport async function mergeKeepItems<TMeta>(\n localItems: KeepItem<TMeta>[],\n target: StorageAdapter<TMeta>,\n): Promise<KeepItem<TMeta>[]> {\n if (target.merge) return target.merge(localItems);\n\n const merged = mergeKeepItemLists(await target.getAll(), localItems);\n await Promise.all(merged.map((item) => target.set(item)));\n return merged;\n}\n\n/** Read anonymous items, merge them into the target, then clear the source. */\nexport async function migrateKeepItems<TMeta>(\n source: StorageAdapter<TMeta>,\n target: StorageAdapter<TMeta>,\n): Promise<KeepItem<TMeta>[]> {\n const localItems = await source.getAll();\n const merged = await mergeKeepItems(localItems, target);\n await source.clear();\n return merged;\n}\n\nexport type LegacyCollectionMigrationResult = {\n migrated: number;\n skipped: number;\n missingCollectionIds: string[];\n};\n\n/** Move legacy single-collection item fields into course-specific memberships once. */\nexport async function migrateLegacyCollectionMemberships<TMeta>(\n storage: StorageAdapter<TMeta>,\n): Promise<LegacyCollectionMigrationResult> {\n if (!storage.getCollectionMemberships || !storage.setCollectionMembership) {\n throw new TypeError(\"The storage adapter does not support collection memberships.\");\n }\n\n const [items, memberships, collections] = await Promise.all([\n storage.getAll(),\n storage.getCollectionMemberships(),\n storage.getCollections?.() ?? Promise.resolve([]),\n ]);\n const existing = new Set(memberships.map(membershipKey));\n const collectionIds = new Set(collections.map((collection) => collectionKey(collection.id, collection.scope)));\n const missingCollectionIds = new Set<string>();\n let migrated = 0;\n let skipped = 0;\n\n for (const item of items) {\n if (!item.collectionId) continue;\n const membership: KeepCollectionMembership = {\n collectionId: item.collectionId,\n itemId: item.id,\n order: item.order ?? 0,\n ...(item.scope ? { scope: item.scope } : {}),\n };\n const key = membershipKey(membership);\n if (!collectionIds.has(collectionKey(item.collectionId, item.scope))) {\n missingCollectionIds.add(item.collectionId);\n }\n if (existing.has(key)) {\n skipped += 1;\n const { collectionId: _legacyCollectionId, ...migratedItem } = item;\n await storage.set(migratedItem);\n continue;\n }\n await storage.setCollectionMembership(membership);\n existing.add(key);\n migrated += 1;\n const { collectionId: _legacyCollectionId, ...migratedItem } = item;\n await storage.set(migratedItem);\n }\n\n return { migrated, skipped, missingCollectionIds: [...missingCollectionIds] };\n}\n\nfunction membershipKey(membership: KeepCollectionMembership): string {\n return `${membership.scope?.tenantId ?? \"\"}\\u0000${membership.scope?.userId ?? \"\"}\\u0000${membership.collectionId}\\u0000${membership.itemId}`;\n}\n\nfunction collectionKey(collectionId: string, scope?: SyncScope): string {\n return `${scope?.tenantId ?? \"\"}\\u0000${scope?.userId ?? \"\"}\\u0000${collectionId}`;\n}\n","import type {\n KeepCollectionDefinition,\n KeepCollectionMembership,\n KeepInvalidItemPolicy,\n KeepItem,\n KeepSchema,\n StorageAdapter,\n SyncScope,\n} from \"../items/types\";\nimport { isKeepCollectionDefinition, isKeepCollectionMembership, isKeepItem, isRecord } from \"./helpers\";\nimport { mergeKeepItems } from \"./migration\";\nimport { validateKeepItem } from \"./schema\";\n\nexport const KEEP_BACKUP_FORMAT = \"keepkit\";\nexport const KEEP_BACKUP_VERSION = 2;\n\nexport type KeepBackupV1<TMeta = Record<string, unknown>> = {\n format: typeof KEEP_BACKUP_FORMAT;\n version: 1;\n exportedAt: number;\n items: KeepItem<TMeta>[];\n};\n\nexport type KeepBackupV2<TMeta = Record<string, unknown>> = {\n format: typeof KEEP_BACKUP_FORMAT;\n version: typeof KEEP_BACKUP_VERSION;\n exportedAt: number;\n items: KeepItem<TMeta>[];\n collections: KeepCollectionDefinition[];\n memberships: KeepCollectionMembership[];\n /** Scopes represented by the records in this backup. Omitted by early v2 backups. */\n scopes?: SyncScope[];\n};\n\nexport type KeepBackup<TMeta = Record<string, unknown>> = KeepBackupV1<TMeta> | KeepBackupV2<TMeta>;\n\ntype ParsedKeepBackup<TMeta> = Omit<KeepBackupV2<TMeta>, \"version\"> & { version: 1 | typeof KEEP_BACKUP_VERSION };\n\nexport type ImportItemsOptions<TMeta = unknown> = {\n mode?: \"replace\" | \"merge\";\n schema?: KeepSchema<TMeta>;\n invalidItemPolicy?: KeepInvalidItemPolicy;\n onInvalidItem?: (error: unknown, item: KeepItem<unknown>) => void;\n /** Resolves a same-ID collection name collision when merging. Defaults to the backup name. */\n collectionNameConflict?: \"backup\" | \"existing\";\n};\n\nexport type KeepBackupDataType = \"items\" | \"collections\" | \"memberships\";\nexport type KeepBackupAppliedCounts = { items: number; collections: number; memberships: number };\n\nexport type ImportItemsResult<TMeta = Record<string, unknown>> = {\n mode: \"replace\" | \"merge\";\n imported: number;\n failed: number;\n total: number;\n items: KeepItem<TMeta>[];\n applied: KeepBackupAppliedCounts;\n missingCollectionIds: string[];\n includedData: KeepBackupDataType[];\n scopes: SyncScope[];\n};\n\nexport class KeepBackupParseError extends Error {\n readonly cause?: unknown;\n\n constructor(message: string, options?: { cause?: unknown }) {\n super(message);\n this.name = \"KeepBackupParseError\";\n if (options?.cause !== undefined) this.cause = options.cause;\n }\n}\n\nexport class KeepBackupImportError extends Error {\n readonly mode: \"replace\" | \"merge\";\n readonly imported: number;\n readonly failed: number;\n readonly applied: KeepBackupAppliedCounts;\n readonly failedStage: \"items\" | \"collections\" | \"memberships\" | \"clear\" | \"validation\";\n readonly cause?: unknown;\n\n constructor(\n message: string,\n options: {\n mode: \"replace\" | \"merge\";\n imported: number;\n failed: number;\n applied?: KeepBackupAppliedCounts;\n failedStage?: \"items\" | \"collections\" | \"memberships\" | \"clear\" | \"validation\";\n cause?: unknown;\n },\n ) {\n super(message);\n this.name = \"KeepBackupImportError\";\n this.mode = options.mode;\n this.imported = options.imported;\n this.failed = options.failed;\n this.applied = options.applied ?? { items: options.imported, collections: 0, memberships: 0 };\n this.failedStage = options.failedStage ?? \"items\";\n if (options.cause !== undefined) this.cause = options.cause;\n }\n}\n\n/** Serialize all adapter data into a versioned JSON backup. */\nexport async function exportItems<TMeta>(adapter: StorageAdapter<TMeta>): Promise<string> {\n const items = await adapter.getAll();\n const collections = (await adapter.getCollections?.()) ?? [];\n const memberships = (await adapter.getCollectionMemberships?.()) ?? [];\n const backup: KeepBackupV2<TMeta> = {\n format: KEEP_BACKUP_FORMAT,\n version: KEEP_BACKUP_VERSION,\n exportedAt: Date.now(),\n items,\n collections,\n memberships,\n scopes: collectScopes(items, collections, memberships, adapter.scope ? [adapter.scope] : []),\n };\n return JSON.stringify(backup, null, 2);\n}\n\n/** Validate and restore a backup, either replacing or merging existing data. */\nexport async function importItems<TMeta>(\n adapter: StorageAdapter<TMeta>,\n data: string | KeepBackup<TMeta>,\n options: ImportItemsOptions<TMeta> = {},\n): Promise<ImportItemsResult<TMeta>> {\n const backup = parseBackup<TMeta>(data);\n const mode = options.mode ?? \"merge\";\n const scopes = collectScopes(backup.items, backup.collections, backup.memberships, backup.scopes);\n if (adapter.scope && scopes.some((scope) => !sameScope(scope, adapter.scope))) {\n throw new KeepBackupImportError(\"The backup contains data outside the active storage scope.\", {\n mode,\n imported: 0,\n failed: backup.items.length + backup.collections.length + backup.memberships.length,\n applied: { items: 0, collections: 0, memberships: 0 },\n failedStage: \"validation\",\n });\n }\n if (\n (backup.collections.length > 0 &&\n (!adapter.getCollections || !adapter.setCollection || !adapter.removeCollection)) ||\n (backup.memberships.length > 0 &&\n (!adapter.getCollectionMemberships || !adapter.setCollectionMembership || !adapter.removeCollectionMembership))\n ) {\n throw new KeepBackupImportError(\"The storage adapter cannot restore collection data from this backup.\", {\n mode,\n imported: 0,\n failed: backup.items.length + backup.collections.length + backup.memberships.length,\n applied: { items: 0, collections: 0, memberships: 0 },\n failedStage: \"validation\",\n });\n }\n const validItems: KeepItem<TMeta>[] = [];\n let failed = 0;\n for (const item of backup.items) {\n if (!options.schema) {\n validItems.push(item);\n continue;\n }\n try {\n validItems.push(await validateKeepItem(item, options.schema));\n } catch (cause) {\n options.onInvalidItem?.(cause, item);\n if ((options.invalidItemPolicy ?? \"error\") === \"drop\") {\n failed += 1;\n continue;\n }\n throw cause;\n }\n }\n const applied: KeepBackupAppliedCounts = { items: 0, collections: 0, memberships: 0 };\n const missingCollectionIds =\n backup.version === 1\n ? [...new Set(backup.items.flatMap((item) => (item.collectionId ? [item.collectionId] : [])))]\n : [];\n\n try {\n if (mode === \"replace\") {\n await adapter.clear();\n await restoreCollections(adapter, [], \"replace\", \"backup\", applied);\n await restoreMemberships(adapter, [], \"replace\", applied);\n }\n } catch (cause) {\n throw new KeepBackupImportError(\"KeepKit could not clear the data being replaced.\", {\n mode,\n imported: applied.items,\n failed: validItems.length + failed,\n applied,\n failedStage: \"clear\",\n cause,\n });\n }\n\n for (const item of validItems) {\n try {\n if (mode === \"merge\") await mergeKeepItems([item], adapter);\n else await adapter.set(item);\n applied.items += 1;\n } catch (cause) {\n throw new KeepBackupImportError(`KeepKit could not restore item ${item.id}.`, {\n mode,\n imported: applied.items,\n failed: validItems.length + failed - applied.items,\n applied,\n failedStage: \"items\",\n cause,\n });\n }\n }\n\n try {\n await restoreCollections(adapter, backup.collections, mode, options.collectionNameConflict ?? \"backup\", applied);\n } catch (cause) {\n throw new KeepBackupImportError(\"KeepKit could not restore collection definitions.\", {\n mode,\n imported: applied.items,\n failed: validItems.length + failed - applied.items,\n applied,\n failedStage: \"collections\",\n cause,\n });\n }\n try {\n await restoreMemberships(adapter, backup.memberships, mode, applied);\n } catch (cause) {\n throw new KeepBackupImportError(\"KeepKit could not restore collection memberships.\", {\n mode,\n imported: applied.items,\n failed: validItems.length + failed - applied.items,\n applied,\n failedStage: \"memberships\",\n cause,\n });\n }\n let items: KeepItem<TMeta>[];\n try {\n items = await adapter.getAll();\n } catch (cause) {\n throw new KeepBackupImportError(\"KeepKit restored the backup but could not read the resulting items.\", {\n mode,\n imported: applied.items,\n failed: failed,\n applied,\n failedStage: \"items\",\n cause,\n });\n }\n\n return {\n mode,\n imported: applied.items,\n failed,\n total: items.length,\n items,\n applied,\n missingCollectionIds,\n includedData: backup.version === 1 ? [\"items\"] : [\"items\", \"collections\", \"memberships\"],\n scopes,\n };\n}\n\nfunction parseBackup<TMeta>(data: string | KeepBackup<TMeta>): ParsedKeepBackup<TMeta> {\n let value: unknown = data;\n if (typeof data === \"string\") {\n try {\n value = JSON.parse(data);\n } catch (cause) {\n throw new KeepBackupParseError(\"KeepKit backup is not valid JSON.\", { cause });\n }\n }\n\n if (!isRecord(value)) throw new KeepBackupParseError(\"KeepKit backup must be an object.\");\n if (value.format !== KEEP_BACKUP_FORMAT || (value.version !== 1 && value.version !== KEEP_BACKUP_VERSION)) {\n throw new KeepBackupParseError(\"KeepKit backup format or version is unsupported.\");\n }\n if (typeof value.exportedAt !== \"number\" || !Number.isFinite(value.exportedAt)) {\n throw new KeepBackupParseError(\"KeepKit backup has an invalid export timestamp.\");\n }\n if (!Array.isArray(value.items) || !value.items.every(isKeepItem)) {\n throw new KeepBackupParseError(\"KeepKit backup contains invalid items.\");\n }\n if (value.version === 1) {\n return {\n format: KEEP_BACKUP_FORMAT,\n version: 1,\n exportedAt: value.exportedAt,\n items: value.items as KeepItem<TMeta>[],\n collections: [],\n memberships: [],\n scopes: [],\n };\n }\n if (\n !Array.isArray(value.collections) ||\n !value.collections.every(isKeepCollectionDefinition) ||\n !Array.isArray(value.memberships) ||\n !value.memberships.every(isKeepCollectionMembership)\n ) {\n throw new KeepBackupParseError(\"KeepKit backup contains invalid collection data.\");\n }\n if (value.scopes !== undefined && (!Array.isArray(value.scopes) || !value.scopes.every(isSyncScope))) {\n throw new KeepBackupParseError(\"KeepKit backup contains invalid scope data.\");\n }\n return value as ParsedKeepBackup<TMeta>;\n}\n\nfunction collectScopes(\n items: KeepItem<unknown>[],\n collections: KeepCollectionDefinition[],\n memberships: KeepCollectionMembership[],\n declaredScopes: SyncScope[] = [],\n): SyncScope[] {\n const scopes = [\n ...declaredScopes,\n ...[...items, ...collections, ...memberships].flatMap((record) => (record.scope ? [record.scope] : [])),\n ].filter((scope, index, all) => all.findIndex((entry) => sameScope(entry, scope)) === index);\n return scopes.map((scope) => ({ ...scope }));\n}\n\nfunction sameScope(left: SyncScope, right: SyncScope | undefined): boolean {\n return left.userId === right?.userId && left.tenantId === right?.tenantId;\n}\n\nfunction isSyncScope(value: unknown): value is SyncScope {\n return (\n isRecord(value) &&\n (value.userId === undefined || typeof value.userId === \"string\") &&\n (value.tenantId === undefined || typeof value.tenantId === \"string\")\n );\n}\n\nasync function restoreCollections<TMeta>(\n adapter: StorageAdapter<TMeta>,\n collections: KeepCollectionDefinition[],\n mode: \"merge\" | \"replace\",\n conflict: \"backup\" | \"existing\",\n applied: KeepBackupAppliedCounts,\n): Promise<void> {\n if (!adapter.getCollections || !adapter.setCollection || !adapter.removeCollection) return;\n const existing = await adapter.getCollections();\n if (mode === \"replace\") {\n for (const collection of existing) await adapter.removeCollection(collection.id, collection.scope);\n }\n for (const collection of collections) {\n const current = existing.find(\n (entry) => entry.id === collection.id && sameScope(entry.scope ?? {}, collection.scope),\n );\n const value =\n mode === \"merge\" && conflict === \"existing\" && current ? { ...collection, name: current.name } : collection;\n await adapter.setCollection(value);\n applied.collections += 1;\n }\n}\n\nasync function restoreMemberships<TMeta>(\n adapter: StorageAdapter<TMeta>,\n memberships: KeepCollectionMembership[],\n mode: \"merge\" | \"replace\",\n applied: KeepBackupAppliedCounts,\n): Promise<void> {\n if (!adapter.getCollectionMemberships || !adapter.setCollectionMembership || !adapter.removeCollectionMembership)\n return;\n if (mode === \"replace\") {\n const current = await adapter.getCollectionMemberships();\n for (const membership of current) {\n await adapter.removeCollectionMembership(membership.collectionId, membership.itemId, membership.scope);\n }\n }\n for (const membership of memberships) {\n await adapter.setCollectionMembership(membership);\n applied.memberships += 1;\n }\n}\n","import { orderKeepItems } from \"./navigation\";\nimport type { KeepItem } from \"./types\";\n\nexport type KeepListQuery<TMeta = Record<string, unknown>> = {\n targetType?: string;\n tags?: string[];\n /** Defaults to active (not archived) items; true selects archived items. */\n archived?: boolean;\n /** Optional explicit archive scope. When provided, this takes precedence over `archived`. */\n archiveScope?: \"active\" | \"archived\" | \"all\";\n collectionId?: string;\n organization?: {\n collection?: \"assigned\" | \"unassigned\";\n tags?: \"assigned\" | \"unassigned\";\n note?: \"assigned\" | \"unassigned\";\n };\n pinnedFirst?: boolean;\n activity?: {\n opened?: \"ever\" | \"never\";\n lastOpenedBefore?: number;\n lastOpenedAfter?: number;\n inactiveForMs?: number;\n };\n sort?: {\n by: \"savedAt\" | \"updatedAt\" | \"lastOpenedAt\";\n direction?: \"asc\" | \"desc\";\n };\n search?: {\n query?: string;\n mode?: \"and\" | \"or\";\n tokenize?: boolean;\n fields?: Array<\"note\" | \"meta\" | \"tags\">;\n };\n pagination?: {\n page?: number;\n pageSize?: number;\n };\n filter?: (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 page: number;\n pageCount: number;\n hasNextPage: boolean;\n hasPreviousPage: boolean;\n};\n\nexport type KeepRediscoveryStrategy = \"never-opened\" | \"forgotten\" | \"recently-opened\";\n\nexport type KeepRediscoveryOptions = {\n strategy: KeepRediscoveryStrategy;\n inactiveForMs?: number;\n};\n\n/** Build the default Inbox query: active items without a collection. */\nexport function createInboxQuery<TMeta = Record<string, unknown>>(\n options: { organization?: KeepListQuery<TMeta>[\"organization\"] } = {},\n): KeepListQuery<TMeta> {\n return { organization: { collection: \"unassigned\", ...options.organization } };\n}\n\nconst DEFAULT_REDISCOVERY_INACTIVE_FOR_MS = 30 * 24 * 60 * 60 * 1000;\n\n/** Build a serializable query for common rediscovery views. */\nexport function createRediscoveryQuery<TMeta = Record<string, unknown>>(\n options: KeepRediscoveryOptions,\n): KeepListQuery<TMeta> {\n if (options.strategy === \"never-opened\") return { activity: { opened: \"never\" } };\n if (options.strategy === \"recently-opened\") {\n return { activity: { opened: \"ever\" }, sort: { by: \"lastOpenedAt\", direction: \"desc\" } };\n }\n return { activity: { inactiveForMs: options.inactiveForMs ?? DEFAULT_REDISCOVERY_INACTIVE_FOR_MS } };\n}\n\n/** Apply the collection query and one-based pagination without React. */\nexport function queryKeepItems<TMeta = Record<string, unknown>>(\n source: KeepItem<TMeta>[],\n query: KeepListQuery<TMeta> = {},\n): QueryKeepItemsResult<TMeta> {\n const filtered = source.filter((item) => {\n const [from, to] = query.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 (query.targetType === undefined || item.targetType === query.targetType) &&\n (query.tags === undefined || query.tags.every((tag) => item.tags?.includes(tag))) &&\n matchesArchiveScope(item, query) &&\n (query.collectionId === undefined || item.collectionId === query.collectionId) &&\n matchesOrganization(item, query.organization) &&\n matchesActivity(item, query.activity) &&\n (lowerBound === undefined || savedAt >= lowerBound) &&\n (upperBound === undefined || savedAt <= upperBound) &&\n matchesSearch(item, query.search) &&\n (query.filter?.(item) ?? true)\n );\n });\n const tagCounts = getTagCounts(filtered);\n const sortBy = query.sort?.by;\n const direction = query.sort?.direction === \"asc\" ? 1 : -1;\n const sortedBase = sortBy\n ? filtered\n .map((item, index) => ({ item, index }))\n .sort((a, b) => compareSortValues(a.item[sortBy], b.item[sortBy], direction) || a.index - b.index)\n .map(({ item }) => item)\n : orderKeepItems(filtered);\n const sorted = query.pinnedFirst\n ? [...sortedBase.filter((item) => item.pinned === true), ...sortedBase.filter((item) => item.pinned !== true)]\n : sortedBase;\n const pageSize = Math.max(1, query.pagination?.pageSize ?? (sorted.length || 1));\n const pageCount = Math.max(1, Math.ceil(sorted.length / pageSize));\n const page = Math.min(Math.max(1, query.pagination?.page ?? 1), pageCount);\n const offset = (page - 1) * pageSize;\n\n return {\n items: sorted.slice(offset, offset + pageSize),\n totalCount: sorted.length,\n tagCounts,\n page,\n pageCount,\n hasNextPage: page < pageCount,\n hasPreviousPage: page > 1,\n };\n}\n\nfunction matchesOrganization<TMeta>(\n item: KeepItem<TMeta>,\n organization: KeepListQuery<TMeta>[\"organization\"],\n): boolean {\n if (organization?.collection === \"assigned\" && !item.collectionId?.trim()) return false;\n if (organization?.collection === \"unassigned\" && item.collectionId?.trim()) return false;\n if (organization?.tags === \"assigned\" && !item.tags?.some((tag) => tag.trim())) return false;\n if (organization?.tags === \"unassigned\" && item.tags?.some((tag) => tag.trim())) return false;\n if (organization?.note === \"assigned\" && !item.note?.trim()) return false;\n if (organization?.note === \"unassigned\" && item.note?.trim()) return false;\n return true;\n}\n\nfunction matchesActivity<TMeta>(item: KeepItem<TMeta>, activity?: KeepListQuery<TMeta>[\"activity\"]): boolean {\n if (!activity) return true;\n const lastOpenedAt = item.lastOpenedAt;\n const opened = lastOpenedAt !== undefined;\n if (activity.opened === \"ever\" && !opened) return false;\n if (activity.opened === \"never\" && opened) return false;\n if (activity.lastOpenedBefore !== undefined && (!opened || lastOpenedAt >= activity.lastOpenedBefore)) return false;\n if (activity.lastOpenedAfter !== undefined && (!opened || lastOpenedAt <= activity.lastOpenedAfter)) return false;\n if (activity.inactiveForMs !== undefined) {\n const cutoff = Date.now() - activity.inactiveForMs;\n const lastActivityAt = item.lastOpenedAt ?? item.savedAt;\n if (lastActivityAt > cutoff) return false;\n }\n return true;\n}\n\nfunction compareSortValues(left: number | undefined, right: number | undefined, direction: 1 | -1): number {\n if (left === undefined && right === undefined) return 0;\n if (left === undefined) return 1;\n if (right === undefined) return -1;\n return (left - right) * direction;\n}\n\nfunction matchesArchiveScope<TMeta>(item: KeepItem<TMeta>, query: KeepListQuery<TMeta>): boolean {\n if (query.archiveScope === \"all\") return true;\n if (query.archiveScope === \"archived\") return item.archived === true;\n if (query.archiveScope === \"active\") return item.archived !== true;\n return query.archived === true ? item.archived === true : item.archived !== true;\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>(item: KeepItem<TMeta>, search?: KeepListQuery<TMeta>[\"search\"]): boolean {\n const query = search?.query;\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 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 { persistKeepItems, removeKeepItems } from \"../persistence/helpers\";\nimport type { KeepItem, KeepItemStatus, StorageAdapter } from \"./types\";\n\nexport type { KeepItemStatus } from \"./types\";\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 KeepItemResolver<TMeta = Record<string, unknown>> = (\n item: KeepItem<TMeta>,\n result: KeepItemRevalidationResult<TMeta>,\n) => KeepItem<TMeta> | undefined | Promise<KeepItem<TMeta> | undefined>;\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<TMeta = Record<string, unknown>> = {\n /** Statuses that should be removed after they are detected. Detection is the default. */\n removeStatuses?: Array<Exclude<KeepItemStatus, \"available\">>;\n now?: () => number;\n resolveItem?: KeepItemResolver<TMeta>;\n};\n\nexport type KeepAutoRevalidationOptions<TMeta = Record<string, unknown>> = {\n /** Revalidate after the provider hydrates. Defaults to true when validateItem is supplied. */\n onMount?: boolean;\n /** Revalidate when the browser returns online. Defaults to true. */\n onReconnect?: boolean;\n /** Revalidate repeatedly while mounted. Disabled by default. */\n intervalMs?: number;\n removeStatuses?: Array<Exclude<KeepItemStatus, \"available\">>;\n revalidator?: KeepItemRevalidator<TMeta>;\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<TMeta> = {},\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 const reason = result.status === \"available\" ? undefined : result.reason;\n const defaultResolved = {\n ...item,\n status: result.status,\n ...(result.status === \"available\" ? { statusReason: undefined } : { statusReason: reason }),\n };\n const resolved = options.resolveItem\n ? await (options.resolveItem as KeepItemResolver<TMeta>)(item, result)\n : defaultResolved;\n if (resolved === undefined) {\n removedIds.push(item.id);\n results.push({ item, status: result.status, reason, updated: false });\n continue;\n }\n if (result.status === \"available\") {\n const timestamp = now();\n const availableItem = clearItemStatus(resolved);\n const updated =\n result.meta === undefined\n ? availableItem\n : { ...availableItem, 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 withStatus = { ...resolved, status: result.status, ...(reason ? { statusReason: reason } : {}) };\n const shouldRemove = removeStatuses.has(result.status);\n if (shouldRemove) removedIds.push(item.id);\n else items.push(withStatus);\n if (!shouldRemove && withStatus !== item) updatedItems.push(withStatus);\n results.push({ item: withStatus, status: result.status, reason, updated: !shouldRemove && withStatus !== item });\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<TMeta> = {},\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 persistKeepItems(storage, summary.updatedItems);\n if (summary.removedIds.length > 0) await removeKeepItems(storage, summary.removedIds);\n return summary;\n}\n\nfunction clearItemStatus<TMeta>(item: KeepItem<TMeta>): KeepItem<TMeta> {\n const next = { ...item };\n delete next.status;\n delete next.statusReason;\n return next;\n}\n","import type {\n KeepItemMetadataRefresher,\n KeepItemRevalidationSummary,\n KeepItemRevalidator,\n RevalidateKeepItemsOptions,\n} from \"../items/revalidation\";\nimport type { KeepChangeContext, KeepItem, KeepUndoState } from \"../items/types\";\n\nexport type KeepCollectionMeta = { name: string };\n\nexport type KeepStoreState<TMeta = Record<string, unknown>> = {\n items: KeepItem<TMeta>[];\n collections: Record<string, KeepCollectionMeta>;\n isLoading: boolean;\n isHydrated: boolean;\n isMutating: boolean;\n error: unknown | null;\n lastChange?: KeepChangeContext<TMeta>;\n undo?: KeepUndoState;\n};\n\nexport type KeepStoreActions<TMeta = Record<string, unknown>> = {\n saveItem: (item: KeepItem<TMeta>) => Promise<void>;\n recordOpen: (id: string, openedAt?: number) => Promise<void>;\n updateNote: (id: string, note?: string) => Promise<void>;\n updateTags: (id: string, tags?: string[]) => Promise<void>;\n toggleArchive: (id: string) => Promise<void>;\n archiveItem: (id: string) => Promise<void>;\n unarchiveItem: (id: string) => Promise<void>;\n togglePin: (id: string) => Promise<void>;\n moveToCollection: (id: string, collectionId?: string) => Promise<void>;\n archiveBatch: (ids: string[]) => Promise<void>;\n unarchiveBatch: (ids: string[]) => Promise<void>;\n pinBatch: (ids: string[]) => Promise<void>;\n unpinBatch: (ids: string[]) => Promise<void>;\n moveToCollectionBatch: (ids: string[], collectionId?: string) => Promise<void>;\n createCollection: (id: string, name: string) => Promise<void>;\n renameCollection: (id: string, name: string) => Promise<void>;\n removeCollection: (id: 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 removeItemWithUndo: (id: string) => Promise<void>;\n removeItemsWithUndo: (ids: string[]) => Promise<void>;\n undoLastRemoval: () => 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<TMeta>,\n ) => Promise<KeepItemRevalidationSummary<TMeta>>;\n reorderItems: (orderedIds: string[]) => Promise<void>;\n moveItem: (id: string, targetIndex: number) => Promise<void>;\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":["item","_legacyCollectionId","migratedItem"]}
@@ -61,18 +61,22 @@ async function removeKeepItems(storage, ids) {
61
61
  for (const id of ids) await storage.remove(id);
62
62
  }
63
63
  function mergeKeepItemLists(remoteItems, localItems) {
64
- const byId = new Map(remoteItems.map((item) => [item.id, item]));
64
+ const byScopeAndId = new Map(remoteItems.map((item) => [itemStorageKey(item), item]));
65
65
  for (const localItem of localItems) {
66
- const remoteItem = byId.get(localItem.id);
66
+ const key = itemStorageKey(localItem);
67
+ const remoteItem = byScopeAndId.get(key);
67
68
  if (!remoteItem) {
68
- byId.set(localItem.id, localItem);
69
+ byScopeAndId.set(key, localItem);
69
70
  continue;
70
71
  }
71
72
  const content = localItem.updatedAt > remoteItem.updatedAt ? localItem : remoteItem;
72
73
  const lastOpenedAt = remoteItem.lastOpenedAt === void 0 ? localItem.lastOpenedAt : localItem.lastOpenedAt === void 0 ? remoteItem.lastOpenedAt : Math.max(remoteItem.lastOpenedAt, localItem.lastOpenedAt);
73
- byId.set(localItem.id, lastOpenedAt === void 0 ? content : { ...content, lastOpenedAt });
74
+ byScopeAndId.set(key, lastOpenedAt === void 0 ? content : { ...content, lastOpenedAt });
74
75
  }
75
- return [...byId.values()].sort((a, b) => b.updatedAt - a.updatedAt);
76
+ return [...byScopeAndId.values()].sort((a, b) => b.updatedAt - a.updatedAt);
77
+ }
78
+ function itemStorageKey(item) {
79
+ return JSON.stringify([item.scope?.tenantId ?? null, item.scope?.userId ?? null, item.id]);
76
80
  }
77
81
  function isKeepItem(value) {
78
82
  if (!isRecord(value)) return false;
@@ -96,7 +100,7 @@ function isSyncScope(value) {
96
100
 
97
101
  // src/features/persistence/scope.ts
98
102
  function getKeepScopeKey(scope) {
99
- if (!scope?.userId && !scope?.tenantId) return "";
103
+ if (scope?.userId === void 0 && scope?.tenantId === void 0) return "";
100
104
  return `:${encodeURIComponent(scope.tenantId ?? "_")}:${encodeURIComponent(scope.userId ?? "_")}`;
101
105
  }
102
106
  function isSameKeepScope(left, right) {
@@ -176,8 +180,10 @@ var ScopedStorageAdapter = class {
176
180
  await this.clearMembershipsForScope();
177
181
  }
178
182
  async merge(items) {
179
- await this.setMany(items);
180
- return this.getAll();
183
+ const scopedItems = items.map((item) => ({ ...item, ...this.scope ? { scope: this.scope } : {} }));
184
+ const merged = mergeKeepItemLists(await this.getAll(), scopedItems);
185
+ await this.setMany(merged);
186
+ return merged;
181
187
  }
182
188
  subscribe(listener) {
183
189
  return this.base.subscribe?.(listener) ?? (() => void 0);
@@ -237,8 +243,8 @@ var ScopedSyncQueueAdapter = class {
237
243
  };
238
244
  async function writeAll(base, items, scope) {
239
245
  const existing = await base.getAll();
240
- const desiredItems = new Set(items.filter((item) => isSameKeepScope(item.scope, scope)).map(itemStorageKey));
241
- const removedIds = existing.filter((item) => isSameKeepScope(item.scope, scope) && !desiredItems.has(itemStorageKey(item))).map((item) => item.id);
246
+ const desiredItems = new Set(items.filter((item) => isSameKeepScope(item.scope, scope)).map(itemStorageKey2));
247
+ const removedIds = existing.filter((item) => isSameKeepScope(item.scope, scope) && !desiredItems.has(itemStorageKey2(item))).map((item) => item.id);
242
248
  if (removedIds.length > 0) {
243
249
  if (base.removeMany) await base.removeMany(removedIds, scope);
244
250
  else await Promise.all(removedIds.map((id) => base.remove(id, scope)));
@@ -249,7 +255,7 @@ async function writeAll(base, items, scope) {
249
255
  }
250
256
  for (const item of items) await base.set(item);
251
257
  }
252
- function itemStorageKey(item) {
258
+ function itemStorageKey2(item) {
253
259
  return JSON.stringify([item.scope?.tenantId ?? null, item.scope?.userId ?? null, item.id]);
254
260
  }
255
261
 
@@ -1448,6 +1454,9 @@ var LocalStorageAdapter = class {
1448
1454
  );
1449
1455
  }
1450
1456
  async getCollectionMemberships() {
1457
+ return this.readMemberships();
1458
+ }
1459
+ readMemberships() {
1451
1460
  if (!this.storage) return [];
1452
1461
  const storageKey = `${this.storageKey}:memberships`;
1453
1462
  let raw;
@@ -1470,12 +1479,12 @@ var LocalStorageAdapter = class {
1470
1479
  }
1471
1480
  async setCollectionMembership(membership) {
1472
1481
  if (!isKeepCollectionMembership(membership)) throw new Error("Collection membership is invalid.");
1473
- const current = await this.getCollectionMemberships();
1482
+ const current = this.readMemberships();
1474
1483
  const sameMembership = (entry) => entry.collectionId === membership.collectionId && entry.itemId === membership.itemId && isSameKeepScope(entry.scope, membership.scope);
1475
1484
  await this.writeMemberships([...current.filter((entry) => !sameMembership(entry)), membership], "set");
1476
1485
  }
1477
1486
  async removeCollectionMembership(collectionId, itemId, scope) {
1478
- const current = await this.getCollectionMemberships();
1487
+ const current = this.readMemberships();
1479
1488
  await this.writeMemberships(
1480
1489
  current.filter(
1481
1490
  (entry) => entry.collectionId !== collectionId || entry.itemId !== itemId || !isSameKeepScope(entry.scope, scope)
@@ -1941,4 +1950,4 @@ export {
1941
1950
  LocalStorageAdapter,
1942
1951
  IndexedDBAdapter
1943
1952
  };
1944
- //# sourceMappingURL=chunk-QRPQYE7K.js.map
1953
+ //# sourceMappingURL=chunk-SLH4DFYQ.js.map