@platformatic/runtime 3.4.1 → 3.5.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.
Files changed (49) hide show
  1. package/README.md +1 -1
  2. package/config.d.ts +224 -77
  3. package/eslint.config.js +3 -5
  4. package/index.d.ts +73 -24
  5. package/index.js +173 -29
  6. package/lib/config.js +279 -197
  7. package/lib/errors.js +126 -34
  8. package/lib/generator.js +640 -0
  9. package/lib/logger.js +43 -41
  10. package/lib/management-api.js +109 -118
  11. package/lib/prom-server.js +202 -16
  12. package/lib/runtime.js +1963 -585
  13. package/lib/scheduler.js +119 -0
  14. package/lib/schema.js +22 -234
  15. package/lib/shared-http-cache.js +43 -0
  16. package/lib/upgrade.js +6 -8
  17. package/lib/utils.js +6 -61
  18. package/lib/version.js +7 -0
  19. package/lib/versions/v1.36.0.js +2 -4
  20. package/lib/versions/v1.5.0.js +2 -4
  21. package/lib/versions/v2.0.0.js +3 -5
  22. package/lib/versions/v3.0.0.js +16 -0
  23. package/lib/worker/controller.js +302 -0
  24. package/lib/worker/http-cache.js +171 -0
  25. package/lib/worker/interceptors.js +190 -10
  26. package/lib/worker/itc.js +146 -59
  27. package/lib/worker/main.js +220 -81
  28. package/lib/worker/messaging.js +182 -0
  29. package/lib/worker/round-robin-map.js +62 -0
  30. package/lib/worker/shared-context.js +22 -0
  31. package/lib/worker/symbols.js +14 -5
  32. package/package.json +47 -38
  33. package/schema.json +1383 -55
  34. package/help/compile.txt +0 -8
  35. package/help/help.txt +0 -5
  36. package/help/start.txt +0 -21
  37. package/index.test-d.ts +0 -41
  38. package/lib/build-server.js +0 -69
  39. package/lib/compile.js +0 -98
  40. package/lib/dependencies.js +0 -59
  41. package/lib/generator/README.md +0 -32
  42. package/lib/generator/errors.js +0 -10
  43. package/lib/generator/runtime-generator.d.ts +0 -37
  44. package/lib/generator/runtime-generator.js +0 -498
  45. package/lib/start.js +0 -190
  46. package/lib/worker/app.js +0 -278
  47. package/lib/worker/default-stackable.js +0 -33
  48. package/lib/worker/metrics.js +0 -122
  49. package/runtime.mjs +0 -54
