diff --git a/README.md b/README.md
index dd950be..8b80fba 100644
--- a/README.md
+++ b/README.md
@@ -71,6 +71,25 @@ You can also make a component use its own local cache:
const api = useCached({context: false})
```
+#### DBDriver
+
+This library defaults to use [IndexedDB](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API) as device level cache per `idb-keyval`.
+
+You can switch out the driver used for local cache in the context of a `CacheProvider`.
+
+```js
+import { CacheProvider } from 'react-idb-cache'
+import NullDB from 'react-idb-cache/driver/NullDB'
+
+function MyWrapperComponent() {
+ return
+
+
+}
+```
+
+> Supplying your own driver is possible but considered experimental as the `DBDriver` API might be subject to change in `minor` versions. If you'd like to add another local DB implementation, PRs are very much welcome!
+
### Get
#### Get a single value
diff --git a/package.json b/package.json
index 9d73419..4b707ba 100644
--- a/package.json
+++ b/package.json
@@ -18,6 +18,35 @@
"files": [
"/dist"
],
+ "exports": {
+ ".": {
+ "node": "./dist/cjs/index.js",
+ "import": "./dist/esm/index.js",
+ "default": "./dist/cjs/index.js"
+ },
+ "./dist/*": "./dist/*.js",
+ "./driver/*": {
+ "node": "./dist/cjs/driver/*.js",
+ "import": "./dist/esm/driver/*.js",
+ "default": "./dist/cjs/driver/*.js"
+ }
+ },
+ "typesVersions": {
+ "*": {
+ "dist/types/*": [
+ "./dist/types/*"
+ ],
+ "dist/cjs/*": [
+ "./dist/types/*.d.ts"
+ ],
+ "dist/esm/*": [
+ "./dist/types/*.d.ts"
+ ],
+ "*": [
+ "./dist/types/*.d.ts"
+ ]
+ }
+ },
"dependencies": {
"debug": "^4.3.2",
"idb-keyval": "^5.1.3"
diff --git a/src/context.ts b/src/context.ts
deleted file mode 100644
index a09bfad..0000000
--- a/src/context.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-import React, { useRef } from 'react'
-import { reactCache } from './shared'
-
-type globalCache = {
- [dbName: string]: {
- [storeName: string]: reactCache,
- },
-}
-
-export const CacheContext = React.createContext({})
-
-export function CacheProvider(
- {children}: React.PropsWithChildren,
-): React.ReactElement {
- const cache = useRef({}).current
- return React.createElement(CacheContext.Provider, {value: cache}, children)
-}
diff --git a/src/context.tsx b/src/context.tsx
new file mode 100644
index 0000000..6418882
--- /dev/null
+++ b/src/context.tsx
@@ -0,0 +1,53 @@
+import React, { useMemo, useRef } from 'react'
+import { reactCache } from './shared'
+import { DBDriverFactory } from './driver/abstract'
+import { lazyDBDriver } from './driver/lazy'
+
+/**
+ * @internal
+ */
+type CacheContext = {
+ cache: {
+ [dbName: string]: {
+ [storeName: string]: reactCache,
+ },
+ },
+ dbDriverFactory: DBDriverFactory,
+}
+
+const lazyIndexedDB = lazyDBDriver(() => import('./driver/IndexedDB'))
+
+export const globalContext: CacheContext = {
+ cache: {},
+ dbDriverFactory: lazyIndexedDB,
+}
+
+export const CacheContext = React.createContext(globalContext)
+
+/**
+ * Configure the cache used outside of CacheProvider.
+ * Be aware that changing the global config does not rerender any components.
+ */
+export function configureGlobalCache(
+ {
+ dbDriverFactory,
+ }: {
+ dbDriverFactory: DBDriverFactory,
+ },
+): void {
+ globalContext.dbDriverFactory = dbDriverFactory
+}
+
+export function CacheProvider(
+ {
+ dbDriverFactory = lazyIndexedDB,
+ children,
+ }: React.PropsWithChildren<{
+ dbDriverFactory?: DBDriverFactory
+ }>,
+): React.ReactElement {
+ const cache = useRef({}).current
+ const context: CacheContext = useMemo(() => ({cache, dbDriverFactory}), [cache, dbDriverFactory])
+
+ return {children}
+}
diff --git a/src/driver/IndexedDB.ts b/src/driver/IndexedDB.ts
new file mode 100644
index 0000000..7c3bea6
--- /dev/null
+++ b/src/driver/IndexedDB.ts
@@ -0,0 +1,31 @@
+import { clear, createStore, del, entries, getMany, setMany } from 'idb-keyval';
+import { DBDriver, DBKey, DBValue } from './abstract';
+
+export default (dbName: string, storeName: string): IndexedDBDriver => new IndexedDBDriver(dbName, storeName)
+
+class IndexedDBDriver implements DBDriver {
+ constructor(dbName: string, storeName: string) {
+ this.store = createStore(dbName, storeName)
+ }
+ store: ReturnType
+
+ clear(): Promise {
+ return clear(this.store)
+ }
+
+ del(key: DBKey): Promise {
+ return del(key, this.store)
+ }
+
+ entries(): Promise<[DBKey, NonNullable>][]> {
+ return entries(this.store)
+ }
+
+ getMany(keys: DBKey[]): Promise[]> {
+ return getMany(keys, this.store)
+ }
+
+ setMany(entries: [DBKey, DBValue][]): Promise {
+ return setMany(entries, this.store)
+ }
+}
diff --git a/src/driver/NullDB.ts b/src/driver/NullDB.ts
new file mode 100644
index 0000000..f8ec360
--- /dev/null
+++ b/src/driver/NullDB.ts
@@ -0,0 +1,21 @@
+import { DBDriver, DBKey, DBValue } from './abstract';
+
+export default (): typeof NullDBDriver => NullDBDriver
+
+const NullDBDriver: DBDriver = {
+ async clear(): Promise {
+ return
+ },
+ async del(): Promise {
+ return
+ },
+ async entries(): Promise<[DBKey, NonNullable>][]> {
+ return []
+ },
+ async getMany(keys: DBKey[]): Promise[]> {
+ return keys.map(() => undefined)
+ },
+ async setMany(): Promise {
+ return
+ },
+}
diff --git a/src/driver/abstract.ts b/src/driver/abstract.ts
new file mode 100644
index 0000000..ca6d51e
--- /dev/null
+++ b/src/driver/abstract.ts
@@ -0,0 +1,26 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+
+import { cachedObj } from '../shared'
+
+export type DBKey = IDBValidKey
+export type DBValue = cachedObj | undefined
+
+/**
+ * @internal
+ */
+export interface DBDriverFactory {
+ (dbName: string, storeName: string): DBDriver
+}
+
+/**
+ * Driver for local/device/session cache.
+ *
+ * @internal
+ */
+export interface DBDriver {
+ clear(): Promise
+ del(key: DBKey): Promise
+ entries(): Promise<[DBKey, NonNullable>][]>
+ getMany(keys: DBKey[]): Promise[]>
+ setMany(entries: [DBKey, DBValue][]): Promise
+}
diff --git a/src/driver/lazy.ts b/src/driver/lazy.ts
new file mode 100644
index 0000000..cb1db8a
--- /dev/null
+++ b/src/driver/lazy.ts
@@ -0,0 +1,44 @@
+import { DBDriver, DBDriverFactory, DBKey, DBValue } from './abstract'
+
+export function lazyDBDriver(loader: () => Promise): DBDriverFactory {
+ return (dbName, storeName) => new LazyDB(loader, dbName, storeName)
+}
+
+class LazyDB implements DBDriver {
+ constructor(
+ loader: () => Promise,
+ dbName: string,
+ storeName: string,
+ ) {
+ this.loader = loader
+ this.dbName = dbName
+ this.storeName = storeName
+ }
+ private loader: () => Promise
+ private driver?: DBDriver
+ private dbName: string
+ private storeName: string
+
+ private async loadDriver() {
+ return this.driver ?? this.loader().then(m => {
+ const factory = typeof m === 'function' ? m : m.default
+ return this.driver = factory(this.dbName, this.storeName)
+ })
+ }
+
+ async clear(): Promise {
+ return this.loadDriver().then(d => d.clear())
+ }
+ async del(key: DBKey): Promise {
+ return this.loadDriver().then(d => d.del(key))
+ }
+ async entries(): Promise<[DBKey, NonNullable>][]> {
+ return this.loadDriver().then(d => d.entries())
+ }
+ async getMany(keys: DBKey[]): Promise[]> {
+ return this.loadDriver().then(d => d.getMany(keys))
+ }
+ async setMany(entries: [DBKey, DBValue][]): Promise {
+ return this.loadDriver().then(d => d.setMany(entries))
+ }
+}
diff --git a/src/methods/clear.ts b/src/methods/clear.ts
index 52716d4..97cf3bb 100644
--- a/src/methods/clear.ts
+++ b/src/methods/clear.ts
@@ -1,20 +1,19 @@
-import { entries, setMany, clear as idbClear } from 'idb-keyval'
+import { DBDriver } from '../driver/abstract'
import { delProperty, dispatch, expire, reactCache, verifyEntry } from '../shared'
export async function clear(
cache: reactCache,
- store: Parameters[0],
+ driver: DBDriver,
expire?: expire,
): Promise {
await (expire
- ? entries(store)
- .then(entries => setMany(
+ ? driver.entries()
+ .then(entries => driver.setMany(
entries
.filter(([, obj]) => !verifyEntry({ obj }, expire))
.map(([key]) => [key, undefined]),
- store,
))
- : idbClear(store)
+ : driver.clear()
)
const keys: string[] = []
diff --git a/src/methods/del.ts b/src/methods/del.ts
index 51f960c..3ad41bd 100644
--- a/src/methods/del.ts
+++ b/src/methods/del.ts
@@ -1,12 +1,12 @@
-import { del as idbDel } from 'idb-keyval'
+import { DBDriver } from '../driver/abstract'
import { delProperty, dispatch, reactCache } from '../shared'
export async function del(
cache: reactCache,
- store: Parameters[1],
+ driver: DBDriver,
key: string,
): Promise {
- await idbDel(key, store)
+ await driver.del(key)
delProperty(cache, [key, 'obj'])
dispatch(cache, [key])
diff --git a/src/methods/get.ts b/src/methods/get.ts
index dab0c20..248b7ec 100644
--- a/src/methods/get.ts
+++ b/src/methods/get.ts
@@ -1,4 +1,4 @@
-import { getMany } from 'idb-keyval'
+import { DBDriver } from '../driver/abstract'
import { addListener, cachedObj, debugLog, dispatch, expire, reactCache, setProperty, verifyEntry } from '../shared'
type returnTypes = {
@@ -24,7 +24,7 @@ export function get<
T extends unknown = unknown,
>(
cache: reactCache,
- store: Parameters[1],
+ driver: DBDriver,
id: string,
rerender: () => void,
keyOrKeys: K,
@@ -38,7 +38,7 @@ export function get<
T extends unknown = unknown,
>(
cache: reactCache,
- store: Parameters[1],
+ driver: DBDriver,
id: string,
rerender: () => void,
keyOrKeys: K,
@@ -53,7 +53,7 @@ export function get<
T extends unknown = unknown,
>(
cache: reactCache,
- store: Parameters[1],
+ driver: DBDriver,
id: string,
rerender: () => void,
keyOrKeys: K,
@@ -83,7 +83,7 @@ export function get<
if (missing.length) {
debugLog('Get from idb: %s', missing.join(', '))
- const idbPromise = getMany(missing, store).then(
+ const idbPromise = driver.getMany(missing).then(
obj => {
debugLog.enabled && debugLog(
' -> received from idb:\n%s',
diff --git a/src/methods/index.ts b/src/methods/index.ts
index 7bd2e24..d55e174 100644
--- a/src/methods/index.ts
+++ b/src/methods/index.ts
@@ -1,4 +1,4 @@
-import { createStore } from 'idb-keyval';
+import { DBDriver } from '../driver/abstract';
import { cachedObj, expire, options, reactCache } from '../shared';
import { clear } from './clear'
import { del } from './del'
@@ -45,11 +45,11 @@ interface cachedApi {
set: typeof boundSet,
}
-export function createApi(cache: reactCache, store: ReturnType, id: string, rerender: () => void): cachedApi {
+export function createApi(cache: reactCache, driver: DBDriver, id: string, rerender: () => void): cachedApi {
return {
- clear: clear.bind(undefined, cache, store) as typeof boundClear,
- del: del.bind(undefined, cache, store) as typeof boundDel,
- get: get.bind(undefined, cache, store, id, rerender) as typeof boundGet,
- set: set.bind(undefined, cache, store) as typeof boundSet,
+ clear: clear.bind(undefined, cache, driver) as typeof boundClear,
+ del: del.bind(undefined, cache, driver) as typeof boundDel,
+ get: get.bind(undefined, cache, driver, id, rerender) as typeof boundGet,
+ set: set.bind(undefined, cache, driver) as typeof boundSet,
}
}
diff --git a/src/methods/set.ts b/src/methods/set.ts
index 5e0c55a..51dbd9e 100644
--- a/src/methods/set.ts
+++ b/src/methods/set.ts
@@ -1,16 +1,16 @@
-import { setMany } from 'idb-keyval'
+import { DBDriver } from '../driver/abstract'
import { cachedObj, delProperty, dispatch, options, reactCache, setProperty } from '../shared'
export function set(
cache: reactCache,
- store: Parameters[1],
+ driver: DBDriver,
recordData: Record,
recordMeta?: Record,
options?: options,
): Promise;
export function set(
cache: reactCache,
- store: Parameters[1],
+ driver: DBDriver,
key: string,
data: cachedObj['data'],
meta?: cachedObj['meta'],
@@ -18,20 +18,20 @@ export function set(
): Promise;
export async function set(
cache: reactCache,
- store: Parameters[1],
+ driver: DBDriver,
...args: unknown[]
): Promise {
if (typeof args[0] == 'string') {
return _set(
cache,
- store,
+ driver,
{ [args[0]]: { data: args[1], meta: args[2] } } as Record,
args[3] as options,
)
} else {
return _set(
cache,
- store,
+ driver,
Object.assign({}, ...Object
.entries(args[0] as Record)
.map(([key, data]) => {
@@ -49,7 +49,7 @@ export async function set(
async function _set(
cache: reactCache,
- store: Parameters[1],
+ driver: DBDriver,
record: Record,
options: options = {},
): Promise {
@@ -68,7 +68,7 @@ async function _set(
})
if (!options.skipIdb) {
- await setMany(entries, store)
+ await driver.setMany(entries)
}
entries.forEach(([key, obj]) => {
diff --git a/src/useCached.ts b/src/useCached.ts
index 1bc29cd..cb55c51 100644
--- a/src/useCached.ts
+++ b/src/useCached.ts
@@ -1,5 +1,4 @@
import { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'
-import { createStore } from 'idb-keyval'
import { addListener, reactCache, removeListener } from './shared'
import { createApi } from './methods'
import { CacheContext } from './context'
@@ -9,10 +8,8 @@ export function useCached({dbName = 'Cached', storeName = 'keyval', context = tr
storeName?: string,
context?: boolean,
} = {}): ReturnType {
- const store = useRef(createStore(dbName, storeName)).current
-
+ const { cache: contextCache, dbDriverFactory } = useContext(CacheContext)
const componentCache = useRef({}).current
- const contextCache = useContext(CacheContext)
if (context) {
if (!contextCache[dbName]) {
@@ -36,7 +33,10 @@ export function useCached({dbName = 'Cached', storeName = 'keyval', context = tr
}
})
- const api = useMemo(() => createApi(cache, store, id, rerender), [cache, store, id, rerender])
+ const api = useMemo(
+ () => createApi(cache, dbDriverFactory(dbName, storeName), id, rerender),
+ [cache, dbDriverFactory, dbName, storeName, id, rerender],
+ )
return api
}
diff --git a/test/context.tsx b/test/context.tsx
index 9e7e476..b526d2b 100644
--- a/test/context.tsx
+++ b/test/context.tsx
@@ -1,10 +1,20 @@
import { render } from '@testing-library/react'
import React, { useContext, useEffect } from 'react'
-import { CacheContext, CacheProvider } from '../src/context'
+import { CacheContext, CacheProvider, configureGlobalCache, globalContext } from '../src/context'
+import NullDB from '../src/driver/NullDB'
+
+let defaultContext: typeof globalContext
+beforeAll(() => {
+ defaultContext = {...globalContext}
+})
+afterEach(() => {
+ globalContext.cache = {}
+ globalContext.dbDriverFactory = defaultContext.dbDriverFactory
+})
it('provide cache context', async () => {
function TestComponentA() {
- const cache = useContext(CacheContext)
+ const {cache} = useContext(CacheContext)
useEffect(() => {
cache['foo'] = {}
cache['foo']['bar'] = {}
@@ -15,7 +25,7 @@ it('provide cache context', async () => {
let valueB = undefined
function TestComponentB() {
- const cache = useContext(CacheContext)
+ const {cache} = useContext(CacheContext)
useEffect(() => {
valueB = cache.foo?.bar?.baz?.obj?.data
})
@@ -24,7 +34,7 @@ it('provide cache context', async () => {
let valueC = undefined
function TestComponentC() {
- const cache = useContext(CacheContext)
+ const {cache} = useContext(CacheContext)
useEffect(() => {
valueC = cache.foo?.bar?.baz?.obj?.data
})
@@ -44,3 +54,35 @@ it('provide cache context', async () => {
expect(valueB).toBe('value')
expect(valueC).toBe(undefined)
})
+
+it('provide dbDriverFactory', () => {
+ let contextFactory = undefined
+ function TestComponent() {
+ const {dbDriverFactory} = useContext(CacheContext)
+ contextFactory = dbDriverFactory
+ return null
+ }
+
+ render(<>
+
+
+
+ >)
+
+ expect(contextFactory).toBe(NullDB)
+})
+
+it('configure global dbDriverFactory', () => {
+ let contextFactory = undefined
+ function TestComponent() {
+ const { dbDriverFactory } = useContext(CacheContext)
+ contextFactory = dbDriverFactory
+ return null
+ }
+
+ configureGlobalCache({dbDriverFactory: NullDB})
+
+ render()
+
+ expect(contextFactory).toBe(NullDB)
+})
diff --git a/test/driver/IndexedDB.ts b/test/driver/IndexedDB.ts
new file mode 100644
index 0000000..61b14a0
--- /dev/null
+++ b/test/driver/IndexedDB.ts
@@ -0,0 +1,38 @@
+import { clear, createStore } from 'idb-keyval'
+import IndexedDB from '../../src/driver/IndexedDB'
+
+const storeParams: Parameters = ['test', 'teststorage']
+
+beforeEach(async () => {
+ await clear(createStore(...storeParams))
+})
+
+test('relay calls to idb-keyval', async () => {
+ const driver = IndexedDB(...storeParams)
+
+ await expect(driver.setMany([
+ ['foo', { data: 'someValue', meta: {} }],
+ ['bar', { data: 'anotherValue', meta: {} }],
+ ])).resolves.toBe(undefined)
+
+ await expect(driver.getMany(['bar', 'foo'])).resolves.toEqual([
+ { data: 'anotherValue', meta: {} },
+ { data: 'someValue', meta: {} },
+ ])
+
+ await expect(driver.entries()).resolves.toEqual(expect.arrayContaining([
+ ['foo', { data: 'someValue', meta: {} }],
+ ['bar', { data: 'anotherValue', meta: {} }],
+ ]))
+
+ await expect(driver.del('foo')).resolves.toBe(undefined)
+
+ await expect(driver.getMany(['bar', 'foo'])).resolves.toEqual([
+ { data: 'anotherValue', meta: {} },
+ undefined,
+ ])
+
+ await expect(driver.clear()).resolves.toBe(undefined)
+
+ await expect(driver.entries()).resolves.toEqual([])
+})
diff --git a/test/driver/NullDB.ts b/test/driver/NullDB.ts
new file mode 100644
index 0000000..5a3dece
--- /dev/null
+++ b/test/driver/NullDB.ts
@@ -0,0 +1,15 @@
+import NullDB from '../../src/driver/NullDB'
+
+test('provide DBDriver api without storage', async () => {
+ const driver = NullDB()
+
+ await expect(driver.setMany([
+ ['foo', {data: 'someValue', meta: {}}],
+ ['bar', {data: 'anotherValue', meta: {}}],
+ ])).resolves.toBe(undefined)
+ await expect(driver.getMany(['foo', 'bar'])).resolves.toEqual([undefined, undefined])
+ await expect(driver.entries()).resolves.toEqual([])
+
+ await expect(driver.del('foo')).resolves.toBe(undefined)
+ await expect(driver.clear()).resolves.toBe(undefined)
+})
diff --git a/test/driver/lazy.ts b/test/driver/lazy.ts
new file mode 100644
index 0000000..29b7711
--- /dev/null
+++ b/test/driver/lazy.ts
@@ -0,0 +1,51 @@
+import { cachedObj } from '../../src'
+import { DBDriver } from '../../src/driver/abstract'
+import { lazyDBDriver } from '../../src/driver/lazy'
+
+
+test('load driver and relay calls', async () => {
+ const driver: DBDriver = {
+ clear: jest.fn(),
+ del: jest.fn(),
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ entries: jest.fn(async (): Promise<[string, cachedObj][]> => [
+ ['foo', { data: 'bar', meta: {} }],
+ ]),
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ getMany: jest.fn(async (): Promise => [
+ { data: 123, meta: {} },
+ ]),
+ setMany: jest.fn(),
+ }
+ const loader = jest.fn(async () => (() => ({...driver})))
+ const lazyFactory = lazyDBDriver(loader)
+ const LazyDB = lazyFactory('test', 'null')
+
+ expect(loader).not.toBeCalled()
+
+ await expect(LazyDB.clear()).resolves.toBe(undefined)
+ expect(driver.clear).toBeCalledTimes(1)
+
+ expect(loader).toBeCalledTimes(1)
+
+ await expect(LazyDB.del('foo')).resolves.toBe(undefined)
+ expect(driver.del).toBeCalledTimes(1)
+ expect(driver.del).toBeCalledWith('foo')
+
+ await expect(LazyDB.entries()).resolves.toEqual([
+ ['foo', { data: 'bar', meta: {} }],
+ ])
+ expect(driver.entries).toBeCalledTimes(1)
+
+ await expect(LazyDB.getMany(['whatever'])).resolves.toEqual([
+ { data: 123, meta: {} },
+ ])
+ expect(driver.getMany).toBeCalledTimes(1)
+ expect(driver.getMany).toBeCalledWith(['whatever'])
+
+ await expect(LazyDB.setMany([['foo', {data: 456, meta: {}}]])).resolves.toBe(undefined)
+ expect(driver.setMany).toBeCalledTimes(1)
+ expect(driver.setMany).toBeCalledWith([['foo', { data: 456, meta: {} }]])
+
+ expect(loader).toBeCalledTimes(1)
+})
diff --git a/test/methods/_.ts b/test/methods/_.ts
index 5a5caac..fc4f5f9 100644
--- a/test/methods/_.ts
+++ b/test/methods/_.ts
@@ -1,4 +1,5 @@
import { clear, createStore, setMany } from 'idb-keyval';
+import IndexedDB from '../../src/driver/IndexedDB';
import { createApi } from '../../src/methods';
import { addListener, cachedObj, reactCache, reactCacheEntry } from '../../src/shared';
@@ -49,7 +50,7 @@ export async function setupApi({cache: reactCache, cacheEntries, cacheObjects, c
addListener(cache, listen, listenerId, listener)
}
- const api = createApi(cache, store, 'testComponent', rerender)
+ const api = createApi(cache, IndexedDB('test', 'teststore'), 'testComponent', rerender)
return {
cache: cache as reactCache,
diff --git a/test/useCached.tsx b/test/useCached.tsx
index e8d779b..e1da844 100644
--- a/test/useCached.tsx
+++ b/test/useCached.tsx
@@ -29,7 +29,7 @@ async function setup(
await clear(store)
}
function ClearComponent() {
- const cache = useContext(CacheContext)
+ const {cache} = useContext(CacheContext)
Object.keys(cache).forEach(k => { delete cache[k] })
return null
}