feat(drivers): add Tauri Store driver and docs#753
Conversation
- Add tauri driver using @tauri-apps/plugin-store (path, base, options) - Support hasItem, getItem, setItem, removeItem, getKeys, clear, watch - Add optional peer @tauri-apps/plugin-store, regen _drivers.ts - Add docs/2.drivers/tauri.md and card in drivers index Closes unjs#748 Made-with: Cursor
📝 WalkthroughWalkthroughAdds a new Tauri Store driver: implementation, type entries, tests, documentation, and package.json dependency metadata to integrate with Changes
Sequence DiagramsequenceDiagram
participant App as Application
participant Driver as Tauri Driver
participant Store as Tauri Store Plugin
participant File as Store File
App->>Driver: create(path, options)
Driver->>Store: Store.load(path, options)
Store->>File: initialize/read file
File-->>Store: return data
Store-->>Driver: store instance ready
App->>Driver: setItem(key, value)
Driver->>Store: normalizedKey -> set(key, value)
Store->>File: persist change
File-->>Store: ack
Store-->>Driver: operation complete
App->>Driver: watch(cb)
Driver->>Store: register onChange listener
File->>Store: external change
Store->>Driver: onChange(key, value)
Driver->>App: cb('update' or 'remove', key)
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~22 minutes Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (2)
src/drivers/tauri.ts (1)
33-33: Redundant type cast.
storePromisealready has typePromise<TauriStore>from theload()call, making theas Promise<TauriStore>cast unnecessary.♻️ Suggested simplification
- getInstance: () => storePromise as Promise<TauriStore>, + getInstance: () => storePromise,🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/drivers/tauri.ts` at line 33, The getInstance return includes an unnecessary type assertion; remove the redundant "as Promise<TauriStore>" so getInstance simply returns storePromise (which is already a Promise<TauriStore>). Update the getInstance property in the same object where storePromise is defined to return storePromise directly, leaving the existing load()-based type intact.package.json (1)
186-188: Consider maintaining alphabetical order inpeerDependenciesMeta.The entry is placed after
ofetch, but other entries appear to be alphabetically sorted by package name.@tauri-apps/plugin-storeshould be placed after@planetscale/database(line 147-149) and before@upstash/redis(line 150-152).🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@package.json` around lines 186 - 188, The peerDependenciesMeta block has entries intended to be alphabetical but "@tauri-apps/plugin-store" is out of order; relocate the "@tauri-apps/plugin-store" object so it appears after the "@planetscale/database" entry and before the "@upstash/redis" entry within peerDependenciesMeta to restore alphabetical order, preserving the same object shape ({"optional": true}) and commas.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/drivers/tauri.ts`:
- Around line 73-78: The watch function currently always calls
callback("update", _key) even when a key was deleted; update the watch
implementation (inside async watch, using storePromise and store.onChange) to
inspect the _value argument from store.onChange and call callback("remove",
_key) when _value is null (otherwise call callback("update", _key)), preserving
the return value from store.onChange and keeping the same callback signature
expected by the WatchEvent type.
In `@test/drivers/tauri.test.ts`:
- Around line 1-4: The test imports afterEach from node:test which doesn't
register with Vitest so vi.resetAllMocks() never runs; change the import to pull
afterEach from "vitest" (alongside describe and vi) so the vitest runtime runs
the hook, and ensure the existing afterEach(() => vi.resetAllMocks()) remains
using that imported afterEach identifier.
---
Nitpick comments:
In `@package.json`:
- Around line 186-188: The peerDependenciesMeta block has entries intended to be
alphabetical but "@tauri-apps/plugin-store" is out of order; relocate the
"@tauri-apps/plugin-store" object so it appears after the
"@planetscale/database" entry and before the "@upstash/redis" entry within
peerDependenciesMeta to restore alphabetical order, preserving the same object
shape ({"optional": true}) and commas.
In `@src/drivers/tauri.ts`:
- Line 33: The getInstance return includes an unnecessary type assertion; remove
the redundant "as Promise<TauriStore>" so getInstance simply returns
storePromise (which is already a Promise<TauriStore>). Update the getInstance
property in the same object where storePromise is defined to return storePromise
directly, leaving the existing load()-based type intact.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: e3a866db-d7a5-4c47-a532-4e347982c247
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (6)
docs/2.drivers/0.index.mddocs/2.drivers/tauri.mdpackage.jsonsrc/_drivers.tssrc/drivers/tauri.tstest/drivers/tauri.test.ts
- Remove base/namespace option from TauriStorageDriverOptions - watch: emit 'remove' when onChange value is null (per WatchEvent) - getInstance: remove redundant Promise<TauriStore> cast - test: import afterEach from vitest so resetAllMocks runs - package.json: alphabetical order for @tauri-apps/plugin-store in peerDependenciesMeta Made-with: Cursor
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/drivers/tauri.ts`:
- Around line 73-78: The watch() handler forwards every store change even when a
base namespace is configured, so update the store.onChange callback in the watch
function to ignore events not in the configured base: inside the onChange
callback (the function created around storePromise and base) first return early
if base is set and the incoming key does not start with base + ":"; otherwise
compute eventKey exactly as now (key.slice(base.length + 1) when base is
present) and invoke callback with "remove" or "update". This uses the existing
symbols watch, storePromise, base and store.onChange to scope emitted events to
the configured namespace.
In `@test/drivers/tauri.test.ts`:
- 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).
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: decf75c9-edd6-4f4f-a60f-c7e6cc880b2b
📒 Files selected for processing (2)
src/drivers/tauri.tstest/drivers/tauri.test.ts
| 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); | ||
| }); |
There was a problem hiding this comment.
Filter watch() events to the configured base namespace.
When base is set, this still forwards changes for every key in the store. A driver scoped to app will emit events for unrelated namespaces like other:*, 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
Verify each finding against the current code and only fix it if needed.
In `@src/drivers/tauri.ts` around lines 73 - 78, The watch() handler forwards
every store change even when a base namespace is configured, so update the
store.onChange callback in the watch function to ignore events not in the
configured base: inside the onChange callback (the function created around
storePromise and base) first return early if base is set and the incoming key
does not start with base + ":"; otherwise compute eventKey exactly as now
(key.slice(base.length + 1) when base is present) and invoke callback with
"remove" or "update". This uses the existing symbols watch, storePromise, base
and store.onChange to scope emitted events to the configured namespace.
| 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.
🧩 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:
@tauri-apps/plugin-store 2.4.2 onChange API documentation
💡 Result:
@tauri-apps/plugin-store — Store.onChange (v2.x, incl. 2.4.2)
Signature
onChange(cb: (key: string, value: unknown) => void): Promise<UnlistenFn>- What it does: listens to any change in the store and calls
cb(key, value)with the changed key and its new value. [1] - Return value: a
Promisethat resolves to an unlisten function (UnlistenFn) to stop listening. [1]
Docs
- Store class API (
onChange): [1] - Tauri JS reference page (shows
onChange<T>(cb): Promise<UnlistenFn>on the Store API listing): [2]
Example
import { load } from '@tauri-apps/plugin-store';
const store = await load('store.json');
const unlisten = await store.onChange((key, value) => {
console.log('changed:', key, value);
});
// later
unlisten();Related API
onKeyChange<T>(key, cb): Promise<UnlistenFn>listens for changes to a single key. [1]
Sources: [1] [2]
🌐 Web query:
tauri plugin store onChange return type listener unsubscribe
💡 Result:
In @tauri-apps/plugin-store (Tauri v2), store.onChange returns a Promise<UnlistenFn> (i.e., you must await it, and it resolves to an unsubscribe function). (v2.tauri.app)
import { Store } from '@tauri-apps/plugin-store';
const store = new Store('settings.json');
const unlisten = await store.onChange((key, value) => {
// handle change
});
// later: unsubscribe
unlisten();UnlistenFn is the same concept used by Tauri’s event system: a function you call to stop listening. (tauri.app)
Citations:
- 1: https://v2.tauri.app/reference/javascript/store/?utm_source=openai
- 2: https://tauri.app/reference/javascript/api/namespaceevent/?utm_source=openai
Make the onChange mock return a Promise to match the plugin API.
The official @tauri-apps/plugin-store v2.4.2 API defines onChange(cb): Promise<UnlistenFn>. The current mock returns the unlisten function synchronously, which doesn't match the real API that requires awaiting the result. This can hide async subscription/unsubscription bugs.
🐛 Proposed fix
- onChange: (_cb: (key: string, value: unknown) => void) => () => {},
+ onChange: async (_cb: (key: string, value: unknown) => void) => () => {},📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| 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).
Summary
Add built-in driver for Tauri Store Plugin and driver documentation.
Changes
unstorage/drivers/tauri:path,base,options(StoreOptions); implements hasItem, getItem, setItem, removeItem, getKeys, clear, watchdocs/2.drivers/tauri.md+ card in drivers index@tauri-apps/plugin-store^2.0.0test/drivers/tauri.test.ts(mocked store)Closes #748
Summary by CodeRabbit
New Features
Documentation
Tests