@platformatic/runtime 3.68.0 → 4.0.0-new-config.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/config.d.ts CHANGED
@@ -29,7 +29,6 @@ export type PlatformaticRuntimeConfig = {
29
29
  build?: boolean;
30
30
  }
31
31
  )[];
32
- entrypoint?: string;
33
32
  basePath?: string;
34
33
  autoload?: {
35
34
  path: string;
@@ -43,9 +42,11 @@ export type PlatformaticRuntimeConfig = {
43
42
  | {
44
43
  [k: string]: boolean;
45
44
  };
46
- config?: string;
47
- useHttp?: boolean;
48
- websocket?: boolean;
45
+ config?:
46
+ | string
47
+ | {
48
+ [k: string]: unknown;
49
+ };
49
50
  reuseTcpPorts?: boolean;
50
51
  workers?:
51
52
  | number
@@ -219,46 +220,6 @@ export type PlatformaticRuntimeConfig = {
219
220
  };
220
221
  [k: string]: unknown;
221
222
  };
222
- server?: {
223
- hostname?: string;
224
- port?: number | string;
225
- /**
226
- * Configures how entrypoint server worker ports are assigned. When set to shared, all workers listen on the same port. When set to perWorkerIncrement, each worker will use its own port, starting from port (worker 0).
227
- */
228
- portAssignment?: "shared" | "perWorkerIncrement";
229
- /**
230
- * The maximum length of the queue of pending connections
231
- */
232
- backlog?: number;
233
- http2?: boolean;
234
- https?: {
235
- allowHTTP1?: boolean;
236
- key:
237
- | string
238
- | {
239
- path?: string;
240
- }
241
- | (
242
- | string
243
- | {
244
- path?: string;
245
- }
246
- )[];
247
- cert:
248
- | string
249
- | {
250
- path?: string;
251
- }
252
- | (
253
- | string
254
- | {
255
- path?: string;
256
- }
257
- )[];
258
- requestCert?: boolean;
259
- rejectUnauthorized?: boolean;
260
- };
261
- };
262
223
  reuseTcpPorts?: boolean;
263
224
  startTimeout?: number;
264
225
  restartOnError?: boolean | number;
package/index.d.ts CHANGED
@@ -37,6 +37,8 @@ export interface LoopbackMessagingOptions {
37
37
  }
38
38
 
39
39
  export namespace errors {
40
+ export const AddressInUseError: (port: number, firstApplication: string, secondApplication: string) => FastifyError
41
+ export const WorkerAddressInUseError: (port: number, application: string) => FastifyError
40
42
  export const RuntimeExitedError: () => FastifyError
41
43
  export const UnknownRuntimeAPICommandError: (command: string) => FastifyError
42
44
  export const ApplicationNotFoundError: (id: string) => FastifyError
@@ -46,8 +48,6 @@ export namespace errors {
46
48
  export const RuntimeNotStartedError: () => FastifyError
47
49
  export const ConfigPathMustBeStringError: () => FastifyError
48
50
  export const NoConfigFileFoundError: (id: string) => FastifyError
49
- export const InvalidEntrypointError: (entrypoint: string) => FastifyError
50
- export const MissingEntrypointError: () => FastifyError
51
51
  export const MissingDependencyError: (dependency: string) => FastifyError
52
52
  export const InspectAndInspectBrkError: () => FastifyError
53
53
  export const InspectorPortError: () => FastifyError
@@ -89,6 +89,7 @@ export namespace symbols {
89
89
  export const kLastWorkerScalerELU: unique symbol
90
90
  export const kWorkerStatus: unique symbol
91
91
  export const kWorkerHealthSignals: unique symbol
92
+ export const kWorkerUrl: unique symbol
92
93
  export const kStderrMarker: string
93
94
  export const kWorkersBroadcast: unique symbol
94
95
  }
@@ -119,14 +120,13 @@ export interface ApplicationDetails {
119
120
  dependencies?: string[]
120
121
  version?: string
121
122
  localUrl?: string
122
- entrypoint?: boolean
123
123
  sourceMaps?: boolean
124
124
  workers?: number
125
125
  url?: string | null
126
+ urls?: string[]
126
127
  }
127
128
 
128
129
  export interface ApplicationsTopology {
129
- entrypoint: string
130
130
  production: boolean
131
131
  applications: ApplicationDetails[]
132
132
  }
@@ -196,8 +196,8 @@ export interface RuntimeMetadata {
196
196
  projectDir: string
197
197
  packageName: string | null
198
198
  packageVersion: string | null
199
- url: string | null
200
199
  platformaticVersion: string
200
+ urls: Record<string, string>
201
201
  }
202
202
 
203
203
  export interface WorkerLifecycleEvent {
@@ -234,9 +234,7 @@ export interface HealthMetricsEvent extends WorkerLifecycleEvent {
234
234
  healthSignals: RuntimeHealthSignal[]
235
235
  }
236
236
 
237
- export declare class ManagementClient {
238
- constructor (allowedOperations?: string[])
239
-
237
+ export interface ManagementClient {
240
238
  getRuntimeStatus (): Promise<string>
241
239
  getRuntimeMetadata (): Promise<RuntimeMetadata>
242
240
  getRuntimeConfig (): Promise<Record<string, unknown>>
@@ -250,7 +248,6 @@ export declare class ManagementClient {
250
248
  getApplicationOpenapiSchema (id: string): Promise<unknown>
251
249
  getApplicationGraphqlSchema (id: string): Promise<unknown>
252
250
  getMetrics (format?: string): Promise<{ metrics: unknown }>
253
- getScheduler (): Promise<SchedulerJob[]>
254
251
  getSchedulerJobs (): Promise<SchedulerJob[]>
255
252
  pauseSchedulerJob (name: string): Promise<SchedulerJob>
256
253
  resumeSchedulerJob (name: string): Promise<SchedulerJob>
@@ -258,7 +255,7 @@ export declare class ManagementClient {
258
255
  startApplication (id: string): Promise<void>
259
256
  stopApplication (id: string): Promise<void>
260
257
  restartApplication (id: string): Promise<void>
261
- restart (applications?: string[]): Promise<string>
258
+ restart (applications?: string[]): Promise<void>
262
259
  addApplications (applications: unknown[], start?: boolean): Promise<ApplicationDetails[]>
263
260
  removeApplications (ids: string[]): Promise<ApplicationDetails[]>
264
261
  inject (id: string, injectParams: InjectParams): Promise<InjectResponse>
@@ -370,12 +367,11 @@ export type RuntimeExtension = (
370
367
 
371
368
  export declare class Runtime extends EventEmitter {
372
369
  init (): Promise<void>
373
- start (silent?: boolean): Promise<string | undefined>
370
+ start (silent?: boolean): Promise<Record<string, string>>
374
371
  stop (silent?: boolean): Promise<void>
375
372
  close (silent?: boolean): Promise<void>
376
- restart (applications?: string[]): Promise<string | undefined>
373
+ restart (applications?: string[]): Promise<void>
377
374
  inject (id: string, injectParams: InjectParams): Promise<InjectResponse>
378
- getUrl (): string | undefined
379
375
  getRuntimeStatus (): string
380
376
  getRuntimeMetadata (): Promise<RuntimeMetadata>
381
377
  getRuntimeEnv (): Record<string, string>
@@ -386,6 +382,7 @@ export declare class Runtime extends EventEmitter {
386
382
  getRuntimeConfig (includeMeta: true): RuntimeConfiguration
387
383
  getRuntimeConfig (includeMeta?: boolean): Record<string, unknown>
388
384
  getApplicationsIds (): string[]
385
+ getUrls (applicationId?: string): Record<string, string>
389
386
  /**
390
387
  * Returns topology for every configured application.
391
388
  * When `allowUnloaded` is `true`, applications without running workers are
@@ -463,7 +460,6 @@ export declare class Runtime extends EventEmitter {
463
460
  off (event: 'application:worker:health:metrics', listener: (event: HealthMetricsEvent) => void): this
464
461
  off (event: string | symbol, listener: (...args: any[]) => void): this
465
462
 
466
- getScheduler (): SchedulerJob[]
467
463
  getSchedulerJobs (): SchedulerJob[]
468
464
  pauseSchedulerJob (name: string): Promise<SchedulerJob>
469
465
  resumeSchedulerJob (name: string): Promise<SchedulerJob>
package/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { resolve, validationOptions } from '@platformatic/basic'
1
+ import { importCapabilityPackage, resolve, validationOptions } from '@platformatic/basic'
2
2
  import {
3
3
  abstractLogger,
4
4
  ensureLoggableError,
@@ -8,9 +8,17 @@ import {
8
8
  loadConfigurationModule,
9
9
  loadConfiguration as utilsLoadConfiguration
10
10
  } from '@platformatic/foundation'
11
+ import {
12
+ isConfigurationFileName,
13
+ findDecidingFile,
14
+ loadConfiguration as loadV4Configuration,
15
+ validateCapabilityConfiguration
16
+ } from '@platformatic/foundation/lib/v4/index.js'
11
17
  import closeWithGrace from 'close-with-grace'
18
+ import { stat } from 'node:fs/promises'
12
19
  import inspector from 'node:inspector'
13
- import { transform, wrapInRuntimeConfig } from './lib/config.js'
20
+ import { basename, dirname, resolve as resolvePath } from 'node:path'
21
+ import { transform as v3Transform, transformV4, wrapInRuntimeConfig } from './lib/config.js'
14
22
  import { NodeInspectorFlagsNotSupportedError } from './lib/errors.js'
15
23
  import { Runtime } from './lib/runtime.js'
16
24
  import { schema } from './lib/schema.js'
@@ -59,7 +67,86 @@ function handleSignal (runtime, config) {
59
67
  })
60
68
  }
61
69
 
70
+ /*
71
+ v4 configuration is code, evaluated by the loader in foundation: it walks to the deciding file,
72
+ resolves both views of the env ladder, evaluates the root in a worker and fans out one worker per
73
+ per-app file. Everything the runtime transform used to discover — the application list, each
74
+ application's capability, its schema and its worker environment — arrives already resolved.
75
+
76
+ Routing by filename keeps the v3 path intact while the in-tree JSON fixtures are converted; the
77
+ v3 half is what a later commit deletes, not something this one has to keep working around.
78
+ */
79
+ /*
80
+ A configuration named outright does not have to carry one of the four discoverable names -- that
81
+ is what discovery is for, and --config exists precisely to point at something discovery would not
82
+ find. What decides the dialect is the extension: v4 configuration is code, and v3 configuration is
83
+ a document.
84
+ */
85
+ function isV4ConfigurationPath (path) {
86
+ return isConfigurationFileName(basename(path)) || /\.(js|mjs|ts|mts)$/.test(path)
87
+ }
88
+
89
+ async function findV4ConfigurationFile (configOrRoot, sourceOrConfig) {
90
+ // A programmatic object source is not this path: the v4 object entry point is
91
+ // loadObjectConfiguration, which skips the root eval worker entirely.
92
+ if (sourceOrConfig && typeof sourceOrConfig !== 'string') {
93
+ return null
94
+ }
95
+
96
+ if (typeof sourceOrConfig === 'string') {
97
+ const named = resolvePath(configOrRoot, sourceOrConfig)
98
+
99
+ return isV4ConfigurationPath(named) ? named : null
100
+ }
101
+
102
+ if (typeof configOrRoot !== 'string') {
103
+ return null
104
+ }
105
+
106
+ if (isV4ConfigurationPath(configOrRoot)) {
107
+ return configOrRoot
108
+ }
109
+
110
+ /*
111
+ Only an actual directory is a place to search from. findDecidingFile walks toward the env root,
112
+ so handing it a path that does not exist would answer with whatever project sits above it: a
113
+ typo'd --config would boot the parent rather than failing. v3 looks in the named directory and
114
+ nowhere else, and a wrong path has to stay an error.
115
+ */
116
+ const stats = await stat(configOrRoot).catch(() => null)
117
+
118
+ if (!stats?.isDirectory()) {
119
+ return null
120
+ }
121
+
122
+ try {
123
+ const deciding = await findDecidingFile(configOrRoot, { throwOnMissing: false })
124
+
125
+ return deciding?.path ?? null
126
+ } catch (error) {
127
+ /*
128
+ The walk refuses a v3 configuration wherever it consults, which is right once v3 is gone and
129
+ wrong while it is still supported: here a v3 file means "this project is not v4", and the v3
130
+ loader below is the one that should answer. Deleting this catch is part of deleting the v3
131
+ path, and at that point the refusal becomes the correct answer again.
132
+ */
133
+ if (error.code === 'PLT_LEGACY_CONFIGURATION_FILE') {
134
+ return null
135
+ }
136
+
137
+ throw error
138
+ }
139
+ }
140
+
62
141
  export async function loadConfiguration (configOrRoot, sourceOrConfig, context) {
142
+ // Checked before the v3 resolver, which throws when it finds no v3 file — a v4-only project has
143
+ // none by construction.
144
+ const v4ConfigurationFile = await findV4ConfigurationFile(configOrRoot, sourceOrConfig)
145
+
146
+ if (v4ConfigurationFile) {
147
+ return loadV4RuntimeConfiguration(v4ConfigurationFile, context)
148
+ }
149
+
63
150
  const { root, source } = await resolve(configOrRoot, sourceOrConfig, 'runtime')
64
151
 
65
152
  // First of all, load the configuration without any validation
@@ -81,7 +168,7 @@ export async function loadConfiguration (configOrRoot, sourceOrConfig, context)
81
168
 
82
169
  return utilsLoadConfiguration(source, context?.schema ?? schema, {
83
170
  validationOptions,
84
- transform,
171
+ transform: v3Transform,
85
172
  upgrade,
86
173
  replaceEnv: true,
87
174
  root,
@@ -89,6 +176,86 @@ export async function loadConfiguration (configOrRoot, sourceOrConfig, context)
89
176
  })
90
177
  }
91
178
 
179
+ async function loadV4RuntimeConfiguration (configurationFile, context) {
180
+ const production = context?.isProduction ?? context?.production ?? false
181
+
182
+ /*
183
+ The environment the loader treats as real. An embedder can add to it, and can ask for a
184
+ hermetic runtime that sees none of this process's -- v3 honours both through loadEnv, and a v4
185
+ runtime that ignored them would quietly hand every worker the parent's environment.
186
+ */
187
+ const realEnv = context?.ignoreProcessEnv ? { ...context?.env } : { ...process.env, ...context?.env }
188
+
189
+ const loaded = await loadV4Configuration({
190
+ cwd: dirname(configurationFile),
191
+ configPath: configurationFile,
192
+ realEnv,
193
+ command: context?.command ?? (production ? 'start' : 'dev'),
194
+ mode: context?.mode,
195
+ production,
196
+ customEnvFile: context?.envFile,
197
+ /*
198
+ Validation imports each capability's schema, so it needs the capability installed — not
199
+ merely declared, which is all the detector needs. That moves the moment a capability must be
200
+ resolvable earlier than v3 ever needed it: v3 loaded fine and failed later, in the worker.
201
+
202
+ A caller that is loading only to discover the topology — a tool about to install those
203
+ dependencies, for instance — can say so rather than being made to install first.
204
+ */
205
+ validateCapabilities: context?.validateCapabilities ?? true,
206
+ // The runtime is where the bundled capability copies live, so it is the fallback scope for
207
+ // both the schema import and the version stamp — the application's own dependencies still
208
+ // come first.
209
+ runtimeScope: import.meta.filename,
210
+ onWarning: warning => abstractLogger.warn(warning.message),
211
+ onInfo: info => abstractLogger.info(info.message)
212
+ })
213
+
214
+ const config = loaded.config
215
+
216
+ /*
217
+ The documented pipeline is validation with useDefaults, then kMetadata, then transform. The eval
218
+ worker's own check is a shape check that injects nothing — deliberately, so the resolve
219
+ projection carries authored values — so this is where the runtime schema's defaults arrive, and
220
+ without it the runtime reaches for settings like gracefulShutdown that nothing supplied.
221
+ */
222
+ validateCapabilityConfiguration(config, context?.schema ?? schema, {
223
+ id: 'runtime',
224
+ module: '@platformatic/runtime',
225
+ root: loaded.root
226
+ })
227
+
228
+ // kMetadata is symbol-keyed and cannot cross a worker boundary, so each worker rebuilds it; this
229
+ // is the main process's copy. env is the runtime's own view — every application worker gets the
230
+ // per-application environment the loader resolved, not this one.
231
+ config[kMetadata] = {
232
+ root: loaded.root,
233
+ path: loaded.configPath,
234
+ env: process.env,
235
+ module: '@platformatic/runtime',
236
+ /*
237
+ The evaluation context, kept beside the envelope rather than inside it: applications added
238
+ after boot are evaluated with the same command, mode and production flags the boot pass used,
239
+ and its presence is also what tells the add path this runtime is v4 at all. A v3 runtime has
240
+ no such context and keeps resolving configuration files worker-side.
241
+ */
242
+ v4: {
243
+ command: context?.command ?? (production ? 'start' : 'dev'),
244
+ mode: loaded.mode,
245
+ production: loaded.production
246
+ }
247
+ }
248
+
249
+ /*
250
+ A caller-supplied transform replaces the built-in one, exactly as it does on the v3 path where
251
+ it arrives as part of ...context. Ignoring it here meant a test helper or an embedder that
252
+ customized the configuration was silently not consulted.
253
+ */
254
+ const apply = context?.transform ?? transformV4
255
+
256
+ return apply(config, null, context)
257
+ }
258
+
92
259
  export async function loadApplicationsCommands (executableName = '', configurationFile = null) {
93
260
  const applications = {}
94
261
  const commands = {}
@@ -123,8 +290,16 @@ export async function loadApplicationsCommands (executableName = '', configurati
123
290
 
124
291
  for (const application of config.applications) {
125
292
  try {
126
- const applicationConfig = await utilsLoadConfiguration(application.config)
127
- const pkg = await loadConfigurationModule(application.path, applicationConfig)
293
+ /*
294
+ A v4 application arrives with its configuration already evaluated and its capability
295
+ already named, so there is nothing to read and nothing to infer. Reading application.config
296
+ as a path threw into the catch below, which skipped the application silently -- the command
297
+ it contributes then simply did not exist, and the CLI said the command was unknown.
298
+ */
299
+ const applicationConfig = application.resolvedConfig ?? (await utilsLoadConfiguration(application.config))
300
+ const pkg = application.module
301
+ ? await importCapabilityPackage(application.path, application.module)
302
+ : await loadConfigurationModule(application.path, applicationConfig)
128
303
 
129
304
  if (pkg.createCommands) {
130
305
  const definition = await pkg.createCommands(application.id)
@@ -175,7 +350,17 @@ export async function create (configOrRoot, sourceOrConfig, context) {
175
350
  return runtime
176
351
  }
177
352
 
178
- export { prepareApplication, transform, wrapInRuntimeConfig } from './lib/config.js'
353
+ export { prepareApplication, wrapInRuntimeConfig } from './lib/config.js'
354
+
355
+ /*
356
+ The exported transform dispatches on the dialect. Callers wrap it -- they call it and then adjust
357
+ the result -- and a wrapper that reached the v3 transform with a v4 configuration would run the
358
+ wrong pipeline over it: autoload expansion and enabled filtering have already happened in the
359
+ eval worker. v4 configurations are recognizable by the evaluation context on kMetadata.
360
+ */
361
+ export async function transform (config, schema, context) {
362
+ return config?.[kMetadata]?.v4 ? transformV4(config, schema, context) : v3Transform(config, schema, context)
363
+ }
179
364
  export * as errors from './lib/errors.js'
180
365
  export { RuntimeGenerator as Generator, WrappedGenerator } from './lib/generator.js'
181
366
  export { setupLoopbackMessaging } from './lib/loopback-messaging.js'