@platformatic/runtime 3.71.1-alpha.0 → 4.0.0-alpha.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.
@@ -7,19 +7,18 @@ import {
7
7
  parseMemorySize,
8
8
  scheduleCompileCacheFlush
9
9
  } from '@platformatic/foundation'
10
- import { getITC, getLogger, updateGlobals } from '@platformatic/globals'
11
- import { addPinoInstrumentation } from '@platformatic/telemetry'
10
+ import { getITC, getLogger, setUndiciThreadInterceptor, updateGlobals } from '@platformatic/globals'
11
+ import { addPinoInstrumentation } from '@platformatic/tracing'
12
12
  import { Buffer } from 'node:buffer'
13
13
  import { subscribe } from 'node:diagnostics_channel'
14
14
  import { EventEmitter } from 'node:events'
15
- import { readFile } from 'node:fs/promises'
16
15
  import { ServerResponse } from 'node:http'
17
16
  import inspector from 'node:inspector'
17
+ import { constants as moduleConstants, enableCompileCache } from 'node:module'
18
18
  import { hostname } from 'node:os'
19
- import { join, resolve } from 'node:path'
19
+ import { join } from 'node:path'
20
20
  import { setDefaultHighWaterMark } from 'node:stream'
21
21
  import { pathToFileURL } from 'node:url'
22
- import { parseEnv } from 'node:util'
23
22
  import { threadId, workerData } from 'node:worker_threads'
24
23
  import pino from 'pino'
25
24
  import { install as installUndiciGlobals } from 'undici'
@@ -101,7 +100,7 @@ function createLogger () {
101
100
  pinoOptions.timestamp = buildPinoTimestamp(pinoOptions.timestamp)
102
101
  }
103
102
 
104
- if (workerData.config.logger?.openTelemetryExporter && workerData.applicationConfig.telemetry?.enabled !== false) {
103
+ if (workerData.config.logger?.openTelemetryExporter && workerData.applicationConfig.tracing?.enabled !== false) {
105
104
  addPinoInstrumentation(pinoOptions)
106
105
  }
107
106
 
@@ -150,10 +149,9 @@ function setupDefaultHighWaterMark (runtimeConfig, applicationConfig, logger) {
150
149
  }
151
150
  }
152
151
 
153
- // Whether the module compile cache has been enabled in this worker.
154
152
  let compileCacheEnabled = false
153
+ let compileCacheRequested = false
155
154
 
