@localzet/data-connector 1.0.0

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 (51) hide show
  1. package/LICENSE +674 -0
  2. package/README.md +52 -0
  3. package/dist/api/index.d.ts +4 -0
  4. package/dist/api/index.d.ts.map +1 -0
  5. package/dist/api/index.js +3 -0
  6. package/dist/api/mixIdApi.d.ts +76 -0
  7. package/dist/api/mixIdApi.d.ts.map +1 -0
  8. package/dist/api/mixIdApi.js +275 -0
  9. package/dist/api/offlineQueue.d.ts +24 -0
  10. package/dist/api/offlineQueue.d.ts.map +1 -0
  11. package/dist/api/offlineQueue.js +137 -0
  12. package/dist/api/websocket.d.ts +28 -0
  13. package/dist/api/websocket.d.ts.map +1 -0
  14. package/dist/api/websocket.js +201 -0
  15. package/dist/components/MixIdCallbackPage.d.ts +6 -0
  16. package/dist/components/MixIdCallbackPage.d.ts.map +1 -0
  17. package/dist/components/MixIdCallbackPage.js +38 -0
  18. package/dist/components/MixIdConnection.d.ts +18 -0
  19. package/dist/components/MixIdConnection.d.ts.map +1 -0
  20. package/dist/components/MixIdConnection.js +197 -0
  21. package/dist/components/index.d.ts +5 -0
  22. package/dist/components/index.d.ts.map +1 -0
  23. package/dist/components/index.js +2 -0
  24. package/dist/hooks/index.d.ts +5 -0
  25. package/dist/hooks/index.d.ts.map +1 -0
  26. package/dist/hooks/index.js +4 -0
  27. package/dist/hooks/useMixIdSession.d.ts +19 -0
  28. package/dist/hooks/useMixIdSession.d.ts.map +1 -0
  29. package/dist/hooks/useMixIdSession.js +124 -0
  30. package/dist/hooks/useMixIdStatus.d.ts +9 -0
  31. package/dist/hooks/useMixIdStatus.d.ts.map +1 -0
  32. package/dist/hooks/useMixIdStatus.js +81 -0
  33. package/dist/hooks/useMixIdSync.d.ts +16 -0
  34. package/dist/hooks/useMixIdSync.d.ts.map +1 -0
  35. package/dist/hooks/useMixIdSync.js +263 -0
  36. package/dist/hooks/useNotifications.d.ts +17 -0
  37. package/dist/hooks/useNotifications.d.ts.map +1 -0
  38. package/dist/hooks/useNotifications.js +144 -0
  39. package/dist/index.d.ts +5 -0
  40. package/dist/index.d.ts.map +1 -0
  41. package/dist/index.js +8 -0
  42. package/dist/ui/Button.d.ts +5 -0
  43. package/dist/ui/Button.d.ts.map +1 -0
  44. package/dist/ui/Button.js +7 -0
  45. package/dist/ui/Card.d.ts +5 -0
  46. package/dist/ui/Card.d.ts.map +1 -0
  47. package/dist/ui/Card.js +7 -0
  48. package/dist/ui/index.d.ts +3 -0
  49. package/dist/ui/index.d.ts.map +1 -0
  50. package/dist/ui/index.js +2 -0
  51. package/package.json +69 -0
