diff --git a/src/lib/async/retry.ts b/src/lib/async/retry.ts
new file mode 100644
index 0000000000..f14ae6cf66
--- /dev/null
+++ b/src/lib/async/retry.ts
@@ -0,0 +1,29 @@
+import {isNetworkError} from 'lib/strings/errors'
+
+export async function retry
(
+ retries: number,
+ cond: (err: any) => boolean,
+ fn: () => Promise
,
+): Promise
{
+ let lastErr
+ while (retries > 0) {
+ try {
+ return await fn()
+ } catch (e: any) {
+ lastErr = e
+ if (cond(e)) {
+ retries--
+ continue
+ }
+ throw e
+ }
+ }
+ throw lastErr
+}
+
+export async function networkRetry
(
+ retries: number,
+ fn: () => Promise
,
+): Promise
{
+ return retry(retries, isNetworkError, fn)
+}
diff --git a/src/state/models/log.ts b/src/state/models/log.ts
index f2709f2f1c..ed701dc613 100644
--- a/src/state/models/log.ts
+++ b/src/state/models/log.ts
@@ -1,6 +1,7 @@
import {makeAutoObservable} from 'mobx'
import {XRPCError, XRPCInvalidResponseError} from '@atproto/xrpc'
-import {isObj, hasProp} from 'lib/type-guards'
+
+const MAX_ENTRIES = 300
interface LogEntry {
id: string
@@ -28,27 +29,14 @@ export class LogModel {
entries: LogEntry[] = []
constructor() {
- makeAutoObservable(this, {serialize: false, hydrate: false})
- }
-
- serialize(): unknown {
- return {
- entries: this.entries.slice(-100),
- }
- }
-
- hydrate(v: unknown) {
- if (isObj(v)) {
- if (hasProp(v, 'entries') && Array.isArray(v.entries)) {
- this.entries = v.entries.filter(
- e => isObj(e) && hasProp(e, 'id') && typeof e.id === 'string',
- )
- }
- }
+ makeAutoObservable(this)
}
private add(entry: LogEntry) {
this.entries.push(entry)
+ while (this.entries.length > MAX_ENTRIES) {
+ this.entries = this.entries.slice(50)
+ }
}
debug(summary: string, details?: any) {
diff --git a/src/state/models/root-store.ts b/src/state/models/root-store.ts
index 2af99e47c1..58cad77b9f 100644
--- a/src/state/models/root-store.ts
+++ b/src/state/models/root-store.ts
@@ -78,7 +78,6 @@ export class RootStoreModel {
serialize(): unknown {
return {
appInfo: this.appInfo,
- log: this.log.serialize(),
session: this.session.serialize(),
me: this.me.serialize(),
shell: this.shell.serialize(),
@@ -93,9 +92,6 @@ export class RootStoreModel {
this.setAppInfo(appInfoParsed.data)
}
}
- if (hasProp(v, 'log')) {
- this.log.hydrate(v.log)
- }
if (hasProp(v, 'me')) {
this.me.hydrate(v.me)
}
diff --git a/src/state/models/session.ts b/src/state/models/session.ts
index 7c4d0931c4..306c265d83 100644
--- a/src/state/models/session.ts
+++ b/src/state/models/session.ts
@@ -7,6 +7,7 @@ import {
} from '@atproto/api'
import normalizeUrl from 'normalize-url'
import {isObj, hasProp} from 'lib/type-guards'
+import {networkRetry} from 'lib/async/retry'
import {z} from 'zod'
import {RootStoreModel} from './root-store'
@@ -35,6 +36,25 @@ interface AdditionalAccountData {
}
export class SessionModel {
+ // DEBUG
+ // emergency log facility to help us track down this logout issue
+ // remove when resolved
+ // -prf
+ private _log(message: string, details?: Record) {
+ details = details || {}
+ details.state = {
+ data: this.data,
+ accounts: this.accounts.map(
+ a =>
+ `${!!a.accessJwt && !!a.refreshJwt ? '✅' : '❌'} ${a.handle} (${
+ a.service
+ })`,
+ ),
+ isResumingSession: this.isResumingSession,
+ }
+ this.rootStore.log.debug(message, details)
+ }
+
/**
* Currently-active session
*/
@@ -115,9 +135,7 @@ export class SessionModel {
async attemptSessionResumption() {
const sess = this.currentSession
if (sess) {
- this.rootStore.log.debug(
- 'SessionModel:attemptSessionResumption found stored session',
- )
+ this._log('SessionModel:attemptSessionResumption found stored session')
this.isResumingSession = true
try {
return await this.resumeSession(sess)
@@ -127,7 +145,7 @@ export class SessionModel {
})
}
} else {
- this.rootStore.log.debug(
+ this._log(
'SessionModel:attemptSessionResumption has no session to resume',
)
}
@@ -137,7 +155,7 @@ export class SessionModel {
* Sets the active session
*/
setActiveSession(agent: AtpAgent, did: string) {
- this.rootStore.log.debug('SessionModel:setActiveSession')
+ this._log('SessionModel:setActiveSession')
this.data = {
service: agent.service.toString(),
did,
@@ -155,22 +173,32 @@ export class SessionModel {
session?: AtpSessionData,
addedInfo?: AdditionalAccountData,
) {
- this.rootStore.log.debug('SessionModel:persistSession', {
+ this._log('SessionModel:persistSession', {
service,
did,
event,
hasSession: !!session,
})
- // upsert the account in our listing
const existingAccount = this.accounts.find(
account => account.service === service && account.did === did,
)
+
+ // fall back to any pre-existing access tokens
+ let refreshJwt = session?.refreshJwt || existingAccount?.refreshJwt
+ let accessJwt = session?.accessJwt || existingAccount?.accessJwt
+ if (event === 'expired') {
+ // only clear the tokens when they're known to have expired
+ refreshJwt = undefined
+ accessJwt = undefined
+ }
+
const newAccount = {
service,
did,
- refreshJwt: session?.refreshJwt,
- accessJwt: session?.accessJwt,
+ refreshJwt,
+ accessJwt,
+
handle: session?.handle || existingAccount?.handle || '',
displayName: addedInfo
? addedInfo.displayName
@@ -198,7 +226,7 @@ export class SessionModel {
* Clears any session tokens from the accounts; used on logout.
*/
private clearSessionTokens() {
- this.rootStore.log.debug('SessionModel:clearSessionTokens')
+ this._log('SessionModel:clearSessionTokens')
this.accounts = this.accounts.map(acct => ({
service: acct.service,
handle: acct.handle,
@@ -236,9 +264,9 @@ export class SessionModel {
* Attempt to resume a session that we still have access tokens for.
*/
async resumeSession(account: AccountData): Promise {
- this.rootStore.log.debug('SessionModel:resumeSession')
+ this._log('SessionModel:resumeSession')
if (!(account.accessJwt && account.refreshJwt && account.service)) {
- this.rootStore.log.debug(
+ this._log(
'SessionModel:resumeSession aborted due to lack of access tokens',
)
return false
@@ -252,12 +280,14 @@ export class SessionModel {
})
try {
- await agent.resumeSession({
- accessJwt: account.accessJwt,
- refreshJwt: account.refreshJwt,
- did: account.did,
- handle: account.handle,
- })
+ await networkRetry(3, () =>
+ agent.resumeSession({
+ accessJwt: account.accessJwt || '',
+ refreshJwt: account.refreshJwt || '',
+ did: account.did,
+ handle: account.handle,
+ }),
+ )
const addedInfo = await this.loadAccountInfo(agent, account.did)
this.persistSession(
account.service,
@@ -266,9 +296,9 @@ export class SessionModel {
agent.session,
addedInfo,
)
- this.rootStore.log.debug('SessionModel:resumeSession succeeded')
+ this._log('SessionModel:resumeSession succeeded')
} catch (e: any) {
- this.rootStore.log.debug('SessionModel:resumeSession failed', {
+ this._log('SessionModel:resumeSession failed', {
error: e.toString(),
})
return false
@@ -290,7 +320,7 @@ export class SessionModel {
identifier: string
password: string
}) {
- this.rootStore.log.debug('SessionModel:login')
+ this._log('SessionModel:login')
const agent = new AtpAgent({service})
await agent.login({identifier, password})
if (!agent.session) {
@@ -308,7 +338,7 @@ export class SessionModel {
)
this.setActiveSession(agent, did)
- this.rootStore.log.debug('SessionModel:login succeeded')
+ this._log('SessionModel:login succeeded')
}
async createAccount({
@@ -324,7 +354,7 @@ export class SessionModel {
handle: string
inviteCode?: string
}) {
- this.rootStore.log.debug('SessionModel:createAccount')
+ this._log('SessionModel:createAccount')
const agent = new AtpAgent({service})
await agent.createAccount({
handle,
@@ -348,14 +378,14 @@ export class SessionModel {
this.setActiveSession(agent, did)
this.rootStore.shell.setOnboarding(true)
- this.rootStore.log.debug('SessionModel:createAccount succeeded')
+ this._log('SessionModel:createAccount succeeded')
}
/**
* Close all sessions across all accounts.
*/
async logout() {
- this.rootStore.log.debug('SessionModel:logout')
+ this._log('SessionModel:logout')
// TODO
// need to evaluate why deleting the session has caused errors at times
// -prf
diff --git a/src/view/com/auth/withAuthRequired.tsx b/src/view/com/auth/withAuthRequired.tsx
index 11b67f3833..6073a4f801 100644
--- a/src/view/com/auth/withAuthRequired.tsx
+++ b/src/view/com/auth/withAuthRequired.tsx
@@ -22,11 +22,20 @@ export const withAuthRequired = (
function Loading() {
const pal = usePalette('default')
+
+ const [isTakingTooLong, setIsTakingTooLong] = React.useState(false)
+ React.useEffect(() => {
+ const t = setTimeout(() => setIsTakingTooLong(true), 15e3)
+ return () => clearTimeout(t)
+ }, [setIsTakingTooLong])
+
return (
- Firing up the grill...
+ {isTakingTooLong
+ ? "This is taking too long. There may be a problem with your internet or with the service, but we're going to try a couple more times..."
+ : 'Connecting...'}
)