From 2886816c1ebe7c19529d8cd9bbef552a31a75a81 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Sun, 25 Jan 2026 18:08:12 -0600 Subject: [PATCH] Add idb-keyval backed archival storage --- package.json | 1 + src/storage/archive/db/index.ts | 22 +++++++ src/storage/archive/db/index.web.ts | 22 +++++++ src/storage/archive/db/types.ts | 6 ++ src/storage/archive/index.ts | 90 +++++++++++++++++++++++++++++ src/storage/archive/schema.ts | 3 + src/view/shell/index.web.tsx | 9 +++ yarn.lock | 5 ++ 8 files changed, 158 insertions(+) create mode 100644 src/storage/archive/db/index.ts create mode 100644 src/storage/archive/db/index.web.ts create mode 100644 src/storage/archive/db/types.ts create mode 100644 src/storage/archive/index.ts create mode 100644 src/storage/archive/schema.ts diff --git a/package.json b/package.json index 8ca89a08aa..e30bb09423 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/storage/archive/db/index.ts b/src/storage/archive/db/index.ts new file mode 100644 index 0000000000..02202a8d95 --- /dev/null +++ b/src/storage/archive/db/index.ts @@ -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 { + return store.getString(key) ?? undefined + }, + async set(key: string, value: string): Promise { + store.set(key, value) + }, + async delete(key: string): Promise { + store.delete(key) + }, + async clear(): Promise { + store.clearAll() + }, + } +} diff --git a/src/storage/archive/db/index.web.ts b/src/storage/archive/db/index.web.ts new file mode 100644 index 0000000000..f0f75633b9 --- /dev/null +++ b/src/storage/archive/db/index.web.ts @@ -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 { + return get(key, store) ?? undefined + }, + async set(key: string, value: string): Promise { + await set(key, value, store) + }, + async delete(key: string): Promise { + await del(key, store) + }, + async clear(): Promise { + await clear(store) + }, + } +} diff --git a/src/storage/archive/db/types.ts b/src/storage/archive/db/types.ts new file mode 100644 index 0000000000..4582dfab61 --- /dev/null +++ b/src/storage/archive/db/types.ts @@ -0,0 +1,6 @@ +export type DB = { + get(key: string): Promise + set(key: string, value: string): Promise + delete(key: string): Promise + clear(): Promise +} diff --git a/src/storage/archive/index.ts b/src/storage/archive/index.ts new file mode 100644 index 0000000000..97daa6107c --- /dev/null +++ b/src/storage/archive/index.ts @@ -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 { + 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( + scopes: [...Scopes, Key], + data: Schema[Key], + ): Promise { + // stored as `{ data: }` 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( + scopes: [...Scopes, Key], + ): Promise { + const res = await this.store.get(scopes.join(this.sep)) + if (!res) return undefined + // parsed from storage structure `{ data: }` + return JSON.parse(res).data + } + + /** + * Remove a value from archival storage based on scopes and/or keys + * + * `remove([key])` + * `remove([scope, key])` + */ + async remove(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(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, + } +} diff --git a/src/storage/archive/schema.ts b/src/storage/archive/schema.ts new file mode 100644 index 0000000000..dcd1e9fe7d --- /dev/null +++ b/src/storage/archive/schema.ts @@ -0,0 +1,3 @@ +export type Device = { + test?: boolean +} diff --git a/src/view/shell/index.web.tsx b/src/view/shell/index.web.tsx index 28fed7152d..1b0a13436b 100644 --- a/src/view/shell/index.web.tsx +++ b/src/view/shell/index.web.tsx @@ -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() }) diff --git a/yarn.lock b/yarn.lock index cd151dc15c..a2f9e62c0c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"