@trycourier/courier-js 2.0.2-beta → 2.0.4-beta
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/README.md +2 -2
- package/dist/__tests__/courier-inbox-socket.test.d.ts +1 -0
- package/dist/__tests__/courier-inbox-transaction-manager.test.d.ts +1 -0
- package/dist/__tests__/courier-socket.test.d.ts +1 -0
- package/dist/client/inbox-client.d.ts +14 -2
- package/dist/index.d.ts +4 -4
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +612 -146
- package/dist/index.mjs.map +1 -0
- package/dist/socket/courier-inbox-socket.d.ts +87 -0
- package/dist/socket/courier-inbox-transaction-manager.d.ts +54 -0
- package/dist/socket/courier-socket.d.ts +146 -17
- package/dist/socket/inbox-message-utils.d.ts +8 -0
- package/dist/socket/version.d.ts +1 -0
- package/dist/types/socket/protocol/errors.d.ts +15 -0
- package/dist/types/socket/protocol/messages.d.ts +119 -0
- package/dist/utils/uuid.d.ts +11 -1
- package/package.json +3 -1
- package/dist/socket/inbox-socket.d.ts +0 -19
package/dist/index.mjs
CHANGED
|
@@ -1,96 +1,32 @@
|
|
|
1
1
|
var __defProp = Object.defineProperty;
|
|
2
2
|
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
3
3
|
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
this.webSocket = new WebSocket(this.url);
|
|
31
|
-
this.webSocket.onopen = () => {
|
|
32
|
-
var _a;
|
|
33
|
-
(_a = this.onOpen) == null ? void 0 : _a.call(this);
|
|
34
|
-
resolve();
|
|
35
|
-
};
|
|
36
|
-
this.webSocket.onmessage = (event) => {
|
|
37
|
-
var _a;
|
|
38
|
-
(_a = this.onMessageReceived) == null ? void 0 : _a.call(this, event.data);
|
|
39
|
-
};
|
|
40
|
-
this.webSocket.onclose = (event) => {
|
|
41
|
-
var _a;
|
|
42
|
-
this.webSocket = null;
|
|
43
|
-
(_a = this.onClose) == null ? void 0 : _a.call(this, event.code, event.reason);
|
|
44
|
-
};
|
|
45
|
-
this.webSocket.onerror = (event) => {
|
|
46
|
-
var _a;
|
|
47
|
-
this.webSocket = null;
|
|
48
|
-
const error = new Error("Courier Socket connection failed");
|
|
49
|
-
error.originalEvent = event;
|
|
50
|
-
(_a = this.onError) == null ? void 0 : _a.call(this, error);
|
|
51
|
-
reject(error);
|
|
52
|
-
};
|
|
53
|
-
} catch (error) {
|
|
54
|
-
this.webSocket = null;
|
|
55
|
-
reject(error);
|
|
56
|
-
}
|
|
57
|
-
});
|
|
58
|
-
}
|
|
59
|
-
disconnect() {
|
|
60
|
-
this.stopPing();
|
|
61
|
-
if (this.webSocket) {
|
|
62
|
-
this.webSocket.close(_CourierSocket.NORMAL_CLOSURE_STATUS);
|
|
63
|
-
this.webSocket = null;
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
async send(message) {
|
|
67
|
-
if (!this.webSocket) {
|
|
68
|
-
return false;
|
|
69
|
-
}
|
|
70
|
-
const json = JSON.stringify(message);
|
|
71
|
-
return this.webSocket.send(json) !== void 0;
|
|
72
|
-
}
|
|
73
|
-
keepAlive(props) {
|
|
74
|
-
this.stopPing();
|
|
75
|
-
this.pingInterval = setInterval(async () => {
|
|
76
|
-
var _a;
|
|
77
|
-
try {
|
|
78
|
-
await this.send({ action: "keepAlive" });
|
|
79
|
-
} catch (error) {
|
|
80
|
-
(_a = this.options.logger) == null ? void 0 : _a.error("Error occurred on Keep Alive:", error);
|
|
81
|
-
}
|
|
82
|
-
}, (props == null ? void 0 : props.intervalInMillis) ?? 3e5);
|
|
83
|
-
}
|
|
84
|
-
stopPing() {
|
|
85
|
-
if (this.pingInterval) {
|
|
86
|
-
clearInterval(this.pingInterval);
|
|
87
|
-
this.pingInterval = null;
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
};
|
|
91
|
-
// Constants
|
|
92
|
-
__publicField(_CourierSocket, "NORMAL_CLOSURE_STATUS", 1e3);
|
|
93
|
-
let CourierSocket = _CourierSocket;
|
|
4
|
+
var ClientAction = /* @__PURE__ */ ((ClientAction2) => {
|
|
5
|
+
ClientAction2["Subscribe"] = "subscribe";
|
|
6
|
+
ClientAction2["Unsubscribe"] = "unsubscribe";
|
|
7
|
+
ClientAction2["Pong"] = "pong";
|
|
8
|
+
ClientAction2["Ping"] = "ping";
|
|
9
|
+
ClientAction2["GetConfig"] = "get-config";
|
|
10
|
+
return ClientAction2;
|
|
11
|
+
})(ClientAction || {});
|
|
12
|
+
var ServerAction = /* @__PURE__ */ ((ServerAction2) => {
|
|
13
|
+
ServerAction2["Ping"] = "ping";
|
|
14
|
+
return ServerAction2;
|
|
15
|
+
})(ServerAction || {});
|
|
16
|
+
var InboxMessageEvent = /* @__PURE__ */ ((InboxMessageEvent2) => {
|
|
17
|
+
InboxMessageEvent2["NewMessage"] = "message";
|
|
18
|
+
InboxMessageEvent2["Archive"] = "archive";
|
|
19
|
+
InboxMessageEvent2["ArchiveAll"] = "archive-all";
|
|
20
|
+
InboxMessageEvent2["ArchiveRead"] = "archive-read";
|
|
21
|
+
InboxMessageEvent2["Clicked"] = "clicked";
|
|
22
|
+
InboxMessageEvent2["MarkAllRead"] = "mark-all-read";
|
|
23
|
+
InboxMessageEvent2["Opened"] = "opened";
|
|
24
|
+
InboxMessageEvent2["Read"] = "read";
|
|
25
|
+
InboxMessageEvent2["Unarchive"] = "unarchive";
|
|
26
|
+
InboxMessageEvent2["Unopened"] = "unopened";
|
|
27
|
+
InboxMessageEvent2["Unread"] = "unread";
|
|
28
|
+
return InboxMessageEvent2;
|
|
29
|
+
})(InboxMessageEvent || {});
|
|
94
30
|
const getCourierApiUrls = (urls) => ({
|
|
95
31
|
courier: {
|
|
96
32
|
rest: (urls == null ? void 0 : urls.courier.rest) || "https://api.courier.com",
|
|
@@ -98,7 +34,7 @@ const getCourierApiUrls = (urls) => ({
|
|
|
98
34
|
},
|
|
99
35
|
inbox: {
|
|
100
36
|
graphql: (urls == null ? void 0 : urls.inbox.graphql) || "https://inbox.courier.com/q",
|
|
101
|
-
webSocket: (urls == null ? void 0 : urls.inbox.webSocket) || "wss://realtime.courier.
|
|
37
|
+
webSocket: (urls == null ? void 0 : urls.inbox.webSocket) || "wss://realtime.courier.io"
|
|
102
38
|
}
|
|
103
39
|
});
|
|
104
40
|
class Logger {
|
|
@@ -132,12 +68,27 @@ class Logger {
|
|
|
132
68
|
}
|
|
133
69
|
}
|
|
134
70
|
}
|
|
135
|
-
class
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
71
|
+
const _UUID = class _UUID {
|
|
72
|
+
/**
|
|
73
|
+
* nanoid
|
|
74
|
+
* Copyright 2017 Andrey Sitnik <andrey@sitnik.ru>
|
|
75
|
+
*
|
|
76
|
+
* https://github.com/ai/nanoid/blob/main/LICENSE
|
|
77
|
+
*
|
|
78
|
+
* @param size - The size of the UUID to generate.
|
|
79
|
+
* @returns A string representing the UUID.
|
|
80
|
+
*/
|
|
81
|
+
static nanoid(size = 21) {
|
|
82
|
+
let id = "";
|
|
83
|
+
let bytes = crypto.getRandomValues(new Uint8Array(size |= 0));
|
|
84
|
+
while (size--) {
|
|
85
|
+
id += _UUID.ALPHABET[bytes[size] & 63];
|
|
86
|
+
}
|
|
87
|
+
return id;
|
|
139
88
|
}
|
|
140
|
-
}
|
|
89
|
+
};
|
|
90
|
+
__publicField(_UUID, "ALPHABET", "useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict");
|
|
91
|
+
let UUID = _UUID;
|
|
141
92
|
class CourierRequestError extends Error {
|
|
142
93
|
constructor(code, message, type) {
|
|
143
94
|
super(message);
|
|
@@ -166,7 +117,7 @@ Response JSON: ${JSON.stringify(data.response, null, 2)}
|
|
|
166
117
|
}
|
|
167
118
|
async function http(props) {
|
|
168
119
|
const validCodes = props.validCodes ?? [200];
|
|
169
|
-
const uid = props.options.showLogs ? UUID.
|
|
120
|
+
const uid = props.options.showLogs ? UUID.nanoid() : void 0;
|
|
170
121
|
const request = new Request(props.url, {
|
|
171
122
|
method: props.method,
|
|
172
123
|
headers: {
|
|
@@ -216,7 +167,7 @@ async function http(props) {
|
|
|
216
167
|
return data;
|
|
217
168
|
}
|
|
218
169
|
async function graphql(props) {
|
|
219
|
-
const uid = props.options.showLogs ? UUID.
|
|
170
|
+
const uid = props.options.showLogs ? UUID.nanoid() : void 0;
|
|
220
171
|
if (uid) {
|
|
221
172
|
logRequest(props.options.logger, uid, "GraphQL", {
|
|
222
173
|
url: props.url,
|
|
@@ -305,70 +256,536 @@ class BrandClient extends Client {
|
|
|
305
256
|
return json.data.brand;
|
|
306
257
|
}
|
|
307
258
|
}
|
|
308
|
-
|
|
259
|
+
const CLOSE_CODE_NORMAL_CLOSURE = 1e3;
|
|
260
|
+
const IPW_VERSION = "v1";
|
|
261
|
+
const _CourierSocket = class _CourierSocket {
|
|
309
262
|
constructor(options) {
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
__publicField(this, "
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
263
|
+
/** The WebSocket instance, which may be null if the connection is not established. */
|
|
264
|
+
__publicField(this, "webSocket", null);
|
|
265
|
+
/** The number of connection retry attempts so far, reset after a successful connection. */
|
|
266
|
+
__publicField(this, "retryAttempt", 0);
|
|
267
|
+
/** The timeout ID for the current connectionretry attempt, reset when we attempt to connect. */
|
|
268
|
+
__publicField(this, "retryTimeoutId", null);
|
|
269
|
+
__publicField(this, "url");
|
|
270
|
+
__publicField(this, "options");
|
|
271
|
+
this.url = options.apiUrls.inbox.webSocket;
|
|
272
|
+
this.options = options;
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Connects to the Courier WebSocket server.
|
|
276
|
+
*
|
|
277
|
+
* If the connection is already established, this is a no-op.
|
|
278
|
+
*
|
|
279
|
+
* @returns A promise that resolves when the connection is established or rejects if the connection could not be established.
|
|
280
|
+
*/
|
|
281
|
+
async connect() {
|
|
282
|
+
var _a;
|
|
283
|
+
this.clearRetryTimeout();
|
|
284
|
+
if (this.isConnecting || this.isOpen) {
|
|
285
|
+
(_a = this.options.logger) == null ? void 0 : _a.info("Attempted to open a WebSocket connection, but one already exists.");
|
|
286
|
+
return Promise.reject(new Error("WebSocket connection already exists"));
|
|
287
|
+
}
|
|
288
|
+
return new Promise((resolve, reject) => {
|
|
289
|
+
this.webSocket = new WebSocket(this.getWebSocketUrl());
|
|
290
|
+
this.webSocket.addEventListener("open", (event) => {
|
|
291
|
+
this.retryAttempt = 0;
|
|
292
|
+
this.onOpen(event);
|
|
293
|
+
resolve();
|
|
294
|
+
});
|
|
295
|
+
this.webSocket.addEventListener("message", async (event) => {
|
|
296
|
+
var _a2;
|
|
297
|
+
try {
|
|
298
|
+
const json = JSON.parse(event.data);
|
|
299
|
+
if ("event" in json && json.event === "reconnect") {
|
|
300
|
+
this.close(CLOSE_CODE_NORMAL_CLOSURE);
|
|
301
|
+
await this.retryConnection(json.retryAfter * 1e3);
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
this.onMessageReceived(json);
|
|
305
|
+
} catch (error) {
|
|
306
|
+
(_a2 = this.options.logger) == null ? void 0 : _a2.error("Error parsing socket message", error);
|
|
307
|
+
}
|
|
308
|
+
});
|
|
309
|
+
this.webSocket.addEventListener("close", (event) => {
|
|
310
|
+
if (event.code !== CLOSE_CODE_NORMAL_CLOSURE) {
|
|
311
|
+
const courierCloseEvent = _CourierSocket.parseCloseEvent(event);
|
|
312
|
+
if (courierCloseEvent.retryAfterSeconds) {
|
|
313
|
+
this.retryConnection(courierCloseEvent.retryAfterSeconds * 1e3);
|
|
314
|
+
} else {
|
|
315
|
+
this.retryConnection();
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
this.onClose(event);
|
|
319
|
+
});
|
|
320
|
+
this.webSocket.addEventListener("error", (event) => {
|
|
321
|
+
this.retryConnection();
|
|
322
|
+
this.onError(event);
|
|
323
|
+
reject(event);
|
|
324
|
+
});
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* Closes the WebSocket connection.
|
|
329
|
+
*
|
|
330
|
+
* See {@link https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/close} for more details.
|
|
331
|
+
*
|
|
332
|
+
* @param code The WebSocket close code. Defaults to {@link CLOSE_CODE_NORMAL_CLOSURE}.
|
|
333
|
+
* @param reason The WebSocket close reason.
|
|
334
|
+
*/
|
|
335
|
+
close(code = CLOSE_CODE_NORMAL_CLOSURE, reason) {
|
|
336
|
+
if (this.webSocket === null) {
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
this.webSocket.close(code, reason);
|
|
340
|
+
this.webSocket = null;
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* Sends a message to the Courier WebSocket server.
|
|
344
|
+
*
|
|
345
|
+
* @param message The message to send. The message will be serialized to a JSON string.
|
|
346
|
+
*/
|
|
347
|
+
send(message) {
|
|
348
|
+
var _a;
|
|
349
|
+
if (this.webSocket === null || this.isConnecting) {
|
|
350
|
+
(_a = this.options.logger) == null ? void 0 : _a.info("Attempted to send a message, but the WebSocket is not yet open.");
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
const json = JSON.stringify(message);
|
|
354
|
+
this.webSocket.send(json);
|
|
355
|
+
}
|
|
356
|
+
get userId() {
|
|
357
|
+
return this.options.userId;
|
|
358
|
+
}
|
|
359
|
+
get logger() {
|
|
360
|
+
return this.options.logger;
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* Whether the WebSocket connection is currently being established.
|
|
364
|
+
*/
|
|
365
|
+
get isConnecting() {
|
|
366
|
+
return this.webSocket !== null && this.webSocket.readyState === WebSocket.CONNECTING;
|
|
367
|
+
}
|
|
368
|
+
/**
|
|
369
|
+
* Whether the WebSocket connection is currently open.
|
|
370
|
+
*/
|
|
371
|
+
get isOpen() {
|
|
372
|
+
return this.webSocket !== null && this.webSocket.readyState === WebSocket.OPEN;
|
|
373
|
+
}
|
|
374
|
+
/**
|
|
375
|
+
* Constructs the WebSocket URL for the Courier WebSocket server using context
|
|
376
|
+
* from the {@link CourierClientOptions} passed to the constructor.
|
|
377
|
+
*
|
|
378
|
+
* @returns The WebSocket URL
|
|
379
|
+
*/
|
|
380
|
+
getWebSocketUrl() {
|
|
381
|
+
const accessToken = this.options.accessToken;
|
|
382
|
+
const connectionId = this.options.connectionId;
|
|
383
|
+
const userId = this.userId;
|
|
384
|
+
return `${this.url}?auth=${accessToken}&cid=${connectionId}&iwpv=${IPW_VERSION}&userId=${userId}`;
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* Parses the Retry-After time from the WebSocket close event reason,
|
|
388
|
+
* and returns a new {@link CourierCloseEvent} with the retry after time in seconds
|
|
389
|
+
* if present.
|
|
390
|
+
*
|
|
391
|
+
* The Courier WebSocket server may send the close event reason in the following format:
|
|
392
|
+
*
|
|
393
|
+
* ```json
|
|
394
|
+
* {
|
|
395
|
+
* "Retry-After": "10" // The retry after time in seconds
|
|
396
|
+
* }
|
|
397
|
+
* ```
|
|
398
|
+
*
|
|
399
|
+
* @see https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent/reason
|
|
400
|
+
*
|
|
401
|
+
* @param closeEvent The WebSocket close event.
|
|
402
|
+
* @returns The WebSocket close event with the retry after time in seconds.
|
|
403
|
+
*/
|
|
404
|
+
static parseCloseEvent(closeEvent) {
|
|
405
|
+
if (closeEvent.reason === null || closeEvent.reason === "") {
|
|
406
|
+
return closeEvent;
|
|
407
|
+
}
|
|
318
408
|
try {
|
|
319
|
-
const
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
const message = JSON.parse(data);
|
|
327
|
-
(_b = this.receivedMessage) == null ? void 0 : _b.call(this, message);
|
|
328
|
-
break;
|
|
409
|
+
const jsonReason = JSON.parse(closeEvent.reason);
|
|
410
|
+
if (!jsonReason[_CourierSocket.RETRY_AFTER_KEY]) {
|
|
411
|
+
return closeEvent;
|
|
412
|
+
}
|
|
413
|
+
const retryAfterSeconds = parseInt(jsonReason[_CourierSocket.RETRY_AFTER_KEY]);
|
|
414
|
+
if (Number.isNaN(retryAfterSeconds) || retryAfterSeconds < 0) {
|
|
415
|
+
return closeEvent;
|
|
329
416
|
}
|
|
417
|
+
return {
|
|
418
|
+
...closeEvent,
|
|
419
|
+
retryAfterSeconds
|
|
420
|
+
};
|
|
330
421
|
} catch (error) {
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
422
|
+
return closeEvent;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
/**
|
|
426
|
+
* Calculates the retry backoff time in milliseconds based on the current retry attempt.
|
|
427
|
+
*/
|
|
428
|
+
getBackoffTimeInMillis() {
|
|
429
|
+
const backoffIntervalInMillis = _CourierSocket.BACKOFF_INTERVALS_IN_MILLIS[this.retryAttempt];
|
|
430
|
+
const lowerBound = backoffIntervalInMillis - backoffIntervalInMillis * _CourierSocket.BACKOFF_JITTER_FACTOR;
|
|
431
|
+
const upperBound = backoffIntervalInMillis + backoffIntervalInMillis * _CourierSocket.BACKOFF_JITTER_FACTOR;
|
|
432
|
+
return Math.floor(Math.random() * (upperBound - lowerBound) + lowerBound);
|
|
433
|
+
}
|
|
434
|
+
/**
|
|
435
|
+
* Retries the connection to the Courier WebSocket server after
|
|
436
|
+
* either {@param suggestedBackoffTimeInMillis} or a random backoff time
|
|
437
|
+
* calculated using {@link getBackoffTimeInMillis}.
|
|
438
|
+
*
|
|
439
|
+
* @param suggestedBackoffTimeInMillis The suggested backoff time in milliseconds.
|
|
440
|
+
* @returns A promise that resolves when the connection is established or rejects if the connection could not be established.
|
|
441
|
+
*/
|
|
442
|
+
async retryConnection(suggestedBackoffTimeInMillis) {
|
|
443
|
+
var _a, _b, _c;
|
|
444
|
+
if (this.retryTimeoutId !== null) {
|
|
445
|
+
(_a = this.logger) == null ? void 0 : _a.debug("Skipping retry attempt because a previous retry is already scheduled.");
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
if (this.retryAttempt >= _CourierSocket.MAX_RETRY_ATTEMPTS) {
|
|
449
|
+
(_b = this.logger) == null ? void 0 : _b.error(`Max retry attempts (${_CourierSocket.MAX_RETRY_ATTEMPTS}) reached.`);
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
const backoffTimeInMillis = suggestedBackoffTimeInMillis ?? this.getBackoffTimeInMillis();
|
|
453
|
+
this.retryTimeoutId = window.setTimeout(async () => {
|
|
454
|
+
try {
|
|
455
|
+
await this.connect();
|
|
456
|
+
} catch (error) {
|
|
334
457
|
}
|
|
458
|
+
}, backoffTimeInMillis);
|
|
459
|
+
(_c = this.logger) == null ? void 0 : _c.debug(`Retrying connection in ${Math.floor(backoffTimeInMillis / 1e3)}s. Retry attempt ${this.retryAttempt + 1} of ${_CourierSocket.MAX_RETRY_ATTEMPTS}.`);
|
|
460
|
+
this.retryAttempt++;
|
|
461
|
+
}
|
|
462
|
+
/**
|
|
463
|
+
* Clears the retry timeout if it exists.
|
|
464
|
+
*/
|
|
465
|
+
clearRetryTimeout() {
|
|
466
|
+
if (this.retryTimeoutId !== null) {
|
|
467
|
+
window.clearTimeout(this.retryTimeoutId);
|
|
468
|
+
this.retryTimeoutId = null;
|
|
335
469
|
}
|
|
336
470
|
}
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
471
|
+
};
|
|
472
|
+
/**
|
|
473
|
+
* The jitter factor for the backoff intervals.
|
|
474
|
+
*
|
|
475
|
+
* Backoff with jitter is calculated as a random value in the range:
|
|
476
|
+
* [BACKOFF_INTERVAL - BACKOFF_JITTER_FACTOR * BACKOFF_INTERVAL,
|
|
477
|
+
* BACKOFF_INTERVAL + BACKOFF_JITTER_FACTOR * BACKOFF_INTERVAL).
|
|
478
|
+
*/
|
|
479
|
+
__publicField(_CourierSocket, "BACKOFF_JITTER_FACTOR", 0.5);
|
|
480
|
+
/**
|
|
481
|
+
* The maximum number of retry attempts.
|
|
482
|
+
*/
|
|
483
|
+
__publicField(_CourierSocket, "MAX_RETRY_ATTEMPTS", 5);
|
|
484
|
+
/**
|
|
485
|
+
* Backoff intervals in milliseconds.
|
|
486
|
+
*
|
|
487
|
+
* Each represents an offset from the previous interval, rather than a
|
|
488
|
+
* absolute offset from the initial request time.
|
|
489
|
+
*/
|
|
490
|
+
__publicField(_CourierSocket, "BACKOFF_INTERVALS_IN_MILLIS", [
|
|
491
|
+
3e4,
|
|
492
|
+
// 30 seconds
|
|
493
|
+
6e4,
|
|
494
|
+
// 1 minute
|
|
495
|
+
12e4,
|
|
496
|
+
// 2 minutes
|
|
497
|
+
24e4,
|
|
498
|
+
// 4 minutes
|
|
499
|
+
48e4
|
|
500
|
+
// 8 minutes
|
|
501
|
+
]);
|
|
502
|
+
/**
|
|
503
|
+
* The key of the retry after time in the WebSocket close event reason.
|
|
504
|
+
*
|
|
505
|
+
* The Courier WebSocket server may send the close event reason in the following format:
|
|
506
|
+
*
|
|
507
|
+
* ```json
|
|
508
|
+
* {
|
|
509
|
+
* "Retry-After": "10" // The retry after time in seconds
|
|
510
|
+
* }
|
|
511
|
+
* ```
|
|
512
|
+
*
|
|
513
|
+
* @see https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent/reason
|
|
514
|
+
*/
|
|
515
|
+
__publicField(_CourierSocket, "RETRY_AFTER_KEY", "Retry-After");
|
|
516
|
+
let CourierSocket = _CourierSocket;
|
|
517
|
+
class TransactionManager {
|
|
518
|
+
constructor(completedTransactionsToKeep = 10) {
|
|
519
|
+
/**
|
|
520
|
+
* The map of <transactionId, Transaction> representing outstanding requests.
|
|
521
|
+
*/
|
|
522
|
+
__publicField(this, "outstandingRequestsMap", /* @__PURE__ */ new Map());
|
|
523
|
+
/**
|
|
524
|
+
* The queue of completed requests. This is a FIFO queue of the last N completed requests,
|
|
525
|
+
* where N is {@link completedTransactionsToKeep}.
|
|
526
|
+
*/
|
|
527
|
+
__publicField(this, "completedTransactionsQueue", []);
|
|
528
|
+
/**
|
|
529
|
+
* Number of completed requests to keep in memory.
|
|
530
|
+
*/
|
|
531
|
+
__publicField(this, "completedTransactionsToKeep");
|
|
532
|
+
this.completedTransactionsToKeep = completedTransactionsToKeep;
|
|
533
|
+
}
|
|
534
|
+
addOutstandingRequest(transactionId, request) {
|
|
535
|
+
const isOutstanding = this.outstandingRequestsMap.has(transactionId);
|
|
536
|
+
if (isOutstanding) {
|
|
537
|
+
throw new Error(`Transaction [${transactionId}] already has an outstanding request`);
|
|
538
|
+
}
|
|
539
|
+
const transaction = {
|
|
540
|
+
transactionId,
|
|
541
|
+
request,
|
|
542
|
+
response: null,
|
|
543
|
+
start: /* @__PURE__ */ new Date(),
|
|
544
|
+
end: null
|
|
545
|
+
};
|
|
546
|
+
this.outstandingRequestsMap.set(transactionId, transaction);
|
|
547
|
+
}
|
|
548
|
+
addResponse(transactionId, response) {
|
|
549
|
+
const transaction = this.outstandingRequestsMap.get(transactionId);
|
|
550
|
+
if (transaction === void 0) {
|
|
551
|
+
throw new Error(`Transaction [${transactionId}] does not have an outstanding request`);
|
|
552
|
+
}
|
|
553
|
+
transaction.response = response;
|
|
554
|
+
transaction.end = /* @__PURE__ */ new Date();
|
|
555
|
+
this.outstandingRequestsMap.delete(transactionId);
|
|
556
|
+
this.addCompletedTransaction(transaction);
|
|
557
|
+
}
|
|
558
|
+
get outstandingRequests() {
|
|
559
|
+
return Array.from(this.outstandingRequestsMap.values());
|
|
560
|
+
}
|
|
561
|
+
get completedTransactions() {
|
|
562
|
+
return this.completedTransactionsQueue;
|
|
563
|
+
}
|
|
564
|
+
clearOutstandingRequests() {
|
|
565
|
+
this.outstandingRequestsMap.clear();
|
|
566
|
+
}
|
|
567
|
+
/**
|
|
568
|
+
* Adds a completed request to the queue.
|
|
569
|
+
*
|
|
570
|
+
* If the number of completed requests exceeds the maximum number of completed requests to keep,
|
|
571
|
+
* remove the oldest completed request.
|
|
572
|
+
*/
|
|
573
|
+
addCompletedTransaction(transaction) {
|
|
574
|
+
this.completedTransactionsQueue.push(transaction);
|
|
575
|
+
if (this.completedTransactionsQueue.length > this.completedTransactionsToKeep) {
|
|
576
|
+
this.completedTransactionsQueue.shift();
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
function ensureCreatedTime(envelope) {
|
|
581
|
+
if (envelope.event === InboxMessageEvent.NewMessage) {
|
|
582
|
+
const message = envelope.data;
|
|
583
|
+
if (!message.created) {
|
|
584
|
+
message.created = (/* @__PURE__ */ new Date()).toISOString();
|
|
585
|
+
}
|
|
586
|
+
return {
|
|
587
|
+
...envelope,
|
|
588
|
+
data: message
|
|
589
|
+
};
|
|
590
|
+
}
|
|
591
|
+
return envelope;
|
|
592
|
+
}
|
|
593
|
+
function fixMessageEventEnvelope(envelope) {
|
|
594
|
+
return ensureCreatedTime(envelope);
|
|
595
|
+
}
|
|
596
|
+
const _CourierInboxSocket = class _CourierInboxSocket extends CourierSocket {
|
|
597
|
+
constructor(options) {
|
|
598
|
+
super(options);
|
|
599
|
+
/**
|
|
600
|
+
* The interval ID for the ping interval.
|
|
601
|
+
*
|
|
602
|
+
* @see https://developer.mozilla.org/en-US/docs/Web/API/Window/setInterval
|
|
603
|
+
*/
|
|
604
|
+
__publicField(this, "pingIntervalId", null);
|
|
605
|
+
/**
|
|
606
|
+
* The list of message event listeners, called when a message event is received
|
|
607
|
+
* from the Courier WebSocket server.
|
|
608
|
+
*/
|
|
609
|
+
__publicField(this, "messageEventListeners", []);
|
|
610
|
+
/** Server-provided configuration for the client. */
|
|
611
|
+
__publicField(this, "config", null);
|
|
612
|
+
/**
|
|
613
|
+
* The transaction manager, used to track outstanding requests and responses.
|
|
614
|
+
*/
|
|
615
|
+
__publicField(this, "pingTransactionManager", new TransactionManager());
|
|
616
|
+
}
|
|
617
|
+
onOpen(_) {
|
|
618
|
+
this.pingTransactionManager.clearOutstandingRequests();
|
|
619
|
+
this.restartPingInterval();
|
|
620
|
+
this.sendGetConfig();
|
|
621
|
+
this.sendSubscribe();
|
|
622
|
+
return Promise.resolve();
|
|
623
|
+
}
|
|
624
|
+
onMessageReceived(data) {
|
|
625
|
+
if ("action" in data && data.action === ServerAction.Ping) {
|
|
626
|
+
const envelope = data;
|
|
627
|
+
this.sendPong(envelope);
|
|
628
|
+
}
|
|
629
|
+
if ("response" in data && data.response === "pong") {
|
|
630
|
+
const envelope = data;
|
|
631
|
+
this.pingTransactionManager.addResponse(envelope.tid, envelope);
|
|
632
|
+
this.pingTransactionManager.clearOutstandingRequests();
|
|
633
|
+
}
|
|
634
|
+
if ("response" in data && data.response === "config") {
|
|
635
|
+
const envelope = data;
|
|
636
|
+
this.setConfig(envelope.data);
|
|
637
|
+
}
|
|
638
|
+
if ("event" in data && _CourierInboxSocket.isInboxMessageEvent(data.event)) {
|
|
639
|
+
const envelope = data;
|
|
640
|
+
const fixedEnvelope = fixMessageEventEnvelope(envelope);
|
|
641
|
+
for (const listener of this.messageEventListeners) {
|
|
642
|
+
listener(fixedEnvelope);
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
this.restartPingInterval();
|
|
646
|
+
return Promise.resolve();
|
|
647
|
+
}
|
|
648
|
+
onClose(_) {
|
|
649
|
+
return Promise.resolve();
|
|
650
|
+
}
|
|
651
|
+
onError(_) {
|
|
652
|
+
return Promise.resolve();
|
|
653
|
+
}
|
|
654
|
+
/**
|
|
655
|
+
* Sends a subscribe message to the server.
|
|
656
|
+
*
|
|
657
|
+
* Subscribes to all events for the user.
|
|
658
|
+
*/
|
|
659
|
+
sendSubscribe() {
|
|
660
|
+
const envelope = {
|
|
661
|
+
tid: UUID.nanoid(),
|
|
662
|
+
action: ClientAction.Subscribe,
|
|
341
663
|
data: {
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
channel: this.options.userId,
|
|
345
|
-
event: "*",
|
|
346
|
-
version: (props == null ? void 0 : props.version) ?? 5
|
|
664
|
+
channel: this.userId,
|
|
665
|
+
event: "*"
|
|
347
666
|
}
|
|
348
667
|
};
|
|
349
|
-
|
|
350
|
-
|
|
668
|
+
this.send(envelope);
|
|
669
|
+
}
|
|
670
|
+
/**
|
|
671
|
+
* Sends an unsubscribe message to the server.
|
|
672
|
+
*
|
|
673
|
+
* Unsubscribes from all events for the user.
|
|
674
|
+
*/
|
|
675
|
+
sendUnsubscribe() {
|
|
676
|
+
const envelope = {
|
|
677
|
+
tid: UUID.nanoid(),
|
|
678
|
+
action: ClientAction.Unsubscribe,
|
|
679
|
+
data: {
|
|
680
|
+
channel: this.userId
|
|
681
|
+
}
|
|
682
|
+
};
|
|
683
|
+
this.send(envelope);
|
|
684
|
+
}
|
|
685
|
+
/**
|
|
686
|
+
* Adds a message event listener, called when a message event is received
|
|
687
|
+
* from the Courier WebSocket server.
|
|
688
|
+
*
|
|
689
|
+
* @param listener The listener function
|
|
690
|
+
*/
|
|
691
|
+
addMessageEventListener(listener) {
|
|
692
|
+
this.messageEventListeners.push(listener);
|
|
693
|
+
}
|
|
694
|
+
/**
|
|
695
|
+
* Send a ping message to the server.
|
|
696
|
+
*
|
|
697
|
+
* ping/pong is implemented at the application layer since the browser's
|
|
698
|
+
* WebSocket implementation does not support control-level ping/pong.
|
|
699
|
+
*/
|
|
700
|
+
sendPing() {
|
|
701
|
+
var _a;
|
|
702
|
+
if (this.pingTransactionManager.outstandingRequests.length >= this.maxOutstandingPings) {
|
|
703
|
+
(_a = this.logger) == null ? void 0 : _a.debug("Max outstanding pings reached, retrying connection.");
|
|
704
|
+
this.close(CLOSE_CODE_NORMAL_CLOSURE, "Max outstanding pings reached, retrying connection.");
|
|
705
|
+
this.retryConnection();
|
|
706
|
+
return;
|
|
707
|
+
}
|
|
708
|
+
const envelope = {
|
|
709
|
+
tid: UUID.nanoid(),
|
|
710
|
+
action: ClientAction.Ping
|
|
711
|
+
};
|
|
712
|
+
this.send(envelope);
|
|
713
|
+
this.pingTransactionManager.addOutstandingRequest(envelope.tid, envelope);
|
|
714
|
+
}
|
|
715
|
+
/**
|
|
716
|
+
* Send a pong response to the server.
|
|
717
|
+
*
|
|
718
|
+
* ping/pong is implemented at the application layer since the browser's
|
|
719
|
+
* WebSocket implementation does not support control-level ping/pong.
|
|
720
|
+
*/
|
|
721
|
+
sendPong(incomingMessage) {
|
|
722
|
+
const response = {
|
|
723
|
+
tid: incomingMessage.tid,
|
|
724
|
+
action: ClientAction.Pong
|
|
725
|
+
};
|
|
726
|
+
this.send(response);
|
|
727
|
+
}
|
|
728
|
+
/**
|
|
729
|
+
* Send a request for the client's configuration.
|
|
730
|
+
*/
|
|
731
|
+
sendGetConfig() {
|
|
732
|
+
const envelope = {
|
|
733
|
+
tid: UUID.nanoid(),
|
|
734
|
+
action: ClientAction.GetConfig
|
|
735
|
+
};
|
|
736
|
+
this.send(envelope);
|
|
737
|
+
}
|
|
738
|
+
/**
|
|
739
|
+
* Restart the ping interval, clearing the previous interval if it exists.
|
|
740
|
+
*/
|
|
741
|
+
restartPingInterval() {
|
|
742
|
+
if (this.pingIntervalId) {
|
|
743
|
+
window.clearInterval(this.pingIntervalId);
|
|
351
744
|
}
|
|
352
|
-
|
|
353
|
-
|
|
745
|
+
this.pingIntervalId = window.setInterval(() => {
|
|
746
|
+
this.sendPing();
|
|
747
|
+
}, this.pingInterval);
|
|
748
|
+
}
|
|
749
|
+
get pingInterval() {
|
|
750
|
+
if (this.config) {
|
|
751
|
+
return this.config.pingInterval * 1e3;
|
|
354
752
|
}
|
|
355
|
-
|
|
356
|
-
await this.send(subscription);
|
|
753
|
+
return _CourierInboxSocket.DEFAULT_PING_INTERVAL_MILLIS;
|
|
357
754
|
}
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
if (options.accessToken) {
|
|
362
|
-
url += `/?auth=${options.accessToken}`;
|
|
755
|
+
get maxOutstandingPings() {
|
|
756
|
+
if (this.config) {
|
|
757
|
+
return this.config.maxOutstandingPings;
|
|
363
758
|
}
|
|
364
|
-
return
|
|
759
|
+
return _CourierInboxSocket.DEFAULT_MAX_OUTSTANDING_PINGS;
|
|
365
760
|
}
|
|
366
|
-
|
|
761
|
+
setConfig(config) {
|
|
762
|
+
this.config = config;
|
|
763
|
+
}
|
|
764
|
+
static isInboxMessageEvent(event) {
|
|
765
|
+
return Object.values(InboxMessageEvent).includes(event);
|
|
766
|
+
}
|
|
767
|
+
};
|
|
768
|
+
/**
|
|
769
|
+
* The default interval in milliseconds at which to send a ping message to the server
|
|
770
|
+
* if no other message has been received from the server.
|
|
771
|
+
*
|
|
772
|
+
* Fallback when the server does not provide a config.
|
|
773
|
+
*/
|
|
774
|
+
__publicField(_CourierInboxSocket, "DEFAULT_PING_INTERVAL_MILLIS", 6e4);
|
|
775
|
+
// 1 minute
|
|
776
|
+
/**
|
|
777
|
+
* The default maximum number of outstanding pings before the client should
|
|
778
|
+
* close the connection and retry connecting.
|
|
779
|
+
*
|
|
780
|
+
* Fallback when the server does not provide a config.
|
|
781
|
+
*/
|
|
782
|
+
__publicField(_CourierInboxSocket, "DEFAULT_MAX_OUTSTANDING_PINGS", 3);
|
|
783
|
+
let CourierInboxSocket = _CourierInboxSocket;
|
|
367
784
|
class InboxClient extends Client {
|
|
368
785
|
constructor(options) {
|
|
369
786
|
super(options);
|
|
370
787
|
__publicField(this, "socket");
|
|
371
|
-
this.socket = new
|
|
788
|
+
this.socket = new CourierInboxSocket(options);
|
|
372
789
|
}
|
|
373
790
|
/**
|
|
374
791
|
* Get paginated messages
|
|
@@ -646,6 +1063,31 @@ class InboxClient extends Client {
|
|
|
646
1063
|
url: this.options.apiUrls.inbox.graphql
|
|
647
1064
|
});
|
|
648
1065
|
}
|
|
1066
|
+
/**
|
|
1067
|
+
* Unarchive a message
|
|
1068
|
+
* @param messageId - ID of the message to unarchive
|
|
1069
|
+
* @returns Promise resolving when message is unarchived
|
|
1070
|
+
*/
|
|
1071
|
+
async unarchive(props) {
|
|
1072
|
+
const query = `
|
|
1073
|
+
mutation TrackEvent {
|
|
1074
|
+
unarchive(messageId: "${props.messageId}")
|
|
1075
|
+
}
|
|
1076
|
+
`;
|
|
1077
|
+
const headers = {
|
|
1078
|
+
"x-courier-user-id": this.options.userId,
|
|
1079
|
+
"Authorization": `Bearer ${this.options.accessToken}`
|
|
1080
|
+
};
|
|
1081
|
+
if (this.options.connectionId) {
|
|
1082
|
+
headers["x-courier-client-source-id"] = this.options.connectionId;
|
|
1083
|
+
}
|
|
1084
|
+
await graphql({
|
|
1085
|
+
options: this.options,
|
|
1086
|
+
query,
|
|
1087
|
+
headers,
|
|
1088
|
+
url: this.options.apiUrls.inbox.graphql
|
|
1089
|
+
});
|
|
1090
|
+
}
|
|
649
1091
|
/**
|
|
650
1092
|
* Archive all read messages.
|
|
651
1093
|
*/
|
|
@@ -669,6 +1111,29 @@ class InboxClient extends Client {
|
|
|
669
1111
|
url: this.options.apiUrls.inbox.graphql
|
|
670
1112
|
});
|
|
671
1113
|
}
|
|
1114
|
+
/**
|
|
1115
|
+
* Archive all read messages.
|
|
1116
|
+
*/
|
|
1117
|
+
async archiveAll() {
|
|
1118
|
+
const query = `
|
|
1119
|
+
mutation TrackEvent {
|
|
1120
|
+
archiveAll
|
|
1121
|
+
}
|
|
1122
|
+
`;
|
|
1123
|
+
const headers = {
|
|
1124
|
+
"x-courier-user-id": this.options.userId,
|
|
1125
|
+
"Authorization": `Bearer ${this.options.accessToken}`
|
|
1126
|
+
};
|
|
1127
|
+
if (this.options.connectionId) {
|
|
1128
|
+
headers["x-courier-client-source-id"] = this.options.connectionId;
|
|
1129
|
+
}
|
|
1130
|
+
await graphql({
|
|
1131
|
+
options: this.options,
|
|
1132
|
+
query,
|
|
1133
|
+
headers,
|
|
1134
|
+
url: this.options.apiUrls.inbox.graphql
|
|
1135
|
+
});
|
|
1136
|
+
}
|
|
672
1137
|
}
|
|
673
1138
|
class PreferenceTransformer {
|
|
674
1139
|
/**
|
|
@@ -983,7 +1448,7 @@ const _Courier = class _Courier {
|
|
|
983
1448
|
/**
|
|
984
1449
|
* The unique identifier for the Courier instance
|
|
985
1450
|
*/
|
|
986
|
-
__publicField(this, "id", UUID.
|
|
1451
|
+
__publicField(this, "id", UUID.nanoid());
|
|
987
1452
|
/**
|
|
988
1453
|
* The Courier client instance
|
|
989
1454
|
*/
|
|
@@ -1025,7 +1490,7 @@ const _Courier = class _Courier {
|
|
|
1025
1490
|
* @param options - The options for the Courier client
|
|
1026
1491
|
*/
|
|
1027
1492
|
signIn(props) {
|
|
1028
|
-
const connectionId = props.connectionId ?? UUID.
|
|
1493
|
+
const connectionId = props.connectionId ?? UUID.nanoid();
|
|
1029
1494
|
this.instanceClient = new CourierClient({ ...props, connectionId });
|
|
1030
1495
|
this.notifyAuthenticationListeners({ userId: props.userId });
|
|
1031
1496
|
}
|
|
@@ -1074,9 +1539,10 @@ export {
|
|
|
1074
1539
|
BrandClient,
|
|
1075
1540
|
Courier,
|
|
1076
1541
|
CourierClient,
|
|
1077
|
-
CourierSocket,
|
|
1078
1542
|
InboxClient,
|
|
1543
|
+
InboxMessageEvent,
|
|
1079
1544
|
ListClient,
|
|
1080
1545
|
PreferenceClient,
|
|
1081
1546
|
TokenClient
|
|
1082
1547
|
};
|
|
1548
|
+
//# sourceMappingURL=index.mjs.map
|