Today I would like to share this snippet of a svelte store that synchronizes with a key in the localStorage, adapted from the fractils (Archived) library.

// Synchronize localStorage with a svelte store.
// Inspired by https://github.com/FractalHQ/fractils
import { writable, get } from 'svelte/store'
import { browser } from '$app/env'

// Help functions to work with the localStorage
async function setLS(key, value) {
    try {
	if (!browser) return
	await Promise.resolve()
	if (value === undefined)
	    localStorage.removeItem(key)
	else
	    localStorage.setItem(key, JSON.stringify(value))
    } catch (err) {
	console.error(`Could not persist ${key} => ${value} to localStorage because of ${err}`)
    }
}

async function getLS(key) {
    try {
	if (!browser) return
	await Promise.resolve()
	const value = localStorage.getItem(key)
	if (value)
	    return JSON.parse(value)
	return undefined
    } catch (err) {
	console.error(`Could not retrieve ${key} from localStorage because of ${err}`)
    }
}

// Serializes the operations on the localStorage
function createOperationChain() {
    let promiseChain = Promise.resolve()
    function addPromise(promise) {
	promiseChain = promiseChain.then(promise)
    }
    return { addPromise }
}

/* A Svelte store that stores data to localStorage
 * @param key - The key to store the data under
 * @param value - The initial value of the store
 * @returns a writable store.
 *
 * If value is undefined then deletes the key.
 */
export default function localStorageStore(key, value) {
    let operationChain = createOperationChain()
    const { set: setStore, update: updateStore, ...readableStore } = writable(value, () => {
	if (!browser) return
	// Launch a synchronization with the localStore
	operationChain.addPromise(syncLS())
    })

    // Sets both the localStorage and this svelte store
    async function set(value) {
	operationChain.addPromise(async () => {
	    setStore(value)
	    await setLS(key, value)
	})
    }
    
    async function update(fn) {
        let newvalue = fn(get(readableStore))
        await set(newvalue)
    }
    
    // Function to synchronize the store current value with localStorage
    // Note: this function is always executed inside the operationChain
    async function syncLS() {
	// Get the value from the localStorage
	let localValue = await getLS(key)
	if (localValue === undefined)
	    // If there is none, set the default value
	    await setLS(key, value)
	else
	    // Otherwise set the current store value
	    setStore(localValue)
    }

    return { ...readableStore, set, update }
}

As you can see, the snippet operates on localStorage only inside a single Promise chain, since otherwise there could be race conditions: according to this SO answer (Archived) await never returns synchronously, and if the Promise is already resolved it still schedules the job for later.

Thus when we do await Promise.resolve() the rest of the job gets effectively enqueued till later. This is not a problem if the actions to store things in localStore are initiated by the user, since they will be separated enough that this is not a problem, however shit would happen when doing multiple modifications in a scripted way, thus the need to wrap every modification to the store inside a promise chain.

You can now initialize the store and use it like any other svelte store:

import localStorageStore from './localStorageStore.js'
const defaultValue = "something"
const store = localStorageStore("store_name", defaultValue)
store.subscribe(newval => console.log("NEW VALUE", newval))
store.set("Change value")
// Setting it to undefined removes the key from localStorage
// Except that when the page is realoaded the key would be recreated from defaultValue (but not if defaultValue is undefined of course)
store.set(undefined)

and be guaranteed that all your updates are safely persisted.