@unrdf/kgc-runtime 26.4.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.
Files changed (70) hide show
  1. package/IMPLEMENTATION_SUMMARY.json +150 -0
  2. package/PLUGIN_SYSTEM_SUMMARY.json +149 -0
  3. package/README.md +98 -0
  4. package/TRANSACTION_IMPLEMENTATION.json +119 -0
  5. package/capability-map.md +93 -0
  6. package/docs/api-stability.md +269 -0
  7. package/docs/extensions/plugin-development.md +382 -0
  8. package/package.json +40 -0
  9. package/plugins/registry.json +35 -0
  10. package/src/admission-gate.mjs +414 -0
  11. package/src/api-version.mjs +373 -0
  12. package/src/atomic-admission.mjs +310 -0
  13. package/src/bounds.mjs +289 -0
  14. package/src/bulkhead-manager.mjs +280 -0
  15. package/src/capsule.mjs +524 -0
  16. package/src/crdt.mjs +361 -0
  17. package/src/enhanced-bounds.mjs +614 -0
  18. package/src/executor.mjs +73 -0
  19. package/src/freeze-restore.mjs +521 -0
  20. package/src/index.mjs +62 -0
  21. package/src/materialized-views.mjs +371 -0
  22. package/src/merge.mjs +472 -0
  23. package/src/plugin-isolation.mjs +392 -0
  24. package/src/plugin-manager.mjs +441 -0
  25. package/src/projections-api.mjs +336 -0
  26. package/src/projections-cli.mjs +238 -0
  27. package/src/projections-docs.mjs +300 -0
  28. package/src/projections-ide.mjs +278 -0
  29. package/src/receipt.mjs +340 -0
  30. package/src/rollback.mjs +258 -0
  31. package/src/saga-orchestrator.mjs +355 -0
  32. package/src/schemas.mjs +1330 -0
  33. package/src/storage-optimization.mjs +359 -0
  34. package/src/tool-registry.mjs +272 -0
  35. package/src/transaction.mjs +466 -0
  36. package/src/validators.mjs +485 -0
  37. package/src/work-item.mjs +449 -0
  38. package/templates/plugin-template/README.md +58 -0
  39. package/templates/plugin-template/index.mjs +162 -0
  40. package/templates/plugin-template/plugin.json +19 -0
  41. package/test/admission-gate.test.mjs +583 -0
  42. package/test/api-version.test.mjs +74 -0
  43. package/test/atomic-admission.test.mjs +155 -0
  44. package/test/bounds.test.mjs +341 -0
  45. package/test/bulkhead-manager.test.mjs +236 -0
  46. package/test/capsule.test.mjs +625 -0
  47. package/test/crdt.test.mjs +215 -0
  48. package/test/enhanced-bounds.test.mjs +487 -0
  49. package/test/freeze-restore.test.mjs +472 -0
  50. package/test/materialized-views.test.mjs +243 -0
  51. package/test/merge.test.mjs +665 -0
  52. package/test/plugin-isolation.test.mjs +109 -0
  53. package/test/plugin-manager.test.mjs +208 -0
  54. package/test/projections-api.test.mjs +293 -0
  55. package/test/projections-cli.test.mjs +204 -0
  56. package/test/projections-docs.test.mjs +173 -0
  57. package/test/projections-ide.test.mjs +230 -0
  58. package/test/receipt.test.mjs +295 -0
  59. package/test/rollback.test.mjs +132 -0
  60. package/test/saga-orchestrator.test.mjs +279 -0
  61. package/test/schemas.test.mjs +716 -0
  62. package/test/storage-optimization.test.mjs +503 -0
  63. package/test/tool-registry.test.mjs +341 -0
  64. package/test/transaction.test.mjs +189 -0
  65. package/test/validators.test.mjs +463 -0
  66. package/test/work-item.test.mjs +548 -0
  67. package/test/work-item.test.mjs.bak +548 -0
  68. package/var/kgc/test-atomic-log.json +519 -0
  69. package/var/kgc/test-cascading-log.json +145 -0
  70. package/vitest.config.mjs +18 -0
