Skip to content

Commit e40222c

Browse files
committed
feat: support multiple stores per database
1 parent 0ac2a53 commit e40222c

File tree

9 files changed

+199
-99
lines changed

9 files changed

+199
-99
lines changed

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
],
1818
"dependencies": {
1919
"debug": "^4.3.2",
20+
"idb": "^6.1.4",
2021
"idb-keyval": "^6.0.2"
2122
},
2223
"devDependencies": {
@@ -27,7 +28,7 @@
2728
"@types/jest": "^27.0.2",
2829
"@types/react": "^17.0.27",
2930
"eslint": "^7.32.0",
30-
"fake-indexeddb": "^3.1.3",
31+
"fake-indexeddb": "^3.1.4",
3132
"jest": "^27.2.5",
3233
"react": "^17.0.2",
3334
"react-dom": "^17.0.2",

src/driver/IndexedDB.ts

Lines changed: 81 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,95 @@
1-
import { clear, createStore, del, entries, getMany, setMany } from 'idb-keyval'
2-
import { DBDriver, DBKey, DBValue } from './abstract'
1+
import { openDB, IDBPDatabase } from 'idb'
2+
import { DBDriver, DBKey } from './abstract'
33

4-
export default (dbName: string, storeName: string): IndexedDBDriver => new IndexedDBDriver(dbName, storeName)
4+
export default function (
5+
dbName: string,
6+
storeName: string,
7+
): DBDriver {
8+
return new IndexedDB(() => getDBWithStore(dbName, storeName), storeName)
9+
}
10+
11+
const dbs: Record<string, Promise<IDBPDatabase>> = {}
12+
13+
export async function resetConnections(): Promise<void> {
14+
for(const [name, db] of Object.entries(dbs)) {
15+
(await db).close()
16+
delete dbs[name]
17+
}
18+
}
519

6-
class IndexedDBDriver implements DBDriver {
7-
constructor(dbName: string, storeName: string) {
8-
this.store = createStore(dbName, storeName)
20+
export async function getDBWithStore(
21+
dbName: string,
22+
storeName: string,
23+
): Promise<IDBPDatabase> {
24+
const db = await (dbs[dbName] ?? open(dbName, storeName))
25+
26+
if (!db.objectStoreNames.contains(storeName)) {
27+
return open(dbName, storeName, db.version + 1)
928
}
10-
store: ReturnType<typeof createStore>
1129

12-
clear(): Promise<void> {
13-
return clear(this.store)
30+
return db
31+
}
32+
33+
async function open(dbName: string, storeName: string, version: number | undefined = undefined) {
34+
if (dbName in dbs) {
35+
(await dbs[dbName]).close()
1436
}
37+
return dbs[dbName] = openDB(dbName, version, {
38+
upgrade(db) {
39+
db.createObjectStore(storeName)
40+
},
41+
})
42+
}
1543

16-
del(key: DBKey): Promise<void> {
17-
return del(key, this.store)
44+
export class IndexedDB {
45+
private getDB: () => Promise<IDBPDatabase>
46+
private storeName: string
47+
constructor(getDB: () => Promise<IDBPDatabase>, storeName: string) {
48+
this.getDB = getDB
49+
this.storeName = storeName
1850
}
1951

20-
entries<T extends unknown>(): Promise<[DBKey, NonNullable<DBValue<T>>][]> {
21-
return entries(this.store)
52+
async clear(): Promise<void> {
53+
return (await this.getDB()).clear(this.storeName)
2254
}
2355

24-
getMany<T extends unknown>(keys: DBKey[]): Promise<DBValue<T>[]> {
25-
return getMany(keys, this.store)
56+
async del(key: DBKey): Promise<void> {
57+
return (await this.getDB()).delete(this.storeName, key)
2658
}
2759

28-
setMany<T extends unknown>(entries: [DBKey, DBValue<T>][]): Promise<void> {
29-
return setMany(entries, this.store)
60+
async entries<T>(): Promise<Array<[IDBValidKey, T]>> {
61+
const items: Array<[IDBValidKey, T]> = []
62+
const transaction = (await this.getDB()).transaction(this.storeName, 'readonly')
63+
64+
let cursor = await transaction.store.openCursor()
65+
while(cursor) {
66+
items.push([cursor.key, cursor.value])
67+
cursor = await cursor.continue()
68+
}
69+
70+
await transaction.done
71+
return items
72+
}
73+
74+
async getMany<T>(keys: DBKey[]): Promise<Array<T>> {
75+
const transaction = (await this.getDB()).transaction(this.storeName, 'readonly')
76+
77+
const r = await Promise.all(keys.map(k => transaction.store.get(k)))
78+
79+
await transaction.done
80+
return r
81+
}
82+
83+
async setMany(entries: [DBKey, unknown][]): Promise<void> {
84+
const transaction = (await this.getDB()).transaction(this.storeName, 'readwrite')
85+
86+
await Promise.all<unknown>(entries.map(([key, value]) => (
87+
value !== undefined
88+
? transaction.store.put(value, key)
89+
: transaction.store.delete(key)
90+
)))
91+
92+
transaction.commit()
93+
return transaction.done
3094
}
3195
}

src/driver/IndexedKeyvalDB.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { clear, createStore, del, entries, getMany, setMany } from 'idb-keyval'
2+
import { DBDriver, DBKey, DBValue } from './abstract'
3+
4+
export default (dbName: string, storeName: string): IndexedDBDriver => new IndexedDBDriver(dbName, storeName)
5+
6+
class IndexedDBDriver implements DBDriver {
7+
constructor(dbName: string, storeName: string) {
8+
this.store = createStore(dbName, storeName)
9+
}
10+
store: ReturnType<typeof createStore>
11+
12+
clear(): Promise<void> {
13+
return clear(this.store)
14+
}
15+
16+
del(key: DBKey): Promise<void> {
17+
return del(key, this.store)
18+
}
19+
20+
entries<T extends unknown>(): Promise<[DBKey, NonNullable<DBValue<T>>][]> {
21+
return entries(this.store)
22+
}
23+
24+
getMany<T extends unknown>(keys: DBKey[]): Promise<DBValue<T>[]> {
25+
return getMany(keys, this.store)
26+
}
27+
28+
setMany<T extends unknown>(entries: [DBKey, DBValue<T>][]): Promise<void> {
29+
return setMany(entries, this.store)
30+
}
31+
}

test/_setup.js

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
1-
import fakeIndexDB from 'fake-indexeddb'
1+
import 'fake-indexeddb/auto'
2+
import FDBFactory from 'fake-indexeddb/lib/FDBFactory'
3+
import { resetConnections } from '../src/driver/IndexedDB'
24

3-
beforeEach(() => {
4-
global.indexedDB = fakeIndexDB
5+
beforeEach(async () => {
6+
global.indexedDB = new FDBFactory()
7+
await resetConnections()
58
})
69

710
jest.mock('debug', () => ({

test/driver/DB.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import IndexedDB from '../../src/driver/IndexedDB'
2+
import IndexedKeyvalDB from '../../src/driver/IndexedKeyvalDB'
3+
4+
test.each([
5+
IndexedDB,
6+
IndexedKeyvalDB,
7+
])('store and retrieve data', async (driverFactory) => {
8+
const driver = driverFactory('test', 'teststorage')
9+
10+
await expect(driver.setMany([
11+
['foo', { data: 'someValue', meta: {} }],
12+
['bar', { data: 'anotherValue', meta: {} }],
13+
])).resolves.toBe(undefined)
14+
15+
await expect(driver.getMany(['bar', 'foo'])).resolves.toEqual([
16+
{ data: 'anotherValue', meta: {} },
17+
{ data: 'someValue', meta: {} },
18+
])
19+
20+
await expect(driver.entries()).resolves.toEqual(expect.arrayContaining([
21+
['foo', { data: 'someValue', meta: {} }],
22+
['bar', { data: 'anotherValue', meta: {} }],
23+
]))
24+
25+
await expect(driver.del('foo')).resolves.toBe(undefined)
26+
27+
await expect(driver.getMany(['bar', 'foo'])).resolves.toEqual([
28+
{ data: 'anotherValue', meta: {} },
29+
undefined,
30+
])
31+
32+
await expect(driver.clear()).resolves.toBe(undefined)
33+
34+
await expect(driver.entries()).resolves.toEqual([])
35+
})

test/driver/IndexedDB.ts

Lines changed: 13 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,20 @@
1-
import { clear, createStore } from 'idb-keyval'
21
import IndexedDB from '../../src/driver/IndexedDB'
32

4-
const storeParams: Parameters<typeof createStore> = ['test', 'teststorage']
3+
test('use multiple stores', async () => {
4+
const driverA = IndexedDB('test', 'teststorageA')
5+
const driverB = IndexedDB('test', 'teststorageB')
56

6-
beforeEach(async () => {
7-
await clear(createStore(...storeParams))
8-
})
9-
10-
test('relay calls to idb-keyval', async () => {
11-
const driver = IndexedDB(...storeParams)
12-
13-
await expect(driver.setMany([
14-
['foo', { data: 'someValue', meta: {} }],
15-
['bar', { data: 'anotherValue', meta: {} }],
16-
])).resolves.toBe(undefined)
17-
18-
await expect(driver.getMany(['bar', 'foo'])).resolves.toEqual([
19-
{ data: 'anotherValue', meta: {} },
20-
{ data: 'someValue', meta: {} },
7+
await driverA.setMany([
8+
['key1', {data: 'value1', meta: {}}],
219
])
22-
23-
await expect(driver.entries()).resolves.toEqual(expect.arrayContaining([
24-
['foo', { data: 'someValue', meta: {} }],
25-
['bar', { data: 'anotherValue', meta: {} }],
26-
]))
27-
28-
await expect(driver.del('foo')).resolves.toBe(undefined)
29-
30-
await expect(driver.getMany(['bar', 'foo'])).resolves.toEqual([
31-
{ data: 'anotherValue', meta: {} },
32-
undefined,
10+
await driverB.setMany([
11+
['key2', {data: 'value2', meta: {}}],
3312
])
3413

35-
await expect(driver.clear()).resolves.toBe(undefined)
36-
37-
await expect(driver.entries()).resolves.toEqual([])
14+
await expect(driverA.entries()).resolves.toEqual([
15+
['key1', {data: 'value1', meta: {}}],
16+
])
17+
await expect(driverA.entries()).resolves.toEqual([
18+
['key1', {data: 'value1', meta: {}}],
19+
])
3820
})

test/methods/get.ts

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { setupApi } from './_'
22

3+
const wait = () => new Promise(r => setTimeout(r, 10))
4+
35
it('get value from cache', async () => {
46
const { rerender, api } = await setupApi({ cacheValues: { foo: 'bar' } })
57

@@ -133,7 +135,7 @@ it('call loader for expired (per callback) entries', async () => {
133135
expect(fuuPromise).toBeInstanceOf(Promise)
134136
expect(faaPromise).toBeInstanceOf(Promise)
135137

136-
await new Promise(r => setTimeout(r, 10))
138+
await wait()
137139

138140
expect(expire).toHaveBeenNthCalledWith(1, expect.objectContaining({ data: 'bar', meta: { date: new Date('2001-02-03T04:05:06')}}))
139141
expect(expire).toHaveBeenNthCalledWith(2, expect.objectContaining({ data: 'baz', meta: { date: new Date('2001-02-03T04:05:06')}}))
@@ -165,7 +167,7 @@ it('call loader for expired (per age) entries', async () => {
165167
expect(cache.foo?.promise).toBeInstanceOf(Promise)
166168
expect(cache.fuu?.promise).toBeInstanceOf(Promise)
167169

168-
await new Promise(r => setTimeout(r, 10))
170+
await wait()
169171

170172
expect(loader).toBeCalledWith(['foo', 'fuu'])
171173
})
@@ -176,18 +178,18 @@ it('skip fetching object when a promise is pending', async () => {
176178
const loader = jest.fn(() => new Promise<void>(r => { resolveLoader = r}))
177179

178180
expect(api.get('foo', loader)).toEqual(undefined)
179-
await new Promise(r => setTimeout(r, 2))
181+
await wait()
180182
expect(loader).toBeCalledTimes(1)
181183

182184
expect(api.get('foo', loader)).toEqual(undefined)
183-
await new Promise(r => setTimeout(r, 2))
185+
await wait()
184186
expect(loader).toBeCalledTimes(1)
185187

186188
resolveLoader()
187-
await new Promise(r => setTimeout(r, 2))
189+
await wait()
188190

189191
expect(api.get('foo', loader)).toEqual(undefined)
190-
await new Promise(r => setTimeout(r, 2))
192+
await wait()
191193
expect(loader).toBeCalledTimes(2)
192194
})
193195

@@ -201,13 +203,13 @@ it('remove promise when resolved', async () => {
201203
expect(cache.bar.promise).toBeInstanceOf(Promise)
202204
expect(cache.baz.promise).toBeInstanceOf(Promise)
203205

204-
await new Promise(r => setTimeout(r, 2))
206+
await wait()
205207

206208
expect(cache.bar.promise).toBe(undefined)
207209
expect(cache.baz.promise).toBeInstanceOf(Promise)
208210

209211
resolveLoader()
210-
await new Promise(r => setTimeout(r, 2))
212+
await wait()
211213

212214
expect(cache.foo.promise).toBe(undefined)
213215
expect(cache.bar.promise).toBe(undefined)

0 commit comments

Comments
 (0)