-
Notifications
You must be signed in to change notification settings - Fork 176
feat(drivers): add Tauri Store driver and docs #753
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| --- | ||
| icon: simple-icons:tauri | ||
| --- | ||
|
|
||
| # Tauri Store | ||
|
|
||
| > Store data via [Tauri Store Plugin](https://tauri.app/plugin/store) in Tauri desktop/mobile apps. | ||
|
|
||
| ::read-more{to="https://tauri.app/plugin/store"} | ||
| Learn more about Tauri Store Plugin. | ||
| :: | ||
|
|
||
| ## Usage | ||
|
|
||
| **Driver name:** `tauri` | ||
|
|
||
| Install the Tauri store plugin in your Tauri project, then install unstorage: | ||
|
|
||
| :pm-install{name="@tauri-apps/plugin-store"} | ||
|
|
||
| Usage: | ||
|
|
||
| ```js | ||
| import { createStorage } from "unstorage"; | ||
| import tauriDriver from "unstorage/drivers/tauri"; | ||
|
|
||
| const storage = createStorage({ | ||
| driver: tauriDriver({ | ||
| path: "store.json", | ||
| base: "app", | ||
| options: { autoSave: 100 }, | ||
| }), | ||
| }); | ||
| ``` | ||
|
|
||
| **Options:** | ||
|
|
||
| - `path`: Path to the store file (e.g. `"store.json"`). Required. | ||
| - `base`: Optional prefix for all keys (namespace). | ||
| - `options`: Optional [StoreOptions](https://tauri.app/plugin/store/) from `@tauri-apps/plugin-store` (e.g. `autoSave`: `false` to disable auto-save, or a number for debounce ms; default 100). | ||
|
|
||
| The driver supports `watch` via the store’s `onChange` listener for key updates. |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,83 @@ | ||
| import { load } from "@tauri-apps/plugin-store"; | ||
|
|
||
| import { type DriverFactory, joinKeys, normalizeKey } from "./utils/index.ts"; | ||
|
|
||
| const DRIVER_NAME = "tauri"; | ||
|
|
||
| export interface TauriStorageDriverOptions { | ||
| /** | ||
| * Path to the store file (e.g. `"store.json"`). | ||
| */ | ||
| path: string; | ||
| /** | ||
| * Optional [StoreOptions](https://tauri.app/plugin/store/) (e.g. `autoSave`). | ||
| */ | ||
| options?: import("@tauri-apps/plugin-store").StoreOptions; | ||
| /** | ||
| * Optional prefix for all keys (namespace). | ||
| */ | ||
| base?: string; | ||
| } | ||
|
|
||
| type TauriStore = Awaited<ReturnType<typeof load>>; | ||
|
|
||
| const driver: DriverFactory<TauriStorageDriverOptions, Promise<TauriStore>> = (opts) => { | ||
| const base = normalizeKey(opts?.base || ""); | ||
| const resolveKey = (key: string) => joinKeys(base, key); | ||
|
|
||
| const storePromise = load(opts.path, opts.options); | ||
|
|
||
| return { | ||
| name: DRIVER_NAME, | ||
| options: opts, | ||
| getInstance: () => storePromise, | ||
| async hasItem(key) { | ||
| const store = await storePromise; | ||
| return store.has(resolveKey(key)); | ||
| }, | ||
| async getItem(key) { | ||
| const store = await storePromise; | ||
| return store.get(resolveKey(key)) ?? null; | ||
| }, | ||
| async setItem(key, value) { | ||
| const store = await storePromise; | ||
| await store.set(resolveKey(key), value); | ||
| }, | ||
| async removeItem(key) { | ||
| const store = await storePromise; | ||
| return store.delete(resolveKey(key)); | ||
| }, | ||
| async getKeys(basePrefix) { | ||
| const store = await storePromise; | ||
| const prefix = resolveKey(basePrefix || ""); | ||
| const allKeys = await store.keys(); | ||
| if (!prefix) { | ||
| return base ? allKeys.map((k) => (k.startsWith(base + ":") ? k.slice(base.length + 1) : k)).filter(Boolean) : allKeys; | ||
| } | ||
| return allKeys | ||
| .filter((k) => k === prefix || k.startsWith(prefix + ":")) | ||
| .map((k) => (base ? k.slice(base.length + 1) : k)) | ||
| .filter(Boolean); | ||
| }, | ||
| async clear(basePrefix) { | ||
| const store = await storePromise; | ||
| const prefix = resolveKey(basePrefix || ""); | ||
| const allKeys = await store.keys(); | ||
| const toRemove = prefix | ||
| ? allKeys.filter((k) => k === prefix || k.startsWith(prefix + ":")) | ||
| : base | ||
| ? allKeys.filter((k) => k === base || k.startsWith(base + ":")) | ||
| : allKeys; | ||
| await Promise.all(toRemove.map((k) => store.delete(k))); | ||
| }, | ||
| async watch(callback) { | ||
| const store = await storePromise; | ||
| return store.onChange((key, value) => { | ||
| const eventKey = base && key.startsWith(base + ":") ? key.slice(base.length + 1) : key; | ||
| callback(value === null ? "remove" : "update", eventKey); | ||
| }); | ||
| }, | ||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| }; | ||
| }; | ||
|
|
||
| export default driver; | ||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,35 @@ | ||||||
| import { afterEach, describe, vi } from "vitest"; | ||||||
| import driver from "../../src/drivers/tauri.ts"; | ||||||
| import { testDriver } from "./utils.ts"; | ||||||
|
|
||||||
| vi.mock("@tauri-apps/plugin-store", () => { | ||||||
| return { | ||||||
| load: vi.fn(() => { | ||||||
| const data = new Map<string, unknown>(); | ||||||
| return Promise.resolve({ | ||||||
| has: (key: string) => Promise.resolve(data.has(key)), | ||||||
| get: (key: string) => Promise.resolve(data.get(key) ?? null), | ||||||
| set: (key: string, value: unknown) => | ||||||
| Promise.resolve(void data.set(key, value)), | ||||||
| delete: (key: string) => Promise.resolve(void data.delete(key)), | ||||||
| keys: () => Promise.resolve([...data.keys()]), | ||||||
| clear: () => Promise.resolve(void data.clear()), | ||||||
| onChange: (_cb: (key: string, value: unknown) => void) => () => {}, | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: fd "tauri.test.ts" --type fRepository: unjs/unstorage Length of output: 84 🏁 Script executed: cat -n test/drivers/tauri.test.ts | head -50Repository: unjs/unstorage Length of output: 1355 🏁 Script executed: # Check if `@tauri-apps/plugin-store` is documented in the codebase
rg "tauri-apps/plugin-store" -A 3 -B 3Repository: unjs/unstorage Length of output: 3834 🏁 Script executed: # Look for Store interface or type definitions
rg "Store" test/drivers/tauri.test.ts -B 2 -A 2Repository: unjs/unstorage Length of output: 40 🏁 Script executed: cat -n src/drivers/tauri.tsRepository: unjs/unstorage Length of output: 3304 🏁 Script executed: # Search for how onChange is used in the driver
rg "onChange" src/drivers/tauri.ts -B 3 -A 3Repository: unjs/unstorage Length of output: 350 🏁 Script executed: # Look for watch implementation
rg "watch" src/drivers/tauri.ts -B 2 -A 2Repository: unjs/unstorage Length of output: 241 🌐 Web query:
💡 Result:
|
||||||
| onChange: (_cb: (key: string, value: unknown) => void) => () => {}, | |
| onChange: async (_cb: (key: string, value: unknown) => void) => () => {}, |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@test/drivers/tauri.test.ts` at line 17, The mock for onChange currently
returns the unlisten function synchronously; change it to return a
Promise<UnlistenFn> to match `@tauri-apps/plugin-store` v2.4.2 (i.e., have
onChange: (_cb: (key: string, value: unknown) => void) => Promise.resolve(() =>
{ /* unlisten */ })). Ensure the mock signature and callers that await the
result behave like the real API (return a Promise that resolves to the unlisten
function).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Filter
watch()events to the configured base namespace.When
baseis set, this still forwards changes for every key in the store. A driver scoped toappwill emit events for unrelated namespaces likeother:*, which breaks the isolation the rest of the driver enforces.🐛 Proposed fix
async watch(callback) { const store = await storePromise; return store.onChange((key, value) => { - const eventKey = base && key.startsWith(base + ":") ? key.slice(base.length + 1) : key; + if (base && key !== base && !key.startsWith(`${base}:`)) { + return; + } + const eventKey = + base ? (key === base ? "" : key.slice(base.length + 1)) : key; callback(value === null ? "remove" : "update", eventKey); }); },🤖 Prompt for AI Agents