Skip to content
6 changes: 6 additions & 0 deletions .changes/store-defaults-js.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
store: minor
store-js: minor
---

Allow setting defaults from the JavaScript API
6 changes: 6 additions & 0 deletions .changes/store-load-override-defaults.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
store: minor
store-js: minor
---

Add an new option `overrideDefaults` for creating/loading and reloading the store that overrides the store with the on-disk state, ignoring defaults
81 changes: 49 additions & 32 deletions examples/api/src/views/Store.svelte
Original file line number Diff line number Diff line change
@@ -1,71 +1,85 @@
<script>
import { LazyStore } from "@tauri-apps/plugin-store";
import { onMount } from "svelte";
import { appDataDir, resolve } from '@tauri-apps/api/path'
import { LazyStore } from '@tauri-apps/plugin-store'
import { onMount } from 'svelte'

export let onMessage;
let { onMessage } = $props()

let key;
let value;
let key = $state()
let value = $state()

let store = new LazyStore("cache.json");
let cache = {};
const storeName = 'cache.json'

let store = new LazyStore(storeName)
let path = $state('')
let cache = $state({})

async function refreshEntries() {
try {
const values = await store.entries();
cache = {};
const values = await store.entries()
cache = {}
for (const [key, value] of values) {
cache[key] = value;
cache[key] = value
}
} catch (error) {
onMessage(error);
onMessage(error)
}
}

onMount(async () => {
await refreshEntries();
});
path = await resolve(await appDataDir(), storeName)
await refreshEntries()
})

async function write(key, value) {
try {
if (value) {
await store.set(key, value);
await store.set(key, value)
} else {
await store.delete(key);
await store.delete(key)
}
const v = await store.get(key);
const v = await store.get(key)
if (v === undefined) {
delete cache[key];
cache = cache;
delete cache[key]
cache = cache
} else {
cache[key] = v;
cache[key] = v
}
} catch (error) {
onMessage(error);
onMessage(error)
}
}

async function reset() {
try {
await store.reset();
await store.reset()
} catch (error) {
onMessage(error)
}
await refreshEntries()
}

async function reload() {
try {
await store.reload({ overrideDefaults: true })
} catch (error) {
onMessage(error);
onMessage(error)
}
await refreshEntries();
await refreshEntries()
}

async function close() {
try {
await store.close();
onMessage("Store is now closed, any new operations will error out");
await store.close()
onMessage('Store is now closed, any new operations will error out')
} catch (error) {
onMessage(error);
onMessage(error)
}
}

function reopen() {
store = new LazyStore("cache.json");
onMessage("We made a new `LazyStore` instance, operations will now work");
store = new LazyStore(storeName)
onMessage('We made a new `LazyStore` instance, operations will now work')
}
</script>

Expand All @@ -82,14 +96,17 @@
</div>

<div>
<button class="btn" on:click={() => write(key, value)}>Write</button>
<button class="btn" on:click={() => reset()}>Reset</button>
<button class="btn" on:click={() => close()}>Close</button>
<button class="btn" on:click={() => reopen()}>Re-open</button>
<button class="btn" onclick={() => write(key, value)}>Write</button>
<button class="btn" onclick={() => reset()}>Reset</button>
<button class="btn" onclick={() => reload()}>Reload</button>
<button class="btn" onclick={() => close()}>Close</button>
<button class="btn" onclick={() => reopen()}>Re-open</button>
</div>
<div>Store at <code>{path}</code> on disk</div>
</div>

