Skip to content

Commit a4d4ce1

Browse files
committed
feat(retry-policy): retry policy now applies to all requests
The configured retry policy will be used for any failed request. feat(retry-policy): exponential retry policy used by default for subscribe By default, the SDK is configured to use an exponential retry policy for failed subscribe requests. fix(subscribe): add missing `subscription change` status `PNSubscriptionChangedCategory` will be emitted each time the list of channels and groups is changing. refactor(network): request retries moved to the network Automated request retry has been moved into the network layer to handle all requests (not only subscribed). test(cleanup): make proper clean up after each test case Properly destroy `PubNub` instance after each test case to make sure that all connections closed and prevent tests from hanging.
1 parent bee8794 commit a4d4ce1

File tree

91 files changed

+2033
-2523
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

91 files changed

+2033
-2523
lines changed

.mocharc.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
{
22
"require": "tsx",
3+
"file": ["test/setup-why.ts"],
34
"spec": "test/**/*.test.ts",
45
"exclude": [
56
"test/dist/*.{js,ts}",
67
"test/feature/*.{js,ts}"
78
],
89
"timeout": 5000,
910
"reporter": "spec"
10-
}
11+
}

dist/web/pubnub.js

Lines changed: 465 additions & 584 deletions
Large diffs are not rendered by default.

dist/web/pubnub.min.js

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

