@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.
Files changed (2) hide show
  1. package/package.json +8 -8
  2. 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.9",
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.1",
51
- "@commitlint/config-conventional": "17.4.0",
52
- "c8": "7.12.0",
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.31.0",
57
- "glob": "8.0.3",
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.0"
63
+ "ws": "8.12.1"
64
64
  },
65
65
  "optionalDependencies": {
66
66
  "bufferutil": "4.0.7",
67
- "utf-8-validate": "5.0.10"
67
+ "utf-8-validate": "6.0.2"
68
68
  }
69
69
  }
@@ -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
- let id = 0;
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 in ID_TO_RESOLVE_REJECT_MAP) {
55
- let rejectHandler = ID_TO_RESOLVE_REJECT_MAP[sequenceNumber].reject;
56
- rejectHandler(reason);
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
- client.userClosedConnectionCB && client.userClosedConnectionCB();
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.terminate();
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 (!client) {
188
- reject('Please call init before sending message');
189
- return;
190
- }
191
- if (!client.connectionEstablished) {
192
- reject('Db connection is not ready, please retry in some time');
193
- return;
194
- }
195
- if (!isObject(message)) {
196
- reject('Please provide valid Object');
197
- return;
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 requestResolve = ID_TO_RESOLVE_REJECT_MAP[message.id];
229
- if (!isObject(requestResolve)) {
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
- const response = message.response;
236
- requestResolve.resolve(response);
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
  }