spice-couchbase 2.0.62 → 2.0.63

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/build/index.js CHANGED
@@ -29,6 +29,11 @@ var {
29
29
  extractStatementResources
30
30
  } = require("./utility");
31
31
 
32
+ var {
33
+ createHealthMonitor,
34
+ openHandles
35
+ } = require("./connectionHealth");
36
+
32
37
  if (!Promise.allSettled) {
33
38
  Promise.allSettled = promises => Promise.all(promises.map((promise, i) => promise.then(value => ({
34
39
  status: "fulfilled",
@@ -74,7 +79,26 @@ var optimizeState = {
74
79
  // `auto_optimize_resources`.
75
80
 
76
81
  var OPTIMIZE_ENABLED_RESOURCES = parseResourceList(process.env.SPICE_COUCHBASE_OPTIMIZE_RESOURCES);
77
- var autoOptimizePolicy = null;
82
+ var autoOptimizePolicy = null; // One health monitor per process, shared by every copy of this module that
83
+ // ends up loaded. See connectionHealth.js.
84
+
85
+ var HEALTH_MONITOR_KEY = Symbol.for("spice-couchbase.health-monitor");
86
+ var health = globalThis[HEALTH_MONITOR_KEY] || (globalThis[HEALTH_MONITOR_KEY] = createHealthMonitor()); // Cluster handles live on the `spice.database` globals, which a reconnect
87
+ // replaces. Instances read them at call time instead of copying them.
88
+
89
+ var HANDLE_GLOBALS = {
90
+ cluster: "clusters",
91
+ connection: "connections",
92
+ scope: "scopes",
93
+ bucket_handle: "buckets"
94
+ };
95
+
96
+ function globalHandle(kind, connection_name) {
97
+ var _spice, _spice$database, _spice$database$HANDL;
98
+
99
+ if (typeof spice === "undefined") return undefined;
100
+ return (_spice = spice) === null || _spice === void 0 ? void 0 : (_spice$database = _spice.database) === null || _spice$database === void 0 ? void 0 : (_spice$database$HANDL = _spice$database[HANDLE_GLOBALS[kind]]) === null || _spice$database$HANDL === void 0 ? void 0 : _spice$database$HANDL[connection_name];
101
+ }
78
102
 
79
103
  function requestedAutoOptimize(query_options) {
80
104
  var _query_options$auto_o;
@@ -163,15 +187,12 @@ module.exports = class Database {
163
187
  } = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
164
188
 
165
189
  if (connection) {
166
- var _spice, _spice$db;
190
+ var _spice2, _spice2$db;
167
191
 
168
192
  this.connection_name = connection;
169
- this.connection = spice.database.connections[connection];
170
193
  this.query_scope = query_scope;
171
- this.cluster = spice.database.clusters[connection];
172
194
  this.bucket = this.connection._scope._bucket._name;
173
195
  this.db_config = spice.config.database.connections[connection];
174
- this.collection_manager = spice.database.buckets[connection].collections();
175
196
  this.auto_create_collection = auto_create_collection || (collection || spice.config.database.connections[connection].collection) != undefined && (collection || spice.config.database.connections[connection].collection) != "default" && (scope || spice.config.database.connections[connection].scope) != "_default";
176
197
  this.collection_name = collection || spice.config.database.connections[connection].collection || "_default";
177
198
  /* console.log(
@@ -182,14 +203,11 @@ module.exports = class Database {
182
203
  );*/
183
204
 
184
205
  this.scope_name = scope || spice.config.database.connections[connection].scope || "_default";
185
- this.scope = spice.database.scopes[connection];
186
206
  this.full_text_index = spice.config.database.connections[connection].full_text_index || full_text_index;
187
207
  this.N1qlQuery = couchbase.N1qlQuery;
188
208
  this.SearchQuery = couchbase.SearchQuery;
189
- this.collection = this.scope.collection(this.collection_name);
190
- this.userManager = this.getUserManager(connection);
191
209
 
192
- if (!((_spice = spice) === null || _spice === void 0 ? void 0 : (_spice$db = _spice.db) === null || _spice$db === void 0 ? void 0 : _spice$db.collections)) {
210
+ if (!((_spice2 = spice) === null || _spice2 === void 0 ? void 0 : (_spice2$db = _spice2.db) === null || _spice2$db === void 0 ? void 0 : _spice2$db.collections)) {
193
211
  spice.db = spice.db || {};
194
212
  spice.db.collections = new Set();
195
213
  }
@@ -204,6 +222,168 @@ module.exports = class Database {
204
222
  this.auto_optimize = typeof configured_auto_optimize === "boolean" ? configured_auto_optimize : undefined;
205
223
  this.enabled_resources = parseResourceList([...parseResourceList(this.db_config.auto_optimize_resources), ...parseResourceList(auto_optimize_resources)]);
206
224
  }
225
+ } // Handles resolve from the globals on every access, so an instance created
226
+ // before a reconnect uses the new cluster afterwards. Assigning one pins it
227
+ // on that instance instead (tests, custom wiring).
228
+
229
+
230
+ pinHandle(kind, value) {
231
+ if (!Object.prototype.hasOwnProperty.call(this, "_pinned_handles")) {
232
+ Object.defineProperty(this, "_pinned_handles", {
233
+ value: {},
234
+ enumerable: false,
235
+ writable: true
236
+ });
237
+ }
238
+
239
+ this._pinned_handles[kind] = value;
240
+ }
241
+
242
+ pinnedHandle(kind) {
243
+ var pinned = this._pinned_handles;
244
+ return pinned && kind in pinned ? {
245
+ value: pinned[kind]
246
+ } : null;
247
+ }
248
+
249
+ handle(kind) {
250
+ var pinned = this.pinnedHandle(kind);
251
+ return pinned ? pinned.value : globalHandle(kind, this.connection_name);
252
+ }
253
+
254
+ get cluster() {
255
+ return this.handle("cluster");
256
+ }
257
+
258
+ set cluster(value) {
259
+ this.pinHandle("cluster", value);
260
+ }
261
+
262
+ get connection() {
263
+ return this.handle("connection");
264
+ }
265
+
266
+ set connection(value) {
267
+ this.pinHandle("connection", value);
268
+ }
269
+
270
+ get scope() {
271
+ return this.handle("scope");
272
+ }
273
+
274
+ set scope(value) {
275
+ this.pinHandle("scope", value);
276
+ }
277
+
278
+ get collection() {
279
+ var _this$_collection_cac;
280
+
281
+ var pinned = this.pinnedHandle("collection");
282
+ if (pinned) return pinned.value;
283
+ var scope = this.scope;
284
+ if (!scope) return undefined; // Collection handles are cheap but not free; cache one per scope object,
285
+ // which changes exactly when the connection is replaced.
286
+
287
+ if (((_this$_collection_cac = this._collection_cache) === null || _this$_collection_cac === void 0 ? void 0 : _this$_collection_cac.scope) !== scope) {
288
+ Object.defineProperty(this, "_collection_cache", {
289
+ value: {
290
+ scope,
291
+ collection: scope.collection(this.collection_name)
292
+ },
293
+ enumerable: false,
294
+ writable: true,
295
+ configurable: true
296
+ });
297
+ }
298
+
299
+ return this._collection_cache.collection;
300
+ }
301
+
302
+ set collection(value) {
303
+ this.pinHandle("collection", value);
304
+ }
305
+
306
+ get collection_manager() {
307
+ var _this$handle;
308
+
309
+ var pinned = this.pinnedHandle("collection_manager");
310
+ if (pinned) return pinned.value;
311
+ return (_this$handle = this.handle("bucket_handle")) === null || _this$handle === void 0 ? void 0 : _this$handle.collections();
312
+ }
313
+
314
+ set collection_manager(value) {
315
+ this.pinHandle("collection_manager", value);
316
+ }
317
+
318
+ get userManager() {
319
+ var _this$cluster;
320
+
321
+ var pinned = this.pinnedHandle("userManager");
322
+ if (pinned) return pinned.value;
323
+ return (_this$cluster = this.cluster) === null || _this$cluster === void 0 ? void 0 : _this$cluster.users();
324
+ }
325
+
326
+ set userManager(value) {
327
+ this.pinHandle("userManager", value);
328
+ }
329
+ /**
330
+ * Reports a finished operation to the health monitor. Only connectivity
331
+ * errors (timeouts, cancelled requests, service not available) count.
332
+ */
333
+
334
+
335
+ recordOutcome(error) {
336
+ health.recordOutcome(this.connection_name, error);
337
+ }
338
+
339
+ recordSettled(results) {
340
+ for (var result of results) {
341
+ if ((result === null || result === void 0 ? void 0 : result.status) === "rejected") this.recordOutcome(result.reason);
342
+ }
343
+ }
344
+
345
+ observe(operation) {
346
+ var _this = this;
347
+
348
+ return _asyncToGenerator(function* () {
349
+ try {
350
+ return yield operation;
351
+ } catch (error) {
352
+ _this.recordOutcome(error);
353
+
354
+ throw error;
355
+ }
356
+ })();
357
+ }
358
+ /** Per-connection health: healthy, reconnecting, generation, last error. */
359
+
360
+
361
+ static healthStatus() {
362
+ return health.status();
363
+ }
364
+ /** Probes a connection now and reconnects it if the probe fails. */
365
+
366
+
367
+ static checkHealth() {
368
+ var connection_name = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : "default";
369
+ return health.check(connection_name);
370
+ }
371
+ /** Opens a fresh cluster for a connection and swaps it in. */
372
+
373
+
374
+ static forceReconnect() {
375
+ var connection_name = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : "default";
376
+ return health.reconnect(connection_name);
377
+ }
378
+ /**
379
+ * Stops probing and reconnecting and closes replaced clusters still in
380
+ * their grace period. Call before closing `spice.database.clusters` on
381
+ * shutdown so a reconnect cannot race it.
382
+ */
383
+
384
+
385
+ static stopHealthMonitor() {
386
+ return health.stop();
207
387
  }
208
388
  /**
209
389
  * Lets the application decide per resource whether slow statements may
@@ -309,25 +489,15 @@ module.exports = class Database {
309
489
  }
310
490
 
311
491
  connect(connection_name, config) {
312
- var _this = this;
313
-
314
492
  return _asyncToGenerator(function* () {
315
- var connectOptions = {
316
- username: config.username,
317
- password: config.password
318
- };
319
-
320
- if (config.configProfile) {
321
- connectOptions.configProfile = config.configProfile;
322
- }
493
+ var {
494
+ cluster,
495
+ bucket,
496
+ scope,
497
+ collection
498
+ } = yield openHandles(config); // Watch the connection from here on and replace it if it wedges.
323
499
 
324
- var cluster = yield couchbase.connect(config.nodes, connectOptions);
325
- var bucket = cluster.bucket(config.bucket);
326
- var collection;
327
- var scope_name = _this.scope_name || config.scope || "_default";
328
- var collection_name = _this.collection_name || config.collection || "_default";
329
- var scope = bucket.scope(scope_name);
330
- collection = bucket.scope(scope_name).collection(collection_name || "_default");
500
+ health.register(connection_name, config);
331
501
  return {
332
502
  connection: connection_name,
333
503
  bucket,
@@ -339,7 +509,7 @@ module.exports = class Database {
339
509
  }
340
510
 
341
511
  getUserManager(connection) {
342
- return spice.database.clusters[connection].users();
512
+ return globalHandle("cluster", connection).users();
343
513
  }
344
514
 
345
515
  assignUserPermission() {
@@ -412,9 +582,9 @@ module.exports = class Database {
412
582
 
413
583
  return _asyncToGenerator(function* () {
414
584
  try {
415
- var _spice2, _spice2$db, _spice2$db$collection;
585
+ var _spice3, _spice3$db, _spice3$db$collection;
416
586
 
417
- if ((_spice2 = spice) === null || _spice2 === void 0 ? void 0 : (_spice2$db = _spice2.db) === null || _spice2$db === void 0 ? void 0 : (_spice2$db$collection = _spice2$db.collections) === null || _spice2$db$collection === void 0 ? void 0 : _spice2$db$collection.has(collection_name)) {
587
+ if ((_spice3 = spice) === null || _spice3 === void 0 ? void 0 : (_spice3$db = _spice3.db) === null || _spice3$db === void 0 ? void 0 : (_spice3$db$collection = _spice3$db.collections) === null || _spice3$db$collection === void 0 ? void 0 : _spice3$db$collection.has(collection_name)) {
418
588
  return true;
419
589
  } else {
420
590
  var response = yield _this4.cluster.query("SELECT RAW keyspace_id\n FROM system:keyspaces\n WHERE bucket_id = ?\n AND scope_id = ?\n AND keyspace_id = ?\n LIMIT 1", {
@@ -525,7 +695,7 @@ module.exports = class Database {
525
695
  var collection = _this9.use_bucket_name ? _this9.connection : _this9.collection;
526
696
 
527
697
  if (!useLookupIn) {
528
- var response = yield collection.get(doc_id);
698
+ var response = yield _this9.observe(collection.get(doc_id));
529
699
  return _objectSpread({
530
700
  id: doc_id
531
701
  }, response.content);
@@ -543,7 +713,10 @@ module.exports = class Database {
543
713
  var specs = batch.map(col => couchbase.LookupInSpec.get(col));
544
714
  return collection.lookupIn(doc_id, specs);
545
715
  });
546
- var batchResults = yield Promise.allSettled(batchPromises); // Merge results from all batches
716
+ var batchResults = yield Promise.allSettled(batchPromises);
717
+
718
+ _this9.recordSettled(batchResults); // Merge results from all batches
719
+
547
720
 
548
721
  batchResults.forEach((result, batchIndex) => {
549
722
  if (result.status === "fulfilled" && result.value) {
@@ -567,9 +740,9 @@ module.exports = class Database {
567
740
 
568
741
  return _asyncToGenerator(function* () {
569
742
  try {
570
- yield (_this10.use_bucket_name ? _this10.connection : _this10.collection).insert(doc_id, document, {
743
+ yield _this10.observe((_this10.use_bucket_name ? _this10.connection : _this10.collection).insert(doc_id, document, {
571
744
  expiry: expiry
572
- });
745
+ }));
573
746
  } catch (e) {
574
747
  console.log(e.message, e, doc_id);
575
748
  }
@@ -583,7 +756,7 @@ module.exports = class Database {
583
756
 
584
757
  var expiry = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0;
585
758
  return _asyncToGenerator(function* () {
586
- yield (_this11.use_bucket_name ? _this11.connection : _this11.collection).touch(doc_id, expiry);
759
+ yield _this11.observe((_this11.use_bucket_name ? _this11.connection : _this11.collection).touch(doc_id, expiry));
587
760
  return yield _this11.get(doc_id);
588
761
  })();
589
762
  }
@@ -592,9 +765,9 @@ module.exports = class Database {
592
765
  var _this12 = this;
593
766
 
594
767
  return _asyncToGenerator(function* () {
595
- var res = yield (_this12.use_bucket_name ? _this12.connection : _this12.collection).counter(doc_id, adjustment, {
768
+ var res = yield _this12.observe((_this12.use_bucket_name ? _this12.connection : _this12.collection).counter(doc_id, adjustment, {
596
769
  initial: initial_value
597
- });
770
+ }));
598
771
  return res.value;
599
772
  })();
600
773
  }
@@ -603,9 +776,9 @@ module.exports = class Database {
603
776
  var _this13 = this;
604
777
 
605
778
  return _asyncToGenerator(function* () {
606
- var res = yield (_this13.use_bucket_name ? _this13.connection : _this13.collection).replace(doc_id, document, {
779
+ var res = yield _this13.observe((_this13.use_bucket_name ? _this13.connection : _this13.collection).replace(doc_id, document, {
607
780
  expiry: expiry
608
- });
781
+ }));
609
782
  return res.value;
610
783
  })();
611
784
  }
@@ -614,7 +787,7 @@ module.exports = class Database {
614
787
  var _this14 = this;
615
788
 
616
789
  return _asyncToGenerator(function* () {
617
- var res = yield (_this14.use_bucket_name ? _this14.connection : _this14.collection).remove(doc_id);
790
+ var res = yield _this14.observe((_this14.use_bucket_name ? _this14.connection : _this14.collection).remove(doc_id));
618
791
  return res.value;
619
792
  })();
620
793
  }
@@ -641,6 +814,8 @@ module.exports = class Database {
641
814
 
642
815
  var returned_all = yield Promise.allSettled(functions);
643
816
 
817
+ _this15.recordSettled(returned_all);
818
+
644
819
  var responses = _.flatten(_.map(returned_all, returned_obj => returned_obj === null || returned_obj === void 0 ? void 0 : returned_obj.value));
645
820
 
646
821
  var _final_responses = _.compact(_.map(responses, (res, index) => {
@@ -679,7 +854,10 @@ module.exports = class Database {
679
854
  var specs = batch.map(col => couchbase.LookupInSpec.get(col));
680
855
  return collection.lookupIn(key, specs);
681
856
  });
682
- var batchResults = yield Promise.allSettled(batchPromises); // Merge results from all batches
857
+ var batchResults = yield Promise.allSettled(batchPromises);
858
+
859
+ _this15.recordSettled(batchResults); // Merge results from all batches
860
+
683
861
 
684
862
  batchResults.forEach((result, batchIndex) => {
685
863
  if (result.status === "fulfilled" && result.value) {
@@ -1106,7 +1284,7 @@ module.exports = class Database {
1106
1284
 
1107
1285
  var startTime = process.hrtime.bigint();
1108
1286
  var options = buildQueryOptions(query_options);
1109
- var response = yield exec_scope.query(sanitizedQuery, options); // End time
1287
+ var response = yield _this20.observe(exec_scope.query(sanitizedQuery, options)); // End time
1110
1288
 
1111
1289
  var endTime = process.hrtime.bigint(); // Calculate duration in microseconds
1112
1290
 
package/package.json CHANGED
@@ -1,13 +1,14 @@
1
1
  {
2
2
  "name": "spice-couchbase",
3
- "version": "2.0.62",
3
+ "version": "2.0.63",
4
4
  "description": "",
5
5
  "main": "build/index.js",
6
6
  "scripts": {
7
7
  "test": "node src/utility.test.js",
8
8
  "test:auto-optimize": "node --test src/index.auto-optimize.test.js",
9
9
  "test:query-consistency": "node src/index.query-consistency.test.js",
10
- "test:count-query": "node src/index.count-query.test.js"
10
+ "test:count-query": "node src/index.count-query.test.js",
11
+ "test:connection-health": "node --test src/connectionHealth.test.js"
11
12
  },
12
13
  "author": "Chad Fraser <chad@sonover.com> (http://sonover.com)",
13
14
  "license": "ISC",