package/src/bounds.mjs ADDED
@@ -0,0 +1,289 @@
1
+ /**
2
+ * @file Bounds checking and enforcement for capacity-limited channels
3
+ * @module kgc-runtime/bounds
4
+ */
5
+
6
+ import { z } from 'zod';
7
+ import { randomUUID } from 'node:crypto';
8
+
9
+ /**
10
+ * Bounds configuration schema
11
+ */
12
+ const BoundsSchema = z.object({
13
+ max_files_touched: z.number().int().positive().optional(),
14
+ max_bytes_changed: z.number().int().positive().optional(),
15
+ max_tool_ops: z.number().int().positive().optional(),
16
+ max_runtime_ms: z.number().int().positive().optional(),
17
+ max_graph_rewrites: z.number().int().positive().optional(),
18
+ });
19
+
20
+ /**
21
+ * Operation schema
22
+ */
23
+ const OperationSchema = z.object({
24
+ type: z.string(),
25
+ files: z.number().int().nonnegative().optional(),
26
+ bytes: z.number().int().nonnegative().optional(),
27
+ ops: z.number().int().nonnegative().optional(),
28
+ ms: z.number().int().nonnegative().optional(),
29
+ rewrites: z.number().int().nonnegative().optional(),
30
+ });
31
+
32
+ /**
33
+ * Receipt schema
34
+ */
35
+ const ReceiptSchema = z.object({
36
+ admit: z.boolean(),
37
+ receipt_id: z.string().uuid(),
38
+ timestamp: z.number().int().positive(),
39
+ bound_used: z.string().optional(),
40
+ reason: z.string().optional(),
41
+ bound_violated: z.string().optional(),
42
+ parent_receipt_id: z.string().uuid().optional(),
43
+ });
44
+
45
+ /**
46
+ * Bounds checker - enforces capacity limits during execution
47
+ *
48
+ * @class
49
+ */
50
+ export class BoundsChecker {
51
+ /**
52
+ * Create a bounds checker
53
+ *
54
+ * @param {object} bounds - Bound limits
55
+ * @param {number} [bounds.max_files_touched] - Maximum files that can be touched
56
+ * @param {number} [bounds.max_bytes_changed] - Maximum bytes that can be changed
57
+ * @param {number} [bounds.max_tool_ops] - Maximum tool operations
58
+ * @param {number} [bounds.max_runtime_ms] - Maximum runtime in milliseconds
59
+ * @param {number} [bounds.max_graph_rewrites] - Maximum graph rewrites
60
+ */
61
+ constructor(bounds) {
62
+ this.bounds = BoundsSchema.parse(bounds);
63
+
64
+ /** @type {object} Current usage tracking */
65
+ this.usage = {
66
+ files_touched: 0,
67
+ bytes_changed: 0,
68
+ tool_ops: 0,
69
+ runtime_ms: 0,
70
+ graph_rewrites: 0,
71
+ };
72
+ }
73
+
74
+ /**
75
+ * Check if operation can be executed within bounds
76
+ *
77
+ * @param {object} operation - Operation to check
78
+ * @param {string} operation.type - Operation type
79
+ * @param {number} [operation.files] - Files to touch
80
+ * @param {number} [operation.bytes] - Bytes to change
81
+ * @param {number} [operation.ops] - Tool operations
82
+ * @param {number} [operation.ms] - Runtime milliseconds
83
+ * @param {number} [operation.rewrites] - Graph rewrites
84
+ * @returns {boolean} True if operation can be executed
85
+ */
86
+ canExecute(operation) {
87
+ const op = OperationSchema.parse(operation);
88
+
89
+ // Check files bound
90
+ if (this.bounds.max_files_touched !== undefined && op.files !== undefined) {
91
+ if (this.usage.files_touched + op.files > this.bounds.max_files_touched) {
92
+ return false;
93
+ }
94
+ }
95
+
96
+ // Check bytes bound
97
+ if (this.bounds.max_bytes_changed !== undefined && op.bytes !== undefined) {
98
+ if (this.usage.bytes_changed + op.bytes > this.bounds.max_bytes_changed) {
99
+ return false;
100
+ }
101
+ }
102
+
103
+ // Check ops bound
104
+ if (this.bounds.max_tool_ops !== undefined && op.ops !== undefined) {
105
+ if (this.usage.tool_ops + op.ops > this.bounds.max_tool_ops) {
106
+ return false;
107
+ }
108
+ }
109
+
110
+ // Check runtime bound
111
+ if (this.bounds.max_runtime_ms !== undefined && op.ms !== undefined) {
112
+ if (this.usage.runtime_ms + op.ms > this.bounds.max_runtime_ms) {
113
+ return false;
114
+ }
115
+ }
116
+
117
+ // Check rewrites bound
118
+ if (this.bounds.max_graph_rewrites !== undefined && op.rewrites !== undefined) {
119
+ if (this.usage.graph_rewrites + op.rewrites > this.bounds.max_graph_rewrites) {
120
+ return false;
121
+ }
122
+ }
123
+
124
+ return true;
125
+ }
126
+
127
+ /**
128
+ * Record operation execution and update usage
129
+ *
130
+ * @param {object} operation - Operation that was executed
131
+ * @param {string} operation.type - Operation type
132
+ * @param {number} [operation.files] - Files touched
133
+ * @param {number} [operation.bytes] - Bytes changed
134
+ * @param {number} [operation.ops] - Tool operations executed
135
+ * @param {number} [operation.ms] - Runtime consumed
136
+ * @param {number} [operation.rewrites] - Graph rewrites performed
137
+ * @returns {void}
138
+ */
139
+ recordOperation(operation) {
140
+ const op = OperationSchema.parse(operation);
141
+
142
+ if (op.files !== undefined) {
143
+ this.usage.files_touched += op.files;
144
+ }
145
+ if (op.bytes !== undefined) {
146
+ this.usage.bytes_changed += op.bytes;
147
+ }
148
+ if (op.ops !== undefined) {
149
+ this.usage.tool_ops += op.ops;
150
+ }
151
+ if (op.ms !== undefined) {
152
+ this.usage.runtime_ms += op.ms;
153
+ }
154
+ if (op.rewrites !== undefined) {
155
+ this.usage.graph_rewrites += op.rewrites;
156
+ }
157
+ }
158
+
159
+ /**
160
+ * Get current usage statistics
161
+ *
162
+ * @returns {object} Current usage
163
+ * @returns {number} .files_touched - Files touched so far
164
+ * @returns {number} .bytes_changed - Bytes changed so far
165
+ * @returns {number} .tool_ops - Tool operations executed so far
166
+ * @returns {number} .runtime_ms - Runtime consumed so far
167
+ * @returns {number} .graph_rewrites - Graph rewrites performed so far
168
+ */
169
+ getUsage() {
170
+ return { ...this.usage };
171
+ }
172
+ }
173
+
174
+ /**
175
+ * Enforce bounds on an operation and generate receipt
176
+ *
177
+ * @param {object} operation - Operation to check
178
+ * @param {string} operation.type - Operation type
179
+ * @param {number} [operation.files] - Files to touch
180
+ * @param {number} [operation.bytes] - Bytes to change
181
+ * @param {number} [operation.ops] - Tool operations
182
+ * @param {number} [operation.ms] - Runtime milliseconds
183
+ * @param {number} [operation.rewrites] - Graph rewrites
184
+ * @param {object} bounds - Bound limits
185
+ * @param {number} [bounds.max_files_touched] - Maximum files
186
+ * @param {number} [bounds.max_bytes_changed] - Maximum bytes
187
+ * @param {number} [bounds.max_tool_ops] - Maximum operations
188
+ * @param {number} [bounds.max_runtime_ms] - Maximum runtime
189
+ * @param {number} [bounds.max_graph_rewrites] - Maximum rewrites
190
+ * @param {object} [options] - Additional options
191
+ * @param {string} [options.parent_receipt_id] - Parent receipt for chaining
192
+ * @returns {object} Receipt with admission decision
193
+ * @returns {boolean} .admit - Whether operation is admitted
194
+ * @returns {string} .receipt_id - Unique receipt identifier
195
+ * @returns {number} .timestamp - Receipt generation timestamp
196
+ * @returns {string} [.bound_used] - Bound that was checked (on admit)
197
+ * @returns {string} [.reason] - Rejection reason (on deny)
198
+ * @returns {string} [.bound_violated] - Violated bound name (on deny)
199
+ * @returns {string} [.parent_receipt_id] - Parent receipt if chained
200
+ */
201
+ export function enforceBounds(operation, bounds, options = {}) {
202
+ const op = OperationSchema.parse(operation);
203
+ const bnds = BoundsSchema.parse(bounds);
204
+
205
+ const receipt = {
206
+ receipt_id: randomUUID(),
207
+ timestamp: Date.now(),
208
+ };
209
+
210
+ // Include parent receipt if provided
211
+ if (options.parent_receipt_id) {
212
+ receipt.parent_receipt_id = options.parent_receipt_id;
213
+ }
214
+
215
+ // Check files bound
216
+ if (bnds.max_files_touched !== undefined && op.files !== undefined) {
217
+ if (op.files > bnds.max_files_touched) {
218
+ return ReceiptSchema.parse({
219
+ ...receipt,
220
+ admit: false,
221
+ reason: `exceeded max_files: requested ${op.files}, limit ${bnds.max_files_touched}`,
222
+ bound_violated: 'files',
223
+ });
224
+ }
225
+ }
226
+
227
+ // Check bytes bound
228
+ if (bnds.max_bytes_changed !== undefined && op.bytes !== undefined) {
229
+ if (op.bytes > bnds.max_bytes_changed) {
230
+ return ReceiptSchema.parse({
231
+ ...receipt,
232
+ admit: false,
233
+ reason: `exceeded max_bytes: requested ${op.bytes}, limit ${bnds.max_bytes_changed}`,
234
+ bound_violated: 'bytes',
235
+ });
236
+ }
237
+ }
238
+
239
+ // Check ops bound
240
+ if (bnds.max_tool_ops !== undefined && op.ops !== undefined) {
241
+ if (op.ops > bnds.max_tool_ops) {
242
+ return ReceiptSchema.parse({
243
+ ...receipt,
244
+ admit: false,
245
+ reason: `exceeded max_tool_ops: requested ${op.ops}, limit ${bnds.max_tool_ops}`,
246
+ bound_violated: 'ops',
247
+ });
248
+ }
249
+ }
250
+
251
+ // Check runtime bound
252
+ if (bnds.max_runtime_ms !== undefined && op.ms !== undefined) {
253
+ if (op.ms > bnds.max_runtime_ms) {
254
+ return ReceiptSchema.parse({
255
+ ...receipt,
256
+ admit: false,
257
+ reason: `exceeded max_runtime: requested ${op.ms}ms, limit ${bnds.max_runtime_ms}ms`,
258
+ bound_violated: 'runtime',
259
+ });
260
+ }
261
+ }
262
+
263
+ // Check rewrites bound
264
+ if (bnds.max_graph_rewrites !== undefined && op.rewrites !== undefined) {
265
+ if (op.rewrites > bnds.max_graph_rewrites) {
266
+ return ReceiptSchema.parse({
267
+ ...receipt,
268
+ admit: false,
269
+ reason: `exceeded max_graph_rewrites: requested ${op.rewrites}, limit ${bnds.max_graph_rewrites}`,
270
+ bound_violated: 'rewrites',
271
+ });
272
+ }
273
+ }
274
+
275
+ // Determine which bound was used (for receipt tracking)
276
+ let bound_used = 'none';
277
+ if (op.files !== undefined) bound_used = 'files';
278
+ else if (op.bytes !== undefined) bound_used = 'bytes';
279
+ else if (op.ops !== undefined) bound_used = 'ops';
280
+ else if (op.ms !== undefined) bound_used = 'runtime';
281
+ else if (op.rewrites !== undefined) bound_used = 'rewrites';
282
+
283
+ // All checks passed - admit operation
284
+ return ReceiptSchema.parse({
285
+ ...receipt,
286
+ admit: true,
287
+ bound_used,
288
+ });
289
+ }
@@ -0,0 +1,280 @@
1
+ /**
2
+ * @fileoverview Bulkhead Isolation Pattern
3
+ * Resource pools with separate limits to prevent cascade failures
4
+ */
5
+
6
+ import { z } from 'zod';
7
+ import { generateReceipt } from './receipt.mjs';
8
+
9
+ /**
10
+ * Bulkhead configuration schema
11
+ */
12
+ export const BulkheadConfigSchema = z.object({
13
+ name: z.string(),
14
+ maxConcurrent: z.number().positive(),
15
+ maxQueueSize: z.number().nonnegative(),
16
+ timeout: z.number().positive().optional(),
17
+ });
18
+
19
+ /**
20
+ * @typedef {z.infer<typeof BulkheadConfigSchema>} BulkheadConfig
21
+ */
22
+
23
+ /**
24
+ * Bulkhead statistics
25
+ * @typedef {{
26
+ * name: string,
27
+ * active: number,
28
+ * queued: number,
29
+ * completed: number,
30
+ * rejected: number,
31
+ * timedOut: number,
32
+ * maxConcurrent: number,
33
+ * }} BulkheadStats
34
+ */
35
+
36
+ /**
37
+ * Bulkhead isolation manager for resource pools
38
+ * Prevents cascade failures by isolating resources
39
+ */
40
+ export class BulkheadManager {
41
+ /**
42
+ * @param {BulkheadConfig} config - Bulkhead configuration
43
+ */
44
+ constructor(config) {
45
+ this.config = BulkheadConfigSchema.parse(config);
46
+ this.active = new Set();
47
+ this.queue = [];
48
+ this.stats = {
49
+ name: config.name,
50
+ active: 0,
51
+ queued: 0,
52
+ completed: 0,
53
+ rejected: 0,
54
+ timedOut: 0,
55
+ maxConcurrent: config.maxConcurrent,
56
+ };
57
+ }
58
+
59
+ /**
60
+ * Execute function with bulkhead protection
61
+ * @template T
62
+ * @param {() => Promise<T>} fn - Async function to execute
63
+ * @param {object} [options] - Execution options
64
+ * @param {number} [options.priority=0] - Execution priority (higher = sooner)
65
+ * @returns {Promise<T>} Function result
66
+ */
67
+ async execute(fn, options = {}) {
68
+ const priority = options.priority ?? 0;
69
+
70
+ // Check if at capacity
71
+ if (this.active.size >= this.config.maxConcurrent) {
72
+ // Check queue capacity
73
+ if (this.queue.length >= this.config.maxQueueSize) {
74
+ this.stats.rejected++;
75
+ const error = new Error(`Bulkhead '${this.config.name}' queue full (max: ${this.config.maxQueueSize})`);
76
+ error.code = 'BULKHEAD_QUEUE_FULL';
77
+ throw error;
78
+ }
79
+
80
+ // Add to queue
81
+ return new Promise((resolve, reject) => {
82
+ this.queue.push({ fn, resolve, reject, priority });
83
+ this.queue.sort((a, b) => b.priority - a.priority); // Higher priority first
84
+ this.stats.queued = this.queue.length;
85
+ });
86
+ }
87
+
88
+ // Execute immediately
89
+ return this._executeImmediate(fn);
90
+ }
91
+
92
+ /**
93
+ * Execute function immediately (internal)
94
+ * @template T
95
+ * @param {() => Promise<T>} fn - Function to execute
96
+ * @returns {Promise<T>} Function result
97
+ * @private
98
+ */
99
+ async _executeImmediate(fn) {
100
+ const taskId = Symbol('task');
101
+ this.active.add(taskId);
102
+ this.stats.active = this.active.size;
103
+
104
+ try {
105
+ // Apply timeout if configured
106
+ const result = this.config.timeout
107
+ ? await this._withTimeout(fn(), this.config.timeout)
108
+ : await fn();
109
+
110
+ this.stats.completed++;
111
+ return result;
112
+ } catch (error) {
113
+ if (error.code === 'BULKHEAD_TIMEOUT') {
114
+ this.stats.timedOut++;
115
+ }
116
+ throw error;
117
+ } finally {
118
+ this.active.delete(taskId);
119
+ this.stats.active = this.active.size;
120
+ this._processQueue();
121
+ }
122
+ }
123
+
124
+ /**
125
+ * Process queued tasks
126
+ * @private
127
+ */
128
+ _processQueue() {
129
+ while (this.queue.length > 0 && this.active.size < this.config.maxConcurrent) {
130
+ const task = this.queue.shift();
131
+ this.stats.queued = this.queue.length;
132
+
133
+ this._executeImmediate(task.fn)
134
+ .then(task.resolve)
135
+ .catch(task.reject);
136
+ }
137
+ }
138
+
139
+ /**
140
+ * Wrap promise with timeout
141
+ * @template T
142
+ * @param {Promise<T>} promise - Promise to wrap
143
+ * @param {number} timeout - Timeout in milliseconds
144
+ * @returns {Promise<T>} Promise with timeout
145
+ * @private
146
+ */
147
+ async _withTimeout(promise, timeout) {
148
+ return Promise.race([
149
+ promise,
150
+ new Promise((_, reject) => {
151
+ setTimeout(() => {
152
+ const error = new Error(`Bulkhead '${this.config.name}' operation timed out after ${timeout}ms`);
153
+ error.code = 'BULKHEAD_TIMEOUT';
154
+ reject(error);
155
+ }, timeout);
156
+ }),
157
+ ]);
158
+ }
159
+
160
+ /**
161
+ * Get current statistics
162
+ * @returns {BulkheadStats} Current statistics
163
+ */
164
+ getStats() {
165
+ return { ...this.stats };
166
+ }
167
+
168
+ /**
169
+ * Clear queue (reject all pending)
170
+ * @returns {number} Number of tasks rejected
171
+ */
172
+ clearQueue() {
173
+ const count = this.queue.length;
174
+ const error = new Error(`Bulkhead '${this.config.name}' queue cleared`);
175
+ error.code = 'BULKHEAD_CLEARED';
176
+
177
+ for (const task of this.queue) {
178
+ task.reject(error);
179
+ }
180
+
181
+ this.queue = [];
182
+ this.stats.queued = 0;
183
+ this.stats.rejected += count;
184
+
185
+ return count;
186
+ }
187
+
188
+ /**
189
+ * Wait for all active tasks to complete
190
+ * @returns {Promise<void>}
191
+ */
192
+ async drain() {
193
+ while (this.active.size > 0 || this.queue.length > 0) {
194
+ await new Promise(resolve => setTimeout(resolve, 10));
195
+ }
196
+ }
197
+
198
+ /**
199
+ * Generate receipt for bulkhead operation
200
+ * @param {string} operation - Operation name
201
+ * @param {Record<string, any>} inputs - Operation inputs
202
+ * @param {Record<string, any>} outputs - Operation outputs
203
+ * @returns {Promise<import('./receipt.mjs').Receipt>} Receipt
204
+ */
205
+ async generateReceipt(operation, inputs, outputs) {
206
+ return generateReceipt(
207
+ `bulkhead:${this.config.name}:${operation}`,
208
+ { ...inputs, bulkhead: this.config.name },
209
+ { ...outputs, stats: this.getStats() }
210
+ );
211
+ }
212
+ }
213
+
214
+ /**
215
+ * Multi-bulkhead coordinator for managing multiple resource pools
216
+ */
217
+ export class BulkheadCoordinator {
218
+ constructor() {
219
+ /** @type {Map<string, BulkheadManager>} */
220
+ this.bulkheads = new Map();
221
+ }
222
+
223
+ /**
224
+ * Register a bulkhead
225
+ * @param {BulkheadConfig} config - Bulkhead configuration
226
+ * @returns {BulkheadManager} Created bulkhead
227
+ */
228
+ register(config) {
229
+ const bulkhead = new BulkheadManager(config);
230
+ this.bulkheads.set(config.name, bulkhead);
231
+ return bulkhead;
232
+ }
233
+
234
+ /**
235
+ * Get bulkhead by name
236
+ * @param {string} name - Bulkhead name
237
+ * @returns {BulkheadManager | undefined} Bulkhead manager
238
+ */
239
+ get(name) {
240
+ return this.bulkheads.get(name);
241
+ }
242
+
243
+ /**
244
+ * Execute on named bulkhead
245
+ * @template T
246
+ * @param {string} name - Bulkhead name
247
+ * @param {() => Promise<T>} fn - Function to execute
248
+ * @param {object} [options] - Execution options
249
+ * @returns {Promise<T>} Function result
250
+ */
251
+ async execute(name, fn, options) {
252
+ const bulkhead = this.bulkheads.get(name);
253
+ if (!bulkhead) {
254
+ throw new Error(`Bulkhead '${name}' not found`);
255
+ }
256
+ return bulkhead.execute(fn, options);
257
+ }
258
+
259
+ /**
260
+ * Get statistics for all bulkheads
261
+ * @returns {Record<string, BulkheadStats>} All statistics
262
+ */
263
+ getAllStats() {
264
+ const stats = {};
265
+ for (const [name, bulkhead] of this.bulkheads.entries()) {
266
+ stats[name] = bulkhead.getStats();
267
+ }
268
+ return stats;
269
+ }
270
+
271
+ /**
272
+ * Drain all bulkheads
273
+ * @returns {Promise<void>}
274
+ */
275
+ async drainAll() {
276
+ await Promise.all(
277
+ Array.from(this.bulkheads.values()).map(b => b.drain())
278
+ );
279
+ }
280
+ }