@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/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
- const _CourierSocket = class _CourierSocket {
5
- constructor(url, options) {
6
- // Properties
7
- __publicField(this, "webSocket", null);
8
- __publicField(this, "pingInterval", null);
9
- // Callbacks
10
- __publicField(this, "onOpen");
11
- __publicField(this, "onMessageReceived");
12
- __publicField(this, "onClose");
13
- __publicField(this, "onError");
14
- // Properties
15
- __publicField(this, "url");
16
- __publicField(this, "options");
17
- this.url = url;
18
- this.options = options;
19
- }
20
- /**
21
- * Dynamically checks if the WebSocket is connected
22
- */
23
- get isConnected() {
24
- return this.webSocket !== null;
25
- }
26
- async connect() {
27
- this.disconnect();
28
- return new Promise((resolve, reject) => {
29
- try {
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.com"
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 UUID {
136
- static generate(prefix) {
137
- const id = Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
138
- return prefix ? prefix + id : id;
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.generate() : void 0;
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.generate() : void 0;
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
- class InboxSocket extends CourierSocket {
259
+ const CLOSE_CODE_NORMAL_CLOSURE = 1e3;
260
+ const IPW_VERSION = "v1";
261
+ const _CourierSocket = class _CourierSocket {
309
262
  constructor(options) {
310
- const url = InboxSocket.buildUrl(options);
311
- super(url, options);
312
- __publicField(this, "receivedMessage");
313
- __publicField(this, "receivedMessageEvent");
314
- this.onMessageReceived = (data) => this.convertToType(data);
315
- }
316
- convertToType(data) {
317
- var _a, _b, _c, _d;
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 payload = JSON.parse(data);
320
- switch (payload.type) {
321
- case "event":
322
- const messageEvent = JSON.parse(data);
323
- (_a = this.receivedMessageEvent) == null ? void 0 : _a.call(this, messageEvent);
324
- break;
325
- case "message":
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
- (_c = this.options.logger) == null ? void 0 : _c.error("Error parsing socket message", error);
332
- if (error instanceof Error) {
333
- (_d = this.onError) == null ? void 0 : _d.call(this, error);
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
- async sendSubscribe(props) {
338
- var _a;
339
- const subscription = {
340
- action: "subscribe",
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
- userAgent: "courier-js",
343
- // TODO: Equivalent to Courier.agent.value()
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
- if (this.options.connectionId) {
350
- subscription.data.clientSourceId = this.options.connectionId;
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
- if (this.options.tenantId) {
353
- subscription.data.accountId = this.options.tenantId;
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
- (_a = this.options.logger) == null ? void 0 : _a.debug("Sending subscribe request", subscription);
356
- await this.send(subscription);
753
+ return _CourierInboxSocket.DEFAULT_PING_INTERVAL_MILLIS;
357
754
  }
358
- static buildUrl(options) {
359
- var _a;
360
- let url = ((_a = options.apiUrls) == null ? void 0 : _a.inbox.webSocket) ?? "";
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 url;
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 InboxSocket(options);
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.generate());
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.generate();
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