Skip to content

Commit

Permalink
feat: plug in db implementation for device cache
Browse files Browse the repository at this point in the history
  • Loading branch information
ph-fritsche committed Sep 1, 2021
1 parent c8f83cf commit 8cf5ca1
Show file tree
Hide file tree
Showing 20 changed files with 408 additions and 56 deletions.
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <CacheProvider dbDriverFactory={NullDB}>
<MyComponent/>
</CacheProvider>
}
```

> 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
Expand Down
29 changes: 29 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
17 changes: 0 additions & 17 deletions src/context.ts

This file was deleted.

53 changes: 53 additions & 0 deletions src/context.tsx
Original file line number Diff line number Diff line change
@@ -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<CacheContext>(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<CacheContext['cache']>({}).current
const context: CacheContext = useMemo(() => ({cache, dbDriverFactory}), [cache, dbDriverFactory])

return <CacheContext.Provider value={context}>{children}</CacheContext.Provider>
}
31 changes: 31 additions & 0 deletions src/driver/IndexedDB.ts
Original file line number Diff line number Diff line change
@@ -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<typeof createStore>

clear(): Promise<void> {
return clear(this.store)
}

del(key: DBKey): Promise<void> {
return del(key, this.store)
}

entries<T extends unknown>(): Promise<[DBKey, NonNullable<DBValue<T>>][]> {
return entries(this.store)
}

getMany<T extends unknown>(keys: DBKey[]): Promise<DBValue<T>[]> {
return getMany(keys, this.store)
}

setMany<T extends unknown>(entries: [DBKey, DBValue<T>][]): Promise<void> {
return setMany(entries, this.store)
}
}
21 changes: 21 additions & 0 deletions src/driver/NullDB.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { DBDriver, DBKey, DBValue } from './abstract';

export default (): typeof NullDBDriver => NullDBDriver

const NullDBDriver: DBDriver = {
async clear(): Promise<void> {
return
},
async del(): Promise<void> {
return
},
async entries<T extends unknown>(): Promise<[DBKey, NonNullable<DBValue<T>>][]> {
return []
},
async getMany<T extends unknown>(keys: DBKey[]): Promise<DBValue<T>[]> {
return keys.map(() => undefined)
},
async setMany(): Promise<void> {
return
},
}
26 changes: 26 additions & 0 deletions src/driver/abstract.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/* eslint-disable @typescript-eslint/no-explicit-any */

import { cachedObj } from '../shared'

export type DBKey = IDBValidKey
export type DBValue<T> = cachedObj<T> | undefined

/**
* @internal
*/
export interface DBDriverFactory {
(dbName: string, storeName: string): DBDriver
}

/**
* Driver for local/device/session cache.
*
* @internal
*/
export interface DBDriver {
clear(): Promise<void>
del(key: DBKey): Promise<void>
entries<T = any>(): Promise<[DBKey, NonNullable<DBValue<T>>][]>
getMany<T = any>(keys: DBKey[]): Promise<DBValue<T>[]>
setMany<T = any>(entries: [DBKey, DBValue<T>][]): Promise<void>
}
44 changes: 44 additions & 0 deletions src/driver/lazy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { DBDriver, DBDriverFactory, DBKey, DBValue } from './abstract'

export function lazyDBDriver(loader: () => Promise<DBDriverFactory|{default: DBDriverFactory}>): DBDriverFactory {
return (dbName, storeName) => new LazyDB(loader, dbName, storeName)
}

class LazyDB implements DBDriver {
constructor(
loader: () => Promise<DBDriverFactory|{default: DBDriverFactory}>,
dbName: string,
storeName: string,
) {
this.loader = loader
this.dbName = dbName
this.storeName = storeName
}
private loader: () => Promise<DBDriverFactory|{default: DBDriverFactory}>
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<void> {
return this.loadDriver().then(d => d.clear())
}
async del(key: DBKey): Promise<void> {
return this.loadDriver().then(d => d.del(key))
}
async entries<T extends unknown>(): Promise<[DBKey, NonNullable<DBValue<T>>][]> {
return this.loadDriver().then(d => d.entries())
}
async getMany<T extends unknown>(keys: DBKey[]): Promise<DBValue<T>[]> {
return this.loadDriver().then(d => d.getMany(keys))
}
async setMany<T extends unknown>(entries: [DBKey, DBValue<T>][]): Promise<void> {
return this.loadDriver().then(d => d.setMany(entries))
}
}
11 changes: 5 additions & 6 deletions src/methods/clear.ts
Original file line number Diff line number Diff line change
@@ -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<typeof idbClear>[0],
driver: DBDriver,
expire?: expire,
): Promise<void> {
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[] = []
Expand Down
6 changes: 3 additions & 3 deletions src/methods/del.ts
Original file line number Diff line number Diff line change
@@ -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<typeof idbDel>[1],
driver: DBDriver,
key: string,
): Promise<void> {
await idbDel(key, store)
await driver.del(key)
delProperty(cache, [key, 'obj'])

dispatch(cache, [key])
Expand Down
10 changes: 5 additions & 5 deletions src/methods/get.ts
Original file line number Diff line number Diff line change
@@ -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<T> = {
Expand All @@ -24,7 +24,7 @@ export function get<
T extends unknown = unknown,
>(
cache: reactCache,
store: Parameters<typeof getMany>[1],
driver: DBDriver,
id: string,
rerender: () => void,
keyOrKeys: K,
Expand All @@ -38,7 +38,7 @@ export function get<
T extends unknown = unknown,
>(
cache: reactCache,
store: Parameters<typeof getMany>[1],
driver: DBDriver,
id: string,
rerender: () => void,
keyOrKeys: K,
Expand All @@ -53,7 +53,7 @@ export function get<
T extends unknown = unknown,
>(
cache: reactCache,
store: Parameters<typeof getMany>[1],
driver: DBDriver,
id: string,
rerender: () => void,
keyOrKeys: K,
Expand Down Expand Up @@ -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',
Expand Down
12 changes: 6 additions & 6 deletions src/methods/index.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -45,11 +45,11 @@ interface cachedApi {
set: typeof boundSet,
}

export function createApi(cache: reactCache, store: ReturnType<typeof createStore>, 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,
}
}
Loading

0 comments on commit 8cf5ca1

Please sign in to comment.