@oxyhq/services 5.13.1 → 5.13.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (204) hide show
  1. package/README.md +71 -0
  2. package/lib/commonjs/core/HttpClient.js +238 -0
  3. package/lib/commonjs/core/HttpClient.js.map +1 -0
  4. package/lib/commonjs/core/OxyServices.js +530 -332
  5. package/lib/commonjs/core/OxyServices.js.map +1 -1
  6. package/lib/commonjs/core/RequestManager.js +199 -0
  7. package/lib/commonjs/core/RequestManager.js.map +1 -0
  8. package/lib/commonjs/core/index.js +38 -1
  9. package/lib/commonjs/core/index.js.map +1 -1
  10. package/lib/commonjs/index.js +36 -0
  11. package/lib/commonjs/index.js.map +1 -1
  12. package/lib/commonjs/ui/components/Avatar.js +94 -27
  13. package/lib/commonjs/ui/components/Avatar.js.map +1 -1
  14. package/lib/commonjs/ui/components/FollowButton.js +1 -0
  15. package/lib/commonjs/ui/components/FollowButton.js.map +1 -1
  16. package/lib/commonjs/ui/components/internal/TextField.js +13 -8
  17. package/lib/commonjs/ui/components/internal/TextField.js.map +1 -1
  18. package/lib/commonjs/ui/context/OxyContext.js +183 -224
  19. package/lib/commonjs/ui/context/OxyContext.js.map +1 -1
  20. package/lib/commonjs/ui/hooks/useSessionSocket.js +80 -22
  21. package/lib/commonjs/ui/hooks/useSessionSocket.js.map +1 -1
  22. package/lib/commonjs/ui/index.js +4 -1
  23. package/lib/commonjs/ui/index.js.map +1 -1
  24. package/lib/commonjs/ui/screens/AccountSettingsScreen.js +32 -2
  25. package/lib/commonjs/ui/screens/AccountSettingsScreen.js.map +1 -1
  26. package/lib/commonjs/ui/screens/AccountSwitcherScreen.js +101 -59
  27. package/lib/commonjs/ui/screens/AccountSwitcherScreen.js.map +1 -1
  28. package/lib/commonjs/ui/screens/FileManagementScreen.js +3 -2
  29. package/lib/commonjs/ui/screens/FileManagementScreen.js.map +1 -1
  30. package/lib/commonjs/ui/screens/LanguageSelectorScreen.js +75 -117
  31. package/lib/commonjs/ui/screens/LanguageSelectorScreen.js.map +1 -1
  32. package/lib/commonjs/ui/screens/SignInScreen.js +0 -11
  33. package/lib/commonjs/ui/screens/SignInScreen.js.map +1 -1
  34. package/lib/commonjs/ui/screens/SignUpScreen.js +14 -16
  35. package/lib/commonjs/ui/screens/SignUpScreen.js.map +1 -1
  36. package/lib/commonjs/ui/screens/WelcomeNewUserScreen.js +50 -18
  37. package/lib/commonjs/ui/screens/WelcomeNewUserScreen.js.map +1 -1
  38. package/lib/commonjs/ui/screens/internal/SignInPasswordStep.js +10 -10
  39. package/lib/commonjs/ui/screens/internal/SignInPasswordStep.js.map +1 -1
  40. package/lib/commonjs/ui/screens/steps/SignInPasswordStep.js +16 -26
  41. package/lib/commonjs/ui/screens/steps/SignInPasswordStep.js.map +1 -1
  42. package/lib/commonjs/ui/screens/steps/SignInUsernameStep.js +104 -212
  43. package/lib/commonjs/ui/screens/steps/SignInUsernameStep.js.map +1 -1
  44. package/lib/commonjs/ui/stores/accountStore.js +237 -0
  45. package/lib/commonjs/ui/stores/accountStore.js.map +1 -0
  46. package/lib/commonjs/ui/stores/authStore.js +2 -1
  47. package/lib/commonjs/ui/stores/authStore.js.map +1 -1
  48. package/lib/commonjs/ui/styles/authStyles.js +14 -7
  49. package/lib/commonjs/ui/styles/authStyles.js.map +1 -1
  50. package/lib/commonjs/utils/asyncUtils.js +9 -22
  51. package/lib/commonjs/utils/asyncUtils.js.map +1 -1
  52. package/lib/commonjs/utils/cache.js +259 -0
  53. package/lib/commonjs/utils/cache.js.map +1 -0
  54. package/lib/commonjs/utils/index.js +99 -0
  55. package/lib/commonjs/utils/index.js.map +1 -1
  56. package/lib/commonjs/utils/languageUtils.js +159 -0
  57. package/lib/commonjs/utils/languageUtils.js.map +1 -0
  58. package/lib/commonjs/utils/requestUtils.js +217 -0
  59. package/lib/commonjs/utils/requestUtils.js.map +1 -0
  60. package/lib/commonjs/utils/sessionUtils.js +191 -0
  61. package/lib/commonjs/utils/sessionUtils.js.map +1 -0
  62. package/lib/module/core/HttpClient.js +232 -0
  63. package/lib/module/core/HttpClient.js.map +1 -0
  64. package/lib/module/core/OxyServices.js +528 -326
  65. package/lib/module/core/OxyServices.js.map +1 -1
  66. package/lib/module/core/RequestManager.js +194 -0
  67. package/lib/module/core/RequestManager.js.map +1 -0
  68. package/lib/module/core/index.js +2 -0
  69. package/lib/module/core/index.js.map +1 -1
  70. package/lib/module/index.js +2 -0
  71. package/lib/module/index.js.map +1 -1
  72. package/lib/module/ui/components/Avatar.js +94 -27
  73. package/lib/module/ui/components/Avatar.js.map +1 -1
  74. package/lib/module/ui/components/FollowButton.js +1 -0
  75. package/lib/module/ui/components/FollowButton.js.map +1 -1
  76. package/lib/module/ui/components/internal/TextField.js +13 -8
  77. package/lib/module/ui/components/internal/TextField.js.map +1 -1
  78. package/lib/module/ui/context/OxyContext.js +182 -223
  79. package/lib/module/ui/context/OxyContext.js.map +1 -1
  80. package/lib/module/ui/hooks/useSessionSocket.js +80 -22
  81. package/lib/module/ui/hooks/useSessionSocket.js.map +1 -1
  82. package/lib/module/ui/index.js +4 -2
  83. package/lib/module/ui/index.js.map +1 -1
  84. package/lib/module/ui/screens/AccountSettingsScreen.js +33 -2
  85. package/lib/module/ui/screens/AccountSettingsScreen.js.map +1 -1
  86. package/lib/module/ui/screens/AccountSwitcherScreen.js +102 -60
  87. package/lib/module/ui/screens/AccountSwitcherScreen.js.map +1 -1
  88. package/lib/module/ui/screens/FileManagementScreen.js +3 -2
  89. package/lib/module/ui/screens/FileManagementScreen.js.map +1 -1
  90. package/lib/module/ui/screens/LanguageSelectorScreen.js +73 -117
  91. package/lib/module/ui/screens/LanguageSelectorScreen.js.map +1 -1
  92. package/lib/module/ui/screens/SignInScreen.js +0 -11
  93. package/lib/module/ui/screens/SignInScreen.js.map +1 -1
  94. package/lib/module/ui/screens/SignUpScreen.js +14 -16
  95. package/lib/module/ui/screens/SignUpScreen.js.map +1 -1
  96. package/lib/module/ui/screens/WelcomeNewUserScreen.js +50 -18
  97. package/lib/module/ui/screens/WelcomeNewUserScreen.js.map +1 -1
  98. package/lib/module/ui/screens/internal/SignInPasswordStep.js +10 -10
  99. package/lib/module/ui/screens/internal/SignInPasswordStep.js.map +1 -1
  100. package/lib/module/ui/screens/steps/SignInPasswordStep.js +16 -26
  101. package/lib/module/ui/screens/steps/SignInPasswordStep.js.map +1 -1
  102. package/lib/module/ui/screens/steps/SignInUsernameStep.js +105 -214
  103. package/lib/module/ui/screens/steps/SignInUsernameStep.js.map +1 -1
  104. package/lib/module/ui/stores/accountStore.js +229 -0
  105. package/lib/module/ui/stores/accountStore.js.map +1 -0
  106. package/lib/module/ui/stores/authStore.js +2 -1
  107. package/lib/module/ui/stores/authStore.js.map +1 -1
  108. package/lib/module/ui/styles/authStyles.js +14 -7
  109. package/lib/module/ui/styles/authStyles.js.map +1 -1
  110. package/lib/module/utils/asyncUtils.js +10 -22
  111. package/lib/module/utils/asyncUtils.js.map +1 -1
  112. package/lib/module/utils/cache.js +250 -0
  113. package/lib/module/utils/cache.js.map +1 -0
  114. package/lib/module/utils/index.js +7 -0
  115. package/lib/module/utils/index.js.map +1 -1
  116. package/lib/module/utils/languageUtils.js +151 -0
  117. package/lib/module/utils/languageUtils.js.map +1 -0
  118. package/lib/module/utils/requestUtils.js +210 -0
  119. package/lib/module/utils/requestUtils.js.map +1 -0
  120. package/lib/module/utils/sessionUtils.js +180 -0
  121. package/lib/module/utils/sessionUtils.js.map +1 -0
  122. package/lib/typescript/core/HttpClient.d.ts +64 -0
  123. package/lib/typescript/core/HttpClient.d.ts.map +1 -0
  124. package/lib/typescript/core/OxyServices.d.ts +82 -71
  125. package/lib/typescript/core/OxyServices.d.ts.map +1 -1
  126. package/lib/typescript/core/RequestManager.d.ts +67 -0
  127. package/lib/typescript/core/RequestManager.d.ts.map +1 -0
  128. package/lib/typescript/core/index.d.ts +2 -0
  129. package/lib/typescript/core/index.d.ts.map +1 -1
  130. package/lib/typescript/index.d.ts +2 -0
  131. package/lib/typescript/index.d.ts.map +1 -1
  132. package/lib/typescript/models/interfaces.d.ts +15 -0
  133. package/lib/typescript/models/interfaces.d.ts.map +1 -1
  134. package/lib/typescript/models/session.d.ts +1 -0
  135. package/lib/typescript/models/session.d.ts.map +1 -1
  136. package/lib/typescript/ui/components/Avatar.d.ts +6 -7
  137. package/lib/typescript/ui/components/Avatar.d.ts.map +1 -1
  138. package/lib/typescript/ui/components/FollowButton.d.ts.map +1 -1
  139. package/lib/typescript/ui/components/internal/TextField.d.ts.map +1 -1
  140. package/lib/typescript/ui/context/OxyContext.d.ts +4 -0
  141. package/lib/typescript/ui/context/OxyContext.d.ts.map +1 -1
  142. package/lib/typescript/ui/hooks/useSessionSocket.d.ts.map +1 -1
  143. package/lib/typescript/ui/index.d.ts +2 -2
  144. package/lib/typescript/ui/index.d.ts.map +1 -1
  145. package/lib/typescript/ui/screens/AccountSettingsScreen.d.ts.map +1 -1
  146. package/lib/typescript/ui/screens/AccountSwitcherScreen.d.ts.map +1 -1
  147. package/lib/typescript/ui/screens/LanguageSelectorScreen.d.ts +3 -3
  148. package/lib/typescript/ui/screens/LanguageSelectorScreen.d.ts.map +1 -1
  149. package/lib/typescript/ui/screens/SignInScreen.d.ts.map +1 -1
  150. package/lib/typescript/ui/screens/SignUpScreen.d.ts.map +1 -1
  151. package/lib/typescript/ui/screens/WelcomeNewUserScreen.d.ts.map +1 -1
  152. package/lib/typescript/ui/screens/internal/SignInPasswordStep.d.ts.map +1 -1
  153. package/lib/typescript/ui/screens/steps/SignInPasswordStep.d.ts.map +1 -1
  154. package/lib/typescript/ui/screens/steps/SignInUsernameStep.d.ts.map +1 -1
  155. package/lib/typescript/ui/stores/accountStore.d.ts +34 -0
  156. package/lib/typescript/ui/stores/accountStore.d.ts.map +1 -0
  157. package/lib/typescript/ui/stores/authStore.d.ts.map +1 -1
  158. package/lib/typescript/ui/styles/authStyles.d.ts +18 -2
  159. package/lib/typescript/ui/styles/authStyles.d.ts.map +1 -1
  160. package/lib/typescript/utils/asyncUtils.d.ts +2 -0
  161. package/lib/typescript/utils/asyncUtils.d.ts.map +1 -1
  162. package/lib/typescript/utils/cache.d.ts +128 -0
  163. package/lib/typescript/utils/cache.d.ts.map +1 -0
  164. package/lib/typescript/utils/index.d.ts +4 -0
  165. package/lib/typescript/utils/index.d.ts.map +1 -1
  166. package/lib/typescript/utils/languageUtils.d.ts +38 -0
  167. package/lib/typescript/utils/languageUtils.d.ts.map +1 -0
  168. package/lib/typescript/utils/requestUtils.d.ts +122 -0
  169. package/lib/typescript/utils/requestUtils.d.ts.map +1 -0
  170. package/lib/typescript/utils/sessionUtils.d.ts +55 -0
  171. package/lib/typescript/utils/sessionUtils.d.ts.map +1 -0
  172. package/package.json +1 -1
  173. package/src/core/HttpClient.ts +277 -0
  174. package/src/core/OxyServices.ts +458 -351
  175. package/src/core/RequestManager.ts +240 -0
  176. package/src/core/index.ts +10 -0
  177. package/src/index.ts +10 -0
  178. package/src/models/interfaces.ts +19 -0
  179. package/src/models/session.ts +1 -1
  180. package/src/ui/components/Avatar.tsx +151 -35
  181. package/src/ui/components/FollowButton.tsx +1 -0
  182. package/src/ui/components/internal/TextField.tsx +7 -6
  183. package/src/ui/context/OxyContext.tsx +213 -217
  184. package/src/ui/hooks/useSessionSocket.ts +72 -18
  185. package/src/ui/index.ts +4 -1
  186. package/src/ui/screens/AccountSettingsScreen.tsx +34 -2
  187. package/src/ui/screens/AccountSwitcherScreen.tsx +102 -68
  188. package/src/ui/screens/FileManagementScreen.tsx +1 -1
  189. package/src/ui/screens/LanguageSelectorScreen.tsx +86 -143
  190. package/src/ui/screens/SignInScreen.tsx +0 -7
  191. package/src/ui/screens/SignUpScreen.tsx +14 -15
  192. package/src/ui/screens/WelcomeNewUserScreen.tsx +52 -15
  193. package/src/ui/screens/internal/SignInPasswordStep.tsx +4 -6
  194. package/src/ui/screens/steps/SignInPasswordStep.tsx +4 -8
  195. package/src/ui/screens/steps/SignInUsernameStep.tsx +110 -256
  196. package/src/ui/stores/accountStore.ts +285 -0
  197. package/src/ui/stores/authStore.ts +2 -1
  198. package/src/ui/styles/authStyles.ts +14 -7
  199. package/src/utils/asyncUtils.ts +10 -24
  200. package/src/utils/cache.ts +264 -0
  201. package/src/utils/index.ts +19 -0
  202. package/src/utils/languageUtils.ts +174 -0
  203. package/src/utils/requestUtils.ts +234 -0
  204. package/src/utils/sessionUtils.ts +206 -0
