diff --git a/docs/bun.lockb b/docs/bun.lockb index b59261c..d48e821 100755 Binary files a/docs/bun.lockb and b/docs/bun.lockb differ diff --git a/docs/src/content/docs/reference/synchronizer.mdx b/docs/src/content/docs/reference/synchronizer.mdx index 05710e7..ded6234 100644 --- a/docs/src/content/docs/reference/synchronizer.mdx +++ b/docs/src/content/docs/reference/synchronizer.mdx @@ -52,18 +52,35 @@ const { useStore } = createStore({ }) ``` -#### Custom serializer +#### Custom storage -If you want to store more complex objects that aren't supported by JSON you can either write your own storage synchronizer or pass custom `serialize` and `deserialize` functions to the options' parameter. For example, you can use [superjson](https://github.com/blitz-js/superjson) package: +If you want to customize storage, for example to use your own mmkv instance - you can use `createStorage` factory function. It creates a new storage function that you can use across your app. + +```ts +import { createStorage } from 'stan-js' +import { MMKV } from 'react-native-mmkv' + +const encryptedMMKV = new MMKV({ + encryptionKey: 'my-encryption-key' +}) + +const myStorage = createStorage({ + mmkvInstance: encryptedMMKV, +}) +``` + +Or if you want to store more complex objects that aren't supported by JSON you can pass custom `serialize` and `deserialize` functions. For example, you can use [superjson](https://github.com/blitz-js/superjson) package: ```typescript import { createStore } from 'stan-js' -import { storage } from 'stan-js/storage' +import { createStorage } from 'stan-js/storage' import superjson from 'superjson' +const superJsonStorage = createStorage({ + serialize: superjson.stringify, + deserialize: superjson.deserialize +}) + const { useStore } = createStore({ - user: storage(new Set(), { - serialize: superjson.stringify, - deserialize: superjson.deserialize - }) + user: superJsonStorage(new Set()) }) ``` diff --git a/package.json b/package.json index f64000c..7553539 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,7 @@ }, "./storage": { "module": "./dist/storage/index.mjs", - "react-native": "./dist/storage/mmkv.cjs", + "react-native": "./dist/storage/native.cjs", "browser": "./dist/storage/index.cjs", "import": { "types": "./dist/storage/index.d.mts", diff --git a/src/storage/factory.ts b/src/storage/factory.ts new file mode 100644 index 0000000..7e92c46 --- /dev/null +++ b/src/storage/factory.ts @@ -0,0 +1,96 @@ +import { CreateStorageOptions, Storage, StorageOptions, Synchronizer } from '../types' + +export const isLocalStorageAvailable = () => { + // for SSR + if (typeof window === 'undefined') { + return false + } + + try { + window.localStorage.length + + return true + } catch { + // if localStorage is disabled + return false + } +} +const mapStorage = new Map() + +const ssrSaveStorage = { + setItem: (key: string, value: string) => { + if (isLocalStorageAvailable()) { + localStorage.setItem(key, value) + + return + } + + mapStorage.set(key, value) + }, + getItem: (key: string) => { + if (isLocalStorageAvailable()) { + return localStorage.getItem(key) + } + + return mapStorage.get(key) + }, + removeItem: (key: string) => { + if (isLocalStorageAvailable()) { + localStorage.removeItem(key) + + return + } + + mapStorage.delete(key) + }, +} + +export const createStorage = (storageOptions?: CreateStorageOptions): Storage => +( + initialValue: T, + { + deserialize = storageOptions?.deserialize ?? JSON.parse, + serialize = storageOptions?.serialize ?? JSON.stringify, + storageKey, + }: StorageOptions = {}, +) => ({ + value: initialValue, + subscribe: (update, key) => { + const handleEvent = (event: StorageEvent) => { + if (event.storageArea !== localStorage) { + return + } + + if (event.key !== (storageKey ?? key) || event.newValue === null) { + return + } + + update(deserialize(event.newValue)) + } + + if (!isLocalStorageAvailable()) { + return + } + + window.addEventListener('storage', handleEvent) + }, + update: (value, key) => { + if (value === undefined) { + ssrSaveStorage.removeItem(storageKey ?? key) + + return + } + + ssrSaveStorage.setItem(storageKey ?? key, serialize(value)) + }, + getSnapshot: key => { + const value = ssrSaveStorage.getItem(storageKey ?? key) + + if (value === null || value === undefined) { + // Value is not in storage + throw new Error() + } + + return deserialize(value) + }, +} as Synchronizer) diff --git a/src/storage/index.ts b/src/storage/index.ts index 45b8304..62607db 100644 --- a/src/storage/index.ts +++ b/src/storage/index.ts @@ -1 +1,2 @@ +export { createStorage } from './factory' export { storage } from './storage' diff --git a/src/storage/mmkv.ts b/src/storage/mmkv.ts index 89bf698..3c2c0c7 100644 --- a/src/storage/mmkv.ts +++ b/src/storage/mmkv.ts @@ -1,36 +1,3 @@ -import { MMKV } from 'react-native-mmkv' -import { Storage, StorageOptions, Synchronizer } from '../types' +import { createStorage } from './mmkvFactory' -const mmkv = new MMKV() - -export const storage: Storage = ( - initialValue: T, - { - deserialize = JSON.parse, - serialize = JSON.stringify, - storageKey, - }: StorageOptions = {}, -) => ({ - value: initialValue, - update: (value, key) => { - const storageKeyToUse = storageKey ?? key - - if (value === undefined) { - mmkv.delete(storageKeyToUse) - - return - } - - mmkv.set(storageKeyToUse, serialize(value)) - }, - getSnapshot: key => { - const value = mmkv.getString(storageKey ?? key) - - if (value === undefined) { - // Value is not in storage - throw new Error() - } - - return deserialize(value) - }, -} as Synchronizer) +export const storage = createStorage() diff --git a/src/storage/mmkvFactory.ts b/src/storage/mmkvFactory.ts new file mode 100644 index 0000000..6582d3c --- /dev/null +++ b/src/storage/mmkvFactory.ts @@ -0,0 +1,39 @@ +import { MMKV } from 'react-native-mmkv' +import { CreateStorageOptions, StorageOptions, Synchronizer } from '../types' + +export const createStorage = (storageOptions?: CreateStorageOptions) => +( + initialValue: T, + { + deserialize = storageOptions?.deserialize ?? JSON.parse, + serialize = storageOptions?.serialize ?? JSON.stringify, + storageKey, + }: StorageOptions = {}, +) => { + const mmkv = storageOptions?.mmkvInstance ?? new MMKV() + + return { + value: initialValue, + update: (value, key) => { + const storageKeyToUse = storageKey ?? key + + if (value === undefined) { + mmkv.delete(storageKeyToUse) + + return + } + + mmkv.set(storageKeyToUse, serialize(value)) + }, + getSnapshot: key => { + const value = mmkv.getString(storageKey ?? key) + + if (value === undefined) { + // Value is not in storage + throw new Error() + } + + return deserialize(value) + }, + } as Synchronizer +} diff --git a/src/storage/native.ts b/src/storage/native.ts new file mode 100644 index 0000000..5d15aae --- /dev/null +++ b/src/storage/native.ts @@ -0,0 +1,2 @@ +export { storage } from './mmkv' +export { createStorage } from './mmkvFactory' diff --git a/src/storage/storage.ts b/src/storage/storage.ts index 1e9a290..e8e81e4 100644 --- a/src/storage/storage.ts +++ b/src/storage/storage.ts @@ -1,49 +1,4 @@ -import { Storage, StorageOptions, Synchronizer } from '../types' - -export const isLocalStorageAvailable = () => { - // for SSR - if (typeof window === 'undefined') { - return false - } - - try { - window.localStorage.length - - return true - } catch { - // if localStorage is disabled - return false - } -} -const mapStorage = new Map() - -const ssrSaveStorage = { - setItem: (key: string, value: string) => { - if (isLocalStorageAvailable()) { - localStorage.setItem(key, value) - - return - } - - mapStorage.set(key, value) - }, - getItem: (key: string) => { - if (isLocalStorageAvailable()) { - return localStorage.getItem(key) - } - - return mapStorage.get(key) - }, - removeItem: (key: string) => { - if (isLocalStorageAvailable()) { - localStorage.removeItem(key) - - return - } - - mapStorage.delete(key) - }, -} +import { createStorage } from './factory' /** * A Synchronizer implementation using localStorage and mmkv @@ -53,51 +8,4 @@ const ssrSaveStorage = { * @param options.storageKey - key to use in storage * @see {@link https://codemask-labs.github.io/stan-js/reference/synchronizer/} */ -export const storage: Storage = ( - initialValue: T, - { - deserialize = JSON.parse, - serialize = JSON.stringify, - storageKey, - }: StorageOptions = {}, -) => ({ - value: initialValue, - subscribe: (update, key) => { - const handleEvent = (event: StorageEvent) => { - if (event.storageArea !== localStorage) { - return - } - - if (event.key !== (storageKey ?? key) || event.newValue === null) { - return - } - - update(deserialize(event.newValue)) - } - - if (!isLocalStorageAvailable()) { - return - } - - window.addEventListener('storage', handleEvent) - }, - update: (value, key) => { - if (value === undefined) { - ssrSaveStorage.removeItem(storageKey ?? key) - - return - } - - ssrSaveStorage.setItem(storageKey ?? key, serialize(value)) - }, - getSnapshot: key => { - const value = ssrSaveStorage.getItem(storageKey ?? key) - - if (value === null || value === undefined) { - // Value is not in storage - throw new Error() - } - - return deserialize(value) - }, -} as Synchronizer) +export const storage = createStorage() diff --git a/src/tests/storage.test.ts b/src/tests/storage.test.ts index d095cf9..49faec9 100644 --- a/src/tests/storage.test.ts +++ b/src/tests/storage.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it, jest, Mock, spyOn } from 'bun:test' import { storage } from '../storage' -import { isLocalStorageAvailable } from '../storage/storage' +import { isLocalStorageAvailable } from '../storage/factory' import { Synchronizer } from '../types' const windowStub = spyOn(window, 'window') as Mock<() => undefined | Window> @@ -23,7 +23,7 @@ describe('isLocalStorageAvailable', () => { describe('storage', () => { it('should return an object with keys', () => { - const returnedValue = storage() + const returnedValue = storage(1) expect(returnedValue).toHaveProperty('value') expect(returnedValue).toHaveProperty('subscribe') diff --git a/src/types.ts b/src/types.ts index b796a97..6e31533 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,3 +1,5 @@ +import type { MMKV } from 'react-native-mmkv' + export type Synchronizer = { value: T subscribe?: (update: (value: T) => void, key: string) => void @@ -46,3 +48,9 @@ export type CustomActionsBuilderParams = { export type CustomActionsBuilder = ( builderParams: CustomActionsBuilderParams, ) => TCustomActions + +export type CreateStorageOptions = { + mmkvInstance?: MMKV + deserialize?: (value: string) => unknown + serialize?: (value: unknown) => string +}