@zero-server/sdk 0.9.0 → 0.9.2
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/LICENSE +21 -21
- package/README.md +460 -437
- package/index.js +414 -412
- package/lib/app.js +1172 -1172
- package/lib/auth/authorize.js +399 -399
- package/lib/auth/enrollment.js +367 -367
- package/lib/auth/index.js +57 -57
- package/lib/auth/jwt.js +731 -731
- package/lib/auth/oauth.js +362 -362
- package/lib/auth/session.js +588 -588
- package/lib/auth/trustedDevice.js +409 -409
- package/lib/auth/twoFactor.js +1150 -1150
- package/lib/auth/webauthn.js +946 -946
- package/lib/body/index.js +14 -14
- package/lib/body/json.js +109 -109
- package/lib/body/multipart.js +440 -440
- package/lib/body/raw.js +71 -71
- package/lib/body/rawBuffer.js +160 -160
- package/lib/body/sendError.js +25 -25
- package/lib/body/text.js +75 -75
- package/lib/body/typeMatch.js +41 -41
- package/lib/body/urlencoded.js +235 -235
- package/lib/cli.js +845 -845
- package/lib/cluster.js +666 -666
- package/lib/debug.js +372 -372
- package/lib/env/index.js +460 -460
- package/lib/errors.js +683 -683
- package/lib/fetch/index.js +256 -256
- package/lib/grpc/balancer.js +378 -378
- package/lib/grpc/call.js +708 -708
- package/lib/grpc/client.js +764 -764
- package/lib/grpc/codec.js +1221 -1221
- package/lib/grpc/credentials.js +398 -398
- package/lib/grpc/frame.js +262 -262
- package/lib/grpc/health.js +287 -287
- package/lib/grpc/index.js +121 -121
- package/lib/grpc/metadata.js +461 -461
- package/lib/grpc/proto.js +821 -821
- package/lib/grpc/reflection.js +590 -590
- package/lib/grpc/server.js +445 -445
- package/lib/grpc/status.js +118 -118
- package/lib/grpc/watch.js +173 -173
- package/lib/http/index.js +10 -10
- package/lib/http/request.js +727 -727
- package/lib/http/response.js +799 -799
- package/lib/lifecycle.js +557 -557
- package/lib/middleware/compress.js +230 -230
- package/lib/middleware/cookieParser.js +237 -237
- package/lib/middleware/cors.js +93 -93
- package/lib/middleware/csrf.js +136 -136
- package/lib/middleware/errorHandler.js +101 -101
- package/lib/middleware/helmet.js +175 -175
- package/lib/middleware/index.js +19 -17
- package/lib/middleware/logger.js +74 -74
- package/lib/middleware/rateLimit.js +88 -88
- package/lib/middleware/requestId.js +53 -53
- package/lib/middleware/static.js +326 -326
- package/lib/middleware/timeout.js +71 -71
- package/lib/middleware/validator.js +254 -254
- package/lib/observe/health.js +326 -326
- package/lib/observe/index.js +50 -50
- package/lib/observe/logger.js +359 -359
- package/lib/observe/metrics.js +805 -805
- package/lib/observe/tracing.js +592 -592
- package/lib/orm/adapters/json.js +290 -290
- package/lib/orm/adapters/memory.js +764 -764
- package/lib/orm/adapters/mongo.js +764 -764
- package/lib/orm/adapters/mysql.js +933 -933
- package/lib/orm/adapters/postgres.js +1144 -1144
- package/lib/orm/adapters/redis.js +1534 -1534
- package/lib/orm/adapters/sql-base.js +212 -212
- package/lib/orm/adapters/sqlite.js +858 -858
- package/lib/orm/audit.js +649 -649
- package/lib/orm/cache.js +394 -394
- package/lib/orm/geo.js +387 -387
- package/lib/orm/index.js +784 -784
- package/lib/orm/migrate.js +432 -432
- package/lib/orm/model.js +1706 -1706
- package/lib/orm/plugin.js +375 -375
- package/lib/orm/procedures.js +836 -836
- package/lib/orm/profiler.js +233 -233
- package/lib/orm/query.js +1772 -1772
- package/lib/orm/replicas.js +241 -241
- package/lib/orm/schema.js +307 -307
- package/lib/orm/search.js +380 -380
- package/lib/orm/seed/data/commerce.js +136 -136
- package/lib/orm/seed/data/internet.js +111 -111
- package/lib/orm/seed/data/locations.js +204 -204
- package/lib/orm/seed/data/names.js +338 -338
- package/lib/orm/seed/data/person.js +128 -128
- package/lib/orm/seed/data/phone.js +211 -211
- package/lib/orm/seed/data/words.js +134 -134
- package/lib/orm/seed/factory.js +178 -178
- package/lib/orm/seed/fake.js +1186 -1186
- package/lib/orm/seed/index.js +18 -18
- package/lib/orm/seed/rng.js +70 -70
- package/lib/orm/seed/seeder.js +124 -124
- package/lib/orm/seed/unique.js +68 -68
- package/lib/orm/snapshot.js +366 -366
- package/lib/orm/tenancy.js +605 -605
- package/lib/orm/views.js +350 -350
- package/lib/router/index.js +436 -436
- package/lib/sse/index.js +8 -8
- package/lib/sse/stream.js +349 -349
- package/lib/ws/connection.js +451 -451
- package/lib/ws/handshake.js +125 -125
- package/lib/ws/index.js +14 -14
- package/lib/ws/room.js +223 -223
- package/package.json +73 -73
- package/types/app.d.ts +223 -223
- package/types/auth.d.ts +520 -520
- package/types/cluster.d.ts +75 -75
- package/types/env.d.ts +80 -80
- package/types/errors.d.ts +316 -316
- package/types/fetch.d.ts +43 -43
- package/types/grpc.d.ts +432 -432
- package/types/index.d.ts +384 -384
- package/types/lifecycle.d.ts +60 -60
- package/types/middleware.d.ts +320 -320
- package/types/observe.d.ts +304 -304
- package/types/orm.d.ts +1887 -1887
- package/types/request.d.ts +109 -109
- package/types/response.d.ts +157 -157
- package/types/router.d.ts +78 -78
- package/types/sse.d.ts +78 -78
- package/types/websocket.d.ts +126 -126
package/lib/orm/replicas.js
CHANGED
|
@@ -1,241 +1,241 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @module orm/replicas
|
|
3
|
-
* @description Read replica management with automatic read/write splitting,
|
|
4
|
-
* round-robin and random selection strategies, sticky writes,
|
|
5
|
-
* and health checking.
|
|
6
|
-
*
|
|
7
|
-
* @example
|
|
8
|
-
* const { Database, ReplicaManager } = require('@zero-server/sdk');
|
|
9
|
-
*
|
|
10
|
-
* const db = Database.connectWithReplicas('postgres',
|
|
11
|
-
* { host: 'primary.db', database: 'app' },
|
|
12
|
-
* [
|
|
13
|
-
* { host: 'replica1.db', database: 'app' },
|
|
14
|
-
* { host: 'replica2.db', database: 'app' },
|
|
15
|
-
* ],
|
|
16
|
-
* { strategy: 'round-robin', stickyWindow: 2000 }
|
|
17
|
-
* );
|
|
18
|
-
*/
|
|
19
|
-
|
|
20
|
-
const log = require('../debug')('zero:replicas');
|
|
21
|
-
|
|
22
|
-
class ReplicaManager
|
|
23
|
-
{
|
|
24
|
-
/**
|
|
25
|
-
* @constructor
|
|
26
|
-
* @param {object} [options] - Configuration options.
|
|
27
|
-
* @param {string} [options.strategy='round-robin'] - Selection strategy: 'round-robin' | 'random'.
|
|
28
|
-
* @param {boolean} [options.stickyWrite=true] - Read from primary after a write for stickyWindow ms.
|
|
29
|
-
* @param {number} [options.stickyWindow=1000] - Duration (ms) to read from primary after a write.
|
|
30
|
-
*/
|
|
31
|
-
constructor(options = {})
|
|
32
|
-
{
|
|
33
|
-
/** @private */ this._primary = null;
|
|
34
|
-
/** @private */ this._replicas = [];
|
|
35
|
-
|
|
36
|
-
// Validate strategy against whitelist
|
|
37
|
-
const allowed = ['round-robin', 'random'];
|
|
38
|
-
const strategy = options.strategy || 'round-robin';
|
|
39
|
-
if (!allowed.includes(strategy))
|
|
40
|
-
{
|
|
41
|
-
throw new Error(`Invalid replica strategy: "${strategy}". Must be one of: ${allowed.join(', ')}`);
|
|
42
|
-
}
|
|
43
|
-
/** @private */ this._strategy = strategy;
|
|
44
|
-
/** @private */ this._idx = 0;
|
|
45
|
-
|
|
46
|
-
// Sticky writes: read from primary after a write to avoid stale reads
|
|
47
|
-
/** @private */ this._stickyWrite = options.stickyWrite !== false;
|
|
48
|
-
/** @private */ this._stickyWindow = Math.max(0, Number(options.stickyWindow) || 1000);
|
|
49
|
-
/** @private */ this._lastWriteAt = 0;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Set the primary (read-write) adapter.
|
|
54
|
-
* @param {object} adapter - Database adapter instance.
|
|
55
|
-
* @throws {Error} If adapter is null or undefined.
|
|
56
|
-
*/
|
|
57
|
-
setPrimary(adapter)
|
|
58
|
-
{
|
|
59
|
-
if (!adapter) throw new Error('Primary adapter must not be null');
|
|
60
|
-
this._primary = adapter;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Add a read replica adapter.
|
|
65
|
-
* @param {object} adapter - Database adapter instance.
|
|
66
|
-
*/
|
|
67
|
-
addReplica(adapter)
|
|
68
|
-
{
|
|
69
|
-
if (!adapter) throw new Error('Replica adapter must not be null');
|
|
70
|
-
this._replicas.push({ adapter, healthy: true, lastChecked: 0 });
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* Number of registered replicas.
|
|
75
|
-
* @type {number}
|
|
76
|
-
*/
|
|
77
|
-
get replicaCount()
|
|
78
|
-
{
|
|
79
|
-
return this._replicas.length;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* Get an adapter for read operations.
|
|
84
|
-
* Respects strategy, health status, and sticky writes.
|
|
85
|
-
*
|
|
86
|
-
* @returns {object} Adapter instance.
|
|
87
|
-
*/
|
|
88
|
-
getReadAdapter()
|
|
89
|
-
{
|
|
90
|
-
// Sticky writes: use primary during the sticky window
|
|
91
|
-
if (this._stickyWrite && (Date.now() - this._lastWriteAt) < this._stickyWindow)
|
|
92
|
-
{
|
|
93
|
-
log('Sticky write window active, using primary for read');
|
|
94
|
-
return this._primary;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
const healthy = this._replicas.filter(r => r.healthy);
|
|
98
|
-
if (!healthy.length)
|
|
99
|
-
{
|
|
100
|
-
log('No healthy replicas, falling back to primary');
|
|
101
|
-
return this._primary;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
let replica;
|
|
105
|
-
if (this._strategy === 'random')
|
|
106
|
-
{
|
|
107
|
-
replica = healthy[Math.floor(Math.random() * healthy.length)];
|
|
108
|
-
}
|
|
109
|
-
else
|
|
110
|
-
{
|
|
111
|
-
// round-robin (reset index to prevent unbounded growth)
|
|
112
|
-
replica = healthy[this._idx % healthy.length];
|
|
113
|
-
this._idx = (this._idx + 1) % Number.MAX_SAFE_INTEGER;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
return replica.adapter;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
/**
|
|
120
|
-
* Get the primary adapter for write operations.
|
|
121
|
-
* Also updates the last write timestamp for sticky window tracking.
|
|
122
|
-
*
|
|
123
|
-
* @returns {object} Primary adapter instance.
|
|
124
|
-
*/
|
|
125
|
-
getWriteAdapter()
|
|
126
|
-
{
|
|
127
|
-
this._lastWriteAt = Date.now();
|
|
128
|
-
return this._primary;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
/**
|
|
132
|
-
* Mark a replica as unhealthy (excluded from read routing).
|
|
133
|
-
* @param {object} adapter - Database adapter instance.
|
|
134
|
-
*/
|
|
135
|
-
markUnhealthy(adapter)
|
|
136
|
-
{
|
|
137
|
-
const replica = this._replicas.find(r => r.adapter === adapter);
|
|
138
|
-
if (replica)
|
|
139
|
-
{
|
|
140
|
-
replica.healthy = false;
|
|
141
|
-
log('Replica marked unhealthy');
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
/**
|
|
146
|
-
* Mark a replica as healthy (re-included in read routing).
|
|
147
|
-
* @param {object} adapter - Database adapter instance.
|
|
148
|
-
*/
|
|
149
|
-
markHealthy(adapter)
|
|
150
|
-
{
|
|
151
|
-
const replica = this._replicas.find(r => r.adapter === adapter);
|
|
152
|
-
if (replica)
|
|
153
|
-
{
|
|
154
|
-
replica.healthy = true;
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
/**
|
|
159
|
-
* Run a health check on all replicas.
|
|
160
|
-
* Calls adapter.ping() if available.
|
|
161
|
-
*
|
|
162
|
-
* @returns {Promise<Array<{ healthy: boolean, lastChecked: number }>>}
|
|
163
|
-
*/
|
|
164
|
-
async healthCheck()
|
|
165
|
-
{
|
|
166
|
-
const checks = this._replicas.map(async (replica) =>
|
|
167
|
-
{
|
|
168
|
-
try
|
|
169
|
-
{
|
|
170
|
-
if (typeof replica.adapter.ping === 'function')
|
|
171
|
-
{
|
|
172
|
-
replica.healthy = await replica.adapter.ping();
|
|
173
|
-
}
|
|
174
|
-
else
|
|
175
|
-
{
|
|
176
|
-
// Adapters without ping are assumed healthy
|
|
177
|
-
replica.healthy = true;
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
catch
|
|
181
|
-
{
|
|
182
|
-
replica.healthy = false;
|
|
183
|
-
}
|
|
184
|
-
replica.lastChecked = Date.now();
|
|
185
|
-
return { healthy: replica.healthy, lastChecked: replica.lastChecked };
|
|
186
|
-
});
|
|
187
|
-
|
|
188
|
-
return Promise.all(checks);
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
/**
|
|
192
|
-
* Get all adapters (primary + replicas).
|
|
193
|
-
* @returns {object[]} Primary and all replica adapters.
|
|
194
|
-
*/
|
|
195
|
-
getAllAdapters()
|
|
196
|
-
{
|
|
197
|
-
return [this._primary, ...this._replicas.map(r => r.adapter)].filter(Boolean);
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
/**
|
|
201
|
-
* Remove a replica adapter from the pool.
|
|
202
|
-
* @param {object} adapter - Database adapter instance.
|
|
203
|
-
*/
|
|
204
|
-
removeReplica(adapter)
|
|
205
|
-
{
|
|
206
|
-
this._replicas = this._replicas.filter(r => r.adapter !== adapter);
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
/**
|
|
210
|
-
* Get pool status summary.
|
|
211
|
-
* @returns {{ primary: boolean, total: number, healthy: number, unhealthy: number, strategy: string }}
|
|
212
|
-
*/
|
|
213
|
-
status()
|
|
214
|
-
{
|
|
215
|
-
const healthy = this._replicas.filter(r => r.healthy).length;
|
|
216
|
-
return {
|
|
217
|
-
primary: !!this._primary,
|
|
218
|
-
total: this._replicas.length,
|
|
219
|
-
healthy,
|
|
220
|
-
unhealthy: this._replicas.length - healthy,
|
|
221
|
-
strategy: this._strategy,
|
|
222
|
-
};
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
/**
|
|
226
|
-
* Close all adapters (primary + replicas).
|
|
227
|
-
* @returns {Promise<void>}
|
|
228
|
-
*/
|
|
229
|
-
async closeAll()
|
|
230
|
-
{
|
|
231
|
-
for (const adapter of this.getAllAdapters())
|
|
232
|
-
{
|
|
233
|
-
if (typeof adapter.close === 'function')
|
|
234
|
-
{
|
|
235
|
-
await adapter.close();
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
module.exports = { ReplicaManager };
|
|
1
|
+
/**
|
|
2
|
+
* @module orm/replicas
|
|
3
|
+
* @description Read replica management with automatic read/write splitting,
|
|
4
|
+
* round-robin and random selection strategies, sticky writes,
|
|
5
|
+
* and health checking.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* const { Database, ReplicaManager } = require('@zero-server/sdk');
|
|
9
|
+
*
|
|
10
|
+
* const db = Database.connectWithReplicas('postgres',
|
|
11
|
+
* { host: 'primary.db', database: 'app' },
|
|
12
|
+
* [
|
|
13
|
+
* { host: 'replica1.db', database: 'app' },
|
|
14
|
+
* { host: 'replica2.db', database: 'app' },
|
|
15
|
+
* ],
|
|
16
|
+
* { strategy: 'round-robin', stickyWindow: 2000 }
|
|
17
|
+
* );
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const log = require('../debug')('zero:replicas');
|
|
21
|
+
|
|
22
|
+
class ReplicaManager
|
|
23
|
+
{
|
|
24
|
+
/**
|
|
25
|
+
* @constructor
|
|
26
|
+
* @param {object} [options] - Configuration options.
|
|
27
|
+
* @param {string} [options.strategy='round-robin'] - Selection strategy: 'round-robin' | 'random'.
|
|
28
|
+
* @param {boolean} [options.stickyWrite=true] - Read from primary after a write for stickyWindow ms.
|
|
29
|
+
* @param {number} [options.stickyWindow=1000] - Duration (ms) to read from primary after a write.
|
|
30
|
+
*/
|
|
31
|
+
constructor(options = {})
|
|
32
|
+
{
|
|
33
|
+
/** @private */ this._primary = null;
|
|
34
|
+
/** @private */ this._replicas = [];
|
|
35
|
+
|
|
36
|
+
// Validate strategy against whitelist
|
|
37
|
+
const allowed = ['round-robin', 'random'];
|
|
38
|
+
const strategy = options.strategy || 'round-robin';
|
|
39
|
+
if (!allowed.includes(strategy))
|
|
40
|
+
{
|
|
41
|
+
throw new Error(`Invalid replica strategy: "${strategy}". Must be one of: ${allowed.join(', ')}`);
|
|
42
|
+
}
|
|
43
|
+
/** @private */ this._strategy = strategy;
|
|
44
|
+
/** @private */ this._idx = 0;
|
|
45
|
+
|
|
46
|
+
// Sticky writes: read from primary after a write to avoid stale reads
|
|
47
|
+
/** @private */ this._stickyWrite = options.stickyWrite !== false;
|
|
48
|
+
/** @private */ this._stickyWindow = Math.max(0, Number(options.stickyWindow) || 1000);
|
|
49
|
+
/** @private */ this._lastWriteAt = 0;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Set the primary (read-write) adapter.
|
|
54
|
+
* @param {object} adapter - Database adapter instance.
|
|
55
|
+
* @throws {Error} If adapter is null or undefined.
|
|
56
|
+
*/
|
|
57
|
+
setPrimary(adapter)
|
|
58
|
+
{
|
|
59
|
+
if (!adapter) throw new Error('Primary adapter must not be null');
|
|
60
|
+
this._primary = adapter;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Add a read replica adapter.
|
|
65
|
+
* @param {object} adapter - Database adapter instance.
|
|
66
|
+
*/
|
|
67
|
+
addReplica(adapter)
|
|
68
|
+
{
|
|
69
|
+
if (!adapter) throw new Error('Replica adapter must not be null');
|
|
70
|
+
this._replicas.push({ adapter, healthy: true, lastChecked: 0 });
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Number of registered replicas.
|
|
75
|
+
* @type {number}
|
|
76
|
+
*/
|
|
77
|
+
get replicaCount()
|
|
78
|
+
{
|
|
79
|
+
return this._replicas.length;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Get an adapter for read operations.
|
|
84
|
+
* Respects strategy, health status, and sticky writes.
|
|
85
|
+
*
|
|
86
|
+
* @returns {object} Adapter instance.
|
|
87
|
+
*/
|
|
88
|
+
getReadAdapter()
|
|
89
|
+
{
|
|
90
|
+
// Sticky writes: use primary during the sticky window
|
|
91
|
+
if (this._stickyWrite && (Date.now() - this._lastWriteAt) < this._stickyWindow)
|
|
92
|
+
{
|
|
93
|
+
log('Sticky write window active, using primary for read');
|
|
94
|
+
return this._primary;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const healthy = this._replicas.filter(r => r.healthy);
|
|
98
|
+
if (!healthy.length)
|
|
99
|
+
{
|
|
100
|
+
log('No healthy replicas, falling back to primary');
|
|
101
|
+
return this._primary;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
let replica;
|
|
105
|
+
if (this._strategy === 'random')
|
|
106
|
+
{
|
|
107
|
+
replica = healthy[Math.floor(Math.random() * healthy.length)];
|
|
108
|
+
}
|
|
109
|
+
else
|
|
110
|
+
{
|
|
111
|
+
// round-robin (reset index to prevent unbounded growth)
|
|
112
|
+
replica = healthy[this._idx % healthy.length];
|
|
113
|
+
this._idx = (this._idx + 1) % Number.MAX_SAFE_INTEGER;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return replica.adapter;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Get the primary adapter for write operations.
|
|
121
|
+
* Also updates the last write timestamp for sticky window tracking.
|
|
122
|
+
*
|
|
123
|
+
* @returns {object} Primary adapter instance.
|
|
124
|
+
*/
|
|
125
|
+
getWriteAdapter()
|
|
126
|
+
{
|
|
127
|
+
this._lastWriteAt = Date.now();
|
|
128
|
+
return this._primary;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Mark a replica as unhealthy (excluded from read routing).
|
|
133
|
+
* @param {object} adapter - Database adapter instance.
|
|
134
|
+
*/
|
|
135
|
+
markUnhealthy(adapter)
|
|
136
|
+
{
|
|
137
|
+
const replica = this._replicas.find(r => r.adapter === adapter);
|
|
138
|
+
if (replica)
|
|
139
|
+
{
|
|
140
|
+
replica.healthy = false;
|
|
141
|
+
log('Replica marked unhealthy');
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Mark a replica as healthy (re-included in read routing).
|
|
147
|
+
* @param {object} adapter - Database adapter instance.
|
|
148
|
+
*/
|
|
149
|
+
markHealthy(adapter)
|
|
150
|
+
{
|
|
151
|
+
const replica = this._replicas.find(r => r.adapter === adapter);
|
|
152
|
+
if (replica)
|
|
153
|
+
{
|
|
154
|
+
replica.healthy = true;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Run a health check on all replicas.
|
|
160
|
+
* Calls adapter.ping() if available.
|
|
161
|
+
*
|
|
162
|
+
* @returns {Promise<Array<{ healthy: boolean, lastChecked: number }>>}
|
|
163
|
+
*/
|
|
164
|
+
async healthCheck()
|
|
165
|
+
{
|
|
166
|
+
const checks = this._replicas.map(async (replica) =>
|
|
167
|
+
{
|
|
168
|
+
try
|
|
169
|
+
{
|
|
170
|
+
if (typeof replica.adapter.ping === 'function')
|
|
171
|
+
{
|
|
172
|
+
replica.healthy = await replica.adapter.ping();
|
|
173
|
+
}
|
|
174
|
+
else
|
|
175
|
+
{
|
|
176
|
+
// Adapters without ping are assumed healthy
|
|
177
|
+
replica.healthy = true;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
catch
|
|
181
|
+
{
|
|
182
|
+
replica.healthy = false;
|
|
183
|
+
}
|
|
184
|
+
replica.lastChecked = Date.now();
|
|
185
|
+
return { healthy: replica.healthy, lastChecked: replica.lastChecked };
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
return Promise.all(checks);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Get all adapters (primary + replicas).
|
|
193
|
+
* @returns {object[]} Primary and all replica adapters.
|
|
194
|
+
*/
|
|
195
|
+
getAllAdapters()
|
|
196
|
+
{
|
|
197
|
+
return [this._primary, ...this._replicas.map(r => r.adapter)].filter(Boolean);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Remove a replica adapter from the pool.
|
|
202
|
+
* @param {object} adapter - Database adapter instance.
|
|
203
|
+
*/
|
|
204
|
+
removeReplica(adapter)
|
|
205
|
+
{
|
|
206
|
+
this._replicas = this._replicas.filter(r => r.adapter !== adapter);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Get pool status summary.
|
|
211
|
+
* @returns {{ primary: boolean, total: number, healthy: number, unhealthy: number, strategy: string }}
|
|
212
|
+
*/
|
|
213
|
+
status()
|
|
214
|
+
{
|
|
215
|
+
const healthy = this._replicas.filter(r => r.healthy).length;
|
|
216
|
+
return {
|
|
217
|
+
primary: !!this._primary,
|
|
218
|
+
total: this._replicas.length,
|
|
219
|
+
healthy,
|
|
220
|
+
unhealthy: this._replicas.length - healthy,
|
|
221
|
+
strategy: this._strategy,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Close all adapters (primary + replicas).
|
|
227
|
+
* @returns {Promise<void>}
|
|
228
|
+
*/
|
|
229
|
+
async closeAll()
|
|
230
|
+
{
|
|
231
|
+
for (const adapter of this.getAllAdapters())
|
|
232
|
+
{
|
|
233
|
+
if (typeof adapter.close === 'function')
|
|
234
|
+
{
|
|
235
|
+
await adapter.close();
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
module.exports = { ReplicaManager };
|