@@ -0,0 +1,285 @@
1
+ import { create } from 'zustand';
2
+ import { shallow } from 'zustand/shallow';
3
+ import type { OxyServices } from '../../core';
4
+
5
+ export interface QuickAccount {
6
+ sessionId: string;
7
+ userId?: string; // User ID for deduplication
8
+ username: string;
9
+ displayName: string;
10
+ avatar?: string;
11
+ avatarUrl?: string; // Cached avatar URL to prevent recalculation
12
+ }
13
+
14
+ interface AccountState {
15
+ // Account data
16
+ accounts: Record<string, QuickAccount>;
17
+ accountOrder: string[]; // Maintain order for display
18
+ accountsArray: QuickAccount[]; // Cached array to prevent infinite loops
19
+
20
+ // Loading states
21
+ loading: boolean;
22
+ loadingSessionIds: Set<string>;
23
+
24
+ // Error state
25
+ error: string | null;
26
+
27
+ // Actions
28
+ setAccounts: (accounts: QuickAccount[]) => void;
29
+ addAccount: (account: QuickAccount) => void;
30
+ updateAccount: (sessionId: string, updates: Partial<QuickAccount>) => void;
31
+ removeAccount: (sessionId: string) => void;
32
+ moveAccountToTop: (sessionId: string) => void;
33
+
34
+ // Loading actions
35
+ setLoading: (loading: boolean) => void;
36
+ setLoadingSession: (sessionId: string, loading: boolean) => void;
37
+
38
+ // Error actions
39
+ setError: (error: string | null) => void;
40
+
41
+ // Load accounts from API
42
+ loadAccounts: (sessionIds: string[], oxyServices: OxyServices, existingAccounts?: QuickAccount[], preserveOrder?: boolean) => Promise<void>;
43
+
44
+ // Reset store
45
+ reset: () => void;
46
+ }
47
+
48
+ const initialState = {
49
+ accounts: {} as Record<string, QuickAccount>,
50
+ accountOrder: [] as string[],
51
+ accountsArray: [] as QuickAccount[],
52
+ loading: false,
53
+ loadingSessionIds: new Set<string>(),
54
+ error: null,
55
+ };
56
+
57
+ // Helper: Build accounts array from accounts map and order
58
+ const buildAccountsArray = (accounts: Record<string, QuickAccount>, order: string[]): QuickAccount[] => {
59
+ const result: QuickAccount[] = [];
60
+ for (const id of order) {
61
+ const account = accounts[id];
62
+ if (account) result.push(account);
63
+ }
64
+ return result;
65
+ };
66
+
67
+ // Helper: Create QuickAccount from user data
68
+ const createQuickAccount = (sessionId: string, userData: any, existingAccount?: QuickAccount, oxyServices?: OxyServices): QuickAccount => {
69
+ const displayName = userData.name?.full || userData.name?.first || userData.username || 'Account';
70
+ const userId = userData.id || userData._id?.toString();
71
+
72
+ // Preserve existing avatarUrl if avatar hasn't changed (prevents image reload)
73
+ let avatarUrl: string | undefined;
74
+ if (existingAccount && existingAccount.avatar === userData.avatar && existingAccount.avatarUrl) {
75
+ avatarUrl = existingAccount.avatarUrl; // Reuse existing URL
76
+ } else if (userData.avatar && oxyServices) {
77
+ avatarUrl = oxyServices.getFileDownloadUrl(userData.avatar, 'thumb');
78
+ }
79
+
80
+ return {
81
+ sessionId,
82
+ userId,
83
+ username: userData.username || '',
84
+ displayName,
85
+ avatar: userData.avatar,
86
+ avatarUrl,
87
+ };
88
+ };
89
+
90
+ export const useAccountStore = create<AccountState>((set, get) => ({
91
+ ...initialState,
92
+
93
+ setAccounts: (accounts) => set((state) => {
94
+ const accountMap: Record<string, QuickAccount> = {};
95
+ const order: string[] = [];
96
+ const seenSessionIds = new Set<string>();
97
+
98
+ for (const account of accounts) {
99
+ if (seenSessionIds.has(account.sessionId)) continue;
100
+ seenSessionIds.add(account.sessionId);
101
+ accountMap[account.sessionId] = account;
102
+ order.push(account.sessionId);
103
+ }
104
+
105
+ const accountsArray = buildAccountsArray(accountMap, order);
106
+ const sameOrder = order.length === state.accountOrder.length &&
107
+ order.every((id, i) => id === state.accountOrder[i]);
108
+ const sameAccounts = sameOrder &&
109
+ order.every(id => {
110
+ const existing = state.accounts[id];
111
+ const newAccount = accountMap[id];
112
+ return existing &&
113
+ existing.sessionId === newAccount.sessionId &&
114
+ existing.userId === newAccount.userId &&
115
+ existing.avatar === newAccount.avatar &&
116
+ existing.avatarUrl === newAccount.avatarUrl;
117
+ });
118
+
119
+ if (sameAccounts) return {} as any;
120
+
121
+ return { accounts: accountMap, accountOrder: order, accountsArray };
122
+ }),
123
+
124
+ addAccount: (account) => set((state) => {
125
+ // Check if account with same sessionId exists
126
+ if (state.accounts[account.sessionId]) {
127
+ // Update existing
128
+ const existing = state.accounts[account.sessionId];
129
+ if (existing.avatar === account.avatar && existing.avatarUrl === account.avatarUrl) {
130
+ return {} as any; // No change
131
+ }
132
+ const newAccounts = { ...state.accounts, [account.sessionId]: account };
133
+ return {
134
+ accounts: newAccounts,
135
+ accountsArray: buildAccountsArray(newAccounts, state.accountOrder),
136
+ };
137
+ }
138
+
139
+ const newAccounts = { ...state.accounts, [account.sessionId]: account };
140
+ const newOrder = [account.sessionId, ...state.accountOrder];
141
+ return {
142
+ accounts: newAccounts,
143
+ accountOrder: newOrder,
144
+ accountsArray: buildAccountsArray(newAccounts, newOrder),
145
+ };
146
+ }),
147
+
148
+ updateAccount: (sessionId, updates) => set((state) => {
149
+ const existing = state.accounts[sessionId];
150
+ if (!existing) return {} as any;
151
+
152
+ const updated = { ...existing, ...updates };
153
+ if (existing.avatar === updated.avatar && existing.avatarUrl === updated.avatarUrl) {
154
+ return {} as any; // No change
155
+ }
156
+
157
+ const newAccounts = { ...state.accounts, [sessionId]: updated };
158
+ return {
159
+ accounts: newAccounts,
160
+ accountsArray: buildAccountsArray(newAccounts, state.accountOrder),
161
+ };
162
+ }),
163
+
164
+ removeAccount: (sessionId) => set((state) => {
165
+ if (!state.accounts[sessionId]) return {} as any;
166
+
167
+ const { [sessionId]: _removed, ...rest } = state.accounts;
168
+ const newOrder = state.accountOrder.filter(id => id !== sessionId);
169
+
170
+ return {
171
+ accounts: rest,
172
+ accountOrder: newOrder,
173
+ accountsArray: buildAccountsArray(rest, newOrder),
174
+ };
175
+ }),
176
+
177
+ moveAccountToTop: (sessionId) => set((state) => {
178
+ if (!state.accounts[sessionId]) return {} as any;
179
+
180
+ const filtered = state.accountOrder.filter(id => id !== sessionId);
181
+ const newOrder = [sessionId, ...filtered];
182
+
183
+ return {
184
+ accountOrder: newOrder,
185
+ accountsArray: buildAccountsArray(state.accounts, newOrder),
186
+ };
187
+ }),
188
+
189
+ setLoading: (loading) => set({ loading }),
190
+
191
+ setLoadingSession: (sessionId, loading) => set((state) => {
192
+ const newSet = new Set(state.loadingSessionIds);
193
+ if (loading) {
194
+ newSet.add(sessionId);
195
+ } else {
196
+ newSet.delete(sessionId);
197
+ }
198
+ return { loadingSessionIds: newSet };
199
+ }),
200
+
201
+ setError: (error) => set({ error }),
202
+
203
+ loadAccounts: async (sessionIds, oxyServices, existingAccounts = [], preserveOrder = true) => {
204
+ const state = get();
205
+
206
+ const uniqueSessionIds = Array.from(new Set(sessionIds));
207
+ if (uniqueSessionIds.length === 0) {
208
+ get().setAccounts([]);
209
+ return;
210
+ }
211
+
212
+ const existingMap = new Map(existingAccounts.map(a => [a.sessionId, a]));
213
+ for (const account of Object.values(state.accounts)) {
214
+ existingMap.set(account.sessionId, account);
215
+ }
216
+
217
+ const missingSessionIds = uniqueSessionIds.filter(id => !existingMap.has(id));
218
+
219
+ if (missingSessionIds.length === 0) {
220
+ const ordered = uniqueSessionIds
221
+ .map(id => existingMap.get(id))
222
+ .filter((acc): acc is QuickAccount => acc !== undefined);
223
+ get().setAccounts(ordered);
224
+ return;
225
+ }
226
+
227
+ if (state.loading) {
228
+ return;
229
+ }
230
+
231
+ set({ loading: true, error: null });
232
+
233
+ try {
234
+ const batchResults = await oxyServices.getUsersBySessions(missingSessionIds);
235
+
236
+ const accountMap = new Map<string, QuickAccount>();
237
+
238
+ for (const { sessionId, user: userData } of batchResults) {
239
+ if (userData && !accountMap.has(sessionId)) {
240
+ const existing = existingMap.get(sessionId);
241
+ accountMap.set(sessionId, createQuickAccount(sessionId, userData, existing, oxyServices));
242
+ }
243
+ }
244
+
245
+ for (const [sessionId, account] of accountMap) {
246
+ existingMap.set(sessionId, account);
247
+ }
248
+
249
+ const orderToUse = preserveOrder ? uniqueSessionIds : [...uniqueSessionIds, ...state.accountOrder];
250
+ const seen = new Set<string>();
251
+ const ordered: QuickAccount[] = [];
252
+
253
+ for (const sessionId of orderToUse) {
254
+ if (seen.has(sessionId)) continue;
255
+ seen.add(sessionId);
256
+
257
+ const account = existingMap.get(sessionId);
258
+ if (account) ordered.push(account);
259
+ }
260
+
261
+ get().setAccounts(ordered);
262
+ } catch (error) {
263
+ const errorMessage = error instanceof Error ? error.message : 'Failed to load accounts';
264
+ if (__DEV__) {
265
+ console.error('AccountStore: Failed to load accounts:', error);
266
+ }
267
+ set({ error: errorMessage });
268
+ } finally {
269
+ set({ loading: false });
270
+ }
271
+ },
272
+
273
+ reset: () => set(initialState),
274
+ }));
275
+
276
+ // Selectors for performance - return cached array to prevent infinite loops
277
+ export const useAccounts = (): QuickAccount[] => {
278
+ return useAccountStore(state => state.accountsArray);
279
+ };
280
+
281
+ export const useAccountLoading = () => useAccountStore(s => s.loading);
282
+ export const useAccountError = () => useAccountStore(s => s.error);
283
+ export const useAccountLoadingSession = (sessionId: string) =>
284
+ useAccountStore(s => s.loadingSessionIds.has(sessionId));
285
+
@@ -56,7 +56,8 @@ export const useAuthStore = create<AuthState>((set: (state: Partial<AuthState>)
56
56
  try {
57
57
  await oxyServices.updateProfile(updates);
58
58
  // Immediately fetch the latest user data after update
59
- await useAuthStore.getState().fetchUser({ getCurrentUser: oxyServices.getCurrentUser }, true);
59
+ // Use arrow function to preserve 'this' context
60
+ await useAuthStore.getState().fetchUser({ getCurrentUser: () => oxyServices.getCurrentUser() }, true);
60
61
  } catch (error) {
61
62
  const errorMessage = error instanceof Error ? error.message : 'Failed to update user';
62
63
  if (__DEV__) {
@@ -191,13 +191,20 @@ export const createAuthStyles = (colors: AuthThemeColors, theme: string) => Styl
191
191
  paddingHorizontal: 32,
192
192
  borderRadius: 16,
193
193
  marginVertical: 8,
194
- shadowOffset: {
195
- width: 0,
196
- height: 4,
197
- },
198
- shadowOpacity: 0.3,
199
- shadowRadius: 8,
200
- elevation: 6,
194
+ ...Platform.select({
195
+ web: {
196
+ boxShadow: '0 4px 8px rgba(0,0,0,0.3)',
197
+ },
198
+ default: {
199
+ shadowOffset: {
200
+ width: 0,
201
+ height: 4,
202
+ },
203
+ shadowOpacity: 0.3,
204
+ shadowRadius: 8,
205
+ elevation: 6,
206
+ }
207
+ }),
201
208
  gap: 8,
202
209
  width: '100%',
203
210
  },
@@ -2,6 +2,8 @@
2
2
  * Async utilities for common asynchronous patterns and error handling
3
3
  */
4
4
 
5
+ import { TTLCache, registerCacheForCleanup } from './cache';
6
+
5
7
  /**
6
8
  * Wrapper for async operations with automatic error handling
7
9
  */
@@ -196,36 +198,20 @@ export async function withTimeout<T>(
196
198
 
197
199
  /**
198
200
  * Cache async operation results
201
+ * @deprecated Use TTLCache from '../utils/cache' instead
202
+ * This function is kept for backward compatibility but will be removed in a future version
199
203
  */
200
204
  export function createAsyncCache<T>(
201
205
  ttl: number = 5 * 60 * 1000 // 5 minutes default
202
206
  ) {
203
- const cache = new Map<string, { data: T; timestamp: number }>();
207
+ const cache = new TTLCache<T>(ttl);
208
+ registerCacheForCleanup(cache);
204
209
 
205
210
  return {
206
- get: (key: string): T | null => {
207
- const item = cache.get(key);
208
- if (!item) return null;
209
-
210
- if (Date.now() - item.timestamp > ttl) {
211
- cache.delete(key);
212
- return null;
213
- }
214
-
215
- return item.data;
216
- },
217
-
218
- set: (key: string, data: T): void => {
219
- cache.set(key, { data, timestamp: Date.now() });
220
- },
221
-
222
- clear: (): void => {
223
- cache.clear();
224
- },
225
-
226
- delete: (key: string): boolean => {
227
- return cache.delete(key);
228
- }
211
+ get: (key: string): T | null => cache.get(key),
212
+ set: (key: string, data: T): void => cache.set(key, data),
213
+ clear: (): void => cache.clear(),
214
+ delete: (key: string): boolean => cache.delete(key),
229
215
  };
230
216
  }
231
217
 
@@ -0,0 +1,264 @@
1
+ /**
2
+ * Centralized cache utility with TTL support
3
+ *
4
+ * This is a production-ready cache implementation used across the codebase
5
+ * for consistent caching behavior and performance optimization.
6
+ */
7
+
8
+ /**
9
+ * Cache entry with expiration tracking
10
+ */
11
+ interface CacheEntry<T> {
12
+ data: T;
13
+ timestamp: number;
14
+ expiresAt: number;
15
+ }
16
+
17
+ /**
18
+ * Cache statistics
19
+ */
20
+ export interface CacheStats {
21
+ size: number;
22
+ hits: number;
23
+ misses: number;
24
+ hitRate: number;
25
+ }
26
+
27
+ /**
28
+ * TTL-based cache implementation
29
+ *
30
+ * Features:
31
+ * - Automatic expiration based on TTL
32
+ * - Manual cleanup of expired entries
33
+ * - Statistics tracking (hits, misses, hit rate)
34
+ * - Type-safe generic interface
35
+ *
36
+ * @example
37
+ * ```typescript
38
+ * const cache = new TTLCache<string>(5 * 60 * 1000); // 5 minutes
39
+ *
40
+ * // Set with default TTL
41
+ * cache.set('key', 'value');
42
+ *
43
+ * // Set with custom TTL
44
+ * cache.set('key', 'value', 10 * 60 * 1000); // 10 minutes
45
+ *
46
+ * // Get value
47
+ * const value = cache.get('key');
48
+ *
49
+ * // Get statistics
50
+ * const stats = cache.getStats();
51
+ * ```
52
+ */
53
+ export class TTLCache<T> {
54
+ private cache = new Map<string, CacheEntry<T>>();
55
+ private defaultTTL: number;
56
+ private hits = 0;
57
+ private misses = 0;
58
+
59
+ /**
60
+ * Create a new TTL cache
61
+ * @param defaultTTL Default TTL in milliseconds (default: 5 minutes)
62
+ */
63
+ constructor(defaultTTL: number = 5 * 60 * 1000) {
64
+ this.defaultTTL = defaultTTL;
65
+ }
66
+
67
+ /**
68
+ * Get a value from cache
69
+ * @param key Cache key
70
+ * @returns Cached value or null if not found or expired
71
+ */
72
+ get(key: string): T | null {
73
+ const entry = this.cache.get(key);
74
+ if (!entry) {
75
+ this.misses++;
76
+ return null;
77
+ }
78
+
79
+ const now = Date.now();
80
+ if (now > entry.expiresAt) {
81
+ this.cache.delete(key);
82
+ this.misses++;
83
+ return null;
84
+ }
85
+
86
+ this.hits++;
87
+ return entry.data;
88
+ }
89
+
90
+ /**
91
+ * Set a value in cache
92
+ * @param key Cache key
93
+ * @param data Data to cache
94
+ * @param ttl Optional TTL override (uses default if not provided)
95
+ */
96
+ set(key: string, data: T, ttl?: number): void {
97
+ const now = Date.now();
98
+ const expiresAt = now + (ttl || this.defaultTTL);
99
+ this.cache.set(key, { data, timestamp: now, expiresAt });
100
+ }
101
+
102
+ /**
103
+ * Delete a specific cache entry
104
+ * @param key Cache key
105
+ * @returns true if entry was deleted, false if not found
106
+ */
107
+ delete(key: string): boolean {
108
+ return this.cache.delete(key);
109
+ }
110
+
111
+ /**
112
+ * Clear all cache entries
113
+ */
114
+ clear(): void {
115
+ this.cache.clear();
116
+ this.hits = 0;
117
+ this.misses = 0;
118
+ }
119
+
120
+ /**
121
+ * Check if a key exists and is not expired
122
+ * @param key Cache key
123
+ * @returns true if key exists and is valid
124
+ */
125
+ has(key: string): boolean {
126
+ const entry = this.cache.get(key);
127
+ if (!entry) return false;
128
+
129
+ if (Date.now() > entry.expiresAt) {
130
+ this.cache.delete(key);
131
+ return false;
132
+ }
133
+
134
+ return true;
135
+ }
136
+
137
+ /**
138
+ * Get all valid cache keys
139
+ * @returns Array of valid cache keys
140
+ */
141
+ keys(): string[] {
142
+ const now = Date.now();
143
+ const validKeys: string[] = [];
144
+
145
+ for (const [key, entry] of this.cache.entries()) {
146
+ if (now <= entry.expiresAt) {
147
+ validKeys.push(key);
148
+ } else {
149
+ this.cache.delete(key);
150
+ }
151
+ }
152
+
153
+ return validKeys;
154
+ }
155
+
156
+ /**
157
+ * Clean up expired entries
158
+ * @returns Number of entries removed
159
+ */
160
+ cleanup(): number {
161
+ const now = Date.now();
162
+ let removed = 0;
163
+
164
+ for (const [key, entry] of this.cache.entries()) {
165
+ if (now > entry.expiresAt) {
166
+ this.cache.delete(key);
167
+ removed++;
168
+ }
169
+ }
170
+
171
+ return removed;
172
+ }
173
+
174
+ /**
175
+ * Get cache size (number of entries)
176
+ */
177
+ size(): number {
178
+ return this.cache.size;
179
+ }
180
+
181
+ /**
182
+ * Get cache statistics
183
+ */
184
+ getStats(): CacheStats {
185
+ const total = this.hits + this.misses;
186
+ return {
187
+ size: this.cache.size,
188
+ hits: this.hits,
189
+ misses: this.misses,
190
+ hitRate: total > 0 ? this.hits / total : 0,
191
+ };
192
+ }
193
+
194
+ /**
195
+ * Reset statistics (keeps cache entries)
196
+ */
197
+ resetStats(): void {
198
+ this.hits = 0;
199
+ this.misses = 0;
200
+ }
201
+ }
202
+
203
+ /**
204
+ * Create a TTL cache instance (convenience function)
205
+ *
206
+ * @example
207
+ * ```typescript
208
+ * const cache = createCache<string>(5 * 60 * 1000);
209
+ * ```
210
+ */
211
+ export function createCache<T>(ttl: number = 5 * 60 * 1000): TTLCache<T> {
212
+ return new TTLCache<T>(ttl);
213
+ }
214
+
215
+ /**
216
+ * Global cache cleanup interval (runs every minute)
217
+ * This helps prevent memory leaks from expired cache entries
218
+ */
219
+ let cleanupInterval: NodeJS.Timeout | null = null;
220
+ const activeCaches = new Set<TTLCache<any>>();
221
+
222
+ /**
223
+ * Register a cache for automatic cleanup
224
+ * @param cache Cache instance to register
225
+ */
226
+ export function registerCacheForCleanup(cache: TTLCache<any>): void {
227
+ activeCaches.add(cache);
228
+
229
+ // Start cleanup interval if not already running
230
+ if (!cleanupInterval) {
231
+ cleanupInterval = setInterval(() => {
232
+ for (const cache of activeCaches) {
233
+ cache.cleanup();
234
+ }
235
+ }, 60000); // Every minute
236
+ }
237
+ }
238
+
239
+ /**
240
+ * Unregister a cache from automatic cleanup
241
+ * @param cache Cache instance to unregister
242
+ */
243
+ export function unregisterCacheFromCleanup(cache: TTLCache<any>): void {
244
+ activeCaches.delete(cache);
245
+
246
+ // Stop cleanup interval if no caches are registered
247
+ if (activeCaches.size === 0 && cleanupInterval) {
248
+ clearInterval(cleanupInterval);
249
+ cleanupInterval = null;
250
+ }
251
+ }
252
+
253
+ /**
254
+ * Stop all cache cleanup intervals
255
+ * Useful for tests to prevent Jest from hanging
256
+ */
257
+ export function stopAllCacheCleanup(): void {
258
+ if (cleanupInterval) {
259
+ clearInterval(cleanupInterval);
260
+ cleanupInterval = null;
261
+ }
262
+ activeCaches.clear();
263
+ }
264
+
@@ -1,2 +1,21 @@
1
1
  export { DeviceManager } from './deviceManager';
2
2
  export type { DeviceFingerprint, StoredDeviceInfo } from './deviceManager';
3
+
4
+ // Request utilities
5
+ export { RequestDeduplicator, RequestQueue, SimpleLogger } from './requestUtils';
6
+
7
+ // Cache utilities
8
+ export { TTLCache, createCache, registerCacheForCleanup, unregisterCacheFromCleanup, stopAllCacheCleanup } from './cache';
9
+ export type { CacheStats } from './cache';
10
+
11
+ // Session utilities
12
+ export {
13
+ normalizeSession,
14
+ sortSessions,
15
+ deduplicateSessions,
16
+ deduplicateSessionsByUserId,
17
+ normalizeAndSortSessions,
18
+ mergeSessions,
19
+ sessionsEqual,
20
+ sessionsArraysEqual
21
+ } from './sessionUtils';