lib/core/components/configuration.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
99
};
1010
Object.defineProperty(exports, "__esModule", { value: true });
1111
exports.makeConfiguration = void 0;
12+
const retryPolicy_1 = require("./retryPolicy");
1213
const uuid_1 = __importDefault(require("./uuid"));
1314
// --------------------------------------------------------
1415
// ----------------------- Defaults -----------------------
@@ -32,6 +33,24 @@ const USE_RANDOM_INITIALIZATION_VECTOR = true;
3233
*/
3334
const makeConfiguration = (base, setupCryptoModule) => {
3435
var _a, _b, _c;
36+
// Set default retry policy for subscribe (if new subscribe logic not used).
37+
if (!base.retryConfiguration && base.enableEventEngine) {
38+
base.retryConfiguration = retryPolicy_1.RetryPolicy.ExponentialRetryPolicy({
39+
minimumDelay: 2,
40+
maximumDelay: 150,
41+
maximumRetry: 6,
42+
excluded: [
43+
retryPolicy_1.Endpoint.MessageSend,
44+
retryPolicy_1.Endpoint.Presence,
45+
retryPolicy_1.Endpoint.Files,
46+
retryPolicy_1.Endpoint.MessageStorage,
47+
retryPolicy_1.Endpoint.ChannelGroups,
48+
retryPolicy_1.Endpoint.DevicePushNotifications,
49+
retryPolicy_1.Endpoint.AppContext,
50+
retryPolicy_1.Endpoint.MessageReactions,
51+
],
52+
});
53+
}
3554
// Ensure that retry policy has proper configuration (if has been set).
3655
(_a = base.retryConfiguration) === null || _a === void 0 ? void 0 : _a.validate();
3756
(_b = base.useRandomIVs) !== null && _b !== void 0 ? _b : (base.useRandomIVs = USE_RANDOM_INITIALIZATION_VECTOR);

lib/core/components/retryPolicy.js

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
"use strict";
2+
var __importDefault = (this && this.__importDefault) || function (mod) {
3+
return (mod && mod.__esModule) ? mod : { "default": mod };
4+
};
5+
Object.defineProperty(exports, "__esModule", { value: true });
6+
exports.RetryPolicy = exports.Endpoint = void 0;
7+
const categories_1 = __importDefault(require("../constants/categories"));
8+
// --------------------------------------------------------
9+
// ------------------------ Types -------------------------
10+
// --------------------------------------------------------
11+
// region Types
12+
/**
13+
* List of known endpoint groups (by context).
14+
*/
15+
var Endpoint;
16+
(function (Endpoint) {
17+
/**
18+
* Unknown endpoint.
19+
*
20+
* @internal
21+
*/
22+
Endpoint["Unknown"] = "UnknownEndpoint";
23+
/**
24+
* The endpoints to send messages.
25+
*/
26+
Endpoint["MessageSend"] = "MessageSendEndpoint";
27+
/**
28+
* The endpoint for real-time update retrieval.
29+
*/
30+
Endpoint["Subscribe"] = "SubscribeEndpoint";
31+
/**
32+
* The endpoint to access and manage `user_id` presence and fetch channel presence information.
33+
*/
34+
Endpoint["Presence"] = "PresenceEndpoint";
35+
/**
36+
* The endpoint to access and manage files in channel-specific storage.
37+
*/
38+
Endpoint["Files"] = "FilesEndpoint";
39+
/**
40+
* The endpoint to access and manage messages for a specific channel(s) in the persistent storage.
41+
*/
42+
Endpoint["MessageStorage"] = "MessageStorageEndpoint";
43+
/**
44+
* The endpoint to access and manage channel groups.
45+
*/
46+
Endpoint["ChannelGroups"] = "ChannelGroupsEndpoint";
47+
/**
48+
* The endpoint to access and manage device registration for channel push notifications.
49+
*/
50+
Endpoint["DevicePushNotifications"] = "DevicePushNotificationsEndpoint";
51+
/**
52+
* The endpoint to access and manage App Context objects.
53+
*/
54+
Endpoint["AppContext"] = "AppContextEndpoint";
55+
/**
56+
* The endpoint to access and manage reactions for a specific message.
57+
*/
58+
Endpoint["MessageReactions"] = "MessageReactionsEndpoint";
59+
})(Endpoint || (exports.Endpoint = Endpoint = {}));
60+
// endregion
61+
/**
62+
* Failed request retry policy.
63+
*/
64+
class RetryPolicy {
65+
static LinearRetryPolicy(configuration) {
66+
var _a;
67+
return {
68+
delay: configuration.delay,
69+
maximumRetry: configuration.maximumRetry,
70+
excluded: (_a = configuration.excluded) !== null && _a !== void 0 ? _a : [],
71+
shouldRetry(request, response, error, attempt) {
72+
return isRetriableRequest(request, response, error, attempt !== null && attempt !== void 0 ? attempt : 0, this.maximumRetry, this.excluded);
73+
},
74+
getDelay(_, response) {
75+
let delay = -1;
76+
if (response && response.headers['retry-after'] !== undefined)
77+
delay = parseInt(response.headers['retry-after'], 10);
78+
if (delay === -1)
79+
delay = this.delay;
80+
return (delay + Math.random()) * 1000;
81+
},
82+
validate() {
83+
if (this.delay < 2)
84+
throw new Error('Delay can not be set less than 2 seconds for retry');
85+
if (this.maximumRetry > 10)
86+
throw new Error('Maximum retry for linear retry policy can not be more than 10');
87+
},
88+
};
89+
}
90+
static ExponentialRetryPolicy(configuration) {
91+
var _a;
92+
return {
93+
minimumDelay: configuration.minimumDelay,
94+
maximumDelay: configuration.maximumDelay,
95+
maximumRetry: configuration.maximumRetry,
96+
excluded: (_a = configuration.excluded) !== null && _a !== void 0 ? _a : [],
97+
shouldRetry(request, response, error, attempt) {
98+
return isRetriableRequest(request, response, error, attempt !== null && attempt !== void 0 ? attempt : 0, this.maximumRetry, this.excluded);
99+
},
100+
getDelay(attempt, response) {
101+
let delay = -1;
102+
if (response && response.headers['retry-after'] !== undefined)
103+
delay = parseInt(response.headers['retry-after'], 10);
104+
if (delay === -1)
105+
delay = Math.min(Math.pow(2, attempt), this.maximumDelay);
106+
return (delay + Math.random()) * 1000;
107+
},
108+
validate() {
109+
if (this.minimumDelay < 2)
110+
throw new Error('Minimum delay can not be set less than 2 seconds for retry');
111+
else if (this.maximumDelay > 150)
112+
throw new Error('Maximum delay can not be set more than 150 seconds for' + ' retry');
113+
else if (this.maximumRetry > 6)
114+
throw new Error('Maximum retry for exponential retry policy can not be more than 6');
115+
},
116+
};
117+
}
118+
}
119+
exports.RetryPolicy = RetryPolicy;
120+
/**
121+
* Check whether request can be retried or not.
122+
*
123+
* @param req - Request for which retry ability is checked.
124+
* @param res - Service response which should be taken into consideration.
125+
* @param errorCategory - Request processing error category.
126+
* @param retryAttempt - Current retry attempt.
127+
* @param maximumRetry - Maximum retry attempts count according to the retry policy.
128+
* @param excluded - List of endpoints for which retry policy won't be applied.
129+
*
130+
* @return `true` if request can be retried.
131+
*
132+
* @internal
133+
*/
134+
const isRetriableRequest = (req, res, errorCategory, retryAttempt, maximumRetry, excluded) => {
135+
if (errorCategory && errorCategory === categories_1.default.PNCancelledCategory)
136+
return false;
137+
else if (isExcludedRequest(req, excluded))
138+
return false;
139+
else if (retryAttempt > maximumRetry)
140+
return false;
141+
return res ? res.status === 429 || res.status >= 500 : true;
142+
};
143+
/**
144+
* Check whether the provided request is in the list of endpoints for which retry is not allowed or not.
145+
*
146+
* @param req - Request which will be tested.
147+
* @param excluded - List of excluded endpoints configured for retry policy.
148+
*
149+
* @returns `true` if request has been excluded and shouldn't be retried.
150+
*
151+
* @internal
152+
*/
153+
const isExcludedRequest = (req, excluded) => excluded && excluded.length > 0 ? excluded.includes(endpointFromRequest(req)) : false;
154+
/**
155+
* Identify API group from transport request.
156+
*
157+
* @param req - Request for which `path` will be analyzed to identify REST API group.
158+
*
159+
* @returns Endpoint group to which request belongs.
160+
*
161+
* @internal
162+
*/
163+
const endpointFromRequest = (req) => {
164+
let endpoint = Endpoint.Unknown;
165+
if (req.path.startsWith('/v2/subscribe'))
166+
endpoint = Endpoint.Subscribe;
167+
else if (req.path.startsWith('/publish/') || req.path.startsWith('/signal/'))
168+
endpoint = Endpoint.MessageSend;
169+
else if (req.path.startsWith('/v2/presence'))
170+
endpoint = Endpoint.Presence;
171+
else if (req.path.startsWith('/v2/history') || req.path.startsWith('/v3/history'))
172+
endpoint = Endpoint.MessageStorage;
173+
else if (req.path.startsWith('/v1/message-actions/'))
174+
endpoint = Endpoint.MessageReactions;
175+
else if (req.path.startsWith('/v1/channel-registration/'))
176+
endpoint = Endpoint.ChannelGroups;
177+
else if (req.path.startsWith('/v2/objects/'))
178+
endpoint = Endpoint.ChannelGroups;
179+
else if (req.path.startsWith('/v1/push/') || req.path.startsWith('/v2/push/'))
180+
endpoint = Endpoint.DevicePushNotifications;
181+
else if (req.path.startsWith('/v1/files/'))
182+
endpoint = Endpoint.Files;
183+
return endpoint;
184+
};

lib/core/constants/categories.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,10 @@ var StatusCategory;
6969
* PubNub client connected to the real-time updates stream.
7070
*/
7171
StatusCategory["PNConnectedCategory"] = "PNConnectedCategory";
72+
/**
73+
* Set of active channels and groups has been changed.
74+
*/
75+
StatusCategory["PNSubscriptionChangedCategory"] = "PNSubscriptionChangedCategory";
7276
/**
7377
* Received real-time updates exceed specified threshold.
7478
*

lib/core/endpoints/presence/heartbeat.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ const utils_1 = require("../../utils");
2929
*/
3030
class HeartbeatRequest extends request_1.AbstractRequest {
3131
constructor(parameters) {
32-
super();
32+
super({ cancellable: true });
3333
this.parameters = parameters;
3434
}
3535
operation() {

lib/core/pubnub-common.js

Lines changed: 48 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -64,9 +64,9 @@ const categories_1 = __importDefault(require("./constants/categories"));
6464
// endregion
6565
const pubnub_error_1 = require("../errors/pubnub-error");
6666
const pubnub_api_error_1 = require("../errors/pubnub-api-error");
67+
const retryPolicy_1 = require("./components/retryPolicy");
6768
// region Event Engine
6869
const presence_1 = require("../event-engine/presence/presence");
69-
const retryPolicy_1 = require("../event-engine/core/retryPolicy");
7070
const event_engine_1 = require("../event-engine");
7171
// endregion
7272
// region Publish & Signal
@@ -198,7 +198,6 @@ class PubNubCore {
198198
else
199199
setTimeout(resolve, heartbeatInterval * 1000);
200200
}),
201-
retryDelay: (amount) => new Promise((resolve) => setTimeout(resolve, amount)),
202201
emitStatus: (status) => this.listenerManager.announceStatus(status),
203202
config: this._configuration,
204203
presenceState: this.presenceState,
@@ -212,6 +211,8 @@ class PubNubCore {
212211
join: this.join.bind(this),
213212
leave: this.leave.bind(this),
214213
leaveAll: this.leaveAll.bind(this),
214+
presenceReconnect: this.presenceReconnect.bind(this),
215+
presenceDisconnect: this.presenceDisconnect.bind(this),
215216
presenceState: this.presenceState,
216217
config: this._configuration,
217218
emitMessages: (events) => {
@@ -636,6 +637,10 @@ class PubNubCore {
636637
}
637638
else if (this.eventEngine)
638639
this.eventEngine.dispose();
640+
if (process.env.PRESENCE_MODULE !== 'disabled') {
641+
if (this.presenceEventEngine)
642+
this.presenceEventEngine.dispose();
643+
}
639644
}
640645
}
641646
/**
@@ -946,13 +951,17 @@ class PubNubCore {
946951
}
947952
/**
948953
* Temporarily disconnect from real-time events stream.
954+
*
955+
* **Note:** `isOffline` is set to `true` only when client experience network issues.
956+
*
957+
* @param [isOffline] - Whether `offline` presence should be notified or not.
949958
*/
950-
disconnect() {
959+
disconnect(isOffline) {
951960
if (process.env.SUBSCRIBE_MODULE !== 'disabled') {
952961
if (this.subscriptionManager)
953962
this.subscriptionManager.disconnect();
954963
else if (this.eventEngine)
955-
this.eventEngine.disconnect();
964+
this.eventEngine.disconnect(isOffline);
956965
}
957966
else
958967
throw new Error('Disconnection error: subscription module disabled');
@@ -1363,6 +1372,24 @@ class PubNubCore {
13631372
else
13641373
throw new Error('Announce UUID Presence error: presence module disabled');
13651374
}
1375+
/**
1376+
* Reconnect presence event engine after network issues.
1377+
*
1378+
* @param parameters - List of channels and groups where `join` event should be sent.
1379+
*
1380+
* @internal
1381+
*/
1382+
presenceReconnect(parameters) {
1383+
if (process.env.PRESENCE_MODULE !== 'disabled') {
1384+
if (this.presenceEventEngine)
1385+
this.presenceEventEngine.reconnect();
1386+
else {
1387+
this.heartbeat(Object.assign(Object.assign({ channels: parameters.channels, channelGroups: parameters.groups }, (this._configuration.maintainPresenceState && { state: this.presenceState })), { heartbeat: this._configuration.getPresenceTimeout() }), () => { });
1388+
}
1389+
}
1390+
else
1391+
throw new Error('Announce UUID Presence error: presence module disabled');
1392+
}
13661393
// endregion
13671394
// region Leave
13681395
/**
@@ -1400,6 +1427,23 @@ class PubNubCore {
14001427
else
14011428
throw new Error('Announce UUID Leave error: presence module disabled');
14021429
}
1430+
/**
1431+
* Announce user `leave` on disconnection.
1432+
*
1433+
* @internal
1434+
*
1435+
* @param parameters - List of channels and groups where `leave` event should be sent.
1436+
*/
1437+
presenceDisconnect(parameters) {
1438+
if (process.env.PRESENCE_MODULE !== 'disabled') {
1439+
if (this.presenceEventEngine)
1440+
this.presenceEventEngine.disconnect(parameters.isOffline);
1441+
else if (!parameters.isOffline)
1442+
this.makeUnsubscribe({ channels: parameters.channels, channelGroups: parameters.groups }, () => { });
1443+
}
1444+
else
1445+
throw new Error('Announce UUID Leave error: presence module disabled');
1446+
}
14031447
/**
14041448
* Grant token permission.
14051449
*

0 commit comments

Comments
 (0)