mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
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}