@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,174 @@
1
+ /**
2
+ * Language utilities for OxyServices
3
+ * Provides access to supported languages and language metadata
4
+ */
5
+
6
+ export interface LanguageMetadata {
7
+ id: string;
8
+ name: string;
9
+ nativeName: string;
10
+ flag: string;
11
+ icon: string;
12
+ color: string;
13
+ }
14
+
15
+ // Supported languages with their metadata
16
+ export const SUPPORTED_LANGUAGES: LanguageMetadata[] = [
17
+ {
18
+ id: 'en-US',
19
+ name: 'English',
20
+ nativeName: 'English',
21
+ flag: '🇺🇸',
22
+ icon: 'language-outline',
23
+ color: '#007AFF',
24
+ },
25
+ {
26
+ id: 'es-ES',
27
+ name: 'Spanish',
28
+ nativeName: 'Español',
29
+ flag: '🇪🇸',
30
+ icon: 'language-outline',
31
+ color: '#FF3B30',
32
+ },
33
+ {
34
+ id: 'ca-ES',
35
+ name: 'Catalan',
36
+ nativeName: 'Català',
37
+ flag: '🇪🇸',
38
+ icon: 'language-outline',
39
+ color: '#0CA678',
40
+ },
41
+ {
42
+ id: 'fr-FR',
43
+ name: 'French',
44
+ nativeName: 'Français',
45
+ flag: '🇫🇷',
46
+ icon: 'language-outline',
47
+ color: '#5856D6',
48
+ },
49
+ {
50
+ id: 'de-DE',
51
+ name: 'German',
52
+ nativeName: 'Deutsch',
53
+ flag: '🇩🇪',
54
+ icon: 'language-outline',
55
+ color: '#FF9500',
56
+ },
57
+ {
58
+ id: 'it-IT',
59
+ name: 'Italian',
60
+ nativeName: 'Italiano',
61
+ flag: '🇮🇹',
62
+ icon: 'language-outline',
63
+ color: '#34C759',
64
+ },
65
+ {
66
+ id: 'pt-PT',
67
+ name: 'Portuguese',
68
+ nativeName: 'Português',
69
+ flag: '🇵🇹',
70
+ icon: 'language-outline',
71
+ color: '#AF52DE',
72
+ },
73
+ {
74
+ id: 'ja-JP',
75
+ name: 'Japanese',
76
+ nativeName: '日本語',
77
+ flag: '🇯🇵',
78
+ icon: 'language-outline',
79
+ color: '#FF2D92',
80
+ },
81
+ {
82
+ id: 'ko-KR',
83
+ name: 'Korean',
84
+ nativeName: '한국어',
85
+ flag: '🇰🇷',
86
+ icon: 'language-outline',
87
+ color: '#32D74B',
88
+ },
89
+ {
90
+ id: 'zh-CN',
91
+ name: 'Chinese',
92
+ nativeName: '中文',
93
+ flag: '🇨🇳',
94
+ icon: 'language-outline',
95
+ color: '#FF9F0A',
96
+ },
97
+ {
98
+ id: 'ar-SA',
99
+ name: 'Arabic',
100
+ nativeName: 'العربية',
101
+ flag: '🇸🇦',
102
+ icon: 'language-outline',
103
+ color: '#30B0C7',
104
+ },
105
+ ];
106
+
107
+ const FALLBACK_LANGUAGE = 'en-US';
108
+
109
+ /**
110
+ * Get language metadata by language code
111
+ * @param languageCode - BCP-47 language code (e.g., 'en-US', 'es-ES')
112
+ * @returns Language metadata or null if not found
113
+ */
114
+ export function getLanguageMetadata(languageCode: string | null | undefined): LanguageMetadata | null {
115
+ if (!languageCode) return null;
116
+
117
+ // Direct match
118
+ const exactMatch = SUPPORTED_LANGUAGES.find(lang => lang.id === languageCode);
119
+ if (exactMatch) return exactMatch;
120
+
121
+ // Try to match base language code (e.g., 'en' matches 'en-US')
122
+ const baseCode = languageCode.split('-')[0];
123
+ const baseMatch = SUPPORTED_LANGUAGES.find(lang => lang.id.startsWith(baseCode + '-'));
124
+ if (baseMatch) return baseMatch;
125
+
126
+ return null;
127
+ }
128
+
129
+ /**
130
+ * Get language name by language code
131
+ * @param languageCode - BCP-47 language code (e.g., 'en-US', 'es-ES')
132
+ * @returns Language name (e.g., 'English') or the code if not found
133
+ */
134
+ export function getLanguageName(languageCode: string | null | undefined): string {
135
+ const metadata = getLanguageMetadata(languageCode);
136
+ return metadata?.name || languageCode || FALLBACK_LANGUAGE;
137
+ }
138
+
139
+ /**
140
+ * Get native language name by language code
141
+ * @param languageCode - BCP-47 language code (e.g., 'en-US', 'es-ES')
142
+ * @returns Native language name (e.g., 'Español') or the code if not found
143
+ */
144
+ export function getNativeLanguageName(languageCode: string | null | undefined): string {
145
+ const metadata = getLanguageMetadata(languageCode);
146
+ return metadata?.nativeName || languageCode || FALLBACK_LANGUAGE;
147
+ }
148
+
149
+ /**
150
+ * Normalize language code to BCP-47 format
151
+ * @param lang - Language code (may be short like 'en' or full like 'en-US')
152
+ * @returns Normalized BCP-47 language code
153
+ */
154
+ export function normalizeLanguageCode(lang?: string | null): string {
155
+ if (!lang) return FALLBACK_LANGUAGE;
156
+ if (lang.includes('-')) return lang;
157
+
158
+ const map: Record<string, string> = {
159
+ en: 'en-US',
160
+ es: 'es-ES',
161
+ ca: 'ca-ES',
162
+ fr: 'fr-FR',
163
+ de: 'de-DE',
164
+ it: 'it-IT',
165
+ pt: 'pt-PT',
166
+ ja: 'ja-JP',
167
+ ko: 'ko-KR',
168
+ zh: 'zh-CN',
169
+ ar: 'ar-SA',
170
+ };
171
+
172
+ return map[lang] || lang;
173
+ }
174
+
@@ -0,0 +1,234 @@
1
+ /**
2
+ * Request utilities for HTTP clients
3
+ *
4
+ * Provides reusable components for request deduplication, queuing, and logging
5
+ */
6
+
7
+ /**
8
+ * Request deduplication - prevents duplicate concurrent requests
9
+ *
10
+ * When multiple requests with the same key are made simultaneously,
11
+ * only one request is executed and all callers receive the same result.
12
+ *
13
+ * @example
14
+ * ```typescript
15
+ * const deduplicator = new RequestDeduplicator();
16
+ *
17
+ * // Multiple calls with same key will share the same promise
18
+ * const promise1 = deduplicator.deduplicate('user-123', () => fetchUser('123'));
19
+ * const promise2 = deduplicator.deduplicate('user-123', () => fetchUser('123'));
20
+ * // promise1 === promise2, only one API call is made
21
+ * ```
22
+ */
23
+ export class RequestDeduplicator {
24
+ private pendingRequests = new Map<string, Promise<any>>();
25
+
26
+ /**
27
+ * Deduplicate a request by key
28
+ * @param key Unique key for the request
29
+ * @param requestFn Function that returns a promise
30
+ * @returns Promise that will be shared if key already exists
31
+ */
32
+ async deduplicate<T>(
33
+ key: string,
34
+ requestFn: () => Promise<T>
35
+ ): Promise<T> {
36
+ const existing = this.pendingRequests.get(key);
37
+ if (existing) {
38
+ return existing;
39
+ }
40
+
41
+ const promise = requestFn()
42
+ .finally(() => {
43
+ this.pendingRequests.delete(key);
44
+ });
45
+
46
+ this.pendingRequests.set(key, promise);
47
+ return promise;
48
+ }
49
+
50
+ /**
51
+ * Clear all pending requests
52
+ */
53
+ clear(): void {
54
+ this.pendingRequests.clear();
55
+ }
56
+
57
+ /**
58
+ * Get number of pending requests
59
+ */
60
+ size(): number {
61
+ return this.pendingRequests.size;
62
+ }
63
+ }
64
+
65
+ /**
66
+ * Request queue with concurrency control
67
+ *
68
+ * Limits the number of concurrent requests and queues excess requests.
69
+ * Useful for rate limiting and preventing request flooding.
70
+ *
71
+ * @example
72
+ * ```typescript
73
+ * const queue = new RequestQueue(5, 100); // Max 5 concurrent, queue up to 100
74
+ *
75
+ * // All requests will be queued and processed with max 5 concurrent
76
+ * await queue.enqueue(() => fetchUser('1'));
77
+ * await queue.enqueue(() => fetchUser('2'));
78
+ * // ...
79
+ * ```
80
+ */
81
+ export class RequestQueue {
82
+ private queue: Array<() => Promise<any>> = [];
83
+ private running = 0;
84
+ private maxConcurrent: number;
85
+ private maxQueueSize: number;
86
+
87
+ /**
88
+ * Create a new request queue
89
+ * @param maxConcurrent Maximum number of concurrent requests (default: 10)
90
+ * @param maxQueueSize Maximum queue size (default: 100)
91
+ */
92
+ constructor(maxConcurrent: number = 10, maxQueueSize: number = 100) {
93
+ this.maxConcurrent = maxConcurrent;
94
+ this.maxQueueSize = maxQueueSize;
95
+ }
96
+
97
+ /**
98
+ * Enqueue a request
99
+ * @param requestFn Function that returns a promise
100
+ * @returns Promise that resolves when request completes
101
+ */
102
+ async enqueue<T>(requestFn: () => Promise<T>): Promise<T> {
103
+ if (this.queue.length >= this.maxQueueSize) {
104
+ throw new Error('Request queue is full');
105
+ }
106
+
107
+ return new Promise<T>((resolve, reject) => {
108
+ this.queue.push(async () => {
109
+ try {
110
+ const result = await requestFn();
111
+ resolve(result);
112
+ } catch (error) {
113
+ reject(error);
114
+ }
115
+ });
116
+
117
+ this.process();
118
+ });
119
+ }
120
+
121
+ /**
122
+ * Process queued requests
123
+ */
124
+ private async process(): Promise<void> {
125
+ if (this.running >= this.maxConcurrent || this.queue.length === 0) {
126
+ return;
127
+ }
128
+
129
+ this.running++;
130
+ const requestFn = this.queue.shift();
131
+ if (requestFn) {
132
+ try {
133
+ await requestFn();
134
+ } finally {
135
+ this.running--;
136
+ this.process();
137
+ }
138
+ } else {
139
+ this.running--;
140
+ }
141
+ }
142
+
143
+ /**
144
+ * Clear all queued requests
145
+ */
146
+ clear(): void {
147
+ this.queue = [];
148
+ }
149
+
150
+ /**
151
+ * Get queue size
152
+ */
153
+ size(): number {
154
+ return this.queue.length;
155
+ }
156
+
157
+ /**
158
+ * Get number of currently running requests
159
+ */
160
+ runningCount(): number {
161
+ return this.running;
162
+ }
163
+ }
164
+
165
+ /**
166
+ * Simple logger with level support
167
+ *
168
+ * Lightweight logger for HTTP clients and utilities.
169
+ * For more advanced logging, use loggerUtils.ts
170
+ *
171
+ * @example
172
+ * ```typescript
173
+ * const logger = new SimpleLogger(true, 'debug');
174
+ * logger.debug('Debug message');
175
+ * logger.info('Info message');
176
+ * logger.error('Error message');
177
+ * ```
178
+ */
179
+ export class SimpleLogger {
180
+ private enabled: boolean;
181
+ private level: 'none' | 'error' | 'warn' | 'info' | 'debug';
182
+ private prefix: string;
183
+
184
+ /**
185
+ * Create a new simple logger
186
+ * @param enabled Whether logging is enabled
187
+ * @param level Minimum log level
188
+ * @param prefix Prefix for log messages (default: '')
189
+ */
190
+ constructor(
191
+ enabled: boolean = false,
192
+ level: string = 'error',
193
+ prefix: string = ''
194
+ ) {
195
+ this.enabled = enabled;
196
+ this.level = level as any;
197
+ this.prefix = prefix;
198
+ }
199
+
200
+ private shouldLog(level: string): boolean {
201
+ if (!this.enabled || this.level === 'none') return false;
202
+ const levels = ['none', 'error', 'warn', 'info', 'debug'];
203
+ return levels.indexOf(level) <= levels.indexOf(this.level);
204
+ }
205
+
206
+ private formatMessage(...args: any[]): any[] {
207
+ return this.prefix ? [`[${this.prefix}]`, ...args] : args;
208
+ }
209
+
210
+ error(...args: any[]): void {
211
+ if (this.shouldLog('error')) {
212
+ console.error(...this.formatMessage(...args));
213
+ }
214
+ }
215
+
216
+ warn(...args: any[]): void {
217
+ if (this.shouldLog('warn')) {
218
+ console.warn(...this.formatMessage(...args));
219
+ }
220
+ }
221
+
222
+ info(...args: any[]): void {
223
+ if (this.shouldLog('info')) {
224
+ console.info(...this.formatMessage(...args));
225
+ }
226
+ }
227
+
228
+ debug(...args: any[]): void {
229
+ if (this.shouldLog('debug')) {
230
+ console.log(...this.formatMessage(...args));
231
+ }
232
+ }
233
+ }
234
+
@@ -0,0 +1,206 @@
1
+ /**
2
+ * Session management utilities
3
+ *
4
+ * Provides consistent session normalization, deduplication, and sorting
5
+ * to ensure sessions are always displayed in a predictable order.
6
+ */
7
+
8
+ import type { ClientSession } from '../models/session';
9
+
10
+ /**
11
+ * Normalize a session to ensure all required fields are present
12
+ */
13
+ export function normalizeSession(session: Partial<ClientSession> & { sessionId: string }): ClientSession {
14
+ const now = new Date().toISOString();
15
+ return {
16
+ sessionId: session.sessionId,
17
+ deviceId: session.deviceId || '',
18
+ expiresAt: session.expiresAt || now,
19
+ lastActive: session.lastActive || now,
20
+ userId: session.userId || '',
21
+ };
22
+ }
23
+
24
+ /**
25
+ * Compare two sessions for equality
26
+ */
27
+ export function sessionsEqual(a: ClientSession, b: ClientSession): boolean {
28
+ return a.sessionId === b.sessionId;
29
+ }
30
+
31
+ /**
32
+ * Sort sessions by lastActive (most recent first), then by sessionId for stability
33
+ */
34
+ export function sortSessions(sessions: ClientSession[]): ClientSession[] {
35
+ return [...sessions].sort((a, b) => {
36
+ // Sort by lastActive descending (most recent first)
37
+ const timeA = new Date(a.lastActive).getTime();
38
+ const timeB = new Date(b.lastActive).getTime();
39
+ if (timeA !== timeB) {
40
+ return timeB - timeA; // Descending order
41
+ }
42
+ // If lastActive is the same, sort by sessionId for stability
43
+ return a.sessionId.localeCompare(b.sessionId);
44
+ });
45
+ }
46
+
47
+ /**
48
+ * Deduplicate sessions by sessionId, keeping the most recent version
49
+ */
50
+ export function deduplicateSessions(sessions: ClientSession[]): ClientSession[] {
51
+ const sessionMap = new Map<string, ClientSession>();
52
+
53
+ for (const session of sessions) {
54
+ const existing = sessionMap.get(session.sessionId);
55
+ if (!existing) {
56
+ sessionMap.set(session.sessionId, session);
57
+ } else {
58
+ // Keep the one with more recent lastActive
59
+ const existingTime = new Date(existing.lastActive).getTime();
60
+ const currentTime = new Date(session.lastActive).getTime();
61
+ if (currentTime > existingTime) {
62
+ sessionMap.set(session.sessionId, session);
63
+ }
64
+ }
65
+ }
66
+
67
+ return Array.from(sessionMap.values());
68
+ }
69
+
70
+ /**
71
+ * Deduplicate sessions by userId, keeping only one session per user
72
+ * Priority: 1) Active session (if provided), 2) Most recent session
73
+ * This prevents showing duplicate accounts for the same user
74
+ */
75
+ export function deduplicateSessionsByUserId(
76
+ sessions: ClientSession[],
77
+ activeSessionId?: string | null
78
+ ): ClientSession[] {
79
+ if (!sessions.length) return [];
80
+
81
+ const userSessionMap = new Map<string, ClientSession>();
82
+
83
+ for (const session of sessions) {
84
+ if (!session.userId) continue; // Skip sessions without userId
85
+
86
+ const existing = userSessionMap.get(session.userId);
87
+ if (!existing) {
88
+ userSessionMap.set(session.userId, session);
89
+ } else {
90
+ // Prioritize active session
91
+ const isCurrentActive = activeSessionId && session.sessionId === activeSessionId;
92
+ const isExistingActive = activeSessionId && existing.sessionId === activeSessionId;
93
+
94
+ if (isCurrentActive && !isExistingActive) {
95
+ userSessionMap.set(session.userId, session);
96
+ } else if (!isCurrentActive && isExistingActive) {
97
+ // Keep existing (active) session
98
+ continue;
99
+ } else {
100
+ // Neither is active, keep the one with more recent lastActive
101
+ const existingTime = new Date(existing.lastActive).getTime();
102
+ const currentTime = new Date(session.lastActive).getTime();
103
+ if (currentTime > existingTime) {
104
+ userSessionMap.set(session.userId, session);
105
+ }
106
+ }
107
+ }
108
+ }
109
+
110
+ return Array.from(userSessionMap.values());
111
+ }
112
+
113
+ /**
114
+ * Normalize, deduplicate, and sort sessions
115
+ * This ensures consistent session ordering across the application
116
+ *
117
+ * @param sessions - Array of sessions to normalize
118
+ * @param activeSessionId - Optional active session ID to prioritize
119
+ * @param deduplicateByUserId - If true, deduplicate by userId (one account per user). Default: true
120
+ */
121
+ export function normalizeAndSortSessions(
122
+ sessions: ClientSession[],
123
+ activeSessionId?: string | null,
124
+ deduplicateByUserId: boolean = true
125
+ ): ClientSession[] {
126
+ if (!sessions.length) return [];
127
+
128
+ // Normalize all sessions
129
+ const normalized = sessions.map(normalizeSession);
130
+
131
+ // First deduplicate by sessionId (exact duplicates)
132
+ const deduplicatedBySessionId = deduplicateSessions(normalized);
133
+
134
+ // Then deduplicate by userId if requested (one account per user)
135
+ const finalSessions = deduplicateByUserId
136
+ ? deduplicateSessionsByUserId(deduplicatedBySessionId, activeSessionId)
137
+ : deduplicatedBySessionId;
138
+
139
+ // Sort consistently
140
+ return sortSessions(finalSessions);
141
+ }
142
+
143
+ /**
144
+ * Merge two session arrays, prioritizing newer data
145
+ * Returns normalized, deduplicated, and sorted sessions
146
+ *
147
+ * @param existing - Existing sessions array
148
+ * @param incoming - New sessions to merge in
149
+ * @param activeSessionId - Optional active session ID to prioritize
150
+ * @param deduplicateByUserId - If true, deduplicate by userId (one account per user). Default: true
151
+ */
152
+ export function mergeSessions(
153
+ existing: ClientSession[],
154
+ incoming: ClientSession[],
155
+ activeSessionId?: string | null,
156
+ deduplicateByUserId: boolean = true
157
+ ): ClientSession[] {
158
+ if (!existing.length && !incoming.length) return [];
159
+ if (!existing.length) return normalizeAndSortSessions(incoming, activeSessionId, deduplicateByUserId);
160
+ if (!incoming.length) return normalizeAndSortSessions(existing, activeSessionId, deduplicateByUserId);
161
+
162
+ // Normalize both arrays
163
+ const normalizedExisting = existing.map(normalizeSession);
164
+ const normalizedIncoming = incoming.map(normalizeSession);
165
+
166
+ // Create a map with existing sessions (by sessionId)
167
+ const sessionMap = new Map<string, ClientSession>();
168
+
169
+ // Add existing sessions first
170
+ for (const session of normalizedExisting) {
171
+ sessionMap.set(session.sessionId, session);
172
+ }
173
+
174
+ // Merge incoming sessions - backend data always replaces existing
175
+ for (const session of normalizedIncoming) {
176
+ sessionMap.set(session.sessionId, session);
177
+ }
178
+
179
+ // Convert to array
180
+ const merged = Array.from(sessionMap.values());
181
+
182
+ // Apply userId deduplication if requested
183
+ const finalSessions = deduplicateByUserId
184
+ ? deduplicateSessionsByUserId(merged, activeSessionId)
185
+ : merged;
186
+
187
+ // Sort consistently
188
+ return sortSessions(finalSessions);
189
+ }
190
+
191
+ /**
192
+ * Check if two session arrays are equal (same sessionIds in same order)
193
+ */
194
+ export function sessionsArraysEqual(a: ClientSession[], b: ClientSession[]): boolean {
195
+ if (a.length !== b.length) {
196
+ return false;
197
+ }
198
+
199
+ const sortedA = sortSessions(a);
200
+ const sortedB = sortSessions(b);
201
+
202
+ return sortedA.every((session, index) =>
203
+ sessionsEqual(session, sortedB[index])
204
+ );
205
+ }
206
+