@platformatic/runtime 3.72.0 → 4.0.0-alpha.2
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 +602 -673
- package/index.d.ts +31 -28
- package/index.js +290 -30
- package/lib/config.js +270 -367
- package/lib/errors.js +32 -13
- package/lib/generator.js +249 -127
- package/lib/logger.js +3 -3
- package/lib/management-api.js +44 -72
- package/lib/management-handlers.js +3 -5
- package/lib/memory.js +29 -0
- package/lib/policies.js +27 -0
- package/lib/predictive-scaling.js +910 -0
- package/lib/predictive-worker-scaler.js +378 -0
- package/lib/runtime.js +1179 -606
- package/lib/scheduler.js +6 -41
- package/lib/schema.js +77 -15
- package/lib/upgrade.js +16 -1
- package/lib/versions/v4.0.0.js +31 -0
- package/lib/worker/controller.js +89 -109
- package/lib/worker/interceptors.js +34 -34
- package/lib/worker/itc.js +248 -66
- package/lib/worker/main.js +50 -98
- package/lib/worker/messaging.js +42 -1
- package/lib/worker/round-robin-map.js +10 -3
- package/lib/worker/symbols.js +6 -2
- package/package.json +33 -24
- package/schema.json +252 -433
- package/.gitignore +0 -3
- package/.npmignore +0 -2
- package/lib/scaling-algorithm.js +0 -256
- package/lib/worker-scaler.js +0 -249
package/index.d.ts
CHANGED
|
@@ -7,10 +7,15 @@ import { JSONSchemaType } from 'ajv'
|
|
|
7
7
|
import * as colorette from 'colorette'
|
|
8
8
|
import { EventEmitter } from 'node:events'
|
|
9
9
|
import { Logger } from 'pino'
|
|
10
|
-
import { PlatformaticRuntimeConfig } from './config.js'
|
|
10
|
+
import { MetricsOptions, PlatformaticRuntimeConfig } from './config.js'
|
|
11
11
|
|
|
12
12
|
export type RuntimeConfiguration = Configuration<PlatformaticRuntimeConfig>
|
|
13
13
|
|
|
14
|
+
export interface MetricsConfigUpdateResult {
|
|
15
|
+
success: boolean
|
|
16
|
+
config: MetricsOptions
|
|
17
|
+
}
|
|
18
|
+
|
|
14
19
|
export type ApplicationCommandContext = {
|
|
15
20
|
colorette: typeof colorette
|
|
16
21
|
parseArgs: typeof parseArgs
|
|
@@ -37,7 +42,11 @@ export interface LoopbackMessagingOptions {
|
|
|
37
42
|
}
|
|
38
43
|
|
|
39
44
|
export namespace errors {
|
|
45
|
+
export const AddressInUseError: (port: number, firstApplication: string, secondApplication: string) => FastifyError
|
|
46
|
+
export const WorkerAddressInUseError: (port: number, application: string) => FastifyError
|
|
40
47
|
export const RuntimeExitedError: () => FastifyError
|
|
48
|
+
export const MetricsDisabledError: () => FastifyError
|
|
49
|
+
export const FailedToUpdateMetricsConfigError: (worker: string, reason: string) => FastifyError
|
|
41
50
|
export const UnknownRuntimeAPICommandError: (command: string) => FastifyError
|
|
42
51
|
export const ApplicationNotFoundError: (id: string) => FastifyError
|
|
43
52
|
export const ApplicationNotStartedError: (id: string) => FastifyError
|
|
@@ -46,8 +55,6 @@ export namespace errors {
|
|
|
46
55
|
export const RuntimeNotStartedError: () => FastifyError
|
|
47
56
|
export const ConfigPathMustBeStringError: () => FastifyError
|
|
48
57
|
export const NoConfigFileFoundError: (id: string) => FastifyError
|
|
49
|
-
export const InvalidEntrypointError: (entrypoint: string) => FastifyError
|
|
50
|
-
export const MissingEntrypointError: () => FastifyError
|
|
51
58
|
export const MissingDependencyError: (dependency: string) => FastifyError
|
|
52
59
|
export const InspectAndInspectBrkError: () => FastifyError
|
|
53
60
|
export const InspectorPortError: () => FastifyError
|
|
@@ -86,9 +93,9 @@ export namespace symbols {
|
|
|
86
93
|
export const kHealthCheckTimer: unique symbol
|
|
87
94
|
export const kHealthMetricsTimer: unique symbol
|
|
88
95
|
export const kLastHealthCheckELU: unique symbol
|
|
89
|
-
export const kLastWorkerScalerELU: unique symbol
|
|
90
96
|
export const kWorkerStatus: unique symbol
|
|
91
97
|
export const kWorkerHealthSignals: unique symbol
|
|
98
|
+
export const kWorkerUrl: unique symbol
|
|
92
99
|
export const kStderrMarker: string
|
|
93
100
|
export const kWorkersBroadcast: unique symbol
|
|
94
101
|
}
|
|
@@ -110,23 +117,29 @@ export interface InjectResponse {
|
|
|
110
117
|
rawPayload: ArrayBuffer
|
|
111
118
|
}
|
|
112
119
|
|
|
120
|
+
export type ServingState = 'listening' | 'mesh-only' | 'background' | 'inactive'
|
|
121
|
+
|
|
113
122
|
export interface ApplicationDetails {
|
|
114
123
|
id: string
|
|
115
124
|
type?: string
|
|
125
|
+
/** v3 only: the path to the application's configuration file. v4 reports configPath. */
|
|
116
126
|
config?: string
|
|
127
|
+
/** v4 only: the configuration file the loader decided on, absent for an inline definition. */
|
|
128
|
+
configPath?: string
|
|
117
129
|
path?: string
|
|
118
130
|
status?: string
|
|
131
|
+
/** How the application serves, as opposed to whether it runs. Absent when it is not started. */
|
|
132
|
+
servingState?: ServingState
|
|
119
133
|
dependencies?: string[]
|
|
120
134
|
version?: string
|
|
121
135
|
localUrl?: string
|
|
122
|
-
entrypoint?: boolean
|
|
123
136
|
sourceMaps?: boolean
|
|
124
137
|
workers?: number
|
|
125
138
|
url?: string | null
|
|
139
|
+
urls?: string[]
|
|
126
140
|
}
|
|
127
141
|
|
|
128
142
|
export interface ApplicationsTopology {
|
|
129
|
-
entrypoint: string
|
|
130
143
|
production: boolean
|
|
131
144
|
applications: ApplicationDetails[]
|
|
132
145
|
}
|
|
@@ -157,14 +170,6 @@ export interface SchedulerJobBase {
|
|
|
157
170
|
nextRunAt?: string | null
|
|
158
171
|
}
|
|
159
172
|
|
|
160
|
-
export interface ConfiguredSchedulerJob extends SchedulerJobBase {
|
|
161
|
-
source: 'config'
|
|
162
|
-
callbackUrl: string
|
|
163
|
-
method: string
|
|
164
|
-
headers?: Record<string, string>
|
|
165
|
-
body?: string | Record<string, unknown>
|
|
166
|
-
}
|
|
167
|
-
|
|
168
173
|
export interface ApplicationSchedulerJob extends SchedulerJobBase {
|
|
169
174
|
source: 'application'
|
|
170
175
|
applicationId: string
|
|
@@ -172,7 +177,7 @@ export interface ApplicationSchedulerJob extends SchedulerJobBase {
|
|
|
172
177
|
tasks: string[]
|
|
173
178
|
}
|
|
174
179
|
|
|
175
|
-
export type SchedulerJob =
|
|
180
|
+
export type SchedulerJob = ApplicationSchedulerJob
|
|
176
181
|
|
|
177
182
|
export interface SchedulerRunResult {
|
|
178
183
|
name: string
|
|
@@ -194,10 +199,14 @@ export interface RuntimeMetadata {
|
|
|
194
199
|
execPath: string
|
|
195
200
|
nodeVersion: string
|
|
196
201
|
projectDir: string
|
|
202
|
+
/** The configuration file the loader decided on. Null when the runtime was built from an object. */
|
|
203
|
+
configPath: string | null
|
|
204
|
+
/** The autoload declaration, with its path resolved. Null when the project does not autoload. */
|
|
205
|
+
autoload: { path: string, exclude?: string[], mappings?: Record<string, unknown> } | null
|
|
197
206
|
packageName: string | null
|
|
198
207
|
packageVersion: string | null
|
|
199
|
-
url: string | null
|
|
200
208
|
platformaticVersion: string
|
|
209
|
+
urls: Record<string, string>
|
|
201
210
|
}
|
|
202
211
|
|
|
203
212
|
export interface WorkerLifecycleEvent {
|
|
@@ -234,9 +243,7 @@ export interface HealthMetricsEvent extends WorkerLifecycleEvent {
|
|
|
234
243
|
healthSignals: RuntimeHealthSignal[]
|
|
235
244
|
}
|
|
236
245
|
|
|
237
|
-
export
|
|
238
|
-
constructor (allowedOperations?: string[])
|
|
239
|
-
|
|
246
|
+
export interface ManagementClient {
|
|
240
247
|
getRuntimeStatus (): Promise<string>
|
|
241
248
|
getRuntimeMetadata (): Promise<RuntimeMetadata>
|
|
242
249
|
getRuntimeConfig (): Promise<Record<string, unknown>>
|
|
@@ -258,7 +265,7 @@ export declare class ManagementClient {
|
|
|
258
265
|
startApplication (id: string): Promise<void>
|
|
259
266
|
stopApplication (id: string): Promise<void>
|
|
260
267
|
restartApplication (id: string): Promise<void>
|
|
261
|
-
restart (applications?: string[]): Promise<
|
|
268
|
+
restart (applications?: string[]): Promise<void>
|
|
262
269
|
addApplications (applications: unknown[], start?: boolean): Promise<ApplicationDetails[]>
|
|
263
270
|
removeApplications (ids: string[]): Promise<ApplicationDetails[]>
|
|
264
271
|
inject (id: string, injectParams: InjectParams): Promise<InjectResponse>
|
|
@@ -370,12 +377,11 @@ export type RuntimeExtension = (
|
|
|
370
377
|
|
|
371
378
|
export declare class Runtime extends EventEmitter {
|
|
372
379
|
init (): Promise<void>
|
|
373
|
-
start (silent?: boolean): Promise<string
|
|
380
|
+
start (silent?: boolean): Promise<Record<string, string>>
|
|
374
381
|
stop (silent?: boolean): Promise<void>
|
|
375
382
|
close (silent?: boolean): Promise<void>
|
|
376
|
-
restart (applications?: string[]): Promise<
|
|
383
|
+
restart (applications?: string[]): Promise<void>
|
|
377
384
|
inject (id: string, injectParams: InjectParams): Promise<InjectResponse>
|
|
378
|
-
getUrl (): string | undefined
|
|
379
385
|
getRuntimeStatus (): string
|
|
380
386
|
getRuntimeMetadata (): Promise<RuntimeMetadata>
|
|
381
387
|
getRuntimeEnv (): Record<string, string>
|
|
@@ -386,6 +392,7 @@ export declare class Runtime extends EventEmitter {
|
|
|
386
392
|
getRuntimeConfig (includeMeta: true): RuntimeConfiguration
|
|
387
393
|
getRuntimeConfig (includeMeta?: boolean): Record<string, unknown>
|
|
388
394
|
getApplicationsIds (): string[]
|
|
395
|
+
getUrls (applicationId?: string): Record<string, string>
|
|
389
396
|
/**
|
|
390
397
|
* Returns topology for every configured application.
|
|
391
398
|
* When `allowUnloaded` is `true`, applications without running workers are
|
|
@@ -419,6 +426,7 @@ export declare class Runtime extends EventEmitter {
|
|
|
419
426
|
getApplicationOpenapiSchema (id: string): Promise<unknown>
|
|
420
427
|
getApplicationGraphqlSchema (id: string): Promise<unknown>
|
|
421
428
|
getMetrics (format?: string): Promise<{ metrics: unknown }>
|
|
429
|
+
updateMetricsConfig (metricsConfig: Partial<MetricsOptions>): Promise<MetricsConfigUpdateResult>
|
|
422
430
|
/**
|
|
423
431
|
* Returns the current shared context. Synchronous on the main thread.
|
|
424
432
|
* Do not mutate it in place — use `updateSharedContext()` so changes are
|
|
@@ -472,11 +480,6 @@ export declare class Runtime extends EventEmitter {
|
|
|
472
480
|
runApplicationScheduledTasks (id: string, scheduleId: string, scheduledTime: number): Promise<unknown>
|
|
473
481
|
}
|
|
474
482
|
|
|
475
|
-
export function wrapInRuntimeConfig (
|
|
476
|
-
config: Configuration<unknown>,
|
|
477
|
-
context?: ConfigurationOptions
|
|
478
|
-
): Promise<RuntimeConfiguration>
|
|
479
|
-
|
|
480
483
|
export declare const version: string
|
|
481
484
|
|
|
482
485
|
export declare function loadConfiguration (
|
package/index.js
CHANGED
|
@@ -1,20 +1,27 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { importCapabilityPackage, resolve } from '@platformatic/basic'
|
|
2
2
|
import {
|
|
3
3
|
abstractLogger,
|
|
4
4
|
ensureLoggableError,
|
|
5
|
-
extractModuleFromSchemaUrl,
|
|
6
5
|
findRuntimeConfigurationFile,
|
|
7
6
|
kMetadata,
|
|
8
|
-
loadConfigurationModule
|
|
9
|
-
loadConfiguration as utilsLoadConfiguration
|
|
7
|
+
loadConfigurationModule
|
|
10
8
|
} from '@platformatic/foundation'
|
|
9
|
+
import {
|
|
10
|
+
findDecidingFile,
|
|
11
|
+
isProductionCommand,
|
|
12
|
+
LegacyConfigurationFileError,
|
|
13
|
+
loadConfiguration as loadFileConfiguration,
|
|
14
|
+
loadObjectConfiguration,
|
|
15
|
+
validateCapabilityConfiguration
|
|
16
|
+
} from '@platformatic/foundation/loader'
|
|
11
17
|
import closeWithGrace from 'close-with-grace'
|
|
18
|
+
import { stat } from 'node:fs/promises'
|
|
12
19
|
import inspector from 'node:inspector'
|
|
13
|
-
import {
|
|
20
|
+
import { dirname, resolve as resolvePath } from 'node:path'
|
|
21
|
+
import { transformConfiguration } from './lib/config.js'
|
|
14
22
|
import { NodeInspectorFlagsNotSupportedError } from './lib/errors.js'
|
|
15
23
|
import { Runtime } from './lib/runtime.js'
|
|
16
|
-
import {
|
|
17
|
-
import { upgrade } from './lib/upgrade.js'
|
|
24
|
+
import { configurationSchema } from './lib/schema.js'
|
|
18
25
|
|
|
19
26
|
async function restartRuntime (runtime) {
|
|
20
27
|
runtime.logger.info('Received SIGUSR2, restarting all applications ...')
|
|
@@ -59,34 +66,264 @@ function handleSignal (runtime, config) {
|
|
|
59
66
|
})
|
|
60
67
|
}
|
|
61
68
|
|
|
69
|
+
/*
|
|
70
|
+
The configuration is code, evaluated by the loader in foundation: it walks to the deciding file,
|
|
71
|
+
resolves both views of the env ladder, evaluates the root in a worker and fans out one worker per
|
|
72
|
+
per-app file. Everything the runtime transform used to discover — the application list, each
|
|
73
|
+
application's capability, its schema and its worker environment — arrives already resolved.
|
|
74
|
+
|
|
75
|
+
Routing by filename keeps the legacy path intact while the in-tree JSON fixtures are converted; the
|
|
76
|
+
legacy half is what a later commit deletes, not something this one has to keep working around.
|
|
77
|
+
*/
|
|
78
|
+
/*
|
|
79
|
+
`--config` names a configuration; it does not widen what a configuration may be called. The four
|
|
80
|
+
names are the format, and `resolveNamedConfigurationFile` is what refuses anything else -- it
|
|
81
|
+
separates a legacy name, which gets the migrate hint, from an unrecognised one, which gets told
|
|
82
|
+
what the four names are. Deciding here instead produced one of those answers for both.
|
|
83
|
+
|
|
84
|
+
What `--config` does change is *where*: a named file need not sit where discovery would look.
|
|
85
|
+
*/
|
|
86
|
+
function isNamedConfigurationPath (path) {
|
|
87
|
+
return /\.(json|json5|ya?ml|to?ml|js|mjs|ts|mts)$/.test(path)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function findConfigurationForSource (configOrRoot, sourceOrConfig) {
|
|
91
|
+
// A programmatic object source is not this path: the object entry point is
|
|
92
|
+
// loadObjectConfiguration, which skips the root eval worker entirely.
|
|
93
|
+
if (sourceOrConfig && typeof sourceOrConfig !== 'string') {
|
|
94
|
+
return null
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (typeof sourceOrConfig === 'string') {
|
|
98
|
+
const named = resolvePath(configOrRoot, sourceOrConfig)
|
|
99
|
+
|
|
100
|
+
// Named outright: the loader decides whether the name is one it accepts, and says which it is.
|
|
101
|
+
return isNamedConfigurationPath(named) ? named : null
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (typeof configOrRoot !== 'string') {
|
|
105
|
+
return null
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (isNamedConfigurationPath(configOrRoot)) {
|
|
109
|
+
return configOrRoot
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/*
|
|
113
|
+
Only an actual directory is a place to search from. findDecidingFile walks toward the env root,
|
|
114
|
+
so handing it a path that does not exist would answer with whatever project sits above it: a
|
|
115
|
+
typo'd --config would boot the parent rather than failing. Discovery looks in the named directory
|
|
116
|
+
and nowhere else, and a wrong path has to stay an error.
|
|
117
|
+
*/
|
|
118
|
+
const stats = await stat(configOrRoot).catch(() => null)
|
|
119
|
+
|
|
120
|
+
if (!stats?.isDirectory()) {
|
|
121
|
+
return null
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/*
|
|
125
|
+
A legacy configuration found by the walk is refused, with the error that names migrate. It used to
|
|
126
|
+
be swallowed so the legacy loader below could answer instead; that loader is gone, and the refusal
|
|
127
|
+
is the correct answer again.
|
|
128
|
+
*/
|
|
129
|
+
const deciding = await findDecidingFile(configOrRoot, { throwOnMissing: false })
|
|
130
|
+
|
|
131
|
+
return deciding?.path ?? null
|
|
132
|
+
}
|
|
133
|
+
|
|
62
134
|
export async function loadConfiguration (configOrRoot, sourceOrConfig, context) {
|
|
135
|
+
// Checked before the legacy resolver, which throws when it finds no legacy file — a project has
|
|
136
|
+
// none by construction.
|
|
137
|
+
const decidingConfigurationFile = await findConfigurationForSource(configOrRoot, sourceOrConfig)
|
|
138
|
+
|
|
139
|
+
if (decidingConfigurationFile) {
|
|
140
|
+
return loadRuntimeConfiguration(decidingConfigurationFile, context)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/*
|
|
144
|
+
Level 0. A directory with no configuration file of any kind is not an error: the loader
|
|
145
|
+
synthesizes one in memory from what the directory contains. Reaching the legacy resolver instead
|
|
146
|
+
made a configless project fail to load, which is what the removed temporary-file fallback --
|
|
147
|
+
detect the type, write a watt.json into the user's tree, then load that -- existed to paper
|
|
148
|
+
over. Nothing is written to disk now.
|
|
149
|
+
*/
|
|
150
|
+
if (!sourceOrConfig && typeof configOrRoot === 'string' && !isNamedConfigurationPath(configOrRoot)) {
|
|
151
|
+
const stats = await stat(configOrRoot).catch(() => null)
|
|
152
|
+
|
|
153
|
+
if (stats?.isDirectory()) {
|
|
154
|
+
return loadRuntimeConfiguration({ cwd: configOrRoot }, context)
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
63
158
|
const { root, source } = await resolve(configOrRoot, sourceOrConfig, 'runtime')
|
|
64
159
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
root
|
|
68
|
-
|
|
69
|
-
|
|
160
|
+
/*
|
|
161
|
+
A configuration handed over as an object rather than named as a file: what an embedder builds in
|
|
162
|
+
memory, and what the ICC generates. There is nothing to evaluate, so it skips the root eval
|
|
163
|
+
worker and joins the pipeline at validation.
|
|
164
|
+
*/
|
|
165
|
+
if (source && typeof source !== 'string') {
|
|
166
|
+
return loadRuntimeConfiguration({ root, source }, context)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/*
|
|
170
|
+
A path that reaches here named a file the walk did not recognise as configuration, so the walk's
|
|
171
|
+
own refusal is the answer -- it names the file and tells the reader to run migrate, which is more
|
|
172
|
+
use than loading a legacy configuration into a runtime that no longer implements what it says.
|
|
173
|
+
*/
|
|
174
|
+
throw new LegacyConfigurationFileError(source)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/*
|
|
178
|
+
`target` is either a configuration file to evaluate or `{ root, source }` for a configuration
|
|
179
|
+
handed over as an object -- what an embedder builds in memory and what the ICC generates. The two
|
|
180
|
+
differ only in where the configuration comes from: an object skips the root eval worker, since
|
|
181
|
+
there is nothing to evaluate, and everything after that is the same pipeline.
|
|
182
|
+
*/
|
|
183
|
+
async function loadRuntimeConfiguration (target, context) {
|
|
184
|
+
/*
|
|
185
|
+
The command decides `production` when the caller did not, rather than the other way round:
|
|
186
|
+
`build` produces production artifacts, so a build that evaluated as a development boot read the
|
|
187
|
+
development env files and gave every callback `production: false` while writing what `start`
|
|
188
|
+
would later run.
|
|
189
|
+
*/
|
|
190
|
+
const diagnostics = context?.logger ?? abstractLogger
|
|
191
|
+
const explicitProduction = context?.isProduction ?? context?.production
|
|
192
|
+
const command = context?.command ?? (explicitProduction ? 'start' : 'dev')
|
|
193
|
+
const production = explicitProduction ?? isProductionCommand(command)
|
|
194
|
+
|
|
195
|
+
/*
|
|
196
|
+
The environment the loader treats as real. An embedder can add to it, and can ask for a
|
|
197
|
+
hermetic runtime that sees none of this process's -- the loader honours both through loadEnv, and
|
|
198
|
+
a runtime that ignored them would quietly hand every worker the parent's environment.
|
|
199
|
+
*/
|
|
200
|
+
const realEnv = context?.ignoreProcessEnv ? { ...context?.env } : { ...process.env, ...context?.env }
|
|
70
201
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
202
|
+
const objectSource = typeof target !== 'string' && target.source !== undefined
|
|
203
|
+
const shared = {
|
|
204
|
+
realEnv,
|
|
205
|
+
command,
|
|
206
|
+
mode: context?.mode,
|
|
207
|
+
production,
|
|
208
|
+
customEnvFile: context?.envFile,
|
|
209
|
+
/*
|
|
210
|
+
Step 3 of the pipeline validates orchestration in the eval worker, before autoload expansion
|
|
211
|
+
acts on it -- without the schema that step is skipped, and a malformed `autoload` reaches the
|
|
212
|
+
filesystem walk as a raw TypeError instead of a validation error naming the property.
|
|
213
|
+
*/
|
|
214
|
+
schema: context?.schema ?? configurationSchema,
|
|
215
|
+
/*
|
|
216
|
+
The evaluation deadline. A configuration that never resolves — an awaited fetch to a dead
|
|
217
|
+
host, a forgotten promise — otherwise hangs the boot rather than failing it, and the default
|
|
218
|
+
is a guess about how long a reasonable one takes. A deployment that knows better says so.
|
|
219
|
+
*/
|
|
220
|
+
timeout: context?.configTimeout,
|
|
221
|
+
// The one file --debug-config evaluates in this process when an inspector is attached, so a
|
|
222
|
+
// breakpoint in it is reachable at all: a throwaway worker dies before one can attach.
|
|
223
|
+
inProcessTarget: context?.inProcessTarget,
|
|
224
|
+
/*
|
|
225
|
+
Validation imports each capability's schema, so it needs the capability installed — not
|
|
226
|
+
merely declared, which is all the detector needs. That moves the moment a capability must be
|
|
227
|
+
resolvable earlier than it once had to be: loading used to succeed and fail later, in the worker.
|
|
228
|
+
|
|
229
|
+
A caller that is loading only to discover the topology — a tool about to install those
|
|
230
|
+
dependencies, for instance — can say so rather than being made to install first.
|
|
231
|
+
*/
|
|
232
|
+
validateCapabilities: context?.validateCapabilities ?? true,
|
|
233
|
+
// The runtime is where the bundled capability copies live, so it is the fallback scope for
|
|
234
|
+
// both the schema import and the version stamp — the application's own dependencies still
|
|
235
|
+
// come first.
|
|
236
|
+
runtimeScope: import.meta.filename,
|
|
237
|
+
/*
|
|
238
|
+
These are the loader's user-facing diagnostics — the standalone-boot warning, the zero-config
|
|
239
|
+
inference notice, the capability the detector chose — and they were being handed to
|
|
240
|
+
abstractLogger, which is a no-op. Every one of them was discarded, including the warning the
|
|
241
|
+
format relies on to tell someone that nothing the root says is being applied.
|
|
242
|
+
|
|
243
|
+
A caller with a real logger passes it. The default stays silent because an embedder loading a
|
|
244
|
+
configuration has not asked for anything on its stderr.
|
|
245
|
+
*/
|
|
246
|
+
onWarning: warning => diagnostics.warn(warning.message),
|
|
247
|
+
onInfo: info => diagnostics.info(info.message)
|
|
75
248
|
}
|
|
76
249
|
|
|
77
|
-
|
|
78
|
-
if (
|
|
79
|
-
|
|
250
|
+
let loaded
|
|
251
|
+
if (objectSource) {
|
|
252
|
+
loaded = await loadObjectConfiguration({ root: target.root, source: target.source, ...shared })
|
|
253
|
+
} else if (typeof target === 'string') {
|
|
254
|
+
loaded = await loadFileConfiguration({ cwd: dirname(target), configPath: target, ...shared })
|
|
255
|
+
} else {
|
|
256
|
+
// No file was named and none was found: the loader searches from this directory and, finding
|
|
257
|
+
// nothing, synthesizes.
|
|
258
|
+
loaded = await loadFileConfiguration({ cwd: target.cwd, ...shared })
|
|
80
259
|
}
|
|
81
260
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
261
|
+
const config = loaded.config
|
|
262
|
+
|
|
263
|
+
/*
|
|
264
|
+
The documented pipeline is validation with useDefaults, then kMetadata, then transform. The eval
|
|
265
|
+
worker's own check is a shape check that injects nothing — deliberately, so the resolve
|
|
266
|
+
projection carries authored values — so this is where the runtime schema's defaults arrive, and
|
|
267
|
+
without it the runtime reaches for settings like gracefulShutdown that nothing supplied.
|
|
268
|
+
*/
|
|
269
|
+
validateCapabilityConfiguration(config, context?.schema ?? configurationSchema, {
|
|
270
|
+
id: 'runtime',
|
|
271
|
+
module: '@platformatic/runtime',
|
|
272
|
+
root: loaded.root
|
|
89
273
|
})
|
|
274
|
+
|
|
275
|
+
// kMetadata is symbol-keyed and cannot cross a worker boundary, so each worker rebuilds it; this
|
|
276
|
+
// is the main process's copy. env is the runtime's own view — the real environment under the root
|
|
277
|
+
// env block and the root's env files — and every application worker gets the per-application
|
|
278
|
+
// environment the loader resolved instead, which this one is not a substitute for.
|
|
279
|
+
config[kMetadata] = {
|
|
280
|
+
root: loaded.root,
|
|
281
|
+
path: loaded.configPath,
|
|
282
|
+
env: loaded.env ?? process.env,
|
|
283
|
+
module: '@platformatic/runtime',
|
|
284
|
+
/*
|
|
285
|
+
The evaluation context, kept beside the envelope rather than inside it: applications added
|
|
286
|
+
after boot are evaluated with the same command, mode and production flags the boot pass used,
|
|
287
|
+
and its presence is also what tells the add path this runtime resolves configuration main-side.
|
|
288
|
+
A legacy runtime has no such context and keeps resolving configuration files worker-side.
|
|
289
|
+
*/
|
|
290
|
+
loader: {
|
|
291
|
+
command,
|
|
292
|
+
mode: loaded.mode,
|
|
293
|
+
production: loaded.production,
|
|
294
|
+
/*
|
|
295
|
+
The environment contract of the boot, carried so an application added while the runtime
|
|
296
|
+
runs is enveloped under the same one: the realEnv the boot resolved ladders against
|
|
297
|
+
(hermetic under ignoreProcessEnv, augmented by context.env) and the --env file that
|
|
298
|
+
replaces every application's env-file rung. Without them the add path defaulted to
|
|
299
|
+
process.env and the conventional .env chain -- an environment the boot says cannot exist.
|
|
300
|
+
*/
|
|
301
|
+
realEnv,
|
|
302
|
+
customEnvFile: context?.envFile,
|
|
303
|
+
/*
|
|
304
|
+
Everything the evaluation actually read: the configuration files, their import graph minus
|
|
305
|
+
node_modules, the env files consulted, and the directories whose membership decides the
|
|
306
|
+
application list. `wattpm dev` reloads on any of it -- watching only the deciding file meant
|
|
307
|
+
a config split across a helper module reloaded for none of its own edits.
|
|
308
|
+
*/
|
|
309
|
+
watchTargets: loaded.watchTargets,
|
|
310
|
+
/*
|
|
311
|
+
Every entry carrying a `url`, recorded on the way past -- before the enabled filter, which
|
|
312
|
+
is the point. `enabled` is the supported way to keep an application out of a boot, and an
|
|
313
|
+
entry it hides from `resolve` is one whose clone never arrives.
|
|
314
|
+
*/
|
|
315
|
+
resolveCandidates: loaded.resolveCandidates
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/*
|
|
320
|
+
A caller-supplied transform replaces the built-in one, exactly as it does on the legacy path where
|
|
321
|
+
it arrives as part of ...context. Ignoring it here meant a test helper or an embedder that
|
|
322
|
+
customized the configuration was silently not consulted.
|
|
323
|
+
*/
|
|
324
|
+
const apply = context?.transform ?? transformConfiguration
|
|
325
|
+
|
|
326
|
+
return apply(config, null, context)
|
|
90
327
|
}
|
|
91
328
|
|
|
92
329
|
export async function loadApplicationsCommands (executableName = '', configurationFile = null) {
|
|
@@ -111,7 +348,13 @@ export async function loadApplicationsCommands (executableName = '', configurati
|
|
|
111
348
|
throw new Error('No runtime configuration file found.')
|
|
112
349
|
}
|
|
113
350
|
|
|
114
|
-
|
|
351
|
+
/*
|
|
352
|
+
'exec' is every non-boot evaluation, and enumerating a capability's commands is one: nothing
|
|
353
|
+
starts. Without it this evaluated as a development boot — development env files, and
|
|
354
|
+
`production: false` handed to every callback — which is a context no invocation of these
|
|
355
|
+
commands actually runs in.
|
|
356
|
+
*/
|
|
357
|
+
config = await loadConfiguration(file, null, { command: 'exec' })
|
|
115
358
|
|
|
116
359
|
/* c8 ignore next 3 - Hard to test */
|
|
117
360
|
if (!config) {
|
|
@@ -123,8 +366,12 @@ export async function loadApplicationsCommands (executableName = '', configurati
|
|
|
123
366
|
|
|
124
367
|
for (const application of config.applications) {
|
|
125
368
|
try {
|
|
126
|
-
|
|
127
|
-
|
|
369
|
+
// An application arrives with its configuration already evaluated and its capability
|
|
370
|
+
// already named, so there is nothing to read and nothing to infer.
|
|
371
|
+
const applicationConfig = application.resolvedConfig
|
|
372
|
+
const pkg = application.module
|
|
373
|
+
? await importCapabilityPackage(application.path, application.module)
|
|
374
|
+
: await loadConfigurationModule(application.path, applicationConfig)
|
|
128
375
|
|
|
129
376
|
if (pkg.createCommands) {
|
|
130
377
|
const definition = await pkg.createCommands(application.id)
|
|
@@ -175,7 +422,20 @@ export async function create (configOrRoot, sourceOrConfig, context) {
|
|
|
175
422
|
return runtime
|
|
176
423
|
}
|
|
177
424
|
|
|
178
|
-
|
|
425
|
+
/*
|
|
426
|
+
The one entry point for an application added while the runtime is running -- what an extension
|
|
427
|
+
calls before `addApplications`. It replaces the exported `prepareApplication`, which is the legacy
|
|
428
|
+
half of the same job: an added application has to be evaluated the way boot evaluates
|
|
429
|
+
one, and an entry that skips that arrives without `resolvedConfig` and fails as an unhelpful
|
|
430
|
+
"unable to initialize the worker".
|
|
431
|
+
*/
|
|
432
|
+
export { prepareAddedApplications } from './lib/config.js'
|
|
433
|
+
|
|
434
|
+
// The one transform there is: the legacy one died with its last caller, the wizard's legacy-JSON
|
|
435
|
+
// branch, and the dispatch that told the two apart went with it.
|
|
436
|
+
export async function transform (config, schema, context) {
|
|
437
|
+
return transformConfiguration(config, schema, context)
|
|
438
|
+
}
|
|
179
439
|
export * as errors from './lib/errors.js'
|
|
180
440
|
export { RuntimeGenerator as Generator, WrappedGenerator } from './lib/generator.js'
|
|
181
441
|
export { setupLoopbackMessaging } from './lib/loopback-messaging.js'
|