@droponair/sdk-js 0.21.0 → 0.22.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/CHANGELOG.md +28 -9
- package/README.md +19 -2
- 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/transport/messaging-transport.d.ts +33 -0
- package/dist/transport/messaging-transport.js +2 -0
- package/dist/transport/protobuf-codec.js +31 -21
- package/dist/transport/sse-messaging-transport.d.ts +28 -0
- package/dist/transport/sse-messaging-transport.js +59 -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
package/CHANGELOG.md
CHANGED
|
@@ -6,15 +6,36 @@ This project follows [Semantic Versioning](https://semver.org/).
|
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
9
|
+
## [0.22.1], 2026-05-31
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- **Message send/receive broken when attachments support is compiled in.** The protobuf reflection tree shared a single `AttachmentRef` type instance across `Envelope`, `GroupEnvelope`, and `GroupMessageNotification`. Because a protobufjs reflection object can have only one parent, adding it to a later message re-parented it and removed it from `Envelope`, so encoding any 1:1 message threw `no such Type or Enum 'AttachmentRef' in Type Envelope` before a frame was ever produced. Each message now builds its own `AttachmentRef` (and nested `DeviceWrappedKey` / `EncryptionType`) instance. Wire format is unchanged, this is purely a client-side construction fix. Also resolves the same latent failure on `GroupMessageNotification` decode.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## [0.22.0], 2026-05-25
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
|
|
21
|
+
- **MessagingClient transport selection.** `initialize({ transport: 'ws' | 'sse' | 'wt' | 'auto' })` lets your app choose the signaling lane at init time. Default stays `'ws'`, so existing integrations are byte-identical. `'auto'` queries `GET /api/info.transports` and picks the best available lane from what the runtime supports. WebSocket close-code semantics (1008 JWT-expiry surfacing, 4001 JWT-refresh-on-event) are preserved on the WebSocket path; SSE/WT synthesize close events.
|
|
22
|
+
- **MQTT (IoT) broadcast lane.** Broadcasts now mirror to MQTT topic `droponair/v1/broadcast/{appId}/{channelId}` on `mqtts://mqtt.droponair.com:8883`. For IoT subscribers that consume your broadcasts without our full SDK. Subscribe-only, per-app credentials, TLS only. Visible via `GET /api/info` (`transports` includes `"mqtt"`, `features` includes `"transport_mqtt"`). No SDK API change, the lane is consumed directly by MQTT clients.
|
|
23
|
+
|
|
24
|
+
### Changed
|
|
25
|
+
|
|
26
|
+
- `MessagingClient` now uses an internal `MessagingTransport` adapter rather than a direct `WebSocket` reference. WebSocket remains the default; no protocol change. Adapters: `WebSocketMessagingTransport` (default), `SseMessagingTransport`, `WtMessagingTransport`.
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
9
30
|
## [0.21.0], 2026-05-24
|
|
10
31
|
|
|
11
32
|
### Added
|
|
12
33
|
|
|
13
|
-
- **Transport auto-select helper
|
|
34
|
+
- **Transport auto-select helper.** New `selectTransport({ httpUrl })` reads `/api/info.transports`, intersects with the runtime's `WebTransport`/`WebSocket`/`EventSource` availability, and returns the best lane (preference: WebTransport -> WebSocket -> SSE; overridable via `preference`). Caller then constructs the corresponding primitive. Pluggable `fetchFn` for Node + tests.
|
|
14
35
|
|
|
15
36
|
### Notes
|
|
16
37
|
|
|
17
|
-
- Foundation for an `init({ transport: 'auto' })` shortcut once full `MessagingClient` integration lands
|
|
38
|
+
- Foundation for an `init({ transport: 'auto' })` shortcut once full `MessagingClient` integration lands in a follow-up release. Today it's a small standalone helper so existing customers can adopt the pattern without waiting.
|
|
18
39
|
|
|
19
40
|
---
|
|
20
41
|
|
|
@@ -22,12 +43,12 @@ This project follows [Semantic Versioning](https://semver.org/).
|
|
|
22
43
|
|
|
23
44
|
### Added
|
|
24
45
|
|
|
25
|
-
- **WebTransport (HTTP/3) transport primitive
|
|
46
|
+
- **WebTransport (HTTP/3) transport primitive.** New `WebTransportTransport` class uses the browser-native `WebTransport` API to ride the platform's `/v1/transport/wt` HTTP/3 endpoint,. Same shape as `SseTransport`: `onFrame`, `sendEnvelope`, `connect`, `close`. Caller brings own protobuf decoder. Browser-only for v1 (Chromium 97+, Firefox 125+, Safari TP-only); Node 22+ has experimental support behind flags.
|
|
26
47
|
|
|
27
48
|
### Notes
|
|
28
49
|
|
|
29
50
|
- WebTransport gives multi-stream perf wins over WebSocket on flaky cellular and similar networks. WebSocket on `/ws` remains the default; this primitive is opt-in for environments that have native WebTransport.
|
|
30
|
-
- The droponair.com endpoint only advertises `transports: ["webtransport"]` once the
|
|
51
|
+
- The droponair.com endpoint only advertises `transports: ["webtransport"]` once the HTTP/3 lane is enabled and `app.transport.webtransport-enabled` is flipped on the platform. Check `GET /api/info.transports` for the live list.
|
|
31
52
|
|
|
32
53
|
---
|
|
33
54
|
|
|
@@ -35,11 +56,11 @@ This project follows [Semantic Versioning](https://semver.org/).
|
|
|
35
56
|
|
|
36
57
|
### Added
|
|
37
58
|
|
|
38
|
-
- **SSE transport primitive
|
|
59
|
+
- **SSE transport primitive.** New `SseTransport` class exposes the platform's HTTP-fallback lane (`/v1/transport/stream` + `/v1/transport/send`) as a small standalone client, for environments where WebSocket upgrades are blocked by corporate firewalls but HTTPS GET/POST work. EventSource-based receive (auto-reconnects), `fetch` POST for send. Bring your own protobuf decoder (use the codec already exported from the SDK).
|
|
39
60
|
|
|
40
61
|
### Notes
|
|
41
62
|
|
|
42
|
-
- v1 is a primitive; full `init({ transport: 'sse' })` integration into `MessagingClient` lands in
|
|
63
|
+
- v1 is a primitive; full `init({ transport: 'sse' })` integration into `MessagingClient` lands in a follow-up release. Customers who need the SSE lane today can use `SseTransport` directly alongside the existing WS client.
|
|
43
64
|
- WebSocket on `/ws` remains the default for all SDK calls; this addition is purely opt-in. Older code paths and existing apps are unaffected.
|
|
44
65
|
|
|
45
66
|
---
|
|
@@ -48,7 +69,7 @@ This project follows [Semantic Versioning](https://semver.org/).
|
|
|
48
69
|
|
|
49
70
|
### Added
|
|
50
71
|
|
|
51
|
-
- **SFU recording
|
|
72
|
+
- **SFU recording.** Server-side recording for SFU-mode rooms with `mediaEncryption: 'SFU'`. The platform's media server records the composite of every track in the room and uploads the finalized file directly to the customer's storage bucket - the platform never holds the recorded bytes. Storage destinations (S3 / GCS / Azure) are registered once in the panel; the SDK references each by an opaque `destinationId`.
|
|
52
73
|
- Three new client methods:
|
|
53
74
|
- `startSfuRecording(roomId, destinationId)` -> `SfuRecording`
|
|
54
75
|
- `stopSfuRecording(roomId, recordingId)` -> `SfuRecording`
|
|
@@ -313,7 +334,6 @@ This project follows [Semantic Versioning](https://semver.org/).
|
|
|
313
334
|
- Wire-level additive: legacy 0.4.x clients ignore the new `Envelope.attachments` field (proto3 forwards-compat). Existing apps continue to work unchanged.
|
|
314
335
|
- v1 storage adapters supported by the server: S3-compatible (S3, R2, MinIO, B2, Wasabi), Google Cloud Storage, Azure Blob.
|
|
315
336
|
- Plan limits: `maxAttachmentSizeMb` per file + `maxAttachmentsPerMonth`. FREE = attachments disabled. PRO = 25 MB / 1000 per month. GROWTH = 100 MB / 10000. PAYG = 250 MB / metered. ENTERPRISE = custom.
|
|
316
|
-
- Requires sdk-be `0.5.0` server.
|
|
317
337
|
|
|
318
338
|
---
|
|
319
339
|
|
|
@@ -329,7 +349,6 @@ This project follows [Semantic Versioning](https://semver.org/).
|
|
|
329
349
|
### Notes
|
|
330
350
|
- Each edit and each `FOR_EVERYONE` delete counts as **one** MESSAGE usage record (subject to plan quotas + rate limits).
|
|
331
351
|
- Wire-level additive: legacy 0.3.x clients ignore unknown frame types (proto3 forwards-compat), so existing TwinFlame and CrowdSyncer apps continue to work unchanged.
|
|
332
|
-
- Requires sdk-be `0.4.0` server.
|
|
333
352
|
|
|
334
353
|
---
|
|
335
354
|
|
package/README.md
CHANGED
|
@@ -183,7 +183,7 @@ E2EE invariant: the push body is sender-supplied cleartext metadata only (e.g. "
|
|
|
183
183
|
|
|
184
184
|
### Message Edit & Delete
|
|
185
185
|
|
|
186
|
-
Available since SDK `0.4.0
|
|
186
|
+
Available since SDK `0.4.0`.
|
|
187
187
|
|
|
188
188
|
```typescript
|
|
189
189
|
// Edit an encrypted message
|
|
@@ -213,7 +213,7 @@ client.onMessageDelete((del) => {
|
|
|
213
213
|
|
|
214
214
|
### Attachments (Customer-Managed Storage)
|
|
215
215
|
|
|
216
|
-
Available since SDK `0.5.0
|
|
216
|
+
Available since SDK `0.5.0`. Configure your bucket once in the DropOnAir panel (S3-compatible, GCS, or Azure Blob). DropOnAir never holds file bytes - the SDK uploads/downloads directly against your bucket via short-lived presigned URLs.
|
|
217
217
|
|
|
218
218
|
```typescript
|
|
219
219
|
// Convenience: encrypt + upload + finalize in one call, then send with the message.
|
|
@@ -435,6 +435,23 @@ await wt.sendEnvelope(envelopeBytes);
|
|
|
435
435
|
|
|
436
436
|
Uses the browser-native `WebTransport` API; opens a single bidirectional stream and rides raw bytes both ways. Browser support: Chromium 97+ (stable), Firefox 125+ (stable), Safari Technology Preview only, Node 22+ behind experimental flags. The `droponair.com` endpoint advertises `transports: ["webtransport"]` only when the `droponair-webtransport` sidecar is live; check `GET /api/info.transports` and `features.transport_webtransport` to detect availability. Bring your own protobuf decoder. WebSocket remains the default; this primitive is purely opt-in.
|
|
437
437
|
|
|
438
|
+
### Transport auto-select
|
|
439
|
+
|
|
440
|
+
Available since SDK `0.21.0`. `selectTransport({ httpUrl })` queries `/api/info.transports`, intersects with what the runtime supports (`WebTransport` / `WebSocket` / `EventSource`), and returns the best lane for this client + platform pair. Default preference: WebTransport → WebSocket → SSE (overridable).
|
|
441
|
+
|
|
442
|
+
```typescript
|
|
443
|
+
import { selectTransport, WebTransportTransport, SseTransport } from '@droponair/sdk-js';
|
|
444
|
+
|
|
445
|
+
const lane = await selectTransport({ httpUrl: 'https://sdk.droponair.com' });
|
|
446
|
+
switch (lane) {
|
|
447
|
+
case 'webtransport': /* new WebTransportTransport(...) */ break;
|
|
448
|
+
case 'sse': /* new SseTransport(...) */ break;
|
|
449
|
+
default: /* fall through to your WebSocket client */
|
|
450
|
+
}
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
Foundation for an `init({ transport: 'auto' })` shortcut once full `MessagingClient` integration lands (a follow-up). Today it's a small standalone helper so existing customers can adopt the pattern without waiting.
|
|
454
|
+
|
|
438
455
|
### Call Recording
|
|
439
456
|
|
|
440
457
|
Available since SDK `0.16.0`. The SDK **signals** recording state on a group or room call; your app does the actual media capture (`MediaRecorder`) and uploads the file to your own storage — the platform never holds the media. The recording signal is broadcast to every participant (including anyone who joins later); that transparency is enforced server-side.
|
|
@@ -14,7 +14,8 @@ export declare class MessagingClient implements DropOnAirClient {
|
|
|
14
14
|
private readonly fetchFn;
|
|
15
15
|
private readonly codec;
|
|
16
16
|
private attachmentClient;
|
|
17
|
-
private
|
|
17
|
+
private transport;
|
|
18
|
+
private resolvedTransportName;
|
|
18
19
|
private shouldReconnect;
|
|
19
20
|
private reconnectTimer;
|
|
20
21
|
private dropOnAirJwt;
|
|
@@ -324,6 +325,7 @@ export declare class MessagingClient implements DropOnAirClient {
|
|
|
324
325
|
private emitBroadcast;
|
|
325
326
|
private handleIncomingGroupMessage;
|
|
326
327
|
private emitGroupCallEvent;
|
|
328
|
+
private resolveTransport;
|
|
327
329
|
private connectWebSocket;
|
|
328
330
|
private handleIncomingMessageEdit;
|
|
329
331
|
private handleIncomingMessageDelete;
|
|
@@ -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 {
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Internal transport interface used by `MessagingClient`. Abstracts the
|
|
3
|
+
* WebSocket, SSE-fallback, and WebTransport lanes behind a single
|
|
4
|
+
* lifecycle surface so the client can pick a lane at init time without
|
|
5
|
+
* the rest of the SDK caring which one is in use.
|
|
6
|
+
*
|
|
7
|
+
* Public consumers of the SDK continue to use the standalone primitives
|
|
8
|
+
* (`SseTransport`, `WebTransportTransport`); this interface is an internal
|
|
9
|
+
* adapter target only.
|
|
10
|
+
*/
|
|
11
|
+
export interface MessagingTransportClose {
|
|
12
|
+
code: number;
|
|
13
|
+
reason: string;
|
|
14
|
+
}
|
|
15
|
+
export interface MessagingTransportEvents {
|
|
16
|
+
onOpen(handler: () => void): void;
|
|
17
|
+
onFrame(handler: (bytes: Uint8Array) => void): void;
|
|
18
|
+
onClose(handler: (info: MessagingTransportClose) => void): void;
|
|
19
|
+
onError(handler: (err: Error) => void): void;
|
|
20
|
+
}
|
|
21
|
+
export interface MessagingTransport extends MessagingTransportEvents {
|
|
22
|
+
/** Human-readable transport name, mirrors the `/api/info.transports` token. */
|
|
23
|
+
readonly name: 'ws' | 'sse' | 'wt';
|
|
24
|
+
/** Open the underlying stream. Resolves once frames can be sent and received. */
|
|
25
|
+
connect(): Promise<void>;
|
|
26
|
+
/** Send a raw protobuf frame (no framing of its own). */
|
|
27
|
+
send(bytes: Uint8Array): void;
|
|
28
|
+
/** True if the transport can accept `send()` right now. */
|
|
29
|
+
isOpen(): boolean;
|
|
30
|
+
/** Close the stream. Idempotent. `code`/`reason` are best-effort hints. */
|
|
31
|
+
close(code?: number, reason?: string): void;
|
|
32
|
+
}
|
|
33
|
+
export type MessagingTransportSelection = 'ws' | 'sse' | 'wt' | 'auto';
|
|
@@ -42,29 +42,38 @@ const DeviceEncryptedPayloadType = new protobuf.Type('DeviceEncryptedPayload')
|
|
|
42
42
|
.add(new protobuf.Field('deviceId', 1, 'string'))
|
|
43
43
|
.add(new protobuf.Field('encryptedPayload', 2, 'bytes'))
|
|
44
44
|
.add(new protobuf.Field('senderPublicKey', 3, 'bytes'));
|
|
45
|
-
//
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
//
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
45
|
+
// Reference to an out-of-band attachment in customer-managed storage, plus its
|
|
46
|
+
// nested per-device wrapped file key (PROTOCOL_VERSION 4+).
|
|
47
|
+
//
|
|
48
|
+
// A protobufjs reflection object can have only ONE parent: calling `.add(obj)`
|
|
49
|
+
// on a second message re-parents `obj` and silently removes it from the first.
|
|
50
|
+
// Envelope, GroupEnvelope and GroupMessageNotification each nest AttachmentRef,
|
|
51
|
+
// so each needs its OWN instance, built fresh here. Sharing a single instance
|
|
52
|
+
// across them re-parented it onto the last adder and orphaned it from Envelope,
|
|
53
|
+
// breaking every 1:1 send with "no such Type or Enum 'AttachmentRef' in Type
|
|
54
|
+
// Envelope". The EncryptionType enum and DeviceWrappedKey type are nested inside
|
|
55
|
+
// each AttachmentRef and so are rebuilt per call for the same reason.
|
|
56
|
+
function buildAttachmentRefType() {
|
|
57
|
+
return new protobuf.Type('AttachmentRef')
|
|
58
|
+
.add(new protobuf.Enum('EncryptionType', { E2EE: 0, CLEARTEXT: 1 }))
|
|
59
|
+
.add(new protobuf.Type('DeviceWrappedKey')
|
|
60
|
+
.add(new protobuf.Field('deviceId', 1, 'string'))
|
|
61
|
+
.add(new protobuf.Field('wrappedKey', 2, 'bytes'))
|
|
62
|
+
.add(new protobuf.Field('senderPublicKey', 3, 'bytes'))
|
|
63
|
+
.add(new protobuf.Field('nonce', 4, 'bytes')))
|
|
64
|
+
.add(new protobuf.Field('attachmentId', 1, 'string'))
|
|
65
|
+
.add(new protobuf.Field('storageHint', 2, 'string'))
|
|
66
|
+
.add(new protobuf.Field('mimeType', 3, 'string'))
|
|
67
|
+
.add(new protobuf.Field('sizeBytes', 4, 'int64'))
|
|
68
|
+
.add(new protobuf.Field('sha256', 5, 'string'))
|
|
69
|
+
.add(new protobuf.Field('encryptionType', 6, 'EncryptionType'))
|
|
70
|
+
.add(new protobuf.Field('wrappedKeys', 7, 'DeviceWrappedKey', 'repeated'))
|
|
71
|
+
.add(new protobuf.Field('thumbnailAttachmentId', 8, 'string'));
|
|
72
|
+
}
|
|
64
73
|
const EnvelopeType = new protobuf.Type('Envelope')
|
|
65
74
|
.add(EncryptionTypeEnum)
|
|
66
75
|
.add(DeviceEncryptedPayloadType) // nested type must be added first
|
|
67
|
-
.add(
|
|
76
|
+
.add(buildAttachmentRefType())
|
|
68
77
|
.add(new protobuf.Field('messageId', 1, 'string'))
|
|
69
78
|
.add(new protobuf.Field('appId', 2, 'string'))
|
|
70
79
|
.add(new protobuf.Field('fromUserId', 3, 'string'))
|
|
@@ -128,7 +137,7 @@ const GroupEnvelopeEncryptionTypeEnum = new protobuf.Enum('EncryptionType', { E2
|
|
|
128
137
|
const GroupEnvelopeType = new protobuf.Type('GroupEnvelope')
|
|
129
138
|
.add(GroupEnvelopeEncryptionTypeEnum)
|
|
130
139
|
.add(GroupMemberPayloadType)
|
|
131
|
-
.add(
|
|
140
|
+
.add(buildAttachmentRefType())
|
|
132
141
|
.add(new protobuf.Field('messageId', 1, 'string'))
|
|
133
142
|
.add(new protobuf.Field('appId', 2, 'string'))
|
|
134
143
|
.add(new protobuf.Field('groupId', 3, 'string'))
|
|
@@ -149,6 +158,7 @@ const GroupNotifDeviceType = new protobuf.Type('DeviceEncryptedPayload')
|
|
|
149
158
|
const GroupMessageNotificationType = new protobuf.Type('GroupMessageNotification')
|
|
150
159
|
.add(GroupNotifEncryptionTypeEnum)
|
|
151
160
|
.add(GroupNotifDeviceType)
|
|
161
|
+
.add(buildAttachmentRefType())
|
|
152
162
|
.add(new protobuf.Field('messageId', 1, 'string'))
|
|
153
163
|
.add(new protobuf.Field('groupId', 2, 'string'))
|
|
154
164
|
.add(new protobuf.Field('fromUserId', 3, 'string'))
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { type SseTransportOptions } from './sse-transport';
|
|
2
|
+
import type { MessagingTransport, MessagingTransportClose } from './messaging-transport';
|
|
3
|
+
/**
|
|
4
|
+
* SSE adapter for {@link MessagingTransport}. Wraps the standalone
|
|
5
|
+
* {@link SseTransport} primitive so `MessagingClient` can use the HTTP
|
|
6
|
+
* fallback lane behind the same lifecycle API as WebSocket.
|
|
7
|
+
*
|
|
8
|
+
* v1 limitations: no auto-reconnect logic at the client layer beyond
|
|
9
|
+
* what EventSource already does; close events synthesize code 1000.
|
|
10
|
+
*/
|
|
11
|
+
export declare class SseMessagingTransport implements MessagingTransport {
|
|
12
|
+
readonly name: "sse";
|
|
13
|
+
private inner;
|
|
14
|
+
private opened;
|
|
15
|
+
private openHandler;
|
|
16
|
+
private frameHandler;
|
|
17
|
+
private closeHandler;
|
|
18
|
+
private errorHandler;
|
|
19
|
+
constructor(options: SseTransportOptions);
|
|
20
|
+
onOpen(handler: () => void): void;
|
|
21
|
+
onFrame(handler: (bytes: Uint8Array) => void): void;
|
|
22
|
+
onClose(handler: (info: MessagingTransportClose) => void): void;
|
|
23
|
+
onError(handler: (err: Error) => void): void;
|
|
24
|
+
connect(): Promise<void>;
|
|
25
|
+
send(bytes: Uint8Array): void;
|
|
26
|
+
isOpen(): boolean;
|
|
27
|
+
close(_code?: number, _reason?: string): void;
|
|
28
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SseMessagingTransport = void 0;
|
|
4
|
+
const sse_transport_1 = require("./sse-transport");
|
|
5
|
+
/**
|
|
6
|
+
* SSE adapter for {@link MessagingTransport}. Wraps the standalone
|
|
7
|
+
* {@link SseTransport} primitive so `MessagingClient` can use the HTTP
|
|
8
|
+
* fallback lane behind the same lifecycle API as WebSocket.
|
|
9
|
+
*
|
|
10
|
+
* v1 limitations: no auto-reconnect logic at the client layer beyond
|
|
11
|
+
* what EventSource already does; close events synthesize code 1000.
|
|
12
|
+
*/
|
|
13
|
+
class SseMessagingTransport {
|
|
14
|
+
constructor(options) {
|
|
15
|
+
this.name = 'sse';
|
|
16
|
+
this.opened = false;
|
|
17
|
+
this.openHandler = null;
|
|
18
|
+
this.frameHandler = null;
|
|
19
|
+
this.closeHandler = null;
|
|
20
|
+
this.errorHandler = null;
|
|
21
|
+
this.inner = new sse_transport_1.SseTransport(options);
|
|
22
|
+
this.inner.onFrame((bytes) => this.frameHandler?.(bytes));
|
|
23
|
+
this.inner.onState((state) => {
|
|
24
|
+
if (state === 'open') {
|
|
25
|
+
this.opened = true;
|
|
26
|
+
this.openHandler?.();
|
|
27
|
+
}
|
|
28
|
+
else if (state === 'closed') {
|
|
29
|
+
this.opened = false;
|
|
30
|
+
this.closeHandler?.({ code: 1000, reason: 'CLOSED' });
|
|
31
|
+
}
|
|
32
|
+
else if (state === 'error') {
|
|
33
|
+
this.errorHandler?.(new Error('SseMessagingTransport: stream error'));
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
onOpen(handler) { this.openHandler = handler; }
|
|
38
|
+
onFrame(handler) { this.frameHandler = handler; }
|
|
39
|
+
onClose(handler) { this.closeHandler = handler; }
|
|
40
|
+
onError(handler) { this.errorHandler = handler; }
|
|
41
|
+
async connect() {
|
|
42
|
+
await this.inner.connect();
|
|
43
|
+
}
|
|
44
|
+
send(bytes) {
|
|
45
|
+
// Fire-and-forget; surface failure via the error handler so the client's
|
|
46
|
+
// existing `this.transport.send(...)` call sites don't need to await.
|
|
47
|
+
this.inner.sendEnvelope(bytes).catch((err) => {
|
|
48
|
+
this.errorHandler?.(err instanceof Error ? err : new Error(String(err)));
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
isOpen() {
|
|
52
|
+
return this.opened;
|
|
53
|
+
}
|
|
54
|
+
close(_code, _reason) {
|
|
55
|
+
this.inner.close();
|
|
56
|
+
this.opened = false;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
exports.SseMessagingTransport = SseMessagingTransport;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { MessagingTransport, MessagingTransportClose } from './messaging-transport';
|
|
2
|
+
/**
|
|
3
|
+
* WebSocket adapter for {@link MessagingTransport}. Wraps the global
|
|
4
|
+
* WebSocket constructor; the client supplies the full URL (already
|
|
5
|
+
* carrying the JWT + deviceId query params).
|
|
6
|
+
*/
|
|
7
|
+
export declare class WebSocketMessagingTransport implements MessagingTransport {
|
|
8
|
+
private readonly url;
|
|
9
|
+
readonly name: "ws";
|
|
10
|
+
private ws;
|
|
11
|
+
private openHandler;
|
|
12
|
+
private frameHandler;
|
|
13
|
+
private closeHandler;
|
|
14
|
+
private errorHandler;
|
|
15
|
+
constructor(url: string);
|
|
16
|
+
onOpen(handler: () => void): void;
|
|
17
|
+
onFrame(handler: (bytes: Uint8Array) => void): void;
|
|
18
|
+
onClose(handler: (info: MessagingTransportClose) => void): void;
|
|
19
|
+
onError(handler: (err: Error) => void): void;
|
|
20
|
+
connect(): Promise<void>;
|
|
21
|
+
send(bytes: Uint8Array): void;
|
|
22
|
+
isOpen(): boolean;
|
|
23
|
+
close(code?: number, reason?: string): void;
|
|
24
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.WebSocketMessagingTransport = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* WebSocket adapter for {@link MessagingTransport}. Wraps the global
|
|
6
|
+
* WebSocket constructor; the client supplies the full URL (already
|
|
7
|
+
* carrying the JWT + deviceId query params).
|
|
8
|
+
*/
|
|
9
|
+
class WebSocketMessagingTransport {
|
|
10
|
+
constructor(url) {
|
|
11
|
+
this.url = url;
|
|
12
|
+
this.name = 'ws';
|
|
13
|
+
this.ws = null;
|
|
14
|
+
this.openHandler = null;
|
|
15
|
+
this.frameHandler = null;
|
|
16
|
+
this.closeHandler = null;
|
|
17
|
+
this.errorHandler = null;
|
|
18
|
+
}
|
|
19
|
+
onOpen(handler) { this.openHandler = handler; }
|
|
20
|
+
onFrame(handler) { this.frameHandler = handler; }
|
|
21
|
+
onClose(handler) { this.closeHandler = handler; }
|
|
22
|
+
onError(handler) { this.errorHandler = handler; }
|
|
23
|
+
connect() {
|
|
24
|
+
return new Promise((resolve, reject) => {
|
|
25
|
+
const ws = new WebSocket(this.url);
|
|
26
|
+
ws.binaryType = 'arraybuffer';
|
|
27
|
+
ws.onopen = () => {
|
|
28
|
+
this.ws = ws;
|
|
29
|
+
this.openHandler?.();
|
|
30
|
+
resolve();
|
|
31
|
+
};
|
|
32
|
+
ws.onerror = (event) => {
|
|
33
|
+
const err = new Error('Failed to connect DropOnAir websocket');
|
|
34
|
+
this.errorHandler?.(err);
|
|
35
|
+
// If the socket never opened, reject the connect promise.
|
|
36
|
+
if (ws.readyState !== WebSocket.OPEN) {
|
|
37
|
+
reject(err);
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
ws.onclose = (event) => {
|
|
41
|
+
this.ws = null;
|
|
42
|
+
this.closeHandler?.({ code: event.code, reason: event.reason });
|
|
43
|
+
};
|
|
44
|
+
ws.onmessage = (event) => {
|
|
45
|
+
if (event.data instanceof ArrayBuffer) {
|
|
46
|
+
this.frameHandler?.(new Uint8Array(event.data));
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
send(bytes) {
|
|
52
|
+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
53
|
+
throw new Error('WebSocketMessagingTransport: not open');
|
|
54
|
+
}
|
|
55
|
+
this.ws.send(bytes);
|
|
56
|
+
}
|
|
57
|
+
isOpen() {
|
|
58
|
+
return !!this.ws && this.ws.readyState === WebSocket.OPEN;
|
|
59
|
+
}
|
|
60
|
+
close(code, reason) {
|
|
61
|
+
if (this.ws && this.ws.readyState !== WebSocket.CLOSED) {
|
|
62
|
+
this.ws.close(code, reason);
|
|
63
|
+
}
|
|
64
|
+
this.ws = null;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
exports.WebSocketMessagingTransport = WebSocketMessagingTransport;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { type WebTransportTransportOptions } from './webtransport-transport';
|
|
2
|
+
import type { MessagingTransport, MessagingTransportClose } from './messaging-transport';
|
|
3
|
+
/**
|
|
4
|
+
* WebTransport (HTTP/3) adapter for {@link MessagingTransport}. Wraps the
|
|
5
|
+
* standalone {@link WebTransportTransport} primitive so `MessagingClient`
|
|
6
|
+
* can use the QUIC lane behind the same lifecycle API as WebSocket.
|
|
7
|
+
*/
|
|
8
|
+
export declare class WtMessagingTransport implements MessagingTransport {
|
|
9
|
+
readonly name: "wt";
|
|
10
|
+
private inner;
|
|
11
|
+
private opened;
|
|
12
|
+
private openHandler;
|
|
13
|
+
private frameHandler;
|
|
14
|
+
private closeHandler;
|
|
15
|
+
private errorHandler;
|
|
16
|
+
constructor(options: WebTransportTransportOptions);
|
|
17
|
+
onOpen(handler: () => void): void;
|
|
18
|
+
onFrame(handler: (bytes: Uint8Array) => void): void;
|
|
19
|
+
onClose(handler: (info: MessagingTransportClose) => void): void;
|
|
20
|
+
onError(handler: (err: Error) => void): void;
|
|
21
|
+
connect(): Promise<void>;
|
|
22
|
+
send(bytes: Uint8Array): void;
|
|
23
|
+
isOpen(): boolean;
|
|
24
|
+
close(_code?: number, _reason?: string): void;
|
|
25
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.WtMessagingTransport = void 0;
|
|
4
|
+
const webtransport_transport_1 = require("./webtransport-transport");
|
|
5
|
+
/**
|
|
6
|
+
* WebTransport (HTTP/3) adapter for {@link MessagingTransport}. Wraps the
|
|
7
|
+
* standalone {@link WebTransportTransport} primitive so `MessagingClient`
|
|
8
|
+
* can use the QUIC lane behind the same lifecycle API as WebSocket.
|
|
9
|
+
*/
|
|
10
|
+
class WtMessagingTransport {
|
|
11
|
+
constructor(options) {
|
|
12
|
+
this.name = 'wt';
|
|
13
|
+
this.opened = false;
|
|
14
|
+
this.openHandler = null;
|
|
15
|
+
this.frameHandler = null;
|
|
16
|
+
this.closeHandler = null;
|
|
17
|
+
this.errorHandler = null;
|
|
18
|
+
this.inner = new webtransport_transport_1.WebTransportTransport(options);
|
|
19
|
+
this.inner.onFrame((bytes) => this.frameHandler?.(bytes));
|
|
20
|
+
this.inner.onState((state) => {
|
|
21
|
+
if (state === 'open') {
|
|
22
|
+
this.opened = true;
|
|
23
|
+
this.openHandler?.();
|
|
24
|
+
}
|
|
25
|
+
else if (state === 'closed') {
|
|
26
|
+
this.opened = false;
|
|
27
|
+
this.closeHandler?.({ code: 1000, reason: 'CLOSED' });
|
|
28
|
+
}
|
|
29
|
+
else if (state === 'error') {
|
|
30
|
+
this.errorHandler?.(new Error('WtMessagingTransport: session error'));
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
onOpen(handler) { this.openHandler = handler; }
|
|
35
|
+
onFrame(handler) { this.frameHandler = handler; }
|
|
36
|
+
onClose(handler) { this.closeHandler = handler; }
|
|
37
|
+
onError(handler) { this.errorHandler = handler; }
|
|
38
|
+
async connect() {
|
|
39
|
+
await this.inner.connect();
|
|
40
|
+
}
|
|
41
|
+
send(bytes) {
|
|
42
|
+
this.inner.sendEnvelope(bytes).catch((err) => {
|
|
43
|
+
this.errorHandler?.(err instanceof Error ? err : new Error(String(err)));
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
isOpen() {
|
|
47
|
+
return this.opened;
|
|
48
|
+
}
|
|
49
|
+
close(_code, _reason) {
|
|
50
|
+
this.inner.close();
|
|
51
|
+
this.opened = false;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
exports.WtMessagingTransport = WtMessagingTransport;
|
package/dist/version.d.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* MINOR, additive feature (e.g. multi-device payloads, new call event type)
|
|
8
8
|
* PATCH, bug-fix / perf improvement with no wire or API change
|
|
9
9
|
*/
|
|
10
|
-
export declare const SDK_VERSION = "0.
|
|
10
|
+
export declare const SDK_VERSION = "0.22.1";
|
|
11
11
|
/**
|
|
12
12
|
* Binary encrypted-payload format version.
|
|
13
13
|
* Included as the first byte of every encrypted payload so receivers can
|
package/dist/version.js
CHANGED
|
@@ -10,7 +10,7 @@ exports.PROTOCOL_VERSION = exports.PAYLOAD_FORMAT_VERSION = exports.SDK_VERSION
|
|
|
10
10
|
* MINOR, additive feature (e.g. multi-device payloads, new call event type)
|
|
11
11
|
* PATCH, bug-fix / perf improvement with no wire or API change
|
|
12
12
|
*/
|
|
13
|
-
exports.SDK_VERSION = '0.
|
|
13
|
+
exports.SDK_VERSION = '0.22.1';
|
|
14
14
|
/**
|
|
15
15
|
* Binary encrypted-payload format version.
|
|
16
16
|
* Included as the first byte of every encrypted payload so receivers can
|