@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.
package/lib/scheduler.js CHANGED
@@ -1,37 +1,24 @@
1
1
  import { CronJob, validateCronExpression } from 'cron'
2
2
  import { setTimeout } from 'node:timers/promises'
3
- import { request } from 'undici'
4
- import { DuplicateSchedulerJobError, SchedulerJobNotFoundError } from './errors.js'
3
+ import { DuplicateSchedulerJobError, InvalidSchedulerCronError, SchedulerJobNotFoundError } from './errors.js'
5
4
 
6
5
  export class SchedulerService {
7
- constructor (schedulerConfig, dispatcher, logger) {
6
+ constructor (logger) {
8
7
  this.logger = logger
9
- this.dispatcher = dispatcher
10
8
  this.jobs = new Map()
11
9
  this.started = false
12
-
13
- for (const config of schedulerConfig) {
14
- this.addJob({ ...config, source: 'config' })
15
- }
16
10
  }
17
11
 
18
12
  addJob (config, executor) {
19
- // Skip disabled schedulers
20
- if (config.enabled === false) {
21
- return null
22
- }
23
-
24
13
  // Validate cron expression
25
14
  const validation = validateCronExpression(config.cron)
26
15
  if (!validation.valid) {
27
- throw new Error(`Invalid cron expression "${config.cron}" for scheduler "${config.name}"`)
16
+ throw new InvalidSchedulerCronError(config.cron, config.name)
28
17
  }
29
18
 
30
19
  // Set defaults for optional fields
31
20
  const job = {
32
21
  ...config,
33
- headers: config.headers || {},
34
- body: config.body || {},
35
22
  maxRetries: config.maxRetries ?? 3,
36
23
  source: config.source ?? 'application',
37
24
  paused: false,
@@ -182,29 +169,7 @@ export class SchedulerService {
182
169
  )
183
170
  await setTimeout(delay)
184
171
  }
185
- if (scheduler.executor) {
186
- await scheduler.executor({ scheduledTime })
187
- } else {
188
- const headers = {
189
- 'x-retry-attempt': attempt + 1,
190
- ...scheduler.headers
191
- }
192
-
193
- const bodyString = typeof scheduler.body === 'string' ? scheduler.body : JSON.stringify(scheduler.body)
194
- const response = await request(scheduler.callbackUrl, {
195
- method: scheduler.method,
196
- headers,
197
- body: bodyString,
198
- dispatcher: this.dispatcher
199
- })
200
-
201
- // Consume the body to release the connection.
202
- await response.body.dump()
203
-
204
- if (response.statusCode < 200 || response.statusCode >= 300) {
205
- throw new Error(`HTTP error ${response.statusCode}`)
206
- }
207
- }
172
+ await scheduler.executor({ scheduledTime })
208
173
 
209
174
  this.logger.info(`Scheduler "${scheduler.name}" executed successfully`)
210
175
  success = true
@@ -244,8 +209,8 @@ export class SchedulerService {
244
209
  }
245
210
  }
246
211
 
247
- export function startScheduler (config, interceptors, logger) {
248
- const schedulerService = new SchedulerService(config, interceptors, logger)
212
+ export function startScheduler (logger) {
213
+ const schedulerService = new SchedulerService(logger)
249
214
  schedulerService.start()
250
215
  return schedulerService
251
216
  }
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/schema'
4
4
  import { version } from './version.js'
5
5
 
6
6
  const runtimeLogger = {
@@ -33,21 +33,13 @@ const runtimeLogger = {
33
33
  }
34
34
  }
35
35
 
36
- schemaComponents.runtimeProperties.logger = runtimeLogger
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'
37
41
 
38
- schemaComponents.runtimeProperties.verticalScaler.properties.applications = {
39
- type: 'object',
40
- additionalProperties: {
41
- type: 'object',
42
- properties: {
43
- minWorkers: { type: 'number', minimum: 1 },
44
- maxWorkers: { type: 'number', minimum: 1 },
45
- scaleUpELU: { type: 'number', minimum: 0, maximum: 1 },
46
- scaleDownELU: { type: 'number', minimum: 0, maximum: 1 }
47
- },
48
- additionalProperties: false
49
- }
50
- }
42
+ schemaComponents.runtimeProperties.logger = runtimeLogger
51
43
 
52
44
  const platformaticRuntimeSchema = {
53
45
  $id: `https://schemas.platformatic.dev/@platformatic/runtime/${version}.json`,
@@ -71,6 +63,76 @@ const platformaticRuntimeSchema = {
71
63
 
72
64
  export const schema = platformaticRuntimeSchema
73
65
 
66
+ /*
67
+ The keys this schema does not implement. Validating one of these configurations against the legacy
68
+ schema accepted them and then ignored them, which is the worst of both: `envfile: './deploy.env'`
69
+ at the root looked like it was doing something. They survive on the legacy schema, which still
70
+ serves legacy projects, and inside migrate's legacy reader.
71
+
72
+ Root `envfile` is removed, not renamed — an entry may still declare one, and that property lives
73
+ on the application schema rather than here.
74
+
75
+ `$schema` is different in kind: machine writers of the plain-object form still stamp it, and the
76
+ loader reads it for version detection and strips it before validation. The schema refusing it is
77
+ what makes the strip load-bearing rather than decorative — a stamp that reached AJV would mean the
78
+ loader had skipped the step that checks the file is not a legacy one.
79
+
80
+ `verticalScaler` is the deprecated spelling of `workers`, kept on the legacy schema with a transform
81
+ that migrates it. There is now one spelling: a configuration that still says `verticalScaler` is told
82
+ so by the schema rather than being quietly rewritten, which is the only way the two cannot
83
+ disagree about which of them a project meant.
84
+
85
+ `services` and `web` are the same story: the legacy spellings of `applications`, folded by the shared
86
+ transform. Alone they fail the pipeline's topology check by name; beside an `applications` key
87
+ they would validate and be folded silently, so the schema refuses them instead.
88
+ */
89
+ const removedKeys = ['envfile', 'strictEnv', '$schema', 'verticalScaler', 'services', 'web']
90
+
91
+ const { ...configurationProperties } = platformaticRuntimeSchema.properties
92
+
93
+ for (const key of removedKeys) {
94
+ delete configurationProperties[key]
95
+ }
96
+
97
+ /*
98
+ The legacy entry anyOf required an id beside the path or url, because ids were derived only for
99
+ autoloaded applications. Ids are now derived at every position -- explicit entries included, from the
100
+ package name and then the directory -- so requiring one would refuse an entry the loader handles.
101
+ What an entry genuinely needs is a place: a path or a url.
102
+
103
+ Copied rather than edited, because `applications.items` is shared with the legacy schema, where the
104
+ requirement still holds.
105
+ */
106
+ const applicationEntryItems = {
107
+ ...configurationProperties.applications.items,
108
+ anyOf: [{ required: ['path'] }, { required: ['url'] }]
109
+ }
110
+
111
+ configurationProperties.applications = { ...configurationProperties.applications, items: applicationEntryItems }
112
+
113
+ /*
114
+ The singular shorthand: one application with runtime options, Level 1b. It is an application
115
+ entry whose identity and place the loader both supply -- the id derives from the package name
116
+ and the path defaults to the configuration file's own directory -- so it keeps the entry's
117
+ properties and drops the anyOf entirely.
118
+ */
119
+ const { anyOf: _entryRequirements, ...applicationShorthand } = applicationEntryItems
120
+
121
+ configurationProperties.application = applicationShorthand
122
+
123
+ export const configurationSchema = {
124
+ ...platformaticRuntimeSchema,
125
+ $id: `https://schemas.platformatic.dev/@platformatic/runtime/${version}-strict.json`,
126
+ properties: configurationProperties,
127
+ /*
128
+ The legacy anyOf listed the removed spellings, and it is decorative here anyway -- by the
129
+ time either validation pass runs, normalization has already defaulted `applications`. The gate
130
+ that actually asks the question is the pipeline's topology check, before that default; this
131
+ matches it so the schema and the loader tell the same story.
132
+ */
133
+ anyOf: [{ required: ['autoload'] }, { required: ['applications'] }, { required: ['application'] }]
134
+ }
135
+
74
136
  if (import.meta.main) {
75
137
  console.log(JSON.stringify(platformaticRuntimeSchema, null, 2))
76
138
  }
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
+ 'This runtime 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
+ }
@@ -1,12 +1,9 @@
1
+ import { configurationFileNames } from '@platformatic/foundation/loader'
1
2
  import {
2
- convertApplicationNameToPrefix,
3
3
  ensureLoggableError,
4
4
  FileWatcher,
5
5
  kHandledError,
6
- listRecognizedConfigurationFiles,
7
- loadConfiguration,
8
- loadConfigurationModule,
9
- mirrorGlobalDispatcherForBuiltinFetch
6
+ loadConfigurationModule
10
7
  } from '@platformatic/foundation'
11
8
  import {
12
9
  getLogger,
@@ -18,38 +15,33 @@ import {
18
15
  getOnHttpStatsRunning,
19
16
  getOnHttpStatsSize
20
17
  } from '@platformatic/globals'
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'
21
+ import { getCompileCacheDir } from 'node:module'
24
22
  import { resolve } from 'node:path'
25
23
  import { getActiveResourcesInfo } from 'node:process'
26
24
  import { workerData } from 'node:worker_threads'
27
- import { getGlobalDispatcher, setGlobalDispatcher } from 'undici'
28
- import { ApplicationAlreadyStartedError, exitCodes, RuntimeNotStartedError } from '../errors.js'
25
+ import { getGlobalDispatcher } from 'undici'
26
+ import {
27
+ ApplicationAlreadyStartedError,
28
+ exitCodes,
29
+ InvalidApplicationModuleError,
30
+ RuntimeNotStartedError
31
+ } from '../errors.js'
29
32
  import { getApplicationUrl } from '../utils.js'
30
- import { markAsPlatformaticDispatcher, refreshGlobalDispatcher } from './interceptors.js'
31
-
32
- /**
33
- * Resolves {PLT_<APPLICATION>_URL} placeholders that no environment variable defines, so that
34
- * applications can reference each other without the user having to set a variable per pair.
35
- *
36
- * The mapping is built forwards, from the ids of the applications that actually exist in the runtime
37
- * to the variable name each of them owns. Parsing the variable name instead would be ambiguous, both
38
- * because the id to prefix conversion is not reversible ("with-logger" and "with_logger" share a
39
- * prefix) and because plenty of _URL variables do not name an application at all: a connection string
40
- * fragment like {VALKEY_URL} or a DSN like {PLT_DATABASE_URL} must keep resolving to nothing rather
41
- * than silently becoming an HTTP URL.
42
- */
43
- function buildApplicationUrlResolver (applications) {
44
- const urls = new Map()
45
-
46
- for (const { id } of applications ?? []) {
47
- if (id) {
48
- urls.set(`PLT_${convertApplicationNameToPrefix(id)}_URL`, getApplicationUrl(id))
33
+ import { installGlobalDispatcher, refreshGlobalDispatcher } from './interceptors.js'
34
+
35
+ function fetchApplicationUrl (applications, key) {
36
+ // Only named application placeholders may fall back to a mesh URL.
37
+ for (const application of applications) {
38
+ const name = application.id.toUpperCase().replaceAll(/[^A-Z0-9_]/g, '_')
39
+ if (key === `PLT_${name}_URL`) {
40
+ return getApplicationUrl(application.id)
49
41
  }
50
42
  }
51
43
 
52
- return key => urls.get(key) ?? null
44
+ return null
53
45
  }
54
46
 
55
47
  function handleUnhandled (app, event, listeners, timeout, err, ...args) {
@@ -69,23 +61,20 @@ function handleUnhandled (app, event, listeners, timeout, err, ...args) {
69
61
  }
70
62
  }
71
63
 
72
- // stop() rejects while the controller is not started. Left unobserved, that rejection re-enters
73
- // this handler forever and starves the event loop, so the exit scheduled above never runs.
74
- app.stop().catch(stopError => {
75
- logger.debug({ err: ensureLoggableError(stopError) }, `Stopping the ${label} after the ${event} event failed.`)
64
+ app.stop().catch(err => {
65
+ logger.debug({ err: ensureLoggableError(err) }, `Stopping the ${label} after the ${event} event failed.`)
76
66
  })
77
67
  }
78
68
 
79
69
  export class Controller extends EventEmitter {
80
70
  #starting
81
71
  #started
82
- #listening
83
72
  #watch
84
73
  #fileWatcher
85
74
  #debouncedRestart
86
75
  #context
87
76
 
88
- constructor (runtimeConfig, applicationConfig, workerId, serverConfig, metricsConfig) {
77
+ constructor (runtimeConfig, applicationConfig, workerId, metricsConfig) {
89
78
  super()
90
79
  this.runtimeConfig = runtimeConfig
91
80
  this.applicationConfig = applicationConfig
@@ -94,12 +83,9 @@ export class Controller extends EventEmitter {
94
83
  this.#watch = !!runtimeConfig.watch
95
84
  this.#starting = false
96
85
  this.#started = false
97
- this.#listening = false
98
86
  this.capability = null
99
87
  this.#fileWatcher = null
100
88
 
101
- const onMissingEnv = buildApplicationUrlResolver(runtimeConfig.applications)
102
-
103
89
  this.#context = {
104
90
  controller: this,
105
91
  runtimeConfig: this.runtimeConfig,
@@ -107,20 +93,16 @@ export class Controller extends EventEmitter {
107
93
  applicationId: this.applicationId,
108
94
  workerId: this.workerId,
109
95
  directory: this.applicationConfig.path,
96
+ sourcePath: this.applicationConfig.sourcePath,
110
97
  dependencies: this.applicationConfig.dependencies,
111
- isEntrypoint: this.applicationConfig.entrypoint,
112
98
  isProduction: this.applicationConfig.isProduction,
113
- telemetryConfig: this.applicationConfig.telemetry,
99
+ tracingConfig: this.applicationConfig.tracing,
114
100
  loggerConfig: runtimeConfig.logger,
115
101
  metricsConfig,
116
- serverConfig,
117
102
  worker: workerData?.worker,
118
103
  resourceLimits: workerData?.resourceLimits,
119
104
  hasManagementApi: !!runtimeConfig.managementApi,
120
- fetchApplicationUrl: onMissingEnv,
121
- // Capabilities spread the whole context into their loadConfiguration call, which is what makes
122
- // the configuration they load resolve application references like the throwaway load below.
123
- onMissingEnv,
105
+ onMissingEnv: fetchApplicationUrl.bind(null, runtimeConfig.applications ?? [applicationConfig]),
124
106
  strictEnv: runtimeConfig.strictEnv
125
107
  }
126
108
  }
@@ -154,31 +136,45 @@ export class Controller extends EventEmitter {
154
136
  process.env.NODE_ENV = 'production'
155
137
  }
156
138
 
157
- // Before returning the base application, check if there is any file we recognize
158
- // and the user just forgot to specify in the configuration.
159
- if (!appConfig.config) {
160
- const candidate = listRecognizedConfigurationFiles().find(f => existsSync(resolve(appConfig.path, f)))
139
+ /*
140
+ The configuration was evaluated exactly once, main-side, and this worker receives the
141
+ validated capability payload as data. There is no file to re-read and no schema to
142
+ rediscover — which is the whole point, since re-parsing per worker meant an application
143
+ with workers: 4 evaluated user code five times and could reach five different answers.
144
+
145
+ The capability is imported through the canonical resolution order, application-scoped first,
146
+ so the copy that runs here is the copy whose schema validated the payload main-side. When the
147
+ entry names an npm module, that module is the capability, resolved the same way.
148
+ */
149
+ if (appConfig.resolvedConfig) {
150
+ const pkg = await importCapabilityPackage(appConfig.path, appConfig.module, {
151
+ runtimeScope: import.meta.filename
152
+ })
161
153
 
162
- if (candidate) {
163
- appConfig.config = resolve(appConfig.path, candidate)
154
+ // A module application names its capability directly. If that package exports no `create`,
155
+ // name it rather than letting the call below throw a bare "pkg.create is not a function".
156
+ if (appConfig.module && typeof pkg.create !== 'function') {
157
+ throw new InvalidApplicationModuleError(appConfig.module)
164
158
  }
165
- }
166
159
 
167
- if (appConfig.config) {
168
- // Parse the configuration file the first time to obtain the schema. This load is thrown away:
169
- // the capability loads the configuration again below and that is the configuration the
170
- // application actually runs on. This one only has to yield $schema and module, so it must not
171
- // diverge from the second load in any way the user can observe: it resolves environment
172
- // variables identically, and leaves the strictEnv report to the load the application is
173
- // built from, which would otherwise be duplicated for every application.
174
- const unvalidatedConfig = await loadConfiguration(appConfig.config, null, {
175
- onMissingEnv: this.#context.onMissingEnv,
176
- strictEnv: false
160
+ /*
161
+ `resolved` says this object has already been through the loader: its environment was
162
+ layered main-side, it holds no placeholders, and it has been validated against this
163
+ capability's schema. It is what tells the capability apart from an embedder handing over
164
+ an object nobody has checked, which still gets validated on the way in.
165
+ */
166
+ this.capability = await pkg.create(appConfig.path, appConfig.resolvedConfig, {
167
+ ...this.#context,
168
+ resolved: true
177
169
  })
178
- const pkg = await loadConfigurationModule(appConfig.path, unvalidatedConfig)
179
- this.capability = await pkg.create(appConfig.path, appConfig.config, this.#context)
180
- // We could not find a configuration file, we use the bundle @platformatic/basic with the runtime to load it
181
170
  } else {
171
+ /*
172
+ No payload at all, which the loader does not produce -- `prepareRuntimeApplication` gives
173
+ every entry a `resolvedConfig`, an empty object where there is nothing to say. What is
174
+ left here is an embedder constructing a Controller by hand, and the bundled base
175
+ capability is the answer for that: there is no configuration file to look for, because the
176
+ runtime decides that main-side and hands the result over.
177
+ */
182
178
  const pkg = await loadConfigurationModule(resolve(import.meta.dirname, '../..'), {}, '@platformatic/basic')
183
179
  this.capability = await pkg.create(appConfig.path, {}, this.#context)
184
180
  }
@@ -219,6 +215,7 @@ export class Controller extends EventEmitter {
219
215
 
220
216
  try {
221
217
  await this.capability.init?.()
218
+
222
219
  this.emit('init')
223
220
  } catch (err) {
224
221
  this.#logAndThrow(err)
@@ -228,7 +225,6 @@ export class Controller extends EventEmitter {
228
225
  return
229
226
  }
230
227
 
231
- this.#updateCapabilityStatus('starting')
232
228
  this.emit('starting')
233
229
 
234
230
  if (this.#watch) {
@@ -245,17 +241,13 @@ export class Controller extends EventEmitter {
245
241
  }
246
242
  }
247
243
 
248
- const listen = !!(this.applicationConfig.useHttp || this.applicationConfig.websocket)
249
-
250
244
  try {
251
- await this.capability.start({ listen })
245
+ await this.capability.start()
252
246
  if (refreshGlobalDispatcher()) {
253
247
  this.#updateDispatcher()
254
248
  }
255
- this.#listening = listen
256
249
  /* c8 ignore next 5 */
257
250
  } catch (err) {
258
- this.#updateCapabilityStatus('start:error')
259
251
  this.emit('start:error', err)
260
252
 
261
253
  this.capability.log({ message: err.message, level: 'debug' })
@@ -266,53 +258,36 @@ export class Controller extends EventEmitter {
266
258
  this.#started = true
267
259
  this.#starting = false
268
260
 
269
- this.#updateCapabilityStatus('started')
270
261
  this.emit('started')
271
262
  }
272
263
 
273
- async stop (force = false, dependents = []) {
264
+ getUrl () {
265
+ return this.capability.getUrl()
266
+ }
267
+
268
+ async stop (force = false, dependents = [], shutdownTimeout) {
274
269
  if (!force && (!this.#started || this.#starting)) {
275
270
  throw new RuntimeNotStartedError()
276
271
  }
277
272
 
278
273
  this.emit('stopping')
279
- // Do not update status of the capability to "stopping" here otherwise
280
- // if stop is called before start is finished, the capability will not
281
- // be able to wait for start to finish and it will create a race condition.
282
274
 
283
275
  await this.#stopFileWatching()
284
276
  await this.capability.waitForDependentsStop(dependents)
277
+ this.capability.shutdownTimeout = shutdownTimeout
285
278
  await this.capability.stop()
286
279
 
287
280
  this.#started = false
288
281
  this.#starting = false
289
- this.#listening = false
290
282
 
291
- this.#updateCapabilityStatus('stopped')
292
283
  this.emit('stopped')
293
284
  }
294
285
 
295
- async listen () {
296
- // This server is not an entrypoint or already listened in start. Behave as no-op.
297
- if (
298
- !this.applicationConfig.entrypoint ||
299
- this.applicationConfig.useHttp ||
300
- this.applicationConfig.websocket ||
301
- this.#listening
302
- ) {
303
- return
304
- }
305
-
306
- await this.capability.start({ listen: true })
307
- }
308
-
309
286
  async getMetrics ({ format }) {
310
287
  const dispatcher = getGlobalDispatcher()
311
288
  const onHttpStatsFree = getOnHttpStatsFree({ throwOnMissing: false })
312
289
 
313
290
  if (onHttpStatsFree && dispatcher?.stats) {
314
- // The capability might come from an older version of @platformatic/basic
315
- // which registered these globals without the fields tracking, so never throw.
316
291
  const onHttpStatsConnected = getOnHttpStatsConnected({ throwOnMissing: false })
317
292
  const onHttpStatsPending = getOnHttpStatsPending({ throwOnMissing: false })
318
293
  const onHttpStatsQueued = getOnHttpStatsQueued({ throwOnMissing: false })
@@ -352,11 +327,28 @@ export class Controller extends EventEmitter {
352
327
  return
353
328
  }
354
329
 
330
+ /*
331
+ The application's own configuration file is not the worker's to watch. The runtime evaluates
332
+ configuration once, main-side, and watches every file that evaluation read -- so a change to
333
+ one is a configuration change and the runtime reloads for it.
334
+
335
+ Watching it here as well meant both fired for a single edit: the runtime tore itself down and
336
+ rebuilt while this worker restarted the same application, and the rebuilt runtime could not
337
+ bind its management socket because the one it replaced had not released it yet.
338
+ */
355
339
  const fileWatcher = new FileWatcher({
356
340
  path: watch.path,
357
341
  /* c8 ignore next 2 */
358
342
  allowToWatch: watch?.allow,
359
- watchIgnore: watch?.ignore || []
343
+ watchIgnore: [...(watch?.ignore || []), ...configurationFileNames],
344
+ // Cache flushes are runtime output, not source edits. Cover both worker and command caches,
345
+ // including an already-enabled cache inherited from the environment.
346
+ watchIgnorePaths: [
347
+ getCompileCacheDir(),
348
+ resolve(this.applicationConfig.path, '.plt', 'compile-cache'),
349
+ this.applicationConfig.compileCache?.directory ?? this.runtimeConfig.compileCache?.directory,
350
+ this.capability.config?.compileCache?.directory
351
+ ].filter(Boolean)
360
352
  })
361
353
 
362
354
  fileWatcher.on('update', this.#debouncedRestart)
@@ -387,8 +379,8 @@ export class Controller extends EventEmitter {
387
379
  }
388
380
 
389
381
  #updateDispatcher () {
390
- const telemetryConfig = this.#context.telemetryConfig
391
- const telemetryId = telemetryConfig?.applicationName
382
+ const tracingConfig = this.#context.tracingConfig
383
+ const telemetryId = tracingConfig?.applicationName
392
384
 
393
385
  const interceptor = dispatch => {
394
386
  return function InterceptedDispatch (opts, handler) {
@@ -404,9 +396,7 @@ export class Controller extends EventEmitter {
404
396
 
405
397
  const dispatcher = getGlobalDispatcher().compose(interceptor)
406
398
 
407
- markAsPlatformaticDispatcher(dispatcher)
408
- setGlobalDispatcher(dispatcher)
409
- mirrorGlobalDispatcherForBuiltinFetch(dispatcher)
399
+ installGlobalDispatcher(dispatcher)
410
400
  }
411
401
 
412
402
  #setupHandlers (timeout) {
@@ -431,14 +421,4 @@ export class Controller extends EventEmitter {
431
421
  }
432
422
  })
433
423
  }
434
-
435
- #updateCapabilityStatus (status) {
436
- if (typeof this.capability.updateStatus === 'function') {
437
- this.capability.updateStatus(status)
438
- } else {
439
- // This is horrible but needed for backward compatibility
440
- this.capability.status = status
441
- this.capability.emit(status)
442
- }
443
- }
444
424
  }