@platformatic/runtime 3.4.1 → 3.5.1

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
@@ -0,0 +1,119 @@
1
+ import { CronJob, validateCronExpression } from 'cron'
2
+ import { setTimeout } from 'node:timers/promises'
3
+ import { request } from 'undici'
4
+
5
+ export class SchedulerService {
6
+ constructor (schedulerConfig, dispatcher, logger) {
7
+ this.logger = logger
8
+ this.jobsConfig = []
9
+ this.cronJobs = []
10
+ this.dispatcher = dispatcher
11
+ this.validateCronSchedulers(schedulerConfig)
12
+ }
13
+
14
+ validateCronSchedulers (schedulerConfig) {
15
+ for (const config of schedulerConfig) {
16
+ // Skip disabled schedulers
17
+ if (config.enabled === false) {
18
+ continue
19
+ }
20
+
21
+ // Validate cron expression
22
+ const validation = validateCronExpression(config.cron)
23
+ if (!validation.valid) {
24
+ throw new Error(`Invalid cron expression "${config.cron}" for scheduler "${config.name}"`)
25
+ }
26
+
27
+ // Set defaults for optional fields
28
+ const job = {
29
+ ...config,
30
+ headers: config.headers || {},
31
+ body: config.body || {},
32
+ maxRetries: config.maxRetries || 3
33
+ }
34
+ this.jobsConfig.push(job)
35
+ }
36
+ }
37
+
38
+ start () {
39
+ for (const job of this.jobsConfig) {
40
+ this.logger.info(`Configuring scheduler "${job.name}" with cron "${job.cron}"`)
41
+ const cronJob = CronJob.from({
42
+ cronTime: job.cron,
43
+ onTick: async () => {
44
+ this.logger.info(`Executing scheduler "${job.name}"`)
45
+ // This cannot throw, the try/catch is inside
46
+ await this.executeCallback(job)
47
+ },
48
+ start: true,
49
+ timeZone: 'UTC',
50
+ waitForCompletion: true
51
+ })
52
+
53
+ this.cronJobs.push(cronJob)
54
+ }
55
+ }
56
+
57
+ async stop () {
58
+ for (const job of this.cronJobs) {
59
+ await job.stop()
60
+ }
61
+ }
62
+
63
+ async executeCallback (scheduler) {
64
+ let attempt = 0
65
+ let success = false
66
+
67
+ while (!success && attempt < scheduler.maxRetries) {
68
+ try {
69
+ const delay = attempt > 0 ? 100 * Math.pow(2, attempt) : 0
70
+
71
+ if (delay > 0) {
72
+ this.logger.info(
73
+ `Retrying scheduler "${scheduler.name}" in ${delay}ms (attempt ${attempt + 1}/${scheduler.maxRetries})`
74
+ )
75
+ await setTimeout(delay)
76
+ }
77
+ const headers = {
78
+ 'x-retry-attempt': attempt + 1,
79
+ ...scheduler.headers
80
+ }
81
+
82
+ const bodyString = typeof scheduler.body === 'string' ? scheduler.body : JSON.stringify(scheduler.body)
83
+ const response = await request(scheduler.callbackUrl, {
84
+ method: scheduler.method,
85
+ headers,
86
+ body: bodyString,
87
+ dispatcher: this.dispatcher
88
+ })
89
+
90
+ // Consumes the body, but we are not interested in the body content,
91
+ // we don't save it anywere, so we just dump it
92
+ await response.body.dump()
93
+
94
+ if (response.statusCode >= 200 && response.statusCode < 300) {
95
+ this.logger.info(`Scheduler "${scheduler.name}" executed successfully`)
96
+ success = true
97
+ } else {
98
+ throw new Error(`HTTP error ${response.statusCode}`)
99
+ }
100
+ } catch (error) {
101
+ this.logger.error(
102
+ `Error executing scheduler "${scheduler.name}" (attempt ${attempt + 1}/${scheduler.maxRetries}):`,
103
+ error.message
104
+ )
105
+ attempt++
106
+ }
107
+ }
108
+
109
+ if (!success) {
110
+ this.logger.error(`Scheduler "${scheduler.name}" failed after ${scheduler.maxRetries} attempts`)
111
+ }
112
+ }
113
+ }
114
+
115
+ export function startScheduler (config, interceptors, logger) {
116
+ const schedulerService = new SchedulerService(config, interceptors, logger)
117
+ schedulerService.start()
118
+ return schedulerService
119
+ }
package/lib/schema.js CHANGED
@@ -1,250 +1,38 @@
1
1
  #! /usr/bin/env node
