@platformatic/runtime 3.68.0 → 4.0.0-new-config.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.
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,32 +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. This load is thrown away:
144
- // the capability loads the configuration again below and that is the configuration the
145
- // application actually runs on. This one only has to yield $schema and module, so it must not
146
- // diverge from the second load in any way the user can observe: it deliberately applies no
147
- // onMissingEnv fallback and emits no strictEnv report, both of which are left to the load the
148
- // application is built from.
149
- const unvalidatedConfig = await loadConfiguration(appConfig.config, null, {
150
- strictEnv: false
151
- })
152
- const pkg = await loadConfigurationModule(appConfig.path, unvalidatedConfig)
153
- this.capability = await pkg.create(appConfig.path, appConfig.config, this.#context)
154
- // We could not find a configuration file, we use the bundle @platformatic/basic with the runtime to load it
155
- } else {
156
- const pkg = await loadConfigurationModule(resolve(import.meta.dirname, '../..'), {}, '@platformatic/basic')
157
- 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
+ }
158
175
  }
159
176
 
160
177
  this.#updateDispatcher()
@@ -193,6 +210,7 @@ export class Controller extends EventEmitter {
193
210
 
194
211
  try {
195
212
  await this.capability.init?.()
213
+
196
214
  this.emit('init')
197
215
  } catch (err) {
198
216
  this.#logAndThrow(err)
@@ -202,7 +220,6 @@ export class Controller extends EventEmitter {
202
220
  return
203
221
  }
204
222
 
205
- this.#updateCapabilityStatus('starting')
206
223
  this.emit('starting')
207
224
 
208
225
  if (this.#watch) {
@@ -219,17 +236,13 @@ export class Controller extends EventEmitter {
219
236
  }
220
237
  }
221
238
 
222
- const listen = !!(this.applicationConfig.useHttp || this.applicationConfig.websocket)
223
-
224
239
  try {
225
- await this.capability.start({ listen })
240
+ await this.capability.start()
226
241
  if (refreshGlobalDispatcher()) {
227
242
  this.#updateDispatcher()
228
243
  }
229
- this.#listening = listen
230
244
  /* c8 ignore next 5 */
231
245
  } catch (err) {
232
- this.#updateCapabilityStatus('start:error')
233
246
  this.emit('start:error', err)
234
247
 
235
248
  this.capability.log({ message: err.message, level: 'debug' })
@@ -240,19 +253,19 @@ export class Controller extends EventEmitter {
240
253
  this.#started = true
241
254
  this.#starting = false
242
255
 
243
- this.#updateCapabilityStatus('started')
244
256
  this.emit('started')
245
257
  }
246
258
 
259
+ getUrl () {
260
+ return this.capability.getUrl()
261
+ }
262
+
247
263
  async stop (force = false, dependents = []) {
248
264
  if (!force && (!this.#started || this.#starting)) {
249
265
  throw new RuntimeNotStartedError()
250
266
  }
251
267
 
252
268
  this.emit('stopping')
253
- // Do not update status of the capability to "stopping" here otherwise
254
- // if stop is called before start is finished, the capability will not
255
- // be able to wait for start to finish and it will create a race condition.
256
269
 
257
270
  await this.#stopFileWatching()
258
271
  await this.capability.waitForDependentsStop(dependents)
@@ -260,26 +273,10 @@ export class Controller extends EventEmitter {
260
273
 
261
274
  this.#started = false
262
275
  this.#starting = false
263
- this.#listening = false
264
276
 
265
- this.#updateCapabilityStatus('stopped')
266
277
  this.emit('stopped')
267
278
  }
268
279
 
269
- async listen () {
270
- // This server is not an entrypoint or already listened in start. Behave as no-op.
271
- if (
272
- !this.applicationConfig.entrypoint ||
273
- this.applicationConfig.useHttp ||
274
- this.applicationConfig.websocket ||
275
- this.#listening
276
- ) {
277
- return
278
- }
279
-
280
- await this.capability.start({ listen: true })
281
- }
282
-
283
280
  async getMetrics ({ format }) {
284
281
  const dispatcher = getGlobalDispatcher()
285
282
  const onHttpStatsFree = getOnHttpStatsFree({ throwOnMissing: false })
@@ -405,14 +402,4 @@ export class Controller extends EventEmitter {
405
402
  }
406
403
  })
407
404
  }
408
-
409
- #updateCapabilityStatus (status) {
410
- if (typeof this.capability.updateStatus === 'function') {
411
- this.capability.updateStatus(status)
412
- } else {
413
- // This is horrible but needed for backward compatibility
414
- this.capability.status = status
415
- this.capability.emit(status)
416
- }
417
- }
418
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 || applicationConfig.websocket) {
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
  }
@@ -74,10 +74,6 @@ export class ManagementClient {
74
74
  return this.#send('getSchedulerJobs')
75
75
  }
76
76
 
77
- getScheduler () {
78
- return this.getSchedulerJobs()
79
- }
80
-
81
77
  pauseSchedulerJob (name) {
82
78
  return this.#send('pauseSchedulerJob', { name })
83
79
  }
@@ -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.68.0",
3
+ "version": "4.0.0-new-config.1",
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.68.0",
43
- "@platformatic/db": "3.68.0",
44
- "@platformatic/gateway": "3.68.0",
45
- "@platformatic/node": "3.68.0",
46
- "@platformatic/service": "3.68.0",
47
- "@platformatic/sql-graphql": "3.68.0",
48
- "@platformatic/sql-mapper": "3.68.0",
49
- "@platformatic/wattpm-pprof-capture": "3.68.0"
42
+ "@platformatic/composer": "4.0.0-new-config.1",
43
+ "@platformatic/node": "4.0.0-new-config.1",
44
+ "@platformatic/gateway": "4.0.0-new-config.1",
45
+ "@platformatic/service": "4.0.0-new-config.1",
46
+ "@platformatic/sql-graphql": "4.0.0-new-config.1",
47
+ "@platformatic/db": "4.0.0-new-config.1",
48
+ "@platformatic/wattpm-pprof-capture": "4.0.0-new-config.1",
49
+ "@platformatic/sql-mapper": "4.0.0-new-config.1"
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/generators": "3.68.0",
81
- "@platformatic/globals": "3.68.0",
82
- "@platformatic/itc": "3.68.0",
83
- "@platformatic/metrics": "3.68.0",
84
- "@platformatic/telemetry": "3.68.0",
85
- "@platformatic/basic": "3.68.0",
86
- "@platformatic/foundation": "3.68.0"
81
+ "@platformatic/basic": "4.0.0-new-config.1",
82
+ "@platformatic/globals": "4.0.0-new-config.1",
83
+ "@platformatic/itc": "4.0.0-new-config.1",
84
+ "@platformatic/metrics": "4.0.0-new-config.1",
85
+ "@platformatic/foundation": "4.0.0-new-config.1",
86
+ "@platformatic/generators": "4.0.0-new-config.1",
87
+ "@platformatic/telemetry": "4.0.0-new-config.1"
87
88
  },
88
89
  "engines": {
89
90
  "node": ">=22.19.0"