package/lib/worker/app.js DELETED
@@ -1,278 +0,0 @@
1
- 'use strict'
2
-
3
- const { existsSync } = require('node:fs')
4
- const { EventEmitter } = require('node:events')
5
- const { resolve } = require('node:path')
6
- const { ConfigManager } = require('@platformatic/config')
7
- const { FileWatcher } = require('@platformatic/utils')
8
- const { getGlobalDispatcher, setGlobalDispatcher } = require('undici')
9
- const debounce = require('debounce')
10
-
11
- const errors = require('../errors')
12
- const defaultStackable = require('./default-stackable')
13
- const { collectMetrics } = require('./metrics')
14
- const { getServiceUrl, loadConfig, loadEmptyConfig } = require('../utils')
15
-
16
- class PlatformaticApp extends EventEmitter {
17
- #starting
18
- #started
19
- #listening
20
- #watch
21
- #fileWatcher
22
- #metricsRegistry
23
- #debouncedRestart
24
- #context
25
-
26
- constructor (appConfig, telemetryConfig, loggerConfig, serverConfig, metricsConfig, hasManagementApi, watch) {
27
- super()
28
- this.appConfig = appConfig
29
- this.#watch = watch
30
- this.#starting = false
31
- this.#started = false
32
- this.#listening = false
33
- this.stackable = null
34
- this.#fileWatcher = null
35
- this.#metricsRegistry = null
36
-
37
- this.#context = {
38
- serviceId: this.appConfig.id,
39
- directory: this.appConfig.path,
40
- isEntrypoint: this.appConfig.entrypoint,
41
- isProduction: this.appConfig.isProduction,
42
- telemetryConfig,
43
- metricsConfig,
44
- loggerConfig,
45
- serverConfig,
46
- hasManagementApi: !!hasManagementApi,
47
- localServiceEnvVars: this.appConfig.localServiceEnvVars
48
- }
49
- }
50
-
51
- getStatus () {
52
- if (this.#starting) return 'starting'
53
- if (this.#started) return 'started'
54
- return 'stopped'
55
- }
56
-
57
- async updateContext (context) {
58
- this.#context = { ...this.#context, ...context }
59
- if (this.stackable) {
60
- this.stackable.updateContext(context)
61
- }
62
- }
63
-
64
- async getBootstrapDependencies () {
65
- return this.stackable.getBootstrapDependencies()
66
- }
67
-
68
- async init () {
69
- try {
70
- const appConfig = this.appConfig
71
- let loadedConfig
72
-
73
- // Before returning the base application, check if there is any file we recognize
74
- // and the user just forgot to specify in the configuration.
75
- if (!appConfig.config) {
76
- const candidate = ConfigManager.listConfigFiles().find(f => existsSync(resolve(appConfig.path, f)))
77
-
78
- if (candidate) {
79
- appConfig.config = resolve(appConfig.path, candidate)
80
- }
81
- }
82
-
83
- if (!appConfig.config) {
84
- loadedConfig = await loadEmptyConfig(
85
- appConfig.path,
86
- {
87
- onMissingEnv: this.#fetchServiceUrl,
88
- context: appConfig
89
- },
90
- true
91
- )
92
- } else {
93
- loadedConfig = await loadConfig(
94
- {},
95
- ['-c', appConfig.config],
96
- {
97
- onMissingEnv: this.#fetchServiceUrl,
98
- context: appConfig
99
- },
100
- true
101
- )
102
- }
103
-
104
- const app = loadedConfig.app
105
-
106
- if (appConfig.isProduction && !process.env.NODE_ENV) {
107
- process.env.NODE_ENV = 'production'
108
- }
109
-
110
- const stackable = await app.buildStackable({
111
- onMissingEnv: this.#fetchServiceUrl,
112
- config: this.appConfig.config,
113
- context: this.#context
114
- })
115
- this.stackable = this.#wrapStackable(stackable)
116
-
117
- const metricsConfig = this.#context.metricsConfig
118
- if (metricsConfig !== false) {
119
- this.#metricsRegistry = await collectMetrics(this.stackable, this.appConfig.id, metricsConfig)
120
- }
121
-
122
- this.#updateDispatcher()
123
- } catch (err) {
124
- if (err.validationErrors) {
125
- console.error('Validation errors:', err.validationErrors)
126
- process.exit(1)
127
- } else {
128
- this.#logAndExit(err)
129
- }
130
- }
131
- }
132
-
133
- async start () {
134
- if (this.#starting || this.#started) {
135
- throw new errors.ApplicationAlreadyStartedError()
136
- }
137
-
138
- this.#starting = true
139
-
140
- try {
141
- await this.stackable.init()
142
- } catch (err) {
143
- this.#logAndExit(err)
144
- }
145
-
146
- if (this.#watch) {
147
- const watchConfig = await this.stackable.getWatchConfig()
148
- if (watchConfig.enabled !== false) {
149
- /* c8 ignore next 4 */
150
- this.#debouncedRestart = debounce(() => {
151
- this.stackable.log({ message: 'files changed', level: 'debug' })
152
- this.emit('changed')
153
- }, 100) // debounce restart for 100ms
154
-
155
- this.#startFileWatching(watchConfig)
156
- }
157
- }
158
-
159
- const listen = !!this.appConfig.useHttp
160
- try {
161
- await this.stackable.start({ listen })
162
- this.#listening = listen
163
- /* c8 ignore next 5 */
164
- } catch (err) {
165
- this.stackable.log({ message: err.message, level: 'debug' })
166
- this.#starting = false
167
- throw err
168
- }
169
-
170
- this.#started = true
171
- this.#starting = false
172
- this.emit('start')
173
- }
174
-
175
- async stop () {
176
- if (!this.#started || this.#starting) {
177
- throw new errors.ApplicationNotStartedError()
178
- }
179
-
180
- await this.#stopFileWatching()
181
- await this.stackable.stop()
182
-
183
- this.#started = false
184
- this.#starting = false
185
- this.#listening = false
186
- this.emit('stop')
187
- }
188
-
189
- async listen () {
190
- // This server is not an entrypoint or already listened in start. Behave as no-op.
191
- if (!this.appConfig.entrypoint || this.appConfig.useHttp || this.#listening) {
192
- return
193
- }
194
-
195
- await this.stackable.start({ listen: true })
196
- }
197
-
198
- async getMetrics ({ format }) {
199
- if (!this.#metricsRegistry) return null
200
-
201
- return format === 'json' ? this.#metricsRegistry.getMetricsAsJSON() : this.#metricsRegistry.metrics()
202
- }
203
-
204
- #fetchServiceUrl (key, { parent, context: service }) {
205
- if (service.localServiceEnvVars.has(key)) {
206
- return service.localServiceEnvVars.get(key)
207
- } else if (!key.endsWith('_URL') || !parent.serviceId) {
208
- return null
209
- }
210
-
211
- return getServiceUrl(parent.serviceId)
212
- }
213
-
214
- #startFileWatching (watch) {
215
- if (this.#fileWatcher) {
216
- return
217
- }
218
-
219
- const fileWatcher = new FileWatcher({
220
- path: watch.path,
221
- /* c8 ignore next 2 */
222
- allowToWatch: watch?.allow,
223
- watchIgnore: watch?.ignore || []
224
- })
225
-
226
- fileWatcher.on('update', this.#debouncedRestart)
227
-
228
- fileWatcher.startWatching()
229
- this.stackable.log({ message: 'start watching files', level: 'debug' })
230
- this.#fileWatcher = fileWatcher
231
- }
232
-
233
- async #stopFileWatching () {
234
- const watcher = this.#fileWatcher
235
-
236
- if (watcher) {
237
- this.stackable.log({ message: 'stop watching files', level: 'debug' })
238
- await watcher.stopWatching()
239
- this.#fileWatcher = null
240
- }
241
- }
242
-
243
- #logAndExit (err) {
244
- console.error(err)
245
- process.exit(1)
246
- }
247
-
248
- #wrapStackable (stackable) {
249
- const newStackable = {}
250
- for (const method of Object.keys(defaultStackable)) {
251
- newStackable[method] = stackable[method] ? stackable[method].bind(stackable) : defaultStackable[method]
252
- }
253
- return newStackable
254
- }
255
-
256
- #updateDispatcher () {
257
- const telemetryConfig = this.#context.telemetryConfig
258
- const telemetryId = telemetryConfig?.serviceName
259
-
260
- const interceptor = dispatch => {
261
- return function InterceptedDispatch (opts, handler) {
262
- if (telemetryId) {
263
- opts.headers = {
264
- ...opts.headers,
265
- 'x-plt-telemetry-id': telemetryId
266
- }
267
- }
268
- return dispatch(opts, handler)
269
- }
270
- }
271
-
272
- const dispatcher = getGlobalDispatcher().compose(interceptor)
273
-
274
- setGlobalDispatcher(dispatcher)
275
- }
276
- }
277
-
278
- module.exports = { PlatformaticApp }
@@ -1,33 +0,0 @@
1
- 'use strict'
2
-
3
- const defaultStackable = {
4
- init: () => {},
5
- start: () => {
6
- throw new Error('Stackable start not implemented')
7
- },
8
- stop: () => {},
9
- build: () => {},
10
- getUrl: () => null,
11
- updateContext: () => {},
12
- getConfig: () => null,
13
- getEnv: () => null,
14
- getInfo: () => null,
15
- getDispatchFunc: () => null,
16
- getOpenapiSchema: () => null,
17
- getGraphqlSchema: () => null,
18
- getMeta: () => ({}),
19
- collectMetrics: () => ({
20
- defaultMetrics: true,
21
- httpMetrics: true
22
- }),
23
- inject: () => {
24
- throw new Error('Stackable inject not implemented')
25
- },
26
- log: ({ message }) => {
27
- console.log(message)
28
- },
29
- getBootstrapDependencies: () => [],
30
- getWatchConfig: () => ({ enabled: false })
31
- }
32
-
33
- module.exports = defaultStackable
@@ -1,122 +0,0 @@
1
- 'use strict'
2
-
3
- const os = require('node:os')
4
- const { eventLoopUtilization } = require('node:perf_hooks').performance
5
- const { Registry, Gauge, collectDefaultMetrics } = require('prom-client')
6
- const collectHttpMetrics = require('@platformatic/http-metrics')
7
-
8
- async function collectMetrics (stackable, serviceId, opts = {}) {
9
- const registry = new Registry()
10
-
11
- const httpRequestCallbacks = []
12
- const httpResponseCallbacks = []
13
-
14
- const metricsConfig = await stackable.collectMetrics({
15
- registry,
16
- startHttpTimer: options => httpRequestCallbacks.forEach(cb => cb(options)),
17
- endHttpTimer: options => httpResponseCallbacks.forEach(cb => cb(options))
18
- })
19
-
20
- const labels = opts.labels ?? {}
21
- registry.setDefaultLabels({ ...labels, serviceId })
22
-
23
- if (metricsConfig.defaultMetrics) {
24
- collectDefaultMetrics({ register: registry })
25
- collectEluMetric(registry)
26
- }
27
-
28
- if (metricsConfig.httpMetrics) {
29
- {
30
- const { startTimer, endTimer } = collectHttpMetrics(registry, {
31
- customLabels: ['telemetry_id'],
32
- getCustomLabels: (req) => {
33
- const telemetryId = req.headers?.['x-plt-telemetry-id'] ?? 'unknown'
34
- return { telemetry_id: telemetryId }
35
- }
36
- })
37
- httpRequestCallbacks.push(startTimer)
38
- httpResponseCallbacks.push(endTimer)
39
- }
40
-
41
- {
42
- // TODO: check if it's a nodejs environment
43
- // Needed for the Meraki metrics
44
- const { startTimer, endTimer } = collectHttpMetrics(registry, {
45
- customLabels: ['telemetry_id'],
46
- getCustomLabels: (req) => {
47
- const telemetryId = req.headers?.['x-plt-telemetry-id'] ?? 'unknown'
48
- return { telemetry_id: telemetryId }
49
- },
50
- histogram: {
51
- name: 'http_request_all_duration_seconds',
52
- help: 'request duration in seconds summary for all requests',
53
- collect: function () {
54
- process.nextTick(() => this.reset())
55
- },
56
- },
57
- summary: {
58
- name: 'http_request_all_summary_seconds',
59
- help: 'request duration in seconds histogram for all requests',
60
- collect: function () {
61
- process.nextTick(() => this.reset())
62
- },
63
- },
64
- })
65
- httpRequestCallbacks.push(startTimer)
66
- httpResponseCallbacks.push(endTimer)
67
- }
68
- }
69
-
70
- return registry
71
- }
72
-
73
- function collectEluMetric (register) {
74
- let startELU = eventLoopUtilization()
75
- const eluMetric = new Gauge({
76
- name: 'nodejs_eventloop_utilization',
77
- help: 'The event loop utilization as a fraction of the loop time. 1 is fully utilized, 0 is fully idle.',
78
- collect: () => {
79
- const endELU = eventLoopUtilization()
80
- const result = eventLoopUtilization(endELU, startELU).utilization
81
- eluMetric.set(result)
82
- startELU = endELU
83
- },
84
- registers: [register],
85
- })
86
- register.registerMetric(eluMetric)
87
-
88
- let previousIdleTime = 0
89
- let previousTotalTime = 0
90
- const cpuMetric = new Gauge({
91
- name: 'process_cpu_percent_usage',
92
- help: 'The process CPU percent usage.',
93
- collect: () => {
94
- const cpus = os.cpus()
95
- let idleTime = 0
96
- let totalTime = 0
97
-
98
- cpus.forEach(cpu => {
99
- for (const type in cpu.times) {
100
- totalTime += cpu.times[type]
101
- if (type === 'idle') {
102
- idleTime += cpu.times[type]
103
- }
104
- }
105
- })
106
-
107
- const idleDiff = idleTime - previousIdleTime
108
- const totalDiff = totalTime - previousTotalTime
109
-
110
- const usagePercent = 100 - ((100 * idleDiff) / totalDiff)
111
- const roundedUsage = Math.round(usagePercent * 100) / 100
112
- cpuMetric.set(roundedUsage)
113
-
114
- previousIdleTime = idleTime
115
- previousTotalTime = totalTime
116
- },
117
- registers: [register],
118
- })
119
- register.registerMetric(cpuMetric)
120
- }
121
-
122
- module.exports = { collectMetrics }
package/runtime.mjs DELETED
@@ -1,54 +0,0 @@
1
- #! /usr/bin/env node
2
-
3
- import { readFile } from 'node:fs/promises'
4
- import commist from 'commist'
5
- import { join } from 'desm'
6
- import isMain from 'es-main'
7
- import helpMe from 'help-me'
8
- import parseArgs from 'minimist'
9
- import { startCommand } from './index.js'
10
- import { compile as compileCmd } from './lib/compile.js'
11
-
12
- export const compile = compileCmd
13
-
14
- const help = helpMe({
15
- dir: join(import.meta.url, 'help'),
16
- // the default
17
- ext: '.txt',
18
- })
19
-
20
- const program = commist({ maxDistance: 2 })
21
-
22
- program.register('help', help.toStdout)
23
- program.register('help start', help.toStdout.bind(null, ['start']))
24
- program.register('help compile', help.toStdout.bind(null, ['compile']))
25
- program.register('start', startCommand)
26
- program.register('compile', compile)
27
-
28
- export async function run (argv) {
29
- const args = parseArgs(argv, {
30
- alias: {
31
- v: 'version',
32
- },
33
- })
34
-
35
- if (args.version) {
36
- console.log('v' + JSON.parse(await readFile(join(import.meta.url, 'package.json'), 'utf-8')).version)
37
- process.exit(0)
38
- }
39
-
40
- /* c8 ignore next 4 */
41
- return {
42
- output: await program.parseAsync(argv),
43
- help,
44
- }
45
- }
46
-
47
- if (isMain(import.meta)) {
48
- try {
49
- await run(process.argv.slice(2))
50
- } catch (err) {
51
- console.error(err)
52
- process.exit(1)
53
- }
54
- }