156
- // Enable compile cache if configured (Node.js 22.1.0+)
157
155
  async function setupCompileCache (runtimeConfig, applicationConfig, logger) {
158
156
  // Normalize boolean shorthand: true -> { enabled: true }
159
157
  const normalizeConfig = cfg => {
@@ -171,34 +169,23 @@ async function setupCompileCache (runtimeConfig, applicationConfig, logger) {
171
169
  return
172
170
  }
173
171
 
174
- // Check if API is available (Node.js 22.1.0+)
175
- let moduleApi
176
- try {
177
- moduleApi = await import('node:module')
178
- if (typeof moduleApi.enableCompileCache !== 'function') {
179
- return
180
- }
181
- } catch {
182
- return
183
- }
184
-
172
+ compileCacheRequested = true
185
173
  // Determine cache directory - use applicationConfig.path for the app root
186
174
  const cacheDir = config.directory ?? join(applicationConfig.path, '.plt', 'compile-cache')
187
175
 
188
176
  try {
189
- const result = moduleApi.enableCompileCache(cacheDir)
190
-
191
- const { compileCacheStatus } = moduleApi.constants ?? {}
177
+ const result = enableCompileCache(cacheDir)
178
+ const { compileCacheStatus } = moduleConstants
192
179
 
193
- if (result.status === compileCacheStatus?.ENABLED) {
180
+ if (result.status === compileCacheStatus.ENABLED) {
194
181
  compileCacheEnabled = true
195
182
  logger.debug({ directory: result.directory }, 'Module compile cache enabled')
196
- } else if (result.status === compileCacheStatus?.ALREADY_ENABLED) {
183
+ } else if (result.status === compileCacheStatus.ALREADY_ENABLED) {
197
184
  compileCacheEnabled = true
198
185
  logger.debug({ directory: result.directory }, 'Module compile cache already enabled')
199
- } else if (result.status === compileCacheStatus?.FAILED) {
186
+ } else if (result.status === compileCacheStatus.FAILED) {
200
187
  logger.warn({ message: result.message }, 'Failed to enable module compile cache')
201
- } else if (result.status === compileCacheStatus?.DISABLED) {
188
+ } else if (result.status === compileCacheStatus.DISABLED) {
202
189
  logger.debug('Module compile cache disabled via NODE_DISABLE_COMPILE_CACHE')
203
190
  }
204
191
  } catch (err) {
@@ -237,56 +224,20 @@ async function main () {
237
224
 
238
225
  await performPreloading(runtimeConfig, applicationConfig)
239
226
 
240
- // Load env file and mixin env vars from application config
241
- let envfile
242
- if (applicationConfig.envfile) {
243
- envfile = resolve(workerData.dirname, applicationConfig.envfile)
244
- } else {
245
- envfile = resolve(workerData.applicationConfig.path, '.env')
246
- }
247
-
248
- const logger = getLogger()
249
- logger.debug({ envfile }, 'Loading envfile...')
250
-
251
- // Note that process.loadEnvFile is not used here as it never overrides an already defined
252
- // variable. The worker environment is seeded from the environment the runtime resolved, which
253
- // might contain values coming from an env file of the runtime. Those are only defaults, so the
254
- // env file of this application, which is more specific, is allowed to override them. Real
255
- // environment variables are never overridden.
256
- const envFileFallbackKeys = new Set(workerData.envFileFallbackKeys ?? [])
257
-
258
- try {
259
- const applicationEnv = parseEnv(await readFile(envfile, 'utf-8'))
260
-
261
- for (const [key, value] of Object.entries(applicationEnv)) {
262
- if (!(key in process.env) || envFileFallbackKeys.has(key)) {
263
- process.env[key] = value
264
- }
265
- }
266
- } catch {
267
- // Ignore if the file doesn't exist, similar to dotenv behavior
268
- }
269
-
270
- if (runtimeConfig.env) {
271
- Object.assign(process.env, runtimeConfig.env)
272
- }
273
- if (applicationConfig.env) {
274
- Object.assign(process.env, applicationConfig.env)
275
- }
227
+ /*
228
+ Nothing is layered onto process.env here, and that absence is load-bearing. The loader resolved
229
+ this worker's entire environment main-side -- env-file chains, both env blocks, the injected
230
+ topology URLs, with the real environment authoritative over all of them -- and the worker was
231
+ *spawned* with that result. Re-reading a .env or re-applying an env block on top re-decides
232
+ what was already decided, with the opposite precedence: env blocks were once treated as pins over
233
+ the real environment, and that inversion is now a breaking change -- the real environment is
234
+ authoritative. The suppression of a block value by the real environment is reported at boot,
235
+ main-side, where the ladder is resolved.
236
+ */
237
+ getLogger().debug('Using the worker environment resolved by the loader.')
276
238
 
277
239
  const { threadDispatcher } = await setDispatcher(runtimeConfig)
278
-
279
- // If the application is an entrypoint and runtime server config is defined, use it.
280
- let serverConfig = null
281
- if (runtimeConfig.server && applicationConfig.entrypoint) {
282
- serverConfig = runtimeConfig.server
283
- } else if (applicationConfig.useHttp || applicationConfig.websocket) {
284
- serverConfig = {
285
- port: 0,
286
- hostname: '127.0.0.1',
287
- keepAliveTimeout: 5000
288
- }
289
- }
240
+ setUndiciThreadInterceptor(threadDispatcher.interceptor)
290
241
 
291
242
  const inspectorOptions = workerData.inspectorOptions
292
243
 
@@ -319,40 +270,41 @@ async function main () {
319
270
  runtimeConfig,
320
271
  applicationConfig,
321
272
  workerData.worker.index,
322
- serverConfig,
323
273
  metricsConfig
324
274
  )
325
275
 
276
+ const sharedContext = new SharedContext()
277
+ updateGlobals({
278
+ sharedContext: {
279
+ get: () => sharedContext.get(),
280
+ update: (...args) => sharedContext.update(...args)
281
+ }
282
+ })
283
+
284
+ // Setup interaction with the parent before loading the application so plugins
285
+ // can register messaging handlers during their initialization.
286
+ const itc = await setupITC(controller, applicationConfig, threadDispatcher, sharedContext)
287
+ updateGlobals({ itc })
288
+
326
289
  await controller.init(cleanup)
327
290
 
328
- // Make the compile cache accumulated while booting durable, as Node.js would otherwise only write
329
- // it when the worker terminates.
330
291
  controller.on('started', () => {
331
292
  if (compileCacheEnabled) {
332
- scheduleCompileCacheFlush(logger)
293
+ scheduleCompileCacheFlush(getLogger(), flushed => {
294
+ getITC().notify('compile-cache:flushed', { flushed, source: 'worker' })
295
+ })
296
+ } else if (compileCacheRequested) {
297
+ getITC().notify('compile-cache:unavailable', { source: 'worker' })
333
298
  }
334
299
  })
335
300
 
336
- if (applicationConfig.entrypoint && runtimeConfig.basePath) {
301
+ if (runtimeConfig.basePath) {
337
302
  const meta = await controller.capability.getMeta()
338
- if (!meta.gateway.wantsAbsoluteUrls) {
303
+ if (!meta.gateway?.wantsAbsoluteUrls) {
339
304
  stripBasePath(runtimeConfig.basePath)
340
305
  }
341
306
  }
342
307
 
343
- const sharedContext = new SharedContext()
344
- // Limit the amount of methods a user can call
345
- updateGlobals({
346
- sharedContext: {
347
- get: () => sharedContext.get(),
348
- update: (...args) => sharedContext.update(...args)
349
- }
350
- })
351
-
352
- // Setup interaction with parent port
353
- const itc = await setupITC(controller, applicationConfig, threadDispatcher, sharedContext)
354
- updateGlobals({ itc })
355
-
356
308
  // Setup management client for privileged applications
357
309
  if (applicationConfig.management) {
358
310
  const mgmtEnabled =
@@ -412,7 +364,9 @@ function stripBasePath (basePath) {
412
364
  request.url = '/' + request.url
413
365
  }
414
366
 
415
- response[kBasePath] = basePath
367
+ if (response) {
368
+ response[kBasePath] = basePath
369
+ }
416
370
  }
417
371
  })
418
372
 
@@ -439,10 +393,8 @@ function stripBasePath (basePath) {
439
393
  }
440
394
 
441
395
  ServerResponse.prototype.setHeader = function (name, value) {
442
- if (this[kBasePath]) {
443
- if (name.toLowerCase() === 'location') {
444
- value = prependBasePath(value)
445
- }
396
+ if (this[kBasePath] && name.toLowerCase() === 'location') {
397
+ value = prependBasePath(value)
446
398
  }
447
399
  originSetHeader.call(this, name, value)
448
400
  }
@@ -27,6 +27,8 @@ export class MessagingITC extends ITC {
27
27
  #workers
28
28
  #sources
29
29
  #logger
30
+ #referenced = true
31
+ #activeHandlers = 0
30
32
 
31
33
  constructor (id, runtimeConfig, logger) {
32
34
  super({
@@ -58,6 +60,34 @@ export class MessagingITC extends ITC {
58
60
  this.#listener = listener
59
61
  }
60
62
 
63
+ unref () {
64
+ this.#referenced = false
65
+ this.#updateRefs()
66
+ return this
67
+ }
68
+
69
+ ref () {
70
+ this.#referenced = true
71
+ this.#updateRefs()
72
+ return this
73
+ }
74
+
75
+ #updateRefs () {
76
+ // Outgoing ITC requests own their keep-alive timer; incoming handlers need a
77
+ // reference until their response is ready, even if their work is remote I/O.
78
+ this.#broadcastChannel[this.#referenced || this.#activeHandlers > 0 ? 'ref' : 'unref']()
79
+ const method = this.#referenced ? 'ref' : 'unref'
80
+ for (const channel of this.#notificationsChannels.values()) {
81
+ channel[method]()
82
+ }
83
+ for (const channel of this.#sources) {
84
+ channel[method]()
85
+ }
86
+ for (const worker of this.#workers.values()) {
87
+ worker.channel?.[method]()
88
+ }
89
+ }
90
+
61
91
  handle (message, handler) {
62
92
  if (typeof message === 'object') {
63
93
  for (const [name, fn] of Object.entries(message)) {
@@ -152,6 +182,7 @@ export class MessagingITC extends ITC {
152
182
  if (!channel) {
153
183
  channel = new BroadcastChannel(`plt.messaging.notifications-${application}`)
154
184
  this.#notificationsChannels.set(application, channel)
185
+ this.#updateRefs()
155
186
  }
156
187
 
157
188
  const postMessage = () => channel.postMessage(sanitize(request))
@@ -235,6 +266,7 @@ export class MessagingITC extends ITC {
235
266
  this.#sources.delete(channel)
236
267
  this.#handlePendingResponse(channel)
237
268
  })
269
+ this.#updateRefs()
238
270
  }
239
271
 
240
272
  #updateWorkers (event) {
@@ -282,7 +314,16 @@ export class MessagingITC extends ITC {
282
314
  }
283
315
 
284
316
  #wrapHandler (messageName, handler) {
285
- return (data, context) => traceIncomingMessagingHandler(this.#id, messageName, handler, data, context)
317
+ return async (data, context) => {
318
+ this.#activeHandlers++
319
+ this.#updateRefs()
320
+ try {
321
+ return await traceIncomingMessagingHandler(this.#id, messageName, handler, data, context)
322
+ } finally {
323
+ this.#activeHandlers--
324
+ this.#updateRefs()
325
+ }
326
+ }
286
327
  }
287
328
 
288
329
  #handlePendingResponse (channel) {
@@ -46,15 +46,22 @@ export class RoundRobinMap extends Map {
46
46
  return [...(this.#instances[application]?.keys ?? [])]
47
47
  }
48
48
 
49
- next (application) {
49
+ next (application, filter) {
50
50
  if (!this.#instances[application]) return
51
51
 
52
52
  let { next, keys } = this.#instances[application]
53
53
  if (next === null) {
54
54
  next = Math.floor(Math.random() * keys.length)
55
55
  }
56
- this.#instances[application].next = (next + 1) % keys.length
57
56
 
58
- return this.get(keys[next])
57
+ for (let i = 0; i < keys.length; i++) {
58
+ const candidate = (next + i) % keys.length
59
+ const worker = this.get(keys[candidate])
60
+
61
+ if (!filter || filter(worker)) {
62
+ this.#instances[application].next = (candidate + 1) % keys.length
63
+ return worker
64
+ }
65
+ }
59
66
  }
60
67
  }
@@ -6,12 +6,16 @@ export const kWorkerId = Symbol.for('plt.runtime.worker.id')
6
6
  export const kITC = Symbol.for('plt.runtime.itc')
7
7
  export const kHealthCheckTimer = Symbol.for('plt.runtime.worker.healthCheckTimer')
8
8
  export const kWorkerStatus = Symbol('plt.runtime.worker.status')
9
+ export const kWorkerExitCode = Symbol('plt.runtime.worker.exitCode')
9
10
  export const kWorkerHealthSignals = Symbol.for('plt.runtime.worker.healthSignals')
11
+ export const kWorkerStartAborted = Symbol('plt.runtime.worker.startAborted')
10
12
  export const kWorkerStartTime = Symbol.for('plt.runtime.worker.startTime')
13
+ export const kWorkerUrl = Symbol.for('plt.runtime.worker.url')
14
+ export const kWorkerPortOffset = Symbol.for('plt.runtime.worker.portOffset')
15
+ export const kWorkerServerOptions = Symbol.for('plt.runtime.worker.serverOptions')
11
16
  export const kLastHealthCheckELU = Symbol.for('plt.runtime.worker.lastHealthCheckELU')
12
- export const kLastWorkerScalerELU = Symbol.for('plt.runtime.worker.lastWorkerScalerELU')
13
- export const kInterceptorReadyPromise = Symbol.for('plt.runtime.worker.interceptorReadyPromise')
14
17
  export const kIsSubprocessHost = Symbol.for('plt.runtime.worker.isSubprocessHost')
18
+ export const kSubprocessPid = Symbol('subprocessPid')
15
19
 
16
20
  // This string marker should be safe to use since it belongs to Unicode private area
17
21
  export const kStderrMarker = '\ue002'
package/package.json CHANGED
@@ -1,9 +1,18 @@
1
1
  {
2
2
  "name": "@platformatic/runtime",
3
- "version": "3.71.1-alpha.0",
3
+ "version": "4.0.0-alpha.1",
4
4
  "description": "Platformatic runtime",
5
5
  "main": "index.js",
6
6
  "type": "module",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./index.d.ts",
10
+ "default": "./index.js"
11
+ },
12
+ "./schema": "./lib/schema.js",
13
+ "./package.json": "./package.json",
14
+ "./*": "./*"
15
+ },
7
16
  "author": "Platformatic Inc. <oss@platformatic.dev> (https://platformatic.dev)",
8
17
  "repository": {
9
18
  "type": "git",
@@ -39,14 +48,13 @@
39
48
  "typescript": "~6.0.3",
40
49
  "undici-oidc-interceptor": "^0.9.0",
41
50
  "why-is-node-running": "^2.2.2",
42
- "@platformatic/composer": "3.71.1-alpha.0",
43
- "@platformatic/db": "3.71.1-alpha.0",
44
- "@platformatic/gateway": "3.71.1-alpha.0",
45
- "@platformatic/service": "3.71.1-alpha.0",
46
- "@platformatic/node": "3.71.1-alpha.0",
47
- "@platformatic/sql-graphql": "3.71.1-alpha.0",
48
- "@platformatic/wattpm-pprof-capture": "3.71.1-alpha.0",
49
- "@platformatic/sql-mapper": "3.71.1-alpha.0"
51
+ "@platformatic/db": "4.0.0-alpha.1",
52
+ "@platformatic/gateway": "4.0.0-alpha.1",
53
+ "@platformatic/node": "4.0.0-alpha.1",
54
+ "@platformatic/service": "4.0.0-alpha.1",
55
+ "@platformatic/sql-graphql": "4.0.0-alpha.1",
56
+ "@platformatic/wattpm-pprof-capture": "4.0.0-alpha.1",
57
+ "@platformatic/sql-mapper": "4.0.0-alpha.1"
50
58
  },
51
59
  "dependencies": {
52
60
  "@fastify/accepts": "^5.0.0",
@@ -72,25 +80,26 @@
72
80
  "pino-opentelemetry-transport": "^4.0.2",
73
81
  "pino-pretty": "^13.0.0",
74
82
  "semgrator": "^0.3.0",
83
+ "semver": "^7.6.3",
75
84
  "sonic-boom": "^4.2.0",
76
85
  "systeminformation": "^5.27.11",
77
- "undici": "^7.27.2",
78
- "undici-thread-interceptor": "^1.3.1",
86
+ "undici": "^8.5.0",
87
+ "undici-thread-interceptor": "^2.2.0",
79
88
  "ws": "^8.16.0",
80
- "@platformatic/basic": "3.71.1-alpha.0",
81
- "@platformatic/foundation": "3.71.1-alpha.0",
82
- "@platformatic/generators": "3.71.1-alpha.0",
83
- "@platformatic/globals": "3.71.1-alpha.0",
84
- "@platformatic/itc": "3.71.1-alpha.0",
85
- "@platformatic/metrics": "3.71.1-alpha.0",
86
- "@platformatic/telemetry": "3.71.1-alpha.0"
89
+ "@platformatic/basic": "4.0.0-alpha.1",
90
+ "@platformatic/foundation": "4.0.0-alpha.1",
91
+ "@platformatic/generators": "4.0.0-alpha.1",
92
+ "@platformatic/itc": "4.0.0-alpha.1",
93
+ "@platformatic/metrics": "4.0.0-alpha.1",
94
+ "@platformatic/globals": "4.0.0-alpha.1",
95
+ "@platformatic/tracing": "4.0.0-alpha.1"
87
96
  },
88
97
  "engines": {
89
- "node": ">=22.19.0"
98
+ "node": ">=24.20.0"
90
99
  },
91
100
  "scripts": {
92
101
  "test": "npm run test:types && npm run test:main && npm run test:api && npm run test:cli && npm run test:start && npm run test:multiple-workers",
93
- "test:main": "node --test --test-reporter=cleaner-spec-reporter --test-concurrency=1 --test-timeout=2000000 test/*.test.js test/versions/*.test.js",
102
+ "test:main": "node --test --test-reporter=cleaner-spec-reporter --test-concurrency=1 --test-timeout=2000000 test/*.test.js test/versions/*.test.js test/predictive-scaling/*.test.js",
94
103
  "test:api": "node --test --test-reporter=cleaner-spec-reporter --test-concurrency=1 --test-timeout=2000000 test/api/*.test.js test/management-api/*.test.js",
95
104
  "test:cli": "node --test --test-reporter=cleaner-spec-reporter --test-concurrency=1 --test-timeout=2000000 test/cli/*.test.js test/cli/**/*.test.js",
96
105
  "test:start": "node --test --test-reporter=cleaner-spec-reporter --test-concurrency=1 --test-timeout=2000000 test/start/*.test.js",