@droponair/sdk-js 0.27.0 → 0.29.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 +37 -0
- package/README.md +10 -0
- package/dist/core/messaging-client.d.ts +16 -0
- package/dist/core/messaging-client.js +71 -6
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,42 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.29.0
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- **Delivered and seen no longer depend on the sender being online.** A receipt
|
|
8
|
+
existed only as a frame on a live socket, so a sender who was away when someone
|
|
9
|
+
fetched or read the message never learned it and the mark stayed where it was
|
|
10
|
+
for good. An app that disconnects in the background, which is what an app has to
|
|
11
|
+
do to be pushed at all, missed nearly all of them. The SDK now catches up on
|
|
12
|
+
connect and reports the state as the ordinary delivered and read callbacks, so
|
|
13
|
+
there is no new API to adopt: existing receipt handling starts working.
|
|
14
|
+
|
|
15
|
+
### Notes
|
|
16
|
+
|
|
17
|
+
- Catch-up covers the messages you sent most recently and replays their current
|
|
18
|
+
state, so handling must be idempotent. Marks should only ever move forward.
|
|
19
|
+
- Additive and backward compatible. A relay that predates this answers 404 and
|
|
20
|
+
the SDK treats that as nothing to catch up on.
|
|
21
|
+
|
|
22
|
+
## 0.28.0
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
|
|
26
|
+
- **A push token offered before the client connected was thrown away.** The push
|
|
27
|
+
service hands an app its device token at launch, always before a connection
|
|
28
|
+
exists, so `registerPushToken` refusing it there lost the token exactly when
|
|
29
|
+
every app first offers it, and push simply never arrived. The token is now held
|
|
30
|
+
and registered as soon as a connection exists. It is also re-registered on every
|
|
31
|
+
connect, because the relay ties a token to a session: after a reconnect or a
|
|
32
|
+
relay restart the device would otherwise be silently unreachable.
|
|
33
|
+
|
|
34
|
+
### Notes
|
|
35
|
+
|
|
36
|
+
- `registerPushToken` no longer rejects a call made before connecting. Existing
|
|
37
|
+
code that catches that error still compiles and simply stops seeing it.
|
|
38
|
+
- `unregisterPushToken` drops the held token, so it is not re-registered later.
|
|
39
|
+
|
|
3
40
|
## 0.27.0
|
|
4
41
|
|
|
5
42
|
### Added
|
package/README.md
CHANGED
|
@@ -207,6 +207,10 @@ client.onEvent(e => {
|
|
|
207
207
|
|
|
208
208
|
Available since SDK `0.9.0`. Customers configure their own APNs / FCM / VAPID credentials in the dashboard. The platform delivers a push only when the sender attaches a `pushPayload` to a message and the recipient device has no live WebSocket session. See the per-product availability and limits on your dashboard's Subscription page.
|
|
209
209
|
|
|
210
|
+
|
|
211
|
+
Register the token as soon as you have it. The SDK holds it until a connection
|
|
212
|
+
exists and registers it then, and re-registers on every connect, so there is no
|
|
213
|
+
need to wait for the client to connect or to retry yourself.
|
|
210
214
|
```typescript
|
|
211
215
|
// iOS, after didRegisterForRemoteNotificationsWithDeviceToken
|
|
212
216
|
await client.registerPushToken({ platform: 'APNS', token: deviceTokenHex });
|
|
@@ -359,6 +363,12 @@ client.onMessage(async (msg) => {
|
|
|
359
363
|
| `sendCleartextGroupMessage(groupId, plaintext)` | `Promise<{ messageId }>` | Send a plaintext (non-encrypted) group message; server fans out to every other member. |
|
|
360
364
|
| `onGroupMessage(callback)` | `() => void` | Listen for group messages |
|
|
361
365
|
|
|
366
|
+
Delivered and seen are caught up on every connect, so a sender that was away
|
|
367
|
+
while someone fetched or read still learns about it. The state arrives through
|
|
368
|
+
the same delivered and read callbacks a live receipt uses, so there is nothing
|
|
369
|
+
extra to handle; make sure your handling is idempotent and only ever moves a
|
|
370
|
+
mark forward, since catch-up replays the current state rather than the changes.
|
|
371
|
+
|
|
362
372
|
**Group delivery is per member.** A group message has many recipients, so the platform
|
|
363
373
|
reports delivery as a set rather than a single flag and leaves the presentation to you.
|
|
364
374
|
`DELIVERED` and `SEEN` arrive as events whose `metadata` carries `messageId`, `groupId`
|
|
@@ -22,6 +22,11 @@ export declare class MessagingClient implements DropOnAirClient {
|
|
|
22
22
|
private dropOnAirJwt;
|
|
23
23
|
private currentUserId;
|
|
24
24
|
private deviceId;
|
|
25
|
+
/**
|
|
26
|
+
* The push token this device last offered, held until a connection exists and
|
|
27
|
+
* re-sent on every connect. Cleared by `unregisterPushToken`.
|
|
28
|
+
*/
|
|
29
|
+
private pendingPushRegistration;
|
|
25
30
|
private rateLimited;
|
|
26
31
|
private reconnectAttempt;
|
|
27
32
|
private connectGeneration;
|
|
@@ -169,6 +174,8 @@ export declare class MessagingClient implements DropOnAirClient {
|
|
|
169
174
|
token: string;
|
|
170
175
|
voipToken?: string;
|
|
171
176
|
}): Promise<void>;
|
|
177
|
+
/** Registers the held token, if there is one. Called on every connect. */
|
|
178
|
+
private sendPushRegistration;
|
|
172
179
|
/**
|
|
173
180
|
* Unregister this device's push notification token (e.g. on logout). Future
|
|
174
181
|
* push fan-out for this (appId, userId, deviceId, platform) tuple is dropped.
|
|
@@ -381,6 +388,15 @@ export declare class MessagingClient implements DropOnAirClient {
|
|
|
381
388
|
* live at that instant and is unreachable afterwards, so anyone closed loses it.
|
|
382
389
|
* Fetching is also what tells the sender the message reached this member.
|
|
383
390
|
*/
|
|
391
|
+
/**
|
|
392
|
+
* Catches up on who has the messages this client sent.
|
|
393
|
+
*
|
|
394
|
+
* A receipt used to be a frame on a live socket and nothing else, so a sender
|
|
395
|
+
* that was away when someone fetched or read never found out and the tick
|
|
396
|
+
* stayed where it was. The state is kept per member, and this replays it as the
|
|
397
|
+
* events that would have arrived live, so an app needs no separate path for it.
|
|
398
|
+
*/
|
|
399
|
+
private fetchGroupReceipts;
|
|
384
400
|
private fetchAndProcessOfflineGroupMessages;
|
|
385
401
|
private fetchAndProcessOfflineMessages;
|
|
386
402
|
private getValidDropOnAirJwt;
|
|
@@ -246,6 +246,11 @@ class MessagingClient {
|
|
|
246
246
|
this.dropOnAirJwt = null;
|
|
247
247
|
this.currentUserId = null;
|
|
248
248
|
this.deviceId = null;
|
|
249
|
+
/**
|
|
250
|
+
* The push token this device last offered, held until a connection exists and
|
|
251
|
+
* re-sent on every connect. Cleared by `unregisterPushToken`.
|
|
252
|
+
*/
|
|
253
|
+
this.pendingPushRegistration = null;
|
|
249
254
|
this.rateLimited = false;
|
|
250
255
|
this.reconnectAttempt = 0;
|
|
251
256
|
// Monotonic id for each connect attempt. Transport callbacks (onOpen/onError/
|
|
@@ -650,18 +655,36 @@ class MessagingClient {
|
|
|
650
655
|
* stored token and refreshes its lastSeenAt timestamp.
|
|
651
656
|
*/
|
|
652
657
|
async registerPushToken(opts) {
|
|
653
|
-
if (!this.transport?.isOpen()) {
|
|
654
|
-
throw new Error('DropOnAir websocket is not connected');
|
|
655
|
-
}
|
|
656
658
|
if (!opts.token || opts.token.trim().length === 0) {
|
|
657
659
|
throw new Error('Push token must not be empty');
|
|
658
660
|
}
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
661
|
+
// The push service hands an app its token at startup, usually before a
|
|
662
|
+
// connection exists, so refusing the registration here would lose it exactly
|
|
663
|
+
// when every app first offers it. Hold it instead and send it on connect, and
|
|
664
|
+
// keep holding it: the relay ties a token to a session, so a reconnect or a
|
|
665
|
+
// relay restart has to re-register or the device goes quiet.
|
|
666
|
+
this.pendingPushRegistration = {
|
|
662
667
|
platform: opts.platform,
|
|
663
668
|
token: opts.token,
|
|
664
669
|
voipToken: opts.voipToken ?? '',
|
|
670
|
+
};
|
|
671
|
+
if (!this.transport?.isOpen()) {
|
|
672
|
+
return;
|
|
673
|
+
}
|
|
674
|
+
await this.sendPushRegistration();
|
|
675
|
+
}
|
|
676
|
+
/** Registers the held token, if there is one. Called on every connect. */
|
|
677
|
+
async sendPushRegistration() {
|
|
678
|
+
const pending = this.pendingPushRegistration;
|
|
679
|
+
if (!pending || !this.transport?.isOpen()) {
|
|
680
|
+
return;
|
|
681
|
+
}
|
|
682
|
+
const deviceId = this.deviceId ?? await this.getOrCreateDeviceId();
|
|
683
|
+
const frame = {
|
|
684
|
+
type: 'PUSH_REGISTER',
|
|
685
|
+
platform: pending.platform,
|
|
686
|
+
token: pending.token,
|
|
687
|
+
voipToken: pending.voipToken,
|
|
665
688
|
deviceId,
|
|
666
689
|
};
|
|
667
690
|
this.transport.send(this.codec.encodePushRegistrationFrame(frame));
|
|
@@ -671,6 +694,7 @@ class MessagingClient {
|
|
|
671
694
|
* push fan-out for this (appId, userId, deviceId, platform) tuple is dropped.
|
|
672
695
|
*/
|
|
673
696
|
async unregisterPushToken(opts) {
|
|
697
|
+
this.pendingPushRegistration = null;
|
|
674
698
|
if (!this.transport?.isOpen()) {
|
|
675
699
|
throw new Error('DropOnAir websocket is not connected');
|
|
676
700
|
}
|
|
@@ -1779,6 +1803,12 @@ class MessagingClient {
|
|
|
1779
1803
|
if (this.dropOnAirJwt) {
|
|
1780
1804
|
this.scheduleProactiveTokenRefresh(this.dropOnAirJwt);
|
|
1781
1805
|
}
|
|
1806
|
+
this.sendPushRegistration().catch((err) => {
|
|
1807
|
+
this.logError('push_registration_failed', { error: String(err) });
|
|
1808
|
+
});
|
|
1809
|
+
this.fetchGroupReceipts().catch((err) => {
|
|
1810
|
+
this.logError('group_receipts_fetch_failed', { error: String(err) });
|
|
1811
|
+
});
|
|
1782
1812
|
this.fetchAndProcessOfflineGroupMessages().catch((err) => {
|
|
1783
1813
|
this.logError('group_offline_fetch_failed', { error: String(err) });
|
|
1784
1814
|
});
|
|
@@ -2250,6 +2280,41 @@ class MessagingClient {
|
|
|
2250
2280
|
* live at that instant and is unreachable afterwards, so anyone closed loses it.
|
|
2251
2281
|
* Fetching is also what tells the sender the message reached this member.
|
|
2252
2282
|
*/
|
|
2283
|
+
/**
|
|
2284
|
+
* Catches up on who has the messages this client sent.
|
|
2285
|
+
*
|
|
2286
|
+
* A receipt used to be a frame on a live socket and nothing else, so a sender
|
|
2287
|
+
* that was away when someone fetched or read never found out and the tick
|
|
2288
|
+
* stayed where it was. The state is kept per member, and this replays it as the
|
|
2289
|
+
* events that would have arrived live, so an app needs no separate path for it.
|
|
2290
|
+
*/
|
|
2291
|
+
async fetchGroupReceipts() {
|
|
2292
|
+
const jwt = await this.getValidDropOnAirJwt(false);
|
|
2293
|
+
const response = await this.fetchFn(`${this.httpUrl}/v1/groups/messages/receipts?limit=100`, {
|
|
2294
|
+
method: 'GET',
|
|
2295
|
+
headers: { Authorization: `Bearer ${jwt}`, 'Content-Type': 'application/json' },
|
|
2296
|
+
});
|
|
2297
|
+
// A relay that predates this endpoint answers 404: nothing to catch up on.
|
|
2298
|
+
if (!response.ok) {
|
|
2299
|
+
return;
|
|
2300
|
+
}
|
|
2301
|
+
const body = await response.json();
|
|
2302
|
+
for (const entry of body.messages ?? []) {
|
|
2303
|
+
for (const memberUserId of entry.deliveredTo ?? []) {
|
|
2304
|
+
this.emitEvent({
|
|
2305
|
+
type: 'DELIVERED',
|
|
2306
|
+
metadata: JSON.stringify({ messageId: entry.messageId, groupId: entry.groupId, memberUserId }),
|
|
2307
|
+
});
|
|
2308
|
+
}
|
|
2309
|
+
for (const memberUserId of entry.seenBy ?? []) {
|
|
2310
|
+
this.emitEvent({
|
|
2311
|
+
type: 'SEEN',
|
|
2312
|
+
metadata: JSON.stringify({ messageId: entry.messageId, groupId: entry.groupId, memberUserId }),
|
|
2313
|
+
});
|
|
2314
|
+
}
|
|
2315
|
+
}
|
|
2316
|
+
this.log('group_receipts_fetched', { messages: (body.messages ?? []).length });
|
|
2317
|
+
}
|
|
2253
2318
|
async fetchAndProcessOfflineGroupMessages() {
|
|
2254
2319
|
const jwt = await this.getValidDropOnAirJwt(false);
|
|
2255
2320
|
let page = 0;
|
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.29.0";
|
|
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.29.0';
|
|
14
14
|
/**
|
|
15
15
|
* Binary encrypted-payload format version.
|
|
16
16
|
* Included as the first byte of every encrypted payload so receivers can
|
package/package.json
CHANGED