@cemscale-voip/voip-sdk 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/dist/client.js ADDED
@@ -0,0 +1,651 @@
1
+ // ============================================================
2
+ // @cemscale/voip-sdk — API Client (HTTP + WebSocket)
3
+ // ============================================================
4
+ // --- HTTP Transport ---
5
+ class HttpError extends Error {
6
+ status;
7
+ statusText;
8
+ body;
9
+ constructor(status, statusText, body) {
10
+ super(`HTTP ${status}: ${typeof body === 'object' && body && 'error' in body ? body.error : statusText}`);
11
+ this.status = status;
12
+ this.statusText = statusText;
13
+ this.body = body;
14
+ this.name = 'HttpError';
15
+ }
16
+ }
17
+ export { HttpError };
18
+ // --- VoIPClient ---
19
+ export class VoIPClient {
20
+ apiUrl;
21
+ token;
22
+ tenantId;
23
+ timeout;
24
+ // WebSocket
25
+ ws = null;
26
+ wsReconnectTimer = null;
27
+ wsListeners = new Map();
28
+ wsPingInterval = null;
29
+ wsConnected = false;
30
+ constructor(config) {
31
+ this.apiUrl = config.apiUrl.replace(/\/+$/, '');
32
+ this.token = config.token ?? null;
33
+ this.tenantId = config.tenantId ?? null;
34
+ this.timeout = config.timeout ?? 15000;
35
+ }
36
+ // -------------------------------------------------------------------
37
+ // Token management
38
+ // -------------------------------------------------------------------
39
+ setToken(token) { this.token = token; }
40
+ getToken() { return this.token; }
41
+ setTenantId(tenantId) { this.tenantId = tenantId; }
42
+ getTenantId() { return this.tenantId; }
43
+ // -------------------------------------------------------------------
44
+ // HTTP helpers
45
+ // -------------------------------------------------------------------
46
+ async request(method, path, body, query) {
47
+ let url = `${this.apiUrl}${path}`;
48
+ if (query) {
49
+ const params = new URLSearchParams();
50
+ for (const [key, value] of Object.entries(query)) {
51
+ if (value !== undefined)
52
+ params.set(key, String(value));
53
+ }
54
+ const qs = params.toString();
55
+ if (qs)
56
+ url += `?${qs}`;
57
+ }
58
+ const headers = {};
59
+ if (this.token)
60
+ headers['Authorization'] = `Bearer ${this.token}`;
61
+ if (this.tenantId && this.tenantId !== 'all')
62
+ headers['X-Tenant-ID'] = this.tenantId;
63
+ // Only set Content-Type when there's a body
64
+ if (body != null)
65
+ headers['Content-Type'] = 'application/json';
66
+ const controller = new AbortController();
67
+ const timer = setTimeout(() => controller.abort(), this.timeout);
68
+ try {
69
+ const response = await fetch(url, {
70
+ method,
71
+ headers,
72
+ body: body != null ? JSON.stringify(body) : undefined,
73
+ signal: controller.signal,
74
+ });
75
+ if (response.status === 204)
76
+ return {};
77
+ const contentType = response.headers.get('content-type') || '';
78
+ const data = contentType.includes('application/json')
79
+ ? await response.json()
80
+ : await response.text();
81
+ if (!response.ok)
82
+ throw new HttpError(response.status, response.statusText, data);
83
+ return data;
84
+ }
85
+ finally {
86
+ clearTimeout(timer);
87
+ }
88
+ }
89
+ // -------------------------------------------------------------------
90
+ // Auth
91
+ // -------------------------------------------------------------------
92
+ /** Login as an extension user */
93
+ async login(params) {
94
+ const result = await this.request('POST', '/api/auth/login', params);
95
+ this.token = result.token;
96
+ this.tenantId = params.tenantId;
97
+ return result;
98
+ }
99
+ /** Login as a tenant admin */
100
+ async adminLogin(params) {
101
+ const result = await this.request('POST', '/api/auth/admin-login', params);
102
+ this.token = result.token;
103
+ if (result.tenant)
104
+ this.tenantId = result.tenant.id;
105
+ return result;
106
+ }
107
+ /** Login as platform superadmin */
108
+ async superadminLogin(params) {
109
+ const result = await this.request('POST', '/api/auth/superadmin-login', params);
110
+ this.token = result.token;
111
+ return result;
112
+ }
113
+ /** Get current user info */
114
+ async me() {
115
+ return this.request('GET', '/api/auth/me');
116
+ }
117
+ /** Get TURN credentials for WebRTC */
118
+ async getTurnCredentials() {
119
+ return this.request('GET', '/api/auth/turn-credentials');
120
+ }
121
+ // -------------------------------------------------------------------
122
+ // Calls (CDR)
123
+ // -------------------------------------------------------------------
124
+ /** List call records with pagination and filters */
125
+ async listCalls(params) {
126
+ return this.request('GET', '/api/calls', undefined, params);
127
+ }
128
+ /** Get a single call record */
129
+ async getCall(id) {
130
+ return this.request('GET', `/api/calls/${id}`);
131
+ }
132
+ /** Originate a call via FreeSWITCH */
133
+ async originate(params) {
134
+ return this.request('POST', '/api/calls/originate', params);
135
+ }
136
+ /** Hang up an active call */
137
+ async hangup(uuid) {
138
+ return this.request('POST', `/api/calls/${uuid}/hangup`);
139
+ }
140
+ /** Transfer an active call (blind or attended) */
141
+ async transfer(uuid, params) {
142
+ return this.request('POST', `/api/calls/${uuid}/transfer`, params);
143
+ }
144
+ /** List active calls from FreeSWITCH */
145
+ async getActiveCalls() {
146
+ return this.request('GET', '/api/calls/active');
147
+ }
148
+ /** Get call statistics for a period */
149
+ async getCallStats(period) {
150
+ return this.request('GET', '/api/calls/stats', undefined, period ? { period } : undefined);
151
+ }
152
+ /** Place a call on hold or resume */
153
+ async holdCall(uuid, hold = true) {
154
+ return this.request('POST', `/api/calls/${uuid}/hold`, { hold });
155
+ }
156
+ /** Park an active call */
157
+ async parkCall(uuid, slot) {
158
+ return this.request('POST', `/api/calls/${uuid}/park`, slot ? { slot } : undefined);
159
+ }
160
+ /** List parked calls */
161
+ async getParkedCalls() {
162
+ return this.request('GET', '/api/calls/parked');
163
+ }
164
+ /** Export CDR as CSV. Returns raw CSV text. */
165
+ async exportCalls(params) {
166
+ return this.request('GET', '/api/calls/export', undefined, params);
167
+ }
168
+ // -------------------------------------------------------------------
169
+ // Extensions
170
+ // -------------------------------------------------------------------
171
+ /** List extensions for the current tenant */
172
+ async listExtensions() {
173
+ return this.request('GET', '/api/extensions');
174
+ }
175
+ /** Get a single extension */
176
+ async getExtension(id) {
177
+ return this.request('GET', `/api/extensions/${id}`);
178
+ }
179
+ /** Create an extension (also creates Kamailio subscriber) */
180
+ async createExtension(params) {
181
+ return this.request('POST', '/api/extensions', params);
182
+ }
183
+ /** Update an extension */
184
+ async updateExtension(id, params) {
185
+ return this.request('PUT', `/api/extensions/${id}`, params);
186
+ }
187
+ /** Delete an extension */
188
+ async deleteExtension(id) {
189
+ return this.request('DELETE', `/api/extensions/${id}`);
190
+ }
191
+ /** Get call forward settings for an extension */
192
+ async getCallForward(extensionId) {
193
+ return this.request('GET', `/api/extensions/${extensionId}/forward`);
194
+ }
195
+ /** Set call forward for an extension */
196
+ async setCallForward(extensionId, params) {
197
+ return this.request('PUT', `/api/extensions/${extensionId}/forward`, params);
198
+ }
199
+ /** Lookup extension by CRM user ID */
200
+ async getExtensionByCrmUser(crmUserId) {
201
+ return this.request('GET', `/api/extensions/by-crm-user/${crmUserId}`);
202
+ }
203
+ /** Update CRM mapping for an extension */
204
+ async updateCrmMapping(extensionId, params) {
205
+ return this.request('PUT', `/api/extensions/${extensionId}/crm-mapping`, params);
206
+ }
207
+ // -------------------------------------------------------------------
208
+ // Tenants (superadmin)
209
+ // -------------------------------------------------------------------
210
+ /** List all tenants (superadmin only) */
211
+ async listTenants() {
212
+ return this.request('GET', '/api/tenants');
213
+ }
214
+ /** Get a single tenant */
215
+ async getTenant(id) {
216
+ return this.request('GET', `/api/tenants/${id}`);
217
+ }
218
+ /** Create a tenant (superadmin only) */
219
+ async createTenant(params) {
220
+ return this.request('POST', '/api/tenants', params);
221
+ }
222
+ /** Update a tenant (superadmin only) */
223
+ async updateTenant(id, params) {
224
+ return this.request('PUT', `/api/tenants/${id}`, params);
225
+ }
226
+ /** Delete a tenant and all data (superadmin only) */
227
+ async deleteTenant(id) {
228
+ return this.request('DELETE', `/api/tenants/${id}`);
229
+ }
230
+ /** Get tenant statistics */
231
+ async getTenantStats(id) {
232
+ return this.request('GET', `/api/tenants/${id}/stats`);
233
+ }
234
+ // -------------------------------------------------------------------
235
+ // DIDs
236
+ // -------------------------------------------------------------------
237
+ /** List DID numbers */
238
+ async listDids() {
239
+ return this.request('GET', '/api/dids');
240
+ }
241
+ /** Create a DID */
242
+ async createDid(params) {
243
+ return this.request('POST', '/api/dids', params);
244
+ }
245
+ /** Update a DID */
246
+ async updateDid(id, params) {
247
+ return this.request('PUT', `/api/dids/${id}`, params);
248
+ }
249
+ /** Delete a DID */
250
+ async deleteDid(id) {
251
+ return this.request('DELETE', `/api/dids/${id}`);
252
+ }
253
+ // -------------------------------------------------------------------
254
+ // IVR
255
+ // -------------------------------------------------------------------
256
+ /** List IVR menus */
257
+ async listIvrMenus() {
258
+ return this.request('GET', '/api/ivr');
259
+ }
260
+ /** Get a single IVR menu */
261
+ async getIvrMenu(id) {
262
+ return this.request('GET', `/api/ivr/${id}`);
263
+ }
264
+ /** Create an IVR menu */
265
+ async createIvrMenu(params) {
266
+ return this.request('POST', '/api/ivr', params);
267
+ }
268
+ /** Update an IVR menu */
269
+ async updateIvrMenu(id, params) {
270
+ return this.request('PUT', `/api/ivr/${id}`, params);
271
+ }
272
+ /** Delete an IVR menu */
273
+ async deleteIvrMenu(id) {
274
+ return this.request('DELETE', `/api/ivr/${id}`);
275
+ }
276
+ // -------------------------------------------------------------------
277
+ // Recordings
278
+ // -------------------------------------------------------------------
279
+ /** List recordings with pagination */
280
+ async listRecordings(params) {
281
+ return this.request('GET', '/api/recordings', undefined, params);
282
+ }
283
+ /** Get a presigned download URL for a recording */
284
+ async getRecordingUrl(id) {
285
+ return this.request('GET', `/api/recordings/${id}/url`);
286
+ }
287
+ /** Delete a recording */
288
+ async deleteRecording(id) {
289
+ return this.request('DELETE', `/api/recordings/${id}`);
290
+ }
291
+ // -------------------------------------------------------------------
292
+ // Conferences
293
+ // -------------------------------------------------------------------
294
+ /** List active conferences */
295
+ async listConferences() {
296
+ return this.request('GET', '/api/conferences');
297
+ }
298
+ /** Get conference details */
299
+ async getConference(name) {
300
+ return this.request('GET', `/api/conferences/${name}`);
301
+ }
302
+ /** Join a call to a conference */
303
+ async joinConference(conferenceName, callUuid) {
304
+ return this.request('POST', `/api/conferences/${conferenceName}/join`, { callUuid });
305
+ }
306
+ /** Transfer a call to conference */
307
+ async transferToConference(uuid, conferenceName) {
308
+ return this.request('POST', `/api/calls/${uuid}/conference`, conferenceName ? { conferenceName } : undefined);
309
+ }
310
+ /** Kick a member from conference */
311
+ async kickFromConference(conferenceName, memberId) {
312
+ return this.request('POST', `/api/conferences/${conferenceName}/kick`, { memberId });
313
+ }
314
+ /** Mute/unmute a conference member */
315
+ async muteConferenceMember(conferenceName, memberId, mute = true) {
316
+ return this.request('POST', `/api/conferences/${conferenceName}/mute`, { memberId, mute });
317
+ }
318
+ /** Deaf/undeaf a conference member */
319
+ async deafConferenceMember(conferenceName, memberId, deaf = true) {
320
+ return this.request('POST', `/api/conferences/${conferenceName}/deaf`, { memberId, deaf });
321
+ }
322
+ /** Lock/unlock a conference */
323
+ async lockConference(conferenceName, lock = true) {
324
+ return this.request('POST', `/api/conferences/${conferenceName}/lock`, { lock });
325
+ }
326
+ /** Start/stop conference recording */
327
+ async recordConference(conferenceName, action) {
328
+ return this.request('POST', `/api/conferences/${conferenceName}/record`, { action });
329
+ }
330
+ // -------------------------------------------------------------------
331
+ // Ring Groups
332
+ // -------------------------------------------------------------------
333
+ /** List ring groups */
334
+ async listRingGroups() {
335
+ return this.request('GET', '/api/ring-groups');
336
+ }
337
+ /** Get a ring group */
338
+ async getRingGroup(id) {
339
+ return this.request('GET', `/api/ring-groups/${id}`);
340
+ }
341
+ /** Create a ring group */
342
+ async createRingGroup(params) {
343
+ return this.request('POST', '/api/ring-groups', params);
344
+ }
345
+ /** Update a ring group */
346
+ async updateRingGroup(id, params) {
347
+ return this.request('PUT', `/api/ring-groups/${id}`, params);
348
+ }
349
+ /** Delete a ring group */
350
+ async deleteRingGroup(id) {
351
+ return this.request('DELETE', `/api/ring-groups/${id}`);
352
+ }
353
+ // -------------------------------------------------------------------
354
+ // Call Queues
355
+ // -------------------------------------------------------------------
356
+ /** List call queues */
357
+ async listQueues() {
358
+ return this.request('GET', '/api/queues');
359
+ }
360
+ /** Get a call queue */
361
+ async getQueue(id) {
362
+ return this.request('GET', `/api/queues/${id}`);
363
+ }
364
+ /** Create a call queue */
365
+ async createQueue(params) {
366
+ return this.request('POST', '/api/queues', params);
367
+ }
368
+ /** Update a call queue */
369
+ async updateQueue(id, params) {
370
+ return this.request('PUT', `/api/queues/${id}`, params);
371
+ }
372
+ /** Delete a call queue */
373
+ async deleteQueue(id) {
374
+ return this.request('DELETE', `/api/queues/${id}`);
375
+ }
376
+ /** Pause/unpause a queue agent */
377
+ async pauseQueueAgent(queueId, agentId, paused = true) {
378
+ return this.request('POST', `/api/queues/${queueId}/agents/${agentId}/pause`, { paused });
379
+ }
380
+ /** Login/logout a queue agent */
381
+ async loginQueueAgent(queueId, agentId, loggedIn = true) {
382
+ return this.request('POST', `/api/queues/${queueId}/agents/${agentId}/login`, { loggedIn });
383
+ }
384
+ /** Get real-time queue statistics (agent counts, today's call metrics) */
385
+ async getQueueStats(queueId) {
386
+ return this.request('GET', `/api/queues/${queueId}/stats`);
387
+ }
388
+ // -------------------------------------------------------------------
389
+ // Webhooks
390
+ // -------------------------------------------------------------------
391
+ /** List webhooks */
392
+ async listWebhooks() {
393
+ return this.request('GET', '/api/webhooks');
394
+ }
395
+ /** Get webhook details with recent deliveries */
396
+ async getWebhook(id) {
397
+ return this.request('GET', `/api/webhooks/${id}`);
398
+ }
399
+ /** Create a webhook */
400
+ async createWebhook(params) {
401
+ return this.request('POST', '/api/webhooks', params);
402
+ }
403
+ /** Update a webhook */
404
+ async updateWebhook(id, params) {
405
+ return this.request('PUT', `/api/webhooks/${id}`, params);
406
+ }
407
+ /** Delete a webhook */
408
+ async deleteWebhook(id) {
409
+ return this.request('DELETE', `/api/webhooks/${id}`);
410
+ }
411
+ /** Send a test webhook event */
412
+ async testWebhook(id) {
413
+ return this.request('POST', `/api/webhooks/${id}/test`);
414
+ }
415
+ /** List webhook delivery history */
416
+ async listWebhookDeliveries(id, page, limit) {
417
+ return this.request('GET', `/api/webhooks/${id}/deliveries`, undefined, { page, limit });
418
+ }
419
+ // -------------------------------------------------------------------
420
+ // Voicemail
421
+ // -------------------------------------------------------------------
422
+ /** List voicemail messages with pagination */
423
+ async listVoicemails(params) {
424
+ return this.request('GET', '/api/voicemail', undefined, params);
425
+ }
426
+ /** Get voicemail count (total and unread) */
427
+ async getVoicemailCount(extension) {
428
+ return this.request('GET', '/api/voicemail/count', undefined, extension ? { extension } : undefined);
429
+ }
430
+ /** Get a single voicemail */
431
+ async getVoicemail(id) {
432
+ return this.request('GET', `/api/voicemail/${id}`);
433
+ }
434
+ /** Get voicemail audio URL (presigned S3 or direct) */
435
+ async getVoicemailAudioUrl(id) {
436
+ return this.request('GET', `/api/voicemail/${id}/audio`);
437
+ }
438
+ /** Mark voicemail as read */
439
+ async markVoicemailRead(id) {
440
+ return this.request('PUT', `/api/voicemail/${id}/read`);
441
+ }
442
+ /** Delete a voicemail */
443
+ async deleteVoicemail(id) {
444
+ return this.request('DELETE', `/api/voicemail/${id}`);
445
+ }
446
+ /** Bulk delete voicemails */
447
+ async bulkDeleteVoicemails(params) {
448
+ return this.request('DELETE', '/api/voicemail', params);
449
+ }
450
+ // -------------------------------------------------------------------
451
+ // Presence (REST)
452
+ // -------------------------------------------------------------------
453
+ /** Get simple presence (extension -> status) */
454
+ async getPresence() {
455
+ return this.request('GET', '/api/presence');
456
+ }
457
+ /** Get detailed presence with call info */
458
+ async getPresenceDetailed() {
459
+ return this.request('GET', '/api/presence/detailed');
460
+ }
461
+ // -------------------------------------------------------------------
462
+ // Reports
463
+ // -------------------------------------------------------------------
464
+ /** Get dashboard summary stats */
465
+ async getDashboardStats() {
466
+ return this.request('GET', '/api/reports/dashboard');
467
+ }
468
+ /** Get calls grouped by day */
469
+ async getCallsByDay(days) {
470
+ return this.request('GET', '/api/reports/calls-by-day', undefined, days ? { days } : undefined);
471
+ }
472
+ /** Get top extensions by call volume */
473
+ async getTopExtensions(days, limit) {
474
+ return this.request('GET', '/api/reports/top-extensions', undefined, { days, limit });
475
+ }
476
+ // -------------------------------------------------------------------
477
+ // Blocklist (Call Blocking)
478
+ // -------------------------------------------------------------------
479
+ /** List all blocked numbers for the tenant */
480
+ async listBlockedNumbers() {
481
+ return this.request('GET', '/api/blocklist');
482
+ }
483
+ /** Block a phone number */
484
+ async blockNumber(params) {
485
+ return this.request('POST', '/api/blocklist', params);
486
+ }
487
+ /** Unblock a phone number by ID */
488
+ async unblockNumber(id) {
489
+ return this.request('DELETE', `/api/blocklist/${id}`);
490
+ }
491
+ /** Check if a specific number is blocked */
492
+ async checkBlocked(number) {
493
+ return this.request('GET', `/api/blocklist/check/${encodeURIComponent(number)}`);
494
+ }
495
+ // -------------------------------------------------------------------
496
+ // Business Hours / Schedules
497
+ // -------------------------------------------------------------------
498
+ /** List business hour schedules */
499
+ async listSchedules() {
500
+ return this.request('GET', '/api/schedules');
501
+ }
502
+ /** Get a single schedule */
503
+ async getSchedule(id) {
504
+ return this.request('GET', `/api/schedules/${id}`);
505
+ }
506
+ /** Create a business hours schedule */
507
+ async createSchedule(params) {
508
+ return this.request('POST', '/api/schedules', params);
509
+ }
510
+ /** Update a business hours schedule */
511
+ async updateSchedule(id, params) {
512
+ return this.request('PUT', `/api/schedules/${id}`, params);
513
+ }
514
+ /** Delete a business hours schedule */
515
+ async deleteSchedule(id) {
516
+ return this.request('DELETE', `/api/schedules/${id}`);
517
+ }
518
+ /** Check if a schedule is currently open or closed */
519
+ async getScheduleStatus(id) {
520
+ return this.request('GET', `/api/schedules/${id}/status`);
521
+ }
522
+ // -------------------------------------------------------------------
523
+ // SIP Trunks
524
+ // -------------------------------------------------------------------
525
+ /** List SIP trunks (passwords masked) */
526
+ async listTrunks() {
527
+ return this.request('GET', '/api/trunks');
528
+ }
529
+ /** Get a single SIP trunk (password masked) */
530
+ async getTrunk(id) {
531
+ return this.request('GET', `/api/trunks/${id}`);
532
+ }
533
+ /** Create a SIP trunk */
534
+ async createTrunk(params) {
535
+ return this.request('POST', '/api/trunks', params);
536
+ }
537
+ /** Update a SIP trunk */
538
+ async updateTrunk(id, params) {
539
+ return this.request('PUT', `/api/trunks/${id}`, params);
540
+ }
541
+ /** Delete a SIP trunk */
542
+ async deleteTrunk(id) {
543
+ return this.request('DELETE', `/api/trunks/${id}`);
544
+ }
545
+ // -------------------------------------------------------------------
546
+ // WebSocket — real-time events
547
+ // -------------------------------------------------------------------
548
+ /** Connect to the real-time WebSocket */
549
+ connectWebSocket() {
550
+ if (this.ws && (this.ws.readyState === WebSocket.OPEN || this.ws.readyState === WebSocket.CONNECTING)) {
551
+ return;
552
+ }
553
+ const wsUrl = this.apiUrl.replace(/^http/, 'ws') + '/api/ws';
554
+ this.ws = new WebSocket(wsUrl);
555
+ this.ws.onopen = () => {
556
+ if (this.token) {
557
+ this.ws.send(JSON.stringify({ type: 'auth', token: this.token }));
558
+ }
559
+ this.wsPingInterval = setInterval(() => {
560
+ if (this.ws?.readyState === WebSocket.OPEN) {
561
+ this.ws.send(JSON.stringify({ type: 'ping' }));
562
+ }
563
+ }, 30000);
564
+ };
565
+ this.ws.onmessage = (event) => {
566
+ try {
567
+ const message = JSON.parse(event.data);
568
+ if (message.type === 'auth_success')
569
+ this.wsConnected = true;
570
+ this.emitWs(message.type, message);
571
+ this.emitWs('*', message);
572
+ }
573
+ catch {
574
+ // Ignore malformed messages
575
+ }
576
+ };
577
+ this.ws.onclose = () => {
578
+ this.wsConnected = false;
579
+ this.clearWsPing();
580
+ this.emitWs('disconnected', undefined);
581
+ this.scheduleWsReconnect();
582
+ };
583
+ this.ws.onerror = () => {
584
+ // onclose will fire after onerror
585
+ };
586
+ }
587
+ /** Disconnect the WebSocket */
588
+ disconnectWebSocket() {
589
+ this.clearWsReconnect();
590
+ this.clearWsPing();
591
+ if (this.ws) {
592
+ this.ws.onclose = null;
593
+ this.ws.close();
594
+ this.ws = null;
595
+ }
596
+ this.wsConnected = false;
597
+ }
598
+ /** Check if WebSocket is connected and authenticated */
599
+ isWebSocketConnected() {
600
+ return this.wsConnected;
601
+ }
602
+ /** Subscribe to a WebSocket event type. Returns unsubscribe function. */
603
+ onWsEvent(eventType, callback) {
604
+ if (!this.wsListeners.has(eventType)) {
605
+ this.wsListeners.set(eventType, new Set());
606
+ }
607
+ this.wsListeners.get(eventType).add(callback);
608
+ return () => { this.wsListeners.get(eventType)?.delete(callback); };
609
+ }
610
+ emitWs(eventType, data) {
611
+ const listeners = this.wsListeners.get(eventType);
612
+ if (listeners) {
613
+ for (const listener of listeners) {
614
+ try {
615
+ listener(data);
616
+ }
617
+ catch { /* Don't let one bad listener break others */ }
618
+ }
619
+ }
620
+ }
621
+ scheduleWsReconnect() {
622
+ if (this.wsReconnectTimer)
623
+ return;
624
+ this.wsReconnectTimer = setTimeout(() => {
625
+ this.wsReconnectTimer = null;
626
+ this.connectWebSocket();
627
+ }, 3000);
628
+ }
629
+ clearWsReconnect() {
630
+ if (this.wsReconnectTimer) {
631
+ clearTimeout(this.wsReconnectTimer);
632
+ this.wsReconnectTimer = null;
633
+ }
634
+ }
635
+ clearWsPing() {
636
+ if (this.wsPingInterval) {
637
+ clearInterval(this.wsPingInterval);
638
+ this.wsPingInterval = null;
639
+ }
640
+ }
641
+ // -------------------------------------------------------------------
642
+ // Cleanup
643
+ // -------------------------------------------------------------------
644
+ /** Disconnect all connections and clean up */
645
+ destroy() {
646
+ this.disconnectWebSocket();
647
+ this.token = null;
648
+ this.tenantId = null;
649
+ }
650
+ }
651
+ //# sourceMappingURL=client.js.map