2
- 'use strict'
3
2
 
4
- const telemetry = require('@platformatic/telemetry').schema
5
- const {
6
- schemaComponents: { server, logger }
7
- } = require('@platformatic/utils')
3
+ import { schemaComponents } from '@platformatic/foundation'
4
+ import { version } from './version.js'
8
5
 
9
- const services = {
10
- type: 'array',
11
- items: {
12
- type: 'object',
13
- anyOf: [{ required: ['id', 'path'] }, { required: ['id', 'url'] }],
14
- properties: {
15
- id: {
16
- type: 'string'
17
- },
18
- path: {
19
- type: 'string',
20
- resolvePath: true
21
- },
22
- config: {
23
- type: 'string'
24
- },
25
- url: {
26
- type: 'string'
27
- },
28
- useHttp: {
29
- type: 'boolean'
30
- }
6
+ const runtimeLogger = {
7
+ ...schemaComponents.runtimeProperties.logger,
8
+ properties: {
9
+ ...schemaComponents.runtimeProperties.logger.properties,
10
+ captureStdio: {
11
+ type: 'boolean',
12
+ default: true
31
13
  }
32
14
  }
33
15
  }
34
16
 
35
- const pkg = require('../package.json')
17
+ schemaComponents.runtimeProperties.logger = runtimeLogger
36
18
 
37
19
  const platformaticRuntimeSchema = {
38
- $id: `https://schemas.platformatic.dev/@platformatic/runtime/${pkg.version}.json`,
20
+ $id: `https://schemas.platformatic.dev/@platformatic/runtime/${version}.json`,
39
21
  $schema: 'http://json-schema.org/draft-07/schema#',
22
+ title: 'Platformatic Runtime Config',
40
23
  type: 'object',
41
- properties: {
42
- $schema: {
43
- type: 'string'
44
- },
45
- preload: {
46
- type: 'string',
47
- resolvePath: true
48
- },
49
- entrypoint: {
50
- type: 'string'
51
- },
52
- autoload: {
53
- type: 'object',
54
- additionalProperties: false,
55
- required: ['path'],
56
- properties: {
57
- path: {
58
- type: 'string',
59
- resolvePath: true
60
- },
61
- exclude: {
62
- type: 'array',
63
- default: [],
64
- items: {
65
- type: 'string'
66
- }
67
- },
68
- mappings: {
69
- type: 'object',
70
- additionalProperties: {
71
- type: 'object',
72
- additionalProperties: false,
73
- required: ['id'],
74
- properties: {
75
- id: {
76
- type: 'string'
77
- },
78
- config: {
79
- type: 'string'
80
- },
81
- useHttp: {
82
- type: 'boolean'
83
- }
84
- }
85
- }
86
- }
87
- }
88
- },
89
- services,
90
- web: services,
91
- logger,
92
- server,
93
- restartOnError: {
94
- default: true,
95
- anyOf: [
96
- { type: 'boolean' },
97
- {
98
- type: 'number',
99
- minimum: 100
100
- }
101
- ]
102
- },
103
- undici: {
104
- type: 'object',
105
- properties: {
106
- agentOptions: {
107
- type: 'object',
108
- additionalProperties: true
109
- },
110
- interceptors: {
111
- anyOf: [
112
- {
113
- type: 'array',
114
- items: {
115
- $ref: '#/$defs/undiciInterceptor'
116
- }
117
- },
118
- {
119
- type: 'object',
120
- properties: {
121
- Client: {
122
- type: 'array',
123
- items: {
124
- $ref: '#/$defs/undiciInterceptor'
125
- }
126
- },
127
- Pool: {
128
- type: 'array',
129
- items: {
130
- $ref: '#/$defs/undiciInterceptor'
131
- }
132
- },
133
- Agent: {
134
- type: 'array',
135
- items: {
136
- $ref: '#/$defs/undiciInterceptor'
137
- }
138
- }
139
- }
140
- }
141
- ]
142
- }
143
- }
144
- },
145
- watch: {
146
- anyOf: [
147
- {
148
- type: 'boolean'
149
- },
150
- {
151
- type: 'string'
152
- }
153
- ]
154
- },
155
- managementApi: {
156
- anyOf: [
157
- { type: 'boolean' },
158
- { type: 'string' },
159
- {
160
- type: 'object',
161
- properties: {
162
- logs: {
163
- type: 'object',
164
- properties: {
165
- maxSize: {
166
- type: 'number',
167
- minimum: 5,
168
- default: 200
169
- }
170
- },
171
- additionalProperties: false
172
- }
173
- },
174
- additionalProperties: false
175
- }
176
- ],
177
- default: true
178
- },
179
- metrics: {
180
- anyOf: [
181
- { type: 'boolean' },
182
- {
183
- type: 'object',
184
- properties: {
185
- port: {
186
- anyOf: [{ type: 'integer' }, { type: 'string' }]
187
- },
188
- hostname: { type: 'string' },
189
- endpoint: { type: 'string' },
190
- auth: {
191
- type: 'object',
192
- properties: {
193
- username: { type: 'string' },
194
- password: { type: 'string' }
195
- },
196
- additionalProperties: false,
197
- required: ['username', 'password']
198
- },
199
- labels: {
200
- type: 'object',
201
- additionalProperties: { type: 'string' }
202
- }
203
- },
204
- additionalProperties: false
205
- }
206
- ]
207
- },
208
- telemetry,
209
- inspectorOptions: {
210
- type: 'object',
211
- properties: {
212
- host: {
213
- type: 'string'
214
- },
215
- port: {
216
- type: 'number'
217
- },
218
- breakFirstLine: {
219
- type: 'boolean'
220
- },
221
- watchDisabled: {
222
- type: 'boolean'
223
- }
224
- }
225
- }
226
- },
227
- anyOf: [{ required: ['autoload'] }, { required: ['services'] }, { required: ['web'] }],
228
- additionalProperties: false,
229
- $defs: {
230
- undiciInterceptor: {
231
- type: 'object',
232
- properties: {
233
- module: {
234
- type: 'string'
235
- },
236
- options: {
237
- type: 'object',
238
- additionalProperties: true
239
- }
240
- },
241
- required: ['module', 'options']
242
- }
243
- }
24
+ properties: schemaComponents.runtimeProperties,
25
+ anyOf: [
26
+ { required: ['autoload'] },
27
+ { required: ['applications'] },
28
+ { required: ['services'] },
29
+ { required: ['web'] }
30
+ ],
31
+ additionalProperties: false
244
32
  }
245
33
 
246
- module.exports.schema = platformaticRuntimeSchema
34
+ export const schema = platformaticRuntimeSchema
247
35
 
248
- if (require.main === module) {
36
+ if (import.meta.main) {
249
37
  console.log(JSON.stringify(platformaticRuntimeSchema, null, 2))
250
38
  }
@@ -0,0 +1,43 @@
1
+ import { loadModule } from '@platformatic/foundation'
2
+ import MemoryCacheStore from '@platformatic/undici-cache-memory'
3
+ import { createRequire } from 'node:module'
4
+ import { join } from 'node:path'
5
+
6
+ export async function createSharedStore (projectDir, httpCacheConfig = {}) {
7
+ const runtimeRequire = createRequire(join(projectDir, 'file'))
8
+
9
+ const { store, ...storeConfig } = httpCacheConfig
10
+ const CacheStore = store ? await loadModule(runtimeRequire, store) : MemoryCacheStore
11
+
12
+ class SharedCacheStore extends CacheStore {
13
+ async getValue (req) {
14
+ const cachedValue = await this.get(req)
15
+ if (!cachedValue) return null
16
+
17
+ const { body, ...response } = cachedValue
18
+
19
+ const acc = []
20
+ for await (const chunk of body) {
21
+ acc.push(chunk)
22
+ }
23
+
24
+ let payload
25
+ if (acc.length > 0 && typeof acc[0] === 'string') {
26
+ payload = acc.join('')
27
+ } else {
28
+ payload = Buffer.concat(acc)
29
+ }
30
+
31
+ return { response, payload }
32
+ }
33
+
34
+ setValue (req, opts, data) {
35
+ const writeStream = this.createWriteStream(req, opts)
36
+ writeStream.write(data)
37
+ writeStream.end()
38
+ return null
39
+ }
40
+ }
41
+
42
+ return new SharedCacheStore(storeConfig)
43
+ }
package/lib/upgrade.js CHANGED
@@ -1,15 +1,13 @@
1
- 'use strict'
2
-
3
- const { join } = require('node:path')
4
-
5
- module.exports = async function upgrade (config, version) {
6
- const { semgrator } = await import('semgrator')
1
+ import { abstractLogger } from '@platformatic/foundation'
2
+ import { join } from 'node:path'
3
+ import { semgrator } from 'semgrator'
7
4
 
5
+ export async function upgrade (logger, config, version) {
8
6
  const iterator = semgrator({
9
7
  version,
10
- path: join(__dirname, 'versions'),
8
+ path: join(import.meta.dirname, 'versions'),
11
9
  input: config,
12
- logger: this.logger.child({ name: '@platformatic/runtime' }),
10
+ logger: logger?.child({ name: '@platformatic/runtime' }) ?? abstractLogger
13
11
  })
14
12
 
15
13
  let result
package/lib/utils.js CHANGED
@@ -1,74 +1,19 @@
1
- 'use strict'
1
+ import { createHash } from 'node:crypto'
2
+ import { tmpdir } from 'node:os'
3
+ import { join } from 'node:path'
2
4
 
3
- const { createHash } = require('node:crypto')
4
- const { tmpdir } = require('node:os')
5
- const { join } = require('node:path')
6
-
7
- const {
8
- Store,
9
- loadConfig: pltConfigLoadConfig,
10
- loadEmptyConfig: pltConfigLoadEmptyConfig
11
- } = require('@platformatic/config')
12
-
13
- const { platformaticRuntime } = require('./config')
14
-
15
- function getArrayDifference (a, b) {
5
+ export function getArrayDifference (a, b) {
16
6
  return a.filter(element => {
17
7
  return !b.includes(element)
18
8
  })
19
9
  }
20
10
 
21
- function getServiceUrl (id) {
11
+ export function getApplicationUrl (id) {
22
12
  return `http://${id}.plt.local`
23
13
  }
24
14
 
25
- function getRuntimeTmpDir (runtimeDir) {
15
+ export function getRuntimeTmpDir (runtimeDir) {
26
16
  const platformaticTmpDir = join(tmpdir(), 'platformatic', 'applications')
27
17
  const runtimeDirHash = createHash('md5').update(runtimeDir).digest('hex')
28
18
  return join(platformaticTmpDir, runtimeDirHash)
29
19
  }
30
-
31
- function getRuntimeLogsDir (runtimeDir, runtimePID) {
32
- const runtimeTmpDir = getRuntimeTmpDir(runtimeDir)
33
- return join(runtimeTmpDir, runtimePID.toString(), 'logs')
34
- }
35
-
36
- async function loadConfig (minimistConfig, args, overrides, replaceEnv = true) {
37
- const { default: platformaticBasic } = await import('@platformatic/basic')
38
- const store = new Store()
39
- store.add(platformaticRuntime)
40
-
41
- const id = platformaticRuntime.schema.$id.replace('@platformatic/runtime', 'wattpm')
42
- const schema = {
43
- ...platformaticRuntime.schema,
44
- $id: id
45
- }
46
- const configManagerConfig = {
47
- ...platformaticRuntime.configManagerConfig,
48
- schema
49
- }
50
- const wattpm = {
51
- ...platformaticRuntime,
52
- schema,
53
- configManagerConfig
54
- }
55
- store.add(wattpm)
56
- store.add(platformaticBasic)
57
-
58
- return pltConfigLoadConfig(minimistConfig, args, store, overrides, replaceEnv)
59
- }
60
-
61
- async function loadEmptyConfig (path, overrides, replaceEnv = true) {
62
- const { default: platformaticBasic } = await import('@platformatic/basic')
63
-
64
- return pltConfigLoadEmptyConfig(path, platformaticBasic, overrides, replaceEnv)
65
- }
66
-
67
- module.exports = {
68
- getArrayDifference,
69
- getRuntimeLogsDir,
70
- getRuntimeTmpDir,
71
- getServiceUrl,
72
- loadConfig,
73
- loadEmptyConfig
74
- }
package/lib/version.js ADDED
@@ -0,0 +1,7 @@
1
+ import { readFileSync } from 'node:fs'
2
+ import { resolve } from 'node:path'
3
+
4
+ const packageJson = JSON.parse(readFileSync(resolve(import.meta.dirname, '../package.json'), 'utf-8'))
5
+
6
+ export const name = packageJson.name
7
+ export const version = packageJson.version
@@ -1,11 +1,9 @@
1
- 'use strict'
2
-
3
- module.exports = {
1
+ export default {
4
2
  version: '1.36.0',
5
3
  up: function (config) {
6
4
  if (config.restartOnError === undefined) {
7
5
  config.restartOnError = false
8
6
  }
9
7
  return config
10
- },
8
+ }
11
9
  }
@@ -1,11 +1,9 @@
1
- 'use strict'
2
-
3
- module.exports = {
1
+ export default {
4
2
  version: '1.5.0',
5
3
  up: function (config) {
6
4
  if (config.watch !== undefined) {
7
5
  delete config.watch
8
6
  }
9
7
  return config
10
- },
8
+ }
11
9
  }
@@ -1,8 +1,6 @@
1
- 'use strict'
1
+ import { version } from '../version.js'
2
2
 
3
- const pkg = require('../../package.json')
4
-
5
- module.exports = {
3
+ export default {
6
4
  version: '1.99.0', // This is to account alpha versions as well
7
5
  up: function (config) {
8
6
  if (config.restartOnError === undefined) {
@@ -16,7 +14,7 @@ module.exports = {
16
14
 
17
15
  delete config.allowCycles
18
16
 
19
- config.$schema = `https://schemas.platformatic.dev/@platformatic/runtime/${pkg.version}.json`
17
+ config.$schema = `https://schemas.platformatic.dev/@platformatic/runtime/${version}.json`
20
18
 
21
19
  if (config.server?.logger) {
22
20
  config.logger = config.server.logger
@@ -0,0 +1,16 @@
1
+ export default {
2
+ version: '2.99.0',
3
+ up (config) {
4
+ if (typeof config.gracefulShutdown?.service === 'number') {
5
+ config.gracefulShutdown.application = config.gracefulShutdown.service
6
+ delete config.gracefulShutdown.service
7
+ }
8
+
9
+ if (typeof config.serviceTimeout === 'number') {
10
+ config.applicationTimeout = config.serviceTimeout
11
+ delete config.serviceTimeout
12
+ }
13
+
14
+ return config
15
+ }
16
+ }