Bluesky app fork with some witchin' additions 💫
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Add idb-keyval backed archival storage (#9754)

* Add idb-keyval backed archival storage

* Cleanup

* Remove test

authored by

Eric Bailey and committed by
GitHub
f26ac583 2ad84d7d

+157 -1
+1
package.json
··· 171 171 "fast-text-encoding": "^1.0.6", 172 172 "history": "^5.3.0", 173 173 "hls.js": "^1.6.2", 174 + "idb-keyval": "^6.2.2", 174 175 "js-sha256": "^0.9.0", 175 176 "jwt-decode": "^4.0.0", 176 177 "lande": "^1.0.10",
+22
src/storage/archive/db/index.ts
··· 1 + import {MMKV} from '@bsky.app/react-native-mmkv' 2 + 3 + import {type DB} from '#/storage/archive/db/types' 4 + 5 + export function create({id}: {id: string}): DB { 6 + const store = new MMKV({id}) 7 + 8 + return { 9 + async get(key: string): Promise<string | undefined> { 10 + return store.getString(key) ?? undefined 11 + }, 12 + async set(key: string, value: string): Promise<void> { 13 + store.set(key, value) 14 + }, 15 + async delete(key: string): Promise<void> { 16 + store.delete(key) 17 + }, 18 + async clear(): Promise<void> { 19 + store.clearAll() 20 + }, 21 + } 22 + }
+22
src/storage/archive/db/index.web.ts
··· 1 + import {clear, createStore, del, get, set} from 'idb-keyval' 2 + 3 + import {type DB} from '#/storage/archive/db/types' 4 + 5 + export function create({id}: {id: string}): DB { 6 + const store = createStore(id, id) 7 + 8 + return { 9 + async get(key: string): Promise<string | undefined> { 10 + return get(key, store) ?? undefined 11 + }, 12 + async set(key: string, value: string): Promise<void> { 13 + await set(key, value, store) 14 + }, 15 + async delete(key: string): Promise<void> { 16 + await del(key, store) 17 + }, 18 + async clear(): Promise<void> { 19 + await clear(store) 20 + }, 21 + } 22 + }
+6
src/storage/archive/db/types.ts
··· 1 + export type DB = { 2 + get(key: string): Promise<string | undefined> 3 + set(key: string, value: string): Promise<void> 4 + delete(key: string): Promise<void> 5 + clear(): Promise<void> 6 + }
+90
src/storage/archive/index.ts
··· 1 + import {create} from '#/storage/archive/db' 2 + import {type DB} from '#/storage/archive/db/types' 3 + import {type Device} from '#/storage/archive/schema' 4 + 5 + export * from '#/storage/archive/schema' 6 + 7 + /** 8 + * Generic archival storage class. DO NOT use this directly. Instead, use the 9 + * exported `Archive` instances below. 10 + */ 11 + export class Archive<Scopes extends unknown[], Schema> { 12 + protected sep = ':' 13 + protected store: DB 14 + 15 + constructor({id}: {id: string}) { 16 + this.store = create({id}) 17 + } 18 + 19 + /** 20 + * Store a value in archival storage based on scopes and/or keys 21 + * 22 + * `set([key], value)` 23 + * `set([scope, key], value)` 24 + */ 25 + async set<Key extends keyof Schema>( 26 + scopes: [...Scopes, Key], 27 + data: Schema[Key], 28 + ): Promise<void> { 29 + // stored as `{ data: <value> }` structure to ease stringification 30 + return this.store.set(scopes.join(this.sep), JSON.stringify({data})) 31 + } 32 + 33 + /** 34 + * Get a value from archival storage based on scopes and/or keys 35 + * 36 + * `get([key])` 37 + * `get([scope, key])` 38 + */ 39 + async get<Key extends keyof Schema>( 40 + scopes: [...Scopes, Key], 41 + ): Promise<Schema[Key] | undefined> { 42 + const res = await this.store.get(scopes.join(this.sep)) 43 + if (!res) return undefined 44 + // parsed from storage structure `{ data: <value> }` 45 + return JSON.parse(res).data 46 + } 47 + 48 + /** 49 + * Remove a value from archival storage based on scopes and/or keys 50 + * 51 + * `remove([key])` 52 + * `remove([scope, key])` 53 + */ 54 + async remove<Key extends keyof Schema>(scopes: [...Scopes, Key]) { 55 + return this.store.delete(scopes.join(this.sep)) 56 + } 57 + 58 + /** 59 + * Remove many values from the same archival storage scope by keys 60 + * 61 + * `removeMany([], [key])` 62 + * `removeMany([scope], [key])` 63 + */ 64 + async removeMany<Key extends keyof Schema>(scopes: [...Scopes], keys: Key[]) { 65 + return Promise.all(keys.map(key => this.remove([...scopes, key]))) 66 + } 67 + 68 + /** 69 + * For debugging purposes 70 + */ 71 + async removeAll() { 72 + return this.store.clear() 73 + } 74 + } 75 + 76 + /** 77 + * Device data that's specific to the device and does not vary based on account 78 + * 79 + * `device.set([key], true)` 80 + */ 81 + export const deviceArchive = new Archive<[], Device>({ 82 + id: 'bsky_archive_device', 83 + }) 84 + 85 + if (__DEV__ && typeof window !== 'undefined') { 86 + // @ts-expect-error - dev global 87 + window.bsky_archive = { 88 + deviceArchive, 89 + } 90 + }
+10
src/storage/archive/schema.ts
··· 1 + /** 2 + * Data that's specific to the device and does not vary based account. 3 + * Values here should be optional (since they may not have been saved 4 + * yet), hence the `Partial`. 5 + * 6 + * See `#/storage/schema.ts` for examples. 7 + */ 8 + export type Device = Partial<{ 9 + // add values here 10 + }>
+1 -1
src/storage/schema.ts
··· 2 2 import {type Geolocation} from '#/geolocation/types' 3 3 4 4 /** 5 - * Device data that's specific to the device and does not vary based account 5 + * Data that's specific to the device and does not vary based account 6 6 */ 7 7 export type Device = { 8 8 /**
+5
yarn.lock
··· 13093 13093 resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-5.1.0.tgz#c6be6858abd013d768e98366ae47e25d5887b1ae" 13094 13094 integrity sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA== 13095 13095 13096 + idb-keyval@^6.2.2: 13097 + version "6.2.2" 13098 + resolved "https://registry.yarnpkg.com/idb-keyval/-/idb-keyval-6.2.2.tgz#b0171b5f73944854a3291a5cdba8e12768c4854a" 13099 + integrity sha512-yjD9nARJ/jb1g+CvD0tlhUHOrJ9Sy0P8T9MF3YaLlHnSRpwPfpTX0XIvpmw3gAJUmEu3FiICLBDPXVwyEvrleg== 13100 + 13096 13101 ieee754@^1.1.13, ieee754@^1.1.4, ieee754@^1.2.1: 13097 13102 version "1.2.1" 13098 13103 resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"