[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 matchzio/stable
parent
74b0318d89
commit
5bf7f3769d
|
@ -1,67 +0,0 @@
|
|||
import type {LegacySchema} from '#/state/persisted/legacy'
|
||||
|
||||
export const ALICE_DID = 'did:plc:ALICE_DID'
|
||||
export const BOB_DID = 'did:plc:BOB_DID'
|
||||
|
||||
export const LEGACY_DATA_DUMP: LegacySchema = {
|
||||
session: {
|
||||
data: {
|
||||
service: 'https://bsky.social/',
|
||||
did: ALICE_DID,
|
||||
},
|
||||
accounts: [
|
||||
{
|
||||
service: 'https://bsky.social',
|
||||
did: ALICE_DID,
|
||||
refreshJwt: 'refreshJwt',
|
||||
accessJwt: 'accessJwt',
|
||||
handle: 'alice.test',
|
||||
email: 'alice@bsky.test',
|
||||
displayName: 'Alice',
|
||||
aviUrl: 'avi',
|
||||
emailConfirmed: true,
|
||||
},
|
||||
{
|
||||
service: 'https://bsky.social',
|
||||
did: BOB_DID,
|
||||
refreshJwt: 'refreshJwt',
|
||||
accessJwt: 'accessJwt',
|
||||
handle: 'bob.test',
|
||||
email: 'bob@bsky.test',
|
||||
displayName: 'Bob',
|
||||
aviUrl: 'avi',
|
||||
emailConfirmed: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
me: {
|
||||
did: ALICE_DID,
|
||||
handle: 'alice.test',
|
||||
displayName: 'Alice',
|
||||
description: '',
|
||||
avatar: 'avi',
|
||||
},
|
||||
onboarding: {step: 'Home'},
|
||||
shell: {colorMode: 'system'},
|
||||
preferences: {
|
||||
primaryLanguage: 'en',
|
||||
contentLanguages: ['en'],
|
||||
postLanguage: 'en',
|
||||
postLanguageHistory: ['en', 'en', 'ja', 'pt', 'de', 'en'],
|
||||
contentLabels: {
|
||||
nsfw: 'warn',
|
||||
nudity: 'warn',
|
||||
suggestive: 'warn',
|
||||
gore: 'warn',
|
||||
hate: 'hide',
|
||||
spam: 'hide',
|
||||
impersonation: 'warn',
|
||||
},
|
||||
savedFeeds: ['feed_a', 'feed_b', 'feed_c'],
|
||||
pinnedFeeds: ['feed_a', 'feed_b'],
|
||||
requireAltTextEnabled: false,
|
||||
},
|
||||
invitedUsers: {seenDids: [], copiedInvites: []},
|
||||
mutedThreads: {uris: []},
|
||||
reminders: {},
|
||||
}
|
|
@ -1,49 +0,0 @@
|
|||
import {jest, expect, test, afterEach} from '@jest/globals'
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage'
|
||||
|
||||
import {defaults} from '#/state/persisted/schema'
|
||||
import {migrate} from '#/state/persisted/legacy'
|
||||
import * as store from '#/state/persisted/store'
|
||||
import * as persisted from '#/state/persisted'
|
||||
|
||||
const write = jest.mocked(store.write)
|
||||
const read = jest.mocked(store.read)
|
||||
|
||||
jest.mock('#/logger')
|
||||
jest.mock('#/state/persisted/legacy', () => ({
|
||||
migrate: jest.fn(),
|
||||
}))
|
||||
jest.mock('#/state/persisted/store', () => ({
|
||||
write: jest.fn(),
|
||||
read: jest.fn(),
|
||||
}))
|
||||
|
||||
afterEach(() => {
|
||||
jest.useFakeTimers()
|
||||
jest.clearAllMocks()
|
||||
AsyncStorage.clear()
|
||||
})
|
||||
|
||||
test('init: fresh install, no migration', async () => {
|
||||
await persisted.init()
|
||||
|
||||
expect(migrate).toHaveBeenCalledTimes(1)
|
||||
expect(read).toHaveBeenCalledTimes(1)
|
||||
expect(write).toHaveBeenCalledWith(defaults)
|
||||
|
||||
// default value
|
||||
expect(persisted.get('colorMode')).toBe('system')
|
||||
})
|
||||
|
||||
test('init: fresh install, migration ran', async () => {
|
||||
read.mockResolvedValueOnce(defaults)
|
||||
|
||||
await persisted.init()
|
||||
|
||||
expect(migrate).toHaveBeenCalledTimes(1)
|
||||
expect(read).toHaveBeenCalledTimes(1)
|
||||
expect(write).not.toHaveBeenCalled()
|
||||
|
||||
// default value
|
||||
expect(persisted.get('colorMode')).toBe('system')
|
||||
})
|
|
@ -1,93 +0,0 @@
|
|||
import {jest, expect, test, afterEach} from '@jest/globals'
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage'
|
||||
|
||||
import {defaults, schema} from '#/state/persisted/schema'
|
||||
import {transform, migrate} from '#/state/persisted/legacy'
|
||||
import * as store from '#/state/persisted/store'
|
||||
import {logger} from '#/logger'
|
||||
import * as fixtures from '#/state/persisted/__tests__/fixtures'
|
||||
|
||||
const write = jest.mocked(store.write)
|
||||
const read = jest.mocked(store.read)
|
||||
|
||||
jest.mock('#/logger')
|
||||
jest.mock('#/state/persisted/store', () => ({
|
||||
write: jest.fn(),
|
||||
read: jest.fn(),
|
||||
}))
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks()
|
||||
AsyncStorage.clear()
|
||||
})
|
||||
|
||||
test('migrate: fresh install', async () => {
|
||||
await migrate()
|
||||
|
||||
expect(AsyncStorage.getItem).toHaveBeenCalledWith('root')
|
||||
expect(read).toHaveBeenCalledTimes(1)
|
||||
expect(logger.debug).toHaveBeenCalledWith(
|
||||
'persisted state: no migration needed',
|
||||
)
|
||||
})
|
||||
|
||||
test('migrate: fresh install, existing new storage', async () => {
|
||||
read.mockResolvedValueOnce(defaults)
|
||||
|
||||
await migrate()
|
||||
|
||||
expect(AsyncStorage.getItem).toHaveBeenCalledWith('root')
|
||||
expect(read).toHaveBeenCalledTimes(1)
|
||||
expect(logger.debug).toHaveBeenCalledWith(
|
||||
'persisted state: no migration needed',
|
||||
)
|
||||
})
|
||||
|
||||
test('migrate: fresh install, AsyncStorage error', async () => {
|
||||
const prevGetItem = AsyncStorage.getItem
|
||||
|
||||
const error = new Error('test error')
|
||||
|
||||
AsyncStorage.getItem = jest.fn(() => {
|
||||
throw error
|
||||
})
|
||||
|
||||
await migrate()
|
||||
|
||||
expect(AsyncStorage.getItem).toHaveBeenCalledWith('root')
|
||||
expect(logger.error).toHaveBeenCalledWith(error, {
|
||||
message: 'persisted state: error migrating legacy storage',
|
||||
})
|
||||
|
||||
AsyncStorage.getItem = prevGetItem
|
||||
})
|
||||
|
||||
test('migrate: has legacy data', async () => {
|
||||
await AsyncStorage.setItem('root', JSON.stringify(fixtures.LEGACY_DATA_DUMP))
|
||||
|
||||
await migrate()
|
||||
|
||||
expect(write).toHaveBeenCalledWith(transform(fixtures.LEGACY_DATA_DUMP))
|
||||
expect(logger.debug).toHaveBeenCalledWith(
|
||||
'persisted state: migrated legacy storage',
|
||||
)
|
||||
})
|
||||
|
||||
test('migrate: has legacy data, fails validation', async () => {
|
||||
const legacy = fixtures.LEGACY_DATA_DUMP
|
||||
// @ts-ignore
|
||||
legacy.shell.colorMode = 'invalid'
|
||||
await AsyncStorage.setItem('root', JSON.stringify(legacy))
|
||||
|
||||
await migrate()
|
||||
|
||||
const transformed = transform(legacy)
|
||||
const validate = schema.safeParse(transformed)
|
||||
|
||||
expect(write).not.toHaveBeenCalled()
|
||||
expect(logger.error).toHaveBeenCalledWith(
|
||||
'persisted state: legacy data failed validation',
|
||||
// @ts-ignore
|
||||
{message: validate.error},
|
||||
)
|
||||
})
|
|
@ -1,21 +0,0 @@
|
|||
import {expect, test} from '@jest/globals'
|
||||
|
||||
import {transform} from '#/state/persisted/legacy'
|
||||
import {defaults, schema} from '#/state/persisted/schema'
|
||||
import * as fixtures from '#/state/persisted/__tests__/fixtures'
|
||||
|
||||
test('defaults', () => {
|
||||
expect(() => schema.parse(defaults)).not.toThrow()
|
||||
})
|
||||
|
||||
test('transform', () => {
|
||||
const data = transform({})
|
||||
expect(() => schema.parse(data)).not.toThrow()
|
||||
})
|
||||
|
||||
test('transform: legacy fixture', () => {
|
||||
const data = transform(fixtures.LEGACY_DATA_DUMP)
|
||||
expect(() => schema.parse(data)).not.toThrow()
|
||||
expect(data.session.currentAccount?.did).toEqual(fixtures.ALICE_DID)
|
||||
expect(data.session.accounts.length).toEqual(2)
|
||||
})
|
|
@ -1,49 +1,35 @@
|
|||
import EventEmitter from 'eventemitter3'
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage'
|
||||
|
||||
import BroadcastChannel from '#/lib/broadcast'
|
||||
import {logger} from '#/logger'
|
||||
import {migrate} from '#/state/persisted/legacy'
|
||||
import {defaults, Schema} from '#/state/persisted/schema'
|
||||
import * as store from '#/state/persisted/store'
|
||||
import {defaults, Schema, schema} from '#/state/persisted/schema'
|
||||
import {PersistedApi} from './types'
|
||||
|
||||
export type {PersistedAccount, Schema} from '#/state/persisted/schema'
|
||||
export {defaults} from '#/state/persisted/schema'
|
||||
|
||||
const broadcast = new BroadcastChannel('BSKY_BROADCAST_CHANNEL')
|
||||
const UPDATE_EVENT = 'BSKY_UPDATE'
|
||||
const BSKY_STORAGE = 'BSKY_STORAGE'
|
||||
|
||||
let _state: Schema = defaults
|
||||
const _emitter = new EventEmitter()
|
||||
|
||||
/**
|
||||
* Initializes and returns persisted data state, so that it can be passed to
|
||||
* the Provider.
|
||||
*/
|
||||
export async function init() {
|
||||
logger.debug('persisted state: initializing')
|
||||
|
||||
broadcast.onmessage = onBroadcastMessage
|
||||
|
||||
try {
|
||||
await migrate() // migrate old store
|
||||
const stored = await store.read() // check for new store
|
||||
const stored = await readFromStorage()
|
||||
if (!stored) {
|
||||
logger.debug('persisted state: initializing default storage')
|
||||
await store.write(defaults) // opt: init new store
|
||||
await writeToStorage(defaults)
|
||||
}
|
||||
_state = stored || defaults // return new store
|
||||
logger.debug('persisted state: initialized')
|
||||
_state = stored || defaults
|
||||
} catch (e) {
|
||||
logger.error('persisted state: failed to load root state from storage', {
|
||||
message: e,
|
||||
})
|
||||
// AsyncStorage failure, but we can still continue in memory
|
||||
return defaults
|
||||
}
|
||||
}
|
||||
init satisfies PersistedApi['init']
|
||||
|
||||
export function get<K extends keyof Schema>(key: K): Schema[K] {
|
||||
return _state[key]
|
||||
}
|
||||
get satisfies PersistedApi['get']
|
||||
|
||||
export async function write<K extends keyof Schema>(
|
||||
key: K,
|
||||
|
@ -51,47 +37,55 @@ export async function write<K extends keyof Schema>(
|
|||
): Promise<void> {
|
||||
try {
|
||||
_state[key] = value
|
||||
await store.write(_state)
|
||||
// must happen on next tick, otherwise the tab will read stale storage data
|
||||
setTimeout(() => broadcast.postMessage({event: UPDATE_EVENT}), 0)
|
||||
logger.debug(`persisted state: wrote root state to storage`, {
|
||||
updatedKey: key,
|
||||
})
|
||||
await writeToStorage(_state)
|
||||
} catch (e) {
|
||||
logger.error(`persisted state: failed writing root state to storage`, {
|
||||
message: e,
|
||||
})
|
||||
}
|
||||
}
|
||||
write satisfies PersistedApi['write']
|
||||
|
||||
export function onUpdate(cb: () => void): () => void {
|
||||
_emitter.addListener('update', cb)
|
||||
return () => _emitter.removeListener('update', cb)
|
||||
export function onUpdate(_cb: () => void): () => void {
|
||||
return () => {}
|
||||
}
|
||||
onUpdate satisfies PersistedApi['onUpdate']
|
||||
|
||||
async function onBroadcastMessage({data}: MessageEvent) {
|
||||
// validate event
|
||||
if (typeof data === 'object' && data.event === UPDATE_EVENT) {
|
||||
export async function clearStorage() {
|
||||
try {
|
||||
// read next state, possibly updated by another tab
|
||||
const next = await store.read()
|
||||
await AsyncStorage.removeItem(BSKY_STORAGE)
|
||||
} catch (e: any) {
|
||||
logger.error(`persisted store: failed to clear`, {message: e.toString()})
|
||||
}
|
||||
}
|
||||
clearStorage satisfies PersistedApi['clearStorage']
|
||||
|
||||
if (next) {
|
||||
logger.debug(`persisted state: handling update from broadcast channel`)
|
||||
_state = next
|
||||
_emitter.emit('update')
|
||||
async function writeToStorage(value: Schema) {
|
||||
schema.parse(value)
|
||||
await AsyncStorage.setItem(BSKY_STORAGE, JSON.stringify(value))
|
||||
}
|
||||
|
||||
async function readFromStorage(): Promise<Schema | undefined> {
|
||||
const rawData = await AsyncStorage.getItem(BSKY_STORAGE)
|
||||
const objData = rawData ? JSON.parse(rawData) : undefined
|
||||
|
||||
// new user
|
||||
if (!objData) return undefined
|
||||
|
||||
// existing user, validate
|
||||
const parsed = schema.safeParse(objData)
|
||||
|
||||
if (parsed.success) {
|
||||
return objData
|
||||
} else {
|
||||
logger.error(
|
||||
`persisted state: handled update update from broadcast channel, but found no data`,
|
||||
)
|
||||
}
|
||||
} catch (e) {
|
||||
logger.error(
|
||||
`persisted state: failed handling update from broadcast channel`,
|
||||
{
|
||||
message: e,
|
||||
},
|
||||
)
|
||||
}
|
||||
const errors =
|
||||
parsed.error?.errors?.map(e => ({
|
||||
code: e.code,
|
||||
// @ts-ignore exists on some types
|
||||
expected: e?.expected,
|
||||
path: e.path?.join('.'),
|
||||
})) || []
|
||||
logger.error(`persisted store: data failed validation on read`, {errors})
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,126 @@
|
|||
import EventEmitter from 'eventemitter3'
|
||||
|
||||
import BroadcastChannel from '#/lib/broadcast'
|
||||
import {logger} from '#/logger'
|
||||
import {defaults, Schema, schema} from '#/state/persisted/schema'
|
||||
import {PersistedApi} from './types'
|
||||
|
||||
export type {PersistedAccount, Schema} from '#/state/persisted/schema'
|
||||
export {defaults} from '#/state/persisted/schema'
|
||||
|
||||
const BSKY_STORAGE = 'BSKY_STORAGE'
|
||||
|
||||
const broadcast = new BroadcastChannel('BSKY_BROADCAST_CHANNEL')
|
||||
const UPDATE_EVENT = 'BSKY_UPDATE'
|
||||
|
||||
let _state: Schema = defaults
|
||||
const _emitter = new EventEmitter()
|
||||
|
||||
export async function init() {
|
||||
broadcast.onmessage = onBroadcastMessage
|
||||
|
||||
try {
|
||||
const stored = readFromStorage()
|
||||
if (!stored) {
|
||||
writeToStorage(defaults)
|
||||
}
|
||||
_state = stored || defaults
|
||||
} catch (e) {
|
||||
logger.error('persisted state: failed to load root state from storage', {
|
||||
message: e,
|
||||
})
|
||||
}
|
||||
}
|
||||
init satisfies PersistedApi['init']
|
||||
|
||||
export function get<K extends keyof Schema>(key: K): Schema[K] {
|
||||
return _state[key]
|
||||
}
|
||||
get satisfies PersistedApi['get']
|
||||
|
||||
export async function write<K extends keyof Schema>(
|
||||
key: K,
|
||||
value: Schema[K],
|
||||
): Promise<void> {
|
||||
try {
|
||||
_state[key] = value
|
||||
writeToStorage(_state)
|
||||
// must happen on next tick, otherwise the tab will read stale storage data
|
||||
setTimeout(() => broadcast.postMessage({event: UPDATE_EVENT}), 0)
|
||||
} catch (e) {
|
||||
logger.error(`persisted state: failed writing root state to storage`, {
|
||||
message: e,
|
||||
})
|
||||
}
|
||||
}
|
||||
write satisfies PersistedApi['write']
|
||||
|
||||
export function onUpdate(cb: () => void): () => void {
|
||||
_emitter.addListener('update', cb)
|
||||
return () => _emitter.removeListener('update', cb)
|
||||
}
|
||||
onUpdate satisfies PersistedApi['onUpdate']
|
||||
|
||||
export async function clearStorage() {
|
||||
try {
|
||||
localStorage.removeItem(BSKY_STORAGE)
|
||||
} catch (e: any) {
|
||||
logger.error(`persisted store: failed to clear`, {message: e.toString()})
|
||||
}
|
||||
}
|
||||
clearStorage satisfies PersistedApi['clearStorage']
|
||||
|
||||
async function onBroadcastMessage({data}: MessageEvent) {
|
||||
if (typeof data === 'object' && data.event === UPDATE_EVENT) {
|
||||
try {
|
||||
// read next state, possibly updated by another tab
|
||||
const next = readFromStorage()
|
||||
|
||||
if (next) {
|
||||
_state = next
|
||||
_emitter.emit('update')
|
||||
} else {
|
||||
logger.error(
|
||||
`persisted state: handled update update from broadcast channel, but found no data`,
|
||||
)
|
||||
}
|
||||
} catch (e) {
|
||||
logger.error(
|
||||
`persisted state: failed handling update from broadcast channel`,
|
||||
{
|
||||
message: e,
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function writeToStorage(value: Schema) {
|
||||
schema.parse(value)
|
||||
localStorage.setItem(BSKY_STORAGE, JSON.stringify(value))
|
||||
}
|
||||
|
||||
function readFromStorage(): Schema | undefined {
|
||||
const rawData = localStorage.getItem(BSKY_STORAGE)
|
||||
const objData = rawData ? JSON.parse(rawData) : undefined
|
||||
|
||||
// new user
|
||||
if (!objData) return undefined
|
||||
|
||||
// existing user, validate
|
||||
const parsed = schema.safeParse(objData)
|
||||
|
||||
if (parsed.success) {
|
||||
return objData
|
||||
} else {
|
||||
const errors =
|
||||
parsed.error?.errors?.map(e => ({
|
||||
code: e.code,
|
||||
// @ts-ignore exists on some types
|
||||
expected: e?.expected,
|
||||
path: e.path?.join('.'),
|
||||
})) || []
|
||||
logger.error(`persisted store: data failed validation on read`, {errors})
|
||||
return undefined
|
||||
}
|
||||
}
|
|
@ -1,167 +0,0 @@
|
|||
import AsyncStorage from '@react-native-async-storage/async-storage'
|
||||
|
||||
import {logger} from '#/logger'
|
||||
import {defaults, Schema, schema} from '#/state/persisted/schema'
|
||||
import {read, write} from '#/state/persisted/store'
|
||||
|
||||
/**
|
||||
* The shape of the serialized data from our legacy Mobx store.
|
||||
*/
|
||||
export type LegacySchema = {
|
||||
shell: {
|
||||
colorMode: 'system' | 'light' | 'dark'
|
||||
}
|
||||
session: {
|
||||
data: {
|
||||
service: string
|
||||
did: `did:plc:${string}`
|
||||
} | null
|
||||
accounts: {
|
||||
service: string
|
||||
did: `did:plc:${string}`
|
||||
refreshJwt: string
|
||||
accessJwt: string
|
||||
handle: string
|
||||
email: string
|
||||
displayName: string
|
||||
aviUrl: string
|
||||
emailConfirmed: boolean
|
||||
}[]
|
||||
}
|
||||
me: {
|
||||
did: `did:plc:${string}`
|
||||
handle: string
|
||||
displayName: string
|
||||
description: string
|
||||
avatar: string
|
||||
}
|
||||
onboarding: {
|
||||
step: string
|
||||
}
|
||||
preferences: {
|
||||
primaryLanguage: string
|
||||
contentLanguages: string[]
|
||||
postLanguage: string
|
||||
postLanguageHistory: string[]
|
||||
contentLabels: {
|
||||
nsfw: string
|
||||
nudity: string
|
||||
suggestive: string
|
||||
gore: string
|
||||
hate: string
|
||||
spam: string
|
||||
impersonation: string
|
||||
}
|
||||
savedFeeds: string[]
|
||||
pinnedFeeds: string[]
|
||||
requireAltTextEnabled: boolean
|
||||
}
|
||||
invitedUsers: {
|
||||
seenDids: string[]
|
||||
copiedInvites: string[]
|
||||
}
|
||||
mutedThreads: {uris: string[]}
|
||||
reminders: {lastEmailConfirm?: string}
|
||||
}
|
||||
|
||||
const DEPRECATED_ROOT_STATE_STORAGE_KEY = 'root'
|
||||
|
||||
export function transform(legacy: Partial<LegacySchema>): Schema {
|
||||
return {
|
||||
colorMode: legacy.shell?.colorMode || defaults.colorMode,
|
||||
darkTheme: defaults.darkTheme,
|
||||
session: {
|
||||
accounts: legacy.session?.accounts || defaults.session.accounts,
|
||||
currentAccount:
|
||||
legacy.session?.accounts?.find(
|
||||
a => a.did === legacy.session?.data?.did,
|
||||
) || defaults.session.currentAccount,
|
||||
},
|
||||
reminders: {
|
||||
lastEmailConfirm:
|
||||
legacy.reminders?.lastEmailConfirm ||
|
||||
defaults.reminders.lastEmailConfirm,
|
||||
},
|
||||
languagePrefs: {
|
||||
primaryLanguage:
|
||||
legacy.preferences?.primaryLanguage ||
|
||||
defaults.languagePrefs.primaryLanguage,
|
||||
contentLanguages:
|
||||
legacy.preferences?.contentLanguages ||
|
||||
defaults.languagePrefs.contentLanguages,
|
||||
postLanguage:
|
||||
legacy.preferences?.postLanguage || defaults.languagePrefs.postLanguage,
|
||||
postLanguageHistory:
|
||||
legacy.preferences?.postLanguageHistory ||
|
||||
defaults.languagePrefs.postLanguageHistory,
|
||||
appLanguage:
|
||||
legacy.preferences?.primaryLanguage ||
|
||||
defaults.languagePrefs.appLanguage,
|
||||
},
|
||||
requireAltTextEnabled:
|
||||
legacy.preferences?.requireAltTextEnabled ||
|
||||
defaults.requireAltTextEnabled,
|
||||
mutedThreads: legacy.mutedThreads?.uris || defaults.mutedThreads,
|
||||
invites: {
|
||||
copiedInvites:
|
||||
legacy.invitedUsers?.copiedInvites || defaults.invites.copiedInvites,
|
||||
},
|
||||
onboarding: {
|
||||
step: legacy.onboarding?.step || defaults.onboarding.step,
|
||||
},
|
||||
hiddenPosts: defaults.hiddenPosts,
|
||||
externalEmbeds: defaults.externalEmbeds,
|
||||
lastSelectedHomeFeed: defaults.lastSelectedHomeFeed,
|
||||
pdsAddressHistory: defaults.pdsAddressHistory,
|
||||
disableHaptics: defaults.disableHaptics,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrates legacy persisted state to new store if new store doesn't exist in
|
||||
* local storage AND old storage exists.
|
||||
*/
|
||||
export async function migrate() {
|
||||
logger.debug('persisted state: check need to migrate')
|
||||
|
||||
try {
|
||||
const rawLegacyData = await AsyncStorage.getItem(
|
||||
DEPRECATED_ROOT_STATE_STORAGE_KEY,
|
||||
)
|
||||
const newData = await read()
|
||||
const alreadyMigrated = Boolean(newData)
|
||||
|
||||
if (!alreadyMigrated && rawLegacyData) {
|
||||
logger.debug('persisted state: migrating legacy storage')
|
||||
|
||||
const legacyData = JSON.parse(rawLegacyData)
|
||||
const newData = transform(legacyData)
|
||||
const validate = schema.safeParse(newData)
|
||||
|
||||
if (validate.success) {
|
||||
await write(newData)
|
||||
logger.debug('persisted state: migrated legacy storage')
|
||||
} else {
|
||||
logger.error('persisted state: legacy data failed validation', {
|
||||
message: validate.error,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
logger.debug('persisted state: no migration needed')
|
||||
}
|
||||
} catch (e: any) {
|
||||
logger.error(e, {
|
||||
message: 'persisted state: error migrating legacy storage',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export async function clearLegacyStorage() {
|
||||
try {
|
||||
await AsyncStorage.removeItem(DEPRECATED_ROOT_STATE_STORAGE_KEY)
|
||||
} catch (e: any) {
|
||||
logger.error(`persisted legacy store: failed to clear`, {
|
||||
message: e.toString(),
|
||||
})
|
||||
}
|
||||
}
|
|
@ -1,44 +0,0 @@
|
|||
import AsyncStorage from '@react-native-async-storage/async-storage'
|
||||
|
||||
import {logger} from '#/logger'
|
||||
import {Schema, schema} from '#/state/persisted/schema'
|
||||
|
||||
const BSKY_STORAGE = 'BSKY_STORAGE'
|
||||
|
||||
export async function write(value: Schema) {
|
||||
schema.parse(value)
|
||||
await AsyncStorage.setItem(BSKY_STORAGE, JSON.stringify(value))
|
||||
}
|
||||
|
||||
export async function read(): Promise<Schema | undefined> {
|
||||
const rawData = await AsyncStorage.getItem(BSKY_STORAGE)
|
||||
const objData = rawData ? JSON.parse(rawData) : undefined
|
||||
|
||||
// new user
|
||||
if (!objData) return undefined
|
||||
|
||||
// existing user, validate
|
||||
const parsed = schema.safeParse(objData)
|
||||
|
||||
if (parsed.success) {
|
||||
return objData
|
||||
} else {
|
||||
const errors =
|
||||
parsed.error?.errors?.map(e => ({
|
||||
code: e.code,
|
||||
// @ts-ignore exists on some types
|
||||
expected: e?.expected,
|
||||
path: e.path?.join('.'),
|
||||
})) || []
|
||||
logger.error(`persisted store: data failed validation on read`, {errors})
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
export async function clear() {
|
||||
try {
|
||||
await AsyncStorage.removeItem(BSKY_STORAGE)
|
||||
} catch (e: any) {
|
||||
logger.error(`persisted store: failed to clear`, {message: e.toString()})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
import type {Schema} from './schema'
|
||||
|
||||
export type PersistedApi = {
|
||||
init(): Promise<void>
|
||||
get<K extends keyof Schema>(key: K): Schema[K]
|
||||
write<K extends keyof Schema>(key: K, value: Schema[K]): Promise<void>
|
||||
onUpdate(_cb: () => void): () => void
|
||||
clearStorage: () => Promise<void>
|
||||
}
|
|
@ -20,8 +20,7 @@ import {useQueryClient} from '@tanstack/react-query'
|
|||
|
||||
import {isNative} from '#/platform/detection'
|
||||
import {useModalControls} from '#/state/modals'
|
||||
import {clearLegacyStorage} from '#/state/persisted/legacy'
|
||||
import {clear as clearStorage} from '#/state/persisted/store'
|
||||
import {clearStorage} from '#/state/persisted'
|
||||
import {
|
||||
useInAppBrowser,
|
||||
useSetInAppBrowser,
|
||||
|
@ -299,10 +298,6 @@ export function SettingsScreen({}: Props) {
|
|||
await clearStorage()
|
||||
Toast.show(_(msg`Storage cleared, you need to restart the app now.`))
|
||||
}, [_])
|
||||
const clearAllLegacyStorage = React.useCallback(async () => {
|
||||
await clearLegacyStorage()
|
||||
Toast.show(_(msg`Legacy storage cleared, you need to restart the app now.`))
|
||||
}, [_])
|
||||
|
||||
const deactivateAccountControl = useDialogControl()
|
||||
const onPressDeactivateAccount = React.useCallback(() => {
|
||||
|
@ -863,18 +858,6 @@ export function SettingsScreen({}: Props) {
|
|||
<Trans>Reset onboarding state</Trans>
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
style={[pal.view, styles.linkCardNoIcon]}
|
||||
onPress={clearAllLegacyStorage}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={_(msg`Clear all legacy storage data`)}
|
||||
accessibilityHint={_(msg`Clears all legacy storage data`)}>
|
||||
<Text type="lg" style={pal.text}>
|
||||
<Trans>
|
||||
Clear all legacy storage data (restart after this)
|
||||
</Trans>
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
style={[pal.view, styles.linkCardNoIcon]}
|
||||
onPress={clearAllStorage}
|
||||
|
|
Loading…
Reference in New Issue