@platformatic/runtime 3.55.0 → 3.57.0

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/config.d.ts CHANGED
@@ -333,6 +333,33 @@ export type PlatformaticRuntimeConfig = {
333
333
  enabled?: boolean | string;
334
334
  hostname?: string;
335
335
  endpoint?: string;
336
+ https?: {
337
+ allowHTTP1?: boolean;
338
+ key:
339
+ | string
340
+ | {
341
+ path?: string;
342
+ }
343
+ | (
344
+ | string
345
+ | {
346
+ path?: string;
347
+ }
348
+ )[];
349
+ cert:
350
+ | string
351
+ | {
352
+ path?: string;
353
+ }
354
+ | (
355
+ | string
356
+ | {
357
+ path?: string;
358
+ }
359
+ )[];
360
+ requestCert?: boolean;
361
+ rejectUnauthorized?: boolean;
362
+ };
336
363
  auth?: {
337
364
  username: string;
338
365
  password: string;
@@ -471,6 +498,14 @@ export type PlatformaticRuntimeConfig = {
471
498
  * The path to write the traces to. Only for file exporter.
472
499
  */
473
500
  path?: string;
501
+ /**
502
+ * The OTLP transport protocol to use. Only for the otlp exporter. Defaults to http.
503
+ */
504
+ protocol?: "http" | "grpc";
505
+ /**
506
+ * Alias for protocol. Only for the otlp exporter. Defaults to http.
507
+ */
508
+ transport?: "http" | "grpc";
474
509
  [k: string]: unknown;
475
510
  };
476
511
  additionalProperties?: never;
@@ -496,6 +531,14 @@ export type PlatformaticRuntimeConfig = {
496
531
  * The path to write the traces to. Only for file exporter.
497
532
  */
498
533
  path?: string;
534
+ /**
535
+ * The OTLP transport protocol to use. Only for the otlp exporter. Defaults to http.
536
+ */
537
+ protocol?: "http" | "grpc";
538
+ /**
539
+ * Alias for protocol. Only for the otlp exporter. Defaults to http.
540
+ */
541
+ transport?: "http" | "grpc";
499
542
  [k: string]: unknown;
500
543
  };
501
544
  additionalProperties?: never;
package/index.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { FastifyError } from '@fastify/error'
2
2
  import { Configuration, ConfigurationOptions, logFatalError, parseArgs } from '@platformatic/foundation'
3
3
  import { BaseGenerator } from '@platformatic/generators'
4
+ import { PlatformaticGlobal } from '@platformatic/globals'
4
5
  import { JSONSchemaType } from 'ajv'
5
6
  import * as colorette from 'colorette'
6
7
  import { Logger } from 'pino'
@@ -27,6 +28,12 @@ export interface ApplicationsCommands {
27
28
  help: Record<string, string | (() => string)>
28
29
  }
29
30
 
31
+ export interface LoopbackMessagingOptions {
32
+ logger?: Logger
33
+ mount?: boolean
34
+ runtimeConfig?: { messagingTimeout?: number; [key: string]: unknown }
35
+ }
36
+
30
37
  export namespace errors {
31
38
  export const RuntimeExitedError: () => FastifyError
32
39
  export const UnknownRuntimeAPICommandError: (command: string) => FastifyError
@@ -61,7 +68,6 @@ export namespace symbols {
61
68
  export const kWorkerStatus: unique symbol
62
69
  export const kWorkerHealthSignals: unique symbol
63
70
  export const kStderrMarker: string
64
- export const kInterceptors: unique symbol
65
71
  export const kWorkersBroadcast: unique symbol
66
72
  }
67
73
 
@@ -191,3 +197,8 @@ export declare function transform (
191
197
  ): Promise<RuntimeConfiguration>
192
198
 
193
199
  export declare function loadApplicationsCommands (): Promise<ApplicationsCommands>
200
+
201
+ export declare function setupLoopbackMessaging (
202
+ targetId: string,
203
+ options?: LoopbackMessagingOptions
204
+ ): PlatformaticGlobal['messaging'] & { unmount: () => void }
package/index.js CHANGED
@@ -176,6 +176,7 @@ export async function create (configOrRoot, sourceOrConfig, context) {
176
176
  export { prepareApplication, transform, wrapInRuntimeConfig } from './lib/config.js'
177
177
  export * as errors from './lib/errors.js'
178
178
  export { RuntimeGenerator as Generator, WrappedGenerator } from './lib/generator.js'
179
+ export { setupLoopbackMessaging } from './lib/loopback-messaging.js'
179
180
  export { Runtime } from './lib/runtime.js'
180
181
  export { schema } from './lib/schema.js'
181
182
  export * from './lib/version.js'
package/lib/config.js CHANGED
@@ -18,8 +18,7 @@ import {
18
18
  InspectorHostError,
19
19
  InspectorPortError,
20
20
  InvalidArgumentError,
21
- InvalidEntrypointError,
22
- MissingEntrypointError
21
+ InvalidEntrypointError
23
22
  } from './errors.js'
24
23
  import { schema } from './schema.js'
25
24
  import { upgrade } from './upgrade.js'
@@ -438,15 +437,8 @@ export async function transform (config, _, context) {
438
437
  }
439
438
  }
440
439
 
441
- if (!hasValidEntrypoint && !context.allowMissingEntrypoint) {
442
- if (config.entrypoint) {
443
- throw new InvalidEntrypointError(config.entrypoint)
444
- } else if (applications.length >= 1) {
445
- throw new MissingEntrypointError()
446
- }
447
- // If there are no applications, and no entrypoint it's an empty app.
448
- // It won't start, but we should be able to parse and operate on it,
449
- // like adding other applications.
440
+ if (!hasValidEntrypoint && config.entrypoint && !context.allowMissingEntrypoint) {
441
+ throw new InvalidEntrypointError(config.entrypoint)
450
442
  }
451
443
 
452
444
  if (typeof config.metrics === 'boolean') {
package/lib/errors.js CHANGED
@@ -49,6 +49,10 @@ export const ApplicationStartTimeoutError = createError(
49
49
  `${ERROR_PREFIX}_APPLICATION_START_TIMEOUT`,
50
50
  "Application with id '%s' failed to start in %dms."
51
51
  )
52
+ export const ApplicationDependencyNotFoundError = createError(
53
+ `${ERROR_PREFIX}_APPLICATION_DEPENDENCY_NOT_FOUND`,
54
+ 'Application dependency %s not found. Available applications are: %s'
55
+ )
52
56
  export const ApplicationsDependenciesCycleError = createError(
53
57
  `${ERROR_PREFIX}_APPLICATIONS_DEPENDENCIES_CYCLE`,
54
58
  'Detected a cycle in the applications dependencies: %s'
package/lib/generator.js CHANGED
@@ -19,17 +19,6 @@ import { transform } from './config.js'
19
19
  import { schema } from './schema.js'
20
20
  import { getArrayDifference } from './utils.js'
21
21
 
22
- const wrappableProperties = {
23
- logger: {
24
- level: '{PLT_SERVER_LOGGER_LEVEL}'
25
- },
26
- server: {
27
- hostname: '{PLT_SERVER_HOSTNAME}',
28
- port: '{PORT}'
29
- },
30
- managementApi: '{PLT_MANAGEMENT_API}'
31
- }
32
-
33
22
  const engines = {
34
23
  node: '>=22.19.0'
35
24
  }
@@ -42,13 +31,36 @@ const NoApplicationNamedError = createError(
42
31
  )
43
32
  const NoEntryPointError = createError(`${ERROR_PREFIX}_NO_ENTRYPOINT`, 'No entrypoint had been defined.')
44
33
 
34
+ function getRuntimeWrappableProperties (skipServer) {
35
+ const wrappableProperties = {
36
+ logger: {
37
+ level: '{PLT_SERVER_LOGGER_LEVEL}'
38
+ },
39
+ managementApi: '{PLT_MANAGEMENT_API}'
40
+ }
41
+
42
+ if (!skipServer) {
43
+ wrappableProperties.server = {
44
+ hostname: '{PLT_SERVER_HOSTNAME}',
45
+ port: '{PORT}'
46
+ }
47
+ }
48
+
49
+ return wrappableProperties
50
+ }
51
+
45
52
  function getRuntimeBaseEnvVars (config) {
46
- return {
47
- PLT_SERVER_HOSTNAME: '127.0.0.1',
48
- PORT: config.port || 3042,
49
- PLT_SERVER_LOGGER_LEVEL: config.logLevel || 'info',
50
- PLT_MANAGEMENT_API: true
53
+ const env = {}
54
+
55
+ if (!config.skipServer) {
56
+ env.PLT_SERVER_HOSTNAME = '127.0.0.1'
57
+ env.PORT = config.port || 3042
51
58
  }
59
+
60
+ env.PLT_SERVER_LOGGER_LEVEL = config.logLevel || 'info'
61
+ env.PLT_MANAGEMENT_API = true
62
+
63
+ return env
52
64
  }
53
65
 
54
66
  export class RuntimeGenerator extends BaseGenerator {
@@ -195,7 +207,7 @@ export class RuntimeGenerator extends BaseGenerator {
195
207
  path: this.config.autoload || this.applicationsFolder,
196
208
  exclude: ['docs']
197
209
  },
198
- ...wrappableProperties
210
+ ...getRuntimeWrappableProperties(this.config.skipServer)
199
211
  }
200
212
 
201
213
  return config
@@ -251,13 +263,15 @@ export class RuntimeGenerator extends BaseGenerator {
251
263
  return
252
264
  }
253
265
 
254
- // port
255
- this.questions.push({
256
- type: 'input',
257
- name: 'port',
258
- default: 3042,
259
- message: 'What port do you want to use?'
260
- })
266
+ if (!this.config.skipServer) {
267
+ // port
268
+ this.questions.push({
269
+ type: 'input',
270
+ name: 'port',
271
+ default: 3042,
272
+ message: 'What port do you want to use?'
273
+ })
274
+ }
261
275
  }
262
276
 
263
277
  setApplicationsDirectory () {
@@ -611,7 +625,7 @@ export class WrappedGenerator extends BaseGenerator {
611
625
  async #createConfigFile () {
612
626
  const config = {
613
627
  $schema: `https://schemas.platformatic.dev/${this.module}/${this.platformaticVersion}.json`,
614
- runtime: wrappableProperties
628
+ runtime: getRuntimeWrappableProperties(this.config.skipServer)
615
629
  }
616
630
 
617
631
  this.addFile({
@@ -0,0 +1,78 @@
1
+ import { abstractLogger } from '@platformatic/foundation'
2
+ import { getMessaging, removeGlobals, updateGlobals } from '@platformatic/globals'
3
+ import { MessageChannel } from 'node:worker_threads'
4
+ import { MessagingITC } from './worker/messaging.js'
5
+
6
+ class LoopbackMessagingITC extends MessagingITC {
7
+ #application
8
+ #channel
9
+
10
+ constructor (id, application, runtimeConfig, logger, channel) {
11
+ super(id, runtimeConfig, logger)
12
+
13
+ this.#application = application
14
+ this.#channel = channel
15
+ this.addSource(channel)
16
+ }
17
+
18
+ // Force the next worker to be the channel
19
+ _getNextWorker () {
20
+ return { application: this.#application, channel: this.#channel }
21
+ }
22
+ }
23
+
24
+ class LoopbackMessaging extends LoopbackMessagingITC {
25
+ #target
26
+ #channel
27
+ #previousGlobals
28
+
29
+ constructor (targetId, config, logger) {
30
+ const channel = new MessageChannel()
31
+
32
+ super('loopback-messaging', targetId, config, logger, channel.port1)
33
+ this.#channel = channel
34
+ this.#target = new LoopbackMessagingITC(targetId, 'loopback-messaging', config, logger, channel.port2)
35
+ }
36
+
37
+ mount () {
38
+ this.#previousGlobals = getMessaging({ throwOnMissing: false })
39
+
40
+ updateGlobals({
41
+ messaging: {
42
+ handle: this.#target.handle.bind(this.#target),
43
+ send: this.#target.send.bind(this.#target),
44
+ notify: this.#target.notify.bind(this.#target)
45
+ }
46
+ })
47
+
48
+ return this
49
+ }
50
+
51
+ unmount () {
52
+ if (this.#previousGlobals) {
53
+ updateGlobals({ messaging: this.#previousGlobals })
54
+ } else {
55
+ removeGlobals(['messaging'])
56
+ }
57
+
58
+ this.close()
59
+ this.#target.close()
60
+ this.#channel.port1.close()
61
+ this.#channel.port2.close()
62
+ }
63
+ }
64
+
65
+ export function setupLoopbackMessaging (targetId, options) {
66
+ options ??= {}
67
+
68
+ const runtimeConfig = { ...(options.runtimeConfig ?? {}) }
69
+ runtimeConfig.messagingTimeout ??= 5000
70
+
71
+ const messaging = new LoopbackMessaging(targetId, runtimeConfig, options.logger ?? abstractLogger)
72
+
73
+ if (options.mount !== false) {
74
+ messaging.mount()
75
+ }
76
+
77
+ return messaging
78
+ }
@@ -9,7 +9,7 @@ import {
9
9
  } from '@platformatic/foundation'
10
10
  import fastify from 'fastify'
11
11
  import { platform, tmpdir } from 'node:os'
12
- import { join } from 'node:path'
12
+ import { dirname, join } from 'node:path'
13
13
  import { setTimeout as sleep } from 'node:timers/promises'
14
14
  import { createWebSocketStream } from 'ws'
15
15
  import { prepareApplication } from './config.js'
@@ -342,8 +342,8 @@ export async function startManagementApi (runtime, config) {
342
342
  const customSocket = typeof config === 'object' ? config?.socket : null
343
343
 
344
344
  const runtimePIDDir = join(PLATFORMATIC_TMP_DIR, runtimePID.toString())
345
- if (platform() !== 'win32' && !customSocket) {
346
- await createDirectory(runtimePIDDir, true)
345
+ if (platform() !== 'win32') {
346
+ await createDirectory(customSocket ? dirname(customSocket) : runtimePIDDir, typeof customSocket !== 'string')
347
347
  }
348
348
 
349
349
  let socketPath = null
@@ -2,6 +2,7 @@ import fastifyAccepts from '@fastify/accepts'
2
2
  import fastifyBasicAuth from '@fastify/basic-auth'
3
3
  import { loadModule } from '@platformatic/foundation'
4
4
  import fastify from 'fastify'
5
+ import { readFile } from 'node:fs/promises'
5
6
  import { createRequire } from 'node:module'
6
7
  import { resolve } from 'node:path'
7
8
 
@@ -52,6 +53,33 @@ async function checkReadiness (runtime) {
52
53
  return { response, status }
53
54
  }
54
55
 
56
+ async function sanitizeHTTPSArgument (arg) {
57
+ if (typeof arg === 'string') {
58
+ return arg
59
+ } else if (!Array.isArray(arg)) {
60
+ return readFile(arg.path)
61
+ }
62
+
63
+ const sanitized = []
64
+ for (const item of arg) {
65
+ sanitized.push(typeof item === 'string' ? item : await readFile(item.path))
66
+ }
67
+
68
+ return sanitized
69
+ }
70
+
71
+ async function sanitizeHTTPSOptions (https) {
72
+ if (!https) {
73
+ return
74
+ }
75
+
76
+ return {
77
+ ...https,
78
+ key: await sanitizeHTTPSArgument(https.key),
79
+ cert: await sanitizeHTTPSArgument(https.cert)
80
+ }
81
+ }
82
+
55
83
  async function checkLiveness (runtime) {
56
84
  const { status: ready, response: readinessResponse } = await checkReadiness(runtime)
57
85
  if (!ready) {
@@ -84,8 +112,9 @@ export async function startPrometheusServer (runtime, opts) {
84
112
  const port = opts.port ?? DEFAULT_PORT
85
113
  const metricsEndpoint = opts.endpoint ?? DEFAULT_METRICS_ENDPOINT
86
114
  const auth = opts.auth ?? null
115
+ const https = await sanitizeHTTPSOptions(opts.https)
87
116
 
88
- const promServer = fastify({ name: 'Prometheus server', loggerInstance: runtime.logger })
117
+ const promServer = fastify({ name: 'Prometheus server', loggerInstance: runtime.logger, https })
89
118
  promServer.register(fastifyAccepts)
90
119
 
91
120
  let onRequestHook
package/lib/runtime.js CHANGED
@@ -9,6 +9,7 @@ import {
9
9
  kTimeout,
10
10
  parseMemorySize
11
11
  } from '@platformatic/foundation'
12
+ import { getExecutable } from '@platformatic/globals'
12
13
  import { ITC } from '@platformatic/itc'
13
14
  import { collectProcessMetrics, client as metricsClient } from '@platformatic/metrics'
14
15
  import fastify from 'fastify'
@@ -36,7 +37,6 @@ import {
36
37
  CannotRemoveEntrypointError,
37
38
  InvalidArgumentError,
38
39
  MessagingError,
39
- MissingEntrypointError,
40
40
  MissingPprofCapture,
41
41
  RuntimeAbortedError,
42
42
  WorkerInterceptorJoinTimeoutError,
@@ -274,14 +274,18 @@ export class Runtime extends EventEmitter {
274
274
  await this.init()
275
275
  }
276
276
 
277
- if (typeof this.#config.entrypoint === 'undefined') {
278
- throw new MissingEntrypointError()
279
- }
280
277
  this.#updateStatus('starting')
281
278
  this.#createWorkersBroadcastChannel()
282
279
 
283
280
  try {
284
- await this.startApplications(this.getApplicationsIds(), silent)
281
+ const applications = this.getApplicationsIds()
282
+ await this.startApplications(applications, silent)
283
+
284
+ if (applications.length === 0) {
285
+ this.#updateStatus('started')
286
+ await this.close(silent)
287
+ return
288
+ }
285
289
 
286
290
  if (this.#config.inspectorOptions) {
287
291
  const { port } = this.#config.inspectorOptions
@@ -327,7 +331,9 @@ export class Runtime extends EventEmitter {
327
331
  this.#startHealthMetricsCollectionIfNeeded()
328
332
 
329
333
  await this.#dynamicWorkersScaler?.start()
330
- this.#showUrl()
334
+ if (this.#url) {
335
+ this.#showUrl()
336
+ }
331
337
  return this.#url
332
338
  }
333
339
 
@@ -582,7 +588,7 @@ export class Runtime extends EventEmitter {
582
588
 
583
589
  // Now, topological sort the applications based on their dependencies.
584
590
  // If circular dependencies are detected, an error with proper error code is thrown.
585
- applications = topologicalSort(dependencies)
591
+ applications = topologicalSort(applications, dependencies)
586
592
 
587
593
  // Group into dependency levels so that each level's dependencies are all
588
594
  // in previous levels. Levels are started sequentially, but applications
@@ -1122,6 +1128,10 @@ export class Runtime extends EventEmitter {
1122
1128
  }
1123
1129
 
1124
1130
  async getEntrypointDetails () {
1131
+ if (!this.#entrypointId) {
1132
+ return null
1133
+ }
1134
+
1125
1135
  return this.getApplicationDetails(this.#entrypointId)
1126
1136
  }
1127
1137
 
@@ -1168,6 +1178,10 @@ export class Runtime extends EventEmitter {
1168
1178
  }
1169
1179
 
1170
1180
  async getMetrics (format = 'json') {
1181
+ if (this.#config.metrics === false || this.#config.metrics?.enabled === false) {
1182
+ throw new Error('Metrics are disabled')
1183
+ }
1184
+
1171
1185
  let metrics = null
1172
1186
 
1173
1187
  const applicationRestartMetrics = format === 'json'
@@ -1716,6 +1730,10 @@ export class Runtime extends EventEmitter {
1716
1730
  }
1717
1731
 
1718
1732
  #showUrl () {
1733
+ if (!this.#url) {
1734
+ return
1735
+ }
1736
+
1719
1737
  this.logger.info(`Platformatic is now listening at ${this.#url}`)
1720
1738
  }
1721
1739
 
@@ -1734,7 +1752,7 @@ export class Runtime extends EventEmitter {
1734
1752
  applicationConfig.path = join(this.#root, config.resolvedApplicationsBasePath, id)
1735
1753
 
1736
1754
  if (!existsSync(applicationConfig.path)) {
1737
- const executable = globalThis.platformatic?.executable ?? 'platformatic'
1755
+ const executable = getExecutable() ?? 'platformatic'
1738
1756
  this.logger.error(
1739
1757
  `The path for application "%s" does not exist. Please run "${executable} resolve" and try again.`,
1740
1758
  id
@@ -2713,6 +2731,12 @@ export class Runtime extends EventEmitter {
2713
2731
 
2714
2732
  async #createWorkersBroadcastChannel () {
2715
2733
  this.#workersBroadcastChannel?.close()
2734
+
2735
+ if (this.#config.applications.length === 0) {
2736
+ this.#workersBroadcastChannel = undefined
2737
+ return
2738
+ }
2739
+
2716
2740
  this.#workersBroadcastChannel = new BroadcastChannel(kWorkersBroadcast)
2717
2741
  }
2718
2742
 
package/lib/utils.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { createHash } from 'node:crypto'
2
2
  import { tmpdir } from 'node:os'
3
3
  import { join } from 'node:path'
4
- import { ApplicationsDependenciesCycleError } from './errors.js'
4
+ import { ApplicationDependencyNotFoundError, ApplicationsDependenciesCycleError } from './errors.js'
5
5
 
6
6
  export function getArrayDifference (a, b) {
7
7
  return a.filter(element => {
@@ -48,13 +48,22 @@ export function topologicalLevels (sorted, graph) {
48
48
  return levels
49
49
  }
50
50
 
51
- // Graph: Map<string, string[]>
52
- export function topologicalSort (graph) {
51
+ // valid: string[] | Map<string, string[]>, graph?: Map<string, string[]>
52
+ export function topologicalSort (valid, graph) {
53
+ if (!graph) {
54
+ graph = valid
55
+ valid = Array.from(graph.keys())
56
+ }
57
+
53
58
  const result = []
54
59
  const visited = new Set()
55
60
  const path = []
56
61
 
57
62
  function visit (node) {
63
+ if (!valid.includes(node)) {
64
+ throw new ApplicationDependencyNotFoundError(node, valid.join(', '))
65
+ }
66
+
58
67
  if (visited.has(node)) {
59
68
  return
60
69
  }
@@ -8,6 +8,16 @@ import {
8
8
  loadConfigurationModule,
9
9
  mirrorGlobalDispatcherForBuiltinFetch
10
10
  } from '@platformatic/foundation'
11
+ import {
12
+ getLogger,
13
+ getOnActiveResourcesEventLoop,
14
+ getOnHttpStatsConnected,
15
+ getOnHttpStatsFree,
16
+ getOnHttpStatsPending,
17
+ getOnHttpStatsQueued,
18
+ getOnHttpStatsRunning,
19
+ getOnHttpStatsSize
20
+ } from '@platformatic/globals'
11
21
  import debounce from 'debounce'
12
22
  import { EventEmitter } from 'node:events'
13
23
  import { existsSync } from 'node:fs'
@@ -17,6 +27,7 @@ import { workerData } from 'node:worker_threads'
17
27
  import { getGlobalDispatcher, setGlobalDispatcher } from 'undici'
18
28
  import { ApplicationAlreadyStartedError, RuntimeNotStartedError } from '../errors.js'
19
29
  import { getApplicationUrl } from '../utils.js'
30
+ import { markAsPlatformaticDispatcher, refreshGlobalDispatcher } from './interceptors.js'
20
31
 
21
32
  function fetchApplicationUrl (application, key) {
22
33
  if (!key.endsWith('_URL') || !application.id) {
@@ -29,7 +40,8 @@ function fetchApplicationUrl (application, key) {
29
40
  function handleUnhandled (app, type, err) {
30
41
  const label = `worker ${workerData.worker.index} of the application "${workerData.applicationConfig.id}"`
31
42
 
32
- globalThis.platformatic.logger.error({ err: ensureLoggableError(err) }, `The ${label} threw an ${type}.`)
43
+ const logger = getLogger()
44
+ logger.error({ err: ensureLoggableError(err) }, `The ${label} threw an ${type}.`)
33
45
 
34
46
  executeWithTimeout(app?.stop(), 1000)
35
47
  .catch()
@@ -142,7 +154,8 @@ export class Controller extends EventEmitter {
142
154
  }
143
155
  } catch (err) {
144
156
  if (err.validationErrors) {
145
- globalThis.platformatic.logger.error(
157
+ const logger = getLogger()
158
+ logger.error(
146
159
  { err: ensureLoggableError(err) },
147
160
  'The application threw a validation error.'
148
161
  )
@@ -193,6 +206,9 @@ export class Controller extends EventEmitter {
193
206
 
194
207
  try {
195
208
  await this.capability.start({ listen })
209
+ if (refreshGlobalDispatcher()) {
210
+ this.#updateDispatcher()
211
+ }
196
212
  this.#listening = listen
197
213
  /* c8 ignore next 5 */
198
214
  } catch (err) {
@@ -244,18 +260,31 @@ export class Controller extends EventEmitter {
244
260
 
245
261
  async getMetrics ({ format }) {
246
262
  const dispatcher = getGlobalDispatcher()
247
- if (globalThis.platformatic?.onHttpStatsFree && dispatcher?.stats) {
263
+ const onHttpStatsFree = getOnHttpStatsFree({ throwOnMissing: false })
264
+
265
+ if (onHttpStatsFree && dispatcher?.stats) {
266
+ // The capability might come from an older version of @platformatic/basic
267
+ // which registered these globals without the fields tracking, so never throw.
268
+ const onHttpStatsConnected = getOnHttpStatsConnected({ throwOnMissing: false })
269
+ const onHttpStatsPending = getOnHttpStatsPending({ throwOnMissing: false })
270
+ const onHttpStatsQueued = getOnHttpStatsQueued({ throwOnMissing: false })
271
+ const onHttpStatsRunning = getOnHttpStatsRunning({ throwOnMissing: false })
272
+ const onHttpStatsSize = getOnHttpStatsSize({ throwOnMissing: false })
273
+
248
274
  for (const url in dispatcher.stats) {
249
275
  const { free, connected, pending, queued, running, size } = dispatcher.stats[url]
250
- globalThis.platformatic.onHttpStatsFree(url, free || 0)
251
- globalThis.platformatic.onHttpStatsConnected(url, connected || 0)
252
- globalThis.platformatic.onHttpStatsPending(url, pending || 0)
253
- globalThis.platformatic.onHttpStatsQueued(url, queued || 0)
254
- globalThis.platformatic.onHttpStatsRunning(url, running || 0)
255
- globalThis.platformatic.onHttpStatsSize(url, size || 0)
276
+ onHttpStatsFree(url, free || 0)
277
+ onHttpStatsConnected?.(url, connected || 0)
278
+ onHttpStatsPending?.(url, pending || 0)
279
+ onHttpStatsQueued?.(url, queued || 0)
280
+ onHttpStatsRunning?.(url, running || 0)
281
+ onHttpStatsSize?.(url, size || 0)
256
282
  }
257
283
  }
258
- globalThis.platformatic.onActiveResourcesEventLoop(getActiveResourcesInfo().length)
284
+ const onActiveResourcesEventLoop = getOnActiveResourcesEventLoop({ throwOnMissing: false })
285
+ if (onActiveResourcesEventLoop) {
286
+ onActiveResourcesEventLoop(getActiveResourcesInfo().length)
287
+ }
259
288
  return this.capability.getMetrics({ format })
260
289
  }
261
290
 
@@ -300,7 +329,8 @@ export class Controller extends EventEmitter {
300
329
  }
301
330
 
302
331
  #logAndThrow (err) {
303
- globalThis.platformatic.logger.error(
332
+ const logger = getLogger()
333
+ logger.error(
304
334
  { err: ensureLoggableError(err) },
305
335
  err[kHandledError] ? err.message : 'The application threw an error.'
306
336
  )
@@ -326,6 +356,7 @@ export class Controller extends EventEmitter {
326
356
 
327
357
  const dispatcher = getGlobalDispatcher().compose(interceptor)
328
358
 
359
+ markAsPlatformaticDispatcher(dispatcher)
329
360
  setGlobalDispatcher(dispatcher)
330
361
  mirrorGlobalDispatcherForBuiltinFetch(dispatcher)
331
362
  }
@@ -336,7 +367,8 @@ export class Controller extends EventEmitter {
336
367
 
337
368
  process.on('newListener', event => {
338
369
  if (event === 'uncaughtException' || event === 'unhandledRejection') {
339
- globalThis.platformatic.logger.warn(
370
+ const logger = getLogger()
371
+ logger.warn(
340
372
  `A listener has been added for the "process.${event}" event. This listener will be never triggered as Watt default behavior will kill the process before.\n To disable this behavior, set "exitOnUnhandledErrors" to false in the runtime config.`
341
373
  )
342
374
  }