Removal of localStorage dependency v1
This commit is contained in:
@@ -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
@@ -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"
|
||||||
|
|||||||
@@ -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)),
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user