@keepkit/core 0.28.4 → 0.28.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +88 -6
- package/dist/{chunk-Y4U5DEGG.js → chunk-5XSSH3TO.js} +212 -28
- package/dist/chunk-5XSSH3TO.js.map +1 -0
- package/dist/{chunk-2TENUU7B.js → chunk-QRPQYE7K.js} +643 -76
- package/dist/chunk-QRPQYE7K.js.map +1 -0
- package/dist/core.d.ts +192 -7
- package/dist/core.js +565 -7
- package/dist/core.js.map +1 -1
- package/dist/react.d.ts +5 -5
- package/dist/react.js +2 -2
- package/dist/schema.d.ts +1 -1
- package/dist/{scope-Dtkxpnof.d.ts → scope-1bxGd68Y.d.ts} +9 -4
- package/dist/storage.d.ts +42 -18
- package/dist/storage.js +1 -1
- package/dist/{store-45yaDeER.d.ts → store-98vWM3tV.d.ts} +31 -4
- package/dist/{types-C_U5Q44o.d.ts → types-GTCMFt5T.d.ts} +83 -5
- package/package.json +1 -1
- package/dist/chunk-2TENUU7B.js.map +0 -1
- package/dist/chunk-Y4U5DEGG.js.map +0 -1
package/README.md
CHANGED
|
@@ -34,17 +34,27 @@ const list = useKeepList({
|
|
|
34
34
|
|
|
35
35
|
再発見には`useKeepItem(item).recordOpen()`で`lastOpenedAt`を記録し、`activity`の`opened`、`lastOpenedBefore`、`lastOpenedAfter`、`inactiveForMs`で絞り込めます。`inactiveForMs`は開封済みなら`lastOpenedAt`、未開封なら`savedAt`を基準に判定します。開封記録だけでは`updatedAt`を変更せず、同期・mergeでは新しいActivityを保持します。`createRediscoveryQuery()`は`never-opened`、`forgotten`、`recently-opened`のCore向けプリセットを提供し、`useKeepRediscovery()`はReact向けの一覧hookです。
|
|
36
36
|
|
|
37
|
+
未保存ガイドも含む閲覧履歴には`LocalStorageKeepHistoryStorage`を使います。`record(itemId)`は保存一覧へアイテムを追加せず、保存解除でも履歴を消しません。`getAll()`、`remove()`、`clear()`、`setLimit()`で一覧・個別削除・全消去・保持件数を管理できます。自己申告の鑑賞は`LocalStorageKeepViewingRecordStorage.add()`で開封と別の複数レコードとして記録し、`set()`で日時や個人メモを更新し、`remove()` / `removeForItem()` / `clear()`で削除できます。
|
|
38
|
+
|
|
37
39
|
保存順を巡回ルートとして管理する場合は、`reorderKeepItems` / `moveKeepItem`、Reactでは`useKeepNavigator`と`useKeepList().reorder()` / `.move()`を利用できます。`getKeepNavigationState`は現在・前・次のアイテムと進行度を返します。
|
|
38
40
|
|
|
41
|
+
同じ保存アイテムを複数コースに登録する場合は、`StorageAdapter`の`setCollectionMembership()`と`addKeepItemToCollection()`、`getKeepCollectionItems()`、`reorderKeepCollectionItems()`、`removeKeepItemFromCollection()`を使います。コースごとに順序を保持し、所属を外しても保存アイテムや共有メモは削除しません。従来の`KeepItem.collectionId`は単一分類用途として引き続き利用できます。
|
|
42
|
+
|
|
43
|
+
`exportItems()`は空の定義を含むコレクションと複数コース所属をv2バックアップへ含め、`importItems()`は従来のアイテムのみのv1バックアップも読み込みます。mergeでは同じIDのコレクション名と同じコース・ガイドの順序をバックアップ側で更新し、同名でも異なるIDのコレクションは別の定義として保ちます。replaceでは既存のアイテム、定義、所属を置き換えます。復元先の独自`StorageAdapter`はコレクションと所属の各メソッドを実装する必要があります。
|
|
44
|
+
|
|
45
|
+
`RemoteSyncDriver`に`pushCollection()`と`pullCollections()`を実装すると、コレクションの作成・改名・削除を永続キュー経由で同期できます。操作は`userId` / `tenantId`のscopeを持ち、フルスナップショットのpullで見つからない定義は削除として扱い、ローカル所属も削除します。未送信のローカル操作はpullで上書きせず、複数端末で同じ定義を同時変更した場合はリモートtransportが受信順で適用する「最後に受け付けた操作を採用」方針です。認証付き構成では`AuthenticatedSyncTransport`にも同じ2つの関数を指定できます。
|
|
46
|
+
|
|
47
|
+
コースの再開項目やガイドごとの音声・読書位置は`LocalStorageKeepProgressStorage`で保存します。`saveCourse()` / `getCourse()`、`saveItem()` / `getItem()`、`resetCourse()` / `resetItem()`を利用できます。音声位置はミリ秒、読書位置はアプリ定義の数値として扱います。記録に言語または`contentVersion`がある場合、`getItem()`に一致する値を指定しないと位置を返しません。保存APIは再生を開始しないため、音声の再開はViewer側で利用者の操作から行ってください。
|
|
48
|
+
|
|
39
49
|
`@keepkit/core/core`はフレームワーク中立、`@keepkit/core/react`はReact、`@keepkit/core/storage`はlocalStorage、IndexedDB、fallback、同期adapter、`@keepkit/core/schema`はschema処理を公開します。パッケージルートにはexportがありません。
|
|
40
50
|
|
|
41
51
|
`KeepProvider`には描画エラーを局所化する`fallback`、`onBoundaryError`、`boundaryResetKey`を指定できます。より細かい境界が必要な場合は`KeepErrorBoundary`を`@keepkit/core/react`から利用できます。
|
|
42
52
|
|
|
43
53
|
保存対象の公開状態は`KeepItem.status`(`expired`、`removed`、`private`など)と`statusReason`で保持できます。`KeepProvider`の`validateItem` / `resolveItem`を指定すると、引数なしの`revalidateItems()`で検証できます。`revalidateItems`に`removeStatuses`を渡すと検出したアイテムを保存一覧から削除します。`SyncStorageAdapter`は`userId`、`tenantId`、`maxRetries`、`retryDelayMs`、`retryBackoff`に対応し、`retrySync()`で失敗後の同期を再開できます。
|
|
44
54
|
|
|
45
|
-
v0.28.
|
|
55
|
+
v0.28.6では、並行保存・スコープ分離・バックアップ復元・所属同期を補強し、閲覧履歴の文脈、鑑賞記録の作成/更新日時、見出しIDによる読書位置を追加しました。空のコレクション、Inbox、Saved View、利用履歴、Rediscovery query、URL状態codec、認証付き同期も利用できます。
|
|
46
56
|
|
|
47
|
-
`createAuthenticatedSyncKit`は、リクエストごとの`getAuthToken`、注入可能なpush/pull transport、401/403時の再認証callback、永続オフラインキュー、`setScope
|
|
57
|
+
`createAuthenticatedSyncKit`は、リクエストごとの`getAuthToken`、注入可能なpush/pull transport、401/403時の再認証callback、永続オフラインキュー、`setScope`による安全なユーザー/テナント切替を提供します。スコープを切り替えたり対象スコープのキューを削除・全消去しても、他スコープの未送信操作は保持されます。詳細は[`examples/authenticated-sync`](../../examples/authenticated-sync/README.md)を参照してください。
|
|
48
58
|
|
|
49
59
|
### 0.4.xからの変更
|
|
50
60
|
|
|
@@ -83,17 +93,27 @@ Filter organization with `organization: { collection, tags, note }`, each set to
|
|
|
83
93
|
|
|
84
94
|
For rediscovery, call `useKeepItem(item).recordOpen()` to persist `lastOpenedAt`, then filter with `activity.opened`, `lastOpenedBefore`, `lastOpenedAfter`, or `inactiveForMs`. `inactiveForMs` uses `lastOpenedAt` for opened items and `savedAt` for never-opened items. Recording an open does not change `updatedAt`, and sync/merge preserves newer activity independently. `createRediscoveryQuery()` provides framework-neutral `never-opened`, `forgotten`, and `recently-opened` presets, while `useKeepRediscovery()` provides the React list hook.
|
|
85
95
|
|
|
96
|
+
Use `LocalStorageKeepHistoryStorage` when recently opened guides should include unsaved items. `record(itemId)` never adds an item to the saved list, and removing a saved item does not clear its history. Use `getAll()`, `remove()`, `clear()`, and `setLimit()` to list, delete one entry, clear history, and configure retention. Self-reported viewing events use `LocalStorageKeepViewingRecordStorage.add()` and remain distinct from opens; `set()` updates a timestamp or personal note, and `remove()`, `removeForItem()`, and `clear()` delete records.
|
|
97
|
+
|
|
86
98
|
Use `reorderKeepItems` / `moveKeepItem` for framework-neutral route ordering, or `useKeepNavigator` with `useKeepList().reorder()` / `.move()` in React. `getKeepNavigationState` returns the current, previous, next, and progress state.
|
|
87
99
|
|
|
100
|
+
To place one saved guide in several courses, use `StorageAdapter.setCollectionMembership()` with `addKeepItemToCollection()`, `getKeepCollectionItems()`, `reorderKeepCollectionItems()`, and `removeKeepItemFromCollection()`. Each course keeps its own order; removing a membership leaves the shared saved item and note intact. The existing `KeepItem.collectionId` remains available for single-category organization.
|
|
101
|
+
|
|
102
|
+
`exportItems()` writes backup v2 with collection definitions, including empty ones, and course memberships. `importItems()` also reads existing item-only v1 backups. In merge mode, a backup definition replaces the name for the same collection ID, and a backup membership replaces the order for the same course/item pair; a different ID with the same name remains a separate collection. Replace mode replaces items, definitions, and memberships. Custom storage adapters must implement the collection and membership methods to restore backups that contain this data.
|
|
103
|
+
|
|
104
|
+
Implement `RemoteSyncDriver.pushCollection()` and `pullCollections()` to sync collection creation, rename, and deletion through the durable queue. Operations carry the configured `userId` / `tenantId` scope. A full pull snapshot is authoritative: a missing definition is treated as deleted and its local memberships are removed. Pending local operations take precedence during pull. For simultaneous changes, the remote transport applies operations in arrival order, so the last accepted operation wins. `AuthenticatedSyncTransport` exposes the same two hooks.
|
|
105
|
+
|
|
106
|
+
Use `LocalStorageKeepProgressStorage` for a course's current guide and per-guide audio or reading positions. Call `saveCourse()` / `getCourse()`, `saveItem()` / `getItem()`, and `resetCourse()` / `resetItem()`. Audio positions use milliseconds; reading positions are application-defined numbers. When a record has a language or `contentVersion`, pass matching values to `getItem()`; the offset is withheld if the expected value is missing or differs. The storage API never starts playback; the Viewer should resume audio only after a user action.
|
|
107
|
+
|
|
88
108
|
Use `@keepkit/core/core` for framework-neutral code, `@keepkit/core/react` for React bindings, `@keepkit/core/storage` for browser/fallback/sync adapters, and `@keepkit/core/schema` for schema validation. The package root has no export.
|
|
89
109
|
|
|
90
110
|
`KeepProvider` accepts `fallback`, `onBoundaryError`, and `boundaryResetKey` to isolate unexpected render errors. Use the standalone `KeepErrorBoundary` from `@keepkit/core/react` when a smaller boundary is appropriate.
|
|
91
111
|
|
|
92
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.
|
|
93
113
|
|
|
94
|
-
v0.28.
|
|
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.
|
|
95
115
|
|
|
96
|
-
`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. See [`examples/authenticated-sync`](../../examples/authenticated-sync/README.md) for a recipe.
|
|
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.
|
|
97
117
|
|
|
98
118
|
The v0.5 factory returns `Provider`, `Button`, `useContext`, `useItem`, `useList`, and `useShortcut`. Existing v0.4 applications should follow the migration guide in the repository root.
|
|
99
119
|
|
|
@@ -101,10 +121,72 @@ The v0.5 factory returns `Provider`, `Button`, `useContext`, `useItem`, `useList
|
|
|
101
121
|
|
|
102
122
|
`KeepItem` and `KeepItemInput` accept optional `archived`, `pinned`, and `collectionId` fields. `useKeepList` defaults to unarchived items; pass `archived: true` for the archive, `archiveScope: "all"` for both scopes, `collectionId` for an exact collection filter, and `pinnedFirst: true` to stably promote pinned items without changing the existing order inside either group. `useKeepCollections({ targetType, orderBy })` derives de-duplicated collection IDs, names, and counts from the complete saved-item snapshot. `useKeepItem` and the provider expose `toggleArchive`, `archiveItem`, `unarchiveItem`, `togglePin`, and `moveToCollection` operations. Each operation updates `updatedAt`, removes an empty collection ID, and uses the normal persistence, rollback, plugin, and `onChange` pipeline.
|
|
103
123
|
|
|
104
|
-
The standard browser adapters persist explicit collection definitions
|
|
124
|
+
The standard browser adapters persist explicit collection definitions and course memberships separately from items. A custom `StorageAdapter` can opt in by implementing `getCollections`, `setCollection`, `removeCollection`, `getCollectionMemberships`, `setCollectionMembership`, and `removeCollectionMembership`. `clear()` still clears saved items only. JSON backup v2 includes definitions and memberships, and remote sync can include definitions through optional collection transport hooks.
|
|
105
125
|
|
|
106
126
|
## アーカイブ・ピン留め・コレクション
|
|
107
127
|
|
|
108
128
|
`KeepItem` と `KeepItemInput` は `archived`、`pinned`、`collectionId` を任意で受け取れます。`useKeepList` は未アーカイブを既定とし、`archived: true` でアーカイブを、`archiveScope: "all"` で両方を、`collectionId` で完全一致のコレクションを取得できます。`useKeepCollections({ targetType, orderBy })` は全保存アイテムからコレクションID・名前・件数を重複なく導出します。`pinnedFirst: true` は既存の順序を保ったままピン留め項目を先頭へ安定移動します。`useKeepItem` と Provider には `toggleArchive`、`archiveItem`、`unarchiveItem`、`togglePin`、`moveToCollection` を追加しました。各操作は `updatedAt` を更新し、空のコレクション ID はプロパティを削除して、既存の永続化・rollback・plugin・`onChange` 経路を利用します。
|
|
109
129
|
|
|
110
|
-
|
|
130
|
+
標準ブラウザーストレージは、空のコレクション、変更した名前、コース別の所属と順序をアイテムとは別に永続化します。独自の`StorageAdapter`では`getCollections`、`setCollection`、`removeCollection`と`getCollectionMemberships`、`setCollectionMembership`、`removeCollectionMembership`を実装すると保存できます。`clear()`は保存アイテムのみを消去し、JSONバックアップv2はコレクションと所属も含みます。リモート同期は任意のコレクション用transportを追加すると利用できます。
|
|
131
|
+
|
|
132
|
+
### v0.28.6の保存・復元補完
|
|
133
|
+
|
|
134
|
+
同じストレージインスタンス内の履歴・鑑賞記録・進行位置の書き込みは直列化されます。複数タブや別インスタンス間の排他は行いません。ブラウザー保存先が利用できない場合、書き込みは`KeepActivityStorageError`または`KeepProgressStorageError`で失敗します。SSRでは初期化できますが、保存操作はブラウザー側で呼び出してください。
|
|
135
|
+
|
|
136
|
+
既存ストレージを開いた後、コース別所属を読む機能を使う前に`migrateLegacyCollectionMemberships(storage)`を実行して旧単一所属データを移行します。既存所属を上書きせず、所属保存後に旧`collectionId`を消去するため、利用者が所属を外した後の再実行で復活しません。途中でadapterの書き込みが失敗した場合はエラーを返し、再実行しても所属は重複しません。対象scopeに定義名のないIDは戻り値の`missingCollectionIds`で確認できます。項目削除時はlocalStorage、IndexedDB、スコープadapterが所属も消します。
|
|
137
|
+
|
|
138
|
+
バックアップには保存アイテム・コレクション定義・所属のみが含まれ、閲覧履歴・鑑賞記録・進行位置は含まれません。`importItems()`の置換モードは空のバックアップでも対象範囲を空にします。v2バックアップと戻り値の`scopes`は含まれる利用者・テナント範囲を示し、`includedData`はデータ種別を示します。`imported`、`failed`、`total`は保存アイテムの件数で、`applied`は項目・定義・所属の適用数を個別に示します。スコープ付きadapterは別scopeを含むbackupを変更前に拒否します。v1の`collectionId`は名前を復元できないため`missingCollectionIds`で通知します。途中失敗は`KeepBackupImportError.failedStage`と`applied`で確認します。置換にrollbackはありません。merge時の同ID名は既定でバックアップ側を採用し、`collectionNameConflict: "existing"`を指定すると既存名を維持します。
|
|
139
|
+
|
|
140
|
+
履歴の文脈には`record(itemId, viewedAt, { language: "ja" })`で言語などを保存でき、`getAll()`で履歴と一緒に取得できます。鑑賞記録は`viewedAt`と`createdAt` / `updatedAt`を分けます。日時がない旧記録は最初に更新した時刻を作成時刻として補います。読書位置は数値offsetと安定した見出しID文字列を受け付けます。`getItem()`には必要な音声ID・言語・コンテンツ版を渡して互換位置だけを取得します。
|
|
141
|
+
|
|
142
|
+
同期adapterに`RemoteSyncDriver.pushMembership()` / `pullMemberships()`を指定すると、所属の追加・解除・コース別順序を永続キューで同期できます。所属解除はアイテム・コレクション削除より先に送信し、pull時は存在しない親への所属を適用しません。membership transportを実装しないdriverでは所属はローカル保存のみです。コレクション定義には`revision`と`updatedAt`を保存でき、操作は`baseRevision`を送ります。driverは`KeepCollectionSyncResult`で競合と選択結果を返し、アプリは`resolveCollectionSyncConflict()`からローカルまたはリモートを選べます。削除tombstoneの保持と競合方針はdriverまたはサーバーで実装します。これらは低レベルAPIであり標準画面へ自動接続されません。
|
|
143
|
+
|
|
144
|
+
```ts
|
|
145
|
+
import {
|
|
146
|
+
LocalStorageKeepHistoryStorage,
|
|
147
|
+
LocalStorageKeepProgressStorage,
|
|
148
|
+
LocalStorageKeepViewingRecordStorage,
|
|
149
|
+
exportItems,
|
|
150
|
+
importItems,
|
|
151
|
+
migrateLegacyCollectionMemberships,
|
|
152
|
+
} from "@keepkit/core/core";
|
|
153
|
+
import { createBrowserStorageAdapter } from "@keepkit/core/storage";
|
|
154
|
+
|
|
155
|
+
const storage = createBrowserStorageAdapter({ key: "my-app:items", scope: { userId: "user-1" } });
|
|
156
|
+
const history = new LocalStorageKeepHistoryStorage({ key: "my-app:user-1:history" });
|
|
157
|
+
await history.record("guide-1", Date.now(), { language: "ja" });
|
|
158
|
+
await migrateLegacyCollectionMemberships(storage);
|
|
159
|
+
const backup = await exportItems(storage);
|
|
160
|
+
await importItems(storage, backup, { mode: "replace" });
|
|
161
|
+
|
|
162
|
+
const progress = new LocalStorageKeepProgressStorage({ key: "my-app:user-1:progress" });
|
|
163
|
+
await progress.saveItem("guide-1", {
|
|
164
|
+
audioId: "guide-1-ja",
|
|
165
|
+
audioPositionMs: 1250,
|
|
166
|
+
readingPosition: "heading-intro",
|
|
167
|
+
language: "ja",
|
|
168
|
+
contentVersion: "v2",
|
|
169
|
+
});
|
|
170
|
+
const resume = await progress.getItem("guide-1", {
|
|
171
|
+
audioId: "guide-1-ja",
|
|
172
|
+
language: "ja",
|
|
173
|
+
contentVersion: "v2",
|
|
174
|
+
});
|
|
175
|
+
const viewings = new LocalStorageKeepViewingRecordStorage({ key: "my-app:user-1:viewings" });
|
|
176
|
+
const record = { id: "viewing-1", itemId: "guide-1", viewedAt: Date.now(), note: "Finished" };
|
|
177
|
+
await viewings.set(record); // Reuse this stable ID when retrying the write.
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### Persistence and restore improvements in v0.28.6
|
|
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.
|
|
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.
|
|
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.
|
|
187
|
+
|
|
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
|
+
|
|
190
|
+
Implement `RemoteSyncDriver.pushMembership()` / `pullMemberships()` to sync membership changes and course-specific order through the durable queue. Membership removals are sent before item or collection deletions, and pulls skip memberships whose parent is missing. Without membership transport hooks, membership changes remain local. Collection definitions can store `revision` and `updatedAt`, and operations send `baseRevision`. Drivers return `KeepCollectionSyncResult` for conflicts and outcomes; the app can choose local or remote with `resolveCollectionSyncConflict()`. The server or driver owns deletion tombstones and conflict policy. These are low-level APIs and are not automatically connected to the standard UI.
|
|
191
|
+
|
|
192
|
+
The example above imports the persistence APIs from `@keepkit/core/core` and browser adapters from `@keepkit/core/storage`. Keep history and progress keys scoped by the host app's account or tenant when those records should not cross users. Reuse a viewing-record ID with `set()` when retrying so the event is not duplicated.
|
|
@@ -2,12 +2,14 @@ import {
|
|
|
2
2
|
validateKeepItem
|
|
3
3
|
} from "./chunk-5W4QSJHV.js";
|
|
4
4
|
import {
|
|
5
|
+
isKeepCollectionDefinition,
|
|
6
|
+
isKeepCollectionMembership,
|
|
5
7
|
isKeepItem,
|
|
6
8
|
isRecord,
|
|
7
9
|
mergeKeepItemLists,
|
|
8
10
|
persistKeepItems,
|
|
9
11
|
removeKeepItems
|
|
10
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-QRPQYE7K.js";
|
|
11
13
|
|
|
12
14
|
// src/features/items/navigation.ts
|
|
13
15
|
function orderKeepItems(items) {
|
|
@@ -81,10 +83,56 @@ async function migrateKeepItems(source, target) {
|
|
|
81
83
|
await source.clear();
|
|
82
84
|
return merged;
|
|
83
85
|
}
|
|
86
|
+
async function migrateLegacyCollectionMemberships(storage) {
|
|
87
|
+
if (!storage.getCollectionMemberships || !storage.setCollectionMembership) {
|
|
88
|
+
throw new TypeError("The storage adapter does not support collection memberships.");
|
|
89
|
+
}
|
|
90
|
+
const [items, memberships, collections] = await Promise.all([
|
|
91
|
+
storage.getAll(),
|
|
92
|
+
storage.getCollectionMemberships(),
|
|
93
|
+
storage.getCollections?.() ?? Promise.resolve([])
|
|
94
|
+
]);
|
|
95
|
+
const existing = new Set(memberships.map(membershipKey));
|
|
96
|
+
const collectionIds = new Set(collections.map((collection) => collectionKey(collection.id, collection.scope)));
|
|
97
|
+
const missingCollectionIds = /* @__PURE__ */ new Set();
|
|
98
|
+
let migrated = 0;
|
|
99
|
+
let skipped = 0;
|
|
100
|
+
for (const item of items) {
|
|
101
|
+
if (!item.collectionId) continue;
|
|
102
|
+
const membership = {
|
|
103
|
+
collectionId: item.collectionId,
|
|
104
|
+
itemId: item.id,
|
|
105
|
+
order: item.order ?? 0,
|
|
106
|
+
...item.scope ? { scope: item.scope } : {}
|
|
107
|
+
};
|
|
108
|
+
const key = membershipKey(membership);
|
|
109
|
+
if (!collectionIds.has(collectionKey(item.collectionId, item.scope))) {
|
|
110
|
+
missingCollectionIds.add(item.collectionId);
|
|
111
|
+
}
|
|
112
|
+
if (existing.has(key)) {
|
|
113
|
+
skipped += 1;
|
|
114
|
+
const { collectionId: _legacyCollectionId2, ...migratedItem2 } = item;
|
|
115
|
+
await storage.set(migratedItem2);
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
await storage.setCollectionMembership(membership);
|
|
119
|
+
existing.add(key);
|
|
120
|
+
migrated += 1;
|
|
121
|
+
const { collectionId: _legacyCollectionId, ...migratedItem } = item;
|
|
122
|
+
await storage.set(migratedItem);
|
|
123
|
+
}
|
|
124
|
+
return { migrated, skipped, missingCollectionIds: [...missingCollectionIds] };
|
|
125
|
+
}
|
|
126
|
+
function membershipKey(membership) {
|
|
127
|
+
return `${membership.scope?.tenantId ?? ""}\0${membership.scope?.userId ?? ""}\0${membership.collectionId}\0${membership.itemId}`;
|
|
128
|
+
}
|
|
129
|
+
function collectionKey(collectionId, scope) {
|
|
130
|
+
return `${scope?.tenantId ?? ""}\0${scope?.userId ?? ""}\0${collectionId}`;
|
|
131
|
+
}
|
|
84
132
|
|
|
85
133
|
// src/features/persistence/backup.ts
|
|
86
134
|
var KEEP_BACKUP_FORMAT = "keepkit";
|
|
87
|
-
var KEEP_BACKUP_VERSION =
|
|
135
|
+
var KEEP_BACKUP_VERSION = 2;
|
|
88
136
|
var KeepBackupParseError = class extends Error {
|
|
89
137
|
constructor(message, options) {
|
|
90
138
|
super(message);
|
|
@@ -99,21 +147,48 @@ var KeepBackupImportError = class extends Error {
|
|
|
99
147
|
this.mode = options.mode;
|
|
100
148
|
this.imported = options.imported;
|
|
101
149
|
this.failed = options.failed;
|
|
150
|
+
this.applied = options.applied ?? { items: options.imported, collections: 0, memberships: 0 };
|
|
151
|
+
this.failedStage = options.failedStage ?? "items";
|
|
102
152
|
if (options.cause !== void 0) this.cause = options.cause;
|
|
103
153
|
}
|
|
104
154
|
};
|
|
105
155
|
async function exportItems(adapter) {
|
|
156
|
+
const items = await adapter.getAll();
|
|
157
|
+
const collections = await adapter.getCollections?.() ?? [];
|
|
158
|
+
const memberships = await adapter.getCollectionMemberships?.() ?? [];
|
|
106
159
|
const backup = {
|
|
107
160
|
format: KEEP_BACKUP_FORMAT,
|
|
108
161
|
version: KEEP_BACKUP_VERSION,
|
|
109
162
|
exportedAt: Date.now(),
|
|
110
|
-
items
|
|
163
|
+
items,
|
|
164
|
+
collections,
|
|
165
|
+
memberships,
|
|
166
|
+
scopes: collectScopes(items, collections, memberships, adapter.scope ? [adapter.scope] : [])
|
|
111
167
|
};
|
|
112
168
|
return JSON.stringify(backup, null, 2);
|
|
113
169
|
}
|
|
114
170
|
async function importItems(adapter, data, options = {}) {
|
|
115
171
|
const backup = parseBackup(data);
|
|
116
172
|
const mode = options.mode ?? "merge";
|
|
173
|
+
const scopes = collectScopes(backup.items, backup.collections, backup.memberships, backup.scopes);
|
|
174
|
+
if (adapter.scope && scopes.some((scope) => !sameScope(scope, adapter.scope))) {
|
|
175
|
+
throw new KeepBackupImportError("The backup contains data outside the active storage scope.", {
|
|
176
|
+
mode,
|
|
177
|
+
imported: 0,
|
|
178
|
+
failed: backup.items.length + backup.collections.length + backup.memberships.length,
|
|
179
|
+
applied: { items: 0, collections: 0, memberships: 0 },
|
|
180
|
+
failedStage: "validation"
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
if (backup.collections.length > 0 && (!adapter.getCollections || !adapter.setCollection || !adapter.removeCollection) || backup.memberships.length > 0 && (!adapter.getCollectionMemberships || !adapter.setCollectionMembership || !adapter.removeCollectionMembership)) {
|
|
184
|
+
throw new KeepBackupImportError("The storage adapter cannot restore collection data from this backup.", {
|
|
185
|
+
mode,
|
|
186
|
+
imported: 0,
|
|
187
|
+
failed: backup.items.length + backup.collections.length + backup.memberships.length,
|
|
188
|
+
applied: { items: 0, collections: 0, memberships: 0 },
|
|
189
|
+
failedStage: "validation"
|
|
190
|
+
});
|
|
191
|
+
}
|
|
117
192
|
const validItems = [];
|
|
118
193
|
let failed = 0;
|
|
119
194
|
for (const item of backup.items) {
|
|
@@ -132,37 +207,88 @@ async function importItems(adapter, data, options = {}) {
|
|
|
132
207
|
throw cause;
|
|
133
208
|
}
|
|
134
209
|
}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
imported: 0,
|
|
143
|
-
failed: validItems.length + failed,
|
|
144
|
-
cause
|
|
145
|
-
});
|
|
210
|
+
const applied = { items: 0, collections: 0, memberships: 0 };
|
|
211
|
+
const missingCollectionIds = backup.version === 1 ? [...new Set(backup.items.flatMap((item) => item.collectionId ? [item.collectionId] : []))] : [];
|
|
212
|
+
try {
|
|
213
|
+
if (mode === "replace") {
|
|
214
|
+
await adapter.clear();
|
|
215
|
+
await restoreCollections(adapter, [], "replace", "backup", applied);
|
|
216
|
+
await restoreMemberships(adapter, [], "replace", applied);
|
|
146
217
|
}
|
|
147
|
-
}
|
|
148
|
-
|
|
218
|
+
} catch (cause) {
|
|
219
|
+
throw new KeepBackupImportError("KeepKit could not clear the data being replaced.", {
|
|
220
|
+
mode,
|
|
221
|
+
imported: applied.items,
|
|
222
|
+
failed: validItems.length + failed,
|
|
223
|
+
applied,
|
|
224
|
+
failedStage: "clear",
|
|
225
|
+
cause
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
for (const item of validItems) {
|
|
149
229
|
try {
|
|
150
|
-
await adapter
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
imported += 1;
|
|
154
|
-
}
|
|
155
|
-
items = await adapter.getAll();
|
|
230
|
+
if (mode === "merge") await mergeKeepItems([item], adapter);
|
|
231
|
+
else await adapter.set(item);
|
|
232
|
+
applied.items += 1;
|
|
156
233
|
} catch (cause) {
|
|
157
|
-
throw new KeepBackupImportError(
|
|
234
|
+
throw new KeepBackupImportError(`KeepKit could not restore item ${item.id}.`, {
|
|
158
235
|
mode,
|
|
159
|
-
imported,
|
|
160
|
-
failed: validItems.length + failed -
|
|
236
|
+
imported: applied.items,
|
|
237
|
+
failed: validItems.length + failed - applied.items,
|
|
238
|
+
applied,
|
|
239
|
+
failedStage: "items",
|
|
161
240
|
cause
|
|
162
241
|
});
|
|
163
242
|
}
|
|
164
243
|
}
|
|
165
|
-
|
|
244
|
+
try {
|
|
245
|
+
await restoreCollections(adapter, backup.collections, mode, options.collectionNameConflict ?? "backup", applied);
|
|
246
|
+
} catch (cause) {
|
|
247
|
+
throw new KeepBackupImportError("KeepKit could not restore collection definitions.", {
|
|
248
|
+
mode,
|
|
249
|
+
imported: applied.items,
|
|
250
|
+
failed: validItems.length + failed - applied.items,
|
|
251
|
+
applied,
|
|
252
|
+
failedStage: "collections",
|
|
253
|
+
cause
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
try {
|
|
257
|
+
await restoreMemberships(adapter, backup.memberships, mode, applied);
|
|
258
|
+
} catch (cause) {
|
|
259
|
+
throw new KeepBackupImportError("KeepKit could not restore collection memberships.", {
|
|
260
|
+
mode,
|
|
261
|
+
imported: applied.items,
|
|
262
|
+
failed: validItems.length + failed - applied.items,
|
|
263
|
+
applied,
|
|
264
|
+
failedStage: "memberships",
|
|
265
|
+
cause
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
let items;
|
|
269
|
+
try {
|
|
270
|
+
items = await adapter.getAll();
|
|
271
|
+
} catch (cause) {
|
|
272
|
+
throw new KeepBackupImportError("KeepKit restored the backup but could not read the resulting items.", {
|
|
273
|
+
mode,
|
|
274
|
+
imported: applied.items,
|
|
275
|
+
failed,
|
|
276
|
+
applied,
|
|
277
|
+
failedStage: "items",
|
|
278
|
+
cause
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
return {
|
|
282
|
+
mode,
|
|
283
|
+
imported: applied.items,
|
|
284
|
+
failed,
|
|
285
|
+
total: items.length,
|
|
286
|
+
items,
|
|
287
|
+
applied,
|
|
288
|
+
missingCollectionIds,
|
|
289
|
+
includedData: backup.version === 1 ? ["items"] : ["items", "collections", "memberships"],
|
|
290
|
+
scopes
|
|
291
|
+
};
|
|
166
292
|
}
|
|
167
293
|
function parseBackup(data) {
|
|
168
294
|
let value = data;
|
|
@@ -174,7 +300,7 @@ function parseBackup(data) {
|
|
|
174
300
|
}
|
|
175
301
|
}
|
|
176
302
|
if (!isRecord(value)) throw new KeepBackupParseError("KeepKit backup must be an object.");
|
|
177
|
-
if (value.format !== KEEP_BACKUP_FORMAT || value.version !== KEEP_BACKUP_VERSION) {
|
|
303
|
+
if (value.format !== KEEP_BACKUP_FORMAT || value.version !== 1 && value.version !== KEEP_BACKUP_VERSION) {
|
|
178
304
|
throw new KeepBackupParseError("KeepKit backup format or version is unsupported.");
|
|
179
305
|
}
|
|
180
306
|
if (typeof value.exportedAt !== "number" || !Number.isFinite(value.exportedAt)) {
|
|
@@ -183,8 +309,65 @@ function parseBackup(data) {
|
|
|
183
309
|
if (!Array.isArray(value.items) || !value.items.every(isKeepItem)) {
|
|
184
310
|
throw new KeepBackupParseError("KeepKit backup contains invalid items.");
|
|
185
311
|
}
|
|
312
|
+
if (value.version === 1) {
|
|
313
|
+
return {
|
|
314
|
+
format: KEEP_BACKUP_FORMAT,
|
|
315
|
+
version: 1,
|
|
316
|
+
exportedAt: value.exportedAt,
|
|
317
|
+
items: value.items,
|
|
318
|
+
collections: [],
|
|
319
|
+
memberships: [],
|
|
320
|
+
scopes: []
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
if (!Array.isArray(value.collections) || !value.collections.every(isKeepCollectionDefinition) || !Array.isArray(value.memberships) || !value.memberships.every(isKeepCollectionMembership)) {
|
|
324
|
+
throw new KeepBackupParseError("KeepKit backup contains invalid collection data.");
|
|
325
|
+
}
|
|
326
|
+
if (value.scopes !== void 0 && (!Array.isArray(value.scopes) || !value.scopes.every(isSyncScope))) {
|
|
327
|
+
throw new KeepBackupParseError("KeepKit backup contains invalid scope data.");
|
|
328
|
+
}
|
|
186
329
|
return value;
|
|
187
330
|
}
|
|
331
|
+
function collectScopes(items, collections, memberships, declaredScopes = []) {
|
|
332
|
+
const scopes = [
|
|
333
|
+
...declaredScopes,
|
|
334
|
+
...[...items, ...collections, ...memberships].flatMap((record) => record.scope ? [record.scope] : [])
|
|
335
|
+
].filter((scope, index, all) => all.findIndex((entry) => sameScope(entry, scope)) === index);
|
|
336
|
+
return scopes.map((scope) => ({ ...scope }));
|
|
337
|
+
}
|
|
338
|
+
function sameScope(left, right) {
|
|
339
|
+
return left.userId === right?.userId && left.tenantId === right?.tenantId;
|
|
340
|
+
}
|
|
341
|
+
function isSyncScope(value) {
|
|
342
|
+
return isRecord(value) && (value.userId === void 0 || typeof value.userId === "string") && (value.tenantId === void 0 || typeof value.tenantId === "string");
|
|
343
|
+
}
|
|
344
|
+
async function restoreCollections(adapter, collections, mode, conflict, applied) {
|
|
345
|
+
if (!adapter.getCollections || !adapter.setCollection || !adapter.removeCollection) return;
|
|
346
|
+
const existing = await adapter.getCollections();
|
|
347
|
+
if (mode === "replace") {
|
|
348
|
+
for (const collection of existing) await adapter.removeCollection(collection.id, collection.scope);
|
|
349
|
+
}
|
|
350
|
+
for (const collection of collections) {
|
|
351
|
+
const current = existing.find((entry) => entry.id === collection.id);
|
|
352
|
+
const value = mode === "merge" && conflict === "existing" && current ? { ...collection, name: current.name } : collection;
|
|
353
|
+
await adapter.setCollection(value);
|
|
354
|
+
applied.collections += 1;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
async function restoreMemberships(adapter, memberships, mode, applied) {
|
|
358
|
+
if (!adapter.getCollectionMemberships || !adapter.setCollectionMembership || !adapter.removeCollectionMembership)
|
|
359
|
+
return;
|
|
360
|
+
if (mode === "replace") {
|
|
361
|
+
const current = await adapter.getCollectionMemberships();
|
|
362
|
+
for (const membership of current) {
|
|
363
|
+
await adapter.removeCollectionMembership(membership.collectionId, membership.itemId, membership.scope);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
for (const membership of memberships) {
|
|
367
|
+
await adapter.setCollectionMembership(membership);
|
|
368
|
+
applied.memberships += 1;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
188
371
|
|
|
189
372
|
// src/features/items/query.ts
|
|
190
373
|
function createInboxQuery(options = {}) {
|
|
@@ -390,6 +573,7 @@ export {
|
|
|
390
573
|
moveKeepItem,
|
|
391
574
|
mergeKeepItems,
|
|
392
575
|
migrateKeepItems,
|
|
576
|
+
migrateLegacyCollectionMemberships,
|
|
393
577
|
KEEP_BACKUP_FORMAT,
|
|
394
578
|
KEEP_BACKUP_VERSION,
|
|
395
579
|
KeepBackupParseError,
|
|
@@ -405,4 +589,4 @@ export {
|
|
|
405
589
|
reconcileKeepItems,
|
|
406
590
|
KeepStore
|
|
407
591
|
};
|
|
408
|
-
//# sourceMappingURL=chunk-
|
|
592
|
+
//# sourceMappingURL=chunk-5XSSH3TO.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((entry) => entry.id === collection.id);\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"]}
|