spice-couchbase 2.0.61 → 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.
@@ -0,0 +1,573 @@
1
+ "use strict";
2
+
3
+ function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); keys.push.apply(keys, symbols); } return keys; }
4
+
5
+ function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys(Object(source), true).forEach(function (key) { _defineProperty(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; }
6
+
7
+ function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
8
+
9
+ function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { Promise.resolve(value).then(_next, _throw); } }
10
+
11
+ function _asyncToGenerator(fn) { return function () { var self = this, args = arguments; return new Promise(function (resolve, reject) { var gen = fn.apply(self, args); function _next(value) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value); } function _throw(err) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err); } _next(undefined); }); }; }
12
+
13
+ var couchbase = require("couchbase"); // Connection health monitor and self-healing reconnect.
14
+ //
15
+ // The cluster object is created once at boot and stored on the `spice.database`
16
+ // globals. When the SDK wedges (every operation times out, but the SDK never
17
+ // recovers on its own) the only remedy used to be a process restart. This
18
+ // module notices that state and opens a fresh cluster in-process:
19
+ //
20
+ // 1. Passive signal: every driver operation reports its outcome. Enough
21
+ // connectivity errors (timeouts, cancelled requests, service not
22
+ // available) inside a sliding window trigger a probe straight away.
23
+ // 2. Active probe: on an interval, a KV `exists` and a `SELECT 1`, each with
24
+ // a short timeout and a hard deadline in case the SDK ignores it.
25
+ // 3. Reconnect: after repeated probe failures, build a new cluster from the
26
+ // same config, verify it, then swap it into the globals and retire the old
27
+ // one after a grace period. Single-flight, with exponential backoff.
28
+ // 4. Escalation: if the connection stays unhealthy for too long, terminate
29
+ // the process so the supervisor (PM2) restarts it.
30
+
31
+
32
+ function envNumber(name, fallback) {
33
+ var raw = process.env[name];
34
+
35
+ if (raw === undefined || raw === null || String(raw).trim() === "") {
36
+ return fallback;
37
+ }
38
+
39
+ var value = Number(raw);
40
+ return Number.isFinite(value) && value >= 0 ? value : fallback;
41
+ }
42
+
43
+ function defaultSettings() {
44
+ return {
45
+ // 0 disables the interval probe (the passive signal still works).
46
+ healthIntervalMs: envNumber("SPICE_COUCHBASE_HEALTH_INTERVAL_MS", 15000),
47
+ probeTimeoutMs: envNumber("SPICE_COUCHBASE_PROBE_TIMEOUT_MS", 3000),
48
+ // Consecutive failed probes before the connection is declared unhealthy.
49
+ probeFailureThreshold: Math.max(1, envNumber("SPICE_COUCHBASE_PROBE_FAILURE_THRESHOLD", 2)),
50
+ // Connectivity errors inside the window that trigger an immediate probe.
51
+ failureThreshold: Math.max(1, envNumber("SPICE_COUCHBASE_FAILURE_THRESHOLD", 5)),
52
+ failureWindowMs: envNumber("SPICE_COUCHBASE_FAILURE_WINDOW_MS", 30000),
53
+ reconnectTimeoutMs: envNumber("SPICE_COUCHBASE_RECONNECT_TIMEOUT_MS", 20000),
54
+ reconnectBackoffMinMs: envNumber("SPICE_COUCHBASE_RECONNECT_BACKOFF_MIN_MS", 5000),
55
+ reconnectBackoffMaxMs: envNumber("SPICE_COUCHBASE_RECONNECT_BACKOFF_MAX_MS", 60000),
56
+ // How long a replaced cluster stays open so in-flight operations settle.
57
+ retireGraceMs: envNumber("SPICE_COUCHBASE_RETIRE_GRACE_MS", 30000),
58
+ // 0 disables the escalation.
59
+ exitAfterMs: envNumber("SPICE_COUCHBASE_EXIT_AFTER_MS", 300000)
60
+ };
61
+ }
62
+
63
+ var CONNECTIVITY_ERRORS = [couchbase.TimeoutError, couchbase.RequestCanceledError, couchbase.ServiceNotAvailableError].filter(type => typeof type === "function");
64
+
65
+ function isConnectivityError(error) {
66
+ if (!error || typeof error !== "object") return false;
67
+ if (CONNECTIVITY_ERRORS.some(type => error instanceof type)) return true; // Our own deadlines, and SDK errors that crossed a module boundary.
68
+
69
+ return error.spiceConnectivity === true || ["TimeoutError", "AmbiguousTimeoutError", "UnambiguousTimeoutError", "RequestCanceledError", "ServiceNotAvailableError"].includes(error.name);
70
+ }
71
+
72
+ function deadline(promise, ms, label) {
73
+ var timer;
74
+ return Promise.race([promise, new Promise((_resolve, reject) => {
75
+ var _timer$unref, _timer;
76
+
77
+ timer = setTimeout(() => {
78
+ var error = new Error("".concat(label, " timed out after ").concat(ms, "ms"));
79
+ error.spiceConnectivity = true;
80
+ reject(error);
81
+ }, ms);
82
+ (_timer$unref = (_timer = timer).unref) === null || _timer$unref === void 0 ? void 0 : _timer$unref.call(_timer);
83
+ })]).finally(() => clearTimeout(timer));
84
+ }
85
+
86
+ function describe(error) {
87
+ return (error === null || error === void 0 ? void 0 : error.message) || String(error);
88
+ }
89
+
90
+ function closeQuietly(_x, _x2, _x3) {
91
+ return _closeQuietly.apply(this, arguments);
92
+ }
93
+
94
+ function _closeQuietly() {
95
+ _closeQuietly = _asyncToGenerator(function* (cluster, log, label) {
96
+ try {
97
+ var _cluster$close;
98
+
99
+ yield cluster === null || cluster === void 0 ? void 0 : (_cluster$close = cluster.close) === null || _cluster$close === void 0 ? void 0 : _cluster$close.call(cluster);
100
+ } catch (error) {
101
+ log.warn("[spice-couchbase] Closing ".concat(label, " failed: ").concat(describe(error)));
102
+ }
103
+ });
104
+ return _closeQuietly.apply(this, arguments);
105
+ }
106
+
107
+ function buildConnectOptions() {
108
+ var config = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
109
+ var options = {
110
+ username: config.username,
111
+ password: config.password
112
+ };
113
+
114
+ if (config.configProfile) {
115
+ options.configProfile = config.configProfile;
116
+ }
117
+
118
+ var timeouts = {};
119
+
120
+ for (var key of ["kvTimeout", "kvDurableTimeout", "queryTimeout", "searchTimeout", "analyticsTimeout", "managementTimeout", "connectTimeout", "bootstrapTimeout", "resolveTimeout"]) {
121
+ var _config$timeouts;
122
+
123
+ var raw = (_config$timeouts = config.timeouts) === null || _config$timeouts === void 0 ? void 0 : _config$timeouts[key];
124
+ if (raw === undefined || raw === null || raw === "") continue;
125
+ var value = Number(raw);
126
+ if (Number.isFinite(value) && value > 0) timeouts[key] = value;
127
+ }
128
+
129
+ if (Object.keys(timeouts).length > 0) {
130
+ options.timeouts = timeouts;
131
+ }
132
+
133
+ return options;
134
+ }
135
+ /**
136
+ * Opens a cluster and resolves the bucket, scope and collection handles for a
137
+ * connection config. Shared by the boot-time connect and the reconnect path.
138
+ */
139
+
140
+
141
+ function openHandles(_x4) {
142
+ return _openHandles.apply(this, arguments);
143
+ }
144
+
145
+ function _openHandles() {
146
+ _openHandles = _asyncToGenerator(function* (config) {
147
+ var {
148
+ connect = couchbase.connect
149
+ } = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
150
+ var cluster = yield connect(config.nodes, buildConnectOptions(config));
151
+ var bucket = cluster.bucket(config.bucket);
152
+ var scope = bucket.scope(config.scope || "_default");
153
+ var collection = scope.collection(config.collection || "_default");
154
+ return {
155
+ cluster,
156
+ bucket,
157
+ scope,
158
+ collection
159
+ };
160
+ });
161
+ return _openHandles.apply(this, arguments);
162
+ }
163
+
164
+ function createHealthMonitor() {
165
+ var {
166
+ settings: overrides = {},
167
+ connect = couchbase.connect,
168
+ now = () => Date.now(),
169
+ random = Math.random,
170
+ log = console,
171
+ getGlobals = () => typeof spice !== "undefined" ? spice : undefined,
172
+ terminate
173
+ } = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
174
+
175
+ var settings = _objectSpread({}, defaultSettings(), {}, overrides);
176
+
177
+ var states = new Map();
178
+ var stopped = false;
179
+
180
+ var terminateProcess = terminate || (() => {
181
+ var _fallback$unref;
182
+
183
+ // SIGTERM runs the application's graceful shutdown when it has one,
184
+ // and terminates the process when it does not. The fallback covers a
185
+ // shutdown that hangs on the very connection that is stuck.
186
+ process.exitCode = 1;
187
+ var fallback = setTimeout(() => process.exit(1), 15000);
188
+ (_fallback$unref = fallback.unref) === null || _fallback$unref === void 0 ? void 0 : _fallback$unref.call(fallback);
189
+ process.kill(process.pid, "SIGTERM");
190
+ });
191
+
192
+ function stateFor(name) {
193
+ var state = states.get(name);
194
+
195
+ if (!state) {
196
+ state = {
197
+ name,
198
+ config: null,
199
+ generation: 1,
200
+ healthy: true,
201
+ unhealthySince: null,
202
+ failures: [],
203
+ probeFailures: 0,
204
+ probing: null,
205
+ reconnecting: null,
206
+ reconnectAttempts: 0,
207
+ nextReconnectAt: 0,
208
+ lastError: null,
209
+ lastReconnectAt: null,
210
+ timer: null,
211
+ retired: new Set(),
212
+ terminated: false
213
+ };
214
+ states.set(name, state);
215
+ }
216
+
217
+ return state;
218
+ }
219
+
220
+ function currentHandles(name) {
221
+ var _getGlobals, _database$clusters, _database$buckets, _database$scopes, _database$connections;
222
+
223
+ var database = (_getGlobals = getGlobals()) === null || _getGlobals === void 0 ? void 0 : _getGlobals.database;
224
+ if (!database) return null;
225
+ return {
226
+ cluster: (_database$clusters = database.clusters) === null || _database$clusters === void 0 ? void 0 : _database$clusters[name],
227
+ bucket: (_database$buckets = database.buckets) === null || _database$buckets === void 0 ? void 0 : _database$buckets[name],
228
+ scope: (_database$scopes = database.scopes) === null || _database$scopes === void 0 ? void 0 : _database$scopes[name],
229
+ collection: (_database$connections = database.connections) === null || _database$connections === void 0 ? void 0 : _database$connections[name]
230
+ };
231
+ }
232
+
233
+ function probe(_x5) {
234
+ return _probe.apply(this, arguments);
235
+ }
236
+
237
+ function _probe() {
238
+ _probe = _asyncToGenerator(function* (handles) {
239
+ if (!(handles === null || handles === void 0 ? void 0 : handles.cluster) || !(handles === null || handles === void 0 ? void 0 : handles.collection)) {
240
+ var error = new Error("No cluster handles to probe");
241
+ error.spiceConnectivity = true;
242
+ throw error;
243
+ }
244
+
245
+ var timeout = settings.probeTimeoutMs; // The hard deadline is the point: a wedged SDK may never honour its own.
246
+
247
+ yield deadline(Promise.all([handles.collection.exists("__spice_health_probe__", {
248
+ timeout
249
+ }), handles.cluster.query("SELECT 1", {
250
+ timeout
251
+ })]), timeout + 1000, "Couchbase health probe");
252
+ });
253
+ return _probe.apply(this, arguments);
254
+ }
255
+
256
+ function markHealthy(state) {
257
+ state.probeFailures = 0;
258
+ state.failures = [];
259
+ state.reconnectAttempts = 0;
260
+ state.nextReconnectAt = 0;
261
+
262
+ if (!state.healthy) {
263
+ var downFor = state.unhealthySince ? now() - state.unhealthySince : 0;
264
+ log.log("[spice-couchbase] Connection \"".concat(state.name, "\" healthy again ") + "(generation ".concat(state.generation, ", unhealthy for ").concat(downFor, "ms)"));
265
+ }
266
+
267
+ state.healthy = true;
268
+ state.unhealthySince = null;
269
+ state.lastError = null;
270
+ }
271
+
272
+ function markUnhealthy(state, error) {
273
+ state.lastError = describe(error);
274
+
275
+ if (state.healthy) {
276
+ state.healthy = false;
277
+ state.unhealthySince = now();
278
+ log.warn("[spice-couchbase] Connection \"".concat(state.name, "\" unhealthy: ").concat(state.lastError));
279
+ }
280
+ }
281
+
282
+ function escalateIfStuck(state) {
283
+ if (state.terminated || stopped || !settings.exitAfterMs || !state.unhealthySince || now() - state.unhealthySince < settings.exitAfterMs) {
284
+ return;
285
+ }
286
+
287
+ state.terminated = true;
288
+ log.error("[spice-couchbase] Connection \"".concat(state.name, "\" has been unhealthy for ") + "".concat(now() - state.unhealthySince, "ms after ").concat(state.reconnectAttempts, " ") + "failed reconnect(s). Terminating so the supervisor restarts the process.");
289
+ terminateProcess();
290
+ }
291
+
292
+ function retire(state, cluster) {
293
+ var _timer$unref2;
294
+
295
+ if (!cluster) return;
296
+ state.retired.add(cluster);
297
+ var timer = setTimeout(() => {
298
+ state.retired.delete(cluster);
299
+ closeQuietly(cluster, log, "retired cluster for \"".concat(state.name, "\""));
300
+ }, settings.retireGraceMs);
301
+ (_timer$unref2 = timer.unref) === null || _timer$unref2 === void 0 ? void 0 : _timer$unref2.call(timer);
302
+ }
303
+
304
+ function backoffDelay(attempt) {
305
+ var base = Math.min(settings.reconnectBackoffMaxMs, settings.reconnectBackoffMinMs * 2 ** Math.max(0, attempt - 1)); // +/- 20% jitter so a fleet of pods does not reconnect in lockstep.
306
+
307
+ return Math.round(base * (0.8 + random() * 0.4));
308
+ }
309
+
310
+ function doReconnect(_x6) {
311
+ return _doReconnect.apply(this, arguments);
312
+ }
313
+
314
+ function _doReconnect() {
315
+ _doReconnect = _asyncToGenerator(function* (state) {
316
+ var _database$clusters2;
317
+
318
+ var started = now();
319
+ var attempt = state.reconnectAttempts + 1;
320
+ log.warn("[spice-couchbase] Reconnecting \"".concat(state.name, "\" (attempt ").concat(attempt, ")"));
321
+ var abandoned = false;
322
+ var handles;
323
+
324
+ try {
325
+ var _handles$bucket;
326
+
327
+ var opening = openHandles(state.config, {
328
+ connect
329
+ }); // If the connect outlives the deadline, close whatever it produces.
330
+
331
+ opening.then(late => {
332
+ if (abandoned) closeQuietly(late.cluster, log, "abandoned cluster");
333
+ }).catch(() => {});
334
+ handles = yield deadline(opening, settings.reconnectTimeoutMs, "Couchbase reconnect");
335
+
336
+ if (typeof ((_handles$bucket = handles.bucket) === null || _handles$bucket === void 0 ? void 0 : _handles$bucket.waitUntilReady) === "function") {
337
+ var _couchbase$ServiceTyp, _couchbase$ServiceTyp2;
338
+
339
+ var serviceTypes = [(_couchbase$ServiceTyp = couchbase.ServiceType) === null || _couchbase$ServiceTyp === void 0 ? void 0 : _couchbase$ServiceTyp.KeyValue, (_couchbase$ServiceTyp2 = couchbase.ServiceType) === null || _couchbase$ServiceTyp2 === void 0 ? void 0 : _couchbase$ServiceTyp2.Query].filter(Boolean);
340
+ yield deadline(handles.bucket.waitUntilReady(settings.reconnectTimeoutMs, serviceTypes.length ? {
341
+ serviceTypes
342
+ } : undefined), settings.reconnectTimeoutMs + 1000, "Couchbase waitUntilReady");
343
+ }
344
+
345
+ yield probe(handles);
346
+ } catch (error) {
347
+ abandoned = true;
348
+ if (handles) closeQuietly(handles.cluster, log, "unverified cluster");
349
+ state.reconnectAttempts = attempt;
350
+ var delay = backoffDelay(attempt);
351
+ state.nextReconnectAt = now() + delay;
352
+ state.lastError = describe(error);
353
+ log.error("[spice-couchbase] Reconnect of \"".concat(state.name, "\" failed: ") + "".concat(state.lastError, ". Next attempt in ~").concat(delay, "ms"));
354
+ return false;
355
+ }
356
+
357
+ if (stopped) {
358
+ closeQuietly(handles.cluster, log, "cluster opened during shutdown");
359
+ return false;
360
+ }
361
+
362
+ var globals = getGlobals();
363
+ var database = globals === null || globals === void 0 ? void 0 : globals.database;
364
+ var previous = database === null || database === void 0 ? void 0 : (_database$clusters2 = database.clusters) === null || _database$clusters2 === void 0 ? void 0 : _database$clusters2[state.name];
365
+
366
+ if (database) {
367
+ database.clusters[state.name] = handles.cluster;
368
+ database.buckets[state.name] = handles.bucket;
369
+ database.scopes[state.name] = handles.scope;
370
+ database.connections[state.name] = handles.collection;
371
+ } // Collection existence was learnt through the old cluster; re-learn it.
372
+
373
+
374
+ if (globals) {
375
+ var _globals$db, _globals$db$collectio, _globals$db$collectio2;
376
+
377
+ globals.checked_collections = {};
378
+ (_globals$db = globals.db) === null || _globals$db === void 0 ? void 0 : (_globals$db$collectio = _globals$db.collections) === null || _globals$db$collectio === void 0 ? void 0 : (_globals$db$collectio2 = _globals$db$collectio.clear) === null || _globals$db$collectio2 === void 0 ? void 0 : _globals$db$collectio2.call(_globals$db$collectio);
379
+ }
380
+
381
+ state.generation += 1;
382
+ state.lastReconnectAt = now();
383
+ if (previous && previous !== handles.cluster) retire(state, previous);
384
+ log.log("[spice-couchbase] Reconnected \"".concat(state.name, "\" ") + "(generation ".concat(state.generation, ", took ").concat(now() - started, "ms)"));
385
+ markHealthy(state);
386
+ return true;
387
+ });
388
+ return _doReconnect.apply(this, arguments);
389
+ }
390
+
391
+ function reconnect(name) {
392
+ var state = stateFor(name);
393
+ if (stopped || !state.config) return Promise.resolve(false);
394
+
395
+ if (!state.reconnecting) {
396
+ state.reconnecting = doReconnect(state).catch(error => {
397
+ log.error("[spice-couchbase] Reconnect of \"".concat(name, "\" crashed: ").concat(describe(error)));
398
+ return false;
399
+ }).finally(() => {
400
+ state.reconnecting = null;
401
+ });
402
+ }
403
+
404
+ return state.reconnecting;
405
+ }
406
+
407
+ function doCheck(_x7) {
408
+ return _doCheck.apply(this, arguments);
409
+ }
410
+ /** Probes the connection now. Single-flight per connection. */
411
+
412
+
413
+ function _doCheck() {
414
+ _doCheck = _asyncToGenerator(function* (state) {
415
+ var {
416
+ suspected = false
417
+ } = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
418
+
419
+ try {
420
+ yield probe(currentHandles(state.name));
421
+ markHealthy(state);
422
+ return true;
423
+ } catch (error) {
424
+ state.probeFailures += 1; // A failed probe that follows a burst of operation timeouts is two
425
+ // independent signals, so it does not wait for a second probe.
426
+
427
+ if (suspected || state.probeFailures >= settings.probeFailureThreshold) {
428
+ markUnhealthy(state, error);
429
+ }
430
+
431
+ if (!state.healthy) {
432
+ if (!state.reconnecting && now() >= state.nextReconnectAt) {
433
+ yield reconnect(state.name);
434
+ }
435
+
436
+ escalateIfStuck(state);
437
+ }
438
+
439
+ return false;
440
+ }
441
+ });
442
+ return _doCheck.apply(this, arguments);
443
+ }
444
+
445
+ function check(name, options) {
446
+ var state = stateFor(name);
447
+ if (stopped || !state.config) return Promise.resolve(state.healthy);
448
+ if (state.reconnecting) return state.reconnecting;
449
+
450
+ if (!state.probing) {
451
+ state.probing = doCheck(state, options).finally(() => {
452
+ state.probing = null;
453
+ });
454
+ }
455
+
456
+ return state.probing;
457
+ }
458
+ /**
459
+ * Called by the driver after an operation. Only connectivity errors count,
460
+ * and only inside the sliding window, so a busy cluster that answers most
461
+ * requests is not torn down for one slow statement.
462
+ */
463
+
464
+
465
+ function recordOutcome(name, error) {
466
+ if (stopped || !name || !isConnectivityError(error)) return;
467
+ var state = states.get(name);
468
+ if (!(state === null || state === void 0 ? void 0 : state.config)) return;
469
+ var at = now();
470
+ state.failures.push(at);
471
+ var cutoff = at - settings.failureWindowMs;
472
+
473
+ while (state.failures.length && state.failures[0] < cutoff) {
474
+ state.failures.shift();
475
+ }
476
+
477
+ if (state.failures.length >= settings.failureThreshold) {
478
+ state.failures = [];
479
+ check(name, {
480
+ suspected: true
481
+ }).catch(() => {});
482
+ }
483
+ }
484
+ /** Starts monitoring a connection. Called once its first connect succeeds. */
485
+
486
+
487
+ function register(name, config) {
488
+ var _state$timer$unref, _state$timer;
489
+
490
+ var state = stateFor(name);
491
+ state.config = config;
492
+ if (stopped || state.timer || !settings.healthIntervalMs) return;
493
+ state.timer = setInterval(() => {
494
+ check(name).catch(() => {});
495
+ }, settings.healthIntervalMs);
496
+ (_state$timer$unref = (_state$timer = state.timer).unref) === null || _state$timer$unref === void 0 ? void 0 : _state$timer$unref.call(_state$timer);
497
+ }
498
+
499
+ function generation(name) {
500
+ var _states$get;
501
+
502
+ return ((_states$get = states.get(name)) === null || _states$get === void 0 ? void 0 : _states$get.generation) || 1;
503
+ }
504
+
505
+ function status() {
506
+ var result = {};
507
+
508
+ for (var [name, state] of states) {
509
+ result[name] = {
510
+ healthy: state.healthy,
511
+ reconnecting: Boolean(state.reconnecting),
512
+ generation: state.generation,
513
+ recentFailures: state.failures.length,
514
+ probeFailures: state.probeFailures,
515
+ reconnectAttempts: state.reconnectAttempts,
516
+ unhealthySince: state.unhealthySince ? new Date(state.unhealthySince).toISOString() : null,
517
+ lastReconnectAt: state.lastReconnectAt ? new Date(state.lastReconnectAt).toISOString() : null,
518
+ lastError: state.lastError
519
+ };
520
+ }
521
+
522
+ return result;
523
+ }
524
+ /**
525
+ * Stops probing and reconnecting, and closes clusters that were replaced
526
+ * but are still in their grace period. The current clusters stay open: the
527
+ * application closes those through `spice.database.clusters`.
528
+ */
529
+
530
+
531
+ function stop() {
532
+ return _stop.apply(this, arguments);
533
+ }
534
+
535
+ function _stop() {
536
+ _stop = _asyncToGenerator(function* () {
537
+ stopped = true;
538
+ var closing = [];
539
+
540
+ for (var state of states.values()) {
541
+ if (state.timer) clearInterval(state.timer);
542
+ state.timer = null;
543
+
544
+ for (var cluster of state.retired) {
545
+ closing.push(closeQuietly(cluster, log, "retired cluster for \"".concat(state.name, "\"")));
546
+ }
547
+
548
+ state.retired.clear();
549
+ }
550
+
551
+ yield Promise.all(closing);
552
+ });
553
+ return _stop.apply(this, arguments);
554
+ }
555
+
556
+ return {
557
+ settings,
558
+ register,
559
+ recordOutcome,
560
+ check,
561
+ reconnect,
562
+ generation,
563
+ status,
564
+ stop
565
+ };
566
+ }
567
+
568
+ module.exports = {
569
+ createHealthMonitor,
570
+ openHandles,
571
+ buildConnectOptions,
572
+ isConnectivityError
573
+ };