Add idb-keyval backed archival storage

This commit is contained in:
Eric Bailey
2026-01-25 18:08:12 -06:00
parent 282b5e8460
commit 2886816c1e
8 changed files with 158 additions and 0 deletions
+1
View File
@@ -171,6 +171,7 @@
"fast-text-encoding": "^1.0.6",
"history": "^5.3.0",
"hls.js": "^1.6.2",
"idb-keyval": "^6.2.2",
"js-sha256": "^0.9.0",
"jwt-decode": "^4.0.0",
"lande": "^1.0.10",
+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,
}
}
+3
View File
@@ -0,0 +1,3 @@
export type Device = {
test?: boolean
}
+9
View File
@@ -36,6 +36,7 @@ import {NoAccessScreen} from '#/ageAssurance/components/NoAccessScreen'
import {RedirectOverlay} from '#/ageAssurance/components/RedirectOverlay'
import {PassiveAnalytics} from '#/analytics/PassiveAnalytics'
import {FlatNavigator, RoutesContainer} from '#/Navigation'
import {deviceArchive} from '#/storage/archive'
import {Composer} from './Composer.web'
import {DrawerContent} from './Drawer'
@@ -49,6 +50,14 @@ function ShellInner() {
useIntentHandler()
useEffect(() => {
deviceArchive
.set(['test'], true)
.then(() => {
console.log('stored')
})
.catch(e => {
console.error(e)
})
const unsubscribe = navigator.addListener('state', () => {
closeAllActiveElements()
})
+5
View File
@@ -13093,6 +13093,11 @@ icss-utils@^5.0.0, icss-utils@^5.1.0:
resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-5.1.0.tgz#c6be6858abd013d768e98366ae47e25d5887b1ae"
integrity sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==
idb-keyval@^6.2.2:
version "6.2.2"
resolved "https://registry.yarnpkg.com/idb-keyval/-/idb-keyval-6.2.2.tgz#b0171b5f73944854a3291a5cdba8e12768c4854a"
integrity sha512-yjD9nARJ/jb1g+CvD0tlhUHOrJ9Sy0P8T9MF3YaLlHnSRpwPfpTX0XIvpmw3gAJUmEu3FiICLBDPXVwyEvrleg==
ieee754@^1.1.13, ieee754@^1.1.4, ieee754@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"