@aicore/cocodb-ws-client 1.0.9 → 1.0.11
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/package.json +8 -8
- package/src/utils/client.js +127 -37
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aicore/cocodb-ws-client",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.11",
|
|
4
4
|
"description": "Websocket client for cocoDb",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -47,23 +47,23 @@
|
|
|
47
47
|
},
|
|
48
48
|
"homepage": "https://github.com/aicore/cocoDbWsClient#readme",
|
|
49
49
|
"devDependencies": {
|
|
50
|
-
"@commitlint/cli": "17.4.
|
|
51
|
-
"@commitlint/config-conventional": "17.4.
|
|
52
|
-
"c8": "7.
|
|
50
|
+
"@commitlint/cli": "17.4.3",
|
|
51
|
+
"@commitlint/config-conventional": "17.4.3",
|
|
52
|
+
"c8": "7.13.0",
|
|
53
53
|
"chai": "4.3.7",
|
|
54
54
|
"cli-color": "2.0.3",
|
|
55
55
|
"documentation": "14.0.1",
|
|
56
|
-
"eslint": "8.
|
|
57
|
-
"glob": "8.0
|
|
56
|
+
"eslint": "8.34.0",
|
|
57
|
+
"glob": "8.1.0",
|
|
58
58
|
"husky": "8.0.3",
|
|
59
59
|
"mocha": "10.2.0"
|
|
60
60
|
},
|
|
61
61
|
"dependencies": {
|
|
62
62
|
"@aicore/libcommonutils": "1.0.19",
|
|
63
|
-
"ws": "8.12.
|
|
63
|
+
"ws": "8.12.1"
|
|
64
64
|
},
|
|
65
65
|
"optionalDependencies": {
|
|
66
66
|
"bufferutil": "4.0.7",
|
|
67
|
-
"utf-8-validate": "
|
|
67
|
+
"utf-8-validate": "6.0.2"
|
|
68
68
|
}
|
|
69
69
|
}
|
package/src/utils/client.js
CHANGED
|
@@ -3,11 +3,16 @@ import {isString, isObject, isStringEmpty, COCO_DB_FUNCTIONS} from "@aicore/libc
|
|
|
3
3
|
|
|
4
4
|
let client = null,
|
|
5
5
|
cocoDBEndPointURL = null,
|
|
6
|
-
cocoAuthKey = null
|
|
6
|
+
cocoAuthKey = null,
|
|
7
|
+
hibernateTimer = null,
|
|
8
|
+
bufferRequests = false,
|
|
9
|
+
pendingSendMessages = [];
|
|
10
|
+
const MAX_PENDING_SEND_BUFFER_SIZE = 2000;
|
|
7
11
|
const WEBSOCKET_ENDPOINT_COCO_DB = '/ws/';
|
|
8
|
-
const ID_TO_RESOLVE_REJECT_MAP =
|
|
12
|
+
const ID_TO_RESOLVE_REJECT_MAP = new Map();
|
|
9
13
|
const CONNECT_BACKOFF_TIME_MS = [1, 500, 1000, 3000, 5000, 10000, 20000];
|
|
10
|
-
|
|
14
|
+
const INACTIVITY_TIME_FOR_HIBERNATE = 8000;
|
|
15
|
+
let id = 0, activityInHibernateInterval = 0;
|
|
11
16
|
|
|
12
17
|
let currentBackoffIndex = 0;
|
|
13
18
|
function _resetBackoffTime() {
|
|
@@ -23,6 +28,42 @@ function _getBackoffTime() {
|
|
|
23
28
|
// @INCLUDE_IN_API_DOCS
|
|
24
29
|
|
|
25
30
|
|
|
31
|
+
function _checkActivityForHibernation() {
|
|
32
|
+
if(activityInHibernateInterval > 0){
|
|
33
|
+
activityInHibernateInterval = 0;
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
if(!client || client.hibernating
|
|
37
|
+
|| !client.connectionEstablished // cant hibernate if connection isnt already established/ is being establised
|
|
38
|
+
|| ID_TO_RESOLVE_REJECT_MAP.size > 0){ // if there are any pending responses, we cant hibernate
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
// hibernate
|
|
42
|
+
client.hibernating = true;
|
|
43
|
+
client.hibernatingPromise = new Promise((resolve=>{
|
|
44
|
+
client.hibernatingPromiseResolve = resolve;
|
|
45
|
+
}));
|
|
46
|
+
bufferRequests = true;
|
|
47
|
+
client.terminate();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* returns a promise that resolves when hibernation ends.
|
|
52
|
+
* @return {Promise<unknown>}
|
|
53
|
+
* @private
|
|
54
|
+
*/
|
|
55
|
+
function _toAwakeFromHibernate() {
|
|
56
|
+
return client.hibernatingPromise;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function _wakeupHibernatingClient() {
|
|
60
|
+
if(client.hibernatingPromiseResolved) {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
client.hibernatingPromiseResolve();
|
|
64
|
+
client.hibernatingPromiseResolved = true;
|
|
65
|
+
}
|
|
66
|
+
|
|
26
67
|
/**
|
|
27
68
|
* Sets up the websocket client and returns a promise that will be resolved when the connection is closed or broken.
|
|
28
69
|
*
|
|
@@ -41,7 +82,9 @@ function _setupClientAndWaitForClose(connectedCb) {
|
|
|
41
82
|
client.on('open', function open() {
|
|
42
83
|
console.log('connected to server');
|
|
43
84
|
client.connectionEstablished = true;
|
|
85
|
+
bufferRequests = false;
|
|
44
86
|
connectedCb && connectedCb();
|
|
87
|
+
_sendPendingMessages();
|
|
45
88
|
});
|
|
46
89
|
|
|
47
90
|
client.on('message', function message(data) {
|
|
@@ -51,10 +94,9 @@ function _setupClientAndWaitForClose(connectedCb) {
|
|
|
51
94
|
function _connectionTerminated(reason) {
|
|
52
95
|
console.log(reason);
|
|
53
96
|
client.connectionEstablished = false;
|
|
54
|
-
for (let sequenceNumber
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
delete ID_TO_RESOLVE_REJECT_MAP[sequenceNumber];
|
|
97
|
+
for (let [sequenceNumber, handler] of ID_TO_RESOLVE_REJECT_MAP) {
|
|
98
|
+
handler.reject(reason);
|
|
99
|
+
ID_TO_RESOLVE_REJECT_MAP.delete(sequenceNumber);
|
|
58
100
|
}
|
|
59
101
|
resolve();
|
|
60
102
|
}
|
|
@@ -96,15 +138,26 @@ async function _setupAndMaintainConnection(firstConnectionCb, neverConnectedCB)
|
|
|
96
138
|
if(firstConnectionCb){
|
|
97
139
|
firstConnectionCb("connected");
|
|
98
140
|
firstConnectionCb = null;
|
|
141
|
+
// setup hibernate timer on first connection
|
|
142
|
+
activityInHibernateInterval = 1;
|
|
143
|
+
hibernateTimer = setInterval(_checkActivityForHibernation, INACTIVITY_TIME_FOR_HIBERNATE);
|
|
99
144
|
}
|
|
100
145
|
}
|
|
101
146
|
while(!client || !client.userClosedConnection){
|
|
102
147
|
await _setupClientAndWaitForClose(connected);
|
|
148
|
+
if(client && client.hibernating && !client.userClosedConnection){
|
|
149
|
+
await _toAwakeFromHibernate();
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
103
152
|
if(!client || !client.userClosedConnection){
|
|
104
153
|
await _backoffTimer(_getBackoffTime());
|
|
105
154
|
}
|
|
106
155
|
}
|
|
107
|
-
|
|
156
|
+
if(hibernateTimer){
|
|
157
|
+
clearInterval(hibernateTimer);
|
|
158
|
+
hibernateTimer = null;
|
|
159
|
+
}
|
|
160
|
+
client && client.userClosedConnectionCB && client.userClosedConnectionCB();
|
|
108
161
|
client = cocoDBEndPointURL = cocoAuthKey = null;
|
|
109
162
|
id = 0;
|
|
110
163
|
if(neverConnectedCB){
|
|
@@ -114,10 +167,16 @@ async function _setupAndMaintainConnection(firstConnectionCb, neverConnectedCB)
|
|
|
114
167
|
|
|
115
168
|
/**
|
|
116
169
|
* Create a connection to the cocoDbServiceEndPoint and listens for messages. The connection will
|
|
117
|
-
* be maintained and it will try to automatically re-establish broken connections if there are network issues.
|
|
170
|
+
* be maintained and, it will try to automatically re-establish broken connections if there are network issues.
|
|
118
171
|
* You need to await on this function before staring to use any db APIs. Any APIs called while the connection is
|
|
119
172
|
* not fully setup will throw an error.
|
|
120
173
|
*
|
|
174
|
+
* ## Hibernation after inactivity
|
|
175
|
+
* After around 10 seconds of no send activity and if there are no outstanding requests, the db connection will be
|
|
176
|
+
* dropped and the client move into a hibernation state. The connection will be immediately re-established on any
|
|
177
|
+
* db activity transparently, though a slight jitter may be observed during the connection establishment time. This
|
|
178
|
+
* auto start-stop will save database resources as servers can be on for months on end.
|
|
179
|
+
*
|
|
121
180
|
* @param {string} cocoDbServiceEndPoint - The URL of the coco-db service.
|
|
122
181
|
* @param {string} authKey - The authKey is a base64 encoded string of the username and password.
|
|
123
182
|
* @return {Promise<null>} Resolves when the cocodb client is ready to send/receive requests for the first time.
|
|
@@ -159,10 +218,18 @@ export function close() {
|
|
|
159
218
|
currentClient.closePromise = new Promise((resolve)=>{
|
|
160
219
|
currentClient.userClosedConnection = true;
|
|
161
220
|
currentClient.userClosedConnectionCB = function () {
|
|
221
|
+
for(let entry of pendingSendMessages){
|
|
222
|
+
entry.reject();
|
|
223
|
+
}
|
|
224
|
+
pendingSendMessages = [];
|
|
162
225
|
resolve();
|
|
163
226
|
};
|
|
164
227
|
_cancelBackoffTimer(); // this is for if the connection is broken and, we are retrying the connection
|
|
165
|
-
currentClient.
|
|
228
|
+
if(currentClient.hibernating){
|
|
229
|
+
_wakeupHibernatingClient();
|
|
230
|
+
} else {
|
|
231
|
+
currentClient.terminate();
|
|
232
|
+
}
|
|
166
233
|
});
|
|
167
234
|
return currentClient.closePromise;
|
|
168
235
|
}
|
|
@@ -176,6 +243,39 @@ function getId() {
|
|
|
176
243
|
return id.toString(16);
|
|
177
244
|
}
|
|
178
245
|
|
|
246
|
+
function _sendMessage(message, resolve, reject) {
|
|
247
|
+
if (!client) {
|
|
248
|
+
reject('Please call init before sending message');
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
if (!client.connectionEstablished) {
|
|
252
|
+
reject('Db connection is not ready, please retry in some time');
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
if (!isObject(message)) {
|
|
256
|
+
reject('Please provide valid Object');
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
if (!isString(message.fn) || !(message.fn in COCO_DB_FUNCTIONS)) {
|
|
260
|
+
reject('please provide valid function name');
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
const sequenceNumber = getId();
|
|
264
|
+
message.id = sequenceNumber;
|
|
265
|
+
ID_TO_RESOLVE_REJECT_MAP.set(sequenceNumber, {
|
|
266
|
+
resolve: resolve,
|
|
267
|
+
reject: reject
|
|
268
|
+
});
|
|
269
|
+
activityInHibernateInterval++;
|
|
270
|
+
client.send(JSON.stringify(message));
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function _sendPendingMessages() {
|
|
274
|
+
for(let entry of pendingSendMessages){
|
|
275
|
+
_sendMessage(entry.message, entry.resolve, entry.reject);
|
|
276
|
+
}
|
|
277
|
+
pendingSendMessages = [];
|
|
278
|
+
}
|
|
179
279
|
|
|
180
280
|
/**
|
|
181
281
|
* It takes a message object, sends it to the server, and returns a promise that resolves when the server responds
|
|
@@ -183,30 +283,21 @@ function getId() {
|
|
|
183
283
|
* @returns {Promise} A function that returns a promise.
|
|
184
284
|
*/
|
|
185
285
|
export function sendMessage(message) {
|
|
286
|
+
// make a copy as the user may start modifying the object while we are sending it.
|
|
287
|
+
message = structuredClone(message);
|
|
186
288
|
return new Promise(function (resolve, reject) {
|
|
187
|
-
if
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
}
|
|
199
|
-
if (!isString(message.fn) || !(message.fn in COCO_DB_FUNCTIONS)) {
|
|
200
|
-
reject('please provide valid function name');
|
|
201
|
-
return;
|
|
289
|
+
if(bufferRequests){
|
|
290
|
+
if(pendingSendMessages.length > MAX_PENDING_SEND_BUFFER_SIZE){
|
|
291
|
+
reject('Too many requests sent while waking up from hibernation');
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
pendingSendMessages.push({message, resolve, reject});
|
|
295
|
+
if(client&& client.hibernating){
|
|
296
|
+
_wakeupHibernatingClient();
|
|
297
|
+
}
|
|
298
|
+
} else {
|
|
299
|
+
_sendMessage(message, resolve, reject);
|
|
202
300
|
}
|
|
203
|
-
const sequenceNumber = getId();
|
|
204
|
-
message.id = sequenceNumber;
|
|
205
|
-
ID_TO_RESOLVE_REJECT_MAP[sequenceNumber] = {
|
|
206
|
-
resolve: resolve,
|
|
207
|
-
reject: reject
|
|
208
|
-
};
|
|
209
|
-
client.send(JSON.stringify(message));
|
|
210
301
|
});
|
|
211
302
|
}
|
|
212
303
|
|
|
@@ -225,15 +316,14 @@ export function __receiveMessage(rawData) {
|
|
|
225
316
|
console.error('Server message does not have an Id');
|
|
226
317
|
return false;
|
|
227
318
|
}
|
|
228
|
-
const
|
|
229
|
-
if (!isObject(
|
|
319
|
+
const requestHandler = ID_TO_RESOLVE_REJECT_MAP.get(message.id);
|
|
320
|
+
if (!isObject(requestHandler)) {
|
|
230
321
|
//TODO: Emit metrics
|
|
231
322
|
|
|
232
323
|
console.error(`Client did not send message with Id ${message.id} to server`);
|
|
233
324
|
return false;
|
|
234
325
|
}
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
delete ID_TO_RESOLVE_REJECT_MAP[message.id];
|
|
326
|
+
requestHandler.resolve(message.response);
|
|
327
|
+
ID_TO_RESOLVE_REJECT_MAP.delete(message.id);
|
|
238
328
|
return true;
|
|
239
329
|
}
|