@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.
- package/LICENSE +674 -0
- package/README.md +52 -0
- package/dist/api/index.d.ts +4 -0
- package/dist/api/index.d.ts.map +1 -0
- package/dist/api/index.js +3 -0
- package/dist/api/mixIdApi.d.ts +76 -0
- package/dist/api/mixIdApi.d.ts.map +1 -0
- package/dist/api/mixIdApi.js +275 -0
- package/dist/api/offlineQueue.d.ts +24 -0
- package/dist/api/offlineQueue.d.ts.map +1 -0
- package/dist/api/offlineQueue.js +137 -0
- package/dist/api/websocket.d.ts +28 -0
- package/dist/api/websocket.d.ts.map +1 -0
- package/dist/api/websocket.js +201 -0
- package/dist/components/MixIdCallbackPage.d.ts +6 -0
- package/dist/components/MixIdCallbackPage.d.ts.map +1 -0
- package/dist/components/MixIdCallbackPage.js +38 -0
- package/dist/components/MixIdConnection.d.ts +18 -0
- package/dist/components/MixIdConnection.d.ts.map +1 -0
- package/dist/components/MixIdConnection.js +197 -0
- package/dist/components/index.d.ts +5 -0
- package/dist/components/index.d.ts.map +1 -0
- package/dist/components/index.js +2 -0
- package/dist/hooks/index.d.ts +5 -0
- package/dist/hooks/index.d.ts.map +1 -0
- package/dist/hooks/index.js +4 -0
- package/dist/hooks/useMixIdSession.d.ts +19 -0
- package/dist/hooks/useMixIdSession.d.ts.map +1 -0
- package/dist/hooks/useMixIdSession.js +124 -0
- package/dist/hooks/useMixIdStatus.d.ts +9 -0
- package/dist/hooks/useMixIdStatus.d.ts.map +1 -0
- package/dist/hooks/useMixIdStatus.js +81 -0
- package/dist/hooks/useMixIdSync.d.ts +16 -0
- package/dist/hooks/useMixIdSync.d.ts.map +1 -0
- package/dist/hooks/useMixIdSync.js +263 -0
- package/dist/hooks/useNotifications.d.ts +17 -0
- package/dist/hooks/useNotifications.d.ts.map +1 -0
- package/dist/hooks/useNotifications.js +144 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/ui/Button.d.ts +5 -0
- package/dist/ui/Button.d.ts.map +1 -0
- package/dist/ui/Button.js +7 -0
- package/dist/ui/Card.d.ts +5 -0
- package/dist/ui/Card.d.ts.map +1 -0
- package/dist/ui/Card.js +7 -0
- package/dist/ui/index.d.ts +3 -0
- package/dist/ui/index.d.ts.map +1 -0
- package/dist/ui/index.js +2 -0
- 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 @@
|
|
|
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,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"}
|