Skip to content

Commit 930a02e

Browse files
committed
feat: rerender on cache changes
Components that (try to) get a value from cache now rerender on changes to that entry by any loader/component.
1 parent ef93302 commit 930a02e

File tree

19 files changed

+355
-96
lines changed

19 files changed

+355
-96
lines changed

src/methods/clear.ts

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { entries, setMany, clear as idbClear } from 'idb-keyval'
2-
import { expire, reactCache, verifyEntry } from '../shared'
2+
import { delProperty, dispatch, expire, reactCache, verifyEntry } from '../shared'
33

44
export async function clear(
55
cache: reactCache,
@@ -17,11 +17,18 @@ export async function clear(
1717
: idbClear(store)
1818
)
1919

20-
return Object.entries(cache).forEach(([key, entry]) => {
21-
if (entry.promise) {
22-
delete entry.obj
20+
const keys: string[] = []
21+
Object.entries(cache).forEach(([key, entry]) => {
22+
if (expire) {
23+
if (!verifyEntry({ obj: entry.obj }, expire)) {
24+
keys.push(key)
25+
delProperty(cache, [key, 'obj'])
26+
}
2327
} else {
24-
delete cache[key]
28+
keys.push(key)
29+
delProperty(cache, [key, 'obj'])
2530
}
2631
})
32+
33+
dispatch(cache, keys)
2734
}

src/methods/del.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
import { del as idbDel } from 'idb-keyval'
2-
import { reactCache } from '../shared'
2+
import { delProperty, dispatch, reactCache } from '../shared'
33

44
export async function del(
55
cache: reactCache,
66
store: Parameters<typeof idbDel>[1],
77
key: string,
88
): Promise<void> {
99
await idbDel(key, store)
10-
delete cache[key]
10+
delProperty(cache, [key, 'obj'])
11+
12+
dispatch(cache, [key])
1113
}

src/methods/get.ts

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { getMany } from 'idb-keyval'
2-
import { cachedObj, debugLog, expire, reactCache, verifyEntry } from '../shared'
2+
import { addListener, cachedObj, debugLog, dispatch, expire, reactCache, setProperty, verifyEntry } from '../shared'
33

44
type valueTypes = {
55
data: cachedObj['data'],
@@ -22,6 +22,7 @@ export function get<
2222
>(
2323
cache: reactCache,
2424
store: Parameters<typeof getMany>[1],
25+
id: string,
2526
rerender: () => void,
2627
keyOrKeys: K,
2728
loader?: (missingKeys: string[]) => Promise<void>,
@@ -34,6 +35,7 @@ export function get<
3435
>(
3536
cache: reactCache,
3637
store: Parameters<typeof getMany>[1],
38+
id: string,
3739
rerender: () => void,
3840
keyOrKeys: K,
3941
loader?: (missingKeys: string[]) => Promise<void>,
@@ -47,6 +49,7 @@ export function get<
4749
>(
4850
cache: reactCache,
4951
store: Parameters<typeof getMany>[1],
52+
id: string,
5053
rerender: () => void,
5154
keyOrKeys: K,
5255
loader?: (missingKeys: string[]) => Promise<void>,
@@ -55,24 +58,25 @@ export function get<
5558
): getReturn<K, T> {
5659
const keys = (Array.isArray(keyOrKeys) ? keyOrKeys : [keyOrKeys]) as string[]
5760

61+
addListener(cache, keys, id, rerender)
62+
5863
const values: Record<string, getValue<T>> = {}
5964
const missing: string[] = []
6065

6166
keys.forEach(key => {
62-
if(!verifyEntry(cache[key], expire)) {
67+
if(!cache[key].promise && !verifyEntry(cache[key], expire)) {
6368
missing.push(key)
6469
}
6570
if (returnType === 'obj') {
66-
values[key] = cache[key]?.obj as getValue<T>
71+
values[key] = cache[key].obj as getValue<T>
6772
} else {
68-
values[key] = cache[key]?.obj?.data as getValue<T>
73+
values[key] = cache[key].obj?.data as getValue<T>
6974
}
7075
})
7176

7277
if (missing.length) {
7378
debugLog('Get from idb: %s', missing.join(', '))
7479

75-
let hit = false
7680
const idbPromise = getMany(missing, store).then(
7781
obj => {
7882
debugLog.enabled && debugLog(
@@ -81,15 +85,18 @@ export function get<
8185
)
8286

8387
const stillMissing: string[] = []
88+
const hit: string[] = []
8489
obj.forEach((obj, i) => {
8590
if (verifyEntry({ obj }, expire)) {
86-
hit = true
87-
cache[ missing[i] ] = { obj }
91+
setProperty(cache, [missing[i], 'obj'], obj)
92+
hit.push(missing[i])
8893
} else {
8994
stillMissing.push(missing[i])
9095
}
9196
})
9297

98+
dispatch(cache, hit)
99+
93100
const loaderPromise = typeof loader === 'function' ? loader(stillMissing) : undefined
94101

95102
if (loaderPromise) {
@@ -106,11 +113,8 @@ export function get<
106113
},
107114
)
108115

109-
idbPromise.then(() => hit && rerender())
110-
111116
missing.forEach((key, i) => {
112-
cache[key] = cache[key] ?? {}
113-
cache[key].promise = idbPromise.then(values => values[i])
117+
setProperty(cache, [key, 'promise'], idbPromise.then(values => values[i]))
114118
})
115119
}
116120

src/methods/index.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,12 @@ declare function boundDel(
1414
): Promise<void>;
1515

1616
declare function boundGet<
17-
K extends Parameters<typeof get>[3],
18-
T extends Parameters<typeof get>[6],
17+
K extends Parameters<typeof get>[4],
18+
T extends Parameters<typeof get>[7],
1919
>(
2020
keyOrKeys: K,
21-
loader?: Parameters<typeof get>[4],
22-
expire?: Parameters<typeof get>[5],
21+
loader?: Parameters<typeof get>[5],
22+
expire?: Parameters<typeof get>[6],
2323
returnType?: T,
2424
): getReturn<K, T>;
2525

@@ -42,11 +42,11 @@ interface cachedApi {
4242
set: typeof boundSet,
4343
}
4444

45-
export function createApi(cache: reactCache, store: ReturnType<typeof createStore>, rerender: () => void): cachedApi {
45+
export function createApi(cache: reactCache, store: ReturnType<typeof createStore>, id: string, rerender: () => void): cachedApi {
4646
return {
4747
clear: clear.bind(undefined, cache, store) as typeof boundClear,
4848
del: del.bind(undefined, cache, store) as typeof boundDel,
49-
get: get.bind(undefined, cache, store, rerender) as typeof boundGet,
50-
set: set.bind(undefined, cache, store, rerender) as typeof boundSet,
49+
get: get.bind(undefined, cache, store, id, rerender) as typeof boundGet,
50+
set: set.bind(undefined, cache, store) as typeof boundSet,
5151
}
5252
}

src/methods/set.ts

Lines changed: 4 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,16 @@
11
import { setMany } from 'idb-keyval'
2-
import { cachedObj, options, reactCache } from '../shared'
2+
import { cachedObj, delProperty, dispatch, options, reactCache, setProperty } from '../shared'
33

44
export function set(
55
cache: reactCache,
66
store: Parameters<typeof setMany>[1],
7-
rerender: () => void,
87
recordData: Record<string, cachedObj['data']>,
98
recordMeta?: Record<string, cachedObj['meta'] | null>,
109
options?: options,
1110
): Promise<void>;
1211
export function set(
1312
cache: reactCache,
1413
store: Parameters<typeof setMany>[1],
15-
rerender: () => void,
1614
key: string,
1715
data: cachedObj['data'],
1816
meta?: cachedObj['meta'],
@@ -21,22 +19,19 @@ export function set(
2119
export async function set(
2220
cache: reactCache,
2321
store: Parameters<typeof setMany>[1],
24-
rerender: () => void,
2522
...args: unknown[]
2623
): Promise<void> {
2724
if (typeof args[0] == 'string') {
2825
return _set(
2926
cache,
3027
store,
31-
rerender,
3228
{ [args[0]]: { data: args[1], meta: args[2] } } as Record<string, cachedObj>,
3329
args[3] as options,
3430
)
3531
} else {
3632
return _set(
3733
cache,
3834
store,
39-
rerender,
4035
Object.assign({}, ...Object
4136
.entries(args[0] as Record<string, cachedObj['data']>)
4237
.map(([key, data]) => {
@@ -55,7 +50,6 @@ export async function set(
5550
async function _set(
5651
cache: reactCache,
5752
store: Parameters<typeof setMany>[1],
58-
rerender: () => void,
5953
record: Record<string, { data: cachedObj['data'], meta?: cachedObj['meta'] } | undefined>,
6054
options: options = {},
6155
): Promise<void> {
@@ -79,14 +73,11 @@ async function _set(
7973

8074
entries.forEach(([key, obj]) => {
8175
if (obj) {
82-
cache[key] = cache[key] ?? {}
83-
cache[key].obj = obj
84-
} else if(cache[key]?.promise) {
85-
delete cache[key].obj
76+
setProperty(cache, [key, 'obj'], obj)
8677
} else {
87-
delete cache[key]
78+
delProperty(cache, [key, 'obj'])
8879
}
8980
})
9081

91-
rerender()
82+
dispatch(cache, entries.map(([key]) => key))
9283
}

src/shared/cache.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
export interface reactCacheEntry {
22
promise?: Promise<cachedObj | undefined>,
33
obj?: cachedObj,
4+
listeners?: Record<string, () => void>,
45
}
56

67
export type reactCache = Record<string, reactCacheEntry>

src/shared/event.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { reactCache, reactCacheEntry } from './cache'
2+
import { delProperty, setProperty } from './object'
3+
4+
export function addListener(
5+
cache: reactCache,
6+
keys: (keyof reactCache)[],
7+
id: string,
8+
listener: () => void,
9+
): void {
10+
keys.forEach(key => {
11+
setProperty(cache, [key, 'listeners', id], listener)
12+
})
13+
}
14+
15+
export function removeListener(
16+
cache: reactCache,
17+
id: string,
18+
keys?: (keyof reactCache)[],
19+
): void {
20+
(keys ?? Object.keys(cache)).forEach(key => {
21+
delProperty(cache, [key, 'listeners', id])
22+
})
23+
}
24+
25+
export function dispatch(
26+
cache: reactCache,
27+
keys: (keyof reactCache)[],
28+
): void {
29+
const listeners: reactCacheEntry['listeners'] = {}
30+
keys.forEach(key => {
31+
Object.entries(cache[key]?.listeners ?? {}).forEach(([id, listener]) => {
32+
listeners[id] = listener
33+
})
34+
})
35+
36+
Object.values(listeners).forEach(listener => listener())
37+
}

src/shared/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
export * from './cache'
22
export * from './debug'
3+
export * from './event'
4+
export * from './object'
35
export * from './options'
46
export * from './verifyEntry'

src/shared/object.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
export function setProperty(obj: Record<string, unknown>, keys: string[], value: unknown): void {
2+
for(
3+
let o = obj, i = 0;
4+
i < keys.length;
5+
o = o[keys[i]] as Record<string, unknown>, i++
6+
) {
7+
if (o !== undefined && !(o instanceof Object) || Array.isArray(o)) {
8+
throw `Unexpected type at $obj.${keys.join('.')}`
9+
}
10+
if (i < keys.length -1) {
11+
o[keys[i]] = o[keys[i]] ?? {}
12+
} else {
13+
o[keys[i]] = value
14+
}
15+
}
16+
}
17+
18+
export function delProperty(obj: Record<string, unknown>, keys: string[]): void {
19+
const objects: Record<string, unknown>[] = []
20+
let o, i
21+
for(
22+
o = obj, i = 0;
23+
i < keys.length;
24+
o = o[keys[i]] as Record<string, unknown>, i++
25+
) {
26+
if (o !== undefined && !(o instanceof Object) || Array.isArray(o)) {
27+
throw `Unexpected type at $obj.${keys.join('.')}`
28+
}
29+
objects.push(o)
30+
if (o[keys[i]] === undefined) {
31+
break
32+
}
33+
}
34+
for(
35+
i = objects.length - 1;
36+
i >= 0;
37+
i--
38+
) {
39+
if ((i === keys.length - 1) || objects[i+1] !== undefined && Object.keys(objects[i+1]).length === 0) {
40+
delete objects[i][keys[i]]
41+
} else {
42+
break
43+
}
44+
}
45+
}

src/shared/verifyEntry.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
import { expire, reactCacheEntry } from './cache'
22

33
export function verifyEntry(entry: reactCacheEntry | undefined, expire: expire | undefined): boolean {
4-
if (entry?.promise instanceof Promise) {
5-
return true
6-
} else if (!entry?.obj) {
4+
if (!entry?.obj) {
75
return false
86
}
97

0 commit comments

Comments
 (0)