mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
at verify-code 5.3 kB view raw
1import {AtpSessionEvent} from '@atproto/api' 2 3import {createPublicAgent} from './agent' 4import {wrapSessionReducerForLogging} from './logging' 5import {SessionAccount} from './types' 6 7// A hack so that the reducer can't read anything from the agent. 8// From the reducer's point of view, it should be a completely opaque object. 9type OpaqueBskyAgent = { 10 readonly service: URL 11 readonly api: unknown 12 readonly app: unknown 13 readonly com: unknown 14} 15 16type AgentState = { 17 readonly agent: OpaqueBskyAgent 18 readonly did: string | undefined 19} 20 21export type State = { 22 readonly accounts: SessionAccount[] 23 readonly currentAgentState: AgentState 24 needsPersist: boolean // Mutated in an effect. 25} 26 27export type Action = 28 | { 29 type: 'received-agent-event' 30 agent: OpaqueBskyAgent 31 accountDid: string 32 refreshedAccount: SessionAccount | undefined 33 sessionEvent: AtpSessionEvent 34 } 35 | { 36 type: 'switched-to-account' 37 newAgent: OpaqueBskyAgent 38 newAccount: SessionAccount 39 } 40 | { 41 type: 'removed-account' 42 accountDid: string 43 } 44 | { 45 type: 'logged-out-current-account' 46 } 47 | { 48 type: 'logged-out-every-account' 49 } 50 | { 51 type: 'synced-accounts' 52 syncedAccounts: SessionAccount[] 53 syncedCurrentDid: string | undefined 54 } 55 56function createPublicAgentState(): AgentState { 57 return { 58 agent: createPublicAgent(), 59 did: undefined, 60 } 61} 62 63export function getInitialState(persistedAccounts: SessionAccount[]): State { 64 return { 65 accounts: persistedAccounts, 66 currentAgentState: createPublicAgentState(), 67 needsPersist: false, 68 } 69} 70 71let reducer = (state: State, action: Action): State => { 72 switch (action.type) { 73 case 'received-agent-event': { 74 const {agent, accountDid, refreshedAccount, sessionEvent} = action 75 if ( 76 refreshedAccount === undefined && 77 agent !== state.currentAgentState.agent 78 ) { 79 // If the session got cleared out (e.g. due to expiry or network error) but 80 // this account isn't the active one, don't clear it out at this time. 81 // This way, if the problem is transient, it'll work on next resume. 82 return state 83 } 84 if (sessionEvent === 'network-error') { 85 // Assume it's transient. 86 return state 87 } 88 const existingAccount = state.accounts.find(a => a.did === accountDid) 89 if ( 90 !existingAccount || 91 JSON.stringify(existingAccount) === JSON.stringify(refreshedAccount) 92 ) { 93 // Fast path without a state update. 94 return state 95 } 96 return { 97 accounts: state.accounts.map(a => { 98 if (a.did === accountDid) { 99 if (refreshedAccount) { 100 return refreshedAccount 101 } else { 102 return { 103 ...a, 104 // If we didn't receive a refreshed account, clear out the tokens. 105 accessJwt: undefined, 106 refreshJwt: undefined, 107 } 108 } 109 } else { 110 return a 111 } 112 }), 113 currentAgentState: refreshedAccount 114 ? state.currentAgentState 115 : createPublicAgentState(), // Log out if expired. 116 needsPersist: true, 117 } 118 } 119 case 'switched-to-account': { 120 const {newAccount, newAgent} = action 121 return { 122 accounts: [ 123 newAccount, 124 ...state.accounts.filter(a => a.did !== newAccount.did), 125 ], 126 currentAgentState: { 127 did: newAccount.did, 128 agent: newAgent, 129 }, 130 needsPersist: true, 131 } 132 } 133 case 'removed-account': { 134 const {accountDid} = action 135 return { 136 accounts: state.accounts.filter(a => a.did !== accountDid), 137 currentAgentState: 138 state.currentAgentState.did === accountDid 139 ? createPublicAgentState() // Log out if removing the current one. 140 : state.currentAgentState, 141 needsPersist: true, 142 } 143 } 144 case 'logged-out-current-account': { 145 const {currentAgentState} = state 146 return { 147 accounts: state.accounts.map(a => 148 a.did === currentAgentState.did 149 ? { 150 ...a, 151 refreshJwt: undefined, 152 accessJwt: undefined, 153 } 154 : a, 155 ), 156 currentAgentState: createPublicAgentState(), 157 needsPersist: true, 158 } 159 } 160 case 'logged-out-every-account': { 161 return { 162 accounts: state.accounts.map(a => ({ 163 ...a, 164 // Clear tokens for *every* account (this is a hard logout). 165 refreshJwt: undefined, 166 accessJwt: undefined, 167 })), 168 currentAgentState: createPublicAgentState(), 169 needsPersist: true, 170 } 171 } 172 case 'synced-accounts': { 173 const {syncedAccounts, syncedCurrentDid} = action 174 return { 175 accounts: syncedAccounts, 176 currentAgentState: 177 syncedCurrentDid === state.currentAgentState.did 178 ? state.currentAgentState 179 : createPublicAgentState(), // Log out if different user. 180 needsPersist: false, // Synced from another tab. Don't persist to avoid cycles. 181 } 182 } 183 } 184} 185reducer = wrapSessionReducerForLogging(reducer) 186export {reducer}