Removal of localStorage dependency v1

This commit is contained in:
viniersiren
2025-07-09 11:55:42 -07:00
parent 41fe5f4cb0
commit bf587d58c7
5 changed files with 122 additions and 35 deletions
+1 -1
View File
@@ -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 - Clone welshman and the repository that depends on it
- Within each `package` directory in welshman, run `npm link` - 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. 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.
+31 -16
View File
@@ -1,34 +1,49 @@
# Synced Store # 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 ## 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:** **Parameters:**
- `key` - localStorage key to store the value under - `config` - Configuration object containing:
- `defaultValue` - Default value if nothing exists in localStorage - `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: The store automatically:
- Loads initial value from localStorage on creation - Loads initial value from storage on creation
- Saves any changes back to localStorage - Saves any changes back to storage
- Falls back to defaultValue if localStorage is empty or invalid - Falls back to defaultValue if storage is empty or invalid
## Storage Provider Interface
```typescript
interface StorageProvider {
get: (key: string) => Promise<any>
set: (key: string, value: any) => Promise<void>
}
```
## Example ## Example
```typescript ```typescript
import {synced} from "@welshman/store" import {synced, localStorageProvider} from "@welshman/store"
// Create a store that persists user preferences // Create a store that persists user preferences using localStorage
const userPreferences = synced("user-prefs", { const userPreferences = synced({
theme: "dark", key: "user-prefs",
notifications: true, storage: localStorageProvider,
language: "en" defaultValue: {
theme: "dark",
notifications: true,
language: "en"
}
}) })
// Use like any writable store // Use like any writable store
@@ -36,7 +51,7 @@ userPreferences.subscribe(prefs => {
console.log("Preferences:", prefs) console.log("Preferences:", prefs)
}) })
// Update the store - automatically saves to localStorage // Update the store - automatically saves to storage
userPreferences.update(prefs => ({ userPreferences.update(prefs => ({
...prefs, ...prefs,
theme: "light" theme: "light"
+4 -4
View File
@@ -1,6 +1,6 @@
import {derived} from "svelte/store" import {derived, writable} from "svelte/store"
import {cached, omit, equals, assoc} from "@welshman/lib" import {cached, omit, equals, assoc} from "@welshman/lib"
import {withGetter, synced} from "@welshman/store" import {withGetter} from "@welshman/store"
import { import {
Nip46Broker, Nip46Broker,
Nip46Signer, Nip46Signer,
@@ -59,9 +59,9 @@ export type SessionAnyMethod =
export type Session = SessionAnyMethod & Record<string, any> export type Session = SessionAnyMethod & Record<string, any>
export const pubkey = withGetter(synced<string | undefined>("pubkey", undefined)) export const pubkey = withGetter(writable<string | undefined>(undefined))
export const sessions = withGetter(synced<Record<string, Session>>("sessions", {})) export const sessions = withGetter(writable<Record<string, Session>>({}))
export const session = withGetter( export const session = withGetter(
derived([pubkey, sessions], ([$pubkey, $sessions]) => ($pubkey ? $sessions[$pubkey] : undefined)), derived([pubkey, sessions], ([$pubkey, $sessions]) => ($pubkey ? $sessions[$pubkey] : undefined)),
+56 -10
View File
@@ -9,6 +9,7 @@ import {
deriveIsDeleted, deriveIsDeleted,
getter, getter,
synced, synced,
localStorageProvider,
throttled, throttled,
withGetter, withGetter,
} from "../src/index" } from "../src/index"
@@ -40,24 +41,52 @@ describe("Store utilities", () => {
}) })
describe("synced", () => { describe("synced", () => {
it("should sync with localStorage", () => { it("should sync with localStorage", async () => {
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("default") expect(get(store)).toBe("default")
store.set("new value") store.set("new value")
// Wait for async save using vi.runAllTimersAsync
await vi.runAllTimersAsync()
expect(localStorage.getItem("testKey")).toBe(JSON.stringify("new value")) 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")) 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") expect(get(store)).toBe("existing")
}) })
}) })
describe("getter", () => { describe("getter", () => {
it("should return current store value", () => { it("should return current store value", async () => {
const store = synced("test", "initial") const store = synced({
key: "test",
storage: localStorageProvider,
defaultValue: "initial",
})
// Wait for async initialization using vi.runAllTimersAsync
await vi.runAllTimersAsync()
const getValue = getter(store) const getValue = getter(store)
expect(getValue()).toBe("initial") expect(getValue()).toBe("initial")
@@ -67,8 +96,17 @@ describe("Store utilities", () => {
}) })
describe("withGetter", () => { describe("withGetter", () => {
it("should add getter to writable store", () => { it("should add getter to writable store", async () => {
const store = withGetter(synced("test", "initial")) 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") expect(store.get()).toBe("initial")
store.set("updated") store.set("updated")
@@ -77,9 +115,17 @@ describe("Store utilities", () => {
}) })
describe("throttled", () => { describe("throttled", () => {
it("should throttle updates", () => { it("should throttle updates", async () => {
const mockFn = vi.fn() 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) const throttledStore = throttled(100, store)
throttledStore.subscribe(mockFn) throttledStore.subscribe(mockFn)
+30 -4
View File
@@ -1,11 +1,37 @@
import {writable} from "svelte/store" import {writable} from "svelte/store"
import {getJson, setJson} from "@welshman/lib" import {getJson, setJson} from "@welshman/lib"
export const synced = <T>(key: string, defaultValue: T) => { export interface StorageProvider {
const init = getJson(key) get: (key: string) => Promise<any>
const store = writable<T>(init === undefined ? defaultValue : init) set: (key: string, value: any) => Promise<void>
}
store.subscribe((value: T) => setJson(key, value)) export interface SyncedConfig {
key: string
storage: StorageProvider
defaultValue: any
}
export const localStorageProvider: StorageProvider = {
get: async (key: string) => getJson(key),
set: async (key: string, value: any) => setJson(key, value),
}
export const synced = <T>(config: SyncedConfig) => {
const {key, storage, defaultValue} = config
const store = writable<T>(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 return store
} }