Skip to content

feat(drivers): add Tauri Store driver and docs#753

Open
hairyf wants to merge 3 commits intounjs:mainfrom
hairyf:feature/tauri-store-driver
Open

feat(drivers): add Tauri Store driver and docs#753
hairyf wants to merge 3 commits intounjs:mainfrom
hairyf:feature/tauri-store-driver

Conversation

@hairyf
Copy link

@hairyf hairyf commented Mar 6, 2026

Summary

Add built-in driver for Tauri Store Plugin and driver documentation.

Changes

  • Driver unstorage/drivers/tauri: path, base, options (StoreOptions); implements hasItem, getItem, setItem, removeItem, getKeys, clear, watch
  • Docs: docs/2.drivers/tauri.md + card in drivers index
  • Optional peer: @tauri-apps/plugin-store ^2.0.0
  • Tests: test/drivers/tauri.test.ts (mocked store)

Closes #748

Summary by CodeRabbit

  • New Features

    • Added a Tauri Store driver for data persistence in Tauri apps — configurable store path, optional namespace prefix, auto-save debounce, and change-watch support.
  • Documentation

    • New driver docs with installation notes, quick-start examples, options reference, and watch usage.
  • Tests

    • Added tests validating driver behavior using a mocked Tauri store.

- 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
@hairyf hairyf requested a review from pi0 as a code owner March 6, 2026 03:04
@coderabbitai
Copy link

coderabbitai bot commented Mar 6, 2026

📝 Walkthrough

Walkthrough

Adds a new Tauri Store driver: implementation, type entries, tests, documentation, and package.json dependency metadata to integrate with @tauri-apps/plugin-store.

Changes

Cohort / File(s) Summary
Documentation
docs/2.drivers/0.index.md, docs/2.drivers/tauri.md
Inserted a Tauri Store driver card in drivers index and added a new driver page detailing installation, usage, options, and watch behavior.
Package manifest
package.json
Added @tauri-apps/plugin-store to devDependencies (^2.4.2) and peerDependencies (^2.0.0) and marked it optional in peerDependenciesMeta.
Driver registration & types
src/_drivers.ts
Extended builtin driver union and options mapping to include tauri, imported TauriOptions, and registered the driver module path.
Driver implementation
src/drivers/tauri.ts
New driver implementing async store load, key normalization, has/get/set/remove, getKeys, clear, and watch (onChange). Exports driver factory and public option/types.
Tests
test/drivers/tauri.test.ts
Added tests that mock @tauri-apps/plugin-store with an in-memory Map and run driver behavior tests with and without a base prefix.

Sequence Diagram

sequenceDiagram
    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)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Poem

🐇 I found a tiny store today,
Keys and values tucked away,
I hop and listen, watch each change,
Debounced saves across the range,
A little driver leads the way.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title 'feat(drivers): add Tauri Store driver and docs' clearly and concisely summarizes the main changes: adding a new Tauri Store driver implementation and its documentation.
Linked Issues check ✅ Passed All coding requirements from issue #748 are met: Tauri storage driver with path and options configuration, all required methods (hasItem, getItem, setItem, removeItem, getKeys, clear, watch), proper export via defineDriver pattern, and comprehensive tests.
Out of Scope Changes check ✅ Passed All changes are directly related to implementing the Tauri Store driver feature: driver implementation, documentation, type definitions, dependency management, and tests. No extraneous modifications detected.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (2)
src/drivers/tauri.ts (1)

33-33: Redundant type cast.

storePromise already has type Promise<TauriStore> from the load() call, making the as 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 in peerDependenciesMeta.

The entry is placed after ofetch, but other entries appear to be alphabetically sorted by package name. @tauri-apps/plugin-store should 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

📥 Commits

Reviewing files that changed from the base of the PR and between 0fd3f38 and 5aa1449.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (6)
  • docs/2.drivers/0.index.md
  • docs/2.drivers/tauri.md
  • package.json
  • src/_drivers.ts
  • src/drivers/tauri.ts
  • test/drivers/tauri.test.ts

hairyf added 2 commits March 6, 2026 11:42
- 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
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between f7f0341 and 7a0ebd6.

📒 Files selected for processing (2)
  • src/drivers/tauri.ts
  • test/drivers/tauri.test.ts

Comment on lines +73 to +78
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);
});
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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) => () => {},
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

fd "tauri.test.ts" --type f

Repository: unjs/unstorage

Length of output: 84


🏁 Script executed:

cat -n test/drivers/tauri.test.ts | head -50

Repository: 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 3

Repository: 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 2

Repository: unjs/unstorage

Length of output: 40


🏁 Script executed:

cat -n src/drivers/tauri.ts

Repository: 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 3

Repository: unjs/unstorage

Length of output: 350


🏁 Script executed:

# Look for watch implementation
rg "watch" src/drivers/tauri.ts -B 2 -A 2

Repository: unjs/unstorage

Length of output: 241


🌐 Web query:

@tauri-apps/plugin-store 2.4.2 onChange API documentation

💡 Result:

@tauri-apps/plugin-storeStore.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 Promise that 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:


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.

Suggested change
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).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support Tauri(Store Plugin)

1 participant