Big News: Socket raises $60M Series C at a $1B valuation to secure software supply chains for AI-driven development.Announcement
Sign In

@statx/persist

Package Overview
Dependencies
Maintainers
1
Versions
63
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@statx/persist - npm Package Compare versions

Comparing version
1.12.4
to
1.13.0
+130
src/tests/index.test.ts
/* 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()
/* 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)
}
}
}

@@ -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>
/* 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')])
/* 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)

@@ -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()