Add idb-keyval backed archival storage (#9754)
* Add idb-keyval backed archival storage * Cleanup * Remove test
This commit is contained in:
@@ -171,6 +171,7 @@
|
|||||||
"fast-text-encoding": "^1.0.6",
|
"fast-text-encoding": "^1.0.6",
|
||||||
"history": "^5.3.0",
|
"history": "^5.3.0",
|
||||||
"hls.js": "^1.6.2",
|
"hls.js": "^1.6.2",
|
||||||
|
"idb-keyval": "^6.2.2",
|
||||||
"js-sha256": "^0.9.0",
|
"js-sha256": "^0.9.0",
|
||||||
"jwt-decode": "^4.0.0",
|
"jwt-decode": "^4.0.0",
|
||||||
"lande": "^1.0.10",
|
"lande": "^1.0.10",
|
||||||
|
|||||||
@@ -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()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}>
|
||||||
@@ -2,7 +2,7 @@ import {type ID as PolicyUpdate202508} from '#/components/PolicyUpdateOverlay/up
|
|||||||
import {type Geolocation} from '#/geolocation/types'
|
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 = {
|
export type Device = {
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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"
|
resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-5.1.0.tgz#c6be6858abd013d768e98366ae47e25d5887b1ae"
|
||||||
integrity sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==
|
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:
|
ieee754@^1.1.13, ieee754@^1.1.4, ieee754@^1.2.1:
|
||||||
version "1.2.1"
|
version "1.2.1"
|
||||||
resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"
|
resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"
|
||||||
|
|||||||
Reference in New Issue
Block a user