<div>
<h2>Store Values</h2>
{#each Object.entries(cache) as [k, v]}
<div>{k} = {v}</div>
{/each}
Expand Down
2 changes: 1 addition & 1 deletion plugins/store/api-iife.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

37 changes: 30 additions & 7 deletions plugins/store/guest-js/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ interface ChangePayload<T> {
* Options to create a store
*/
export type StoreOptions = {
/**
* Default value of the store
*/
defaults: { [key: string]: unknown }
/**
* Auto save on modification with debounce duration in milliseconds, it's 100ms by default, pass in `false` to disable it
*/
Expand All @@ -34,6 +38,10 @@ export type StoreOptions = {
* Force create a new store with default values even if it already exists.
*/
createNew?: boolean
/**
* When creating the store, override the store with the on-disk state if it exists, ignoring defaults
*/
overrideDefaults?: boolean
}

/**
Expand Down Expand Up @@ -145,8 +153,8 @@ export class LazyStore implements IStore {
return (await this.store).length()
}

async reload(): Promise<void> {
await (await this.store).reload()
async reload(options?: ReloadOptions): Promise<void> {
await (await this.store).reload(options)
}

async save(): Promise<void> {
Expand Down Expand Up @@ -196,7 +204,7 @@ export class Store extends Resource implements IStore {
static async load(path: string, options?: StoreOptions): Promise<Store> {
const rid = await invoke<number>('plugin:store|load', {
path,
...options
options
})
return new Store(rid)
}
Expand Down Expand Up @@ -280,8 +288,8 @@ export class Store extends Resource implements IStore {
return await invoke('plugin:store|length', { rid: this.rid })
}

async reload(): Promise<void> {
await invoke('plugin:store|reload', { rid: this.rid })
async reload(options?: ReloadOptions): Promise<void> {
await invoke('plugin:store|reload', { rid: this.rid, ...options })
}

async save(): Promise<void> {
Expand Down Expand Up @@ -396,10 +404,15 @@ interface IStore {
*
* This method is useful if the on-disk state was edited by the user and you want to synchronize the changes.
*
* Note: This method does not emit change events.
* Note:
* - This method loads the data and merges it with the current store,
* this behavior will be changed to resetting to default first and then merging with the on-disk state in v3,
* to fully match the store with the on-disk state, set {@linkcode ReloadOptions.ignoreDefaults} to `true`
* - This method does not emit change events.
*
* @returns
*/
reload(): Promise<void>
reload(options?: ReloadOptions): Promise<void>

/**
* Saves the store to disk at the store's `path`.
Expand Down Expand Up @@ -437,3 +450,13 @@ interface IStore {
*/
close(): Promise<void>
}

/**
* Options to {@linkcode IStore.reload} a {@linkcode IStore}
*/
export type ReloadOptions = {
/**
* To fully match the store with the on-disk state, ignoring defaults
*/
ignoreDefaults?: boolean
}
66 changes: 43 additions & 23 deletions plugins/store/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,17 +53,36 @@ enum AutoSave {
Bool(bool),
}

fn builder<R: Runtime>(
app: AppHandle<R>,
store_state: State<'_, StoreState>,
path: PathBuf,
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct LoadStoreOptions {
defaults: Option<HashMap<String, JsonValue>>,
auto_save: Option<AutoSave>,
serialize_fn_name: Option<String>,
deserialize_fn_name: Option<String>,
#[serde(default)]
create_new: bool,
#[serde(default)]
override_defaults: bool,
}

fn builder<R: Runtime>(
app: AppHandle<R>,
store_state: State<'_, StoreState>,
path: PathBuf,
options: Option<LoadStoreOptions>,
) -> Result<StoreBuilder<R>> {
let mut builder = app.store_builder(path);
if let Some(auto_save) = auto_save {

let Some(options) = options else {
return Ok(builder);
};

if let Some(defaults) = options.defaults {
builder = builder.defaults(defaults);
}

if let Some(auto_save) = options.auto_save {
match auto_save {
AutoSave::DebounceDuration(duration) => {
builder = builder.auto_save(Duration::from_millis(duration));
Expand All @@ -75,26 +94,30 @@ fn builder<R: Runtime>(
}
}

if let Some(serialize_fn_name) = serialize_fn_name {
if let Some(serialize_fn_name) = options.serialize_fn_name {
let serialize_fn = store_state
.serialize_fns
.get(&serialize_fn_name)
.ok_or_else(|| crate::Error::SerializeFunctionNotFound(serialize_fn_name))?;
builder = builder.serialize(*serialize_fn);
}

if let Some(deserialize_fn_name) = deserialize_fn_name {
if let Some(deserialize_fn_name) = options.deserialize_fn_name {
let deserialize_fn = store_state
.deserialize_fns
.get(&deserialize_fn_name)
.ok_or_else(|| crate::Error::DeserializeFunctionNotFound(deserialize_fn_name))?;
builder = builder.deserialize(*deserialize_fn);
}

if create_new {
if options.create_new {
builder = builder.create_new();
}

if options.override_defaults {
builder = builder.override_defaults();
}

Ok(builder)
}

Expand All @@ -103,20 +126,9 @@ async fn load<R: Runtime>(
app: AppHandle<R>,
store_state: State<'_, StoreState>,
path: PathBuf,
auto_save: Option<AutoSave>,
serialize_fn_name: Option<String>,
deserialize_fn_name: Option<String>,
create_new: Option<bool>,
options: Option<LoadStoreOptions>,
) -> Result<ResourceId> {
let builder = builder(
app,
store_state,
path,
auto_save,
serialize_fn_name,
deserialize_fn_name,
create_new.unwrap_or_default(),
)?;
let builder = builder(app, store_state, path, options)?;
let (_, rid) = builder.build_inner()?;
Ok(rid)
}
Expand Down Expand Up @@ -209,9 +221,17 @@ async fn length<R: Runtime>(app: AppHandle<R>, rid: ResourceId) -> Result<usize>
}

#[tauri::command]
async fn reload<R: Runtime>(app: AppHandle<R>, rid: ResourceId) -> Result<()> {
async fn reload<R: Runtime>(
app: AppHandle<R>,
rid: ResourceId,
ignore_defaults: Option<bool>,
) -> Result<()> {
let store = app.resources_table().get::<Store<R>>(rid)?;
store.reload()
if ignore_defaults.unwrap_or_default() {
store.reload_ignore_defaults()
} else {
store.reload()
}
}

#[tauri::command]
Expand Down
Loading
Loading