@statx/persist
Advanced tools
| /* eslint-disable @typescript-eslint/no-explicit-any */ | ||
| import './init-storages' | ||
| import {test} from 'uvu' | ||
| import {stateSessionStorage, stateLocalStorage} from '../persist.js' | ||
| import * as assert from 'uvu/assert' | ||
| import {getFromLocal, getKey, testAsyncStorage} from './init-storages.js' | ||
| test(`localStorage value can be set and read`, async () => { | ||
| const name = 'test1' | ||
| const value = 'testValue' | ||
| const state = stateLocalStorage('', {name, throttle: 0}) | ||
| state.set('testValue') | ||
| await 1 | ||
| assert.equal(getFromLocal(name), value) | ||
| }) | ||
| test(`localStorage subscribe`, async () => { | ||
| const name = 'test2' | ||
| const value = 'testValue' | ||
| const state = stateLocalStorage('', {name, throttle: 0}) | ||
| localStorage.setItem(getKey(name), value) | ||
| let test = 0 | ||
| state.subscribe(() => { | ||
| test = 1 | ||
| }) | ||
| state.set(value) | ||
| await 1 | ||
| assert.equal(test, 1) | ||
| }) | ||
| test(`localStorage clear`, async () => { | ||
| let test = 0 | ||
| const name = 'test3' | ||
| const value = 'testValue3' | ||
| const state = stateLocalStorage('', {name, throttle: 0}) | ||
| localStorage.setItem(getKey(name), value) | ||
| state.clear() | ||
| await new Promise((r) => setTimeout(r, 10)) | ||
| state.subscribe(() => { | ||
| test++ | ||
| }) | ||
| state.set(value) | ||
| await 1 | ||
| assert.equal(test, 1, '1') | ||
| state.set(value + '=') | ||
| await new Promise((r) => setTimeout(r, 10)) | ||
| assert.equal(test, 2, '2') | ||
| assert.equal(getFromLocal(name), value + '=', '3') | ||
| }) | ||
| test('Custom class restore', async () => { | ||
| class Test { | ||
| constructor(private value: string) {} | ||
| toJSON() { | ||
| return {value: this.value} | ||
| } | ||
| } | ||
| const state = stateLocalStorage([new Test('1'), new Test('2')], { | ||
| name: 'array', | ||
| throttle: 0, | ||
| restoreFn: (data: {value: string}[]) => { | ||
| return data.map((item) => new Test(item.value)) | ||
| }, | ||
| }) | ||
| assert.instance(state()[0], Test) | ||
| state.set([new Test('1'), new Test('2'), new Test('2')]) | ||
| const state2 = stateLocalStorage([new Test('1')], { | ||
| name: 'array', | ||
| throttle: 0, | ||
| restoreFn: (data: {value: string}[]) => { | ||
| return data.map((item) => new Test(item.value)) | ||
| }, | ||
| }) | ||
| assert.instance(state2()[2], Test) | ||
| }) | ||
| test('Function initialization', async () => { | ||
| let i = 0 | ||
| const initializer = () => { | ||
| i++ | ||
| return 'test' | ||
| } | ||
| const state = stateLocalStorage(initializer, { | ||
| name: 'fn', | ||
| }) | ||
| assert.is(state(), 'test') | ||
| assert.is(i, 1) | ||
| state.set('param') | ||
| assert.is(state(), 'param') | ||
| const state2 = stateLocalStorage(initializer, { | ||
| name: 'fn', | ||
| }) | ||
| assert.is(i, 1) | ||
| assert.is(state2(), 'param') | ||
| }) | ||
| test('Async adapter', async () => { | ||
| const state = testAsyncStorage('qwe', {}) | ||
| }) | ||
| test('sessionStorage value can be set and read', async () => { | ||
| const name = 'test4' | ||
| const value = 'testValue' | ||
| const state = stateSessionStorage('', {name, throttle: 0}) | ||
| state.set('testValue') | ||
| await 1 | ||
| assert.equal(getFromLocal(name, true), value) | ||
| }) | ||
| test.run() |
| /* eslint-disable @typescript-eslint/no-explicit-any */ | ||
| import {PREFIXES} from '../consts' | ||
| import {createPersistState} from '../utils' | ||
| import type {PersistAdapter, PersistOptions} from '../types' | ||
| const createStore = (): Storage => { | ||
| let store = {} as Record<string, string> | ||
| return { | ||
| key() { | ||
| return '' | ||
| }, | ||
| getItem(name: string) { | ||
| return store[name] | ||
| }, | ||
| removeItem(name: string) { | ||
| delete store[name] | ||
| }, | ||
| setItem(name: string, value: string) { | ||
| store[name] = value | ||
| }, | ||
| get length() { | ||
| return Object.keys(store).length | ||
| }, | ||
| clear() { | ||
| store = {} | ||
| }, | ||
| } | ||
| } | ||
| const createAsyncTestAdapter = (): PersistAdapter => { | ||
| const store = {} as Record<string, string> | ||
| return { | ||
| isAsync: true, | ||
| get(name: string) { | ||
| return Promise.resolve(store[name]) | ||
| }, | ||
| async set(name: string, value: unknown) { | ||
| //@ts-ignore | ||
| store[name] = value | ||
| await 1 | ||
| }, | ||
| clear(name: string): void { | ||
| delete store[name] | ||
| }, | ||
| } | ||
| } | ||
| export const getKey = (name: string, isSession = false) => { | ||
| return (isSession ? PREFIXES.sessionStorage : PREFIXES.localStorage) + name | ||
| } | ||
| export const getFromLocal = (name: string, isSession = false) => { | ||
| try { | ||
| const key = getKey(name, isSession) | ||
| const value = isSession ? sessionStorage.getItem(key) : localStorage.getItem(key) | ||
| if (value) { | ||
| return JSON.parse(value).value | ||
| } | ||
| } catch { | ||
| return | ||
| } | ||
| } | ||
| const asyncAdapter = createAsyncTestAdapter() | ||
| export const testAsyncStorage = (value: any, {name, onPersisStateInit, throttle}: PersistOptions<any>) => { | ||
| return createPersistState(value, asyncAdapter, { | ||
| name, | ||
| onPersisStateInit, | ||
| throttle, | ||
| }) | ||
| } | ||
| //@ts-ignore | ||
| globalThis.localStorage = createStore() | ||
| //@ts-ignore | ||
| globalThis.sessionStorage = createStore() |
+87
| /* eslint-disable @typescript-eslint/no-explicit-any */ | ||
| import {isStatxFn, state, type State} from '@statx/core' | ||
| import type {PersistAdapter, PersistOptions, PersistState} from './types' | ||
| import {throttle} from '@statx/utils' | ||
| const getInitialValue = (value: unknown, adapter: PersistAdapter, name: string) => { | ||
| if (isStatxFn(value)) { | ||
| return (value as any)() | ||
| } | ||
| const existValue = adapter.get(name) | ||
| if (typeof value === 'function') { | ||
| if (adapter.isAsync) { | ||
| return value() | ||
| } | ||
| return existValue ?? value() | ||
| } | ||
| return existValue ?? value | ||
| } | ||
| const tryGetRestoredValue = (value: unknown, adapter: PersistAdapter, options: PersistOptions<any>) => { | ||
| const initial = getInitialValue(value, adapter, options.name) | ||
| return options.restoreFn?.(initial) ?? initial | ||
| } | ||
| const applyPersist = async ( | ||
| stateValue: State<any>, | ||
| adapter: PersistAdapter, | ||
| options: PersistOptions<any>, | ||
| ) => { | ||
| try { | ||
| if (adapter.isAsync) { | ||
| const currentValue = adapter.get(options.name) | ||
| const restored = await (currentValue as Promise<unknown>) | ||
| stateValue.set(options.restoreFn?.(restored) ?? restored) | ||
| } | ||
| } finally { | ||
| options.onPersisStateInit?.(stateValue()) | ||
| } | ||
| } | ||
| export const createPersistState = <T>( | ||
| value: unknown, | ||
| adapter: PersistAdapter, | ||
| options: PersistOptions<any>, | ||
| ): PersistState<T> => { | ||
| let stateValue: State<any> | ||
| let initialValue | ||
| const name = options.name | ||
| if (isStatxFn(value)) { | ||
| initialValue = value.peek() | ||
| stateValue = value as any | ||
| } else { | ||
| initialValue = value | ||
| stateValue = state<unknown>(tryGetRestoredValue(value, adapter, options), {name}) | ||
| } | ||
| applyPersist(stateValue, adapter, options) | ||
| const throttleSet = throttle((newValue: unknown) => { | ||
| if (newValue === undefined) { | ||
| adapter.clear(name) | ||
| } else { | ||
| adapter.set(name, newValue) | ||
| } | ||
| }, options.throttle ?? 0) | ||
| const baseSet = stateValue.set.bind(stateValue) | ||
| Object.assign(stateValue, { | ||
| set: (v: unknown) => { | ||
| baseSet(v) | ||
| throttleSet(v) | ||
| }, | ||
| clear: () => { | ||
| baseSet(initialValue) | ||
| adapter.clear(name) | ||
| }, | ||
| }) | ||
| return stateValue as PersistState<T> | ||
| } |
+5
-5
| { | ||
| "name": "@statx/persist", | ||
| "version": "1.12.4", | ||
| "version": "1.13.0", | ||
| "private": false, | ||
@@ -45,3 +45,3 @@ "description": "Extry tiny smart statx manager", | ||
| "fix": "eslint --fix \"**/*.{ts,tsx}\"", | ||
| "test": "tsx src/index.test.ts", | ||
| "test": "uvu -r tsm ./src/tests", | ||
| "test:watch": "tsx watch src/index.test.ts" | ||
@@ -53,4 +53,4 @@ }, | ||
| "dependencies": { | ||
| "@statx/core": "^1.12.4", | ||
| "@statx/utils": "^1.12.3" | ||
| "@statx/core": "^1.13.0", | ||
| "@statx/utils": "^1.13.0" | ||
| }, | ||
@@ -79,3 +79,3 @@ "targets": { | ||
| }, | ||
| "gitHead": "acbc89cd719be69b61bc8cc52f0372fe7ad9e521" | ||
| "gitHead": "77c93b35154bf369b3eacad97f27b7d2a12b1c65" | ||
| } |
@@ -1,4 +0,4 @@ | ||
| import {throttle} from '@statx/utils' | ||
| /* eslint-disable @typescript-eslint/no-explicit-any */ | ||
| import {NOT_ALLOWED_TYPES} from '../consts.js' | ||
| import type {AsyncStorage} from '../types.js' | ||
| import type {PersistAdapter, RestoreFn} from '../types.js' | ||
@@ -39,41 +39,40 @@ const DB_NAME = 'statx-store' | ||
| export const indexedDBAdapter = (name: string, throttleValue = 0): AsyncStorage => { | ||
| return { | ||
| isAsync: true, | ||
| async get() { | ||
| const db = await openDb() | ||
| const transaction = db.transaction(STORE_NAME, 'readonly') | ||
| export class IndexedDBAdapter implements PersistAdapter { | ||
| isAsync = true | ||
| constructor(public restoreFn: RestoreFn<any> | undefined = undefined) {} | ||
| set(name: string, value: unknown) { | ||
| const type = typeof value | ||
| if (NOT_ALLOWED_TYPES.includes(type)) { | ||
| throw new Error('Type ' + type + ' not allowed') | ||
| } | ||
| openDb().then((db) => { | ||
| const transaction = db.transaction(STORE_NAME, 'readwrite') | ||
| const store = transaction.objectStore(STORE_NAME) | ||
| const result = await new Promise<StoreValue>((r, j) => { | ||
| const s = store.get(name) | ||
| s.onsuccess = () => { | ||
| r(s.result as StoreValue) | ||
| } | ||
| s.onerror = j | ||
| }) | ||
| if (result) { | ||
| return JSON.parse(result.value).value | ||
| } | ||
| }, | ||
| set: throttle((value: unknown) => { | ||
| const type = typeof value | ||
| if (NOT_ALLOWED_TYPES.includes(type)) { | ||
| throw new Error('Type ' + type + ' not allowed') | ||
| } | ||
| openDb().then((db) => { | ||
| const transaction = db.transaction(STORE_NAME, 'readwrite') | ||
| const store = transaction.objectStore(STORE_NAME) | ||
| store.put({key: name, value: JSON.stringify({value})}) | ||
| }) | ||
| } | ||
| store.put({key: name, value: JSON.stringify({value})}) | ||
| }) | ||
| }, throttleValue), | ||
| clear(): void { | ||
| openDb().then((db) => { | ||
| const transaction = db.transaction(STORE_NAME, 'readwrite') | ||
| const store = transaction.objectStore(STORE_NAME) | ||
| store.delete(name) | ||
| }) | ||
| }, | ||
| clear(name: string): void { | ||
| openDb().then((db) => { | ||
| const transaction = db.transaction(STORE_NAME, 'readwrite') | ||
| const store = transaction.objectStore(STORE_NAME) | ||
| store.delete(name) | ||
| }) | ||
| } | ||
| async get(name: string) { | ||
| const db = await openDb() | ||
| const transaction = db.transaction(STORE_NAME, 'readonly') | ||
| const store = transaction.objectStore(STORE_NAME) | ||
| const result = await new Promise<StoreValue>((r, j) => { | ||
| const s = store.get(name) | ||
| s.onsuccess = () => { | ||
| r(s.result as StoreValue) | ||
| } | ||
| s.onerror = j | ||
| }) | ||
| if (result) { | ||
| return JSON.parse(result.value).value | ||
| } | ||
| } | ||
| } |
@@ -1,19 +0,32 @@ | ||
| import type {StateType} from '@statx/core' | ||
| import {throttle} from '@statx/utils' | ||
| import {PREFIX, NOT_ALLOWED_TYPES} from '../consts.js' | ||
| import type {SyncStorage} from '../types.js' | ||
| /* eslint-disable @typescript-eslint/no-explicit-any */ | ||
| import {PREFIXES, NOT_ALLOWED_TYPES} from '../consts.js' | ||
| import type {PersistAdapter} from '../types.js' | ||
| export const localStorageAdapter = <T extends StateType>( | ||
| name: string, | ||
| throttleValue = 0, | ||
| isSession = false, | ||
| ): SyncStorage => { | ||
| const storage = isSession ? sessionStorage : localStorage | ||
| return { | ||
| isAsync: false, | ||
| clear() { | ||
| storage.removeItem(PREFIX.localStorage + name) | ||
| }, | ||
| get(): T | undefined { | ||
| const v = storage.getItem(PREFIX.localStorage + name) | ||
| export const createLocalAdapter = (storage: Storage) => { | ||
| const storageName = storage === localStorage ? 'localStorage' : 'sessionStorage' | ||
| const prefix = PREFIXES[storageName] | ||
| return class LocalStorageAdapter implements PersistAdapter { | ||
| isAsync = false | ||
| constructor() {} | ||
| set(name: string, value: unknown) { | ||
| const type = typeof value | ||
| if (NOT_ALLOWED_TYPES.includes(type)) { | ||
| console.error(`[TypeError]: ${type} is not allowed`) | ||
| return | ||
| } | ||
| try { | ||
| storage.setItem( | ||
| prefix + name, | ||
| JSON.stringify({ | ||
| value, | ||
| timestamp: Date.now(), | ||
| version: 1, | ||
| }), | ||
| ) | ||
| } catch (e) { | ||
| console.error(`[Storage set item error]: ${(e as Error).message}`) | ||
| } | ||
| } | ||
| get(name: string): unknown { | ||
| const v = storage.getItem(prefix + name) | ||
| if (!v) return undefined | ||
@@ -23,18 +36,7 @@ const data = JSON.parse(v) | ||
| return data.value | ||
| }, | ||
| set: throttle((value: T) => { | ||
| const type = typeof value | ||
| if (NOT_ALLOWED_TYPES.includes(type)) { | ||
| throw new Error('Type ' + type + ' not allowed') | ||
| } | ||
| storage.setItem( | ||
| PREFIX.localStorage + name, | ||
| JSON.stringify({ | ||
| value, | ||
| timestamp: Date.now(), | ||
| version: 1, | ||
| }), | ||
| ) | ||
| }, throttleValue), | ||
| } | ||
| clear(name: string): void { | ||
| storage.removeItem(prefix + name) | ||
| } | ||
| } | ||
| } |
+3
-2
@@ -1,5 +0,6 @@ | ||
| export const PREFIX = { | ||
| localStorage: 'persist-localstorage-', | ||
| export const PREFIXES = { | ||
| localStorage: 'persist-local-storage-', | ||
| sessionStorage: 'persist-session-storage-', | ||
| } | ||
| export const NOT_ALLOWED_TYPES = ['symbol', 'function'] |
@@ -41,4 +41,5 @@ <!DOCTYPE html> | ||
| <pre id = "log"></pre> | ||
| <div id = "test"></div> | ||
| <script type = "module" src = "./index.ts"></script> | ||
| </body> | ||
| </html> |
+22
-8
| /* eslint-disable @typescript-eslint/no-explicit-any */ | ||
| import {list} from '@statx/core' | ||
| import {stateLocalStorage, stateSessionStorage, indexeddbStorage} from '../index.js' | ||
| import {isStatxFn, state, type State} from '@statx/core' | ||
| import {stateLocalStorage, stateSessionStorage, indexedDBStorage} from '../index.js' | ||
| /* | ||
| const logElement = document.getElementById('log') | ||
@@ -13,6 +13,3 @@ | ||
| element.value = v as any | ||
| logElement?.insertAdjacentText( | ||
| 'beforeend', | ||
| 'Subscription of ' + storage.name + `. Value: ${v}. IsLoading: ` + storage.isLoading + '\n', | ||
| ) | ||
| logElement?.insertAdjacentText('beforeend', 'Subscription of ' + storage.name + `. Value: ${v}.\n`) | ||
| }) | ||
@@ -49,3 +46,3 @@ element.value = storage() | ||
| }) | ||
| const testList2 = indexeddbStorage(list(['test']), { | ||
| const testList2 = indexedDBStorage(list(['test']), { | ||
| name: 'local-list', | ||
@@ -77,1 +74,18 @@ throttle: 500, | ||
| }) | ||
| */ | ||
| class Test { | ||
| id = 'test' | ||
| constructor(private value: string) {} | ||
| toJSON() { | ||
| return {value: this.value} | ||
| } | ||
| } | ||
| const b = stateLocalStorage([new Test('1'), new Test('2')], { | ||
| name: 'array', | ||
| throttle: 0, | ||
| restoreFn: (data: {value: string}[]) => { | ||
| return data.map((item) => new Test(item.value)) | ||
| }, | ||
| }) | ||
| //state.set([new Test('1'), new Test('2'), new Test('55')]) |
+17
-131
| /* eslint-disable @typescript-eslint/no-explicit-any */ | ||
| import type {State, StateType} from '@statx/core' | ||
| import {isState, isAsyncComputed, state} from '@statx/core' | ||
| import {indexedDBAdapter} from './adapters/indexeddb-storage.js' | ||
| import {IndexedDBAdapter} from './adapters/indexeddb-storage.js' | ||
| import {createLocalAdapter} from './adapters/local-storage.js' | ||
| import {localStorageAdapter} from './adapters/local-storage.js' | ||
| import type {PersistOptions} from './types.js' | ||
| import {createPersistState} from './utils.js' | ||
| import type { | ||
| AsyncStorage, | ||
| PersistCreatorOptions, | ||
| PersistOptions, | ||
| SyncPersistState, | ||
| AsyncPersistState, | ||
| SyncStorage, | ||
| } from './types.js' | ||
| const uniqNames = new Set<string>() | ||
| const assertUnuniqNames = (name: string) => { | ||
| if (uniqNames.has(name)) { | ||
| throw new Error(`Name: ${name} must be uniq`) | ||
| } | ||
| const adapters = { | ||
| localStorage: new (createLocalAdapter(localStorage))(), | ||
| sessionStorage: new (createLocalAdapter(sessionStorage))(), | ||
| indexedDB: new IndexedDBAdapter(), | ||
| } | ||
| class Persist { | ||
| value: State<unknown> | ||
| storage: AsyncStorage | SyncStorage | ||
| initialValue: unknown | ||
| constructor(value: unknown, storage: AsyncStorage | SyncStorage, {name}: PersistCreatorOptions<any>) { | ||
| this.storage = storage as any | ||
| if (isState(value) || isAsyncComputed(value)) { | ||
| this.initialValue = (value as any)() | ||
| this.value = value as any | ||
| } else { | ||
| this.initialValue = value | ||
| this.value = state<unknown>(this.initialValue, {name}) | ||
| } | ||
| this.value.subscribe((value) => { | ||
| this.storage.set(value) | ||
| }) | ||
| export const stateLocalStorage = <T extends StateType>(value: T | (() => T), options: PersistOptions<T>) => | ||
| createPersistState<T>(value, adapters.localStorage, options) | ||
| Object.defineProperty(this.value, 'clear', { | ||
| writable: false, | ||
| configurable: false, | ||
| value: () => { | ||
| this.storage.clear() | ||
| this.value.set(this.initialValue) | ||
| uniqNames.delete(name) | ||
| }, | ||
| }) | ||
| } | ||
| } | ||
| class SyncPersist extends Persist { | ||
| constructor(value: unknown, storage: SyncStorage, {name, onInitRestore}: PersistCreatorOptions<any>) { | ||
| super(value, storage, {name, onInitRestore}) | ||
| const storeValue = storage.get() | ||
| if (storeValue) { | ||
| this.value.set(storeValue) | ||
| } | ||
| onInitRestore?.(this.value()) | ||
| } | ||
| } | ||
| class AsyncPersist extends Persist { | ||
| constructor(value: unknown, storage: AsyncStorage, {name, onInitRestore}: PersistCreatorOptions<any>) { | ||
| super(value, storage, {name, onInitRestore}) | ||
| Object.defineProperty(this.value, 'isLoading', { | ||
| writable: false, | ||
| configurable: false, | ||
| value: state(false), | ||
| }) | ||
| this.value.set(value) | ||
| ;(this.storage as AsyncStorage) | ||
| .get() | ||
| .then((restored) => { | ||
| this.value.set(restored ?? value) | ||
| }) | ||
| .finally(() => { | ||
| ;(this.value as any).isLoading.set(false) | ||
| onInitRestore?.(this.value()) | ||
| }) | ||
| } | ||
| } | ||
| export const persistSyncState = < | ||
| T extends StateType, | ||
| O extends PersistCreatorOptions<T> = PersistCreatorOptions<T>, | ||
| >( | ||
| value: T, | ||
| storage: SyncStorage, | ||
| options: O, | ||
| ): SyncPersistState<T> => { | ||
| assertUnuniqNames(options.name) | ||
| uniqNames.add(options.name) | ||
| const res = new SyncPersist(value, storage, options) | ||
| return res.value as any | ||
| } | ||
| export const persistAsyncState = <S extends PersistCreatorOptions<T>, T extends StateType>( | ||
| value: T, | ||
| storage: AsyncStorage, | ||
| options: S, | ||
| ): AsyncPersistState<T> => { | ||
| assertUnuniqNames(options.name) | ||
| uniqNames.add(options.name) | ||
| const res = new AsyncPersist(value, storage, options) | ||
| return res.value as any | ||
| } | ||
| export const stateLocalStorage = <T extends StateType>( | ||
| value: T, | ||
| {name, onInitRestore, throttle}: PersistOptions<T>, | ||
| ) => { | ||
| return persistSyncState(value, localStorageAdapter(name, throttle), { | ||
| name, | ||
| onInitRestore, | ||
| }) | ||
| } | ||
| export const stateSessionStorage = <T extends StateType = StateType>( | ||
| value: T, | ||
| {name, onInitRestore, throttle}: PersistOptions<T>, | ||
| ) => { | ||
| return persistSyncState(value, localStorageAdapter(name, throttle, true), { | ||
| name, | ||
| onInitRestore, | ||
| }) | ||
| } | ||
| value: T | (() => T), | ||
| options: PersistOptions<T>, | ||
| ) => createPersistState<T>(value, adapters.sessionStorage, options) | ||
| export const indexeddbStorage = <T extends StateType | State<StateType> = StateType>( | ||
| value: T, | ||
| {name, onInitRestore, throttle}: PersistOptions<T>, | ||
| ) => { | ||
| return persistAsyncState(value, indexedDBAdapter(name, throttle), { | ||
| name, | ||
| onInitRestore, | ||
| }) | ||
| } | ||
| export const indexedDBStorage = <T extends StateType | State<StateType> = StateType>( | ||
| value: T | (() => T), | ||
| options: PersistOptions<T>, | ||
| ) => createPersistState<T>(value, adapters.indexedDB, options) |
+14
-11
@@ -7,3 +7,12 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ | ||
| } | ||
| export type RestoreFn<T> = (data: any) => T | ||
| export type OnInitRestore<T> = (value: T) => void | ||
| export interface PersistAdapter { | ||
| isAsync: boolean | ||
| get(name: string): unknown | Promise<unknown> | ||
| set(name: string, value: unknown): void | ||
| clear(name: string): void | ||
| } | ||
| interface Storage { | ||
@@ -14,2 +23,3 @@ set(value: unknown): void | ||
| } | ||
| export interface SyncStorage extends Storage { | ||
@@ -19,2 +29,3 @@ get(): unknown | ||
| } | ||
| export interface AsyncStorage extends Storage { | ||
@@ -28,11 +39,7 @@ get(): Promise<unknown> | ||
| throttle?: number | ||
| onInitRestore?: (value: T) => void | ||
| restoreFn?: RestoreFn<T> | ||
| onPersisStateInit?: (value: T) => void | ||
| } | ||
| export type PersistCreatorOptions<T> = { | ||
| name: string | ||
| onInitRestore?: (value: T) => void | ||
| } | ||
| export type SyncPersistState<T> = [T] extends [State<any>] | ||
| export type PersistState<T> = [T] extends [State<any>] | ||
| ? T & Persist | ||
@@ -44,5 +51,1 @@ : [T] extends [PublicList<any>] | ||
| : State<T> & Persist | ||
| export type AsyncPersistState<T> = SyncPersistState<T> & { | ||
| isLoading: State<boolean> | ||
| } |
| import {test} from 'uvu' | ||
| import {stateLocalStorage} from './persist.js' | ||
| import * as assert from 'uvu/assert' | ||
| import {PREFIX} from './consts.js' | ||
| const createStore = () => ({ | ||
| store: {} as Record<string, string>, | ||
| getItem(name: string) { | ||
| return this.store[name] | ||
| }, | ||
| removeItem(name: string) { | ||
| delete this.store[name] | ||
| }, | ||
| setItem(name: string, value: string) { | ||
| this.store[name] = value | ||
| }, | ||
| }) | ||
| const getKey = (name: string) => { | ||
| return PREFIX.localStorage + name | ||
| } | ||
| const getFromLocal = (name: string) => { | ||
| try { | ||
| const key = getKey(name) | ||
| const value = localStorage.getItem(key) | ||
| if (value) { | ||
| return JSON.parse(value).value | ||
| } | ||
| } catch { | ||
| return | ||
| } | ||
| } | ||
| //@ts-ignore | ||
| globalThis.localStorage = createStore() | ||
| test(`localstorage value can be set and read`, async () => { | ||
| const name = 'test1' | ||
| const value = 'testvalue' | ||
| const state = stateLocalStorage('', {name, throttle: 0}) | ||
| state.set('testvalue') | ||
| await 1 | ||
| assert.equal(getFromLocal(name), value) | ||
| }) | ||
| test(`localstorage subscibe`, async () => { | ||
| const name = 'test2' | ||
| const value = 'testvalue' | ||
| const state = stateLocalStorage('', {name, throttle: 0}) | ||
| localStorage.setItem(getKey(name), value) | ||
| let test = 0 | ||
| state.subscribe(() => { | ||
| test = 1 | ||
| }) | ||
| state.set(value) | ||
| await 1 | ||
| assert.equal(test, 1) | ||
| }) | ||
| test(`localstorage clear`, async () => { | ||
| let test = 0 | ||
| const name = 'test3' | ||
| const value = 'testvalue' | ||
| const state = stateLocalStorage('', {name, throttle: 0}) | ||
| localStorage.setItem(getKey(name), value) | ||
| state.clear() | ||
| await new Promise((r) => setTimeout(r, 10)) | ||
| state.subscribe(() => { | ||
| test++ | ||
| }) | ||
| state.set(value) | ||
| await 1 | ||
| assert.equal(test, 1) | ||
| state.set(value + '=') | ||
| await 1 | ||
| assert.equal(test, 2) | ||
| assert.equal(getFromLocal(name), value + '=') | ||
| }) | ||
| test.run() |
45256
7.46%38
5.56%869
10.7%Updated
Updated