@droponair/sdk-js 0.19.0 → 0.22.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/CHANGELOG.md +38 -0
- package/README.md +55 -0
- package/dist/core/messaging-client.d.ts +3 -1
- package/dist/core/messaging-client.js +126 -90
- package/dist/core/types.d.ts +8 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +7 -1
- package/dist/transport/auto-select.d.ts +47 -0
- package/dist/transport/auto-select.js +64 -0
- package/dist/transport/messaging-transport.d.ts +33 -0
- package/dist/transport/messaging-transport.js +2 -0
- package/dist/transport/sse-messaging-transport.d.ts +28 -0
- package/dist/transport/sse-messaging-transport.js +59 -0
- package/dist/transport/webtransport-transport.d.ts +61 -0
- package/dist/transport/webtransport-transport.js +124 -0
- package/dist/transport/ws-messaging-transport.d.ts +24 -0
- package/dist/transport/ws-messaging-transport.js +67 -0
- package/dist/transport/wt-messaging-transport.d.ts +25 -0
- package/dist/transport/wt-messaging-transport.js +54 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
|
@@ -3,6 +3,10 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.MessagingClient = void 0;
|
|
4
4
|
const bytes_1 = require("./bytes");
|
|
5
5
|
const protobuf_codec_1 = require("../transport/protobuf-codec");
|
|
6
|
+
const auto_select_1 = require("../transport/auto-select");
|
|
7
|
+
const ws_messaging_transport_1 = require("../transport/ws-messaging-transport");
|
|
8
|
+
const sse_messaging_transport_1 = require("../transport/sse-messaging-transport");
|
|
9
|
+
const wt_messaging_transport_1 = require("../transport/wt-messaging-transport");
|
|
6
10
|
const version_1 = require("../version");
|
|
7
11
|
const attachment_client_1 = require("../attachment/attachment-client");
|
|
8
12
|
const STORAGE_DEVICE_ID = 'droponair.device.id.v1';
|
|
@@ -56,7 +60,7 @@ class MessagingClient {
|
|
|
56
60
|
*/
|
|
57
61
|
onAppForeground() {
|
|
58
62
|
this.log('app_foreground_detected', {
|
|
59
|
-
wsState: this.
|
|
63
|
+
wsState: (this.transport?.isOpen() ? 'OPEN' : 'CLOSED'),
|
|
60
64
|
shouldReconnect: this.shouldReconnect,
|
|
61
65
|
...this.jwtSummary(this.dropOnAirJwt),
|
|
62
66
|
});
|
|
@@ -65,21 +69,21 @@ class MessagingClient {
|
|
|
65
69
|
}
|
|
66
70
|
// If the WebSocket is already gone (background disconnect), the existing
|
|
67
71
|
// onclose handler will already be scheduling a reconnect, nothing to do.
|
|
68
|
-
if (!this.
|
|
72
|
+
if (!this.transport?.isOpen()) {
|
|
69
73
|
return;
|
|
70
74
|
}
|
|
71
75
|
const jwt = this.dropOnAirJwt;
|
|
72
76
|
if (!jwt) {
|
|
73
77
|
// No token at all, close and let the reconnect loop fetch a new one.
|
|
74
78
|
this.log('foreground_reconnect_no_jwt');
|
|
75
|
-
this.
|
|
79
|
+
this.transport.close(1000, 'FOREGROUND_NO_JWT');
|
|
76
80
|
return;
|
|
77
81
|
}
|
|
78
82
|
if ((0, bytes_1.isJwtExpired)(jwt)) {
|
|
79
83
|
// Token already expired while we were in background, close NOW so the
|
|
80
84
|
// reconnect fetches a fresh token before the user tries to send anything.
|
|
81
85
|
this.log('foreground_reconnect_jwt_expired', this.jwtSummary(jwt));
|
|
82
|
-
this.
|
|
86
|
+
this.transport.close(1000, 'FOREGROUND_JWT_EXPIRED');
|
|
83
87
|
return;
|
|
84
88
|
}
|
|
85
89
|
// Token is still valid, reschedule the proactive timer based on actual
|
|
@@ -113,7 +117,7 @@ class MessagingClient {
|
|
|
113
117
|
this.log('proactive_refresh_scheduled', { exp, secondsUntilExpiry, rotateInSeconds });
|
|
114
118
|
this.proactiveRefreshTimer = setTimeout(async () => {
|
|
115
119
|
this.proactiveRefreshTimer = null;
|
|
116
|
-
if (!this.shouldReconnect || !this.
|
|
120
|
+
if (!this.shouldReconnect || !this.transport) {
|
|
117
121
|
return;
|
|
118
122
|
}
|
|
119
123
|
this.log('proactive_refresh_triggered', { exp });
|
|
@@ -125,8 +129,8 @@ class MessagingClient {
|
|
|
125
129
|
this.logError('proactive_refresh_token_failed', { error: String(err?.message ?? err) });
|
|
126
130
|
}
|
|
127
131
|
// Gracefully close the current connection; onclose will schedule reconnect.
|
|
128
|
-
if (this.
|
|
129
|
-
this.
|
|
132
|
+
if (this.transport?.isOpen()) {
|
|
133
|
+
this.transport.close(1000, 'PROACTIVE_TOKEN_REFRESH');
|
|
130
134
|
}
|
|
131
135
|
}, rotateInSeconds * 1000);
|
|
132
136
|
}
|
|
@@ -193,7 +197,8 @@ class MessagingClient {
|
|
|
193
197
|
this.sessionManager = sessionManager;
|
|
194
198
|
this.storage = storage;
|
|
195
199
|
this.codec = new protobuf_codec_1.ProtobufCodec();
|
|
196
|
-
this.
|
|
200
|
+
this.transport = null;
|
|
201
|
+
this.resolvedTransportName = null;
|
|
197
202
|
this.shouldReconnect = true;
|
|
198
203
|
this.reconnectTimer = null;
|
|
199
204
|
this.dropOnAirJwt = null;
|
|
@@ -308,14 +313,14 @@ class MessagingClient {
|
|
|
308
313
|
clearTimeout(this.proactiveRefreshTimer);
|
|
309
314
|
this.proactiveRefreshTimer = null;
|
|
310
315
|
}
|
|
311
|
-
if (this.
|
|
312
|
-
this.
|
|
313
|
-
this.
|
|
316
|
+
if (this.transport) {
|
|
317
|
+
this.transport.close();
|
|
318
|
+
this.transport = null;
|
|
314
319
|
}
|
|
315
320
|
this.emitEvent({ type: 'DISCONNECTED' });
|
|
316
321
|
}
|
|
317
322
|
async sendMessage(toUserId, plaintextMessage, options) {
|
|
318
|
-
if (!this.
|
|
323
|
+
if (!this.transport?.isOpen()) {
|
|
319
324
|
throw new Error('DropOnAir websocket is not connected');
|
|
320
325
|
}
|
|
321
326
|
if (!this.currentUserId) {
|
|
@@ -377,7 +382,7 @@ class MessagingClient {
|
|
|
377
382
|
if (options?.attachments && options.attachments.length > 0) {
|
|
378
383
|
envelope.attachments = options.attachments.map(a => this.attachmentClient.toWire(a));
|
|
379
384
|
}
|
|
380
|
-
this.
|
|
385
|
+
this.transport.send(this.codec.encodeEnvelope(envelope));
|
|
381
386
|
}
|
|
382
387
|
else {
|
|
383
388
|
// Legacy fallback: peer has no device keys (old client)
|
|
@@ -400,15 +405,15 @@ class MessagingClient {
|
|
|
400
405
|
if (options?.attachments && options.attachments.length > 0) {
|
|
401
406
|
envelope.attachments = options.attachments.map(a => this.attachmentClient.toWire(a));
|
|
402
407
|
}
|
|
403
|
-
this.
|
|
408
|
+
this.transport.send(this.codec.encodeEnvelope(envelope));
|
|
404
409
|
}
|
|
405
410
|
return { messageId };
|
|
406
411
|
}
|
|
407
412
|
async ack(messageId) {
|
|
408
|
-
if (!this.
|
|
413
|
+
if (!this.transport?.isOpen()) {
|
|
409
414
|
return;
|
|
410
415
|
}
|
|
411
|
-
this.
|
|
416
|
+
this.transport.send(this.codec.encodeAck({ messageId, type: 'PROCESSED' }));
|
|
412
417
|
}
|
|
413
418
|
onMessage(callback) {
|
|
414
419
|
this.messageListeners.add(callback);
|
|
@@ -439,7 +444,7 @@ class MessagingClient {
|
|
|
439
444
|
// Message edit and delete (PROTOCOL_VERSION 3+)
|
|
440
445
|
// ---------------------------------------------------------------------------
|
|
441
446
|
async editMessage(originalMessageId, toUserId, newPlaintext) {
|
|
442
|
-
if (!this.
|
|
447
|
+
if (!this.transport?.isOpen()) {
|
|
443
448
|
throw new Error('DropOnAir websocket is not connected');
|
|
444
449
|
}
|
|
445
450
|
if (!this.currentUserId) {
|
|
@@ -496,11 +501,11 @@ class MessagingClient {
|
|
|
496
501
|
senderDeviceId: myDeviceId,
|
|
497
502
|
clientEditId: editId,
|
|
498
503
|
};
|
|
499
|
-
this.
|
|
504
|
+
this.transport.send(this.codec.encodeMessageEditFrame(frame));
|
|
500
505
|
return { editId };
|
|
501
506
|
}
|
|
502
507
|
async editCleartextMessage(originalMessageId, toUserId, newPlaintext) {
|
|
503
|
-
if (!this.
|
|
508
|
+
if (!this.transport?.isOpen()) {
|
|
504
509
|
throw new Error('DropOnAir websocket is not connected');
|
|
505
510
|
}
|
|
506
511
|
if (!this.currentUserId) {
|
|
@@ -522,11 +527,11 @@ class MessagingClient {
|
|
|
522
527
|
plaintextPayload: newPlaintext,
|
|
523
528
|
clientEditId: editId,
|
|
524
529
|
};
|
|
525
|
-
this.
|
|
530
|
+
this.transport.send(this.codec.encodeMessageEditFrame(frame));
|
|
526
531
|
return { editId };
|
|
527
532
|
}
|
|
528
533
|
async deleteMessage(originalMessageId, toUserId, scope) {
|
|
529
|
-
if (!this.
|
|
534
|
+
if (!this.transport?.isOpen()) {
|
|
530
535
|
throw new Error('DropOnAir websocket is not connected');
|
|
531
536
|
}
|
|
532
537
|
if (!this.currentUserId) {
|
|
@@ -547,7 +552,7 @@ class MessagingClient {
|
|
|
547
552
|
scope,
|
|
548
553
|
clientDeleteId: deleteId,
|
|
549
554
|
};
|
|
550
|
-
this.
|
|
555
|
+
this.transport.send(this.codec.encodeMessageDeleteFrame(frame));
|
|
551
556
|
return { deleteId };
|
|
552
557
|
}
|
|
553
558
|
// ---------------------------------------------------------------------------
|
|
@@ -567,7 +572,7 @@ class MessagingClient {
|
|
|
567
572
|
* stored token and refreshes its lastSeenAt timestamp.
|
|
568
573
|
*/
|
|
569
574
|
async registerPushToken(opts) {
|
|
570
|
-
if (!this.
|
|
575
|
+
if (!this.transport?.isOpen()) {
|
|
571
576
|
throw new Error('DropOnAir websocket is not connected');
|
|
572
577
|
}
|
|
573
578
|
if (!opts.token || opts.token.trim().length === 0) {
|
|
@@ -581,14 +586,14 @@ class MessagingClient {
|
|
|
581
586
|
voipToken: opts.voipToken ?? '',
|
|
582
587
|
deviceId,
|
|
583
588
|
};
|
|
584
|
-
this.
|
|
589
|
+
this.transport.send(this.codec.encodePushRegistrationFrame(frame));
|
|
585
590
|
}
|
|
586
591
|
/**
|
|
587
592
|
* Unregister this device's push notification token (e.g. on logout). Future
|
|
588
593
|
* push fan-out for this (appId, userId, deviceId, platform) tuple is dropped.
|
|
589
594
|
*/
|
|
590
595
|
async unregisterPushToken(opts) {
|
|
591
|
-
if (!this.
|
|
596
|
+
if (!this.transport?.isOpen()) {
|
|
592
597
|
throw new Error('DropOnAir websocket is not connected');
|
|
593
598
|
}
|
|
594
599
|
const deviceId = this.deviceId ?? await this.getOrCreateDeviceId();
|
|
@@ -599,7 +604,7 @@ class MessagingClient {
|
|
|
599
604
|
voipToken: '',
|
|
600
605
|
deviceId,
|
|
601
606
|
};
|
|
602
|
-
this.
|
|
607
|
+
this.transport.send(this.codec.encodePushRegistrationFrame(frame));
|
|
603
608
|
}
|
|
604
609
|
// ---------------------------------------------------------------------------
|
|
605
610
|
// Device trust (PROTOCOL_VERSION 5+ / Phase 2b)
|
|
@@ -650,8 +655,8 @@ class MessagingClient {
|
|
|
650
655
|
clearTimeout(this.reconnectTimer);
|
|
651
656
|
this.reconnectTimer = null;
|
|
652
657
|
}
|
|
653
|
-
if (this.
|
|
654
|
-
this.
|
|
658
|
+
if (this.transport?.isOpen()) {
|
|
659
|
+
this.transport.close(4001, 'DEVICE_REVOKED');
|
|
655
660
|
}
|
|
656
661
|
}
|
|
657
662
|
// ---------------------------------------------------------------------------
|
|
@@ -671,7 +676,7 @@ class MessagingClient {
|
|
|
671
676
|
* other devices so they can bucket the receipt
|
|
672
677
|
*/
|
|
673
678
|
markRead(messageId, conversationId) {
|
|
674
|
-
if (!this.
|
|
679
|
+
if (!this.transport?.isOpen()) {
|
|
675
680
|
throw new Error('DropOnAir websocket is not connected');
|
|
676
681
|
}
|
|
677
682
|
const frame = {
|
|
@@ -680,7 +685,7 @@ class MessagingClient {
|
|
|
680
685
|
conversationId: conversationId ?? '',
|
|
681
686
|
timestamp: Date.now(),
|
|
682
687
|
};
|
|
683
|
-
this.
|
|
688
|
+
this.transport.send(this.codec.encodeSyncFrame(frame));
|
|
684
689
|
}
|
|
685
690
|
/**
|
|
686
691
|
* Tell this user's other devices that the notification(s) for a
|
|
@@ -689,7 +694,7 @@ class MessagingClient {
|
|
|
689
694
|
* the matching badge. The relay never decides what "dismissed" means.
|
|
690
695
|
*/
|
|
691
696
|
clearNotification(conversationId) {
|
|
692
|
-
if (!this.
|
|
697
|
+
if (!this.transport?.isOpen()) {
|
|
693
698
|
throw new Error('DropOnAir websocket is not connected');
|
|
694
699
|
}
|
|
695
700
|
const frame = {
|
|
@@ -698,7 +703,7 @@ class MessagingClient {
|
|
|
698
703
|
conversationId,
|
|
699
704
|
timestamp: Date.now(),
|
|
700
705
|
};
|
|
701
|
-
this.
|
|
706
|
+
this.transport.send(this.codec.encodeSyncFrame(frame));
|
|
702
707
|
}
|
|
703
708
|
/**
|
|
704
709
|
* Push the current draft text for a conversation to this user's other
|
|
@@ -708,7 +713,7 @@ class MessagingClient {
|
|
|
708
713
|
* feature is disabled the server silently drops the frame.
|
|
709
714
|
*/
|
|
710
715
|
syncDraft(conversationId, draftText) {
|
|
711
|
-
if (!this.
|
|
716
|
+
if (!this.transport?.isOpen()) {
|
|
712
717
|
throw new Error('DropOnAir websocket is not connected');
|
|
713
718
|
}
|
|
714
719
|
const frame = {
|
|
@@ -718,7 +723,7 @@ class MessagingClient {
|
|
|
718
723
|
timestamp: Date.now(),
|
|
719
724
|
payload: draftText,
|
|
720
725
|
};
|
|
721
|
-
this.
|
|
726
|
+
this.transport.send(this.codec.encodeSyncFrame(frame));
|
|
722
727
|
}
|
|
723
728
|
/** Register a listener for notification-clear syncs from the user's other devices. */
|
|
724
729
|
onNotificationCleared(callback) {
|
|
@@ -775,7 +780,7 @@ class MessagingClient {
|
|
|
775
780
|
// Cleartext messaging (no E2EE key exchange required)
|
|
776
781
|
// ---------------------------------------------------------------------------
|
|
777
782
|
async sendCleartextMessage(toUserId, plaintext) {
|
|
778
|
-
if (!this.
|
|
783
|
+
if (!this.transport?.isOpen()) {
|
|
779
784
|
throw new Error('DropOnAir websocket is not connected');
|
|
780
785
|
}
|
|
781
786
|
if (!this.currentUserId) {
|
|
@@ -799,7 +804,7 @@ class MessagingClient {
|
|
|
799
804
|
encryptionType: 1, // CLEARTEXT
|
|
800
805
|
plaintextPayload: plaintext,
|
|
801
806
|
};
|
|
802
|
-
this.
|
|
807
|
+
this.transport.send(this.codec.encodeEnvelope(envelope));
|
|
803
808
|
return { messageId };
|
|
804
809
|
}
|
|
805
810
|
// ---------------------------------------------------------------------------
|
|
@@ -826,7 +831,7 @@ class MessagingClient {
|
|
|
826
831
|
}
|
|
827
832
|
}
|
|
828
833
|
async publishBroadcast(channelId, plaintext) {
|
|
829
|
-
if (!this.
|
|
834
|
+
if (!this.transport?.isOpen()) {
|
|
830
835
|
throw new Error('DropOnAir websocket is not connected');
|
|
831
836
|
}
|
|
832
837
|
if (!this.currentUserId) {
|
|
@@ -847,7 +852,7 @@ class MessagingClient {
|
|
|
847
852
|
plaintextPayload: plaintext,
|
|
848
853
|
sequenceNumber: 0,
|
|
849
854
|
};
|
|
850
|
-
this.
|
|
855
|
+
this.transport.send(this.codec.encodeBroadcastFrame(frame));
|
|
851
856
|
return { broadcastId };
|
|
852
857
|
}
|
|
853
858
|
onBroadcast(callback) {
|
|
@@ -1060,12 +1065,12 @@ class MessagingClient {
|
|
|
1060
1065
|
* call joinRoom again).
|
|
1061
1066
|
*/
|
|
1062
1067
|
joinRoom(roomId) {
|
|
1063
|
-
if (!this.
|
|
1068
|
+
if (!this.transport?.isOpen()) {
|
|
1064
1069
|
return Promise.reject(new Error('DropOnAir websocket is not connected'));
|
|
1065
1070
|
}
|
|
1066
1071
|
return new Promise((resolve, reject) => {
|
|
1067
1072
|
this.pendingRoomJoins.set(roomId, { resolve, reject });
|
|
1068
|
-
this.
|
|
1073
|
+
this.transport.send(this.codec.encodeGroupCallFrame({
|
|
1069
1074
|
type: 'GROUP_CALL_JOIN',
|
|
1070
1075
|
callId: '',
|
|
1071
1076
|
groupId: '',
|
|
@@ -1092,7 +1097,7 @@ class MessagingClient {
|
|
|
1092
1097
|
* {@link sendCleartextGroupMessage} instead.
|
|
1093
1098
|
*/
|
|
1094
1099
|
async sendGroupMessage(groupId, plaintext, memberUserIds, options) {
|
|
1095
|
-
if (!this.
|
|
1100
|
+
if (!this.transport?.isOpen()) {
|
|
1096
1101
|
throw new Error('DropOnAir websocket is not connected');
|
|
1097
1102
|
}
|
|
1098
1103
|
if (!this.currentUserId) {
|
|
@@ -1170,7 +1175,7 @@ class MessagingClient {
|
|
|
1170
1175
|
if (options?.attachments && options.attachments.length > 0) {
|
|
1171
1176
|
frame.attachments = options.attachments.map(a => this.attachmentClient.toWire(a));
|
|
1172
1177
|
}
|
|
1173
|
-
this.
|
|
1178
|
+
this.transport.send(this.codec.encodeGroupEnvelope(frame));
|
|
1174
1179
|
return { messageId };
|
|
1175
1180
|
}
|
|
1176
1181
|
/**
|
|
@@ -1182,7 +1187,7 @@ class MessagingClient {
|
|
|
1182
1187
|
* per-recipient before sending.
|
|
1183
1188
|
*/
|
|
1184
1189
|
async sendCleartextGroupMessage(groupId, plaintext) {
|
|
1185
|
-
if (!this.
|
|
1190
|
+
if (!this.transport?.isOpen()) {
|
|
1186
1191
|
throw new Error('DropOnAir websocket is not connected');
|
|
1187
1192
|
}
|
|
1188
1193
|
if (!this.currentUserId) {
|
|
@@ -1209,7 +1214,7 @@ class MessagingClient {
|
|
|
1209
1214
|
plaintextPayload: plaintext,
|
|
1210
1215
|
memberPayloads: [],
|
|
1211
1216
|
};
|
|
1212
|
-
this.
|
|
1217
|
+
this.transport.send(this.codec.encodeGroupEnvelope(frame));
|
|
1213
1218
|
return { messageId };
|
|
1214
1219
|
}
|
|
1215
1220
|
onGroupMessage(callback) {
|
|
@@ -1220,14 +1225,14 @@ class MessagingClient {
|
|
|
1220
1225
|
// Group call signaling
|
|
1221
1226
|
// ---------------------------------------------------------------------------
|
|
1222
1227
|
startGroupCall(groupId) {
|
|
1223
|
-
if (!this.
|
|
1228
|
+
if (!this.transport?.isOpen()) {
|
|
1224
1229
|
return Promise.reject(new Error('DropOnAir websocket is not connected'));
|
|
1225
1230
|
}
|
|
1226
1231
|
return new Promise((resolve, reject) => {
|
|
1227
1232
|
this.pendingGroupInviteResolve = resolve;
|
|
1228
1233
|
this.pendingGroupInviteReject = reject;
|
|
1229
1234
|
const frame = { type: 'GROUP_CALL_INVITE', callId: '', groupId };
|
|
1230
|
-
this.
|
|
1235
|
+
this.transport.send(this.codec.encodeGroupCallFrame(frame));
|
|
1231
1236
|
});
|
|
1232
1237
|
}
|
|
1233
1238
|
async joinGroupCall(callId, groupId) {
|
|
@@ -1382,10 +1387,10 @@ class MessagingClient {
|
|
|
1382
1387
|
this.sendGroupCallFrame({ type: 'GROUP_CALL_RECORDING_AVAILABLE', callId, groupId: '', payload: location });
|
|
1383
1388
|
}
|
|
1384
1389
|
sendGroupCallFrame(frame) {
|
|
1385
|
-
if (!this.
|
|
1390
|
+
if (!this.transport?.isOpen()) {
|
|
1386
1391
|
throw new Error('DropOnAir websocket is not connected');
|
|
1387
1392
|
}
|
|
1388
|
-
this.
|
|
1393
|
+
this.transport.send(this.codec.encodeGroupCallFrame(frame));
|
|
1389
1394
|
}
|
|
1390
1395
|
/**
|
|
1391
1396
|
* Initiate an outgoing call.
|
|
@@ -1393,14 +1398,14 @@ class MessagingClient {
|
|
|
1393
1398
|
* once the server echoes back CALL_RINGING (which contains the real callId).
|
|
1394
1399
|
*/
|
|
1395
1400
|
startCall(targetUserId) {
|
|
1396
|
-
if (!this.
|
|
1401
|
+
if (!this.transport?.isOpen()) {
|
|
1397
1402
|
return Promise.reject(new Error('DropOnAir websocket is not connected'));
|
|
1398
1403
|
}
|
|
1399
1404
|
return new Promise((resolve, reject) => {
|
|
1400
1405
|
this.pendingInviteResolve = resolve;
|
|
1401
1406
|
this.pendingInviteReject = reject;
|
|
1402
1407
|
const frame = { type: 'CALL_INVITE', targetUserId };
|
|
1403
|
-
this.
|
|
1408
|
+
this.transport.send(this.codec.encodeCallFrame(frame));
|
|
1404
1409
|
});
|
|
1405
1410
|
}
|
|
1406
1411
|
async acceptCall(callId) {
|
|
@@ -1437,10 +1442,10 @@ class MessagingClient {
|
|
|
1437
1442
|
return response.json();
|
|
1438
1443
|
}
|
|
1439
1444
|
sendCallFrame(frame) {
|
|
1440
|
-
if (!this.
|
|
1445
|
+
if (!this.transport?.isOpen()) {
|
|
1441
1446
|
throw new Error('DropOnAir websocket is not connected');
|
|
1442
1447
|
}
|
|
1443
|
-
this.
|
|
1448
|
+
this.transport.send(this.codec.encodeCallFrame(frame));
|
|
1444
1449
|
}
|
|
1445
1450
|
emitCallEvent(wire) {
|
|
1446
1451
|
// Resolve a pending startCall() promise when CALL_RINGING arrives.
|
|
@@ -1598,6 +1603,44 @@ class MessagingClient {
|
|
|
1598
1603
|
listener(event);
|
|
1599
1604
|
}
|
|
1600
1605
|
}
|
|
1606
|
+
async resolveTransport(jwt) {
|
|
1607
|
+
const selection = this.options.transport ?? 'ws';
|
|
1608
|
+
let chosen = 'ws';
|
|
1609
|
+
if (selection === 'auto') {
|
|
1610
|
+
try {
|
|
1611
|
+
const lane = await (0, auto_select_1.selectTransport)({ httpUrl: this.httpUrl, fetchFn: this.fetchFn });
|
|
1612
|
+
chosen = lane === 'webtransport' ? 'wt' : lane === 'sse' ? 'sse' : 'ws';
|
|
1613
|
+
}
|
|
1614
|
+
catch (err) {
|
|
1615
|
+
this.logError('transport_auto_select_failed', { error: String(err?.message ?? err) });
|
|
1616
|
+
chosen = 'ws';
|
|
1617
|
+
}
|
|
1618
|
+
}
|
|
1619
|
+
else {
|
|
1620
|
+
chosen = selection;
|
|
1621
|
+
}
|
|
1622
|
+
this.resolvedTransportName = chosen;
|
|
1623
|
+
if (chosen === 'sse') {
|
|
1624
|
+
return new sse_messaging_transport_1.SseMessagingTransport({
|
|
1625
|
+
httpUrl: this.httpUrl,
|
|
1626
|
+
getJwt: async () => jwt,
|
|
1627
|
+
fetchFn: this.fetchFn,
|
|
1628
|
+
});
|
|
1629
|
+
}
|
|
1630
|
+
if (chosen === 'wt') {
|
|
1631
|
+
return new wt_messaging_transport_1.WtMessagingTransport({
|
|
1632
|
+
httpUrl: this.httpUrl,
|
|
1633
|
+
getJwt: async () => jwt,
|
|
1634
|
+
});
|
|
1635
|
+
}
|
|
1636
|
+
const myDeviceId = this.deviceId;
|
|
1637
|
+
let wsUrlWithParams = `${this.wsUrl}?token=${encodeURIComponent(jwt)}`;
|
|
1638
|
+
if (myDeviceId)
|
|
1639
|
+
wsUrlWithParams += `&deviceId=${encodeURIComponent(myDeviceId)}`;
|
|
1640
|
+
wsUrlWithParams += `&sdkVersion=${encodeURIComponent(version_1.SDK_VERSION)}`;
|
|
1641
|
+
wsUrlWithParams += `&protocolVersion=${version_1.PROTOCOL_VERSION}`;
|
|
1642
|
+
return new ws_messaging_transport_1.WebSocketMessagingTransport(wsUrlWithParams);
|
|
1643
|
+
}
|
|
1601
1644
|
async connectWebSocket() {
|
|
1602
1645
|
// Only exchange a fresh token when the cached one is absent or expired/near-expiry.
|
|
1603
1646
|
// forceRefresh=true on every reconnect caused a token-exchange call every 2 s.
|
|
@@ -1607,28 +1650,21 @@ class MessagingClient {
|
|
|
1607
1650
|
this.logError('ws_connect_jwt_expired', this.jwtSummary(this.dropOnAirJwt));
|
|
1608
1651
|
throw new Error('DropOnAir JWT expired before websocket connect');
|
|
1609
1652
|
}
|
|
1653
|
+
const transport = await this.resolveTransport(this.dropOnAirJwt);
|
|
1610
1654
|
this.log('ws_connect_start', {
|
|
1655
|
+
transport: this.resolvedTransportName,
|
|
1611
1656
|
wsUrl: this.wsUrl,
|
|
1612
1657
|
currentUserId: this.currentUserId,
|
|
1613
1658
|
...this.jwtSummary(this.dropOnAirJwt),
|
|
1614
1659
|
});
|
|
1615
1660
|
await new Promise((resolve, reject) => {
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
}
|
|
1621
|
-
wsUrlWithParams += `&sdkVersion=${encodeURIComponent(version_1.SDK_VERSION)}`;
|
|
1622
|
-
wsUrlWithParams += `&protocolVersion=${version_1.PROTOCOL_VERSION}`;
|
|
1623
|
-
const ws = new WebSocket(wsUrlWithParams);
|
|
1624
|
-
ws.binaryType = 'arraybuffer';
|
|
1625
|
-
ws.onopen = () => {
|
|
1626
|
-
this.ws = ws;
|
|
1661
|
+
let opened = false;
|
|
1662
|
+
transport.onOpen(() => {
|
|
1663
|
+
this.transport = transport;
|
|
1664
|
+
opened = true;
|
|
1627
1665
|
this.rateLimited = false;
|
|
1628
|
-
this.log('ws_connected', { wsUrl: this.wsUrl });
|
|
1666
|
+
this.log('ws_connected', { transport: this.resolvedTransportName, wsUrl: this.wsUrl });
|
|
1629
1667
|
this.emitEvent({ type: 'CONNECTED' });
|
|
1630
|
-
// Schedule proactive token rotation before this JWT expires so we never
|
|
1631
|
-
// send a message on a token the server will immediately reject.
|
|
1632
1668
|
if (this.dropOnAirJwt) {
|
|
1633
1669
|
this.scheduleProactiveTokenRefresh(this.dropOnAirJwt);
|
|
1634
1670
|
}
|
|
@@ -1637,24 +1673,22 @@ class MessagingClient {
|
|
|
1637
1673
|
this.emitEvent({ type: 'ERROR', reason: 'OFFLINE_FETCH_FAILED' });
|
|
1638
1674
|
});
|
|
1639
1675
|
resolve();
|
|
1640
|
-
};
|
|
1641
|
-
|
|
1642
|
-
this.logError('ws_error', { wsUrl: this.wsUrl,
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1676
|
+
});
|
|
1677
|
+
transport.onError((err) => {
|
|
1678
|
+
this.logError('ws_error', { wsUrl: this.wsUrl, error: err.message });
|
|
1679
|
+
if (!opened) {
|
|
1680
|
+
reject(new Error('Failed to connect DropOnAir websocket'));
|
|
1681
|
+
}
|
|
1682
|
+
});
|
|
1683
|
+
transport.onClose((info) => {
|
|
1684
|
+
this.transport = null;
|
|
1647
1685
|
this.handlingJwtExpiry = false;
|
|
1648
|
-
// Cancel any pending proactive refresh, we are already disconnecting.
|
|
1649
1686
|
if (this.proactiveRefreshTimer) {
|
|
1650
1687
|
clearTimeout(this.proactiveRefreshTimer);
|
|
1651
1688
|
this.proactiveRefreshTimer = null;
|
|
1652
1689
|
}
|
|
1653
|
-
//
|
|
1654
|
-
|
|
1655
|
-
// was sent just before the disconnect (and is therefore stuck in PENDING)
|
|
1656
|
-
// gets marked FAILED and shown with a retry button in the UI.
|
|
1657
|
-
if (event.code === 1008) {
|
|
1690
|
+
// 1008 = server-side JWT expiry; surface so PENDING sends can FAIL.
|
|
1691
|
+
if (info.code === 1008) {
|
|
1658
1692
|
this.emitEvent({ type: 'ERROR', reason: 'JWT_EXPIRED' });
|
|
1659
1693
|
}
|
|
1660
1694
|
this.emitEvent({ type: 'DISCONNECTED' });
|
|
@@ -1664,19 +1698,15 @@ class MessagingClient {
|
|
|
1664
1698
|
this.emitEvent({ type: 'RECONNECTING' });
|
|
1665
1699
|
this.reconnectTimer = setTimeout(() => {
|
|
1666
1700
|
this.connectWebSocket().then(() => {
|
|
1667
|
-
// Successful reconnect, reset backoff counter.
|
|
1668
1701
|
this.reconnectAttempt = 0;
|
|
1669
1702
|
}).catch(() => {
|
|
1670
1703
|
this.emitEvent({ type: 'ERROR', reason: 'RECONNECT_FAILED' });
|
|
1671
1704
|
});
|
|
1672
1705
|
}, delay);
|
|
1673
1706
|
}
|
|
1674
|
-
};
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
return;
|
|
1678
|
-
}
|
|
1679
|
-
const frame = this.codec.decodeFrame(new Uint8Array(event.data));
|
|
1707
|
+
});
|
|
1708
|
+
transport.onFrame(async (bytes) => {
|
|
1709
|
+
const frame = this.codec.decodeFrame(bytes);
|
|
1680
1710
|
if (frame.kind === 'event') {
|
|
1681
1711
|
if (frame.data.type === 'LIMIT_REACHED') {
|
|
1682
1712
|
this.rateLimited = true;
|
|
@@ -1693,10 +1723,10 @@ class MessagingClient {
|
|
|
1693
1723
|
this.handlingJwtExpiry = true;
|
|
1694
1724
|
this.log('ws_jwt_expired_received', {
|
|
1695
1725
|
metadata: frame.data.metadata,
|
|
1696
|
-
wsState: this.
|
|
1726
|
+
wsState: (this.transport?.isOpen() ? 'OPEN' : 'CLOSED'),
|
|
1697
1727
|
});
|
|
1698
|
-
if (this.
|
|
1699
|
-
this.
|
|
1728
|
+
if (this.transport?.isOpen()) {
|
|
1729
|
+
this.transport.close(4001, 'JWT_EXPIRED');
|
|
1700
1730
|
}
|
|
1701
1731
|
}
|
|
1702
1732
|
}
|
|
@@ -1752,7 +1782,13 @@ class MessagingClient {
|
|
|
1752
1782
|
if (frame.kind === 'envelope') {
|
|
1753
1783
|
await this.handleIncomingEnvelope(frame.data);
|
|
1754
1784
|
}
|
|
1755
|
-
};
|
|
1785
|
+
});
|
|
1786
|
+
transport.connect().catch((err) => {
|
|
1787
|
+
if (!opened) {
|
|
1788
|
+
this.logError('ws_error', { wsUrl: this.wsUrl, error: String(err?.message ?? err) });
|
|
1789
|
+
reject(err instanceof Error ? err : new Error(String(err)));
|
|
1790
|
+
}
|
|
1791
|
+
});
|
|
1756
1792
|
});
|
|
1757
1793
|
}
|
|
1758
1794
|
async handleIncomingMessageEdit(frame) {
|
package/dist/core/types.d.ts
CHANGED
|
@@ -276,6 +276,14 @@ export interface InitializeOptions {
|
|
|
276
276
|
* Set false to let app decide when a message is actually seen/read and call `ack(messageId)` manually.
|
|
277
277
|
*/
|
|
278
278
|
autoAckIncomingMessages?: boolean;
|
|
279
|
+
/**
|
|
280
|
+
* Signaling transport lane. WebSocket (`'ws'`) is the default and works on
|
|
281
|
+
* every network. `'sse'` uses HTTP fallback for restrictive corporate
|
|
282
|
+
* networks. `'wt'` uses WebTransport (HTTP/3) where browsers support it.
|
|
283
|
+
* `'auto'` queries `GET /api/info.transports` and picks the best available
|
|
284
|
+
* lane from the runtime. The platform never enforces a choice.
|
|
285
|
+
*/
|
|
286
|
+
transport?: 'ws' | 'sse' | 'wt' | 'auto';
|
|
279
287
|
}
|
|
280
288
|
/** A registered device as returned by {@link DropOnAirClient.listMyDevices}. */
|
|
281
289
|
export interface DeviceInfo {
|
package/dist/index.d.ts
CHANGED
|
@@ -4,6 +4,8 @@ export declare function initialize(options: InitializeOptions): Promise<DropOnAi
|
|
|
4
4
|
export type { InitializeOptions, DropOnAirClient, DropOnAirEvent, MessageCallback, EventCallback, DecryptedMessage, KeyStorageAdapter, CallEvent, CallEventType, CallEventCallback, TurnCredentials, BroadcastMessage, BroadcastCallback, GroupInfo, GroupMemberInfo, DecryptedGroupMessage, GroupMessageCallback, GroupCallEvent, GroupCallEventType, GroupCallEventCallback, Room, RoomPolicy, CreateRoomOptions, UpdateRoomOptions, SfuToken, SfuRecording, MessageEditEvent, MessageEditCallback, MessageDeleteEvent, MessageDeleteCallback, DeviceInfo, ReadReceiptEvent, ReadReceiptCallback, NotificationClearEvent, NotificationClearCallback, DraftSyncEvent, DraftSyncCallback, } from './core/types';
|
|
5
5
|
export type { AttachmentEncryptionType, AttachmentConversationType, AttachmentRef, DeviceWrappedKey, CreateUploadSessionOptions, UploadSession, PrepareAttachmentOptions, DownloadedAttachment, } from './attachment/attachment-types';
|
|
6
6
|
export { SseTransport, type SseTransportOptions, type SseFrameHandler, type SseStateHandler, } from './transport/sse-transport';
|
|
7
|
+
export { WebTransportTransport, type WebTransportTransportOptions, type WTFrameHandler, type WTStateHandler, } from './transport/webtransport-transport';
|
|
8
|
+
export { selectTransport, type TransportLane, type SelectTransportOptions, } from './transport/auto-select';
|
|
7
9
|
declare const _default: {
|
|
8
10
|
initialize: typeof initialize;
|
|
9
11
|
};
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.SseTransport = exports.PAYLOAD_FORMAT_VERSION = exports.PROTOCOL_VERSION = exports.SDK_VERSION = void 0;
|
|
3
|
+
exports.selectTransport = exports.WebTransportTransport = exports.SseTransport = exports.PAYLOAD_FORMAT_VERSION = exports.PROTOCOL_VERSION = exports.SDK_VERSION = void 0;
|
|
4
4
|
exports.initialize = initialize;
|
|
5
5
|
const messaging_client_1 = require("./core/messaging-client");
|
|
6
6
|
const session_manager_1 = require("./core/session-manager");
|
|
@@ -29,4 +29,10 @@ async function initialize(options) {
|
|
|
29
29
|
// Phase 4.2.1: HTTP fallback lane primitive for restrictive networks.
|
|
30
30
|
var sse_transport_1 = require("./transport/sse-transport");
|
|
31
31
|
Object.defineProperty(exports, "SseTransport", { enumerable: true, get: function () { return sse_transport_1.SseTransport; } });
|
|
32
|
+
// Phase 4.3: WebTransport (HTTP/3) lane primitive.
|
|
33
|
+
var webtransport_transport_1 = require("./transport/webtransport-transport");
|
|
34
|
+
Object.defineProperty(exports, "WebTransportTransport", { enumerable: true, get: function () { return webtransport_transport_1.WebTransportTransport; } });
|
|
35
|
+
// Phase 4.6: transport auto-select - intersects runtime + platform support.
|
|
36
|
+
var auto_select_1 = require("./transport/auto-select");
|
|
37
|
+
Object.defineProperty(exports, "selectTransport", { enumerable: true, get: function () { return auto_select_1.selectTransport; } });
|
|
32
38
|
exports.default = { initialize };
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transport auto-select (Phase 4.6).
|
|
3
|
+
*
|
|
4
|
+
* Tiny helper that probes which transport lanes the runtime AND the platform
|
|
5
|
+
* BOTH support, and returns the best one for this client/host pair.
|
|
6
|
+
*
|
|
7
|
+
* Preference order (best -> fallback):
|
|
8
|
+
* 1. webtransport - multi-stream, lower head-of-line blocking
|
|
9
|
+
* 2. websocket - universal default
|
|
10
|
+
* 3. sse - HTTP-only fallback for restrictive networks
|
|
11
|
+
*
|
|
12
|
+
* The function fetches `<httpUrl>/api/info` once, intersects its
|
|
13
|
+
* `transports` array with the runtime's capabilities, and returns the
|
|
14
|
+
* highest-preference match. Caller then instantiates the corresponding
|
|
15
|
+
* primitive (`WebTransportTransport` / WebSocket / `SseTransport`) and
|
|
16
|
+
* uses it as their lane.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```ts
|
|
20
|
+
* import { selectTransport, WebTransportTransport, SseTransport } from '@droponair/sdk-js';
|
|
21
|
+
* const lane = await selectTransport({ httpUrl: 'https://sdk.droponair.com' });
|
|
22
|
+
* switch (lane) {
|
|
23
|
+
* case 'webtransport': // construct WebTransportTransport break;
|
|
24
|
+
* case 'sse': // construct SseTransport break;
|
|
25
|
+
* default: // fall through to WebSocket
|
|
26
|
+
* }
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
export type TransportLane = 'webtransport' | 'websocket' | 'sse';
|
|
30
|
+
export interface SelectTransportOptions {
|
|
31
|
+
/** Base URL of the platform's REST API (no trailing slash). */
|
|
32
|
+
httpUrl: string;
|
|
33
|
+
/**
|
|
34
|
+
* Override the default WT -> WS -> SSE preference order. Useful for tests
|
|
35
|
+
* or when a specific lane is preferred (e.g. force `sse` first on a known
|
|
36
|
+
* firewalled network).
|
|
37
|
+
*/
|
|
38
|
+
preference?: TransportLane[];
|
|
39
|
+
/** Custom fetch impl, useful for Node polyfills + test doubles. */
|
|
40
|
+
fetchFn?: typeof fetch;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Returns the highest-preference transport lane available on BOTH the
|
|
44
|
+
* runtime and the platform. Throws if no lane is supported (extremely
|
|
45
|
+
* unusual; WebSocket is the universal floor).
|
|
46
|
+
*/
|
|
47
|
+
export declare function selectTransport(options: SelectTransportOptions): Promise<TransportLane>;
|