@platformatic/runtime 4.0.0-new-config.2 → 4.0.0-new-config.5

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/lib/schema.js CHANGED
@@ -33,6 +33,12 @@ const runtimeLogger = {
33
33
  }
34
34
  }
35
35
 
36
+ /*
37
+ The root logger is not the application one: it carries `captureStdio` and the `pino` key mapping,
38
+ which only the runtime reads. Naming them apart is what lets a person write the type of either.
39
+ */
40
+ runtimeLogger.title = 'RuntimeLoggerOptions'
41
+
36
42
  schemaComponents.runtimeProperties.logger = runtimeLogger
37
43
 
38
44
  schemaComponents.runtimeProperties.verticalScaler.properties.applications = {
@@ -71,6 +77,76 @@ const platformaticRuntimeSchema = {
71
77
 
72
78
  export const schema = platformaticRuntimeSchema
73
79
 
80
+ /*
81
+ The keys v4 does not implement. Validating a v4 configuration against the v3 schema accepted them
82
+ and then ignored them, which is the worst of both: `envfile: './deploy.env'` at the root looked
83
+ like it was doing something. They survive on the v3 schema, which still serves v3 projects, and
84
+ inside migrate's legacy reader.
85
+
86
+ Root `envfile` is removed, not renamed — an entry may still declare one, and that property lives
87
+ on the application schema rather than here.
88
+
89
+ `$schema` is different in kind: machine writers of the plain-object form still stamp it, and the
90
+ loader reads it for version detection and strips it before validation. The schema refusing it is
91
+ what makes the strip load-bearing rather than decorative — a stamp that reached AJV would mean the
92
+ loader had skipped the step that checks the file is not a v3 one.
93
+
94
+ `verticalScaler` is the deprecated spelling of `workers`, kept on the v3 schema with a transform
95
+ that migrates it. v4 has one spelling: a configuration that still says `verticalScaler` is told
96
+ so by the schema rather than being quietly rewritten, which is the only way the two cannot
97
+ disagree about which of them a project meant.
98
+
99
+ `services` and `web` are the same story: v3's spellings of `applications`, folded by the shared
100
+ transform. Alone they fail the pipeline's topology check by name; beside an `applications` key
101
+ they would validate and be folded silently, so the schema refuses them instead.
102
+ */
103
+ const removedInV4 = ['envfile', 'strictEnv', '$schema', 'verticalScaler', 'services', 'web']
104
+
105
+ const { ...v4Properties } = platformaticRuntimeSchema.properties
106
+
107
+ for (const key of removedInV4) {
108
+ delete v4Properties[key]
109
+ }
110
+
111
+ /*
112
+ v3's entry anyOf required an id beside the path or url, because v3 derived ids only for
113
+ autoloaded applications. v4 derives them at every position -- explicit entries included, from the
114
+ package name and then the directory -- so requiring one would refuse an entry the loader handles.
115
+ What a v4 entry genuinely needs is a place: a path or a url.
116
+
117
+ Copied rather than edited, because `applications.items` is shared with the v3 schema, where the
118
+ requirement still holds.
119
+ */
120
+ const v4EntryItems = {
121
+ ...v4Properties.applications.items,
122
+ anyOf: [{ required: ['path'] }, { required: ['url'] }]
123
+ }
124
+
125
+ v4Properties.applications = { ...v4Properties.applications, items: v4EntryItems }
126
+
127
+ /*
128
+ The singular shorthand: one application with runtime options, Level 1b. It is an application
129
+ entry whose identity and place the loader both supply -- the id derives from the package name
130
+ and the path defaults to the configuration file's own directory -- so it keeps the entry's
131
+ properties and drops the anyOf entirely.
132
+ */
133
+ const { anyOf: _entryRequirements, ...applicationShorthand } = v4EntryItems
134
+
135
+ v4Properties.application = applicationShorthand
136
+
137
+ export const v4Schema = {
138
+ ...platformaticRuntimeSchema,
139
+ $id: `https://schemas.platformatic.dev/@platformatic/runtime/${version}-v4.json`,
140
+ properties: v4Properties,
141
+ /*
142
+ The v3 anyOf listed the removed spellings, and it is decorative on the v4 path anyway -- by the
143
+ time either validation pass runs, normalization has already defaulted `applications`. The gate
144
+ that actually asks the question is the pipeline's topology check, before that default; this
145
+ matches it so the schema and the loader tell the same story.
146
+ */
147
+ anyOf: [{ required: ['autoload'] }, { required: ['applications'] }, { required: ['application'] }]
148
+ }
149
+
74
150
  if (import.meta.main) {
75
151
  console.log(JSON.stringify(platformaticRuntimeSchema, null, 2))
76
152
  }
@@ -1,11 +1,9 @@
1
+ import { configurationFileNames } from '@platformatic/foundation/lib/v4/index.js'
1
2
  import {
2
3
  ensureLoggableError,
3
4
  FileWatcher,
4
5
  kHandledError,
5
- listRecognizedConfigurationFiles,
6
- loadConfiguration,
7
- loadConfigurationModule,
8
- mirrorGlobalDispatcherForBuiltinFetch
6
+ loadConfigurationModule
9
7
  } from '@platformatic/foundation'
10
8
  import {
11
9
  getLogger,
@@ -20,14 +18,18 @@ import {
20
18
  import { importCapabilityPackage } from '@platformatic/basic'
21
19
  import debounce from 'debounce'
22
20
  import { EventEmitter } from 'node:events'
23
- import { existsSync } from 'node:fs'
24
21
  import { resolve } from 'node:path'
25
22
  import { getActiveResourcesInfo } from 'node:process'
26
23
  import { workerData } from 'node:worker_threads'
27
- import { getGlobalDispatcher, setGlobalDispatcher } from 'undici'
28
- import { ApplicationAlreadyStartedError, exitCodes, RuntimeNotStartedError } from '../errors.js'
24
+ import { getGlobalDispatcher } from 'undici'
25
+ import {
26
+ ApplicationAlreadyStartedError,
27
+ exitCodes,
28
+ InvalidApplicationModuleError,
29
+ RuntimeNotStartedError
30
+ } from '../errors.js'
29
31
  import { getApplicationUrl } from '../utils.js'
30
- import { markAsPlatformaticDispatcher, refreshGlobalDispatcher } from './interceptors.js'
32
+ import { installGlobalDispatcher, refreshGlobalDispatcher } from './interceptors.js'
31
33
 
32
34
  function fetchApplicationUrl (application, key) {
33
35
  if (!key.endsWith('_URL') || !application.id) {
@@ -84,9 +86,10 @@ export class Controller extends EventEmitter {
84
86
  applicationId: this.applicationId,
85
87
  workerId: this.workerId,
86
88
  directory: this.applicationConfig.path,
89
+ sourcePath: this.applicationConfig.sourcePath,
87
90
  dependencies: this.applicationConfig.dependencies,
88
91
  isProduction: this.applicationConfig.isProduction,
89
- telemetryConfig: this.applicationConfig.telemetry,
92
+ tracingConfig: this.applicationConfig.tracing,
90
93
  loggerConfig: runtimeConfig.logger,
91
94
  metricsConfig,
92
95
  worker: workerData?.worker,
@@ -133,45 +136,40 @@ export class Controller extends EventEmitter {
133
136
  with workers: 4 evaluated user code five times and could reach five different answers.
134
137
 
135
138
  The capability is imported through the canonical resolution order, application-scoped first,
136
- so the copy that runs here is the copy whose schema validated the payload main-side.
137
- */
138
- /*
139
- v4: the configuration was evaluated exactly once, main-side, and this worker receives the
140
- validated capability payload as data. There is no file to re-read and no schema to
141
- rediscover — which is the point, since re-parsing per worker meant an application with
142
- workers: 4 evaluated user code five times and could reach five different answers.
143
-
144
- The capability is imported through the canonical resolution order, application-scoped
145
- first, so the copy that runs here is the copy whose schema validated the payload.
139
+ so the copy that runs here is the copy whose schema validated the payload main-side. When the
140
+ entry names an npm module, that module is the capability, resolved the same way.
146
141
  */
147
142
  if (appConfig.resolvedConfig) {
148
- const pkg = await importCapabilityPackage(appConfig.path, appConfig.module)
149
-
150
- this.capability = await pkg.create(appConfig.path, appConfig.resolvedConfig, this.#context)
151
- } else {
152
- // Before returning the base application, check if there is any file we recognize
153
- // and the user just forgot to specify in the configuration.
154
- if (!appConfig.config) {
155
- const candidate = listRecognizedConfigurationFiles().find(f => existsSync(resolve(appConfig.path, f)))
143
+ const pkg = await importCapabilityPackage(appConfig.path, appConfig.module, {
144
+ runtimeScope: import.meta.filename
145
+ })
156
146
 
157
- if (candidate) {
158
- appConfig.config = resolve(appConfig.path, candidate)
159
- }
147
+ // A module application names its capability directly. If that package exports no `create`,
148
+ // name it rather than letting the call below throw a bare "pkg.create is not a function".
149
+ if (appConfig.module && typeof pkg.create !== 'function') {
150
+ throw new InvalidApplicationModuleError(appConfig.module)
160
151
  }
161
152
 
162
- if (appConfig.config) {
163
- // Parse the configuration file the first time to obtain the schema
164
- const unvalidatedConfig = await loadConfiguration(appConfig.config, null, {
165
- onMissingEnv: this.#context.fetchApplicationUrl,
166
- strictEnv: this.#context.strictEnv
167
- })
168
- const pkg = await loadConfigurationModule(appConfig.path, unvalidatedConfig)
169
- this.capability = await pkg.create(appConfig.path, appConfig.config, this.#context)
170
- // We could not find a configuration file, we use the bundle @platformatic/basic with the runtime to load it
171
- } else {
172
- const pkg = await loadConfigurationModule(resolve(import.meta.dirname, '../..'), {}, '@platformatic/basic')
173
- this.capability = await pkg.create(appConfig.path, {}, this.#context)
174
- }
153
+ /*
154
+ `resolved` says this object has already been through the loader: its environment was
155
+ layered main-side, it holds no placeholders, and it has been validated against this
156
+ capability's schema. It is what tells the capability apart from an embedder handing over
157
+ an object nobody has checked, which still gets validated on the way in.
158
+ */
159
+ this.capability = await pkg.create(appConfig.path, appConfig.resolvedConfig, {
160
+ ...this.#context,
161
+ resolved: true
162
+ })
163
+ } else {
164
+ /*
165
+ No payload at all, which the v4 loader does not produce -- `prepareV4Application` gives
166
+ every entry a `resolvedConfig`, an empty object where there is nothing to say. What is
167
+ left here is an embedder constructing a Controller by hand, and the bundled base
168
+ capability is the answer for that: there is no configuration file to look for, because v4
169
+ decides that main-side and hands the result over.
170
+ */
171
+ const pkg = await loadConfigurationModule(resolve(import.meta.dirname, '../..'), {}, '@platformatic/basic')
172
+ this.capability = await pkg.create(appConfig.path, {}, this.#context)
175
173
  }
176
174
 
177
175
  this.#updateDispatcher()
@@ -323,11 +321,20 @@ export class Controller extends EventEmitter {
323
321
  return
324
322
  }
325
323
 
324
+ /*
325
+ The application's own configuration file is not the worker's to watch. The runtime evaluates
326
+ configuration once, main-side, and watches every file that evaluation read -- so a change to
327
+ one is a configuration change and the runtime reloads for it.
328
+
329
+ Watching it here as well meant both fired for a single edit: the runtime tore itself down and
330
+ rebuilt while this worker restarted the same application, and the rebuilt runtime could not
331
+ bind its management socket because the one it replaced had not released it yet.
332
+ */
326
333
  const fileWatcher = new FileWatcher({
327
334
  path: watch.path,
328
335
  /* c8 ignore next 2 */
329
336
  allowToWatch: watch?.allow,
330
- watchIgnore: watch?.ignore || []
337
+ watchIgnore: [...(watch?.ignore || []), ...configurationFileNames]
331
338
  })
332
339
 
333
340
  fileWatcher.on('update', this.#debouncedRestart)
@@ -358,8 +365,8 @@ export class Controller extends EventEmitter {
358
365
  }
359
366
 
360
367
  #updateDispatcher () {
361
- const telemetryConfig = this.#context.telemetryConfig
362
- const telemetryId = telemetryConfig?.applicationName
368
+ const tracingConfig = this.#context.tracingConfig
369
+ const telemetryId = tracingConfig?.applicationName
363
370
 
364
371
  const interceptor = dispatch => {
365
372
  return function InterceptedDispatch (opts, handler) {
@@ -375,9 +382,7 @@ export class Controller extends EventEmitter {
375
382
 
376
383
  const dispatcher = getGlobalDispatcher().compose(interceptor)
377
384
 
378
- markAsPlatformaticDispatcher(dispatcher)
379
- setGlobalDispatcher(dispatcher)
380
- mirrorGlobalDispatcherForBuiltinFetch(dispatcher)
385
+ installGlobalDispatcher(dispatcher)
381
386
  }
382
387
 
383
388
  #setupHandlers (timeout) {
@@ -6,14 +6,14 @@ import {
6
6
  getOnHttpCacheMiss,
7
7
  getOnHttpCacheRequest
8
8
  } from '@platformatic/globals'
9
- import { createTelemetryThreadInterceptorHooks } from '@platformatic/telemetry'
9
+ import { createTelemetryThreadInterceptorHooks } from '@platformatic/tracing'
10
10
  import { createRequire } from 'node:module'
11
11
  import { join } from 'node:path'
12
12
  import { pathToFileURL } from 'node:url'
13
- import { parentPort, workerData } from 'node:worker_threads'
14
- import { Agent, Client, Pool } from 'undici'
15
- import { wire } from 'undici-thread-interceptor'
16
- import { createChannelCreationHook } from '../policies.js'
13
+ import { workerData } from 'node:worker_threads'
14
+ import { Agent, Client, Pool, setGlobalDispatcher } from 'undici'
15
+ import { createInterceptor } from 'undici-thread-interceptor'
16
+ import { createTargetPermissionHook } from '../policies.js'
17
17
  import { RemoteCacheStore, httpCacheInterceptor } from './http-cache.js'
18
18
 
19
19
  const kPlatformaticGlobalDispatcher = Symbol.for('platformatic.undici.globalDispatcher')
@@ -43,8 +43,11 @@ export function refreshGlobalDispatcher () {
43
43
  }
44
44
 
45
45
  export async function setDispatcher (runtimeConfig) {
46
- const threadDispatcher = createThreadInterceptor(runtimeConfig)
47
- const threadInterceptor = threadDispatcher.interceptor
46
+ const tracingHooks = runtimeConfig.tracing ? createTelemetryThreadInterceptorHooks() : {}
47
+ const threadDispatcher = createThreadInterceptor(runtimeConfig, tracingHooks)
48
+ const threadInterceptor = threadDispatcher
49
+
50
+ await threadDispatcher.ready
48
51
 
49
52
  let cacheInterceptor = null
50
53
  if (runtimeConfig.httpCache) {
@@ -65,30 +68,29 @@ export async function setDispatcher (runtimeConfig) {
65
68
  [threadInterceptor, ...userInterceptors, cacheInterceptor].filter(Boolean)
66
69
  )
67
70
 
68
- markAsPlatformaticDispatcher(dispatcher)
69
- mirrorGlobalDispatcherForBuiltinFetch(dispatcher, createLegacyDispatcher(dispatcher))
71
+ installGlobalDispatcher(dispatcher)
70
72
  return dispatcher
71
73
  }
72
74
 
73
75
  composeRuntimeDispatcher = installDispatcher
74
76
  installDispatcher(new Agent(dispatcherOpts))
75
77
 
76
- return { threadDispatcher }
77
- }
78
-
79
- function createLegacyDispatcher (dispatcher) {
80
- const legacyDispatcher = globalThis[Symbol.for('undici.globalDispatcher.1')]
81
- const currentDispatcher = globalThis[Symbol.for('undici.globalDispatcher.2')]
82
-
83
- if (legacyDispatcher && legacyDispatcher !== currentDispatcher && legacyDispatcher.constructor?.name === 'Dispatcher1Wrapper') {
84
- try {
85
- return new legacyDispatcher.constructor(dispatcher)
86
- } catch {
87
- return dispatcher
78
+ return {
79
+ threadDispatcher: {
80
+ interceptor: threadDispatcher,
81
+ server: null,
82
+ serverHooks: tracingHooks.serverHooks
88
83
  }
89
84
  }
85
+ }
90
86
 
91
- return dispatcher
87
+ export function installGlobalDispatcher (dispatcher) {
88
+ markAsPlatformaticDispatcher(dispatcher)
89
+ setGlobalDispatcher(dispatcher)
90
+ mirrorGlobalDispatcherForBuiltinFetch(
91
+ dispatcher,
92
+ globalThis[Symbol.for('undici.globalDispatcher.1')]
93
+ )
92
94
  }
93
95
 
94
96
  export async function updateUndiciInterceptors (undiciConfig) {
@@ -215,19 +217,17 @@ async function getDispatcherOpts (undiciConfig) {
215
217
  return dispatcherOpts
216
218
  }
217
219
 
218
- function createThreadInterceptor (runtimeConfig) {
219
- const telemetry = runtimeConfig.telemetry
220
-
221
- const telemetryHooks = telemetry ? createTelemetryThreadInterceptorHooks() : {}
222
-
223
- const threadDispatcher = wire({
224
- // Specifying the domain is critical to avoid flooding the DNS
225
- // with requests for a domain that's never going to exist.
220
+ function createThreadInterceptor (runtimeConfig, tracingHooks) {
221
+ const threadDispatcher = createInterceptor({
222
+ meshId: workerData.meshId,
226
223
  domain: '.plt.local',
227
- port: parentPort,
228
- timeout: runtimeConfig.applicationTimeout,
229
- onChannelCreation: createChannelCreationHook(runtimeConfig),
230
- ...telemetryHooks
224
+ connectTimeout: runtimeConfig.applicationTimeout,
225
+ bootstrapTimeout: runtimeConfig.applicationTimeout,
226
+ allowTarget: createTargetPermissionHook({
227
+ ...runtimeConfig,
228
+ applicationId: workerData.applicationConfig.id
229
+ }),
230
+ ...tracingHooks
231
231
  })
232
232
  return threadDispatcher
233
233
  }
package/lib/worker/itc.js CHANGED
@@ -2,6 +2,7 @@ import { ensureLoggableError, executeInParallel, executeWithTimeout, kTimeout }
2
2
  import { getEvents, getLogger, getMessaging, updateGlobals } from '@platformatic/globals'
3
3
  import { ITC, initializeITCTelemetry } from '@platformatic/itc'
4
4
  import { Unpromise } from '@watchable/unpromise'
5
+ import { createServer } from 'undici-thread-interceptor'
5
6
  import { once } from 'node:events'
6
7
  import { createRequire } from 'node:module'
7
8
  import { Duplex } from 'node:stream'
@@ -114,6 +115,7 @@ async function safeHandleInITC (worker, fn) {
114
115
  async function closeITC (dispatcher, itc, messaging) {
115
116
  try {
116
117
  await dispatcher.interceptor.close()
118
+ await dispatcher.server?.close()
117
119
  itc.close()
118
120
  messaging.close()
119
121
  } finally {
@@ -208,7 +210,28 @@ export async function setupITC (controller, application, dispatcher, sharedConte
208
210
  }
209
211
  }
210
212
 
211
- dispatcher.replaceServer(await controller.capability.getDispatchTarget())
213
+ const dispatchTarget = await controller.capability.getDispatchTarget()
214
+ if (dispatchTarget == null) {
215
+ await new Promise(() => {})
216
+ }
217
+
218
+ const serverTarget = dispatcher.serverHooks?.run
219
+ ? wrapDispatchTarget(dispatchTarget, dispatcher.serverHooks.run)
220
+ : dispatchTarget
221
+
222
+ dispatcher.server = createServer({
223
+ meshId: workerData.meshId,
224
+ serverId: workerData.worker.id,
225
+ domain: `${application.id}.plt.local`,
226
+ server: serverTarget,
227
+ bootstrapTimeout: workerData.config.applicationTimeout,
228
+ metadata: {
229
+ applicationId: application.id,
230
+ workerId: workerData.worker.id
231
+ },
232
+ ...dispatcher.serverHooks
233
+ })
234
+ await dispatcher.server.ready
212
235
 
213
236
  const scheduledTasks =
214
237
  typeof controller.capability.getScheduledTasks === 'function'
@@ -259,7 +282,8 @@ export async function setupITC (controller, application, dispatcher, sharedConte
259
282
  },
260
283
 
261
284
  async removeFromMesh () {
262
- return dispatcher.interceptor.close()
285
+ await dispatcher.server?.close()
286
+ dispatcher.server = null
263
287
  },
264
288
 
265
289
  inject (injectParams) {
@@ -291,6 +315,12 @@ export async function setupITC (controller, application, dispatcher, sharedConte
291
315
  return controller.capability.getInfo()
292
316
  },
293
317
 
318
+ // How this worker serves, which only the worker can answer: for a worker-classified
319
+ // capability it depends on what the application's factory returned in this worker.
320
+ getServingState () {
321
+ return controller.capability?.getServingState?.() ?? 'inactive'
322
+ },
323
+
294
324
  async getApplicationConfig () {
295
325
  const current = await controller.capability.getConfig()
296
326
  // Remove all undefined keys from the config
@@ -498,3 +528,20 @@ export async function setupITC (controller, application, dispatcher, sharedConte
498
528
  itc.listen()
499
529
  return itc
500
530
  }
531
+
532
+ function wrapDispatchTarget (target, run) {
533
+ if (typeof target.inject === 'function') {
534
+ return {
535
+ inject: (req, callback) => run(req, () => target.inject(req, callback)),
536
+ server: target.server,
537
+ emit: target.emit?.bind(target),
538
+ listenerCount: target.listenerCount?.bind(target)
539
+ }
540
+ }
541
+
542
+ if (typeof target.emit === 'function') {
543
+ return target
544
+ }
545
+
546
+ return target
547
+ }
@@ -7,18 +7,16 @@ import {
7
7
  parseMemorySize
8
8
  } from '@platformatic/foundation'
9
9
  import { getITC, getLogger, updateGlobals } from '@platformatic/globals'
10
- import { addPinoInstrumentation } from '@platformatic/telemetry'
10
+ import { addPinoInstrumentation } from '@platformatic/tracing'
11
11
  import { Buffer } from 'node:buffer'
12
12
  import { subscribe } from 'node:diagnostics_channel'
13
13
  import { EventEmitter } from 'node:events'
14
- import { readFile } from 'node:fs/promises'
15
14
  import { ServerResponse } from 'node:http'
16
15
  import inspector from 'node:inspector'
17
16
  import { hostname } from 'node:os'
18
- import { join, resolve } from 'node:path'
17
+ import { join } from 'node:path'
19
18
  import { setDefaultHighWaterMark } from 'node:stream'
20
19
  import { pathToFileURL } from 'node:url'
21
- import { parseEnv } from 'node:util'
22
20
  import { threadId, workerData } from 'node:worker_threads'
23
21
  import pino from 'pino'
24
22
  import { install as installUndiciGlobals } from 'undici'
@@ -100,7 +98,7 @@ function createLogger () {
100
98
  pinoOptions.timestamp = buildPinoTimestamp(pinoOptions.timestamp)
101
99
  }
102
100
 
103
- if (workerData.config.logger?.openTelemetryExporter && workerData.applicationConfig.telemetry?.enabled !== false) {
101
+ if (workerData.config.logger?.openTelemetryExporter && workerData.applicationConfig.tracing?.enabled !== false) {
104
102
  addPinoInstrumentation(pinoOptions)
105
103
  }
106
104
 
@@ -231,44 +229,19 @@ async function main () {
231
229
 
232
230
  await performPreloading(runtimeConfig, applicationConfig)
233
231
 
234
- // Load env file and mixin env vars from application config
235
- let envfile
236
- if (applicationConfig.envfile) {
237
- envfile = resolve(workerData.dirname, applicationConfig.envfile)
238
- } else {
239
- envfile = resolve(workerData.applicationConfig.path, '.env')
240
- }
241
-
242
- const logger = getLogger()
243
- logger.debug({ envfile }, 'Loading envfile...')
244
-
245
- // Note that process.loadEnvFile is not used here as it never overrides an already defined
246
- // variable. The worker environment is seeded from the environment the runtime resolved, which
247
- // might contain values coming from an env file of the runtime. Those are only defaults, so the
248
- // env file of this application, which is more specific, is allowed to override them. Real
249
- // environment variables are never overridden.
250
- const envFileFallbackKeys = new Set(workerData.envFileFallbackKeys ?? [])
251
-
252
- try {
253
- const applicationEnv = parseEnv(await readFile(envfile, 'utf-8'))
254
-
255
- for (const [key, value] of Object.entries(applicationEnv)) {
256
- if (!(key in process.env) || envFileFallbackKeys.has(key)) {
257
- process.env[key] = value
258
- }
259
- }
260
- } catch {
261
- // Ignore if the file doesn't exist, similar to dotenv behavior
262
- }
263
-
264
- if (runtimeConfig.env) {
265
- Object.assign(process.env, runtimeConfig.env)
266
- }
267
- if (applicationConfig.env) {
268
- Object.assign(process.env, applicationConfig.env)
269
- }
232
+ /*
233
+ Nothing is layered onto process.env here, and that absence is load-bearing. The loader resolved
234
+ this worker's entire environment main-side -- env-file chains, both env blocks, the injected
235
+ topology URLs, with the real environment authoritative over all of them -- and the worker was
236
+ *spawned* with that result. Re-reading a .env or re-applying an env block on top re-decides
237
+ what was already decided, with the opposite precedence: v3 treated blocks as pins over the real
238
+ environment, and v4 declares the inversion as a breaking change. The suppression of a block
239
+ value by the real environment is reported at boot, main-side, where the ladder is resolved.
240
+ */
241
+ getLogger().debug('Using the worker environment resolved by the loader.')
270
242
 
271
243
  const { threadDispatcher } = await setDispatcher(runtimeConfig)
244
+ updateGlobals({ undiciThreadInterceptor: threadDispatcher.interceptor })
272
245
 
273
246
  const inspectorOptions = workerData.inspectorOptions
274
247
 
@@ -304,17 +277,7 @@ async function main () {
304
277
  metricsConfig
305
278
  )
306
279
 
307
- await controller.init(cleanup)
308
-
309
- if (runtimeConfig.basePath) {
310
- const meta = await controller.capability.getMeta()
311
- if (!meta.gateway?.wantsAbsoluteUrls) {
312
- stripBasePath(runtimeConfig.basePath)
313
- }
314
- }
315
-
316
280
  const sharedContext = new SharedContext()
317
- // Limit the amount of methods a user can call
318
281
  updateGlobals({
319
282
  sharedContext: {
320
283
  get: () => sharedContext.get(),
@@ -322,10 +285,20 @@ async function main () {
322
285
  }
323
286
  })
324
287
 
325
- // Setup interaction with parent port
288
+ // Setup interaction with the parent before loading the application so plugins
289
+ // can register messaging handlers during their initialization.
326
290
  const itc = await setupITC(controller, applicationConfig, threadDispatcher, sharedContext)
327
291
  updateGlobals({ itc })
328
292
 
293
+ await controller.init(cleanup)
294
+
295
+ if (runtimeConfig.basePath) {
296
+ const meta = await controller.capability.getMeta()
297
+ if (!meta.gateway?.wantsAbsoluteUrls) {
298
+ stripBasePath(runtimeConfig.basePath)
299
+ }
300
+ }
301
+
329
302
  // Setup management client for privileged applications
330
303
  if (applicationConfig.management) {
331
304
  const mgmtEnabled =
@@ -385,7 +358,9 @@ function stripBasePath (basePath) {
385
358
  request.url = '/' + request.url
386
359
  }
387
360
 
388
- response[kBasePath] = basePath
361
+ if (response) {
362
+ response[kBasePath] = basePath
363
+ }
389
364
  }
390
365
  })
391
366
 
@@ -10,9 +10,9 @@ export const kWorkerHealthSignals = Symbol.for('plt.runtime.worker.healthSignals
10
10
  export const kWorkerStartTime = Symbol.for('plt.runtime.worker.startTime')
11
11
  export const kWorkerUrl = Symbol.for('plt.runtime.worker.url')
12
12
  export const kWorkerPortOffset = Symbol.for('plt.runtime.worker.portOffset')
13
+ export const kWorkerServerOptions = Symbol.for('plt.runtime.worker.serverOptions')
13
14
  export const kLastHealthCheckELU = Symbol.for('plt.runtime.worker.lastHealthCheckELU')
14
15
  export const kLastWorkerScalerELU = Symbol.for('plt.runtime.worker.lastWorkerScalerELU')
15
- export const kInterceptorReadyPromise = Symbol.for('plt.runtime.worker.interceptorReadyPromise')
16
16
  export const kIsSubprocessHost = Symbol.for('plt.runtime.worker.isSubprocessHost')
17
17
 
18
18
  // This string marker should be safe to use since it belongs to Unicode private area
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@platformatic/runtime",
3
- "version": "4.0.0-new-config.2",
3
+ "version": "4.0.0-new-config.5",
4
4
  "description": "Platformatic runtime",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -39,14 +39,14 @@
39
39
  "typescript": "~6.0.3",
40
40
  "undici-oidc-interceptor": "^0.9.0",
41
41
  "why-is-node-running": "^2.2.2",
42
- "@platformatic/composer": "4.0.0-new-config.2",
43
- "@platformatic/db": "4.0.0-new-config.2",
44
- "@platformatic/gateway": "4.0.0-new-config.2",
45
- "@platformatic/node": "4.0.0-new-config.2",
46
- "@platformatic/sql-mapper": "4.0.0-new-config.2",
47
- "@platformatic/sql-graphql": "4.0.0-new-config.2",
48
- "@platformatic/wattpm-pprof-capture": "4.0.0-new-config.2",
49
- "@platformatic/service": "4.0.0-new-config.2"
42
+ "@platformatic/composer": "4.0.0-new-config.5",
43
+ "@platformatic/db": "4.0.0-new-config.5",
44
+ "@platformatic/gateway": "4.0.0-new-config.5",
45
+ "@platformatic/node": "4.0.0-new-config.5",
46
+ "@platformatic/service": "4.0.0-new-config.5",
47
+ "@platformatic/sql-graphql": "4.0.0-new-config.5",
48
+ "@platformatic/sql-mapper": "4.0.0-new-config.5",
49
+ "@platformatic/wattpm-pprof-capture": "4.0.0-new-config.5"
50
50
  },
51
51
  "dependencies": {
52
52
  "@fastify/accepts": "^5.0.0",
@@ -75,16 +75,16 @@
75
75
  "semver": "^7.6.3",
76
76
  "sonic-boom": "^4.2.0",
77
77
  "systeminformation": "^5.27.11",
78
- "undici": "^7.27.2",
79
- "undici-thread-interceptor": "^1.3.1",
78
+ "undici": "^8.5.0",
79
+ "undici-thread-interceptor": "^2.1.1",
80
80
  "ws": "^8.16.0",
81
- "@platformatic/basic": "4.0.0-new-config.2",
82
- "@platformatic/foundation": "4.0.0-new-config.2",
83
- "@platformatic/globals": "4.0.0-new-config.2",
84
- "@platformatic/generators": "4.0.0-new-config.2",
85
- "@platformatic/itc": "4.0.0-new-config.2",
86
- "@platformatic/metrics": "4.0.0-new-config.2",
87
- "@platformatic/telemetry": "4.0.0-new-config.2"
81
+ "@platformatic/basic": "4.0.0-new-config.5",
82
+ "@platformatic/foundation": "4.0.0-new-config.5",
83
+ "@platformatic/globals": "4.0.0-new-config.5",
84
+ "@platformatic/generators": "4.0.0-new-config.5",
85
+ "@platformatic/itc": "4.0.0-new-config.5",
86
+ "@platformatic/tracing": "4.0.0-new-config.5",
87
+ "@platformatic/metrics": "4.0.0-new-config.5"
88
88
  },
89
89
  "engines": {
90
90
  "node": ">=22.19.0"