@platformatic/runtime 3.67.0 → 4.0.0-new-config.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/lib/schema.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #! /usr/bin/env node
2
2
 
3
- import { schemaComponents } from '@platformatic/foundation'
3
+ import { schemaComponents } from '@platformatic/foundation/lib/schema.js'
4
4
  import { version } from './version.js'
5
5
 
6
6
  const runtimeLogger = {
package/lib/upgrade.js CHANGED
@@ -1,13 +1,28 @@
1
1
  import { abstractLogger } from '@platformatic/foundation'
2
2
  import { join } from 'node:path'
3
+ import { lt } from 'semver'
3
4
  import { semgrator } from 'semgrator'
4
5
 
5
6
  export async function upgrade (logger, config, version) {
7
+ const runtimeLogger = logger?.child({ name: '@platformatic/runtime' }) ?? abstractLogger
8
+
9
+ const schema = config.$schema
10
+ const isRuntimeSchema =
11
+ !schema ||
12
+ /(?:^|\/)(?:@platformatic\/)?runtime(?:\/|$)/.test(schema) ||
13
+ /(?:^|\/)wattpm(?:\/|$)/.test(schema)
14
+
15
+ if (isRuntimeSchema && config.server && lt(version, '4.0.0')) {
16
+ runtimeLogger.warn(
17
+ 'Runtime v4 no longer supports a root server configuration. Move it into the configuration of the capability that owns the listener.'
18
+ )
19
+ }
20
+
6
21
  const iterator = semgrator({
7
22
  version,
8
23
  path: join(import.meta.dirname, 'versions'),
9
24
  input: config,
10
- logger: logger?.child({ name: '@platformatic/runtime' }) ?? abstractLogger
25
+ logger: runtimeLogger
11
26
  })
12
27
 
13
28
  let result
@@ -0,0 +1,31 @@
1
+ export default {
2
+ version: '3.99.0',
3
+ up (config) {
4
+ const schema = config.$schema
5
+ const isRuntimeSchema =
6
+ !schema ||
7
+ /(?:^|\/)(?:@platformatic\/)?runtime(?:\/|$)/.test(schema) ||
8
+ /(?:^|\/)wattpm(?:\/|$)/.test(schema)
9
+
10
+ // Standalone application configurations are wrapped by Runtime and pass
11
+ // through this upgrade chain. Their capability-owned server must survive.
12
+ if (!isRuntimeSchema) {
13
+ return config
14
+ }
15
+
16
+ for (const section of ['applications', 'services', 'web']) {
17
+ for (const application of config[section] ?? []) {
18
+ delete application.server
19
+ }
20
+ }
21
+
22
+ for (const application of Object.values(config.autoload?.mappings ?? {})) {
23
+ delete application.server
24
+ }
25
+
26
+ delete config.entrypoint
27
+ delete config.server
28
+
29
+ return config
30
+ }
31
+ }
@@ -17,6 +17,7 @@ import {
17
17
  getOnHttpStatsRunning,
18
18
  getOnHttpStatsSize
19
19
  } from '@platformatic/globals'
20
+ import { importCapabilityPackage } from '@platformatic/basic'
20
21
  import debounce from 'debounce'
21
22
  import { EventEmitter } from 'node:events'
22
23
  import { existsSync } from 'node:fs'
@@ -59,13 +60,12 @@ function handleUnhandled (app, event, listeners, timeout, err, ...args) {
59
60
  export class Controller extends EventEmitter {
60
61
  #starting
61
62
  #started
62
- #listening
63
63
  #watch
64
64
  #fileWatcher
65
65
  #debouncedRestart
66
66
  #context
67
67
 
68
- constructor (runtimeConfig, applicationConfig, workerId, serverConfig, metricsConfig) {
68
+ constructor (runtimeConfig, applicationConfig, workerId, metricsConfig) {
69
69
  super()
70
70
  this.runtimeConfig = runtimeConfig
71
71
  this.applicationConfig = applicationConfig
@@ -74,7 +74,6 @@ export class Controller extends EventEmitter {
74
74
  this.#watch = !!runtimeConfig.watch
75
75
  this.#starting = false
76
76
  this.#started = false
77
- this.#listening = false
78
77
  this.capability = null
79
78
  this.#fileWatcher = null
80
79
 
@@ -86,12 +85,10 @@ export class Controller extends EventEmitter {
86
85
  workerId: this.workerId,
87
86
  directory: this.applicationConfig.path,
88
87
  dependencies: this.applicationConfig.dependencies,
89
- isEntrypoint: this.applicationConfig.entrypoint,
90
88
  isProduction: this.applicationConfig.isProduction,
91
89
  telemetryConfig: this.applicationConfig.telemetry,
92
90
  loggerConfig: runtimeConfig.logger,
93
91
  metricsConfig,
94
- serverConfig,
95
92
  worker: workerData?.worker,
96
93
  resourceLimits: workerData?.resourceLimits,
97
94
  hasManagementApi: !!runtimeConfig.managementApi,
@@ -129,28 +126,52 @@ export class Controller extends EventEmitter {
129
126
  process.env.NODE_ENV = 'production'
130
127
  }
131
128
 
132
- // Before returning the base application, check if there is any file we recognize
133
- // and the user just forgot to specify in the configuration.
134
- if (!appConfig.config) {
135
- const candidate = listRecognizedConfigurationFiles().find(f => existsSync(resolve(appConfig.path, f)))
129
+ /*
130
+ v4: the configuration was evaluated exactly once, main-side, and this worker receives the
131
+ validated capability payload as data. There is no file to re-read and no schema to
132
+ rediscover — which is the whole point, since re-parsing per worker meant an application
133
+ with workers: 4 evaluated user code five times and could reach five different answers.
134
+
135
+ 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.
146
+ */
147
+ 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)))
136
156
 
137
- if (candidate) {
138
- appConfig.config = resolve(appConfig.path, candidate)
157
+ if (candidate) {
158
+ appConfig.config = resolve(appConfig.path, candidate)
159
+ }
139
160
  }
140
- }
141
161
 
142
- if (appConfig.config) {
143
- // Parse the configuration file the first time to obtain the schema
144
- const unvalidatedConfig = await loadConfiguration(appConfig.config, null, {
145
- onMissingEnv: this.#context.fetchApplicationUrl,
146
- strictEnv: this.#context.strictEnv
147
- })
148
- const pkg = await loadConfigurationModule(appConfig.path, unvalidatedConfig)
149
- this.capability = await pkg.create(appConfig.path, appConfig.config, this.#context)
150
- // We could not find a configuration file, we use the bundle @platformatic/basic with the runtime to load it
151
- } else {
152
- const pkg = await loadConfigurationModule(resolve(import.meta.dirname, '../..'), {}, '@platformatic/basic')
153
- this.capability = await pkg.create(appConfig.path, {}, this.#context)
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
+ }
154
175
  }
155
176
 
156
177
  this.#updateDispatcher()
@@ -189,6 +210,7 @@ export class Controller extends EventEmitter {
189
210
 
190
211
  try {
191
212
  await this.capability.init?.()
213
+
192
214
  this.emit('init')
193
215
  } catch (err) {
194
216
  this.#logAndThrow(err)
@@ -198,7 +220,6 @@ export class Controller extends EventEmitter {
198
220
  return
199
221
  }
200
222
 
201
- this.#updateCapabilityStatus('starting')
202
223
  this.emit('starting')
203
224
 
204
225
  if (this.#watch) {
@@ -215,17 +236,13 @@ export class Controller extends EventEmitter {
215
236
  }
216
237
  }
217
238
 
218
- const listen = !!this.applicationConfig.useHttp
219
-
220
239
  try {
221
- await this.capability.start({ listen })
240
+ await this.capability.start()
222
241
  if (refreshGlobalDispatcher()) {
223
242
  this.#updateDispatcher()
224
243
  }
225
- this.#listening = listen
226
244
  /* c8 ignore next 5 */
227
245
  } catch (err) {
228
- this.#updateCapabilityStatus('start:error')
229
246
  this.emit('start:error', err)
230
247
 
231
248
  this.capability.log({ message: err.message, level: 'debug' })
@@ -236,19 +253,19 @@ export class Controller extends EventEmitter {
236
253
  this.#started = true
237
254
  this.#starting = false
238
255
 
239
- this.#updateCapabilityStatus('started')
240
256
  this.emit('started')
241
257
  }
242
258
 
259
+ getUrl () {
260
+ return this.capability.getUrl()
261
+ }
262
+
243
263
  async stop (force = false, dependents = []) {
244
264
  if (!force && (!this.#started || this.#starting)) {
245
265
  throw new RuntimeNotStartedError()
246
266
  }
247
267
 
248
268
  this.emit('stopping')
249
- // Do not update status of the capability to "stopping" here otherwise
250
- // if stop is called before start is finished, the capability will not
251
- // be able to wait for start to finish and it will create a race condition.
252
269
 
253
270
  await this.#stopFileWatching()
254
271
  await this.capability.waitForDependentsStop(dependents)
@@ -256,21 +273,10 @@ export class Controller extends EventEmitter {
256
273
 
257
274
  this.#started = false
258
275
  this.#starting = false
259
- this.#listening = false
260
276
 
261
- this.#updateCapabilityStatus('stopped')
262
277
  this.emit('stopped')
263
278
  }
264
279
 
265
- async listen () {
266
- // This server is not an entrypoint or already listened in start. Behave as no-op.
267
- if (!this.applicationConfig.entrypoint || this.applicationConfig.useHttp || this.#listening) {
268
- return
269
- }
270
-
271
- await this.capability.start({ listen: true })
272
- }
273
-
274
280
  async getMetrics ({ format }) {
275
281
  const dispatcher = getGlobalDispatcher()
276
282
  const onHttpStatsFree = getOnHttpStatsFree({ throwOnMissing: false })
@@ -396,14 +402,4 @@ export class Controller extends EventEmitter {
396
402
  }
397
403
  })
398
404
  }
399
-
400
- #updateCapabilityStatus (status) {
401
- if (typeof this.capability.updateStatus === 'function') {
402
- this.capability.updateStatus(status)
403
- } else {
404
- // This is horrible but needed for backward compatibility
405
- this.capability.status = status
406
- this.capability.emit(status)
407
- }
408
- }
409
405
  }
package/lib/worker/itc.js CHANGED
@@ -201,14 +201,13 @@ export async function setupITC (controller, application, dispatcher, sharedConte
201
201
  })
202
202
  })
203
203
 
204
- throw ensureLoggableError(e)
204
+ // Errors are structured cloned when sent to the runtime, which drops all their custom properties (like the
205
+ // port and the address of listen errors): send a plain object instead so that the runtime can inspect them.
206
+ // eslint-disable-next-line no-throw-literal
207
+ throw { name: e.name, ...ensureLoggableError(e) }
205
208
  }
206
209
  }
207
210
 
208
- if (application.entrypoint) {
209
- await controller.listen()
210
- }
211
-
212
211
  dispatcher.replaceServer(await controller.capability.getDispatchTarget())
213
212
 
214
213
  const scheduledTasks =
@@ -217,7 +216,7 @@ export async function setupITC (controller, application, dispatcher, sharedConte
217
216
  : []
218
217
 
219
218
  return {
220
- url: application.entrypoint ? controller.capability.getUrl() : null,
219
+ url: controller.getUrl(),
221
220
  scheduledTasks
222
221
  }
223
222
  },
@@ -270,18 +270,6 @@ async function main () {
270
270
 
271
271
  const { threadDispatcher } = await setDispatcher(runtimeConfig)
272
272
 
273
- // If the application is an entrypoint and runtime server config is defined, use it.
274
- let serverConfig = null
275
- if (runtimeConfig.server && applicationConfig.entrypoint) {
276
- serverConfig = runtimeConfig.server
277
- } else if (applicationConfig.useHttp) {
278
- serverConfig = {
279
- port: 0,
280
- hostname: '127.0.0.1',
281
- keepAliveTimeout: 5000
282
- }
283
- }
284
-
285
273
  const inspectorOptions = workerData.inspectorOptions
286
274
 
287
275
  if (inspectorOptions) {
@@ -313,15 +301,14 @@ async function main () {
313
301
  runtimeConfig,
314
302
  applicationConfig,
315
303
  workerData.worker.index,
316
- serverConfig,
317
304
  metricsConfig
318
305
  )
319
306
 
320
307
  await controller.init(cleanup)
321
308
 
322
- if (applicationConfig.entrypoint && runtimeConfig.basePath) {
309
+ if (runtimeConfig.basePath) {
323
310
  const meta = await controller.capability.getMeta()
324
- if (!meta.gateway.wantsAbsoluteUrls) {
311
+ if (!meta.gateway?.wantsAbsoluteUrls) {
325
312
  stripBasePath(runtimeConfig.basePath)
326
313
  }
327
314
  }
@@ -425,10 +412,8 @@ function stripBasePath (basePath) {
425
412
  }
426
413
 
427
414
  ServerResponse.prototype.setHeader = function (name, value) {
428
- if (this[kBasePath]) {
429
- if (name.toLowerCase() === 'location') {
430
- value = prependBasePath(value)
431
- }
415
+ if (this[kBasePath] && name.toLowerCase() === 'location') {
416
+ value = prependBasePath(value)
432
417
  }
433
418
  originSetHeader.call(this, name, value)
434
419
  }
@@ -8,6 +8,8 @@ export const kHealthCheckTimer = Symbol.for('plt.runtime.worker.healthCheckTimer
8
8
  export const kWorkerStatus = Symbol('plt.runtime.worker.status')
9
9
  export const kWorkerHealthSignals = Symbol.for('plt.runtime.worker.healthSignals')
10
10
  export const kWorkerStartTime = Symbol.for('plt.runtime.worker.startTime')
11
+ export const kWorkerUrl = Symbol.for('plt.runtime.worker.url')
12
+ export const kWorkerPortOffset = Symbol.for('plt.runtime.worker.portOffset')
11
13
  export const kLastHealthCheckELU = Symbol.for('plt.runtime.worker.lastHealthCheckELU')
12
14
  export const kLastWorkerScalerELU = Symbol.for('plt.runtime.worker.lastWorkerScalerELU')
13
15
  export const kInterceptorReadyPromise = Symbol.for('plt.runtime.worker.interceptorReadyPromise')
@@ -1,8 +1,13 @@
1
- import { features } from '@platformatic/foundation'
2
1
  import { availableParallelism } from 'node:os'
3
2
  import { getMemoryInfo } from './metrics.js'
4
3
  import { ScalingAlgorithm } from './scaling-algorithm.js'
5
- import { kApplicationId, kId, kLastWorkerScalerELU, kWorkerStartTime, kWorkerStatus } from './worker/symbols.js'
4
+ import {
5
+ kApplicationId,
6
+ kId,
7
+ kLastWorkerScalerELU,
8
+ kWorkerStartTime,
9
+ kWorkerStatus
10
+ } from './worker/symbols.js'
6
11
 
7
12
  const healthCheckInterval = 1000
8
13
  export const kOriginalWorkers = Symbol('plt.runtime.application.dynamicWorkersScalerOriginalWorkers')
@@ -93,14 +98,7 @@ export class DynamicWorkersScaler {
93
98
  async add (application) {
94
99
  const config = {}
95
100
 
96
- if (application.entrypoint && !features.node.reusePort) {
97
- this.#runtime.logger.warn(
98
- `The "${application.id}" application cannot be scaled because it is an entrypoint and the "reusePort" feature is not available in your OS.`
99
- )
100
-
101
- config.minWorkers = 1
102
- config.maxWorkers = 1
103
- } else if (application.workers.dynamic === false) {
101
+ if (application.workers.dynamic === false) {
104
102
  this.#runtime.logger.warn(
105
103
  `The "${application.id}" application cannot be scaled because it has a fixed number of workers (${application.workers.static}).`
106
104
  )
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@platformatic/runtime",
3
- "version": "3.67.0",
3
+ "version": "4.0.0-new-config.0",
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": "3.67.0",
43
- "@platformatic/db": "3.67.0",
44
- "@platformatic/node": "3.67.0",
45
- "@platformatic/service": "3.67.0",
46
- "@platformatic/sql-graphql": "3.67.0",
47
- "@platformatic/sql-mapper": "3.67.0",
48
- "@platformatic/wattpm-pprof-capture": "3.67.0",
49
- "@platformatic/gateway": "3.67.0"
42
+ "@platformatic/composer": "4.0.0-new-config.0",
43
+ "@platformatic/db": "4.0.0-new-config.0",
44
+ "@platformatic/gateway": "4.0.0-new-config.0",
45
+ "@platformatic/service": "4.0.0-new-config.0",
46
+ "@platformatic/sql-graphql": "4.0.0-new-config.0",
47
+ "@platformatic/node": "4.0.0-new-config.0",
48
+ "@platformatic/sql-mapper": "4.0.0-new-config.0",
49
+ "@platformatic/wattpm-pprof-capture": "4.0.0-new-config.0"
50
50
  },
51
51
  "dependencies": {
52
52
  "@fastify/accepts": "^5.0.0",
@@ -72,18 +72,19 @@
72
72
  "pino-opentelemetry-transport": "^4.0.2",
73
73
  "pino-pretty": "^13.0.0",
74
74
  "semgrator": "^0.3.0",
75
+ "semver": "^7.6.3",
75
76
  "sonic-boom": "^4.2.0",
76
77
  "systeminformation": "^5.27.11",
77
78
  "undici": "^7.27.2",
78
79
  "undici-thread-interceptor": "^1.3.1",
79
80
  "ws": "^8.16.0",
80
- "@platformatic/basic": "3.67.0",
81
- "@platformatic/generators": "3.67.0",
82
- "@platformatic/foundation": "3.67.0",
83
- "@platformatic/itc": "3.67.0",
84
- "@platformatic/globals": "3.67.0",
85
- "@platformatic/telemetry": "3.67.0",
86
- "@platformatic/metrics": "3.67.0"
81
+ "@platformatic/globals": "4.0.0-new-config.0",
82
+ "@platformatic/basic": "4.0.0-new-config.0",
83
+ "@platformatic/metrics": "4.0.0-new-config.0",
84
+ "@platformatic/itc": "4.0.0-new-config.0",
85
+ "@platformatic/telemetry": "4.0.0-new-config.0",
86
+ "@platformatic/generators": "4.0.0-new-config.0",
87
+ "@platformatic/foundation": "4.0.0-new-config.0"
87
88
  },
88
89
  "engines": {
89
90
  "node": ">=22.19.0"