Search code examples
sveltesvelte-store

Initializing a custom Svelte store asynchronously


Background
I am attempting to develop a cross-platform desktop app using Svelte and Tauri
When the app starts i need to load a settings.json-file from the filesystem into a custom Svelte store.
It needs to be a custom store because I must validate the data using a custom set-function before writing to it
The store will hold an object.

I am using regular Svelte and not Svelte-kit as SSR is not necessary.

Problems

  1. Tauri does not have any synchronous methods for reading files in their fs-api
  2. Svelte does not seem to have any intuitive way I can find for doing this

Tests

  • Following Svelte's promiseStore example, this works for regular stores but not custom stores as the custom set method cannot be reached
  • Using a recursive timout-function waiting for the file to be read
  • Using a while-loop waiting for the file to be read
  • Attempted to find a way to load the data into a global variable before Svelte initializes

Example
It would be a lot of code if I were to post all the failed attempts, so I will provide a example of what I am attempting to achieve.
Everything in the code works when createStore is not async, except reading the settings-file.

import { writable, get as getStore } from 'svelte/store'; // Svelte store
import _set from 'lodash.set';                            // Creating objects with any key/path
import _merge from 'lodash.merge';                        // Merging objects
import { fs } from '@tauri-apps/api';                     // Accessing local filesystem


async function createStore() {
  // Read settings from the file system
  let settings = {}
  try { settings = JSON.parse(await fs.readTextFile('./settings.json')); }
  catch {}

  // Create the store
  const store = writable(settings);

  // Custom set function
  function set (key, value) {
    if(!key) return;

    // Use lodash to create an object
    const change = _set({}, key, value);

    // Retreive the current store and merge it with the object above
    const currentStore = getStore(store)
    const updated = _merge({}, currentStore, change)

    // Update the store
    store.update(() => updated)
    
    // Save the updated settings back to the filesystem
    fs.writeFile({
      contents: JSON.stringify(updated, null, 2),
      path: './settings.json'}
    )
  }

  // Bundle the custom store
  const customStore = {
    subscribe: store.subscribe,
    set
  }

  return customStore;
}

export default createStore();

Solution

  • When having a custom store which needs to be initialized asynchronously, I do this via an async method on the store which I'd call from the App component, if the store is directly needed
    (note that fs.writeFile() also returns a Promise. If there was an error, this wouldn't be handled yet...)

    App.svelte
    <script>
        import settings from './settings'
        import {onMount} from 'svelte'
        
        let appInitialized
    
        onMount(async () => {
            try {
                await settings.init()           
                appInitialized = true
            }catch(error) {
                console.error(error)
            }
        })
    
    </script>
    
    {#if appInitialized}
        'showing App'
    {:else}
        'initializing App'
    {/if}
    

    alternative component logic when there's just the one store to initialize using an {#await} block

    <script>
        import settings from './settings'
    </script>
    
    {#await settings.init()}
        'initializing store'
    {:then}
        'show App'
    {:catch error}
        'Couldn't initialize - '{error.message}
    {/await}
    

    or one if there were more stores to initialize

    <script>
        import settings from './settings'
        import store2 from './store2'
        import store3 from './store3'
    
        const initStores = [
            settings.init(),
            store2.init(),
            store3.init()
        ]
    </script>
    
    {#await Promise.all(initStores)}
        'initializing stores'
    {:then}
        'showing App'
    {:catch error}
        'Couldn't initialize - '{error.message}
    {/await}
    
    settings.js
    import { writable, get } from 'svelte/store';
    import { fs } from '@tauri-apps/api';  
    
    function createStore() {
    
        let initialValue = {}
        // destructure the store on creation to have 'direct access' to methods
        const {subscribe, update, set} = writable(initialValue);
    
        return {
            subscribe,
    
            async init() {
                const savedSettings = JSON.parse(await fs.readTextFile('./settings.json'))
                set(savedSettings);
            },
    
            changeSetting(key, value) {
                if(!key) return;
    
                const storeValue = get(this)
    
                storeValue[key] = value
    
                update(_ => storeValue)
                
                fs.writeFile({
                    contents: JSON.stringify(storeValue, null, 2),
                    path: './settings.json'
                })
            }
        }
    }
    
    export default createStore();