@platformatic/runtime 3.63.0 → 3.65.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/index.d.ts CHANGED
@@ -9,7 +9,7 @@ import { EventEmitter } from 'node:events'
9
9
  import { Logger } from 'pino'
10
10
  import { PlatformaticRuntimeConfig } from './config.js'
11
11
 
12
- export type RuntimeConfiguration = Promise<Configuration<PlatformaticRuntimeConfig>>
12
+ export type RuntimeConfiguration = Configuration<PlatformaticRuntimeConfig>
13
13
 
14
14
  export type ApplicationCommandContext = {
15
15
  colorette: typeof colorette
@@ -72,6 +72,8 @@ export namespace errors {
72
72
  ) => FastifyError
73
73
  export const ExtensionHealthRoutesUnavailableError: () => FastifyError
74
74
  export const LastProfileTimeoutError: (id: string) => FastifyError
75
+ export const SchedulerJobNotFoundError: (name: string) => FastifyError
76
+ export const DuplicateSchedulerJobError: (name: string) => FastifyError
75
77
  }
76
78
 
77
79
  export namespace symbols {
@@ -145,6 +147,45 @@ export interface RuntimeSharedContextUpdateOptions extends SharedContextUpdateOp
145
147
  context?: object
146
148
  }
147
149
 
150
+ export interface SchedulerJobBase {
151
+ name: string
152
+ cron: string
153
+ paused: boolean
154
+ maxRetries: number
155
+ lastExecutedAt?: string | null
156
+ lastStatus?: 'success' | 'failed' | null
157
+ nextRunAt?: string | null
158
+ }
159
+
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
+ export interface ApplicationSchedulerJob extends SchedulerJobBase {
169
+ source: 'application'
170
+ applicationId: string
171
+ scheduleId: string
172
+ tasks: string[]
173
+ }
174
+
175
+ export type SchedulerJob = ConfiguredSchedulerJob | ApplicationSchedulerJob
176
+
177
+ export interface SchedulerRunResult {
178
+ name: string
179
+ success: boolean
180
+ executedAt: string
181
+ }
182
+
183
+ export interface ApplicationSchedule {
184
+ id: string
185
+ cron: string
186
+ tasks: string[]
187
+ }
188
+
148
189
  export interface RuntimeMetadata {
149
190
  pid: number
150
191
  cwd: string
@@ -159,6 +200,40 @@ export interface RuntimeMetadata {
159
200
  platformaticVersion: string
160
201
  }
161
202
 
203
+ export interface WorkerLifecycleEvent {
204
+ application: string
205
+ worker: number
206
+ workersCount?: number
207
+ }
208
+
209
+ export interface ProfileCapturedEvent extends WorkerLifecycleEvent {
210
+ id: string
211
+ type: 'cpu' | 'heap'
212
+ timestamp: number
213
+ sampleCount: number | null
214
+ }
215
+
216
+ export interface RuntimeHealthSignal {
217
+ type: string
218
+ max?: number
219
+ mean?: number
220
+ p99?: number
221
+ timestamp?: number
222
+ [key: string]: unknown
223
+ }
224
+
225
+ export interface WorkerCurrentHealth {
226
+ elu: number
227
+ heapUsed?: number | null
228
+ heapTotal?: number | null
229
+ }
230
+
231
+ export interface HealthMetricsEvent extends WorkerLifecycleEvent {
232
+ id: string
233
+ currentHealth: WorkerCurrentHealth | null
234
+ healthSignals: RuntimeHealthSignal[]
235
+ }
236
+
162
237
  export declare class ManagementClient {
163
238
  constructor (allowedOperations?: string[])
164
239
 
@@ -175,6 +250,10 @@ export declare class ManagementClient {
175
250
  getApplicationOpenapiSchema (id: string): Promise<unknown>
176
251
  getApplicationGraphqlSchema (id: string): Promise<unknown>
177
252
  getMetrics (format?: string): Promise<{ metrics: unknown }>
253
+ getSchedulerJobs (): Promise<SchedulerJob[]>
254
+ pauseSchedulerJob (name: string): Promise<SchedulerJob>
255
+ resumeSchedulerJob (name: string): Promise<SchedulerJob>
256
+ runSchedulerJob (name: string): Promise<SchedulerRunResult>
178
257
  startApplication (id: string): Promise<void>
179
258
  stopApplication (id: string): Promise<void>
180
259
  restartApplication (id: string): Promise<void>
@@ -286,6 +365,11 @@ export declare class Runtime extends EventEmitter {
286
365
  getRuntimeStatus (): string
287
366
  getRuntimeMetadata (): Promise<RuntimeMetadata>
288
367
  getRuntimeEnv (): Record<string, string>
368
+ // `includeMeta: true` returns the full merged Configuration object (which
369
+ // carries the kMetadata symbol key: `{ root, path, env, module }`), not a
370
+ // plain Record; the argument-invariant overload below covers every other
371
+ // call.
372
+ getRuntimeConfig (includeMeta: true): RuntimeConfiguration
289
373
  getRuntimeConfig (includeMeta?: boolean): Record<string, unknown>
290
374
  getApplicationsIds (): string[]
291
375
  /**
@@ -339,9 +423,38 @@ export declare class Runtime extends EventEmitter {
339
423
  restartApplication (id: string): Promise<void>
340
424
  addApplications (applications: unknown[], start?: boolean): Promise<ApplicationDetails[]>
341
425
  removeApplications (applications: string[], silent?: boolean): Promise<ApplicationDetails[]>
426
+ startApplicationProfiling (id: string, options: Record<string, unknown> & { allWorkers: true }, ensureStarted?: boolean): Promise<{ workers: number[] }>
342
427
  startApplicationProfiling (id: string, options?: Record<string, unknown>, ensureStarted?: boolean): Promise<void>
428
+ stopApplicationProfiling (id: string, options: Record<string, unknown> & { allWorkers: true }, ensureStarted?: boolean): Promise<Array<{ workerIndex: number, profile: Buffer }>>
429
+ stopApplicationProfiling (id: string, options: Record<string, unknown> & { includeSampleCount: true }, ensureStarted?: boolean): Promise<{ profile: Buffer, sampleCount: number }>
343
430
  stopApplicationProfiling (id: string, options?: Record<string, unknown>, ensureStarted?: boolean): Promise<Buffer>
344
- getApplicationLastProfile (id: string, options?: Record<string, unknown>, ensureStarted?: boolean): Promise<{ profile: Buffer, timestamp: number | null, preserved: boolean }>
431
+ getApplicationLastProfile (id: string, options?: Record<string, unknown>, ensureStarted?: boolean): Promise<{ profile: Buffer, timestamp: number | null, sampleCount: number | null, preserved: boolean }>
432
+ // Patches one application's resolved configuration through the same
433
+ // per-application JSON-Patch mechanism the runtime already applies during
434
+ // create()/start().
435
+ setApplicationConfigPatch (applicationId: string, patch: Array<Record<string, unknown>>): void
436
+ removeApplicationConfigPatch (applicationId: string): void
437
+
438
+ // Typed worker lifecycle events. Catch-all overloads preserve EventEmitter's
439
+ // open event surface so listeners for other runtime events still type-check.
440
+ on (event: 'application:worker:started', listener: (event: WorkerLifecycleEvent) => void): this
441
+ on (event: 'application:worker:exited', listener: (event: WorkerLifecycleEvent) => void): this
442
+ on (event: 'application:worker:profile:captured', listener: (event: ProfileCapturedEvent) => void): this
443
+ on (event: 'application:worker:health:metrics', listener: (event: HealthMetricsEvent) => void): this
444
+ on (event: string | symbol, listener: (...args: any[]) => void): this
445
+
446
+ off (event: 'application:worker:started', listener: (event: WorkerLifecycleEvent) => void): this
447
+ off (event: 'application:worker:exited', listener: (event: WorkerLifecycleEvent) => void): this
448
+ off (event: 'application:worker:profile:captured', listener: (event: ProfileCapturedEvent) => void): this
449
+ off (event: 'application:worker:health:metrics', listener: (event: HealthMetricsEvent) => void): this
450
+ off (event: string | symbol, listener: (...args: any[]) => void): this
451
+
452
+ getSchedulerJobs (): SchedulerJob[]
453
+ pauseSchedulerJob (name: string): Promise<SchedulerJob>
454
+ resumeSchedulerJob (name: string): Promise<SchedulerJob>
455
+ runSchedulerJob (name: string): Promise<SchedulerRunResult>
456
+ getApplicationScheduledTasks (id: string): Promise<ApplicationSchedule[]>
457
+ runApplicationScheduledTasks (id: string, scheduleId: string, scheduledTime: number): Promise<unknown>
345
458
  }
346
459
 
347
460
  export function wrapInRuntimeConfig (
@@ -357,13 +470,27 @@ export declare function loadConfiguration (
357
470
  context?: ConfigurationOptions
358
471
  ): Promise<RuntimeConfiguration>
359
472
 
473
+ // create() additionally accepts `setupSignals`, a Runtime-only option (it is
474
+ // never read outside this package) that controls whether SIGUSR2/close-with-grace
475
+ // handlers are installed.
476
+ export interface RuntimeCreateContext extends ConfigurationOptions<PlatformaticRuntimeConfig> {
477
+ setupSignals?: boolean
478
+ }
479
+
360
480
  export function create (
361
481
  root: string,
362
482
  source?: string | PlatformaticRuntimeConfig,
363
- context?: ConfigurationOptions
483
+ context?: RuntimeCreateContext
364
484
  ): Promise<Runtime>
365
485
 
366
- export declare function prepareApplication (config: RuntimeConfiguration, application: object): object
486
+ // The runtime's actual per-application preparation (worker defaults, paths,
487
+ // telemetry hooks, capability resolution) also takes the resolved worker
488
+ // count as a third argument, and is itself async.
489
+ export declare function prepareApplication (
490
+ config: RuntimeConfiguration,
491
+ application: object,
492
+ workers?: PlatformaticRuntimeConfig['workers']
493
+ ): Promise<object>
367
494
 
368
495
  export declare function transform (
369
496
  config: RuntimeConfiguration,
package/index.js CHANGED
@@ -69,6 +69,8 @@ export async function loadConfiguration (configOrRoot, sourceOrConfig, context)
69
69
  })
70
70
 
71
71
  if (config.envfile) {
72
+ // The context is optional, so it might not have been provided at all
73
+ context ??= {}
72
74
  context.envFile = config.envfile
73
75
  }
74
76
 
package/lib/errors.js CHANGED
@@ -140,6 +140,17 @@ export const MessagingError = createError(
140
140
  'Cannot send a message to application "%s": %s'
141
141
  )
142
142
 
143
+ export const SchedulerJobNotFoundError = createError(
144
+ `${ERROR_PREFIX}_SCHEDULER_JOB_NOT_FOUND`,
145
+ 'Scheduler "%s" not found',
146
+ 404
147
+ )
148
+
149
+ export const DuplicateSchedulerJobError = createError(
150
+ `${ERROR_PREFIX}_DUPLICATE_SCHEDULER_JOB`,
151
+ 'Scheduler "%s" is already registered'
152
+ )
153
+
143
154
  export const MissingPprofCapture = createError(
144
155
  `${ERROR_PREFIX}_MISSING_PPROF_CAPTURE`,
145
156
  'Please install @platformatic/wattpm-pprof-capture'
@@ -167,7 +178,7 @@ export const FailedToStopExtensionError = createError(
167
178
 
168
179
  export const InvalidExtensionError = createError(
169
180
  `${ERROR_PREFIX}_INVALID_EXTENSION`,
170
- 'The extension "%s" must export a setup function as its default export'
181
+ 'The extension "%s" must export a setup function as its default export or as a named "setup" export'
171
182
  )
172
183
 
173
184
  export const ReservedITCHandlerNameError = createError(
@@ -79,6 +79,23 @@ export async function managementApiPlugin (app, opts) {
79
79
  await runtime.restart(applications)
80
80
  })
81
81
 
82
+ app.get('/scheduler', async () => {
83
+ const jobs = await runtime.getSchedulerJobs()
84
+ return { jobs }
85
+ })
86
+
87
+ app.post('/scheduler/:name/pause', async request => {
88
+ return runtime.pauseSchedulerJob(request.params.name)
89
+ })
90
+
91
+ app.post('/scheduler/:name/resume', async request => {
92
+ return runtime.resumeSchedulerJob(request.params.name)
93
+ })
94
+
95
+ app.post('/scheduler/:name/run', async request => {
96
+ return runtime.runSchedulerJob(request.params.name)
97
+ })
98
+
82
99
  app.get('/applications', async () => {
83
100
  return runtime.getApplications()
84
101
  })
@@ -203,8 +220,8 @@ export async function managementApiPlugin (app, opts) {
203
220
  app.log.debug('start profiling', { id })
204
221
 
205
222
  const options = request.body || {}
206
- await runtime.startApplicationProfiling(id, options)
207
- reply.code(200).send({})
223
+ const result = await runtime.startApplicationProfiling(id, options)
224
+ reply.code(200).send(result ?? {})
208
225
  })
209
226
 
210
227
  app.post('/applications/:id/pprof/stop', async (request, reply) => {
@@ -213,6 +230,19 @@ export async function managementApiPlugin (app, opts) {
213
230
 
214
231
  const options = request.body || {}
215
232
  const profileData = await runtime.stopApplicationProfiling(id, options)
233
+
234
+ // With allWorkers there is one profile per worker: return them as JSON
235
+ // with base64 encoded payloads since a single binary body cannot carry
236
+ // multiple profiles.
237
+ if (options.allWorkers && Array.isArray(profileData)) {
238
+ const profiles = profileData.map(({ workerIndex, profile }) => ({
239
+ workerIndex,
240
+ profile: Buffer.from(profile).toString('base64')
241
+ }))
242
+
243
+ return { profiles }
244
+ }
245
+
216
246
  reply.type('application/octet-stream').code(200).send(profileData)
217
247
  })
218
248
 
@@ -19,6 +19,10 @@ const allOperations = [
19
19
  'getApplicationOpenapiSchema',
20
20
  'getApplicationGraphqlSchema',
21
21
  'getMetrics',
22
+ 'getSchedulerJobs',
23
+ 'pauseSchedulerJob',
24
+ 'resumeSchedulerJob',
25
+ 'runSchedulerJob',
22
26
  'startApplication',
23
27
  'stopApplication',
24
28
  'restartApplication',
@@ -96,6 +100,10 @@ export function createManagementHandlers (runtime, allowedOperations) {
96
100
  return runtime.getMetrics(format)
97
101
  })
98
102
 
103
+ register('getSchedulerJobs', async () => {
104
+ return runtime.getSchedulerJobs()
105
+ })
106
+
99
107
  // Write operations
100
108
  register('startApplication', async ({ id }) => {
101
109
  await runtime.startApplication(id)
@@ -113,6 +121,18 @@ export function createManagementHandlers (runtime, allowedOperations) {
113
121
  return runtime.restart(applications)
114
122
  })
115
123
 
124
+ register('pauseSchedulerJob', async ({ name }) => {
125
+ return runtime.pauseSchedulerJob(name)
126
+ })
127
+
128
+ register('resumeSchedulerJob', async ({ name }) => {
129
+ return runtime.resumeSchedulerJob(name)
130
+ })
131
+
132
+ register('runSchedulerJob', async ({ name }) => {
133
+ return runtime.runSchedulerJob(name)
134
+ })
135
+
116
136
  register('addApplications', async ({ applications, start }) => {
117
137
  const config = runtime.getRuntimeConfig(true)
118
138