Skip to content

Commit

Permalink
feat: add entries()
Browse files Browse the repository at this point in the history
  • Loading branch information
ph-fritsche committed Sep 3, 2021
1 parent 637991e commit 3ecafae
Show file tree
Hide file tree
Showing 19 changed files with 306 additions and 71 deletions.
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -256,3 +256,14 @@ Deletes some data from cache and IndexedDB.
const { clear } = useCached()
clear(({data, meta}) => meta.someMetaField > 2) // this will remove all entries with meta.someMetaField > 2
```

### Entries

You can get all entries cached in context or local DB:
```js
const { entries } = useCached()
entries().map([key, data] => {
// ...
})
```
The component will automatically rerender if an entry in the cache is updated/deleted.
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@
"idb-keyval": "^5.1.3"
},
"devDependencies": {
"@ph.fritsche/eslint-config": "^0.1.1",
"@ph.fritsche/eslint-config": "^1.0.0",
"@ph.fritsche/scripts-config": "^2.0.0",
"@testing-library/react": "^12.0.0",
"@types/debug": "^4.1.7",
Expand All @@ -65,7 +65,7 @@
"react-dom": "^17.0.2",
"shared-scripts": "^1.4.1",
"ts-jest": "^27.0.4",
"typescript": "^4.3.5"
"typescript": "^4.4.2"
},
"scripts": {
"build": "scripts ts-build",
Expand Down
4 changes: 2 additions & 2 deletions src/driver/IndexedDB.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { clear, createStore, del, entries, getMany, setMany } from 'idb-keyval';
import { DBDriver, DBKey, DBValue } from './abstract';
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)

Expand Down
2 changes: 1 addition & 1 deletion src/driver/NullDB.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { DBDriver, DBKey, DBValue } from './abstract';
import { DBDriver, DBKey, DBValue } from './abstract'

export default (): typeof NullDBDriver => NullDBDriver

Expand Down
45 changes: 25 additions & 20 deletions src/methods/clear.ts
Original file line number Diff line number Diff line change
@@ -1,33 +1,38 @@
import { DBDriver } from '../driver/abstract'
import { delProperty, dispatch, expire, reactCache, verifyEntry } from '../shared'
import { delProperty, dispatch, expire, Keys, reactCache, verifyEntry } from '../shared'

export async function clear(
cache: reactCache,
driver: DBDriver,
expire?: expire,
): Promise<void> {
await (expire
? driver.entries()
.then(entries => driver.setMany(
entries
.filter(([, obj]) => !verifyEntry({ obj }, expire))
.map(([key]) => [key, undefined]),
))
: driver.clear()
)
const updateKeys: Array<keyof reactCache> = []

const keys: string[] = []
Object.entries(cache).forEach(([key, entry]) => {
if (expire) {
if (!verifyEntry({ obj: entry.obj }, expire)) {
keys.push(key)
if (expire) {
const dbEntries = await driver.entries()
await driver.setMany(dbEntries
.filter(([, obj]) => !verifyEntry({ obj }, expire))
.map(([key]) => [key, undefined]),
)
for (const [key, entry] of Object.entries(cache)) {
if (entry.obj !== undefined && !verifyEntry(entry, expire)) {
updateKeys.push(key)
delProperty(cache, [key, 'obj'])
}
} else {
keys.push(key)
delProperty(cache, [key, 'obj'])
}
})
} else {
await driver.clear()

dispatch(cache, keys)
for (const [key, entry] of Object.entries(cache)) {
if (entry.obj !== undefined) {
updateKeys.push(key)
delProperty(cache, [key, 'obj'])
}
}
}

if (updateKeys.length) {
updateKeys.push(Keys)
dispatch(cache, updateKeys)
}
}
11 changes: 9 additions & 2 deletions src/methods/del.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,20 @@
import { DBDriver } from '../driver/abstract'
import { delProperty, dispatch, reactCache } from '../shared'
import { delProperty, dispatch, Keys, reactCache } from '../shared'

export async function del(
cache: reactCache,
driver: DBDriver,
key: string,
): Promise<void> {
const hasCacheKey = !!cache[key]?.obj

await driver.del(key)
delProperty(cache, [key, 'obj'])

if (hasCacheKey) {
delProperty(cache, [key, 'obj'])

dispatch(cache, [Keys])
}

dispatch(cache, [key])
}
68 changes: 68 additions & 0 deletions src/methods/entries.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { DBDriver } from '../driver/abstract'
import { addListener, debugLog, delProperty, dispatch, expire, Keys, reactCache, setProperty, verifyEntry } from '../shared'

export function entries<T = unknown>(
cache: reactCache,
driver: DBDriver,
id: string,
rerender: () => void,
expire?: expire,
): Array<[string, T]> {
addListener(cache, [Keys], id, rerender)

// addListener creates the entry if it does not exists
const keysEntry = cache[Keys] as NonNullable<reactCache[typeof Keys]>

const isFetchingOrValid = keysEntry.promise || verifyEntry(keysEntry, expire)
if (!isFetchingOrValid) {
debugLog('Get entries from idb')

const idbPromise: Promise<[string, unknown][]> = driver.entries().then(
(entries) => {
debugLog(' -> received entries from idb: %d', entries.length)

const dbKeys = new Set()
for (const [key, entry] of entries) {
/* istanbul ignore next */
if (typeof key !== 'string') {
continue
}

dbKeys.add(key)
setProperty(cache, [key, 'obj'], entry)
delProperty(cache, [key, 'isVolatile'])
}

for (const [key, entry] of Object.entries(cache)) {
if (!entry.isVolatile && !dbKeys.has(key)) {
delProperty(cache, [key, 'obj'])
}
}

keysEntry.obj = {data: null, meta: {date: new Date()}}
delete keysEntry.promise
dispatch(cache, [Keys])

return getDataEntries(cache)
},
)

keysEntry.promise = idbPromise
}

return getDataEntries(cache) as Array<[string, T]>
}

function getDataEntries(
cache: reactCache,
) {
const entries: [string, unknown][] = []

for (const [key, entry] of Object.entries(cache)) {
if (entry.obj) {
entries.push([key, entry.obj.data])
}
}

return entries
}
21 changes: 13 additions & 8 deletions src/methods/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { DBDriver } from '../driver/abstract';
import { cachedObj, expire, options, reactCache } from '../shared';
import { DBDriver } from '../driver/abstract'
import { cachedObj, expire, options, reactCache } from '../shared'
import { clear } from './clear'
import { del } from './del'
import { get, getReturn } from './get'
import { entries } from './entries'
import { set } from './set'

export type { getReturn }
Expand All @@ -15,6 +16,8 @@ declare function boundDel(
key: string,
): Promise<void>;

declare function boundEntries<T = unknown>(): Array<[string, T]>

declare function boundGet<
K extends Parameters<typeof get>[4],
R extends Parameters<typeof get>[7],
Expand All @@ -39,16 +42,18 @@ declare function boundSet(
): Promise<void>;

interface cachedApi {
clear: typeof boundClear,
del: typeof boundDel,
get: typeof boundGet,
set: typeof boundSet,
clear: typeof boundClear
del: typeof boundDel
entries: typeof boundEntries
get: typeof boundGet
set: typeof boundSet
}

export function createApi(cache: reactCache, driver: DBDriver, id: string, rerender: () => void): cachedApi {
return {
clear: clear.bind(undefined, cache, driver) as typeof boundClear,
del: del.bind(undefined, cache, driver) as typeof boundDel,
clear: clear.bind(undefined, cache, driver),
del: del.bind(undefined, cache, driver),
entries: entries.bind(undefined, cache, driver, id, rerender) as typeof boundEntries,
get: get.bind(undefined, cache, driver, id, rerender) as typeof boundGet,
set: set.bind(undefined, cache, driver) as typeof boundSet,
}
Expand Down
24 changes: 19 additions & 5 deletions src/methods/set.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { DBDriver } from '../driver/abstract'
import { cachedObj, delProperty, dispatch, options, reactCache, setProperty } from '../shared'
import { cachedObj, delProperty, dispatch, Keys, options, reactCache, setProperty } from '../shared'

export function set(
cache: reactCache,
Expand Down Expand Up @@ -71,13 +71,27 @@ async function _set(
await driver.setMany(entries)
}

entries.forEach(([key, obj]) => {
const updateKeys: (keyof reactCache)[] = []
for (const [key, obj] of entries) {

if (obj) {
updateKeys.push(key)

setProperty(cache, [key, 'obj'], obj)
} else {
if (options.skipIdb) {
cache[key].isVolatile = true
}

} else if(cache[key]?.obj) {
updateKeys.push(key)

delProperty(cache, [key, 'obj'])
delProperty(cache, [key, 'isVolatile'])
}
})
}

dispatch(cache, entries.map(([key]) => key))
if (updateKeys.length) {
updateKeys.push(Keys)
dispatch(cache, updateKeys)
}
}
27 changes: 19 additions & 8 deletions src/shared/cache.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,28 @@
export interface reactCacheEntry<T extends unknown = unknown> {
promise?: Promise<cachedObj<T> | undefined>,
obj?: cachedObj<T>,
listeners?: Record<string, () => void>,
promise?: Promise<cachedObj<T> | undefined>
obj?: cachedObj<T>
listeners?: Record<string | symbol, () => void>
/** only set in react cache - not received from or written to db */
isVolatile?: boolean
}

export type reactCache = Record<string, reactCacheEntry>
export const Keys = Symbol('keys')
export type Keys = Set<string>

export type reactCache = Record<string, reactCacheEntry> & {
[Keys]?: {
promise?: Promise<[string, unknown][]>
obj?: cachedObj<null>
listeners?: Record<string | symbol, () => void>
}
}

export type expire = number | ((obj: cachedObj) => boolean)

export type cachedObj<T extends unknown = unknown> = {
data: T,
data: T
meta: {
date?: Date | string,
[k: string]: unknown,
},
date?: Date | string
[k: string]: unknown
}
}
10 changes: 5 additions & 5 deletions src/shared/object.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
export function setProperty(obj: Record<string, unknown>, keys: string[], value: unknown): void {
export function setProperty(obj: Record<PropertyKey, unknown>, keys: PropertyKey[], value: unknown): void {
for(
let o = obj, i = 0;
i < keys.length;
o = o[keys[i]] as Record<string, unknown>, i++
o = o[keys[i]] as Record<PropertyKey, unknown>, i++
) {
if (o !== undefined && !(o instanceof Object) || Array.isArray(o)) {
throw `Unexpected type at $obj.${keys.join('.')}`
Expand All @@ -15,13 +15,13 @@ export function setProperty(obj: Record<string, unknown>, keys: string[], value:
}
}

export function delProperty(obj: Record<string, unknown>, keys: string[]): void {
const objects: Record<string, unknown>[] = []
export function delProperty(obj: Record<PropertyKey, unknown>, keys: PropertyKey[]): void {
const objects: Record<PropertyKey, unknown>[] = []
let o, i
for(
o = obj, i = 0;
i < keys.length;
o = o[keys[i]] as Record<string, unknown>, i++
o = o[keys[i]] as Record<PropertyKey, unknown>, i++
) {
if (o !== undefined && !(o instanceof Object) || Array.isArray(o)) {
throw `Unexpected type at $obj.${keys.join('.')}`
Expand Down
4 changes: 2 additions & 2 deletions src/shared/verifyEntry.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { expire, reactCacheEntry } from './cache'
import { cachedObj, expire } from './cache'

export function verifyEntry(entry: reactCacheEntry | undefined, expire: expire | undefined): boolean {
export function verifyEntry(entry: {obj?: cachedObj} | undefined, expire: expire | undefined): boolean {
if (!entry?.obj) {
return false
}
Expand Down
2 changes: 1 addition & 1 deletion src/useCached.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export function useCached({dbName = 'Cached', storeName = 'keyval', context = tr
}
const cache = context ? contextCache[dbName][storeName] : componentCache

const subscribedKeys = useRef<string[]>([])
const subscribedKeys = useRef<(keyof reactCache)[]>([])
const id = useRef(Math.random().toString(36)).current
const [, _setState] = useState({})
const rerender = useCallback(() => _setState({}), [_setState])
Expand Down
6 changes: 3 additions & 3 deletions test/methods/_.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export async function setupApi(
dbDriverFactory?: DBDriverFactory
idbObjects?: Record<string, cachedObj>,
idbValues?: Record<string, unknown>,
listen?: string[],
listen?: (keyof reactCache)[],
} = {},
): Promise<{
cache: reactCache,
Expand All @@ -35,10 +35,10 @@ export async function setupApi(
cache[key] = entry
})
Object.entries(cacheObjects ?? {}).forEach(([key, obj]) => {
cache[key] = {obj}
cache[key] = {obj, isVolatile: true}
})
Object.entries(cacheValues ?? {}).forEach(([key, data]) => {
cache[key] = {obj: { data, meta: { date: new Date('2001-02-03T04:05:06') } }}
cache[key] = {obj: { data, meta: { date: new Date('2001-02-03T04:05:06') } }, isVolatile: true}
})

const driver = dbDriverFactory('test', 'teststore')
Expand Down
Loading

0 comments on commit 3ecafae

Please sign in to comment.