@platformatic/runtime 3.67.0 → 3.69.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/config.d.ts +1 -0
- package/index.d.ts +2 -0
- package/lib/config.js +56 -2
- package/lib/errors.js +4 -0
- package/lib/runtime.js +19 -1
- package/lib/worker/controller.js +41 -10
- package/lib/worker/main.js +1 -1
- package/lib/worker/management.js +4 -0
- package/package.json +17 -17
- package/schema.json +13 -1
package/config.d.ts
CHANGED
package/index.d.ts
CHANGED
|
@@ -250,6 +250,7 @@ export declare class ManagementClient {
|
|
|
250
250
|
getApplicationOpenapiSchema (id: string): Promise<unknown>
|
|
251
251
|
getApplicationGraphqlSchema (id: string): Promise<unknown>
|
|
252
252
|
getMetrics (format?: string): Promise<{ metrics: unknown }>
|
|
253
|
+
getScheduler (): Promise<SchedulerJob[]>
|
|
253
254
|
getSchedulerJobs (): Promise<SchedulerJob[]>
|
|
254
255
|
pauseSchedulerJob (name: string): Promise<SchedulerJob>
|
|
255
256
|
resumeSchedulerJob (name: string): Promise<SchedulerJob>
|
|
@@ -462,6 +463,7 @@ export declare class Runtime extends EventEmitter {
|
|
|
462
463
|
off (event: 'application:worker:health:metrics', listener: (event: HealthMetricsEvent) => void): this
|
|
463
464
|
off (event: string | symbol, listener: (...args: any[]) => void): this
|
|
464
465
|
|
|
466
|
+
getScheduler (): SchedulerJob[]
|
|
465
467
|
getSchedulerJobs (): SchedulerJob[]
|
|
466
468
|
pauseSchedulerJob (name: string): Promise<SchedulerJob>
|
|
467
469
|
resumeSchedulerJob (name: string): Promise<SchedulerJob>
|
package/lib/config.js
CHANGED
|
@@ -9,11 +9,13 @@ import {
|
|
|
9
9
|
omitProperties,
|
|
10
10
|
runtimeUnwrappablePropertiesList
|
|
11
11
|
} from '@platformatic/foundation'
|
|
12
|
+
import { realpathSync } from 'node:fs'
|
|
12
13
|
import { readdir, readFile } from 'node:fs/promises'
|
|
13
14
|
import { createRequire } from 'node:module'
|
|
14
15
|
import { isAbsolute, join, resolve as resolvePath } from 'node:path'
|
|
15
16
|
|
|
16
17
|
import {
|
|
18
|
+
ApplicationIdCollisionError,
|
|
17
19
|
InspectAndInspectBrkError,
|
|
18
20
|
InspectorHostError,
|
|
19
21
|
InspectorPortError,
|
|
@@ -295,6 +297,40 @@ export async function prepareApplication (config, application, defaultWorkers) {
|
|
|
295
297
|
return application
|
|
296
298
|
}
|
|
297
299
|
|
|
300
|
+
function canonicalPath (path) {
|
|
301
|
+
try {
|
|
302
|
+
return realpathSync(path)
|
|
303
|
+
} catch {
|
|
304
|
+
return resolvePath(path)
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// An autoloaded directory and an explicitly configured entry with the same id are the same application
|
|
309
|
+
// only when they point to the same place: either the configured path resolves to the autoloaded
|
|
310
|
+
// directory, or the entry is external and the autoloaded directory is where "resolve" would put it.
|
|
311
|
+
// When they don't, returns a description of the configured entry to report in the error.
|
|
312
|
+
function conflictingApplicationSource (root, resolvedApplicationsPath, existing, entryPath) {
|
|
313
|
+
if (existing.path) {
|
|
314
|
+
// The path still contains a placeholder, which means the environment variable was not replaced.
|
|
315
|
+
// There is nothing to compare in that case.
|
|
316
|
+
if (existing.path.match(/^\{.*\}$/)) {
|
|
317
|
+
return null
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const existingPath = isAbsolute(existing.path) ? existing.path : resolvePath(root, existing.path)
|
|
321
|
+
|
|
322
|
+
return canonicalPath(existingPath) === canonicalPath(entryPath) ? null : `the path "${existing.path}"`
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
if (existing.url) {
|
|
326
|
+
const resolvedPath = join(resolvedApplicationsPath, existing.id)
|
|
327
|
+
|
|
328
|
+
return canonicalPath(resolvedPath) === canonicalPath(entryPath) ? null : `the URL "${existing.url}"`
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
return null
|
|
332
|
+
}
|
|
333
|
+
|
|
298
334
|
function isApplicationEnabled (application, environment) {
|
|
299
335
|
const { enabled } = application
|
|
300
336
|
|
|
@@ -378,7 +414,11 @@ export async function transform (config, _, context) {
|
|
|
378
414
|
const { exclude = [], mappings = {} } = config.autoload
|
|
379
415
|
let { path } = config.autoload
|
|
380
416
|
|
|
381
|
-
|
|
417
|
+
// Capture these before the loop, as config is shadowed in its body
|
|
418
|
+
const root = config[kMetadata].root
|
|
419
|
+
const resolvedApplicationsPath = resolvePath(root, config.resolvedApplicationsBasePath ?? 'external')
|
|
420
|
+
|
|
421
|
+
path = resolvePath(root, path)
|
|
382
422
|
const entries = await readdir(path, { withFileTypes: true })
|
|
383
423
|
|
|
384
424
|
for (let i = 0; i < entries.length; ++i) {
|
|
@@ -403,7 +443,21 @@ export async function transform (config, _, context) {
|
|
|
403
443
|
const existingApplicationId = applications.findIndex(application => application.id === id)
|
|
404
444
|
|
|
405
445
|
if (existingApplicationId !== -1) {
|
|
406
|
-
|
|
446
|
+
const existing = applications[existingApplicationId]
|
|
447
|
+
|
|
448
|
+
// Merging on the id alone can boot local code where the configuration named a repository, as
|
|
449
|
+
// the autoloaded path survives next to the configured url. Reject the duplicate instead: an id
|
|
450
|
+
// is the mesh hostname, the injected PLT_<ID>_URL name, the metrics label and the argument of
|
|
451
|
+
// "wattpm inject", so two different applications cannot share one.
|
|
452
|
+
if (isApplicationEnabled(existing, environment)) {
|
|
453
|
+
const source = conflictingApplicationSource(root, resolvedApplicationsPath, existing, entryPath)
|
|
454
|
+
|
|
455
|
+
if (source) {
|
|
456
|
+
throw new ApplicationIdCollisionError(id, entryPath, source)
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
applications[existingApplicationId] = { ...application, ...existing }
|
|
407
461
|
} else {
|
|
408
462
|
applications.push(application)
|
|
409
463
|
}
|
package/lib/errors.js
CHANGED
|
@@ -102,6 +102,10 @@ export const InvalidEntrypointError = createError(
|
|
|
102
102
|
`${ERROR_PREFIX}_INVALID_ENTRYPOINT`,
|
|
103
103
|
"Invalid entrypoint: '%s' does not exist"
|
|
104
104
|
)
|
|
105
|
+
export const ApplicationIdCollisionError = createError(
|
|
106
|
+
`${ERROR_PREFIX}_APPLICATION_ID_COLLISION`,
|
|
107
|
+
'The application id "%s" is used by the autoloaded directory "%s" and by a different application defined in the configuration file via %s. Application ids must be unique.'
|
|
108
|
+
)
|
|
105
109
|
export const MissingEntrypointError = createError(
|
|
106
110
|
`${ERROR_PREFIX}_MISSING_ENTRYPOINT`,
|
|
107
111
|
'Missing application entrypoint.'
|
package/lib/runtime.js
CHANGED
|
@@ -2192,6 +2192,10 @@ export class Runtime extends EventEmitter {
|
|
|
2192
2192
|
return this.#scheduler?.getJobs() ?? []
|
|
2193
2193
|
}
|
|
2194
2194
|
|
|
2195
|
+
getScheduler () {
|
|
2196
|
+
return this.getSchedulerJobs()
|
|
2197
|
+
}
|
|
2198
|
+
|
|
2195
2199
|
pauseSchedulerJob (name) {
|
|
2196
2200
|
return this.#schedulerOrThrow().pauseJob(name)
|
|
2197
2201
|
}
|
|
@@ -2861,7 +2865,21 @@ export class Runtime extends EventEmitter {
|
|
|
2861
2865
|
// Setup the interceptor
|
|
2862
2866
|
// kInterceptorReadyPromise resolves when the worker
|
|
2863
2867
|
// is ready to receive requests: after calling the replaceServer method
|
|
2864
|
-
|
|
2868
|
+
//
|
|
2869
|
+
// It is stored, not awaited: #startWorker awaits it later, and only if the
|
|
2870
|
+
// worker is actually started. A worker that is discarded or torn down
|
|
2871
|
+
// before that — a replacement abandoned because the runtime stopped
|
|
2872
|
+
// mid-restart, for instance — leaves the promise with nobody to observe
|
|
2873
|
+
// it. Closing the mesh interceptor rejects any routing still in flight
|
|
2874
|
+
// ('The dispatcher has been closed.'), so that is a routine shutdown
|
|
2875
|
+
// outcome, not an exotic one, and it surfaced as an unhandledRejection
|
|
2876
|
+
// that could take the process down before graceful shutdown finished.
|
|
2877
|
+
//
|
|
2878
|
+
// Attaching a no-op handler marks the rejection observed WITHOUT
|
|
2879
|
+
// swallowing it: the await in #startWorker still sees and reports it.
|
|
2880
|
+
const interceptorReady = this.#meshInterceptor.route(applicationId, worker)
|
|
2881
|
+
interceptorReady.catch(() => {})
|
|
2882
|
+
worker[kInterceptorReadyPromise] = interceptorReady
|
|
2865
2883
|
|
|
2866
2884
|
// Wait for initialization
|
|
2867
2885
|
try {
|
package/lib/worker/controller.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
|
+
convertApplicationNameToPrefix,
|
|
2
3
|
ensureLoggableError,
|
|
3
4
|
FileWatcher,
|
|
4
5
|
kHandledError,
|
|
@@ -28,12 +29,27 @@ import { ApplicationAlreadyStartedError, exitCodes, RuntimeNotStartedError } fro
|
|
|
28
29
|
import { getApplicationUrl } from '../utils.js'
|
|
29
30
|
import { markAsPlatformaticDispatcher, refreshGlobalDispatcher } from './interceptors.js'
|
|
30
31
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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))
|
|
49
|
+
}
|
|
34
50
|
}
|
|
35
51
|
|
|
36
|
-
return
|
|
52
|
+
return key => urls.get(key) ?? null
|
|
37
53
|
}
|
|
38
54
|
|
|
39
55
|
function handleUnhandled (app, event, listeners, timeout, err, ...args) {
|
|
@@ -78,6 +94,8 @@ export class Controller extends EventEmitter {
|
|
|
78
94
|
this.capability = null
|
|
79
95
|
this.#fileWatcher = null
|
|
80
96
|
|
|
97
|
+
const onMissingEnv = buildApplicationUrlResolver(runtimeConfig.applications)
|
|
98
|
+
|
|
81
99
|
this.#context = {
|
|
82
100
|
controller: this,
|
|
83
101
|
runtimeConfig: this.runtimeConfig,
|
|
@@ -95,7 +113,10 @@ export class Controller extends EventEmitter {
|
|
|
95
113
|
worker: workerData?.worker,
|
|
96
114
|
resourceLimits: workerData?.resourceLimits,
|
|
97
115
|
hasManagementApi: !!runtimeConfig.managementApi,
|
|
98
|
-
fetchApplicationUrl:
|
|
116
|
+
fetchApplicationUrl: onMissingEnv,
|
|
117
|
+
// Capabilities spread the whole context into their loadConfiguration call, which is what makes
|
|
118
|
+
// the configuration they load resolve application references like the throwaway load below.
|
|
119
|
+
onMissingEnv,
|
|
99
120
|
strictEnv: runtimeConfig.strictEnv
|
|
100
121
|
}
|
|
101
122
|
}
|
|
@@ -140,10 +161,15 @@ export class Controller extends EventEmitter {
|
|
|
140
161
|
}
|
|
141
162
|
|
|
142
163
|
if (appConfig.config) {
|
|
143
|
-
// Parse the configuration file the first time to obtain the schema
|
|
164
|
+
// Parse the configuration file the first time to obtain the schema. This load is thrown away:
|
|
165
|
+
// the capability loads the configuration again below and that is the configuration the
|
|
166
|
+
// application actually runs on. This one only has to yield $schema and module, so it must not
|
|
167
|
+
// diverge from the second load in any way the user can observe: it resolves environment
|
|
168
|
+
// variables identically, and leaves the strictEnv report to the load the application is
|
|
169
|
+
// built from, which would otherwise be duplicated for every application.
|
|
144
170
|
const unvalidatedConfig = await loadConfiguration(appConfig.config, null, {
|
|
145
|
-
onMissingEnv: this.#context.
|
|
146
|
-
strictEnv:
|
|
171
|
+
onMissingEnv: this.#context.onMissingEnv,
|
|
172
|
+
strictEnv: false
|
|
147
173
|
})
|
|
148
174
|
const pkg = await loadConfigurationModule(appConfig.path, unvalidatedConfig)
|
|
149
175
|
this.capability = await pkg.create(appConfig.path, appConfig.config, this.#context)
|
|
@@ -215,7 +241,7 @@ export class Controller extends EventEmitter {
|
|
|
215
241
|
}
|
|
216
242
|
}
|
|
217
243
|
|
|
218
|
-
const listen = !!this.applicationConfig.useHttp
|
|
244
|
+
const listen = !!(this.applicationConfig.useHttp || this.applicationConfig.websocket)
|
|
219
245
|
|
|
220
246
|
try {
|
|
221
247
|
await this.capability.start({ listen })
|
|
@@ -264,7 +290,12 @@ export class Controller extends EventEmitter {
|
|
|
264
290
|
|
|
265
291
|
async listen () {
|
|
266
292
|
// This server is not an entrypoint or already listened in start. Behave as no-op.
|
|
267
|
-
if (
|
|
293
|
+
if (
|
|
294
|
+
!this.applicationConfig.entrypoint ||
|
|
295
|
+
this.applicationConfig.useHttp ||
|
|
296
|
+
this.applicationConfig.websocket ||
|
|
297
|
+
this.#listening
|
|
298
|
+
) {
|
|
268
299
|
return
|
|
269
300
|
}
|
|
270
301
|
|
package/lib/worker/main.js
CHANGED
|
@@ -274,7 +274,7 @@ async function main () {
|
|
|
274
274
|
let serverConfig = null
|
|
275
275
|
if (runtimeConfig.server && applicationConfig.entrypoint) {
|
|
276
276
|
serverConfig = runtimeConfig.server
|
|
277
|
-
} else if (applicationConfig.useHttp) {
|
|
277
|
+
} else if (applicationConfig.useHttp || applicationConfig.websocket) {
|
|
278
278
|
serverConfig = {
|
|
279
279
|
port: 0,
|
|
280
280
|
hostname: '127.0.0.1',
|
package/lib/worker/management.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@platformatic/runtime",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.69.0",
|
|
4
4
|
"description": "Platformatic runtime",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -35,18 +35,18 @@
|
|
|
35
35
|
"pino-abstract-transport": "^2.0.0",
|
|
36
36
|
"pino-sentry-transport": "^1.6.0",
|
|
37
37
|
"split2": "^4.2.0",
|
|
38
|
-
"tstyche": "^
|
|
38
|
+
"tstyche": "^7.0.0",
|
|
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.
|
|
43
|
-
"@platformatic/db": "3.
|
|
44
|
-
"@platformatic/
|
|
45
|
-
"@platformatic/
|
|
46
|
-
"@platformatic/
|
|
47
|
-
"@platformatic/sql-
|
|
48
|
-
"@platformatic/
|
|
49
|
-
"@platformatic/
|
|
42
|
+
"@platformatic/composer": "3.69.0",
|
|
43
|
+
"@platformatic/db": "3.69.0",
|
|
44
|
+
"@platformatic/gateway": "3.69.0",
|
|
45
|
+
"@platformatic/node": "3.69.0",
|
|
46
|
+
"@platformatic/service": "3.69.0",
|
|
47
|
+
"@platformatic/sql-graphql": "3.69.0",
|
|
48
|
+
"@platformatic/sql-mapper": "3.69.0",
|
|
49
|
+
"@platformatic/wattpm-pprof-capture": "3.69.0"
|
|
50
50
|
},
|
|
51
51
|
"dependencies": {
|
|
52
52
|
"@fastify/accepts": "^5.0.0",
|
|
@@ -77,13 +77,13 @@
|
|
|
77
77
|
"undici": "^7.27.2",
|
|
78
78
|
"undici-thread-interceptor": "^1.3.1",
|
|
79
79
|
"ws": "^8.16.0",
|
|
80
|
-
"@platformatic/basic": "3.
|
|
81
|
-
"@platformatic/
|
|
82
|
-
"@platformatic/
|
|
83
|
-
"@platformatic/
|
|
84
|
-
"@platformatic/
|
|
85
|
-
"@platformatic/telemetry": "3.
|
|
86
|
-
"@platformatic/
|
|
80
|
+
"@platformatic/basic": "3.69.0",
|
|
81
|
+
"@platformatic/foundation": "3.69.0",
|
|
82
|
+
"@platformatic/globals": "3.69.0",
|
|
83
|
+
"@platformatic/metrics": "3.69.0",
|
|
84
|
+
"@platformatic/generators": "3.69.0",
|
|
85
|
+
"@platformatic/telemetry": "3.69.0",
|
|
86
|
+
"@platformatic/itc": "3.69.0"
|
|
87
87
|
},
|
|
88
88
|
"engines": {
|
|
89
89
|
"node": ">=22.19.0"
|
package/schema.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"$id": "https://schemas.platformatic.dev/@platformatic/runtime/3.
|
|
2
|
+
"$id": "https://schemas.platformatic.dev/@platformatic/runtime/3.69.0.json",
|
|
3
3
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
4
4
|
"title": "Platformatic Runtime Config",
|
|
5
5
|
"type": "object",
|
|
@@ -142,6 +142,9 @@
|
|
|
142
142
|
"useHttp": {
|
|
143
143
|
"type": "boolean"
|
|
144
144
|
},
|
|
145
|
+
"websocket": {
|
|
146
|
+
"type": "boolean"
|
|
147
|
+
},
|
|
145
148
|
"reuseTcpPorts": {
|
|
146
149
|
"type": "boolean",
|
|
147
150
|
"default": true
|
|
@@ -574,6 +577,9 @@
|
|
|
574
577
|
"useHttp": {
|
|
575
578
|
"type": "boolean"
|
|
576
579
|
},
|
|
580
|
+
"websocket": {
|
|
581
|
+
"type": "boolean"
|
|
582
|
+
},
|
|
577
583
|
"reuseTcpPorts": {
|
|
578
584
|
"type": "boolean",
|
|
579
585
|
"default": true
|
|
@@ -1004,6 +1010,9 @@
|
|
|
1004
1010
|
"useHttp": {
|
|
1005
1011
|
"type": "boolean"
|
|
1006
1012
|
},
|
|
1013
|
+
"websocket": {
|
|
1014
|
+
"type": "boolean"
|
|
1015
|
+
},
|
|
1007
1016
|
"reuseTcpPorts": {
|
|
1008
1017
|
"type": "boolean",
|
|
1009
1018
|
"default": true
|
|
@@ -1434,6 +1443,9 @@
|
|
|
1434
1443
|
"useHttp": {
|
|
1435
1444
|
"type": "boolean"
|
|
1436
1445
|
},
|
|
1446
|
+
"websocket": {
|
|
1447
|
+
"type": "boolean"
|
|
1448
|
+
},
|
|
1437
1449
|
"reuseTcpPorts": {
|
|
1438
1450
|
"type": "boolean",
|
|
1439
1451
|
"default": true
|