Bluesky app fork with some witchin' additions 💫

[Persisted] Fork web and native, make it synchronous on the web (#4872)

* Delete logic for legacy storage

* Delete superfluous tests

At this point these tests aren't testing anything useful, let's just get rid of them.

* Inline store.ts methods into persisted/index.ts

* Fork persisted/index.ts into index.web.ts

* Remove non-essential code and comments from both forks

* Remove async/await from web fork of persisted/index.ts

* Remove unused return

* Enforce that forked types match

authored by danabra.mov and committed by

GitHub 5bf7f376 74b0318d

+186 -515
-67
src/state/persisted/__tests__/fixtures.ts
··· 1 - import type {LegacySchema} from '#/state/persisted/legacy' 2 - 3 - export const ALICE_DID = 'did:plc:ALICE_DID' 4 - export const BOB_DID = 'did:plc:BOB_DID' 5 - 6 - export const LEGACY_DATA_DUMP: LegacySchema = { 7 - session: { 8 - data: { 9 - service: 'https://bsky.social/', 10 - did: ALICE_DID, 11 - }, 12 - accounts: [ 13 - { 14 - service: 'https://bsky.social', 15 - did: ALICE_DID, 16 - refreshJwt: 'refreshJwt', 17 - accessJwt: 'accessJwt', 18 - handle: 'alice.test', 19 - email: 'alice@bsky.test', 20 - displayName: 'Alice', 21 - aviUrl: 'avi', 22 - emailConfirmed: true, 23 - }, 24 - { 25 - service: 'https://bsky.social', 26 - did: BOB_DID, 27 - refreshJwt: 'refreshJwt', 28 - accessJwt: 'accessJwt', 29 - handle: 'bob.test', 30 - email: 'bob@bsky.test', 31 - displayName: 'Bob', 32 - aviUrl: 'avi', 33 - emailConfirmed: true, 34 - }, 35 - ], 36 - }, 37 - me: { 38 - did: ALICE_DID, 39 - handle: 'alice.test', 40 - displayName: 'Alice', 41 - description: '', 42 - avatar: 'avi', 43 - }, 44 - onboarding: {step: 'Home'}, 45 - shell: {colorMode: 'system'}, 46 - preferences: { 47 - primaryLanguage: 'en', 48 - contentLanguages: ['en'], 49 - postLanguage: 'en', 50 - postLanguageHistory: ['en', 'en', 'ja', 'pt', 'de', 'en'], 51 - contentLabels: { 52 - nsfw: 'warn', 53 - nudity: 'warn', 54 - suggestive: 'warn', 55 - gore: 'warn', 56 - hate: 'hide', 57 - spam: 'hide', 58 - impersonation: 'warn', 59 - }, 60 - savedFeeds: ['feed_a', 'feed_b', 'feed_c'], 61 - pinnedFeeds: ['feed_a', 'feed_b'], 62 - requireAltTextEnabled: false, 63 - }, 64 - invitedUsers: {seenDids: [], copiedInvites: []}, 65 - mutedThreads: {uris: []}, 66 - reminders: {}, 67 - }
-49
src/state/persisted/__tests__/index.test.ts
··· 1 - import {jest, expect, test, afterEach} from '@jest/globals' 2 - import AsyncStorage from '@react-native-async-storage/async-storage' 3 - 4 - import {defaults} from '#/state/persisted/schema' 5 - import {migrate} from '#/state/persisted/legacy' 6 - import * as store from '#/state/persisted/store' 7 - import * as persisted from '#/state/persisted' 8 - 9 - const write = jest.mocked(store.write) 10 - const read = jest.mocked(store.read) 11 - 12 - jest.mock('#/logger') 13 - jest.mock('#/state/persisted/legacy', () => ({ 14 - migrate: jest.fn(), 15 - })) 16 - jest.mock('#/state/persisted/store', () => ({ 17 - write: jest.fn(), 18 - read: jest.fn(), 19 - })) 20 - 21 - afterEach(() => { 22 - jest.useFakeTimers() 23 - jest.clearAllMocks() 24 - AsyncStorage.clear() 25 - }) 26 - 27 - test('init: fresh install, no migration', async () => { 28 - await persisted.init() 29 - 30 - expect(migrate).toHaveBeenCalledTimes(1) 31 - expect(read).toHaveBeenCalledTimes(1) 32 - expect(write).toHaveBeenCalledWith(defaults) 33 - 34 - // default value 35 - expect(persisted.get('colorMode')).toBe('system') 36 - }) 37 - 38 - test('init: fresh install, migration ran', async () => { 39 - read.mockResolvedValueOnce(defaults) 40 - 41 - await persisted.init() 42 - 43 - expect(migrate).toHaveBeenCalledTimes(1) 44 - expect(read).toHaveBeenCalledTimes(1) 45 - expect(write).not.toHaveBeenCalled() 46 - 47 - // default value 48 - expect(persisted.get('colorMode')).toBe('system') 49 - })
-93
src/state/persisted/__tests__/migrate.test.ts
··· 1 - import {jest, expect, test, afterEach} from '@jest/globals' 2 - import AsyncStorage from '@react-native-async-storage/async-storage' 3 - 4 - import {defaults, schema} from '#/state/persisted/schema' 5 - import {transform, migrate} from '#/state/persisted/legacy' 6 - import * as store from '#/state/persisted/store' 7 - import {logger} from '#/logger' 8 - import * as fixtures from '#/state/persisted/__tests__/fixtures' 9 - 10 - const write = jest.mocked(store.write) 11 - const read = jest.mocked(store.read) 12 - 13 - jest.mock('#/logger') 14 - jest.mock('#/state/persisted/store', () => ({ 15 - write: jest.fn(), 16 - read: jest.fn(), 17 - })) 18 - 19 - afterEach(() => { 20 - jest.clearAllMocks() 21 - AsyncStorage.clear() 22 - }) 23 - 24 - test('migrate: fresh install', async () => { 25 - await migrate() 26 - 27 - expect(AsyncStorage.getItem).toHaveBeenCalledWith('root') 28 - expect(read).toHaveBeenCalledTimes(1) 29 - expect(logger.debug).toHaveBeenCalledWith( 30 - 'persisted state: no migration needed', 31 - ) 32 - }) 33 - 34 - test('migrate: fresh install, existing new storage', async () => { 35 - read.mockResolvedValueOnce(defaults) 36 - 37 - await migrate() 38 - 39 - expect(AsyncStorage.getItem).toHaveBeenCalledWith('root') 40 - expect(read).toHaveBeenCalledTimes(1) 41 - expect(logger.debug).toHaveBeenCalledWith( 42 - 'persisted state: no migration needed', 43 - ) 44 - }) 45 - 46 - test('migrate: fresh install, AsyncStorage error', async () => { 47 - const prevGetItem = AsyncStorage.getItem 48 - 49 - const error = new Error('test error') 50 - 51 - AsyncStorage.getItem = jest.fn(() => { 52 - throw error 53 - }) 54 - 55 - await migrate() 56 - 57 - expect(AsyncStorage.getItem).toHaveBeenCalledWith('root') 58 - expect(logger.error).toHaveBeenCalledWith(error, { 59 - message: 'persisted state: error migrating legacy storage', 60 - }) 61 - 62 - AsyncStorage.getItem = prevGetItem 63 - }) 64 - 65 - test('migrate: has legacy data', async () => { 66 - await AsyncStorage.setItem('root', JSON.stringify(fixtures.LEGACY_DATA_DUMP)) 67 - 68 - await migrate() 69 - 70 - expect(write).toHaveBeenCalledWith(transform(fixtures.LEGACY_DATA_DUMP)) 71 - expect(logger.debug).toHaveBeenCalledWith( 72 - 'persisted state: migrated legacy storage', 73 - ) 74 - }) 75 - 76 - test('migrate: has legacy data, fails validation', async () => { 77 - const legacy = fixtures.LEGACY_DATA_DUMP 78 - // @ts-ignore 79 - legacy.shell.colorMode = 'invalid' 80 - await AsyncStorage.setItem('root', JSON.stringify(legacy)) 81 - 82 - await migrate() 83 - 84 - const transformed = transform(legacy) 85 - const validate = schema.safeParse(transformed) 86 - 87 - expect(write).not.toHaveBeenCalled() 88 - expect(logger.error).toHaveBeenCalledWith( 89 - 'persisted state: legacy data failed validation', 90 - // @ts-ignore 91 - {message: validate.error}, 92 - ) 93 - })
-21
src/state/persisted/__tests__/schema.test.ts
··· 1 - import {expect, test} from '@jest/globals' 2 - 3 - import {transform} from '#/state/persisted/legacy' 4 - import {defaults, schema} from '#/state/persisted/schema' 5 - import * as fixtures from '#/state/persisted/__tests__/fixtures' 6 - 7 - test('defaults', () => { 8 - expect(() => schema.parse(defaults)).not.toThrow() 9 - }) 10 - 11 - test('transform', () => { 12 - const data = transform({}) 13 - expect(() => schema.parse(data)).not.toThrow() 14 - }) 15 - 16 - test('transform: legacy fixture', () => { 17 - const data = transform(fixtures.LEGACY_DATA_DUMP) 18 - expect(() => schema.parse(data)).not.toThrow() 19 - expect(data.session.currentAccount?.did).toEqual(fixtures.ALICE_DID) 20 - expect(data.session.accounts.length).toEqual(2) 21 - })
+50 -56
src/state/persisted/index.ts
··· 1 - import EventEmitter from 'eventemitter3' 1 + import AsyncStorage from '@react-native-async-storage/async-storage' 2 2 3 - import BroadcastChannel from '#/lib/broadcast' 4 3 import {logger} from '#/logger' 5 - import {migrate} from '#/state/persisted/legacy' 6 - import {defaults, Schema} from '#/state/persisted/schema' 7 - import * as store from '#/state/persisted/store' 4 + import {defaults, Schema, schema} from '#/state/persisted/schema' 5 + import {PersistedApi} from './types' 6 + 8 7 export type {PersistedAccount, Schema} from '#/state/persisted/schema' 9 8 export {defaults} from '#/state/persisted/schema' 10 9 11 - const broadcast = new BroadcastChannel('BSKY_BROADCAST_CHANNEL') 12 - const UPDATE_EVENT = 'BSKY_UPDATE' 10 + const BSKY_STORAGE = 'BSKY_STORAGE' 13 11 14 12 let _state: Schema = defaults 15 - const _emitter = new EventEmitter() 16 13 17 - /** 18 - * Initializes and returns persisted data state, so that it can be passed to 19 - * the Provider. 20 - */ 21 14 export async function init() { 22 - logger.debug('persisted state: initializing') 23 - 24 - broadcast.onmessage = onBroadcastMessage 25 - 26 15 try { 27 - await migrate() // migrate old store 28 - const stored = await store.read() // check for new store 16 + const stored = await readFromStorage() 29 17 if (!stored) { 30 - logger.debug('persisted state: initializing default storage') 31 - await store.write(defaults) // opt: init new store 18 + await writeToStorage(defaults) 32 19 } 33 - _state = stored || defaults // return new store 34 - logger.debug('persisted state: initialized') 20 + _state = stored || defaults 35 21 } catch (e) { 36 22 logger.error('persisted state: failed to load root state from storage', { 37 23 message: e, 38 24 }) 39 - // AsyncStorage failure, but we can still continue in memory 40 - return defaults 41 25 } 42 26 } 27 + init satisfies PersistedApi['init'] 43 28 44 29 export function get<K extends keyof Schema>(key: K): Schema[K] { 45 30 return _state[key] 46 31 } 32 + get satisfies PersistedApi['get'] 47 33 48 34 export async function write<K extends keyof Schema>( 49 35 key: K, ··· 51 37 ): Promise<void> { 52 38 try { 53 39 _state[key] = value 54 - await store.write(_state) 55 - // must happen on next tick, otherwise the tab will read stale storage data 56 - setTimeout(() => broadcast.postMessage({event: UPDATE_EVENT}), 0) 57 - logger.debug(`persisted state: wrote root state to storage`, { 58 - updatedKey: key, 59 - }) 40 + await writeToStorage(_state) 60 41 } catch (e) { 61 42 logger.error(`persisted state: failed writing root state to storage`, { 62 43 message: e, 63 44 }) 64 45 } 65 46 } 47 + write satisfies PersistedApi['write'] 66 48 67 - export function onUpdate(cb: () => void): () => void { 68 - _emitter.addListener('update', cb) 69 - return () => _emitter.removeListener('update', cb) 49 + export function onUpdate(_cb: () => void): () => void { 50 + return () => {} 70 51 } 52 + onUpdate satisfies PersistedApi['onUpdate'] 71 53 72 - async function onBroadcastMessage({data}: MessageEvent) { 73 - // validate event 74 - if (typeof data === 'object' && data.event === UPDATE_EVENT) { 75 - try { 76 - // read next state, possibly updated by another tab 77 - const next = await store.read() 54 + export async function clearStorage() { 55 + try { 56 + await AsyncStorage.removeItem(BSKY_STORAGE) 57 + } catch (e: any) { 58 + logger.error(`persisted store: failed to clear`, {message: e.toString()}) 59 + } 60 + } 61 + clearStorage satisfies PersistedApi['clearStorage'] 62 + 63 + async function writeToStorage(value: Schema) { 64 + schema.parse(value) 65 + await AsyncStorage.setItem(BSKY_STORAGE, JSON.stringify(value)) 66 + } 67 + 68 + async function readFromStorage(): Promise<Schema | undefined> { 69 + const rawData = await AsyncStorage.getItem(BSKY_STORAGE) 70 + const objData = rawData ? JSON.parse(rawData) : undefined 71 + 72 + // new user 73 + if (!objData) return undefined 74 + 75 + // existing user, validate 76 + const parsed = schema.safeParse(objData) 78 77 79 - if (next) { 80 - logger.debug(`persisted state: handling update from broadcast channel`) 81 - _state = next 82 - _emitter.emit('update') 83 - } else { 84 - logger.error( 85 - `persisted state: handled update update from broadcast channel, but found no data`, 86 - ) 87 - } 88 - } catch (e) { 89 - logger.error( 90 - `persisted state: failed handling update from broadcast channel`, 91 - { 92 - message: e, 93 - }, 94 - ) 95 - } 78 + if (parsed.success) { 79 + return objData 80 + } else { 81 + const errors = 82 + parsed.error?.errors?.map(e => ({ 83 + code: e.code, 84 + // @ts-ignore exists on some types 85 + expected: e?.expected, 86 + path: e.path?.join('.'), 87 + })) || [] 88 + logger.error(`persisted store: data failed validation on read`, {errors}) 89 + return undefined 96 90 } 97 91 }
+126
src/state/persisted/index.web.ts
··· 1 + import EventEmitter from 'eventemitter3' 2 + 3 + import BroadcastChannel from '#/lib/broadcast' 4 + import {logger} from '#/logger' 5 + import {defaults, Schema, schema} from '#/state/persisted/schema' 6 + import {PersistedApi} from './types' 7 + 8 + export type {PersistedAccount, Schema} from '#/state/persisted/schema' 9 + export {defaults} from '#/state/persisted/schema' 10 + 11 + const BSKY_STORAGE = 'BSKY_STORAGE' 12 + 13 + const broadcast = new BroadcastChannel('BSKY_BROADCAST_CHANNEL') 14 + const UPDATE_EVENT = 'BSKY_UPDATE' 15 + 16 + let _state: Schema = defaults 17 + const _emitter = new EventEmitter() 18 + 19 + export async function init() { 20 + broadcast.onmessage = onBroadcastMessage 21 + 22 + try { 23 + const stored = readFromStorage() 24 + if (!stored) { 25 + writeToStorage(defaults) 26 + } 27 + _state = stored || defaults 28 + } catch (e) { 29 + logger.error('persisted state: failed to load root state from storage', { 30 + message: e, 31 + }) 32 + } 33 + } 34 + init satisfies PersistedApi['init'] 35 + 36 + export function get<K extends keyof Schema>(key: K): Schema[K] { 37 + return _state[key] 38 + } 39 + get satisfies PersistedApi['get'] 40 + 41 + export async function write<K extends keyof Schema>( 42 + key: K, 43 + value: Schema[K], 44 + ): Promise<void> { 45 + try { 46 + _state[key] = value 47 + writeToStorage(_state) 48 + // must happen on next tick, otherwise the tab will read stale storage data 49 + setTimeout(() => broadcast.postMessage({event: UPDATE_EVENT}), 0) 50 + } catch (e) { 51 + logger.error(`persisted state: failed writing root state to storage`, { 52 + message: e, 53 + }) 54 + } 55 + } 56 + write satisfies PersistedApi['write'] 57 + 58 + export function onUpdate(cb: () => void): () => void { 59 + _emitter.addListener('update', cb) 60 + return () => _emitter.removeListener('update', cb) 61 + } 62 + onUpdate satisfies PersistedApi['onUpdate'] 63 + 64 + export async function clearStorage() { 65 + try { 66 + localStorage.removeItem(BSKY_STORAGE) 67 + } catch (e: any) { 68 + logger.error(`persisted store: failed to clear`, {message: e.toString()}) 69 + } 70 + } 71 + clearStorage satisfies PersistedApi['clearStorage'] 72 + 73 + async function onBroadcastMessage({data}: MessageEvent) { 74 + if (typeof data === 'object' && data.event === UPDATE_EVENT) { 75 + try { 76 + // read next state, possibly updated by another tab 77 + const next = readFromStorage() 78 + 79 + if (next) { 80 + _state = next 81 + _emitter.emit('update') 82 + } else { 83 + logger.error( 84 + `persisted state: handled update update from broadcast channel, but found no data`, 85 + ) 86 + } 87 + } catch (e) { 88 + logger.error( 89 + `persisted state: failed handling update from broadcast channel`, 90 + { 91 + message: e, 92 + }, 93 + ) 94 + } 95 + } 96 + } 97 + 98 + function writeToStorage(value: Schema) { 99 + schema.parse(value) 100 + localStorage.setItem(BSKY_STORAGE, JSON.stringify(value)) 101 + } 102 + 103 + function readFromStorage(): Schema | undefined { 104 + const rawData = localStorage.getItem(BSKY_STORAGE) 105 + const objData = rawData ? JSON.parse(rawData) : undefined 106 + 107 + // new user 108 + if (!objData) return undefined 109 + 110 + // existing user, validate 111 + const parsed = schema.safeParse(objData) 112 + 113 + if (parsed.success) { 114 + return objData 115 + } else { 116 + const errors = 117 + parsed.error?.errors?.map(e => ({ 118 + code: e.code, 119 + // @ts-ignore exists on some types 120 + expected: e?.expected, 121 + path: e.path?.join('.'), 122 + })) || [] 123 + logger.error(`persisted store: data failed validation on read`, {errors}) 124 + return undefined 125 + } 126 + }
-167
src/state/persisted/legacy.ts
··· 1 - import AsyncStorage from '@react-native-async-storage/async-storage' 2 - 3 - import {logger} from '#/logger' 4 - import {defaults, Schema, schema} from '#/state/persisted/schema' 5 - import {read, write} from '#/state/persisted/store' 6 - 7 - /** 8 - * The shape of the serialized data from our legacy Mobx store. 9 - */ 10 - export type LegacySchema = { 11 - shell: { 12 - colorMode: 'system' | 'light' | 'dark' 13 - } 14 - session: { 15 - data: { 16 - service: string 17 - did: `did:plc:${string}` 18 - } | null 19 - accounts: { 20 - service: string 21 - did: `did:plc:${string}` 22 - refreshJwt: string 23 - accessJwt: string 24 - handle: string 25 - email: string 26 - displayName: string 27 - aviUrl: string 28 - emailConfirmed: boolean 29 - }[] 30 - } 31 - me: { 32 - did: `did:plc:${string}` 33 - handle: string 34 - displayName: string 35 - description: string 36 - avatar: string 37 - } 38 - onboarding: { 39 - step: string 40 - } 41 - preferences: { 42 - primaryLanguage: string 43 - contentLanguages: string[] 44 - postLanguage: string 45 - postLanguageHistory: string[] 46 - contentLabels: { 47 - nsfw: string 48 - nudity: string 49 - suggestive: string 50 - gore: string 51 - hate: string 52 - spam: string 53 - impersonation: string 54 - } 55 - savedFeeds: string[] 56 - pinnedFeeds: string[] 57 - requireAltTextEnabled: boolean 58 - } 59 - invitedUsers: { 60 - seenDids: string[] 61 - copiedInvites: string[] 62 - } 63 - mutedThreads: {uris: string[]} 64 - reminders: {lastEmailConfirm?: string} 65 - } 66 - 67 - const DEPRECATED_ROOT_STATE_STORAGE_KEY = 'root' 68 - 69 - export function transform(legacy: Partial<LegacySchema>): Schema { 70 - return { 71 - colorMode: legacy.shell?.colorMode || defaults.colorMode, 72 - darkTheme: defaults.darkTheme, 73 - session: { 74 - accounts: legacy.session?.accounts || defaults.session.accounts, 75 - currentAccount: 76 - legacy.session?.accounts?.find( 77 - a => a.did === legacy.session?.data?.did, 78 - ) || defaults.session.currentAccount, 79 - }, 80 - reminders: { 81 - lastEmailConfirm: 82 - legacy.reminders?.lastEmailConfirm || 83 - defaults.reminders.lastEmailConfirm, 84 - }, 85 - languagePrefs: { 86 - primaryLanguage: 87 - legacy.preferences?.primaryLanguage || 88 - defaults.languagePrefs.primaryLanguage, 89 - contentLanguages: 90 - legacy.preferences?.contentLanguages || 91 - defaults.languagePrefs.contentLanguages, 92 - postLanguage: 93 - legacy.preferences?.postLanguage || defaults.languagePrefs.postLanguage, 94 - postLanguageHistory: 95 - legacy.preferences?.postLanguageHistory || 96 - defaults.languagePrefs.postLanguageHistory, 97 - appLanguage: 98 - legacy.preferences?.primaryLanguage || 99 - defaults.languagePrefs.appLanguage, 100 - }, 101 - requireAltTextEnabled: 102 - legacy.preferences?.requireAltTextEnabled || 103 - defaults.requireAltTextEnabled, 104 - mutedThreads: legacy.mutedThreads?.uris || defaults.mutedThreads, 105 - invites: { 106 - copiedInvites: 107 - legacy.invitedUsers?.copiedInvites || defaults.invites.copiedInvites, 108 - }, 109 - onboarding: { 110 - step: legacy.onboarding?.step || defaults.onboarding.step, 111 - }, 112 - hiddenPosts: defaults.hiddenPosts, 113 - externalEmbeds: defaults.externalEmbeds, 114 - lastSelectedHomeFeed: defaults.lastSelectedHomeFeed, 115 - pdsAddressHistory: defaults.pdsAddressHistory, 116 - disableHaptics: defaults.disableHaptics, 117 - } 118 - } 119 - 120 - /** 121 - * Migrates legacy persisted state to new store if new store doesn't exist in 122 - * local storage AND old storage exists. 123 - */ 124 - export async function migrate() { 125 - logger.debug('persisted state: check need to migrate') 126 - 127 - try { 128 - const rawLegacyData = await AsyncStorage.getItem( 129 - DEPRECATED_ROOT_STATE_STORAGE_KEY, 130 - ) 131 - const newData = await read() 132 - const alreadyMigrated = Boolean(newData) 133 - 134 - if (!alreadyMigrated && rawLegacyData) { 135 - logger.debug('persisted state: migrating legacy storage') 136 - 137 - const legacyData = JSON.parse(rawLegacyData) 138 - const newData = transform(legacyData) 139 - const validate = schema.safeParse(newData) 140 - 141 - if (validate.success) { 142 - await write(newData) 143 - logger.debug('persisted state: migrated legacy storage') 144 - } else { 145 - logger.error('persisted state: legacy data failed validation', { 146 - message: validate.error, 147 - }) 148 - } 149 - } else { 150 - logger.debug('persisted state: no migration needed') 151 - } 152 - } catch (e: any) { 153 - logger.error(e, { 154 - message: 'persisted state: error migrating legacy storage', 155 - }) 156 - } 157 - } 158 - 159 - export async function clearLegacyStorage() { 160 - try { 161 - await AsyncStorage.removeItem(DEPRECATED_ROOT_STATE_STORAGE_KEY) 162 - } catch (e: any) { 163 - logger.error(`persisted legacy store: failed to clear`, { 164 - message: e.toString(), 165 - }) 166 - } 167 - }
-44
src/state/persisted/store.ts
··· 1 - import AsyncStorage from '@react-native-async-storage/async-storage' 2 - 3 - import {logger} from '#/logger' 4 - import {Schema, schema} from '#/state/persisted/schema' 5 - 6 - const BSKY_STORAGE = 'BSKY_STORAGE' 7 - 8 - export async function write(value: Schema) { 9 - schema.parse(value) 10 - await AsyncStorage.setItem(BSKY_STORAGE, JSON.stringify(value)) 11 - } 12 - 13 - export async function read(): Promise<Schema | undefined> { 14 - const rawData = await AsyncStorage.getItem(BSKY_STORAGE) 15 - const objData = rawData ? JSON.parse(rawData) : undefined 16 - 17 - // new user 18 - if (!objData) return undefined 19 - 20 - // existing user, validate 21 - const parsed = schema.safeParse(objData) 22 - 23 - if (parsed.success) { 24 - return objData 25 - } else { 26 - const errors = 27 - parsed.error?.errors?.map(e => ({ 28 - code: e.code, 29 - // @ts-ignore exists on some types 30 - expected: e?.expected, 31 - path: e.path?.join('.'), 32 - })) || [] 33 - logger.error(`persisted store: data failed validation on read`, {errors}) 34 - return undefined 35 - } 36 - } 37 - 38 - export async function clear() { 39 - try { 40 - await AsyncStorage.removeItem(BSKY_STORAGE) 41 - } catch (e: any) { 42 - logger.error(`persisted store: failed to clear`, {message: e.toString()}) 43 - } 44 - }
+9
src/state/persisted/types.ts
··· 1 + import type {Schema} from './schema' 2 + 3 + export type PersistedApi = { 4 + init(): Promise<void> 5 + get<K extends keyof Schema>(key: K): Schema[K] 6 + write<K extends keyof Schema>(key: K, value: Schema[K]): Promise<void> 7 + onUpdate(_cb: () => void): () => void 8 + clearStorage: () => Promise<void> 9 + }
+1 -18
src/view/screens/Settings/index.tsx
··· 20 20 21 21 import {isNative} from '#/platform/detection' 22 22 import {useModalControls} from '#/state/modals' 23 - import {clearLegacyStorage} from '#/state/persisted/legacy' 24 - import {clear as clearStorage} from '#/state/persisted/store' 23 + import {clearStorage} from '#/state/persisted' 25 24 import { 26 25 useInAppBrowser, 27 26 useSetInAppBrowser, ··· 298 297 const clearAllStorage = React.useCallback(async () => { 299 298 await clearStorage() 300 299 Toast.show(_(msg`Storage cleared, you need to restart the app now.`)) 301 - }, [_]) 302 - const clearAllLegacyStorage = React.useCallback(async () => { 303 - await clearLegacyStorage() 304 - Toast.show(_(msg`Legacy storage cleared, you need to restart the app now.`)) 305 300 }, [_]) 306 301 307 302 const deactivateAccountControl = useDialogControl() ··· 861 856 accessibilityHint={_(msg`Resets the onboarding state`)}> 862 857 <Text type="lg" style={pal.text}> 863 858 <Trans>Reset onboarding state</Trans> 864 - </Text> 865 - </TouchableOpacity> 866 - <TouchableOpacity 867 - style={[pal.view, styles.linkCardNoIcon]} 868 - onPress={clearAllLegacyStorage} 869 - accessibilityRole="button" 870 - accessibilityLabel={_(msg`Clear all legacy storage data`)} 871 - accessibilityHint={_(msg`Clears all legacy storage data`)}> 872 - <Text type="lg" style={pal.text}> 873 - <Trans> 874 - Clear all legacy storage data (restart after this) 875 - </Trans> 876 859 </Text> 877 860 </TouchableOpacity> 878 861 <TouchableOpacity