diff --git a/docs/2.drivers/0.index.md b/docs/2.drivers/0.index.md index e07eead0..37928742 100644 --- a/docs/2.drivers/0.index.md +++ b/docs/2.drivers/0.index.md @@ -144,6 +144,15 @@ icon: icon-park-outline:hard-disk :: ::card --- + icon: simple-icons:tauri + to: /drivers/tauri + title: Tauri Store + color: gray + --- + Store data via Tauri Store Plugin in Tauri apps. + :: + ::card + --- icon: gg:vercel to: /drivers/vercel title: Vercel KV diff --git a/docs/2.drivers/tauri.md b/docs/2.drivers/tauri.md new file mode 100644 index 00000000..79e29d61 --- /dev/null +++ b/docs/2.drivers/tauri.md @@ -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. diff --git a/package.json b/package.json index 7a7410cd..2ec7c9e0 100644 --- a/package.json +++ b/package.json @@ -47,6 +47,7 @@ "@libsql/client": "^0.17.0", "@netlify/blobs": "^10.7.0", "@planetscale/database": "^1.19.0", + "@tauri-apps/plugin-store": "^2.4.2", "@types/deno": "^2.5.0", "@types/ioredis-mock": "^8.2.6", "@types/jsdom": "^28.0.0", @@ -100,6 +101,7 @@ "@deno/kv": ">=0.13.0", "@netlify/blobs": "^6.5.0 || ^7.0.0 || ^8.1.0 || ^9.0.0 || ^10.0.0", "@planetscale/database": "^1.19.0", + "@tauri-apps/plugin-store": "^2.0.0", "@upstash/redis": "^1.36.2", "@vercel/blob": ">=0.27.3", "@vercel/functions": "^2.2.12 || ^3.0.0", @@ -145,6 +147,9 @@ "@planetscale/database": { "optional": true }, + "@tauri-apps/plugin-store": { + "optional": true + }, "@upstash/redis": { "optional": true }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 806b4a56..372ea906 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -47,6 +47,9 @@ importers: '@planetscale/database': specifier: ^1.19.0 version: 1.19.0 + '@tauri-apps/plugin-store': + specifier: ^2.4.2 + version: 2.4.2 '@types/deno': specifier: ^2.5.0 version: 2.5.0 @@ -1630,6 +1633,12 @@ packages: '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + '@tauri-apps/api@2.10.1': + resolution: {integrity: sha512-hKL/jWf293UDSUN09rR69hrToyIXBb8CjGaWC7gfinvnQrBVvnLr08FeFi38gxtugAVyVcTa5/FD/Xnkb1siBw==} + + '@tauri-apps/plugin-store@2.4.2': + resolution: {integrity: sha512-0ClHS50Oq9HEvLPhNzTNFxbWVOqoAp3dRvtewQBeqfIQ0z5m3JRnOISIn2ZVPCrQC0MyGyhTS9DWhHjpigQE7A==} + '@tybys/wasm-util@0.10.1': resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} @@ -6001,6 +6010,12 @@ snapshots: '@standard-schema/spec@1.1.0': {} + '@tauri-apps/api@2.10.1': {} + + '@tauri-apps/plugin-store@2.4.2': + dependencies: + '@tauri-apps/api': 2.10.1 + '@tybys/wasm-util@0.10.1': dependencies: tslib: 2.8.1 diff --git a/src/_drivers.ts b/src/_drivers.ts index c42493aa..c31c48a2 100644 --- a/src/_drivers.ts +++ b/src/_drivers.ts @@ -28,12 +28,13 @@ import type { PlanetscaleDriverOptions as PlanetscaleOptions } from "unstorage/d import type { RedisOptions } from "unstorage/drivers/redis"; import type { S3DriverOptions as S3Options } from "unstorage/drivers/s3"; import type { SessionStorageOptions } from "unstorage/drivers/session-storage"; +import type { TauriStorageDriverOptions as TauriOptions } from "unstorage/drivers/tauri"; import type { UploadThingOptions as UploadthingOptions } from "unstorage/drivers/uploadthing"; import type { UpstashOptions } from "unstorage/drivers/upstash"; import type { VercelBlobOptions } from "unstorage/drivers/vercel-blob"; import type { VercelCacheOptions as VercelRuntimeCacheOptions } from "unstorage/drivers/vercel-runtime-cache"; -export type BuiltinDriverName = "azure-app-configuration" | "azureAppConfiguration" | "azure-cosmos" | "azureCosmos" | "azure-key-vault" | "azureKeyVault" | "azure-storage-blob" | "azureStorageBlob" | "azure-storage-table" | "azureStorageTable" | "capacitor-preferences" | "capacitorPreferences" | "cloudflare-cache-binding" | "cloudflareCacheBinding" | "cloudflare-kv-binding" | "cloudflareKVBinding" | "cloudflare-kv-http" | "cloudflareKVHttp" | "cloudflare-r2-binding" | "cloudflareR2Binding" | "db0" | "deno-kv-node" | "denoKVNode" | "deno-kv" | "denoKV" | "fs-lite" | "fsLite" | "fs" | "github" | "http" | "indexedb" | "localstorage" | "lru-cache" | "lruCache" | "memory" | "mongodb" | "netlify-blobs" | "netlifyBlobs" | "null" | "overlay" | "planetscale" | "redis" | "s3" | "session-storage" | "sessionStorage" | "uploadthing" | "upstash" | "vercel-blob" | "vercelBlob" | "vercel-runtime-cache" | "vercelRuntimeCache"; +export type BuiltinDriverName = "azure-app-configuration" | "azureAppConfiguration" | "azure-cosmos" | "azureCosmos" | "azure-key-vault" | "azureKeyVault" | "azure-storage-blob" | "azureStorageBlob" | "azure-storage-table" | "azureStorageTable" | "capacitor-preferences" | "capacitorPreferences" | "cloudflare-cache-binding" | "cloudflareCacheBinding" | "cloudflare-kv-binding" | "cloudflareKVBinding" | "cloudflare-kv-http" | "cloudflareKVHttp" | "cloudflare-r2-binding" | "cloudflareR2Binding" | "db0" | "deno-kv-node" | "denoKVNode" | "deno-kv" | "denoKV" | "fs-lite" | "fsLite" | "fs" | "github" | "http" | "indexedb" | "localstorage" | "lru-cache" | "lruCache" | "memory" | "mongodb" | "netlify-blobs" | "netlifyBlobs" | "null" | "overlay" | "planetscale" | "redis" | "s3" | "session-storage" | "sessionStorage" | "tauri" | "uploadthing" | "upstash" | "vercel-blob" | "vercelBlob" | "vercel-runtime-cache" | "vercelRuntimeCache"; export type BuiltinDriverOptions = { "azure-app-configuration": AzureAppConfigurationOptions; @@ -79,6 +80,7 @@ export type BuiltinDriverOptions = { "s3": S3Options; "session-storage": SessionStorageOptions; "sessionStorage": SessionStorageOptions; + "tauri": TauriOptions; "uploadthing": UploadthingOptions; "upstash": UpstashOptions; "vercel-blob": VercelBlobOptions; @@ -133,6 +135,7 @@ export const builtinDrivers = { "s3": "unstorage/drivers/s3", "session-storage": "unstorage/drivers/session-storage", "sessionStorage": "unstorage/drivers/session-storage", + "tauri": "unstorage/drivers/tauri", "uploadthing": "unstorage/drivers/uploadthing", "upstash": "unstorage/drivers/upstash", "vercel-blob": "unstorage/drivers/vercel-blob", diff --git a/src/drivers/tauri.ts b/src/drivers/tauri.ts new file mode 100644 index 00000000..587bebec --- /dev/null +++ b/src/drivers/tauri.ts @@ -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>; + +const driver: DriverFactory> = (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); + }); + }, + }; +}; + +export default driver; diff --git a/test/drivers/tauri.test.ts b/test/drivers/tauri.test.ts new file mode 100644 index 00000000..a44efec9 --- /dev/null +++ b/test/drivers/tauri.test.ts @@ -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(); + 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) => () => {}, + }); + }), + }; +}); + +describe("drivers: tauri", () => { + afterEach(() => { + vi.resetAllMocks(); + }); + + testDriver({ + driver: driver({ path: "store.json" }), + }); + + testDriver({ + driver: driver({ path: "store.json", base: "app" }), + }); +});