diff --git a/.size-limit.js b/.size-limit.js
index ea84935..d5e16ee 100644
--- a/.size-limit.js
+++ b/.size-limit.js
@@ -11,7 +11,7 @@ module.exports = [
{
name: 'root persist, cjs module',
path: 'build/index.cjs',
- limit: '3368 B',
+ limit: '3979 B',
// import: '{ persist }', // tree-shaking is not working with cjs
ignore: ['effector'],
gzip: true,
@@ -21,7 +21,7 @@ module.exports = [
{
name: 'core persist, es module',
path: 'build/core/index.js',
- limit: '1095 B',
+ limit: '1096 B',
import: '{ persist }',
ignore: ['effector'],
gzip: true,
@@ -29,7 +29,7 @@ module.exports = [
{
name: 'core persist, cjs module',
path: 'build/core/index.cjs',
- limit: '1281 B',
+ limit: '1738 B',
// import: '{ persist }', // tree-shaking is not working with cjs
ignore: ['effector'],
gzip: true,
@@ -111,7 +111,7 @@ module.exports = [
{
name: '`localStorage` persist, es module',
path: 'build/local/index.js',
- limit: '1572 B',
+ limit: '1569 B',
import: '{ persist }',
ignore: ['effector'],
gzip: true,
@@ -119,7 +119,7 @@ module.exports = [
{
name: '`localStorage` persist, cjs module',
path: 'build/local/index.cjs',
- limit: '1854 B',
+ limit: '2330 B',
// import: '{ persist }', // tree-shaking is not working with cjs
ignore: ['effector'],
gzip: true,
@@ -130,7 +130,7 @@ module.exports = [
{
name: 'core adapter, es module',
path: 'build/index.js',
- limit: '1553 B',
+ limit: '1550 B',
import: '{ persist, local }',
ignore: ['effector'],
gzip: true,
@@ -138,7 +138,7 @@ module.exports = [
{
name: 'core adapter factory, es module',
path: ['build/index.js', 'build/local/index.js'],
- limit: '1555 B',
+ limit: '1553 B',
import: {
'build/index.js': '{ persist }',
'build/local/index.js': '{ local }',
@@ -151,7 +151,7 @@ module.exports = [
{
name: '`sessionStorage` persist, es module',
path: 'build/session/index.js',
- limit: '1569 B',
+ limit: '1566 B',
import: '{ persist }',
ignore: ['effector'],
gzip: true,
@@ -159,7 +159,7 @@ module.exports = [
{
name: '`sessionStorage` persist, cjs module',
path: 'build/session/index.cjs',
- limit: '1849 B',
+ limit: '2324 B',
// import: '{ persist }', // tree-shaking is not working with cjs
ignore: ['effector'],
gzip: true,
@@ -169,7 +169,7 @@ module.exports = [
{
name: 'query string persist, es module',
path: 'build/query/index.js',
- limit: '1626 B',
+ limit: '1622 B',
import: '{ persist }',
ignore: ['effector'],
gzip: true,
@@ -177,7 +177,7 @@ module.exports = [
{
name: 'query string persist, cjs module',
path: 'build/query/index.cjs',
- limit: '1921 B',
+ limit: '2393 B',
// import: '{ persist }', // tree-shaking is not working with cjs
ignore: ['effector'],
gzip: true,
@@ -187,7 +187,7 @@ module.exports = [
{
name: 'memory adapter, es module',
path: 'build/memory/index.js',
- limit: '1191 B',
+ limit: '1207 B',
import: '{ persist }',
ignore: ['effector'],
gzip: true,
@@ -195,7 +195,7 @@ module.exports = [
{
name: 'memory adapter, cjs module',
path: 'build/memory/index.cjs',
- limit: '1423 B',
+ limit: '1932 B',
// import: '{ persist }', // tree-shaking is not working with cjs
ignore: ['effector'],
gzip: true,
@@ -231,7 +231,7 @@ module.exports = [
{
name: 'broadcast channel adapter, cjs module',
path: 'build/broadcast/index.cjs',
- limit: '1691 B',
+ limit: '2169 B',
// import: '{ broadcast }', // tree-shaking is not working with cjs
ignore: ['effector'],
gzip: true,
diff --git a/README.md b/README.md
index 4aecca1..7d6eb0d 100644
--- a/README.md
+++ b/README.md
@@ -174,7 +174,7 @@ In order to synchronize _something_, you need to specify effector units. Dependi
- `keyPrefix` ([_string_]): Prefix, used in adapter, to be concatenated to `key`. By default = `''`.
- `operation` (_`'set'`_ | _`'get'`_ | _`'validate'`_): Type of operation, read (get), write (set) or validation against contract (validate).
- `error` ([_Error_]): Error instance
- - `value`? (_any_): In case of _'set'_ operation — value from `store`. In case of _'get'_ operation could contain raw value from storage or could be empty.
+ - `value`? (_any_): In case of _'set'_ operation — value from `store`. In case of _'get'_ and _'validate'_ operations could contain raw value from storage or could be empty.
- `finally`? ([_Event_] | [_Effect_] | [_Store_]): Unit, which will be triggered either in case of success or error.
Payload structure:
- `key` ([_string_]): Same `key` as above.
@@ -274,6 +274,57 @@ persist({
- Custom `persist` function, with predefined adapter options.
+## `createStorage` factory
+
+In rare cases you might want to get a granular control over a storage and manually set or get values from it. You can use `createStorage` factory for that.
+
+```javascript
+import { sample, createEvent } from 'effector'
+import { createStorage } from 'effector-storage/local'
+
+const persist = createStorage('my-storage')
+
+const userWantToSave = createEvent()
+const userWantToLoad = createEvent()
+
+// ---8<---
+
+sample({
+ clock: userWantToSave,
+ fn: () => 'some data'
+ target: persist.setFx,
+})
+
+sample({
+ source: userWantToLoad,
+ target: persist.getFx,
+})
+```
+
+### Options
+
+- `key`? ([_string_]): Key for local/session storage, to store value in. If omitted — `store` name is used. **Note!** If `key` is not specified, `store` _must_ have a `name`! You can use `'effector/babel-plugin'` to have those names automatically.
+- `keyPrefix`? ([_string_]): Prefix, used in adapter, to be concatenated to `key`. By default = `''`.
+- `context`? ([_Event_] | [_Effect_] | [_Store_]): Unit, which can set a special context for adapter.
+- `contract`? ([_Contract_]): Rule to statically validate data from storage.
+
+### Returns
+
+An object with fields:
+
+- `getFx` (_Effect_): to get value from storage.
+- `setFx` (_Effect_): to set value to storage.
+- `removeFx` (_Effect_): to remove value from storage.
+
+All fields of returned object are _Effects_ units, so you can use them in `sample` as any other _Effects_. For example, you can add logging on failed storage operations:
+
+```javascript
+sample({
+ clock: getFx.fail,
+ target: sendLogToSentry,
+})
+```
+
## Advanced usage
`effector-storage` consists of a _core_ module and _adapter_ modules.
diff --git a/src/broadcast/index.ts b/src/broadcast/index.ts
index 30a241f..b53cd2e 100644
--- a/src/broadcast/index.ts
+++ b/src/broadcast/index.ts
@@ -3,10 +3,12 @@ import type {
ConfigPersist as BaseConfigPersist,
ConfigStore as BaseConfigStore,
ConfigSourceTarget as BaseConfigSourceTarget,
+ ConfigCreateStorage as BaseConfigCreateStorage,
StorageAdapter,
+ StorageHandles,
} from '../types'
import type { BroadcastConfig } from './adapter'
-import { persist as base } from '../core'
+import { persist as base, createStorage as baseCreateStorage } from '../core'
import { nil } from '../nil'
import { adapter } from './adapter'
@@ -37,6 +39,19 @@ export interface Persist {
(config: ConfigStore): Subscription
}
+export interface ConfigCreateStorage
+ extends BaseConfigCreateStorage {}
+
+export interface CreateStorage {
+ (
+ key: string,
+ config?: BroadcastConfig & BaseConfigCreateStorage
+ ): StorageHandles
+ (
+ config: BroadcastConfig & BaseConfigCreateStorage & { key: string }
+ ): StorageHandles
+}
+
/**
* Function, checking if `BroadcastChannel` exists and accessible
*/
@@ -73,3 +88,19 @@ export function createPersist(defaults?: ConfigPersist): Persist {
* Default partially applied `persist`
*/
export const persist = /*#__PURE__*/ createPersist()
+
+/**
+ * Creates custom partially applied `createStorage`
+ * with predefined BroadcastChannel adapter
+ */
+export function createStorageFactory(
+ defaults?: ConfigCreateStorage
+): CreateStorage {
+ return (...configs: any[]) =>
+ baseCreateStorage({ adapter: broadcast }, defaults, ...configs)
+}
+
+/**
+ * Default partially applied `createStorage`
+ */
+export const createStorage = /*#__PURE__*/ createStorageFactory()
diff --git a/src/core/create-storage.ts b/src/core/create-storage.ts
new file mode 100644
index 0000000..d5448dc
--- /dev/null
+++ b/src/core/create-storage.ts
@@ -0,0 +1,188 @@
+import type { Event, Effect } from 'effector'
+import type {
+ ConfigAdapter,
+ ConfigAdapterFactory,
+ StorageHandles,
+ ConfigCreateStorage,
+ Fail,
+} from '../types'
+import {
+ attach,
+ // clearNode,
+ // createNode,
+ createStore,
+ is,
+ sample,
+ scopeBind,
+} from 'effector'
+import { getAreaStorage } from './area'
+import { validate } from './validate'
+
+type Config = Partial<
+ ConfigCreateStorage & { key: string } & (
+ | ConfigAdapter
+ | ConfigAdapterFactory
+ )
+>
+
+export function createStorage(
+ ...configs: (string | Config | undefined)[]
+): StorageHandles {
+ const config: Config = {}
+ for (const cfg of configs) {
+ Object.assign(config, typeof cfg === 'string' ? { key: cfg } : cfg)
+ }
+
+ const {
+ adapter: adapterOrFactory,
+ context,
+ key: keyName,
+ keyPrefix = '',
+ contract,
+ } = config
+
+ if (!adapterOrFactory) {
+ throw Error('Adapter is not defined')
+ }
+ if (!keyName) {
+ throw Error('Key is not defined')
+ }
+
+ const adapter =
+ 'factory' in adapterOrFactory ? adapterOrFactory(config) : adapterOrFactory
+
+ const key = keyName
+ const storage = getAreaStorage(
+ adapter.keyArea || adapter,
+ keyPrefix + key
+ )
+
+ // const region = createNode()
+ // let disposable: (_: any) => void = () => {}
+ // const desist = () => disposable(clearNode(region))
+
+ const ctx = createStore<[any?]>(
+ [is.store(context) ? context.defaultState : undefined],
+ { serialize: 'ignore' }
+ )
+
+ const value = adapter(keyPrefix + key, (x) => {
+ update(x)
+ })
+
+ // if (typeof value === 'function') {
+ // disposable = value
+ // }
+
+ const fail = (
+ operation: 'get' | 'set' | 'remove' | 'validate',
+ error: unknown,
+ value?: any
+ ) =>
+ ({
+ key,
+ keyPrefix,
+ operation,
+ error,
+ value: typeof value === 'function' ? undefined : value, // hide internal "box" implementation
+ }) as Fail
+
+ const op = (
+ operation: 'get' | 'set' | 'remove' | 'validate',
+ fn: (value: any, arg: any) => T,
+ value: any,
+ arg: any
+ ): T => {
+ try {
+ return fn(value, arg)
+ } catch (error) {
+ throw fail(operation, error, value)
+ }
+ }
+
+ const getFx = attach({
+ source: ctx,
+ effect([ref], raw?: any) {
+ const result = op('get', value.get, raw, ref) as any
+ return typeof result?.then === 'function'
+ ? Promise.resolve(result)
+ .then((result) => op('validate', validate, result, contract))
+ .catch((error) => {
+ throw fail('get', error, raw)
+ })
+ : op('validate', validate, result, contract)
+ },
+ }) as Effect // as Effect>
+
+ const setFx = attach({
+ source: ctx,
+ effect([ref], state: State) {
+ const result = op('set', value.set, state, ref)
+ if (typeof result?.then === 'function') {
+ return Promise.resolve(result)
+ .then(() => undefined)
+ .catch((error) => {
+ throw fail('set', error, state)
+ })
+ }
+ },
+ }) as Effect // as Effect>
+
+ const removeFx = attach({
+ source: ctx,
+ effect([ref]) {
+ const result = op('remove', value.remove ?? value.set, null, ref)
+ if (typeof result?.then === 'function') {
+ return Promise.resolve(result)
+ .then(() => undefined)
+ .catch((error) => {
+ throw fail('remove', error)
+ })
+ }
+ },
+ }) as Effect
+
+ removeFx.watch((_) => console.log('▼ removeFx.watch', _))
+ removeFx.finally.watch((_) => console.log('▼ removeFx.finally.watch', _))
+ getFx.watch((_) => console.log('▼ getFx.watch', _))
+ getFx.finally.watch((_) => console.log('▼ getFx.finally.watch', _))
+ storage.watch((_) => console.log('🚩 storage.watch', _))
+
+ let update: (raw?: any) => any = getFx
+ ctx.updates.watch(() => {
+ update = scopeBind(getFx as any, { safe: true })
+ })
+
+ const external = createStore(true, { serialize: 'ignore' }) //
+ .on([getFx.finally, setFx.finally], () => false)
+
+ sample({
+ clock: [getFx.doneData as Event, sample(setFx, setFx.done)],
+ // filter: (x) => x !== undefined,
+ fn: (x) => x ?? null,
+ target: storage,
+ })
+
+ sample({
+ clock: [storage, removeFx.finally],
+ filter: external,
+ fn: () => undefined,
+ target: getFx,
+ })
+
+ sample({
+ clock: [getFx.finally, setFx.finally],
+ fn: () => true,
+ target: external,
+ })
+
+ if (context) {
+ ctx.on(context, ([ref], payload) => [payload === undefined ? ref : payload])
+ }
+
+ return {
+ getFx,
+ setFx,
+ removeFx,
+ }
+}
diff --git a/src/core/index.ts b/src/core/index.ts
index feeee12..0b68565 100644
--- a/src/core/index.ts
+++ b/src/core/index.ts
@@ -1 +1,2 @@
export { persist } from './persist'
+export { createStorage } from './create-storage'
diff --git a/src/core/persist.ts b/src/core/persist.ts
index 297aaf7..5db9915 100644
--- a/src/core/persist.ts
+++ b/src/core/persist.ts
@@ -133,6 +133,8 @@ export function persist(
source: ctx,
effect: ([ref], raw?: any) => value.get(raw, ref),
}) as Effect
+ getFx.watch((_) => console.log('🔻getFx.watch', _))
+ getFx.finally.watch((_) => console.log('🔻getFx.finally.watch', _))
const setFx = attach({
source: ctx,
diff --git a/src/index.ts b/src/index.ts
index 3fc16da..8ab883c 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,5 +1,13 @@
-import type { ConfigPersist, Persist } from './types'
-import { persist as basePersist } from './core'
+import type {
+ ConfigPersist,
+ Persist,
+ CreateStorage,
+ ConfigCreateStorage,
+} from './types'
+import {
+ persist as basePersist,
+ createStorage as baseCreateStorage,
+} from './core'
export type {
ConfigPersist,
@@ -12,8 +20,11 @@ export type {
Persist,
Adapter,
DisposableAdapter,
+ StorageHandles,
StorageAdapter,
StorageAdapterFactory,
+ CreateStorage,
+ ConfigCreateStorage,
} from './types'
//
@@ -61,3 +72,17 @@ export function createPersist(defaults?: ConfigPersist): Persist {
* Default `persist`
*/
export const persist: Persist = basePersist
+
+/**
+ * Creates custom `createStorage`
+ */
+export function createStorageFactory(
+ defaults?: ConfigCreateStorage
+): CreateStorage {
+ return (...configs: any[]) => baseCreateStorage(defaults, ...configs)
+}
+
+/**
+ * Default `createStorage`
+ */
+export const createStorage: CreateStorage = baseCreateStorage
diff --git a/src/local/index.ts b/src/local/index.ts
index 0563b69..baea1ce 100644
--- a/src/local/index.ts
+++ b/src/local/index.ts
@@ -3,9 +3,11 @@ import type {
ConfigPersist as BaseConfigPersist,
ConfigStore as BaseConfigStore,
ConfigSourceTarget as BaseConfigSourceTarget,
+ ConfigCreateStorage as BaseConfigCreateStorage,
StorageAdapter,
+ StorageHandles,
} from '../types'
-import { persist as base } from '../core'
+import { persist as base, createStorage as baseCreateStorage } from '../core'
import { nil } from '../nil'
import { storage } from '../storage'
@@ -46,6 +48,20 @@ export interface Persist {
(config: ConfigStore): Subscription
}
+export interface ConfigCreateStorage
+ extends BaseConfigCreateStorage {}
+
+export interface CreateStorage {
+ (
+ key: string,
+ config?: LocalStorageConfig & BaseConfigCreateStorage
+ ): StorageHandles
+ (
+ config: LocalStorageConfig &
+ BaseConfigCreateStorage & { key: string }
+ ): StorageHandles
+}
+
/**
* Function, checking if `localStorage` exists
*/
@@ -90,3 +106,19 @@ export function createPersist(defaults?: ConfigPersist): Persist {
* Default partially applied `persist`
*/
export const persist = /*#__PURE__*/ createPersist()
+
+/**
+ * Creates custom partially applied `createStorage`
+ * with predefined `localStorage` adapter
+ */
+export function createStorageFactory(
+ defaults?: ConfigCreateStorage
+): CreateStorage {
+ return (...configs: any[]) =>
+ baseCreateStorage({ adapter: local }, defaults, ...configs)
+}
+
+/**
+ * Default partially applied `createStorage`
+ */
+export const createStorage = /*#__PURE__*/ createStorageFactory()
diff --git a/src/memory/adapter.ts b/src/memory/adapter.ts
index aec870d..4ab71bc 100644
--- a/src/memory/adapter.ts
+++ b/src/memory/adapter.ts
@@ -4,16 +4,21 @@ const data = new Map()
export interface MemoryConfig {
area?: Map
+ def?: any
}
/**
* Memory adapter
*/
adapter.factory = true as const
-export function adapter({ area = data }: MemoryConfig = {}): StorageAdapter {
+export function adapter({
+ area = data,
+ def,
+}: MemoryConfig = {}): StorageAdapter {
const adapter: StorageAdapter = (key: string) => ({
- get: () => area.get(key),
+ get: () => area.get(key) ?? def,
set: (value: State) => void area.set(key, value),
+ remove: () => void area.delete(key),
})
adapter.keyArea = area
diff --git a/src/memory/index.ts b/src/memory/index.ts
index 0e44245..b97878e 100644
--- a/src/memory/index.ts
+++ b/src/memory/index.ts
@@ -3,8 +3,10 @@ import type {
ConfigPersist as BaseConfigPersist,
ConfigStore as BaseConfigStore,
ConfigSourceTarget as BaseConfigSourceTarget,
+ ConfigCreateStorage as BaseConfigCreateStorage,
+ StorageHandles,
} from '../types'
-import { persist as base } from '../core'
+import { persist as base, createStorage as baseCreateStorage } from '../core'
import { adapter } from './adapter'
export type {
@@ -32,6 +34,19 @@ export interface Persist {
(config: ConfigStore): Subscription
}
+export interface ConfigCreateStorage
+ extends BaseConfigCreateStorage {}
+
+export interface CreateStorage {
+ (
+ key: string,
+ config?: BaseConfigCreateStorage
+ ): StorageHandles
+ (
+ config: BaseConfigCreateStorage & { key: string }
+ ): StorageHandles
+}
+
/**
* Returns memory adapter
*/
@@ -54,3 +69,19 @@ export function createPersist(defaults?: ConfigPersist): Persist {
* Default partially applied `persist`
*/
export const persist = /*#__PURE__*/ createPersist()
+
+/**
+ * Creates custom partially applied `createStorage`
+ * with predefined `memory` adapter
+ */
+export function createStorageFactory(
+ defaults?: ConfigCreateStorage
+): CreateStorage {
+ return (...configs: any[]) =>
+ baseCreateStorage({ adapter: adapter() }, defaults, ...configs)
+}
+
+/**
+ * Default partially applied `createStorage`
+ */
+export const createStorage = /*#__PURE__*/ createStorageFactory()
diff --git a/src/query/index.ts b/src/query/index.ts
index 4d6bcea..5fe6983 100644
--- a/src/query/index.ts
+++ b/src/query/index.ts
@@ -3,10 +3,12 @@ import type {
ConfigPersist as BaseConfigPersist,
ConfigStore as BaseConfigStore,
ConfigSourceTarget as BaseConfigSourceTarget,
+ ConfigCreateStorage as BaseConfigCreateStorage,
StorageAdapter,
+ StorageHandles,
} from '../types'
import type { ChangeMethod, StateBehavior, QueryConfig } from './adapter'
-import { persist as base } from '../core'
+import { persist as base, createStorage as baseCreateStorage } from '../core'
import { nil } from '../nil'
import { adapter } from './adapter'
@@ -47,6 +49,19 @@ export interface Persist {
(config: ConfigStore): Subscription
}
+export interface ConfigCreateStorage
+ extends BaseConfigCreateStorage {}
+
+export interface CreateStorage {
+ (
+ key: string,
+ config?: QueryConfig & BaseConfigCreateStorage
+ ): StorageHandles
+ (
+ config: QueryConfig & BaseConfigCreateStorage & { key: string }
+ ): StorageHandles
+}
+
/**
* Function, checking if `history` and `location` exists and accessible
*/
@@ -83,3 +98,19 @@ export function createPersist(defaults?: ConfigPersist): Persist {
* Default partially applied `persist`
*/
export const persist = /*#__PURE__*/ createPersist()
+
+/**
+ * Creates custom partially applied `createStorage`
+ * with predefined `query` adapter
+ */
+export function createStorageFactory(
+ defaults?: ConfigCreateStorage
+): CreateStorage {
+ return (...configs: any[]) =>
+ baseCreateStorage({ adapter: query }, defaults, ...configs)
+}
+
+/**
+ * Default partially applied `createStorage`
+ */
+export const createStorage = /*#__PURE__*/ createStorageFactory()
diff --git a/src/session/index.ts b/src/session/index.ts
index 5402dbb..f4b2adb 100644
--- a/src/session/index.ts
+++ b/src/session/index.ts
@@ -3,9 +3,11 @@ import type {
ConfigPersist as BaseConfigPersist,
ConfigStore as BaseConfigStore,
ConfigSourceTarget as BaseConfigSourceTarget,
+ ConfigCreateStorage as BaseConfigCreateStorage,
StorageAdapter,
+ StorageHandles,
} from '../types'
-import { persist as base } from '../core'
+import { persist as base, createStorage as baseCreateStorage } from '../core'
import { nil } from '../nil'
import { storage } from '../storage'
@@ -46,6 +48,20 @@ export interface Persist {
(config: ConfigStore): Subscription
}
+export interface ConfigCreateStorage
+ extends BaseConfigCreateStorage {}
+
+export interface CreateStorage {
+ (
+ key: string,
+ config?: SessionStorageConfig & BaseConfigCreateStorage
+ ): StorageHandles
+ (
+ config: SessionStorageConfig &
+ BaseConfigCreateStorage & { key: string }
+ ): StorageHandles
+}
+
/**
* Function, checking if `sessionStorage` exists
*/
@@ -89,3 +105,19 @@ export function createPersist(defaults?: ConfigPersist): Persist {
* Default partially applied `persist`
*/
export const persist = /*#__PURE__*/ createPersist()
+
+/**
+ * Creates custom partially applied `createStorage`
+ * with predefined `sessionStorage` adapter
+ */
+export function createStorageFactory(
+ defaults?: ConfigCreateStorage
+): CreateStorage {
+ return (...configs: any[]) =>
+ baseCreateStorage({ adapter: session }, defaults, ...configs)
+}
+
+/**
+ * Default partially applied `createStorage`
+ */
+export const createStorage = /*#__PURE__*/ createStorageFactory()
diff --git a/src/storage/index.ts b/src/storage/index.ts
index bdde271..6af70f3 100644
--- a/src/storage/index.ts
+++ b/src/storage/index.ts
@@ -101,6 +101,13 @@ export function storage({
schedule()
}
},
+
+ remove() {
+ console.log('🔴 remove', key)
+ postponed() // cancel postponed flush
+ storage().removeItem(key)
+ // update(null) // call update with null to clear the value
+ },
})
}
diff --git a/src/types.ts b/src/types.ts
index 90c95ec..06d7d96 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -13,6 +13,10 @@ export interface Adapter {
value: State,
ctx?: any
): void | Promise
+ remove?(
+ this: void, //
+ ctx?: any
+ ): void | Promise
}
export interface DisposableAdapter extends Adapter {
@@ -48,7 +52,7 @@ export type Done = {
export type Fail = {
key: string
keyPrefix: string
- operation: 'set' | 'get'
+ operation: 'set' | 'get' | 'validate'
error: Err
value?: any
}
@@ -119,3 +123,36 @@ export interface Persist {
AdapterConfig
): Subscription
}
+
+export interface StorageHandles {
+ getFx: Effect>
+ setFx: Effect>
+ removeFx: Effect>
+}
+
+export interface ConfigCreateStorage {
+ context?: Unit
+ keyPrefix?: string
+ contract?: Contract
+}
+
+export interface CreateStorage {
+ (
+ key: string,
+ config: ConfigAdapterFactory &
+ ConfigCreateStorage &
+ AdapterConfig
+ ): StorageHandles
+ (
+ config: ConfigAdapterFactory &
+ ConfigCreateStorage &
+ AdapterConfig & { key: string }
+ ): StorageHandles
+ (
+ key: string,
+ config: ConfigAdapter & ConfigCreateStorage
+ ): StorageHandles
+ (
+ config: ConfigAdapter & ConfigCreateStorage & { key: string }
+ ): StorageHandles
+}
diff --git a/tests/broadcast.test.ts b/tests/broadcast.test.ts
index 9242007..ddf7440 100644
--- a/tests/broadcast.test.ts
+++ b/tests/broadcast.test.ts
@@ -3,7 +3,7 @@ import * as assert from 'node:assert/strict'
import { BroadcastChannel, Worker } from 'node:worker_threads'
import { createEffect, createStore, sample } from 'effector'
import { createEventsMock } from './mocks/events.mock'
-import { broadcast, persist } from '../src/broadcast'
+import { broadcast, persist, createStorage } from '../src/broadcast'
import { broadcast as broadcastIndex } from '../src'
import { either } from '../src/tools'
import { log } from '../src/log'
@@ -78,6 +78,7 @@ after(() => {
test('should export adapter and `persist` function', () => {
assert.ok(typeof broadcast === 'function')
assert.ok(typeof persist === 'function')
+ assert.ok(typeof createStorage === 'function')
})
test('should be exported from package root', () => {
@@ -87,6 +88,8 @@ test('should be exported from package root', () => {
test('should be ok on good parameters', () => {
const $store = createStore(0, { name: 'broadcast' })
assert.doesNotThrow(() => persist({ store: $store }))
+ assert.doesNotThrow(() => createStorage('broadcast'))
+ assert.doesNotThrow(() => createStorage({ key: 'broadcast' }))
})
test('should post message to broadcast channel on updates', async () => {
diff --git a/tests/context-create-storage.test.ts b/tests/context-create-storage.test.ts
new file mode 100644
index 0000000..d87dc5b
--- /dev/null
+++ b/tests/context-create-storage.test.ts
@@ -0,0 +1,188 @@
+import type { StorageAdapter } from '../src/types'
+import { test, mock } from 'node:test'
+import * as assert from 'node:assert/strict'
+import { createStore, createEvent, fork, allSettled } from 'effector'
+import { createStorage } from '../src'
+
+//
+// Tests
+//
+
+test('context store value should be passed to adapter', async () => {
+ const watch = mock.fn()
+
+ const context = createEvent()
+
+ const { getFx, setFx } = createStorage('test-context-1', {
+ adapter: () => ({ get: watch, set: watch }),
+ context: createStore(42).on(context, (_, ctx) => ctx),
+ })
+
+ getFx()
+
+ assert.strictEqual(watch.mock.callCount(), 1)
+ assert.deepEqual(watch.mock.calls[0].arguments, [undefined, 42])
+
+ setFx(54)
+
+ assert.strictEqual(watch.mock.callCount(), 2)
+ assert.deepEqual(watch.mock.calls[1].arguments, [54, 42])
+
+ // update context
+ context(72)
+
+ getFx()
+
+ assert.strictEqual(watch.mock.callCount(), 3)
+ assert.deepEqual(watch.mock.calls[2].arguments, [undefined, 72])
+
+ setFx(27)
+
+ assert.strictEqual(watch.mock.callCount(), 4)
+ assert.deepEqual(watch.mock.calls[3].arguments, [27, 72])
+})
+
+test('context event value should be passed to adapter', async () => {
+ const watch = mock.fn()
+
+ const context = createEvent()
+
+ const { getFx, setFx } = createStorage('test-context-2', {
+ adapter: () => ({ get: watch, set: watch }),
+ context,
+ })
+
+ getFx()
+
+ assert.strictEqual(watch.mock.callCount(), 1)
+ assert.deepEqual(watch.mock.calls[0].arguments, [undefined, undefined])
+
+ setFx(54)
+
+ assert.strictEqual(watch.mock.callCount(), 2)
+ assert.deepEqual(watch.mock.calls[1].arguments, [54, undefined])
+
+ // update context
+ context('new context')
+
+ getFx()
+
+ assert.strictEqual(watch.mock.callCount(), 3)
+ assert.deepEqual(watch.mock.calls[2].arguments, [undefined, 'new context'])
+
+ setFx(27)
+
+ assert.strictEqual(watch.mock.callCount(), 4)
+ assert.deepEqual(watch.mock.calls[3].arguments, [27, 'new context'])
+})
+
+test('contexts in different scopes should be different', async () => {
+ const watch = mock.fn()
+
+ const context = createEvent<{ name: string }>()
+
+ const { getFx, setFx } = createStorage('test-context-3', {
+ adapter: () => ({ get: watch, set: watch }),
+ context,
+ })
+
+ const scopeA = fork()
+ const scopeB = fork()
+
+ await allSettled(context, { scope: scopeA, params: { name: 'scopeA' } })
+ await allSettled(context, { scope: scopeB, params: { name: 'scopeB' } })
+
+ await allSettled(getFx, { scope: scopeA })
+ await allSettled(getFx, { scope: scopeB })
+
+ assert.strictEqual(watch.mock.callCount(), 2)
+ assert.deepEqual(watch.mock.calls[0].arguments, [
+ undefined,
+ { name: 'scopeA' },
+ ])
+ assert.deepEqual(watch.mock.calls[1].arguments, [
+ undefined,
+ { name: 'scopeB' },
+ ])
+
+ await allSettled(setFx, { scope: scopeA, params: 'A' })
+ await allSettled(setFx, { scope: scopeB, params: 'B' })
+
+ assert.strictEqual(watch.mock.callCount(), 4)
+ assert.deepEqual(watch.mock.calls[2].arguments, ['A', { name: 'scopeA' }])
+ assert.deepEqual(watch.mock.calls[3].arguments, ['B', { name: 'scopeB' }])
+})
+
+test('context should change scope for async adapter', async () => {
+ const watch = mock.fn((value) => value)
+
+ const updated = createEvent()
+ const context = createEvent()
+
+ createStorage('test-context-4', {
+ context,
+ adapter: (_key, update) => {
+ updated.watch(update)
+ return { get: watch, set: watch }
+ },
+ })
+
+ updated('out of scope') // <- imitate external storage update
+
+ assert.strictEqual(watch.mock.callCount(), 1)
+ assert.deepEqual(watch.mock.calls[0].arguments, ['out of scope', undefined])
+
+ const scope = fork()
+
+ // set context, which should bind given scope
+ await allSettled(context, { scope, params: 'in scope' })
+
+ updated('in scope') // <- pickup new value, within scope
+
+ assert.strictEqual(watch.mock.callCount(), 2)
+ assert.deepEqual(watch.mock.calls[1].arguments, ['in scope', 'in scope'])
+})
+
+test('contexts should update scope / also works with adapter factory', async () => {
+ const queue = new EventTarget()
+
+ adapterFactory.factory = true as const
+ function adapterFactory() {
+ const adapter: StorageAdapter = (
+ _key: string,
+ update: (raw?: any) => void
+ ) => {
+ let value = 1 as State
+ queue.addEventListener('update', () => update((value = 2 as State)))
+ return {
+ get: () => value,
+ set: (x: State) => void (value = x),
+ }
+ }
+ return adapter
+ }
+
+ const context = createEvent()
+
+ const { getFx } = createStorage('test-context-4', {
+ adapter: adapterFactory,
+ context,
+ contract: (raw: unknown): raw is number => typeof raw === 'number',
+ })
+
+ const $store = createStore(0).on(getFx.doneData, (_, data) => data)
+
+ const scope = fork()
+
+ queue.dispatchEvent(new Event('update'))
+
+ assert.strictEqual($store.getState(), 2) // <- changed
+ assert.strictEqual(scope.getState($store), 0) // <- default value
+
+ await allSettled(context, { scope })
+
+ queue.dispatchEvent(new Event('update'))
+
+ assert.strictEqual($store.getState(), 2)
+ assert.strictEqual(scope.getState($store), 2) // <- changed in scope
+})
diff --git a/tests/contract-create-storage.test.ts b/tests/contract-create-storage.test.ts
new file mode 100644
index 0000000..7cd38bd
--- /dev/null
+++ b/tests/contract-create-storage.test.ts
@@ -0,0 +1,469 @@
+import type { StorageAdapter } from '../src'
+import { test, before, after, mock } from 'node:test'
+import * as assert from 'node:assert/strict'
+import { createStore, createEvent } from 'effector'
+import * as s from 'superstruct'
+import { superstructContract } from '@farfetched/superstruct'
+import { createStorage, storage, persist } from '../src'
+import { createStorageMock } from './mocks/storage.mock'
+import { type Events, createEventsMock } from './mocks/events.mock'
+
+//
+// Mock abstract Storage adapter
+//
+
+declare let global: any
+
+const mockStorage = createStorageMock()
+let storageAdapter: StorageAdapter
+let events: Events
+
+before(() => {
+ events = createEventsMock()
+ global.addEventListener = events.addEventListener
+ storageAdapter = storage({ storage: () => mockStorage, sync: true })
+})
+
+after(() => {
+ global.addEventListener = undefined
+})
+
+//
+// Tests
+//
+
+test('shoult validate storage value on get', () => {
+ const watch = mock.fn()
+
+ mockStorage.setItem('number1', '42')
+
+ const { getFx } = createStorage({
+ adapter: storageAdapter,
+ key: 'number1',
+ contract: (raw): raw is number => typeof raw === 'number',
+ })
+
+ getFx.watch(watch)
+ getFx.finally.watch(watch)
+
+ getFx()
+
+ assert.strictEqual(watch.mock.callCount(), 2)
+ assert.deepEqual(watch.mock.calls[0].arguments, [undefined]) // getFx trigger
+ assert.deepEqual(watch.mock.calls[1].arguments, [
+ {
+ status: 'done',
+ params: undefined,
+ result: 42,
+ },
+ ]) // getFx result
+
+ assert.strictEqual(mockStorage.getItem('number1'), '42')
+})
+
+test('shoult fail on invalid initial storage value with simple contract', () => {
+ const watch = mock.fn()
+
+ mockStorage.setItem('number2', '"invalid"') // valid JSON, but invalid number
+
+ const { getFx } = createStorage({
+ adapter: storageAdapter,
+ key: 'number2',
+ contract: (raw): raw is number => typeof raw === 'number',
+ })
+
+ getFx.watch(watch)
+ getFx.finally.watch(watch)
+
+ getFx()
+
+ assert.strictEqual(watch.mock.callCount(), 2)
+ assert.deepEqual(watch.mock.calls[0].arguments, [undefined]) // getFx trigger
+ assert.deepEqual(watch.mock.calls[1].arguments, [
+ {
+ status: 'fail',
+ params: undefined,
+ error: {
+ key: 'number2',
+ keyPrefix: '',
+ operation: 'validate',
+ error: ['Invalid data'],
+ value: 'invalid',
+ },
+ },
+ ]) // getFx result
+
+ assert.strictEqual(mockStorage.getItem('number2'), '"invalid"') // didn't change
+})
+
+test('should handle sync effects with same key and different validators', () => {
+ const watchPlain = mock.fn()
+ const watchBase64 = mock.fn()
+
+ const { getFx: getPlainFx, setFx: setPlainFx } = createStorage({
+ adapter: storageAdapter,
+ key: 'contract-same-key-1',
+ contract: (raw): raw is string => typeof raw === 'string',
+ })
+ const { getFx: getBase64Fx, setFx: setBase64Fx } = createStorage({
+ adapter: storageAdapter,
+ key: 'contract-same-key-1',
+ contract: (raw): raw is string =>
+ global.Buffer.from(raw, 'base64').toString('base64') === raw,
+ })
+
+ getPlainFx.watch(watchPlain)
+ setPlainFx.watch(watchPlain)
+ getPlainFx.finally.watch(watchPlain)
+ setPlainFx.finally.watch(watchPlain)
+
+ getBase64Fx.watch(watchBase64)
+ setBase64Fx.watch(watchBase64)
+ getBase64Fx.finally.watch(watchBase64)
+ setBase64Fx.finally.watch(watchBase64)
+
+ assert.strictEqual(watchPlain.mock.callCount(), 0)
+ assert.strictEqual(watchBase64.mock.callCount(), 0)
+
+ setPlainFx('plain value')
+ assert.strictEqual(
+ mockStorage.getItem('contract-same-key-1'),
+ '"plain value"'
+ )
+
+ assert.strictEqual(watchPlain.mock.callCount(), 2)
+ assert.deepEqual(watchPlain.mock.calls[0].arguments, ['plain value']) // setPlainFx trigger
+ assert.deepEqual(watchPlain.mock.calls[1].arguments, [
+ {
+ status: 'done',
+ params: 'plain value',
+ result: undefined,
+ },
+ ]) // setPlainFx result
+
+ assert.strictEqual(watchBase64.mock.callCount(), 2)
+ assert.deepEqual(watchBase64.mock.calls[0].arguments, [undefined]) // getBase64Fx trigger
+ assert.deepEqual(watchBase64.mock.calls[1].arguments, [
+ {
+ status: 'fail',
+ params: undefined,
+ error: {
+ key: 'contract-same-key-1',
+ keyPrefix: '',
+ operation: 'validate',
+ error: ['Invalid data'],
+ value: 'plain value',
+ },
+ },
+ ]) // getBase64Fx result
+
+ setBase64Fx('YmFzZTY0IHZhbHVl')
+ assert.strictEqual(
+ mockStorage.getItem('contract-same-key-1'),
+ '"YmFzZTY0IHZhbHVl"'
+ )
+
+ assert.strictEqual(watchBase64.mock.callCount(), 4)
+ assert.deepEqual(watchBase64.mock.calls[2].arguments, ['YmFzZTY0IHZhbHVl']) // setBase64Fx trigger
+ assert.deepEqual(watchBase64.mock.calls[3].arguments, [
+ {
+ status: 'done',
+ params: 'YmFzZTY0IHZhbHVl',
+ result: undefined,
+ },
+ ]) // setBase64Fx result
+
+ assert.strictEqual(watchPlain.mock.callCount(), 4)
+ assert.deepEqual(watchPlain.mock.calls[2].arguments, [undefined]) // getPlainFx trigger
+ assert.deepEqual(watchPlain.mock.calls[3].arguments, [
+ {
+ status: 'done',
+ params: undefined,
+ result: 'YmFzZTY0IHZhbHVl', // this is valid string
+ },
+ ]) // getPlainFx result
+})
+
+test('should handle sync with `persist` with different validators, update from store', () => {
+ const watch = mock.fn()
+ const $string = createStore('')
+
+ persist({
+ store: $string,
+ adapter: storageAdapter,
+ key: 'contract-same-key-2',
+ })
+
+ const { getFx, setFx } = createStorage({
+ adapter: storageAdapter,
+ key: 'contract-same-key-2',
+ contract: (raw): raw is string =>
+ global.Buffer.from(raw, 'base64').toString('base64') === raw,
+ })
+
+ getFx.watch(watch)
+ setFx.watch(watch)
+ getFx.finally.watch(watch)
+ setFx.finally.watch(watch)
+
+ //
+ ;($string as any).setState('plain value')
+ assert.strictEqual(
+ mockStorage.getItem('contract-same-key-2'),
+ '"plain value"'
+ )
+
+ assert.strictEqual(watch.mock.callCount(), 2)
+ assert.deepEqual(watch.mock.calls[0].arguments, [undefined]) // getFx trigger
+ assert.deepEqual(watch.mock.calls[1].arguments, [
+ {
+ status: 'fail',
+ params: undefined,
+ error: {
+ key: 'contract-same-key-2',
+ keyPrefix: '',
+ operation: 'validate',
+ error: ['Invalid data'],
+ value: 'plain value',
+ },
+ },
+ ]) // getFx result
+})
+
+test('should handle sync with `persist` with different validators, update from storage', () => {
+ const watch = mock.fn()
+ const fail = createEvent()
+ fail.watch(watch)
+
+ const $base64 = createStore('')
+
+ persist({
+ store: $base64,
+ adapter: storageAdapter,
+ key: 'contract-same-key-3',
+ contract: (raw): raw is string =>
+ global.Buffer.from(raw, 'base64').toString('base64') === raw,
+ fail,
+ })
+
+ const { setFx } = createStorage({
+ adapter: storageAdapter,
+ key: 'contract-same-key-3',
+ contract: (raw): raw is string =>
+ global.Buffer.from(raw, 'base64').toString('base64') === raw,
+ })
+
+ setFx('plain value')
+ assert.strictEqual(
+ mockStorage.getItem('contract-same-key-3'),
+ '"plain value"'
+ )
+
+ assert.strictEqual($base64.getState(), '') // <- didn't change
+
+ assert.strictEqual(watch.mock.callCount(), 1)
+ assert.deepEqual(watch.mock.calls[0].arguments, [
+ {
+ key: 'contract-same-key-3',
+ keyPrefix: '',
+ operation: 'validate',
+ error: ['Invalid data'],
+ value: 'plain value',
+ },
+ ])
+})
+
+test('shoult validate storage value on get with complex contract (valid)', () => {
+ const watch = mock.fn()
+
+ const Asteroid = s.type({
+ type: s.literal('asteroid'),
+ mass: s.number(),
+ })
+
+ mockStorage.setItem('asteroid0', '{"type":"asteroid","mass":42}')
+
+ const { getFx } = createStorage({
+ adapter: storageAdapter,
+ key: 'asteroid0',
+ contract: superstructContract(Asteroid),
+ })
+
+ getFx.watch(watch)
+ getFx.finally.watch(watch)
+
+ getFx()
+
+ assert.strictEqual(watch.mock.callCount(), 2)
+ assert.deepEqual(watch.mock.calls[0].arguments, [undefined]) // getFx trigger
+ assert.deepEqual(watch.mock.calls[1].arguments, [
+ {
+ status: 'done',
+ params: undefined,
+ result: { type: 'asteroid', mass: 42 },
+ },
+ ]) // getFx result
+})
+
+test('shoult validate storage value on get with complex contract (valid undefined)', () => {
+ const watch = mock.fn()
+
+ const Asteroid = s.optional(
+ s.type({
+ type: s.literal('asteroid'),
+ mass: s.number(),
+ })
+ )
+
+ const { getFx } = createStorage({
+ adapter: storageAdapter,
+ key: 'asteroid1',
+ contract: superstructContract(Asteroid),
+ })
+
+ getFx.watch(watch)
+ getFx.finally.watch(watch)
+
+ getFx()
+
+ assert.strictEqual(watch.mock.callCount(), 2)
+ assert.deepEqual(watch.mock.calls[0].arguments, [undefined]) // getFx trigger
+ assert.deepEqual(watch.mock.calls[1].arguments, [
+ {
+ status: 'done',
+ params: undefined,
+ result: undefined,
+ },
+ ]) // getFx result
+})
+
+test('shoult validate storage value on get with complex contract (invalid undefined)', () => {
+ const watch = mock.fn()
+
+ const Asteroid = s.type({
+ type: s.literal('asteroid'),
+ mass: s.number(),
+ })
+
+ const { getFx } = createStorage({
+ adapter: storageAdapter,
+ key: 'asteroid1',
+ contract: superstructContract(Asteroid),
+ })
+
+ getFx.watch(watch)
+ getFx.finally.watch(watch)
+
+ getFx()
+
+ assert.strictEqual(watch.mock.callCount(), 2)
+ assert.deepEqual(watch.mock.calls[0].arguments, [undefined]) // getFx trigger
+ assert.deepEqual(watch.mock.calls[1].arguments, [
+ {
+ status: 'fail',
+ params: undefined,
+ error: {
+ key: 'asteroid1',
+ keyPrefix: '',
+ operation: 'validate',
+ error: ['Expected an object, but received: undefined'],
+ value: undefined,
+ },
+ },
+ ]) // getFx result
+})
+
+test('shoult validate storage value on get with complex contract (invalid)', () => {
+ const watch = mock.fn()
+
+ const Asteroid = s.type({
+ type: s.literal('asteroid'),
+ mass: s.number(),
+ })
+
+ mockStorage.setItem('asteroid2', '42')
+
+ const { getFx } = createStorage({
+ adapter: storageAdapter,
+ key: 'asteroid2',
+ contract: superstructContract(Asteroid),
+ })
+
+ getFx.watch(watch)
+ getFx.finally.watch(watch)
+
+ getFx()
+
+ assert.strictEqual(watch.mock.callCount(), 2)
+ assert.deepEqual(watch.mock.calls[0].arguments, [undefined]) // getFx trigger
+ assert.deepEqual(watch.mock.calls[1].arguments, [
+ {
+ status: 'fail',
+ params: undefined,
+ error: {
+ key: 'asteroid2',
+ keyPrefix: '',
+ operation: 'validate',
+ error: ['Expected an object, but received: 42'],
+ value: 42,
+ },
+ },
+ ]) // getFx result
+
+ assert.strictEqual(mockStorage.getItem('asteroid2'), '42')
+})
+
+test('should validate value on storage external update', async () => {
+ const watch = mock.fn()
+
+ const { getFx } = createStorage({
+ adapter: storageAdapter,
+ key: 'storage-contract-counter-1',
+ contract: superstructContract(s.number()),
+ })
+
+ getFx.watch(watch)
+ getFx.finally.watch(watch)
+
+ mockStorage.setItem('storage-contract-counter-1', '1')
+ await events.dispatchEvent('storage', {
+ storageArea: mockStorage,
+ key: 'storage-contract-counter-1',
+ oldValue: null,
+ newValue: '1',
+ })
+
+ assert.strictEqual(watch.mock.callCount(), 2)
+ assert.deepEqual(watch.mock.calls[0].arguments, ['1']) // getFx trigger with raw value
+ assert.deepEqual(watch.mock.calls[1].arguments, [
+ {
+ status: 'done',
+ params: '1', // raw value from adapter
+ result: 1,
+ },
+ ]) // getFx result
+
+ mockStorage.setItem('storage-contract-counter-1', '"invalid"')
+ await events.dispatchEvent('storage', {
+ storageArea: mockStorage,
+ key: 'storage-contract-counter-1',
+ oldValue: null,
+ newValue: '"invalid"',
+ })
+
+ assert.strictEqual(watch.mock.callCount(), 4)
+ assert.deepEqual(watch.mock.calls[2].arguments, ['"invalid"']) // getFx trigger with raw value
+ assert.deepEqual(watch.mock.calls[3].arguments, [
+ {
+ status: 'fail',
+ params: '"invalid"', // raw value from adapter
+ error: {
+ key: 'storage-contract-counter-1',
+ keyPrefix: '',
+ operation: 'validate',
+ error: ['Expected a number, but received: "invalid"'],
+ value: 'invalid',
+ },
+ },
+ ]) // getFx result
+})
diff --git a/tests/core-create-storage.test.ts b/tests/core-create-storage.test.ts
new file mode 100644
index 0000000..fbcefe8
--- /dev/null
+++ b/tests/core-create-storage.test.ts
@@ -0,0 +1,569 @@
+import { test, mock } from 'node:test'
+import * as assert from 'node:assert/strict'
+import { createStore, createEvent } from 'effector'
+import {
+ createStorage,
+ createStorageFactory,
+ memory,
+ persist,
+ async,
+} from '../src'
+
+// memory adapter with separate storage area, to prevent concurrency issues between tests
+const adapter = memory({ area: new Map() })
+
+//
+// Tests
+//
+
+test('should exports effects', () => {
+ assert.ok(typeof createStorageFactory === 'function')
+ assert.ok(typeof createStorageFactory() === 'function')
+ assert.ok(typeof createStorage === 'function')
+ const ret = createStorage('test-key', { adapter })
+ assert.ok(typeof ret === 'object')
+ assert.ok(typeof ret.getFx === 'function')
+ assert.ok(typeof ret.setFx === 'function')
+ assert.ok(typeof ret.removeFx === 'function')
+})
+
+test('should be ok on good parameters', () => {
+ assert.doesNotThrow(() => {
+ createStorage('test-1', {
+ adapter,
+ })
+ })
+ assert.doesNotThrow(() => {
+ createStorage({
+ adapter,
+ key: 'test-2',
+ })
+ })
+ assert.doesNotThrow(() => {
+ createStorage('test-3', {
+ adapter,
+ keyPrefix: 'prefix-3',
+ })
+ })
+ assert.doesNotThrow(() => {
+ createStorage({
+ adapter,
+ key: 'tets-4',
+ keyPrefix: 'prefix-3',
+ })
+ })
+ assert.doesNotThrow(() => {
+ createStorage('test-1', {
+ adapter,
+ context: createStore(0),
+ })
+ })
+ assert.doesNotThrow(() => {
+ createStorage('test-1', {
+ adapter,
+ contract: (x): x is number => typeof x === 'number',
+ })
+ })
+})
+
+test('should handle wrong parameters', () => {
+ assert.throws(
+ // @ts-expect-error test wrong parameters
+ () => createStorage(),
+ /Adapter is not defined/
+ )
+ assert.throws(
+ // @ts-expect-error test wrong parameters
+ () => createStorage({}),
+ /Adapter is not defined/
+ )
+ assert.throws(
+ // @ts-expect-error test wrong parameters
+ () => createStorage('key', {}),
+ /Adapter is not defined/
+ )
+ assert.throws(
+ // @ts-expect-error test wrong parameters
+ () => createStorage({ key: 'key' }),
+ /Adapter is not defined/
+ )
+ assert.throws(
+ // @ts-expect-error test wrong parameters
+ () => createStorage({ adapter }),
+ /Key is not defined/
+ )
+})
+
+test('should get and set value from storage', async () => {
+ const watch = mock.fn()
+
+ const { getFx, setFx } = createStorage('test-get-set-1', {
+ adapter,
+ })
+
+ getFx.watch(watch)
+ setFx.watch(watch)
+ getFx.finally.watch(watch)
+ setFx.finally.watch(watch)
+
+ assert.strictEqual(watch.mock.callCount(), 0)
+
+ getFx()
+
+ assert.strictEqual(watch.mock.callCount(), 2)
+ assert.deepEqual(watch.mock.calls[0].arguments, [undefined]) // getFx trigger
+ assert.deepEqual(watch.mock.calls[1].arguments, [
+ {
+ status: 'done',
+ params: undefined,
+ result: undefined,
+ },
+ ]) // getFx result
+
+ setFx(1)
+
+ assert.strictEqual(watch.mock.callCount(), 4)
+ assert.deepEqual(watch.mock.calls[2].arguments, [1]) // setFx trigger
+ assert.deepEqual(watch.mock.calls[3].arguments, [
+ {
+ status: 'done',
+ params: 1,
+ result: undefined,
+ },
+ ]) // setFx result
+
+ assert.strictEqual(await getFx(), 1)
+})
+
+test('should remove value from storage', async () => {
+ const { setFx, getFx, removeFx } = createStorage('test-get-set-1', {
+ adapter,
+ })
+
+ await setFx(1)
+
+ assert.strictEqual(await getFx(), 1)
+
+ await removeFx()
+
+ assert.strictEqual(await getFx(), undefined)
+})
+
+test('should get and set value from storage (with adapter factory)', async () => {
+ const watch = mock.fn()
+
+ const area = new Map()
+ const { getFx, setFx } = createStorage('test-get-set-2', {
+ adapter: memory,
+ area,
+ })
+
+ getFx.watch(watch)
+ setFx.watch(watch)
+ getFx.finally.watch(watch)
+ setFx.finally.watch(watch)
+
+ assert.strictEqual(watch.mock.callCount(), 0)
+
+ getFx()
+
+ assert.strictEqual(watch.mock.callCount(), 2)
+ assert.deepEqual(watch.mock.calls[0].arguments, [undefined]) // getFx trigger
+ assert.deepEqual(watch.mock.calls[1].arguments, [
+ {
+ status: 'done',
+ params: undefined,
+ result: undefined,
+ },
+ ]) // getFx result
+
+ setFx(1)
+
+ assert.strictEqual(watch.mock.callCount(), 4)
+ assert.deepEqual(watch.mock.calls[2].arguments, [1]) // setFx trigger
+ assert.deepEqual(watch.mock.calls[3].arguments, [
+ {
+ status: 'done',
+ params: 1,
+ result: undefined,
+ },
+ ]) // setFx result
+
+ assert.strictEqual(await getFx(), 1)
+ assert.strictEqual(area.get('test-get-set-2'), 1)
+})
+
+test('should get and set value from async storage', async () => {
+ const watch = mock.fn()
+
+ const { getFx, setFx } = createStorage('test-get-set-3', {
+ adapter: async(adapter),
+ })
+
+ getFx.watch(watch)
+ setFx.watch(watch)
+ getFx.finally.watch(watch)
+ setFx.finally.watch(watch)
+
+ assert.strictEqual(watch.mock.callCount(), 0)
+
+ await getFx()
+
+ assert.strictEqual(watch.mock.callCount(), 2)
+ assert.deepEqual(watch.mock.calls[0].arguments, [undefined]) // getFx trigger
+ assert.deepEqual(watch.mock.calls[1].arguments, [
+ {
+ status: 'done',
+ params: undefined,
+ result: undefined,
+ },
+ ]) // getFx result
+
+ await setFx(1)
+
+ assert.strictEqual(watch.mock.callCount(), 4)
+ assert.deepEqual(watch.mock.calls[2].arguments, [1]) // setFx trigger
+ assert.deepEqual(watch.mock.calls[3].arguments, [
+ {
+ status: 'done',
+ params: 1,
+ result: undefined,
+ },
+ ]) // setFx result
+
+ assert.strictEqual(await getFx(), 1)
+})
+
+test('should sync effects for the same adapter-key', () => {
+ const watchSet = mock.fn()
+ const watchGet = mock.fn()
+
+ const { setFx } = createStorage({
+ adapter,
+ key: 'test-sync-same-key-1',
+ })
+ const { getFx } = createStorage({
+ adapter,
+ key: 'test-sync-same-key-1',
+ })
+
+ getFx.watch(watchGet)
+ setFx.watch(watchSet)
+ getFx.finally.watch(watchGet)
+ setFx.finally.watch(watchSet)
+
+ assert.strictEqual(watchSet.mock.callCount(), 0)
+ assert.strictEqual(watchGet.mock.callCount(), 0)
+
+ setFx(1)
+
+ assert.strictEqual(watchSet.mock.callCount(), 2)
+ assert.strictEqual(watchGet.mock.callCount(), 2)
+ assert.deepEqual(watchSet.mock.calls[0].arguments, [1]) // setFx trigger
+ assert.deepEqual(watchSet.mock.calls[1].arguments, [
+ {
+ status: 'done',
+ params: 1,
+ result: undefined,
+ },
+ ]) // setFx result
+ assert.deepEqual(watchGet.mock.calls[0].arguments, [undefined]) // getFx trigger
+ assert.deepEqual(watchGet.mock.calls[1].arguments, [
+ {
+ status: 'done',
+ params: undefined,
+ result: 1,
+ },
+ ]) // getFx result
+})
+
+test('should sync with `persist` for the same adapter-key', async () => {
+ const watch = mock.fn()
+ const watchFx = mock.fn()
+
+ const $store = createStore(11)
+ $store.watch(watch)
+
+ assert.strictEqual($store.getState(), 11)
+ assert.strictEqual(watch.mock.callCount(), 1)
+ assert.deepEqual(watch.mock.calls[0].arguments, [11])
+
+ persist({
+ store: $store,
+ adapter,
+ key: 'test-sync-same-key-2',
+ })
+
+ assert.strictEqual($store.getState(), 11) // did not change
+ assert.strictEqual(watch.mock.callCount(), 1) // did not trigger
+
+ const { getFx, setFx } = createStorage({
+ adapter,
+ key: 'test-sync-same-key-2',
+ })
+
+ getFx.watch(watchFx)
+ setFx.watch(watchFx)
+ getFx.finally.watch(watchFx)
+ setFx.finally.watch(watchFx)
+
+ assert.strictEqual(watchFx.mock.callCount(), 0) // did not trigger
+
+ setFx(22)
+
+ assert.strictEqual(watchFx.mock.callCount(), 2)
+ assert.deepEqual(watchFx.mock.calls[0].arguments, [22]) // setFx trigger
+ assert.deepEqual(watchFx.mock.calls[1].arguments, [
+ {
+ status: 'done',
+ params: 22,
+ result: undefined,
+ },
+ ]) // setFx result
+
+ assert.strictEqual($store.getState(), 22) // <- changed
+ assert.strictEqual(watch.mock.callCount(), 2)
+ assert.deepEqual(watch.mock.calls[1].arguments, [22])
+
+ //
+ ;($store as any).setState(33)
+
+ assert.strictEqual($store.getState(), 33)
+ assert.strictEqual(watch.mock.callCount(), 3)
+ assert.deepEqual(watch.mock.calls[2].arguments, [33])
+
+ assert.strictEqual(watchFx.mock.callCount(), 4)
+ assert.deepEqual(watchFx.mock.calls[2].arguments, [undefined]) // getFx trigger
+ assert.deepEqual(watchFx.mock.calls[3].arguments, [
+ {
+ status: 'done',
+ params: undefined,
+ result: 33,
+ },
+ ]) // getFx result
+
+ assert.strictEqual(await getFx(), 33)
+})
+
+test('should sync with `persist` for the same adapter-key when removing value', async () => {
+ const watch = mock.fn()
+ const watchFx = mock.fn()
+
+ const storageArea = new Map()
+ const adapter = memory({ area: storageArea, def: 0 })
+ storageArea.set('test-sync-same-key-3', -123)
+
+ const $store = createStore(0)
+ $store.watch(watch)
+
+ assert.strictEqual($store.getState(), 0)
+ assert.strictEqual(watch.mock.callCount(), 1)
+ assert.deepEqual(watch.mock.calls[0].arguments, [0])
+
+ persist({
+ store: $store,
+ adapter,
+ key: 'test-sync-same-key-3',
+ })
+
+ assert.strictEqual($store.getState(), -123) // got from storage
+ assert.strictEqual(watch.mock.callCount(), 2) // store got updated from storage
+
+ const { removeFx } = createStorage({
+ adapter,
+ key: 'test-sync-same-key-3',
+ })
+
+ removeFx.watch(watchFx)
+ removeFx.finally.watch(watchFx)
+
+ assert.strictEqual(watchFx.mock.callCount(), 0) // did not trigger
+
+ removeFx()
+
+ assert.strictEqual(watchFx.mock.callCount(), 2)
+ assert.deepEqual(watchFx.mock.calls[0].arguments, [undefined]) // removeFx trigger
+ assert.deepEqual(watchFx.mock.calls[1].arguments, [
+ {
+ status: 'done',
+ params: undefined,
+ result: undefined,
+ },
+ ]) // removeFx result
+
+ assert.strictEqual($store.getState(), 0) // <- changed to default state
+ assert.strictEqual(watch.mock.callCount(), 3)
+ assert.deepEqual(watch.mock.calls[2].arguments, [0])
+})
+
+test('should handle synchronous error in `get` and `set` effects', () => {
+ const watch = mock.fn()
+
+ const { getFx, setFx } = createStorage('test-sync-throw', {
+ adapter: () => ({
+ get: () => {
+ throw 'get test error'
+ },
+ set: () => {
+ throw 'set test error'
+ },
+ }),
+ })
+
+ getFx.watch(watch)
+ setFx.watch(watch)
+ getFx.finally.watch(watch)
+ setFx.finally.watch(watch)
+
+ assert.strictEqual(watch.mock.callCount(), 0)
+
+ getFx()
+
+ assert.strictEqual(watch.mock.callCount(), 2)
+ assert.deepEqual(watch.mock.calls[0].arguments, [undefined]) // getFx trigger
+ assert.deepEqual(watch.mock.calls[1].arguments, [
+ {
+ status: 'fail',
+ params: undefined,
+ error: {
+ key: 'test-sync-throw',
+ keyPrefix: '',
+ operation: 'get',
+ error: 'get test error',
+ value: undefined,
+ },
+ },
+ ]) // getFx fail
+
+ setFx(1)
+
+ assert.strictEqual(watch.mock.callCount(), 4)
+ assert.deepEqual(watch.mock.calls[2].arguments, [1]) // setFx trigger
+ assert.deepEqual(watch.mock.calls[3].arguments, [
+ {
+ status: 'fail',
+ params: 1,
+ error: {
+ key: 'test-sync-throw',
+ keyPrefix: '',
+ operation: 'set',
+ error: 'set test error',
+ value: 1,
+ },
+ },
+ ]) // setFx fail
+})
+
+test('should handle asynchronous error in `get` and `set` effects', async () => {
+ const watch = mock.fn()
+
+ const { getFx, setFx } = createStorage('test-async-throw', {
+ adapter: () => ({
+ get: async () => Promise.reject('get test error'),
+ set: async () => Promise.reject('set test error'),
+ }),
+ })
+
+ getFx.watch(watch)
+ setFx.watch(watch)
+ getFx.finally.watch(watch)
+ setFx.finally.watch(watch)
+
+ assert.strictEqual(watch.mock.callCount(), 0)
+
+ try {
+ await getFx()
+ assert.fail('getFx should have thrown')
+ } catch (e) {
+ // ok
+ }
+
+ assert.strictEqual(watch.mock.callCount(), 2)
+ assert.deepEqual(watch.mock.calls[0].arguments, [undefined]) // getFx trigger
+ assert.deepEqual(watch.mock.calls[1].arguments, [
+ {
+ status: 'fail',
+ params: undefined,
+ error: {
+ key: 'test-async-throw',
+ keyPrefix: '',
+ operation: 'get',
+ error: 'get test error',
+ value: undefined,
+ },
+ },
+ ]) // getFx fail
+
+ try {
+ await setFx(1)
+ assert.fail('setFx should have thrown')
+ } catch (e) {
+ // ok
+ }
+
+ assert.strictEqual(watch.mock.callCount(), 4)
+ assert.deepEqual(watch.mock.calls[2].arguments, [1]) // setFx trigger
+ assert.deepEqual(watch.mock.calls[3].arguments, [
+ {
+ status: 'fail',
+ params: 1,
+ error: {
+ key: 'test-async-throw',
+ keyPrefix: '',
+ operation: 'set',
+ error: 'set test error',
+ value: 1,
+ },
+ },
+ ]) // setFx fail
+})
+
+test('should hide internal implementation with `get` effect', () => {
+ const watch = mock.fn()
+ const fail = createEvent()
+
+ const { getFx, setFx } = createStorage('test-throw-box', {
+ adapter: (_, update) => {
+ fail.watch(() => {
+ update(() => {
+ throw 'get box test error'
+ })
+ })
+
+ return {
+ get: (box?: () => any) => {
+ if (box) return box()
+ },
+ set: () => {},
+ }
+ },
+ })
+
+ getFx.watch(watch)
+ setFx.watch(watch)
+ getFx.finally.watch(watch)
+ setFx.finally.watch(watch)
+
+ assert.strictEqual(watch.mock.callCount(), 0)
+
+ fail()
+
+ assert.strictEqual(watch.mock.callCount(), 2)
+
+ const arg1 = watch.mock.calls[0].arguments[0]
+ assert.ok(arg1 instanceof Function) // getFx trigger - "box"ed error, don't know how to hide it here
+
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ const { params, ...arg2 } = watch.mock.calls[1].arguments[0]
+ assert.deepEqual(arg2, {
+ status: 'fail',
+ // params: Function, // "box"ed error...
+ error: {
+ key: 'test-throw-box',
+ keyPrefix: '',
+ operation: 'get',
+ error: 'get box test error',
+ value: undefined,
+ },
+ }) // getFx fail
+})
diff --git a/tests/index.types.ts b/tests/index.types.ts
index b36eeb6..b6adbed 100644
--- a/tests/index.types.ts
+++ b/tests/index.types.ts
@@ -34,6 +34,24 @@ test('General `persist` should handle wrong arguments', async () => {
persist({ adapter: fakeAdapter, target: store })
})
+test('General `createStorage` should handle wrong arguments', async () => {
+ const { createStorage } = await import('../src')
+
+ const fakeAdapter: StorageAdapter = 0 as any
+
+ // @ts-expect-error missing arguments
+ createStorage()
+
+ // @ts-expect-error missing adapter
+ createStorage('key')
+
+ // @ts-expect-error missing adapter
+ createStrorage({ key: 'key' })
+
+ // @ts-expect-error missing key
+ createStorage({ adapter: fakeAdapter })
+})
+
test('General `persist` should return Subscription', async () => {
const { persist } = await import('../src')
diff --git a/tests/local-create-storage.test.ts b/tests/local-create-storage.test.ts
new file mode 100644
index 0000000..4fd58a0
--- /dev/null
+++ b/tests/local-create-storage.test.ts
@@ -0,0 +1,106 @@
+import { test, before, after, mock } from 'node:test'
+import * as assert from 'node:assert/strict'
+import { createEffect, createStore } from 'effector'
+import { createStorageMock } from './mocks/storage.mock'
+import { type Events, createEventsMock } from './mocks/events.mock'
+import { persist, createStorage } from '../src/local'
+
+//
+// Mock `localStorage` and events
+//
+
+declare let global: any
+let events: Events
+
+before(() => {
+ global.localStorage = createStorageMock()
+ events = createEventsMock()
+ global.addEventListener = events.addEventListener
+})
+
+after(() => {
+ global.localStorage = undefined
+ global.addEventListener = undefined
+})
+
+//
+// Tests
+//
+
+test('should get and set values in localStorage', async () => {
+ const storage = createStorage('test-key-1')
+
+ assert.strictEqual(global.localStorage.getItem('test-key-1'), null)
+ assert.strictEqual(await storage.getFx(), undefined)
+
+ await storage.setFx(1)
+
+ assert.strictEqual(global.localStorage.getItem('test-key-1'), '1')
+ assert.strictEqual(await storage.getFx(), 1)
+})
+
+test('should be in sync with persisted store', async () => {
+ const storage = createStorage('test-key-2')
+
+ const $value = createStore(0)
+ persist({ store: $value, key: 'test-key-2' })
+
+ // this is expected, because store initial value is not written to localStorage
+ assert.strictEqual(await storage.getFx(), undefined)
+
+ // set value to localStorage using createStorage effect
+ await storage.setFx(1)
+
+ // storage and persisted store should be updated
+ assert.strictEqual(global.localStorage.getItem('test-key-2'), '1')
+ assert.strictEqual($value.getState(), 1)
+
+ const watch = mock.fn()
+ storage.getFx.doneData.watch(watch)
+
+ // set value to persisted store
+ ;($value as any).setState(2)
+ assert.strictEqual(global.localStorage.getItem('test-key-2'), '2')
+
+ // createStorage effect should be called once
+ assert.strictEqual(watch.mock.callCount(), 1)
+ assert.deepEqual(watch.mock.calls[0].arguments, [2])
+})
+
+test('createStorage get effect should be called on storage event', async () => {
+ const storage = createStorage('test-key-3')
+
+ const watch = mock.fn()
+ storage.getFx.doneData.watch(watch)
+
+ global.localStorage.setItem('test-key-3', '3')
+ await events.dispatchEvent('storage', {
+ storageArea: global.localStorage,
+ key: 'test-key-3',
+ oldValue: null,
+ newValue: '3',
+ })
+
+ // createStorage effect should be called once
+ assert.strictEqual(watch.mock.callCount(), 1)
+ assert.deepEqual(watch.mock.calls[0].arguments, [3])
+})
+
+test.only('persisted store should be restored to initial value on delete effect', async () => {
+ const storage = createStorage('test-key-4')
+ const $value = createStore(0)
+ persist({
+ store: $value,
+ key: 'test-key-4',
+ // def: 0,
+ finally: createEffect((_: any) => console.log('🔵', _)),
+ })
+ assert.strictEqual(global.localStorage.getItem('test-key-4'), null)
+ assert.strictEqual($value.getState(), 0)
+ await storage.setFx(1)
+ assert.strictEqual(global.localStorage.getItem('test-key-4'), '1')
+ assert.strictEqual($value.getState(), 1)
+ await storage.removeFx()
+ assert.strictEqual(global.localStorage.getItem('test-key-4'), null)
+ assert.strictEqual($value.getState(), 0)
+})
diff --git a/tests/local.test.ts b/tests/local.test.ts
index 0528923..d058cc6 100644
--- a/tests/local.test.ts
+++ b/tests/local.test.ts
@@ -3,7 +3,7 @@ import * as assert from 'node:assert/strict'
import { createEvent, createStore } from 'effector'
import { createStorageMock } from './mocks/storage.mock'
import { type Events, createEventsMock } from './mocks/events.mock'
-import { local, persist } from '../src/local'
+import { local, persist, createStorage } from '../src/local'
import { local as localIndex } from '../src'
//
@@ -31,6 +31,7 @@ after(() => {
test('should export adapter and `persist` function', () => {
assert.ok(typeof local === 'function')
assert.ok(typeof persist === 'function')
+ assert.ok(typeof createStorage === 'function')
})
test('should be exported from package root', () => {
@@ -40,6 +41,8 @@ test('should be exported from package root', () => {
test('should be ok on good parameters', () => {
const $store = createStore(0, { name: 'local::store' })
assert.doesNotThrow(() => persist({ store: $store }))
+ assert.doesNotThrow(() => createStorage('local::store'))
+ assert.doesNotThrow(() => createStorage({ key: 'local::store' }))
})
test('persisted store should reset value on init to default', async () => {
diff --git a/tests/memory.test.ts b/tests/memory.test.ts
index 31e496a..ea633f5 100644
--- a/tests/memory.test.ts
+++ b/tests/memory.test.ts
@@ -1,7 +1,7 @@
import { test, mock } from 'node:test'
import * as assert from 'node:assert/strict'
import { createStore } from 'effector'
-import { memory, persist } from '../src/memory'
+import { memory, persist, createStorage } from '../src/memory'
import { memory as memoryIndex } from '../src'
//
@@ -11,6 +11,7 @@ import { memory as memoryIndex } from '../src'
test('should export adapter and `persist` function', () => {
assert.ok(typeof memory === 'function')
assert.ok(typeof persist === 'function')
+ assert.ok(typeof createStorage === 'function')
})
test('should be exported from package root', () => {
@@ -20,6 +21,8 @@ test('should be exported from package root', () => {
test('should be ok on good parameters', () => {
const $store = createStore(0, { name: 'memory::store' })
assert.doesNotThrow(() => persist({ store: $store }))
+ assert.doesNotThrow(() => createStorage('memory::store'))
+ assert.doesNotThrow(() => createStorage({ key: 'memory::store' }))
})
test('should sync stores, persisted with memory adapter', () => {
diff --git a/tests/query.test.ts b/tests/query.test.ts
index 91a17df..525b539 100644
--- a/tests/query.test.ts
+++ b/tests/query.test.ts
@@ -7,6 +7,7 @@ import { type Events, createEventsMock } from './mocks/events.mock'
import {
persist,
query,
+ createStorage,
pushState,
replaceState,
locationAssign,
@@ -53,6 +54,7 @@ after(() => {
test('should export adapter and `persist` function', () => {
assert.ok(typeof query === 'function')
assert.ok(typeof persist === 'function')
+ assert.ok(typeof createStorage === 'function')
assert.ok(typeof pushState === 'function')
assert.ok(typeof replaceState === 'function')
assert.ok(typeof locationAssign === 'function')
@@ -66,6 +68,8 @@ test('should be exported from package root', () => {
test('should be ok on good parameters', () => {
const $store = createStore('0', { name: 'query::store' })
assert.doesNotThrow(() => persist({ store: $store }))
+ assert.doesNotThrow(() => createStorage('query::store'))
+ assert.doesNotThrow(() => createStorage({ key: 'query::store' }))
})
test('store initial value should NOT be put in query string', () => {
diff --git a/tests/session.test.ts b/tests/session.test.ts
index 21737fd..a382390 100644
--- a/tests/session.test.ts
+++ b/tests/session.test.ts
@@ -3,7 +3,7 @@ import * as assert from 'node:assert/strict'
import { createEvent, createStore } from 'effector'
import { createStorageMock } from './mocks/storage.mock'
import { type Events, createEventsMock } from './mocks/events.mock'
-import { session, persist } from '../src/session'
+import { session, persist, createStorage } from '../src/session'
import { session as sessionIndex } from '../src'
//
@@ -31,6 +31,7 @@ after(() => {
test('should export adapter and `persist` function', () => {
assert.ok(typeof session === 'function')
assert.ok(typeof persist === 'function')
+ assert.ok(typeof createStorage === 'function')
})
test('should be exported from package root', () => {
@@ -40,6 +41,8 @@ test('should be exported from package root', () => {
test('should be ok on good parameters', () => {
const $store = createStore(0, { name: 'session::store' })
assert.doesNotThrow(() => persist({ store: $store }))
+ assert.doesNotThrow(() => createStorage('session::store'))
+ assert.doesNotThrow(() => createStorage({ key: 'session::store' }))
})
test('persisted store should reset value on init to default', async () => {