@platformatic/basic 3.4.1 → 3.5.1

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,734 @@
1
+ import {
2
+ buildPinoOptions,
3
+ deepmerge,
4
+ executeWithTimeout,
5
+ kHandledError,
6
+ kMetadata,
7
+ kTimeout
8
+ } from '@platformatic/foundation'
9
+ import { client, collectMetrics, ensureMetricsGroup } from '@platformatic/metrics'
10
+ import { parseCommandString } from 'execa'
11
+ import { spawn } from 'node:child_process'
12
+ import EventEmitter, { once } from 'node:events'
13
+ import { existsSync } from 'node:fs'
14
+ import { platform } from 'node:os'
15
+ import { pathToFileURL } from 'node:url'
16
+ import { workerData } from 'node:worker_threads'
17
+ import pino from 'pino'
18
+ import { NonZeroExitCode } from './errors.js'
19
+ import { cleanBasePath } from './utils.js'
20
+ import { ChildManager } from './worker/child-manager.js'
21
+ const kITC = Symbol.for('plt.runtime.itc')
22
+
23
+ export class BaseCapability extends EventEmitter {
24
+ status
25
+ type
26
+ version
27
+ root
28
+ config
29
+ context
30
+ standardStreams
31
+
32
+ applicationId
33
+ workerId
34
+ telemetryConfig
35
+ serverConfig
36
+ openapiSchema
37
+ graphqlSchema
38
+ connectionString
39
+ basePath
40
+ isEntrypoint
41
+ isProduction
42
+ dependencies
43
+ customHealthCheck
44
+ customReadinessCheck
45
+ clientWs
46
+ runtimeConfig
47
+ stdout
48
+ stderr
49
+ subprocessForceClose
50
+ subprocessTerminationSignal
51
+ logger
52
+ metricsRegistr
53
+
54
+ #subprocessStarted
55
+ #metricsCollected
56
+ #pendingDependenciesWaits
57
+
58
+ constructor (type, version, root, config, context, standardStreams = {}) {
59
+ super()
60
+
61
+ this.status = ''
62
+ this.type = type
63
+ this.version = version
64
+ this.root = root
65
+ this.config = config
66
+ this.context = context ?? {}
67
+ this.context.worker ??= { count: 1, index: 0 }
68
+ this.standardStreams = standardStreams
69
+
70
+ this.applicationId = this.context.applicationId
71
+ this.workerId = this.context.worker.count > 1 ? this.context.worker.index : undefined
72
+ this.telemetryConfig = this.context.telemetryConfig
73
+ this.serverConfig = deepmerge(this.context.serverConfig ?? {}, config.server ?? {})
74
+ this.openapiSchema = null
75
+ this.graphqlSchema = null
76
+ this.connectionString = null
77
+ this.basePath = null
78
+ this.isEntrypoint = this.context.isEntrypoint
79
+ this.isProduction = this.context.isProduction
80
+ this.dependencies = this.context.dependencies ?? []
81
+ this.customHealthCheck = null
82
+ this.customReadinessCheck = null
83
+ this.clientWs = null
84
+ this.runtimeConfig = deepmerge(this.context?.runtimeConfig ?? {}, workerData?.config ?? {})
85
+ this.stdout = standardStreams?.stdout ?? process.stdout
86
+ this.stderr = standardStreams?.stderr ?? process.stderr
87
+ this.subprocessForceClose = false
88
+ this.subprocessTerminationSignal = 'SIGINT'
89
+ this.logger = this._initializeLogger()
90
+
91
+ // Setup globals
92
+ this.registerGlobals({
93
+ capability: this,
94
+ applicationId: this.applicationId,
95
+ workerId: this.workerId,
96
+ logLevel: this.logger.level,
97
+ // Always use URL to avoid serialization problem in Windows
98
+ root: pathToFileURL(this.root).toString(),
99
+ setOpenapiSchema: this.setOpenapiSchema.bind(this),
100
+ setGraphqlSchema: this.setGraphqlSchema.bind(this),
101
+ setConnectionString: this.setConnectionString.bind(this),
102
+ setBasePath: this.setBasePath.bind(this),
103
+ runtimeBasePath: this.runtimeConfig?.basePath ?? null,
104
+ invalidateHttpCache: this.#invalidateHttpCache.bind(this),
105
+ setCustomHealthCheck: this.setCustomHealthCheck.bind(this),
106
+ setCustomReadinessCheck: this.setCustomReadinessCheck.bind(this),
107
+ notifyConfig: this.notifyConfig.bind(this),
108
+ logger: this.logger
109
+ })
110
+
111
+ if (globalThis.platformatic.prometheus) {
112
+ this.metricsRegistry = globalThis.platformatic.prometheus.registry
113
+ } else {
114
+ this.metricsRegistry = new client.Registry()
115
+ this.registerGlobals({ prometheus: { client, registry: this.metricsRegistry } })
116
+ }
117
+
118
+ this.#metricsCollected = false
119
+ this.#pendingDependenciesWaits = new Set()
120
+ }
121
+
122
+ async init () {
123
+ if (this.status) {
124
+ return
125
+ }
126
+
127
+ // Wait for explicit dependencies to start
128
+ await this.waitForDependenciesStart(this.dependencies)
129
+
130
+ if (this.status === 'stopped') {
131
+ return
132
+ }
133
+
134
+ await this.updateContext()
135
+ this.status = 'init'
136
+ }
137
+
138
+ updateContext (_context) {
139
+ // No-op by default
140
+ }
141
+
142
+ start () {
143
+ throw new Error('BaseCapability.start must be overriden by the subclasses')
144
+ }
145
+
146
+ async stop () {
147
+ if (this.#pendingDependenciesWaits.size > 0) {
148
+ await Promise.allSettled(this.#pendingDependenciesWaits)
149
+ }
150
+
151
+ this.status = 'stopped'
152
+ }
153
+
154
+ build () {
155
+ // No-op by default
156
+ }
157
+
158
+ // Alias for stop
159
+ close () {
160
+ return this.stop()
161
+ }
162
+
163
+ inject () {
164
+ throw new Error('BaseCapability.inject must be overriden by the subclasses')
165
+ }
166
+
167
+ async waitForDependenciesStart (dependencies = []) {
168
+ if (!globalThis[kITC]) {
169
+ return
170
+ }
171
+
172
+ const pending = new Set(dependencies)
173
+
174
+ // Ask the runtime the status of the dependencies and don't wait if they are already started
175
+ const workers = await globalThis[kITC].send('getWorkers')
176
+
177
+ for (const worker of Object.values(workers)) {
178
+ if (this.dependencies.includes(worker.application) && worker.status === 'started') {
179
+ pending.delete(worker.application)
180
+ }
181
+ }
182
+
183
+ if (!pending.size) {
184
+ return
185
+ }
186
+
187
+ this.logger.info({ dependencies: Array.from(pending) }, 'Waiting for dependencies to start.')
188
+
189
+ const { promise, resolve, reject } = Promise.withResolvers()
190
+
191
+ function runtimeEventHandler ({ event, payload }) {
192
+ if (event !== 'application:worker:started') {
193
+ return
194
+ }
195
+
196
+ pending.delete(payload.application)
197
+
198
+ if (pending.size === 0) {
199
+ cleanupEvents()
200
+ resolve()
201
+ }
202
+ }
203
+
204
+ function stopHandler () {
205
+ cleanupEvents()
206
+
207
+ const error = new Error('One of the service dependencies was unable to start.')
208
+ error.dependencies = dependencies
209
+ error[kHandledError] = true
210
+ reject(error)
211
+ }
212
+
213
+ const cleanupEvents = () => {
214
+ globalThis[kITC].removeListener('runtime:event', runtimeEventHandler)
215
+ this.context.controller.removeListener('stopping', stopHandler)
216
+ this.#pendingDependenciesWaits.delete(promise)
217
+ }
218
+
219
+ globalThis[kITC].on('runtime:event', runtimeEventHandler)
220
+ this.context.controller.on('stopping', stopHandler)
221
+ this.#pendingDependenciesWaits.add(promise)
222
+
223
+ return promise
224
+ }
225
+
226
+ async waitForDependentsStop (dependents = []) {
227
+ if (!globalThis[kITC]) {
228
+ return
229
+ }
230
+
231
+ const pending = new Set(dependents)
232
+
233
+ // Ask the runtime the status of the dependencies and don't wait if they are already stopped
234
+ const workers = await globalThis[kITC].send('getWorkers')
235
+
236
+ for (const worker of Object.values(workers)) {
237
+ if (this.dependencies.includes(worker.application) && worker.status === 'started') {
238
+ pending.delete(worker.application)
239
+ }
240
+ }
241
+
242
+ if (!pending.size) {
243
+ return
244
+ }
245
+
246
+ this.logger.info({ dependents: Array.from(pending) }, 'Waiting for dependents to stop.')
247
+
248
+ const { promise, resolve } = Promise.withResolvers()
249
+
250
+ function runtimeEventHandler ({ event, payload }) {
251
+ if (event !== 'application:worker:stopped') {
252
+ return
253
+ }
254
+
255
+ pending.delete(payload.application)
256
+
257
+ if (pending.size === 0) {
258
+ globalThis[kITC].removeListener('runtime:event', runtimeEventHandler)
259
+ resolve()
260
+ }
261
+ }
262
+
263
+ globalThis[kITC].on('runtime:event', runtimeEventHandler)
264
+ return promise
265
+ }
266
+
267
+ getUrl () {
268
+ return this.url
269
+ }
270
+
271
+ async getConfig (includeMeta = false) {
272
+ if (includeMeta) {
273
+ return this.config
274
+ }
275
+
276
+ const { [kMetadata]: _, ...config } = this.config
277
+ return config
278
+ }
279
+
280
+ async getEnv () {
281
+ return this.config[kMetadata].env
282
+ }
283
+
284
+ async getWatchConfig () {
285
+ const config = this.config
286
+
287
+ const enabled = config.watch?.enabled !== false
288
+
289
+ if (!enabled) {
290
+ return { enabled, path: this.root }
291
+ }
292
+
293
+ return {
294
+ enabled,
295
+ path: this.root,
296
+ allow: config.watch?.allow,
297
+ ignore: config.watch?.ignore
298
+ }
299
+ }
300
+
301
+ async getInfo () {
302
+ return { type: this.type, version: this.version, dependencies: this.dependencies }
303
+ }
304
+
305
+ getDispatchFunc () {
306
+ return this
307
+ }
308
+
309
+ async getDispatchTarget () {
310
+ return this.getUrl() ?? (await this.getDispatchFunc())
311
+ }
312
+
313
+ getMeta () {
314
+ return {
315
+ gateway: {
316
+ wantsAbsoluteUrls: false
317
+ }
318
+ }
319
+ }
320
+
321
+ async getMetrics ({ format } = {}) {
322
+ if (this.childManager && this.clientWs) {
323
+ return this.childManager.send(this.clientWs, 'getMetrics', { format })
324
+ }
325
+
326
+ return format === 'json' ? await this.metricsRegistry.getMetricsAsJSON() : await this.metricsRegistry.metrics()
327
+ }
328
+
329
+ async getOpenapiSchema () {
330
+ return this.openapiSchema
331
+ }
332
+
333
+ async getGraphqlSchema () {
334
+ return this.graphqlSchema
335
+ }
336
+
337
+ setOpenapiSchema (schema) {
338
+ this.openapiSchema = schema
339
+ }
340
+
341
+ setGraphqlSchema (schema) {
342
+ this.graphqlSchema = schema
343
+ }
344
+
345
+ setCustomHealthCheck (fn) {
346
+ this.customHealthCheck = fn
347
+ }
348
+
349
+ setCustomReadinessCheck (fn) {
350
+ this.customReadinessCheck = fn
351
+ }
352
+
353
+ async getCustomHealthCheck () {
354
+ if (!this.customHealthCheck) {
355
+ return true
356
+ }
357
+ return await this.customHealthCheck()
358
+ }
359
+
360
+ async getCustomReadinessCheck () {
361
+ if (!this.customReadinessCheck) {
362
+ return true
363
+ }
364
+ return await this.customReadinessCheck()
365
+ }
366
+
367
+ setConnectionString (connectionString) {
368
+ this.connectionString = connectionString
369
+ }
370
+
371
+ setBasePath (basePath) {
372
+ this.basePath = basePath
373
+ }
374
+
375
+ async log ({ message, level }) {
376
+ const logLevel = level ?? 'info'
377
+ this.logger[logLevel](message)
378
+ }
379
+
380
+ registerGlobals (globals) {
381
+ globalThis.platformatic = Object.assign(globalThis.platformatic ?? {}, globals)
382
+ }
383
+
384
+ verifyOutputDirectory (path) {
385
+ if (this.isProduction && !existsSync(path)) {
386
+ throw new Error(
387
+ `Cannot access directory '${path}'. Please run the 'build' command before running in production mode.`
388
+ )
389
+ }
390
+ }
391
+
392
+ async buildWithCommand (command, basePath, opts = {}) {
393
+ const { loader, scripts, context, disableChildManager } = opts
394
+
395
+ if (Array.isArray(command)) {
396
+ command = command.join(' ')
397
+ }
398
+
399
+ this.logger.debug(`Executing "${command}" ...`)
400
+
401
+ const baseContext = await this.getChildManagerContext(basePath)
402
+ this.childManager = disableChildManager
403
+ ? null
404
+ : new ChildManager({
405
+ logger: this.logger,
406
+ loader,
407
+ scripts,
408
+ context: { ...baseContext, isBuilding: true, ...context }
409
+ })
410
+
411
+ try {
412
+ await this.childManager?.inject()
413
+
414
+ const subprocess = await this.spawn(command)
415
+ const [exitCode] = await once(subprocess, 'exit')
416
+
417
+ if (exitCode !== 0) {
418
+ const error = new NonZeroExitCode(exitCode)
419
+ error.exitCode = exitCode
420
+ throw error
421
+ }
422
+ } finally {
423
+ await this.childManager?.eject()
424
+ await this.childManager?.close()
425
+ }
426
+ }
427
+
428
+ async startWithCommand (command, loader, scripts) {
429
+ const config = this.config
430
+ const basePath = config.application?.basePath ? cleanBasePath(config.application?.basePath) : ''
431
+
432
+ const context = await this.getChildManagerContext(basePath)
433
+ this.childManager = new ChildManager({
434
+ logger: this.logger,
435
+ loader,
436
+ context,
437
+ scripts
438
+ })
439
+
440
+ this.childManager.on('config', config => {
441
+ this.subprocessConfig = config
442
+ this.notifyConfig(config)
443
+ })
444
+
445
+ this.childManager.on('connectionString', connectionString => {
446
+ this.connectionString = connectionString
447
+ })
448
+
449
+ this.childManager.on('openapiSchema', schema => {
450
+ this.openapiSchema = schema
451
+ })
452
+
453
+ this.childManager.on('graphqlSchema', schema => {
454
+ this.graphqlSchema = schema
455
+ })
456
+
457
+ this.childManager.on('basePath', path => {
458
+ this.basePath = path
459
+ })
460
+
461
+ this.childManager.on('event', event => {
462
+ globalThis[kITC].notify('event', event)
463
+ this.emit('application:worker:event', config)
464
+ })
465
+
466
+ // This is not really important for the URL but sometimes it also a sign
467
+ // that the process has been replaced and thus we need to update the client WebSocket
468
+ this.childManager.on('url', (url, clientWs) => {
469
+ this.url = url
470
+ this.clientWs = clientWs
471
+ })
472
+
473
+ try {
474
+ await this.childManager.inject()
475
+ this.subprocess = await this.spawn(command)
476
+ this.#subprocessStarted = true
477
+ } catch (e) {
478
+ this.childManager.close()
479
+ throw new Error(`Cannot execute command "${command}": executable not found`)
480
+ } finally {
481
+ await this.childManager.eject()
482
+ }
483
+
484
+ // If the process exits prematurely, terminate the thread with the same code
485
+ this.subprocess.on('exit', code => {
486
+ if (this.#subprocessStarted && typeof code === 'number' && code !== 0) {
487
+ this.childManager.close()
488
+ process.exit(code)
489
+ }
490
+ })
491
+
492
+ const [url, clientWs] = await once(this.childManager, 'url')
493
+ this.url = url
494
+ this.clientWs = clientWs
495
+
496
+ await this._collectMetrics()
497
+ }
498
+
499
+ async stopCommand () {
500
+ const exitTimeout = this.runtimeConfig.gracefulShutdown.application
501
+
502
+ this.#subprocessStarted = false
503
+ const exitPromise = once(this.subprocess, 'exit')
504
+
505
+ // Attempt graceful close on the process
506
+ const handled = await this.childManager.send(this.clientWs, 'close', this.subprocessTerminationSignal)
507
+
508
+ if (!handled && this.subprocessForceClose) {
509
+ this.subprocess.kill(this.subprocessTerminationSignal)
510
+ }
511
+
512
+ // If the process hasn't exited in X seconds, kill it in the polite way
513
+ /* c8 ignore next 10 */
514
+ const res = await executeWithTimeout(exitPromise, exitTimeout)
515
+
516
+ if (res === kTimeout) {
517
+ this.subprocess.kill(this.subprocessTerminationSignal)
518
+
519
+ // If the process hasn't exited in X seconds, kill it the hard way
520
+ const res = await executeWithTimeout(exitPromise, exitTimeout)
521
+ if (res === kTimeout) {
522
+ this.subprocess.kill('SIGKILL')
523
+ }
524
+ }
525
+
526
+ await exitPromise
527
+
528
+ // Close the manager
529
+ await this.childManager.close()
530
+ }
531
+
532
+ getChildManager () {
533
+ return this.childManager
534
+ }
535
+
536
+ async getChildManagerContext (basePath) {
537
+ const meta = await this.getMeta()
538
+
539
+ return {
540
+ id: this.id,
541
+ config: this.config,
542
+ applicationId: this.applicationId,
543
+ workerId: this.workerId,
544
+ // Always use URL to avoid serialization problem in Windows
545
+ root: pathToFileURL(this.root).toString(),
546
+ basePath,
547
+ logLevel: this.logger.level,
548
+ isEntrypoint: this.isEntrypoint,
549
+ runtimeBasePath: this.runtimeConfig?.basePath ?? null,
550
+ wantsAbsoluteUrls: meta.gateway?.wantsAbsoluteUrls ?? false,
551
+ exitOnUnhandledErrors: this.runtimeConfig.exitOnUnhandledErrors ?? true,
552
+ /* c8 ignore next 2 - else */
553
+ port: (this.isEntrypoint ? this.serverConfig?.port || 0 : undefined) ?? true,
554
+ host: (this.isEntrypoint ? this.serverConfig?.hostname : undefined) ?? true,
555
+ telemetryConfig: this.telemetryConfig
556
+ }
557
+ }
558
+
559
+ async spawn (command) {
560
+ const [executable, ...args] = parseCommandString(command)
561
+ const hasChainedCommands = command.includes('&&') || command.includes('||') || command.includes(';')
562
+
563
+ /* c8 ignore next 3 */
564
+ const subprocess =
565
+ platform() === 'win32'
566
+ ? spawn(command, { cwd: this.root, shell: true, windowsVerbatimArguments: true })
567
+ : spawn(executable, args, { cwd: this.root, shell: hasChainedCommands })
568
+
569
+ subprocess.stdout.setEncoding('utf8')
570
+ subprocess.stderr.setEncoding('utf8')
571
+
572
+ subprocess.stdout.pipe(this.stdout, { end: false })
573
+ subprocess.stderr.pipe(this.stderr, { end: false })
574
+
575
+ // Wait for the process to be started
576
+ await new Promise((resolve, reject) => {
577
+ subprocess.on('spawn', resolve)
578
+ subprocess.on('error', reject)
579
+ })
580
+
581
+ return subprocess
582
+ }
583
+
584
+ notifyConfig (config) {
585
+ this.emit('config', config)
586
+ }
587
+
588
+ _initializeLogger () {
589
+ const loggerOptions = deepmerge(this.runtimeConfig?.logger ?? {}, this.config?.logger ?? {})
590
+ const pinoOptions = buildPinoOptions(
591
+ loggerOptions,
592
+ this.serverConfig?.logger,
593
+ this.applicationId,
594
+ this.workerId,
595
+ this.context,
596
+ this.root
597
+ )
598
+
599
+ return pino(pinoOptions, this.standardStreams?.stdout)
600
+ }
601
+
602
+ async _collectMetrics () {
603
+ if (this.#metricsCollected) {
604
+ return
605
+ }
606
+
607
+ this.#metricsCollected = true
608
+
609
+ if (this.context.metricsConfig === false) {
610
+ return
611
+ }
612
+
613
+ await this.#collectMetrics()
614
+ this.#setHttpCacheMetrics()
615
+ }
616
+
617
+ async #collectMetrics () {
618
+ const metricsConfig = {
619
+ defaultMetrics: true,
620
+ httpMetrics: true,
621
+ ...this.context.metricsConfig
622
+ }
623
+
624
+ if (this.childManager && this.clientWs) {
625
+ await this.childManager.send(this.clientWs, 'collectMetrics', {
626
+ applicationId: this.applicationId,
627
+ workerId: this.workerId,
628
+ metricsConfig
629
+ })
630
+ return
631
+ }
632
+
633
+ await collectMetrics(this.applicationId, this.workerId, metricsConfig, this.metricsRegistry)
634
+ }
635
+
636
+ #setHttpCacheMetrics () {
637
+ const { client, registry } = globalThis.platformatic.prometheus
638
+
639
+ // Metrics already registered, no need to register them again
640
+ if (ensureMetricsGroup(registry, 'http.cache')) {
641
+ return
642
+ }
643
+
644
+ const cacheHitMetric = new client.Counter({
645
+ name: 'http_cache_hit_count',
646
+ help: 'Number of http cache hits',
647
+ registers: [registry]
648
+ })
649
+
650
+ const cacheMissMetric = new client.Counter({
651
+ name: 'http_cache_miss_count',
652
+ help: 'Number of http cache misses',
653
+ registers: [registry]
654
+ })
655
+
656
+ globalThis.platformatic.onHttpCacheHit = () => {
657
+ cacheHitMetric.inc()
658
+ }
659
+ globalThis.platformatic.onHttpCacheMiss = () => {
660
+ cacheMissMetric.inc()
661
+ }
662
+
663
+ const httpStatsFreeMetric = new client.Gauge({
664
+ name: 'http_client_stats_free',
665
+ help: 'Number of free (idle) http clients (sockets)',
666
+ labelNames: ['dispatcher_stats_url'],
667
+ registers: [registry]
668
+ })
669
+ globalThis.platformatic.onHttpStatsFree = (url, val) => {
670
+ httpStatsFreeMetric.set({ dispatcher_stats_url: url }, val)
671
+ }
672
+
673
+ const httpStatsConnectedMetric = new client.Gauge({
674
+ name: 'http_client_stats_connected',
675
+ help: 'Number of open socket connections',
676
+ labelNames: ['dispatcher_stats_url'],
677
+ registers: [registry]
678
+ })
679
+ globalThis.platformatic.onHttpStatsConnected = (url, val) => {
680
+ httpStatsConnectedMetric.set({ dispatcher_stats_url: url }, val)
681
+ }
682
+
683
+ const httpStatsPendingMetric = new client.Gauge({
684
+ name: 'http_client_stats_pending',
685
+ help: 'Number of pending requests across all clients',
686
+ labelNames: ['dispatcher_stats_url'],
687
+ registers: [registry]
688
+ })
689
+ globalThis.platformatic.onHttpStatsPending = (url, val) => {
690
+ httpStatsPendingMetric.set({ dispatcher_stats_url: url }, val)
691
+ }
692
+
693
+ const httpStatsQueuedMetric = new client.Gauge({
694
+ name: 'http_client_stats_queued',
695
+ help: 'Number of queued requests across all clients',
696
+ labelNames: ['dispatcher_stats_url'],
697
+ registers: [registry]
698
+ })
699
+ globalThis.platformatic.onHttpStatsQueued = (url, val) => {
700
+ httpStatsQueuedMetric.set({ dispatcher_stats_url: url }, val)
701
+ }
702
+
703
+ const httpStatsRunningMetric = new client.Gauge({
704
+ name: 'http_client_stats_running',
705
+ help: 'Number of currently active requests across all clients',
706
+ labelNames: ['dispatcher_stats_url'],
707
+ registers: [registry]
708
+ })
709
+ globalThis.platformatic.onHttpStatsRunning = (url, val) => {
710
+ httpStatsRunningMetric.set({ dispatcher_stats_url: url }, val)
711
+ }
712
+
713
+ const httpStatsSizeMetric = new client.Gauge({
714
+ name: 'http_client_stats_size',
715
+ help: 'Number of active, pending, or queued requests across all clients',
716
+ labelNames: ['dispatcher_stats_url'],
717
+ registers: [registry]
718
+ })
719
+ globalThis.platformatic.onHttpStatsSize = (url, val) => {
720
+ httpStatsSizeMetric.set({ dispatcher_stats_url: url }, val)
721
+ }
722
+
723
+ const activeResourcesEventLoopMetric = new client.Gauge({
724
+ name: 'active_resources_event_loop',
725
+ help: 'Number of active resources keeping the event loop alive',
726
+ registers: [registry]
727
+ })
728
+ globalThis.platformatic.onActiveResourcesEventLoop = val => activeResourcesEventLoopMetric.set(val)
729
+ }
730
+
731
+ async #invalidateHttpCache (opts = {}) {
732
+ await globalThis[kITC].send('invalidateHttpCache', opts)
733
+ }
734
+ }