Add idb-keyval backed archival storage (#9754)

* Add idb-keyval backed archival storage

* Cleanup

* Remove test
This commit is contained in:
Eric Bailey
2026-01-26 13:39:12 -06:00
committed by GitHub
parent 2ad84d7dfc
commit f26ac583e6
8 changed files with 157 additions and 1 deletions
+22
View File
@@ -0,0 +1,22 @@
import {MMKV} from '@bsky.app/react-native-mmkv'
import {type DB} from '#/storage/archive/db/types'
export function create({id}: {id: string}): DB {
const store = new MMKV({id})
return {
async get(key: string): Promise<string | undefined> {
return store.getString(key) ?? undefined
},
async set(key: string, value: string): Promise<void> {
store.set(key, value)
},
async delete(key: string): Promise<void> {
store.delete(key)
},
async clear(): Promise<void> {
store.clearAll()
},
}
}
+22
View File
@@ -0,0 +1,22 @@
import {clear, createStore, del, get, set} from 'idb-keyval'
import {type DB} from '#/storage/archive/db/types'
export function create({id}: {id: string}): DB {
const store = createStore(id, id)
return {
async get(key: string): Promise<string | undefined> {
return get(key, store) ?? undefined
},
async set(key: string, value: string): Promise<void> {
await set(key, value, store)
},
async delete(key: string): Promise<void> {
await del(key, store)
},
async clear(): Promise<void> {
await clear(store)
},
}
}
+6
View File
@@ -0,0 +1,6 @@
export type DB = {
get(key: string): Promise<string | undefined>
set(key: string, value: string): Promise<void>
delete(key: string): Promise<void>
clear(): Promise<void>
}
+90
View File
@@ -0,0 +1,90 @@
import {create} from '#/storage/archive/db'
import {type DB} from '#/storage/archive/db/types'
import {type Device} from '#/storage/archive/schema'
export * from '#/storage/archive/schema'
/**
* Generic archival storage class. DO NOT use this directly. Instead, use the
* exported `Archive` instances below.
*/
export class Archive<Scopes extends unknown[], Schema> {
protected sep = ':'
protected store: DB
constructor({id}: {id: string}) {
this.store = create({id})
}
/**
* Store a value in archival storage based on scopes and/or keys
*
* `set([key], value)`
* `set([scope, key], value)`
*/
async set<Key extends keyof Schema>(
scopes: [...Scopes, Key],
data: Schema[Key],
): Promise<void> {
// stored as `{ data: <value> }` structure to ease stringification
return this.store.set(scopes.join(this.sep), JSON.stringify({data}))
}
/**
* Get a value from archival storage based on scopes and/or keys
*
* `get([key])`
* `get([scope, key])`
*/
async get<Key extends keyof Schema>(
scopes: [...Scopes, Key],
): Promise<Schema[Key] | undefined> {
const res = await this.store.get(scopes.join(this.sep))
if (!res) return undefined
// parsed from storage structure `{ data: <value> }`
return JSON.parse(res).data
}
/**
* Remove a value from archival storage based on scopes and/or keys
*
* `remove([key])`
* `remove([scope, key])`
*/
async remove<Key extends keyof Schema>(scopes: [...Scopes, Key]) {
return this.store.delete(scopes.join(this.sep))
}
/**
* Remove many values from the same archival storage scope by keys
*
* `removeMany([], [key])`
* `removeMany([scope], [key])`
*/
async removeMany<Key extends keyof Schema>(scopes: [...Scopes], keys: Key[]) {
return Promise.all(keys.map(key => this.remove([...scopes, key])))
}
/**
* For debugging purposes
*/
async removeAll() {
return this.store.clear()
}
}
/**
* Device data that's specific to the device and does not vary based on account
*
* `device.set([key], true)`
*/
export const deviceArchive = new Archive<[], Device>({
id: 'bsky_archive_device',
})
if (__DEV__ && typeof window !== 'undefined') {
// @ts-expect-error - dev global
window.bsky_archive = {
deviceArchive,
}
}
+10
View File
@@ -0,0 +1,10 @@
/**
* Data that's specific to the device and does not vary based account.
* Values here should be optional (since they may not have been saved
* yet), hence the `Partial`.
*
* See `#/storage/schema.ts` for examples.
*/
export type Device = Partial<{
// add values here
}>
+1 -1
View File
@@ -2,7 +2,7 @@ import {type ID as PolicyUpdate202508} from '#/components/PolicyUpdateOverlay/up
import {type Geolocation} from '#/geolocation/types'
/**
* Device data that's specific to the device and does not vary based account
* Data that's specific to the device and does not vary based account
*/
export type Device = {
/**