tauri-plugin-store icon indicating copy to clipboard operation
tauri-plugin-store copied to clipboard

[Question] Why not keep the store cache in the WebView process?

Open Narcha opened this issue 1 year ago • 2 comments

I'm currently developing a program that uses this plugin. Currently, if I need a value from the store for the user interface, like for example the user's preferred theme, I need to call an asynchronous function that returns a promise which resolves to the requested value.

This leads to a lot of unnecessary boilerplate code, because I can't "just use" the store values, but have to add a wrapper to each part of my code that uses a store value to abstract away the async nature of the store. This leads to ugly, unnecessary code and lots of frustration.

The contents of the store file are currently cached by the plugin in the main rust process. Having the content of the store cached in the WebView process would solve this: Other parts of the user interface code can immediately use the values without needing "async wrappers".

I propose a different approach, which looks like this:

  • Expose a single async function in the WebView, which loads the store file initially
  • All other get() and set() calls can be synchronous.
  • Cache the store in the WebView
  • Register event handlers to update the cache or the file on disk if either changes

Benefits:

  • Avoid async boilerplate
  • Users can define a scheme of their store in Typescript, get and set functions would thus be automatically typed without needing casts or assertions in user code
  • Less IPC calls

Performance should not be affected by this, as the store is usually called infrequently and IPC calls are reduced.

Also, take a look at the electron-store library, which I have used in the past and does exactly what I want.

Example code to showcase the API I have in mind, and some elegant typescript typing which is possible with this approach: playground

// Incomplete example
class Store<Schema>{
  cache: Partial<Schema>;
  defaults: Required<Schema>;

  constructor(store: string, defaults: Required<Schema>) {
    this.cache = {};
    this.defaults = defaults;
    // TODO: add more options to this constructor
  }

  async init() {
    // Load the store file
    // optional: register callbacks to update the cache if the file changed, using a file watcher
  }

  get<K extends keyof Schema>(key: K): Schema[K] {
    return this.cache[key] ?? this.defaults[key];
  }

  set<K extends keyof Schema>(key: K, value: Schema[K]): void {
    this.cache[key] = value;
    // call IPC to update the file
  }
}

/* --- Example user code --- */

type MyStoreSchema = {
  theme: "dark" | "light",
  foo: number,
}

const store = new Store<MyStoreSchema>("config.json", { theme: "dark", foo: 0 });

await store.init();

const theme = store.get("theme")
const foo = store.get("foo");

// `theme` is of type "dark" | "light", `foo` is a number - no casts or type assertions needed!

if (theme === "light") {
  store.set("theme", "dark");
}

Narcha avatar Sep 14 '22 19:09 Narcha

The electron-store library uses this package to manage lots of the front-end needs of a store library, it could be used/forked for this.

Narcha avatar Sep 14 '22 19:09 Narcha

Forgot to mention that I would be happy to help build a solution to this :)

Narcha avatar Sep 14 '22 20:09 Narcha

I did a test port of an electron app I use in production, and this was the biggest sticking point that made Tauri frustrating to use. Electron-store's approach was superior for two reasons.

  1. Easier function calls with less boilerplate
  2. Ability to use JSON schemas defined in Typescript

I'd love to see an implementation like this for Tauri

ellsong avatar Nov 03 '22 01:11 ellsong

I completely agree. The JSON schema would be a very welcome addition as well.

For now, I settled on using a store-like wrapper around LocalStorage to store used configuration - but I would prefer to use this plugin again once it becomes usable.

Narcha avatar Nov 03 '22 06:11 Narcha