package/README.md ADDED
@@ -0,0 +1,52 @@
1
+ # @localzet/data-connector
2
+
3
+ Библиотека для подключения к MIX ID с поддержкой синхронизации в реальном времени, уведомлений и управления сессиями.
4
+
5
+ ## Установка
6
+
7
+ ```bash
8
+ npm install @localzet/data-connector
9
+ ```
10
+
11
+ ## Использование
12
+
13
+ ### Базовое подключение
14
+
15
+ ```tsx
16
+ import { MixIdConnection } from '@localzet/data-connector/components';
17
+ import { useMixIdSync } from '@localzet/data-connector/hooks';
18
+
19
+ function App() {
20
+ const { performSync } = useMixIdSync();
21
+
22
+ return (
23
+ <div>
24
+ <MixIdConnection />
25
+ </div>
26
+ );
27
+ }
28
+ ```
29
+
30
+ ### Хуки
31
+
32
+ - `useMixIdSync()` - синхронизация данных
33
+ - `useMixIdStatus()` - статус подключения
34
+ - `useNotifications()` - уведомления
35
+ - `useMixIdSession()` - управление сессиями
36
+
37
+ ### API
38
+
39
+ - `mixIdApi` - основной API клиент
40
+ - `wsClient` - WebSocket клиент
41
+ - `offlineQueue` - очередь для офлайн операций
42
+
43
+ ## Особенности
44
+
45
+ - ✅ OAuth 2.0 авторизация
46
+ - ✅ Синхронизация в реальном времени через WebSocket
47
+ - ✅ Уведомления в реальном времени
48
+ - ✅ Управление сессиями с взаимоудалением
49
+ - ✅ Офлайн поддержка с очередью операций
50
+ - ✅ React компоненты для быстрой интеграции
51
+ - ✅ TypeScript поддержка
52
+
@@ -0,0 +1,4 @@
1
+ export * from './mixIdApi';
2
+ export * from './websocket';
3
+ export * from './offlineQueue';
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/api/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAA;AAC1B,cAAc,aAAa,CAAA;AAC3B,cAAc,gBAAgB,CAAA"}
@@ -0,0 +1,3 @@
1
+ export * from './mixIdApi';
2
+ export * from './websocket';
3
+ export * from './offlineQueue';
@@ -0,0 +1,76 @@
1
+ export interface MixIdConfig {
2
+ apiBase: string;
3
+ clientId: string;
4
+ clientSecret: string;
5
+ accessToken?: string;
6
+ refreshToken?: string;
7
+ }
8
+ declare class MixIdApi {
9
+ private config;
10
+ setConfig(config: MixIdConfig): void;
11
+ getConfig(): MixIdConfig | null;
12
+ clearConfig(): void;
13
+ private request;
14
+ private refreshAccessToken;
15
+ initiateOAuth(redirectUri: string, state?: string): Promise<{
16
+ authorizationUrl: string;
17
+ code: string;
18
+ }>;
19
+ exchangeCodeForToken(code: string, redirectUri?: string): Promise<{
20
+ access_token: string;
21
+ refresh_token: string;
22
+ token_type: string;
23
+ expires_in: number;
24
+ }>;
25
+ getSyncStatus(): Promise<{
26
+ syncSettings: boolean;
27
+ syncData: boolean;
28
+ lastSyncAt: string | null;
29
+ }>;
30
+ updateSyncPreferences(syncSettings: boolean, syncData: boolean): Promise<{
31
+ success: boolean;
32
+ }>;
33
+ uploadSettings(settings: any): Promise<{
34
+ success: boolean;
35
+ version: number;
36
+ }>;
37
+ downloadSettings(): Promise<{
38
+ settings: any;
39
+ version: number;
40
+ updatedAt: string;
41
+ }>;
42
+ uploadData(dataType: string, data: Record<string, any>): Promise<{
43
+ success: boolean;
44
+ }>;
45
+ downloadData(dataType: string): Promise<{
46
+ data: Record<string, any>;
47
+ dataType: string;
48
+ }>;
49
+ checkUpdates(settingsVersion?: number, dataTypes?: string[]): Promise<{
50
+ updates: {
51
+ settings?: {
52
+ version: number;
53
+ updatedAt: string;
54
+ };
55
+ data?: Record<string, {
56
+ updatedAt: string;
57
+ }>;
58
+ };
59
+ hasUpdates: boolean;
60
+ }>;
61
+ heartbeat(deviceInfo?: any): Promise<{
62
+ success: boolean;
63
+ }>;
64
+ getSessions(): Promise<Array<{
65
+ id: string;
66
+ deviceInfo: any;
67
+ lastActivityAt: string;
68
+ createdAt: string;
69
+ }>>;
70
+ deleteSession(sessionId: string): Promise<{
71
+ success: boolean;
72
+ }>;
73
+ }
74
+ export declare const mixIdApi: MixIdApi;
75
+ export {};
76
+ //# sourceMappingURL=mixIdApi.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mixIdApi.d.ts","sourceRoot":"","sources":["../../src/api/mixIdApi.ts"],"names":[],"mappings":"AAMA,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,MAAM,CAAA;IACf,QAAQ,EAAE,MAAM,CAAA;IAChB,YAAY,EAAE,MAAM,CAAA;IACpB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,YAAY,CAAC,EAAE,MAAM,CAAA;CACtB;AAED,cAAM,QAAQ;IACZ,OAAO,CAAC,MAAM,CAA2B;IAEzC,SAAS,CAAC,MAAM,EAAE,WAAW;IAmB7B,SAAS,IAAI,WAAW,GAAG,IAAI;IA2B/B,WAAW;YAYG,OAAO;YAkDP,kBAAkB;IA0C1B,aAAa,CAAC,WAAW,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,gBAAgB,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;IAmBvG,oBAAoB,CAAC,IAAI,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;QACtE,YAAY,EAAE,MAAM,CAAA;QACpB,aAAa,EAAE,MAAM,CAAA;QACrB,UAAU,EAAE,MAAM,CAAA;QAClB,UAAU,EAAE,MAAM,CAAA;KACnB,CAAC;IA4CI,aAAa,IAAI,OAAO,CAAC;QAC7B,YAAY,EAAE,OAAO,CAAA;QACrB,QAAQ,EAAE,OAAO,CAAA;QACjB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAA;KAC1B,CAAC;IAII,qBAAqB,CAAC,YAAY,EAAE,OAAO,EAAE,QAAQ,EAAE,OAAO,GAAG,OAAO,CAAC;QAAE,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC;IAO9F,cAAc,CAAC,QAAQ,EAAE,GAAG,GAAG,OAAO,CAAC;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;IAO7E,gBAAgB,IAAI,OAAO,CAAC;QAAE,QAAQ,EAAE,GAAG,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC;IAIlF,UAAU,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,OAAO,CAAC;QAAE,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC;IAkCtF,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC;IAIxF,YAAY,CAAC,eAAe,CAAC,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC;QAC1E,OAAO,EAAE;YACP,QAAQ,CAAC,EAAE;gBAAE,OAAO,EAAE,MAAM,CAAC;gBAAC,SAAS,EAAE,MAAM,CAAA;aAAE,CAAA;YACjD,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE;gBAAE,SAAS,EAAE,MAAM,CAAA;aAAE,CAAC,CAAA;SAC7C,CAAA;QACD,UAAU,EAAE,OAAO,CAAA;KACpB,CAAC;IAQI,SAAS,CAAC,UAAU,CAAC,EAAE,GAAG,GAAG,OAAO,CAAC;QAAE,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC;IAQ1D,WAAW,IAAI,OAAO,CAAC,KAAK,CAAC;QACjC,EAAE,EAAE,MAAM,CAAA;QACV,UAAU,EAAE,GAAG,CAAA;QACf,cAAc,EAAE,MAAM,CAAA;QACtB,SAAS,EAAE,MAAM,CAAA;KAClB,CAAC,CAAC;IAIG,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC;CAKtE;AAED,eAAO,MAAM,QAAQ,UAAiB,CAAA"}
@@ -0,0 +1,275 @@
1
+ const MIX_ID_API_BASE = typeof window !== 'undefined' && window.__MIX_ID_API_BASE
2
+ ? window.__MIX_ID_API_BASE
3
+ : (typeof import.meta !== 'undefined' && import.meta.env?.VITE_MIX_ID_API_BASE)
4
+ ? import.meta.env.VITE_MIX_ID_API_BASE
5
+ : 'http://localhost:3000/api';
6
+ class MixIdApi {
7
+ constructor() {
8
+ Object.defineProperty(this, "config", {
9
+ enumerable: true,
10
+ configurable: true,
11
+ writable: true,
12
+ value: null
13
+ });
14
+ }
15
+ setConfig(config) {
16
+ this.config = config;
17
+ // Save config without tokens (tokens saved separately)
18
+ const { accessToken, refreshToken, ...configWithoutTokens } = config;
19
+ if (typeof window !== 'undefined' && window.localStorage) {
20
+ localStorage.setItem('mixId_config', JSON.stringify(configWithoutTokens));
21
+ if (config.accessToken) {
22
+ localStorage.setItem('mixId_accessToken', config.accessToken);
23
+ }
24
+ if (config.refreshToken) {
25
+ localStorage.setItem('mixId_refreshToken', config.refreshToken);
26
+ }
27
+ // Dispatch custom event for same-tab updates
28
+ window.dispatchEvent(new Event('mixid-config-changed'));
29
+ }
30
+ }
31
+ getConfig() {
32
+ if (!this.config) {
33
+ if (typeof window !== 'undefined' && window.localStorage) {
34
+ try {
35
+ const accessToken = localStorage.getItem('mixId_accessToken');
36
+ const refreshToken = localStorage.getItem('mixId_refreshToken');
37
+ const stored = localStorage.getItem('mixId_config');
38
+ if (stored) {
39
+ const parsed = JSON.parse(stored);
40
+ this.config = {
41
+ ...parsed,
42
+ accessToken: accessToken || undefined,
43
+ refreshToken: refreshToken || undefined
44
+ };
45
+ }
46
+ else if (accessToken || refreshToken) {
47
+ // If we have tokens but no config, try to restore from tokens
48
+ console.warn('MIX ID config missing but tokens found. Please reconfigure MIX ID.');
49
+ }
50
+ }
51
+ catch (error) {
52
+ console.error('Error loading MIX ID config:', error);
53
+ this.config = null;
54
+ }
55
+ }
56
+ }
57
+ return this.config;
58
+ }
59
+ clearConfig() {
60
+ this.config = null;
61
+ if (typeof window !== 'undefined' && window.localStorage) {
62
+ localStorage.removeItem('mixId_config');
63
+ localStorage.removeItem('mixId_accessToken');
64
+ localStorage.removeItem('mixId_refreshToken');
65
+ // Dispatch custom event for same-tab updates
66
+ window.dispatchEvent(new Event('mixid-config-changed'));
67
+ }
68
+ }
69
+ async request(endpoint, options = {}) {
70
+ const config = this.getConfig();
71
+ if (!config) {
72
+ throw new Error('MIX ID not configured');
73
+ }
74
+ const token = config.accessToken || (typeof window !== 'undefined' && window.localStorage
75
+ ? localStorage.getItem('mixId_accessToken')
76
+ : null);
77
+ const headers = {
78
+ 'Content-Type': 'application/json',
79
+ ...(options.headers || {}),
80
+ };
81
+ if (token) {
82
+ headers['Authorization'] = `Bearer ${token}`;
83
+ }
84
+ const response = await fetch(`${config.apiBase || MIX_ID_API_BASE}${endpoint}`, {
85
+ ...options,
86
+ headers,
87
+ });
88
+ if (response.status === 401) {
89
+ // Try to refresh token
90
+ const refreshed = await this.refreshAccessToken();
91
+ if (refreshed) {
92
+ const retryHeaders = {
93
+ ...headers,
94
+ 'Authorization': `Bearer ${refreshed}`,
95
+ };
96
+ const retryResponse = await fetch(`${config.apiBase || MIX_ID_API_BASE}${endpoint}`, {
97
+ ...options,
98
+ headers: retryHeaders,
99
+ });
100
+ if (!retryResponse.ok) {
101
+ throw new Error(`HTTP ${retryResponse.status}`);
102
+ }
103
+ return retryResponse.json();
104
+ }
105
+ }
106
+ if (!response.ok) {
107
+ const error = await response.json().catch(() => ({ error: 'Unknown error' }));
108
+ throw new Error(error.error || `HTTP ${response.status}`);
109
+ }
110
+ return response.json();
111
+ }
112
+ async refreshAccessToken() {
113
+ const config = this.getConfig();
114
+ const refreshToken = config?.refreshToken || (typeof window !== 'undefined' && window.localStorage
115
+ ? localStorage.getItem('mixId_refreshToken')
116
+ : null);
117
+ if (!refreshToken)
118
+ return null;
119
+ try {
120
+ const response = await fetch(`${config?.apiBase || MIX_ID_API_BASE}/auth/refresh`, {
121
+ method: 'POST',
122
+ headers: { 'Content-Type': 'application/json' },
123
+ body: JSON.stringify({ refreshToken }),
124
+ });
125
+ if (!response.ok)
126
+ return null;
127
+ const data = await response.json();
128
+ if (data.accessToken) {
129
+ if (typeof window !== 'undefined' && window.localStorage) {
130
+ localStorage.setItem('mixId_accessToken', data.accessToken);
131
+ }
132
+ if (this.config) {
133
+ this.config.accessToken = data.accessToken;
134
+ // Save updated config
135
+ const { accessToken: _, refreshToken: __, ...configWithoutTokens } = this.config;
136
+ if (typeof window !== 'undefined' && window.localStorage) {
137
+ localStorage.setItem('mixId_config', JSON.stringify(configWithoutTokens));
138
+ }
139
+ }
140
+ // Dispatch custom event for same-tab updates
141
+ if (typeof window !== 'undefined') {
142
+ window.dispatchEvent(new Event('mixid-config-changed'));
143
+ }
144
+ return data.accessToken;
145
+ }
146
+ }
147
+ catch (error) {
148
+ console.error('Failed to refresh token:', error);
149
+ }
150
+ return null;
151
+ }
152
+ // OAuth flow
153
+ async initiateOAuth(redirectUri, state) {
154
+ const config = this.getConfig();
155
+ if (!config) {
156
+ throw new Error('MIX ID not configured');
157
+ }
158
+ return this.request('/auth/oauth/authorize', {
159
+ method: 'POST',
160
+ body: JSON.stringify({
161
+ clientId: config.clientId,
162
+ redirectUri,
163
+ state,
164
+ }),
165
+ });
166
+ }
167
+ async exchangeCodeForToken(code, redirectUri) {
168
+ const config = this.getConfig();
169
+ if (!config) {
170
+ throw new Error('MIX ID not configured');
171
+ }
172
+ // Don't use this.request() here because we don't have a token yet
173
+ // Make direct fetch without Authorization header
174
+ const response = await fetch(`${config.apiBase || MIX_ID_API_BASE}/auth/oauth/token`, {
175
+ method: 'POST',
176
+ headers: {
177
+ 'Content-Type': 'application/json',
178
+ },
179
+ body: JSON.stringify({
180
+ code,
181
+ clientId: config.clientId,
182
+ clientSecret: config.clientSecret,
183
+ redirectUri,
184
+ }),
185
+ });
186
+ if (!response.ok) {
187
+ const error = await response.json().catch(() => ({ error: 'Unknown error' }));
188
+ throw new Error(error.error || `HTTP ${response.status}`);
189
+ }
190
+ const data = await response.json();
191
+ // Save tokens
192
+ this.setConfig({
193
+ ...config,
194
+ accessToken: data.access_token,
195
+ refreshToken: data.refresh_token,
196
+ });
197
+ return data;
198
+ }
199
+ // Sync
200
+ async getSyncStatus() {
201
+ return this.request('/sync/status');
202
+ }
203
+ async updateSyncPreferences(syncSettings, syncData) {
204
+ return this.request('/sync/preferences', {
205
+ method: 'PUT',
206
+ body: JSON.stringify({ syncSettings, syncData }),
207
+ });
208
+ }
209
+ async uploadSettings(settings) {
210
+ return this.request('/sync/settings', {
211
+ method: 'POST',
212
+ body: JSON.stringify({ settings }),
213
+ });
214
+ }
215
+ async downloadSettings() {
216
+ return this.request('/sync/settings');
217
+ }
218
+ async uploadData(dataType, data) {
219
+ // Split large data into chunks to avoid 413 Payload Too Large
220
+ const CHUNK_SIZE = 100; // Number of items per chunk
221
+ const dataEntries = Object.entries(data);
222
+ if (dataEntries.length <= CHUNK_SIZE) {
223
+ // Small enough to send in one request
224
+ return this.request('/sync/data', {
225
+ method: 'POST',
226
+ body: JSON.stringify({ dataType, data }),
227
+ });
228
+ }
229
+ // Split into chunks
230
+ const chunks = [];
231
+ for (let i = 0; i < dataEntries.length; i += CHUNK_SIZE) {
232
+ const chunk = {};
233
+ for (let j = i; j < Math.min(i + CHUNK_SIZE, dataEntries.length); j++) {
234
+ chunk[dataEntries[j][0]] = dataEntries[j][1];
235
+ }
236
+ chunks.push(chunk);
237
+ }
238
+ // Upload chunks sequentially
239
+ for (const chunk of chunks) {
240
+ await this.request('/sync/data', {
241
+ method: 'POST',
242
+ body: JSON.stringify({ dataType, data: chunk }),
243
+ });
244
+ }
245
+ return { success: true };
246
+ }
247
+ async downloadData(dataType) {
248
+ return this.request(`/sync/data?dataType=${dataType}`);
249
+ }
250
+ async checkUpdates(settingsVersion, dataTypes) {
251
+ const params = new URLSearchParams();
252
+ if (settingsVersion)
253
+ params.append('settingsVersion', settingsVersion.toString());
254
+ if (dataTypes)
255
+ params.append('dataTypes', dataTypes.join(','));
256
+ return this.request(`/sync/check-updates?${params.toString()}`);
257
+ }
258
+ // Session heartbeat
259
+ async heartbeat(deviceInfo) {
260
+ return this.request('/sessions/heartbeat', {
261
+ method: 'POST',
262
+ body: JSON.stringify({ deviceInfo }),
263
+ });
264
+ }
265
+ // Session management
266
+ async getSessions() {
267
+ return this.request('/sessions');
268
+ }
269
+ async deleteSession(sessionId) {
270
+ return this.request(`/sessions/${sessionId}`, {
271
+ method: 'DELETE',
272
+ });
273
+ }
274
+ }
275
+ export const mixIdApi = new MixIdApi();
@@ -0,0 +1,24 @@
1
+ interface QueuedOperation {
2
+ id: string;
3
+ type: 'settings' | 'data';
4
+ dataType?: string;
5
+ data: any;
6
+ timestamp: number;
7
+ retries: number;
8
+ }
9
+ declare class OfflineQueue {
10
+ private queue;
11
+ constructor();
12
+ private loadQueue;
13
+ private saveQueue;
14
+ private cleanupOldOperations;
15
+ enqueue(type: 'settings' | 'data', data: any, dataType?: string): string;
16
+ processQueue(processFn: (operation: QueuedOperation) => Promise<void>): Promise<void>;
17
+ remove(id: string): void;
18
+ clear(): void;
19
+ getQueue(): QueuedOperation[];
20
+ getQueueSize(): number;
21
+ }
22
+ export declare const offlineQueue: OfflineQueue;
23
+ export {};
24
+ //# sourceMappingURL=offlineQueue.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"offlineQueue.d.ts","sourceRoot":"","sources":["../../src/api/offlineQueue.ts"],"names":[],"mappings":"AAAA,UAAU,eAAe;IACvB,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE,UAAU,GAAG,MAAM,CAAA;IACzB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,IAAI,EAAE,GAAG,CAAA;IACT,SAAS,EAAE,MAAM,CAAA;IACjB,OAAO,EAAE,MAAM,CAAA;CAChB;AAOD,cAAM,YAAY;IAChB,OAAO,CAAC,KAAK,CAAwB;;IAOrC,OAAO,CAAC,SAAS;IAejB,OAAO,CAAC,SAAS;IA8CjB,OAAO,CAAC,oBAAoB;IAW5B,OAAO,CAAC,IAAI,EAAE,UAAU,GAAG,MAAM,EAAE,IAAI,EAAE,GAAG,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM;IAgBlE,YAAY,CAAC,SAAS,EAAE,CAAC,SAAS,EAAE,eAAe,KAAK,OAAO,CAAC,IAAI,CAAC;IAqB3E,MAAM,CAAC,EAAE,EAAE,MAAM;IAKjB,KAAK;IAKL,QAAQ,IAAI,eAAe,EAAE;IAI7B,YAAY,IAAI,MAAM;CAGvB;AAED,eAAO,MAAM,YAAY,cAAqB,CAAA"}
@@ -0,0 +1,137 @@
1
+ const QUEUE_STORAGE_KEY = 'mixId_offline_queue';
2
+ const MAX_RETRIES = 3;
3
+ const MAX_QUEUE_SIZE = 50; // Maximum number of operations in queue
4
+ const MAX_QUEUE_AGE = 7 * 24 * 60 * 60 * 1000; // 7 days in milliseconds
5
+ class OfflineQueue {
6
+ constructor() {
7
+ Object.defineProperty(this, "queue", {
8
+ enumerable: true,
9
+ configurable: true,
10
+ writable: true,
11
+ value: []
12
+ });
13
+ this.loadQueue();
14
+ this.cleanupOldOperations();
15
+ }
16
+ loadQueue() {
17
+ if (typeof window === 'undefined' || !window.localStorage) {
18
+ return;
19
+ }
20
+ try {
21
+ const stored = localStorage.getItem(QUEUE_STORAGE_KEY);
22
+ if (stored) {
23
+ this.queue = JSON.parse(stored);
24
+ }
25
+ }
26
+ catch (error) {
27
+ console.error('Error loading offline queue:', error);
28
+ this.queue = [];
29
+ }
30
+ }
31
+ saveQueue() {
32
+ if (typeof window === 'undefined' || !window.localStorage) {
33
+ return;
34
+ }
35
+ try {
36
+ // Limit queue size before saving
37
+ if (this.queue.length > MAX_QUEUE_SIZE) {
38
+ // Keep only the most recent operations
39
+ this.queue = this.queue
40
+ .sort((a, b) => b.timestamp - a.timestamp)
41
+ .slice(0, MAX_QUEUE_SIZE);
42
+ }
43
+ const queueJson = JSON.stringify(this.queue);
44
+ // Check if data is too large (localStorage limit is ~5-10MB)
45
+ if (queueJson.length > 4 * 1024 * 1024) {
46
+ // If too large, keep only the most recent 20 operations
47
+ console.warn('Offline queue too large, keeping only most recent operations');
48
+ this.queue = this.queue
49
+ .sort((a, b) => b.timestamp - a.timestamp)
50
+ .slice(0, 20);
51
+ localStorage.setItem(QUEUE_STORAGE_KEY, JSON.stringify(this.queue));
52
+ }
53
+ else {
54
+ localStorage.setItem(QUEUE_STORAGE_KEY, queueJson);
55
+ }
56
+ }
57
+ catch (error) {
58
+ if (error instanceof DOMException && error.name === 'QuotaExceededError') {
59
+ console.warn('localStorage quota exceeded, clearing old operations');
60
+ // Clear old operations and try again
61
+ this.queue = this.queue
62
+ .sort((a, b) => b.timestamp - a.timestamp)
63
+ .slice(0, 10);
64
+ try {
65
+ localStorage.setItem(QUEUE_STORAGE_KEY, JSON.stringify(this.queue));
66
+ }
67
+ catch (e) {
68
+ console.error('Failed to save queue after cleanup:', e);
69
+ // Last resort: clear the queue
70
+ this.queue = [];
71
+ localStorage.removeItem(QUEUE_STORAGE_KEY);
72
+ }
73
+ }
74
+ else {
75
+ console.error('Error saving offline queue:', error);
76
+ }
77
+ }
78
+ }
79
+ cleanupOldOperations() {
80
+ const now = Date.now();
81
+ const initialLength = this.queue.length;
82
+ this.queue = this.queue.filter((op) => now - op.timestamp < MAX_QUEUE_AGE);
83
+ if (this.queue.length < initialLength) {
84
+ console.log(`Cleaned up ${initialLength - this.queue.length} old operations from queue`);
85
+ this.saveQueue();
86
+ }
87
+ }
88
+ enqueue(type, data, dataType) {
89
+ const id = `${type}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
90
+ const operation = {
91
+ id,
92
+ type,
93
+ dataType,
94
+ data,
95
+ timestamp: Date.now(),
96
+ retries: 0,
97
+ };
98
+ this.queue.push(operation);
99
+ this.saveQueue();
100
+ return id;
101
+ }
102
+ async processQueue(processFn) {
103
+ const operations = [...this.queue];
104
+ for (const operation of operations) {
105
+ try {
106
+ await processFn(operation);
107
+ this.remove(operation.id);
108
+ }
109
+ catch (error) {
110
+ console.error(`Error processing queued operation ${operation.id}:`, error);
111
+ operation.retries++;
112
+ if (operation.retries >= MAX_RETRIES) {
113
+ console.warn(`Operation ${operation.id} exceeded max retries, removing from queue`);
114
+ this.remove(operation.id);
115
+ }
116
+ else {
117
+ this.saveQueue();
118
+ }
119
+ }
120
+ }
121
+ }
122
+ remove(id) {
123
+ this.queue = this.queue.filter((op) => op.id !== id);
124
+ this.saveQueue();
125
+ }
126
+ clear() {
127
+ this.queue = [];
128
+ this.saveQueue();
129
+ }
130
+ getQueue() {
131
+ return [...this.queue];
132
+ }
133
+ getQueueSize() {
134
+ return this.queue.length;
135
+ }
136
+ }
137
+ export const offlineQueue = new OfflineQueue();
@@ -0,0 +1,28 @@
1
+ export interface WebSocketMessage {
2
+ type: string;
3
+ [key: string]: any;
4
+ }
5
+ export type WebSocketEventHandler = (message: WebSocketMessage) => void;
6
+ declare class WebSocketClient {
7
+ private ws;
8
+ private reconnectAttempts;
9
+ private maxReconnectAttempts;
10
+ private reconnectDelay;
11
+ private isConnecting;
12
+ private eventHandlers;
13
+ private messageQueue;
14
+ private isOnline;
15
+ constructor();
16
+ connect(): void;
17
+ private attemptReconnect;
18
+ private handleMessage;
19
+ send(message: WebSocketMessage): void;
20
+ private flushMessageQueue;
21
+ on(eventType: string, handler: WebSocketEventHandler): void;
22
+ off(eventType: string, handler: WebSocketEventHandler): void;
23
+ disconnect(): void;
24
+ isConnected(): boolean;
25
+ }
26
+ export declare const wsClient: WebSocketClient;
27
+ export {};
28
+ //# sourceMappingURL=websocket.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"websocket.d.ts","sourceRoot":"","sources":["../../src/api/websocket.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAA;IACZ,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAA;CACnB;AAED,MAAM,MAAM,qBAAqB,GAAG,CAAC,OAAO,EAAE,gBAAgB,KAAK,IAAI,CAAA;AAEvE,cAAM,eAAe;IACnB,OAAO,CAAC,EAAE,CAAyB;IACnC,OAAO,CAAC,iBAAiB,CAAI;IAC7B,OAAO,CAAC,oBAAoB,CAAK;IACjC,OAAO,CAAC,cAAc,CAAO;IAC7B,OAAO,CAAC,YAAY,CAAQ;IAC5B,OAAO,CAAC,aAAa,CAAqD;IAC1E,OAAO,CAAC,YAAY,CAAyB;IAC7C,OAAO,CAAC,QAAQ,CAA6D;;IAkB7E,OAAO;IAkEP,OAAO,CAAC,gBAAgB;IAqBxB,OAAO,CAAC,aAAa;IAoBrB,IAAI,CAAC,OAAO,EAAE,gBAAgB;IAS9B,OAAO,CAAC,iBAAiB;IASzB,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,qBAAqB;IAOpD,GAAG,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,qBAAqB;IAOrD,UAAU;IAeV,WAAW,IAAI,OAAO;CAGvB;AAED,eAAO,MAAM,QAAQ,iBAAwB,CAAA"}