diff --git a/README.md b/README.md index c3ff6b4..1ea7459 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,6 @@ If you're developing an application which requires changes to welshman, you'll n - Clone welshman and the repository that depends on it - Within each `package` directory in welshman, run `npm link` -- Within your application directory, link all welshman dependencies _simultaneously_ (or else only one will get linked. A command that does this is: `rm -rf node_modules; npm i; cat package.json|js '.depedencies|keys[]'|grep welshman|xargs npm link`. +- Within your application directory, link all welshman dependencies _simultaneously_ (or else only one will get linked. A command that does this is: `rm -rf node_modules; npm i; cat package.json|js '.dependencies|keys[]'|grep welshman|xargs npm link`. If you run `npm install` in your application directory, you'll need to repeat the final step above. Finally, if you're using the `editor` module, you may run into some dependency version conflicts. I recommend editing the command above to exclude the editor. diff --git a/docs/store/synced.md b/docs/store/synced.md index f726af1..72b8f71 100644 --- a/docs/store/synced.md +++ b/docs/store/synced.md @@ -1,34 +1,49 @@ # Synced Store -Utility for creating Svelte stores that automatically persist to and restore from localStorage. +Utility for creating Svelte stores that automatically persist to and restore from storage providers. ## Functions -### synced(key, defaultValue) +### synced(config) -Creates a writable store that synchronizes with localStorage using JSON serialization. +Creates a writable store that synchronizes with a storage provider using JSON serialization. **Parameters:** -- `key` - localStorage key to store the value under -- `defaultValue` - Default value if nothing exists in localStorage +- `config` - Configuration object containing: + - `key` - Storage key to store the value under + - `storage` - Storage provider implementing the StorageProvider interface + - `defaultValue` - Default value if nothing exists in storage -**Returns:** Writable Svelte store that persists changes to localStorage +**Returns:** Writable Svelte store that persists changes to storage The store automatically: -- Loads initial value from localStorage on creation -- Saves any changes back to localStorage -- Falls back to defaultValue if localStorage is empty or invalid +- Loads initial value from storage on creation +- Saves any changes back to storage +- Falls back to defaultValue if storage is empty or invalid + +## Storage Provider Interface + +```typescript +interface StorageProvider { + get: (key: string) => Promise + set: (key: string, value: any) => Promise +} +``` ## Example ```typescript -import {synced} from "@welshman/store" +import {synced, localStorageProvider} from "@welshman/store" -// Create a store that persists user preferences -const userPreferences = synced("user-prefs", { - theme: "dark", - notifications: true, - language: "en" +// Create a store that persists user preferences using localStorage +const userPreferences = synced({ + key: "user-prefs", + storage: localStorageProvider, + defaultValue: { + theme: "dark", + notifications: true, + language: "en" + } }) // Use like any writable store @@ -36,7 +51,7 @@ userPreferences.subscribe(prefs => { console.log("Preferences:", prefs) }) -// Update the store - automatically saves to localStorage +// Update the store - automatically saves to storage userPreferences.update(prefs => ({ ...prefs, theme: "light" diff --git a/packages/app/src/session.ts b/packages/app/src/session.ts index 17c7a87..991d31d 100644 --- a/packages/app/src/session.ts +++ b/packages/app/src/session.ts @@ -1,6 +1,6 @@ -import {derived} from "svelte/store" +import {derived, writable} from "svelte/store" import {cached, omit, equals, assoc} from "@welshman/lib" -import {withGetter, synced} from "@welshman/store" +import {withGetter} from "@welshman/store" import { Nip46Broker, Nip46Signer, @@ -59,9 +59,9 @@ export type SessionAnyMethod = export type Session = SessionAnyMethod & Record -export const pubkey = withGetter(synced("pubkey", undefined)) +export const pubkey = withGetter(writable(undefined)) -export const sessions = withGetter(synced>("sessions", {})) +export const sessions = withGetter(writable>({})) export const session = withGetter( derived([pubkey, sessions], ([$pubkey, $sessions]) => ($pubkey ? $sessions[$pubkey] : undefined)), diff --git a/packages/store/__tests__/index.test.ts b/packages/store/__tests__/index.test.ts index b078d66..ec3a774 100644 --- a/packages/store/__tests__/index.test.ts +++ b/packages/store/__tests__/index.test.ts @@ -9,6 +9,7 @@ import { deriveIsDeleted, getter, synced, + localStorageProvider, throttled, withGetter, } from "../src/index" @@ -40,24 +41,52 @@ describe("Store utilities", () => { }) describe("synced", () => { - it("should sync with localStorage", () => { - const store = synced("testKey", "default") + it("should sync with localStorage", async () => { + const store = synced({ + key: "testKey", + storage: localStorageProvider, + defaultValue: "default", + }) + + // Wait for async initialization using vi.runAllTimersAsync + await vi.runAllTimersAsync() + expect(get(store)).toBe("default") store.set("new value") + + // Wait for async save using vi.runAllTimersAsync + await vi.runAllTimersAsync() + expect(localStorage.getItem("testKey")).toBe(JSON.stringify("new value")) }) - it("should load existing value from localStorage", () => { + it("should load existing value from localStorage", async () => { localStorage.setItem("testKey", JSON.stringify("existing")) - const store = synced("testKey", "default") + const store = synced({ + key: "testKey", + storage: localStorageProvider, + defaultValue: "default", + }) + + // Wait for async initialization using vi.runAllTimersAsync + await vi.runAllTimersAsync() + expect(get(store)).toBe("existing") }) }) describe("getter", () => { - it("should return current store value", () => { - const store = synced("test", "initial") + it("should return current store value", async () => { + const store = synced({ + key: "test", + storage: localStorageProvider, + defaultValue: "initial", + }) + + // Wait for async initialization using vi.runAllTimersAsync + await vi.runAllTimersAsync() + const getValue = getter(store) expect(getValue()).toBe("initial") @@ -67,8 +96,17 @@ describe("Store utilities", () => { }) describe("withGetter", () => { - it("should add getter to writable store", () => { - const store = withGetter(synced("test", "initial")) + it("should add getter to writable store", async () => { + const store = withGetter( + synced({ + key: "test", + storage: localStorageProvider, + defaultValue: "initial", + }), + ) + + // Wait for async initialization using vi.runAllTimersAsync + await vi.runAllTimersAsync() expect(store.get()).toBe("initial") store.set("updated") @@ -77,9 +115,17 @@ describe("Store utilities", () => { }) describe("throttled", () => { - it("should throttle updates", () => { + it("should throttle updates", async () => { const mockFn = vi.fn() - const store = synced("test", 0) + const store = synced({ + key: "test", + storage: localStorageProvider, + defaultValue: 0, + }) + + // Wait for async initialization using vi.runAllTimersAsync + await vi.runAllTimersAsync() + const throttledStore = throttled(100, store) throttledStore.subscribe(mockFn) diff --git a/packages/store/src/synced.ts b/packages/store/src/synced.ts index d411ec1..e550623 100644 --- a/packages/store/src/synced.ts +++ b/packages/store/src/synced.ts @@ -1,11 +1,37 @@ import {writable} from "svelte/store" import {getJson, setJson} from "@welshman/lib" -export const synced = (key: string, defaultValue: T) => { - const init = getJson(key) - const store = writable(init === undefined ? defaultValue : init) +export interface StorageProvider { + get: (key: string) => Promise + set: (key: string, value: any) => Promise +} - store.subscribe((value: T) => setJson(key, value)) +export interface SyncedConfig { + key: string + storage: StorageProvider + defaultValue: T +} + +export const localStorageProvider: StorageProvider = { + get: async (key: string) => getJson(key), + set: async (key: string, value: any) => setJson(key, value), +} + +export const synced = (config: SyncedConfig) => { + const {key, storage, defaultValue} = config + const store = writable(defaultValue) + + // Async initialization + storage.get(key).then((value: any) => { + if (value !== undefined) { + store.set(value) + } + }) + + // Subscribe to changes + store.subscribe(async (value: T) => { + await storage.set(key, value) + }) return store }