Improvements to persisted state migration (#2098)

* Fix session email/emailConfirmed types, update usage for safer access

* Handle fallback better, better errors

* Better handling, add test

* Add test for default data

* Remove fallback, not needed, update logs
This commit is contained in:
Eric Bailey 2023-12-05 19:59:34 -06:00 committed by GitHub
parent a915a57b10
commit 3c8036587e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 48 additions and 30 deletions

View file

@ -0,0 +1,13 @@
import {expect, test} from '@jest/globals'
import {transform} from '#/state/persisted/legacy'
import {defaults, schema} from '#/state/persisted/schema'
test('defaults', () => {
expect(() => schema.parse(defaults)).not.toThrow()
})
test('transform', () => {
const data = transform({})
expect(() => schema.parse(data)).not.toThrow()
})

View file

@ -26,7 +26,10 @@ export async function init() {
try {
await migrate() // migrate old store
const stored = await store.read() // check for new store
if (!stored) await store.write(defaults) // opt: init new store
if (!stored) {
logger.info('persisted state: initializing default storage')
await store.write(defaults) // opt: init new store
}
_state = stored || defaults // return new store
logger.log('persisted state: initialized')
} catch (e) {

View file

@ -1,7 +1,7 @@
import AsyncStorage from '@react-native-async-storage/async-storage'
import {logger} from '#/logger'
import {defaults, Schema} from '#/state/persisted/schema'
import {defaults, Schema, schema} from '#/state/persisted/schema'
import {write, read} from '#/state/persisted/store'
/**
@ -66,7 +66,6 @@ type LegacySchema = {
const DEPRECATED_ROOT_STATE_STORAGE_KEY = 'root'
// TODO remove, assume that partial data may be here during our refactor
export function transform(legacy: Partial<LegacySchema>): Schema {
return {
colorMode: legacy.shell?.colorMode || defaults.colorMode,
@ -116,7 +115,7 @@ export function transform(legacy: Partial<LegacySchema>): Schema {
* local storage AND old storage exists.
*/
export async function migrate() {
logger.info('persisted state: migrate')
logger.info('persisted state: check need to migrate')
try {
const rawLegacyData = await AsyncStorage.getItem(
@ -138,6 +137,7 @@ export async function migrate() {
),
})
logger.info(`persisted state: debug new data`, {
hasNewData: Boolean(newData),
hasExistingLoggedInAccount: Boolean(newData?.session?.currentAccount),
numberOfExistingAccounts: newData?.session?.accounts?.length,
existingAccountMatchesLegacy: Boolean(
@ -145,27 +145,32 @@ export async function migrate() {
legacy?.session?.data?.did,
),
})
} else {
logger.info(`persisted state: no legacy to debug, fresh install`)
}
} catch (e) {
logger.error(`persisted state: legacy debugging failed`, {error: e})
} catch (e: any) {
logger.error(e, {message: `persisted state: legacy debugging failed`})
}
if (!alreadyMigrated && rawLegacyData) {
logger.info('persisted state: migrating legacy storage')
const legacyData = JSON.parse(rawLegacyData)
const newData = transform(legacyData)
await write(newData)
// track successful migrations
logger.log('persisted state: migrated legacy storage')
const validate = schema.safeParse(newData)
if (validate.success) {
await write(newData)
logger.log('persisted state: migrated legacy storage')
} else {
logger.error('persisted state: legacy data failed validation', {
error: validate.error,
})
}
} else {
// track successful migrations
logger.log('persisted state: no migration needed')
}
} catch (e) {
logger.error('persisted state: error migrating legacy storage', {
error: String(e),
} catch (e: any) {
logger.error(e, {
message: 'persisted state: error migrating legacy storage',
})
}
}

View file

@ -2,17 +2,14 @@ import {z} from 'zod'
import {deviceLocales} from '#/platform/detection'
// only data needed for rendering account page
// TODO agent.resumeSession requires the following fields
const accountSchema = z.object({
service: z.string(),
did: z.string(),
handle: z.string(),
email: z.string(),
emailConfirmed: z.boolean(),
email: z.string().optional(),
emailConfirmed: z.boolean().optional(),
refreshJwt: z.string().optional(), // optional because it can expire
accessJwt: z.string().optional(), // optional because it can expire
// displayName: z.string().optional(),
// aviUrl: z.string().optional(),
})
export type PersistedAccount = z.infer<typeof accountSchema>

View file

@ -245,7 +245,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
service: agent.service.toString(),
did: agent.session.did,
handle: agent.session.handle,
email: agent.session.email!, // TODO this is always defined?
email: agent.session.email,
emailConfirmed: agent.session.emailConfirmed || false,
refreshJwt: agent.session.refreshJwt,
accessJwt: agent.session.accessJwt,
@ -342,7 +342,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
service: agent.service.toString(),
did: agent.session.did,
handle: agent.session.handle,
email: agent.session.email!, // TODO this is always defined?
email: agent.session.email,
emailConfirmed: agent.session.emailConfirmed || false,
refreshJwt: agent.session.refreshJwt,
accessJwt: agent.session.accessJwt,