dolphin-server-modules 2.11.0 → 2.11.1
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/dist/adapters/mongoose/index.d.ts +4 -4
- package/dist/adapters/mongoose/index.js.map +1 -1
- package/dist/ai/dolphin-agent/agent.js +13 -2
- package/dist/ai/dolphin-agent/agent.js.map +1 -1
- package/dist/ai/dolphin-agent/config.js +37 -40
- package/dist/ai/dolphin-agent/config.js.map +1 -1
- package/dist/authController/authController.js +1 -1
- package/dist/authController/authController.js.map +1 -1
- package/dist/bin/cli.js +45 -79
- package/dist/bin/cli.js.map +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.js +4 -4
- package/dist/index.js.map +1 -1
- package/dist/realtime/core.d.ts +5 -5
- package/dist/realtime/core.js +6 -6
- package/dist/realtime/core.js.map +1 -1
- package/dist/realtime/index.d.ts +4 -4
- package/dist/realtime/index.js +4 -4
- package/dist/realtime/index.js.map +1 -1
- package/dist/server/server.d.ts +8 -8
- package/dist/server/server.js +11 -2
- package/dist/server/server.js.map +1 -1
- package/dist/signaling/index.d.ts +1 -1
- package/dist/swagger/swagger.js +31 -31
- package/dist/templates/index.js +102 -102
- package/package.json +22 -3
- package/scripts/benchmark.js +12 -0
- package/scripts/benchmark.ts +12 -0
- package/scripts/client.js +703 -0
- package/scripts/dolphin-persist.js +211 -0
- package/scripts/list-models.js +34 -0
- package/scripts/run-real-ai-test.js +79 -0
- package/scripts/test-ai-logic.js +44 -0
- package/scripts/test-client.js +105 -0
- package/scripts/test-dolphin.js +36 -0
|
@@ -0,0 +1,703 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {Object} DolphinResponse
|
|
3
|
+
* @property {boolean} success
|
|
4
|
+
* @property {any} [data]
|
|
5
|
+
* @property {string} [message]
|
|
6
|
+
* @property {number} [status]
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @typedef {Object} SignalMessage
|
|
11
|
+
* @property {string} msgId
|
|
12
|
+
* @property {string} type
|
|
13
|
+
* @property {string} from
|
|
14
|
+
* @property {string} to
|
|
15
|
+
* @property {any} data
|
|
16
|
+
* @property {number} timestamp
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @typedef {Object} FileMetadata
|
|
21
|
+
* @property {string} fileId
|
|
22
|
+
* @property {string} name
|
|
23
|
+
* @property {number} size
|
|
24
|
+
* @property {number} totalChunks
|
|
25
|
+
* @property {number} chunkSize
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* @callback TopicCallback
|
|
30
|
+
* @param {any} payload
|
|
31
|
+
* @param {string} [topic]
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Dolphin Client v2.0 - Full-stack Realtime, API & Auth Client
|
|
36
|
+
* Zero-dependency, pure JS.
|
|
37
|
+
*
|
|
38
|
+
* यो लाइब्रेरी डल्फिन सर्भरबाट सिधै उपलब्ध हुने पब-सब, API र Auth लाइब्रेरी हो।
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
class APIHandler {
|
|
42
|
+
/**
|
|
43
|
+
* @param {DolphinClient} client
|
|
44
|
+
*/
|
|
45
|
+
constructor(client) {
|
|
46
|
+
this.client = client;
|
|
47
|
+
return this._createProxy([]);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* @param {string[]} pathParts
|
|
52
|
+
* @private
|
|
53
|
+
*/
|
|
54
|
+
_createProxy(pathParts) {
|
|
55
|
+
const path = pathParts.join('/');
|
|
56
|
+
|
|
57
|
+
const target = (options) => {
|
|
58
|
+
return this.request('GET', path, null, options);
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
// Add standard methods to the function target
|
|
62
|
+
target.get = (pathOrOptions, options) => {
|
|
63
|
+
if (typeof pathOrOptions === 'string') return this.request('GET', pathOrOptions, null, options);
|
|
64
|
+
return this.request('GET', path, null, pathOrOptions);
|
|
65
|
+
};
|
|
66
|
+
target.post = (pathOrBody, bodyOrOptions, options) => {
|
|
67
|
+
if (typeof pathOrBody === 'string') return this.request('POST', pathOrBody, bodyOrOptions, options);
|
|
68
|
+
return this.request('POST', path, pathOrBody, bodyOrOptions);
|
|
69
|
+
};
|
|
70
|
+
target.put = (pathOrBody, bodyOrOptions, options) => {
|
|
71
|
+
if (typeof pathOrBody === 'string') return this.request('PUT', pathOrBody, bodyOrOptions, options);
|
|
72
|
+
return this.request('PUT', path, pathOrBody, bodyOrOptions);
|
|
73
|
+
};
|
|
74
|
+
target.del = (pathOrOptions, options) => {
|
|
75
|
+
if (typeof pathOrOptions === 'string') return this.request('DELETE', pathOrOptions, null, options);
|
|
76
|
+
return this.request('DELETE', path, null, pathOrOptions);
|
|
77
|
+
};
|
|
78
|
+
target.request = (method, subPath, body, options) => {
|
|
79
|
+
const finalPath = subPath ? `${path}/${subPath.startsWith('/') ? subPath.slice(1) : subPath}` : path;
|
|
80
|
+
return this.request(method, finalPath, body, options);
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const methods = ['get', 'post', 'put', 'del', 'request'];
|
|
84
|
+
|
|
85
|
+
return new Proxy(target, {
|
|
86
|
+
get: (t, prop) => {
|
|
87
|
+
if (typeof prop === 'string' && !methods.includes(prop)) {
|
|
88
|
+
return this._createProxy([...pathParts, prop]);
|
|
89
|
+
}
|
|
90
|
+
return t[prop];
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* @param {string} method
|
|
97
|
+
* @param {string} path
|
|
98
|
+
* @param {any} [body]
|
|
99
|
+
* @param {RequestInit} [options]
|
|
100
|
+
* @returns {Promise<any>}
|
|
101
|
+
*/
|
|
102
|
+
async request(method, path, body = null, options = {}) {
|
|
103
|
+
const url = `${this.client.httpUrl}${path.startsWith('/') ? path : '/' + path}`;
|
|
104
|
+
const headers = {
|
|
105
|
+
'Content-Type': 'application/json',
|
|
106
|
+
...options.headers
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
if (this.client.accessToken) {
|
|
110
|
+
headers['Authorization'] = `Bearer ${this.client.accessToken}`;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const fetchOptions = {
|
|
114
|
+
method,
|
|
115
|
+
headers,
|
|
116
|
+
...options
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
if (body) {
|
|
120
|
+
fetchOptions.body = JSON.stringify(body);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const response = await fetch(url, fetchOptions);
|
|
124
|
+
const contentType = response.headers.get('content-type');
|
|
125
|
+
let data;
|
|
126
|
+
if (contentType && contentType.includes('application/json')) {
|
|
127
|
+
data = await response.json();
|
|
128
|
+
} else {
|
|
129
|
+
data = await response.text();
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (!response.ok) {
|
|
133
|
+
throw { status: response.status, data };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return data;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* @param {string|RequestInit} [pathOrOptions]
|
|
141
|
+
* @param {RequestInit} [options]
|
|
142
|
+
* @returns {Promise<any>}
|
|
143
|
+
*/
|
|
144
|
+
get(pathOrOptions, options) { return Promise.resolve(); }
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* @param {string|any} [pathOrBody]
|
|
148
|
+
* @param {any} [bodyOrOptions]
|
|
149
|
+
* @param {RequestInit} [options]
|
|
150
|
+
* @returns {Promise<any>}
|
|
151
|
+
*/
|
|
152
|
+
post(pathOrBody, bodyOrOptions, options) { return Promise.resolve(); }
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* @param {string|any} [pathOrBody]
|
|
156
|
+
* @param {any} [bodyOrOptions]
|
|
157
|
+
* @param {RequestInit} [options]
|
|
158
|
+
* @returns {Promise<any>}
|
|
159
|
+
*/
|
|
160
|
+
put(pathOrBody, bodyOrOptions, options) { return Promise.resolve(); }
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* @param {string|RequestInit} [pathOrOptions]
|
|
164
|
+
* @param {RequestInit} [options]
|
|
165
|
+
* @returns {Promise<any>}
|
|
166
|
+
*/
|
|
167
|
+
del(pathOrOptions, options) { return Promise.resolve(); }
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
class AuthHandler {
|
|
171
|
+
/**
|
|
172
|
+
* @param {DolphinClient} client
|
|
173
|
+
*/
|
|
174
|
+
constructor(client) {
|
|
175
|
+
this.client = client;
|
|
176
|
+
this.user = null;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async login(email, password) {
|
|
180
|
+
const res = await this.client.api.post('/auth/login', { email, password });
|
|
181
|
+
if (res.accessToken) {
|
|
182
|
+
this.client.setToken(res.accessToken);
|
|
183
|
+
this.user = res.user;
|
|
184
|
+
}
|
|
185
|
+
return res;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async register(data) {
|
|
189
|
+
return await this.client.api.post('/auth/register', data);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async me() {
|
|
193
|
+
const res = await this.client.api.get('/auth/me');
|
|
194
|
+
if (res.success) {
|
|
195
|
+
this.user = res.data;
|
|
196
|
+
}
|
|
197
|
+
return res;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
async logout() {
|
|
201
|
+
await this.client.api.post('/auth/logout');
|
|
202
|
+
this.client.setToken(null);
|
|
203
|
+
this.user = null;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** @param {string} email */
|
|
207
|
+
async forgotPassword(email) {
|
|
208
|
+
return await this.client.api.post('/auth/forgot-password', { email });
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* DolphinStore - Reactive State Sync (Zustand Alternative)
|
|
214
|
+
* Automatically syncs database collections with local state.
|
|
215
|
+
*/
|
|
216
|
+
class DolphinStore {
|
|
217
|
+
/** @param {DolphinClient} client */
|
|
218
|
+
constructor(client) {
|
|
219
|
+
this.client = client;
|
|
220
|
+
/** @type {Map<string, { items: any[], loading: boolean, error: string|null, success: boolean }>} */
|
|
221
|
+
this.data = new Map();
|
|
222
|
+
/** @type {Set<function()>} */
|
|
223
|
+
this.listeners = new Set();
|
|
224
|
+
/** @type {Set<string>} */
|
|
225
|
+
this.subscribed = new Set();
|
|
226
|
+
|
|
227
|
+
return new Proxy(this, {
|
|
228
|
+
get: (target, prop) => {
|
|
229
|
+
if (prop in target) return target[prop];
|
|
230
|
+
if (typeof prop === 'string') {
|
|
231
|
+
return this._getCollection(prop);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** @private */
|
|
238
|
+
_getCollection(name) {
|
|
239
|
+
if (!this.data.has(name)) {
|
|
240
|
+
const collection = {
|
|
241
|
+
_rawItems: [],
|
|
242
|
+
items: [],
|
|
243
|
+
loading: true,
|
|
244
|
+
error: null,
|
|
245
|
+
success: false,
|
|
246
|
+
_filter: null,
|
|
247
|
+
_sort: null,
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* फिल्टर सेट गर्ने (Local filtering)
|
|
251
|
+
* @param {function(any): boolean} fn
|
|
252
|
+
*/
|
|
253
|
+
where: (fn) => {
|
|
254
|
+
collection._filter = fn;
|
|
255
|
+
this._applyTransform(collection);
|
|
256
|
+
return collection;
|
|
257
|
+
},
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* सर्टिङ सेट गर्ने
|
|
261
|
+
* @param {string} key
|
|
262
|
+
* @param {'asc'|'desc'} [direction]
|
|
263
|
+
*/
|
|
264
|
+
orderBy: (key, direction = 'asc') => {
|
|
265
|
+
collection._sort = { key, direction };
|
|
266
|
+
this._applyTransform(collection);
|
|
267
|
+
return collection;
|
|
268
|
+
},
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* फिल्टर र सर्ट हटाउने
|
|
272
|
+
*/
|
|
273
|
+
clear: () => {
|
|
274
|
+
collection._filter = null;
|
|
275
|
+
collection._sort = null;
|
|
276
|
+
this._applyTransform(collection);
|
|
277
|
+
return collection;
|
|
278
|
+
}
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
this.data.set(name, collection);
|
|
282
|
+
this._fetchAndSync(name);
|
|
283
|
+
}
|
|
284
|
+
return this.data.get(name);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/** @private */
|
|
288
|
+
async _fetchAndSync(name) {
|
|
289
|
+
const state = this.data.get(name);
|
|
290
|
+
try {
|
|
291
|
+
// 1. Initial Fetch
|
|
292
|
+
const res = await this.client.api.get(`/${name.toLowerCase()}`);
|
|
293
|
+
state._rawItems = Array.isArray(res) ? res : (res.data || []);
|
|
294
|
+
state.loading = false;
|
|
295
|
+
state.success = true;
|
|
296
|
+
state.error = null;
|
|
297
|
+
this._applyTransform(state);
|
|
298
|
+
|
|
299
|
+
// 2. Realtime Sync (if connected)
|
|
300
|
+
if (!this.subscribed.has(name)) {
|
|
301
|
+
const topic = `db:sync/${name.toLowerCase()}`;
|
|
302
|
+
this.client.subscribe(topic, (update) => {
|
|
303
|
+
this._handleRemoteUpdate(name, update);
|
|
304
|
+
});
|
|
305
|
+
this.subscribed.add(name);
|
|
306
|
+
}
|
|
307
|
+
} catch (e) {
|
|
308
|
+
state.loading = false;
|
|
309
|
+
state.success = false;
|
|
310
|
+
state.error = e.data?.error || e.message || 'Fetch failed';
|
|
311
|
+
this._notify();
|
|
312
|
+
console.error(`[DolphinStore] Sync failed for ${name}:`, e);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/** @private */
|
|
317
|
+
_applyTransform(state) {
|
|
318
|
+
let result = [...state._rawItems];
|
|
319
|
+
|
|
320
|
+
// 1. Filter
|
|
321
|
+
if (state._filter) {
|
|
322
|
+
result = result.filter(state._filter);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// 2. Sort
|
|
326
|
+
if (state._sort) {
|
|
327
|
+
const { key, direction } = state._sort;
|
|
328
|
+
result.sort((a, b) => {
|
|
329
|
+
const av = a[key];
|
|
330
|
+
const bv = b[key];
|
|
331
|
+
if (av === bv) return 0;
|
|
332
|
+
const compare = av > bv ? 1 : -1;
|
|
333
|
+
return direction === 'asc' ? compare : -compare;
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
state.items = result;
|
|
338
|
+
this._notify();
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/** @private */
|
|
342
|
+
_handleRemoteUpdate(collection, update) {
|
|
343
|
+
const state = this.data.get(collection);
|
|
344
|
+
if (!state) return;
|
|
345
|
+
|
|
346
|
+
let items = state._rawItems;
|
|
347
|
+
const { type, data } = update; // type: 'create', 'update', 'delete'
|
|
348
|
+
|
|
349
|
+
if (type === 'create') {
|
|
350
|
+
items = [...items, data];
|
|
351
|
+
} else if (type === 'update') {
|
|
352
|
+
items = items.map(item => (item.id === data.id || item._id === data._id) ? { ...item, ...data } : item);
|
|
353
|
+
} else if (type === 'delete') {
|
|
354
|
+
items = items.filter(item => (item.id !== data.id && item._id !== data._id));
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
state._rawItems = items;
|
|
358
|
+
this._applyTransform(state);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/** Subscribe for React components (useSyncExternalStore) */
|
|
362
|
+
subscribe(listener) {
|
|
363
|
+
this.listeners.add(listener);
|
|
364
|
+
return () => this.listeners.delete(listener);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
getSnapshot(collection) {
|
|
368
|
+
return this.data.get(collection) || { items: [], loading: false, error: null, success: false };
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
_notify() {
|
|
372
|
+
this.listeners.forEach(l => l());
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
class DolphinClient {
|
|
377
|
+
/**
|
|
378
|
+
* @param {string} [url]
|
|
379
|
+
* @param {string} [deviceId]
|
|
380
|
+
*/
|
|
381
|
+
constructor(url = '', deviceId = '') {
|
|
382
|
+
// Handle URL formatting
|
|
383
|
+
if (!url && typeof window !== 'undefined') {
|
|
384
|
+
url = window.location.host;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
let protocol = 'http:';
|
|
388
|
+
if (url && url.startsWith('https://')) {
|
|
389
|
+
protocol = 'https:';
|
|
390
|
+
} else if (url && url.startsWith('http://')) {
|
|
391
|
+
protocol = 'http:';
|
|
392
|
+
} else if (typeof window !== 'undefined') {
|
|
393
|
+
protocol = window.location.protocol;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
this.host = (url || 'localhost').replace(/\/$/, "").replace(/^https?:\/\//, "");
|
|
397
|
+
this.httpUrl = `${protocol}//${this.host}`;
|
|
398
|
+
this.deviceId = deviceId || 'web_' + Math.random().toString(36).substr(2, 5);
|
|
399
|
+
|
|
400
|
+
/** @type {WebSocket | null} */
|
|
401
|
+
this.socket = null;
|
|
402
|
+
|
|
403
|
+
// Polyfill Storage if not browser
|
|
404
|
+
this.storage = typeof localStorage !== 'undefined' ? localStorage : {
|
|
405
|
+
getItem: (key) => null,
|
|
406
|
+
setItem: (key, val) => { },
|
|
407
|
+
removeItem: (key) => { }
|
|
408
|
+
};
|
|
409
|
+
|
|
410
|
+
/** @type {string | null} */
|
|
411
|
+
this.accessToken = this.storage.getItem('dolphin_token');
|
|
412
|
+
|
|
413
|
+
// Sub-handlers
|
|
414
|
+
this.api = new APIHandler(this);
|
|
415
|
+
this.auth = new AuthHandler(this);
|
|
416
|
+
this.store = new DolphinStore(this);
|
|
417
|
+
|
|
418
|
+
/** @type {Map<string, Set<TopicCallback>>} */
|
|
419
|
+
this.handlers = new Map(); // topic -> Set of callbacks
|
|
420
|
+
/** @type {Set<function(SignalMessage): void>} */
|
|
421
|
+
this.signalHandlers = new Set();
|
|
422
|
+
/** @type {Set<function(FileMetadata): void>} */
|
|
423
|
+
this.fileHandlers = new Set();
|
|
424
|
+
|
|
425
|
+
this.reconnectAttempts = 0;
|
|
426
|
+
this.maxReconnectAttempts = 5;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* टोकन सेट गर्ने र सेभ गर्ने
|
|
431
|
+
*/
|
|
432
|
+
setToken(token) {
|
|
433
|
+
this.accessToken = token;
|
|
434
|
+
if (token) {
|
|
435
|
+
this.storage.setItem('dolphin_token', token);
|
|
436
|
+
} else {
|
|
437
|
+
this.storage.removeItem('dolphin_token');
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* रियल-टाइम सर्भरसँग कनेक्शन सुरु गर्ने
|
|
443
|
+
* @returns {Promise<void>}
|
|
444
|
+
*/
|
|
445
|
+
async connect() {
|
|
446
|
+
return new Promise((resolve, reject) => {
|
|
447
|
+
const protocol = this.httpUrl.startsWith('https') ? 'wss:' : 'ws:';
|
|
448
|
+
const wsUrl = `${protocol}//${this.host}/realtime?deviceId=${this.deviceId}`;
|
|
449
|
+
|
|
450
|
+
console.log(`[Dolphin] Connecting to ${wsUrl}...`);
|
|
451
|
+
this.socket = new WebSocket(wsUrl);
|
|
452
|
+
|
|
453
|
+
this.socket.onopen = () => {
|
|
454
|
+
console.log(`[Dolphin] Connected as "${this.deviceId}" 🐬`);
|
|
455
|
+
this.reconnectAttempts = 0;
|
|
456
|
+
resolve();
|
|
457
|
+
};
|
|
458
|
+
|
|
459
|
+
this.socket.onmessage = (event) => {
|
|
460
|
+
this._handleMessage(event.data);
|
|
461
|
+
};
|
|
462
|
+
|
|
463
|
+
this.socket.onclose = () => {
|
|
464
|
+
console.warn("[Dolphin] Connection closed");
|
|
465
|
+
this._maybeReconnect();
|
|
466
|
+
};
|
|
467
|
+
|
|
468
|
+
this.socket.onerror = (err) => {
|
|
469
|
+
console.error("[Dolphin] WebSocket Error:", err);
|
|
470
|
+
reject(err);
|
|
471
|
+
};
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
_handleMessage(data) {
|
|
476
|
+
try {
|
|
477
|
+
const parsed = JSON.parse(data);
|
|
478
|
+
|
|
479
|
+
// १. Signaling Messages
|
|
480
|
+
if (parsed.type && parsed.from && (parsed.to === this.deviceId || parsed.to === 'all')) {
|
|
481
|
+
// Auto-ACK for signaling messages
|
|
482
|
+
if (parsed.msgId && parsed.type !== 'ACK') {
|
|
483
|
+
this._sendAck(parsed.from, parsed.msgId);
|
|
484
|
+
}
|
|
485
|
+
this.signalHandlers.forEach(handler => handler(parsed));
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// २. File & Data Responses
|
|
489
|
+
if (parsed.type === 'FILE_AVAILABLE') {
|
|
490
|
+
this.fileHandlers.forEach(handler => handler(parsed));
|
|
491
|
+
}
|
|
492
|
+
if (parsed.type === 'FILE_CHUNK') {
|
|
493
|
+
this.saveFileProgress(parsed.fileId, parsed.chunkIndex);
|
|
494
|
+
this.handlers.forEach((callbacks, pattern) => {
|
|
495
|
+
if (pattern === 'file:chunk' || pattern === `file:chunk/${parsed.fileId}`) {
|
|
496
|
+
callbacks.forEach(cb => cb(parsed));
|
|
497
|
+
}
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
if (parsed.type === 'PULL_RESPONSE') {
|
|
501
|
+
this.handlers.forEach((callbacks, pattern) => {
|
|
502
|
+
if (pattern === 'pull:response' || pattern === `pull:response/${parsed.topic}`) {
|
|
503
|
+
callbacks.forEach(cb => cb(parsed.payload, parsed.topic));
|
|
504
|
+
}
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// ३. Pub/Sub Messages
|
|
509
|
+
if (parsed.topic && parsed.payload !== undefined) {
|
|
510
|
+
const topic = parsed.topic;
|
|
511
|
+
this.handlers.forEach((callbacks, pattern) => {
|
|
512
|
+
if (this._matchTopic(pattern, topic)) {
|
|
513
|
+
callbacks.forEach(cb => cb(parsed.payload, topic));
|
|
514
|
+
}
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
} catch (e) {
|
|
518
|
+
this.handlers.forEach((callbacks, pattern) => {
|
|
519
|
+
if (pattern === 'raw') callbacks.forEach(cb => cb(data));
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/**
|
|
525
|
+
* @param {string} to
|
|
526
|
+
* @param {string} msgId
|
|
527
|
+
* @private
|
|
528
|
+
*/
|
|
529
|
+
_sendAck(to, msgId) {
|
|
530
|
+
this.publish(`phone/signaling/${to}`, {
|
|
531
|
+
type: 'ACK',
|
|
532
|
+
from: this.deviceId,
|
|
533
|
+
to: to,
|
|
534
|
+
data: { ackId: msgId },
|
|
535
|
+
timestamp: Date.now()
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
_matchTopic(pattern, topic) {
|
|
540
|
+
if (pattern === topic || pattern === '#') return true;
|
|
541
|
+
const pParts = pattern.split('/');
|
|
542
|
+
const tParts = topic.split('/');
|
|
543
|
+
if (pParts.length !== tParts.length && !pattern.includes('#')) return false;
|
|
544
|
+
for (let i = 0; i < pParts.length; i++) {
|
|
545
|
+
if (pParts[i] === '#') return true;
|
|
546
|
+
if (pParts[i] !== '+' && pParts[i] !== tParts[i]) return false;
|
|
547
|
+
}
|
|
548
|
+
return pParts.length === tParts.length;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
/**
|
|
552
|
+
* @param {string} topic
|
|
553
|
+
* @param {TopicCallback} callback
|
|
554
|
+
*/
|
|
555
|
+
subscribe(topic, callback) {
|
|
556
|
+
if (!this.handlers.has(topic)) {
|
|
557
|
+
this.handlers.set(topic, new Set());
|
|
558
|
+
// Tell server we want to sub
|
|
559
|
+
if (this.socket && this.socket.readyState === WebSocket.OPEN) {
|
|
560
|
+
this.socket.send(JSON.stringify({ type: 'sub', topic }));
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
this.handlers.get(topic).add(callback);
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
/**
|
|
567
|
+
* @param {string} topic
|
|
568
|
+
* @param {TopicCallback} callback
|
|
569
|
+
*/
|
|
570
|
+
unsubscribe(topic, callback) {
|
|
571
|
+
if (this.handlers.has(topic)) {
|
|
572
|
+
const callbacks = this.handlers.get(topic);
|
|
573
|
+
callbacks.delete(callback);
|
|
574
|
+
if (callbacks.size === 0) {
|
|
575
|
+
this.handlers.delete(topic);
|
|
576
|
+
// Tell server we want to unsub
|
|
577
|
+
if (this.socket && this.socket.readyState === WebSocket.OPEN) {
|
|
578
|
+
this.socket.send(JSON.stringify({ type: 'unsub', topic }));
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
/**
|
|
585
|
+
* @param {string} topic
|
|
586
|
+
* @param {any} payload
|
|
587
|
+
*/
|
|
588
|
+
publish(topic, payload) {
|
|
589
|
+
if (this.socket && this.socket.readyState === WebSocket.OPEN) {
|
|
590
|
+
this.socket.send(JSON.stringify({ topic, payload }));
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
/**
|
|
595
|
+
* High-frequency data push
|
|
596
|
+
* @param {string} topic
|
|
597
|
+
* @param {any} payload
|
|
598
|
+
*/
|
|
599
|
+
pubPush(topic, payload) {
|
|
600
|
+
if (this.socket && this.socket.readyState === WebSocket.OPEN) {
|
|
601
|
+
this.socket.send(JSON.stringify({ type: 'pub', topic, payload }));
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
/**
|
|
606
|
+
* Request historical data from topic
|
|
607
|
+
* @param {string} topic
|
|
608
|
+
* @param {number} [count]
|
|
609
|
+
*/
|
|
610
|
+
subPull(topic, count = 10) {
|
|
611
|
+
if (this.socket && this.socket.readyState === WebSocket.OPEN) {
|
|
612
|
+
this.socket.send(JSON.stringify({
|
|
613
|
+
type: 'PULL_REQUEST',
|
|
614
|
+
topic,
|
|
615
|
+
count
|
|
616
|
+
}));
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
/**
|
|
621
|
+
* Start downloading a file by chunks
|
|
622
|
+
* @param {string} fileId
|
|
623
|
+
* @param {number} [startChunk]
|
|
624
|
+
*/
|
|
625
|
+
subFile(fileId, startChunk = 0) {
|
|
626
|
+
if (this.socket && this.socket.readyState === WebSocket.OPEN) {
|
|
627
|
+
this.socket.send(JSON.stringify({
|
|
628
|
+
type: 'FILE_REQUEST',
|
|
629
|
+
fileId,
|
|
630
|
+
startChunk
|
|
631
|
+
}));
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
/**
|
|
636
|
+
* Resume a file download from last saved progress
|
|
637
|
+
* @param {string} fileId
|
|
638
|
+
*/
|
|
639
|
+
resumeFile(fileId) {
|
|
640
|
+
const lastChunk = parseInt(localStorage.getItem(`dolphin_file_${fileId}`) || "-1");
|
|
641
|
+
this.subFile(fileId, lastChunk + 1);
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
/**
|
|
645
|
+
* Save download progress
|
|
646
|
+
* @param {string} fileId
|
|
647
|
+
* @param {number} chunkIndex
|
|
648
|
+
*/
|
|
649
|
+
saveFileProgress(fileId, chunkIndex) {
|
|
650
|
+
this.storage.setItem(`dolphin_file_${fileId}`, chunkIndex.toString());
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
/**
|
|
654
|
+
* @param {function(SignalMessage): void} handler
|
|
655
|
+
*/
|
|
656
|
+
onSignal(handler) {
|
|
657
|
+
this.signalHandlers.add(handler);
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
/**
|
|
661
|
+
* @param {function(SignalMessage): void} handler
|
|
662
|
+
*/
|
|
663
|
+
offSignal(handler) {
|
|
664
|
+
this.signalHandlers.delete(handler);
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
/**
|
|
668
|
+
* @param {function(FileMetadata): void} handler
|
|
669
|
+
*/
|
|
670
|
+
onFileAvailable(handler) {
|
|
671
|
+
this.fileHandlers.add(handler);
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
/**
|
|
675
|
+
* @param {function(FileMetadata): void} handler
|
|
676
|
+
*/
|
|
677
|
+
offFileAvailable(handler) {
|
|
678
|
+
this.fileHandlers.delete(handler);
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
_maybeReconnect() {
|
|
682
|
+
if (this.reconnectAttempts < this.maxReconnectAttempts) {
|
|
683
|
+
this.reconnectAttempts++;
|
|
684
|
+
const delay = Math.pow(2, this.reconnectAttempts) * 1000;
|
|
685
|
+
setTimeout(() => this.connect(), delay);
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
// Browser Global Export
|
|
691
|
+
if (typeof window !== 'undefined') {
|
|
692
|
+
// @ts-ignore
|
|
693
|
+
window.DolphinClient = DolphinClient;
|
|
694
|
+
// @ts-ignore
|
|
695
|
+
window.dolphin = new DolphinClient();
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
// NodeJS/CommonJS/ESM Export support
|
|
699
|
+
if (typeof module !== 'undefined' && module.exports) {
|
|
700
|
+
module.exports = { DolphinClient };
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
export { DolphinClient };
|