@platformatic/runtime 0.28.1 → 0.30.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 (42) hide show
  1. package/fixtures/dbApp/platformatic.db.json +5 -0
  2. package/fixtures/dbApp/plugin.js +12 -0
  3. package/fixtures/monorepo/serviceApp/plugin.js +4 -0
  4. package/fixtures/start-command-in-runtime.js +2 -1
  5. package/fixtures/typescript/platformatic.runtime.json +12 -0
  6. package/fixtures/typescript/services/composer/platformatic.composer.json +28 -0
  7. package/fixtures/typescript/services/movies/global.d.ts +24 -0
  8. package/fixtures/typescript/services/movies/migrations/001.do.sql +6 -0
  9. package/fixtures/typescript/services/movies/migrations/001.undo.sql +3 -0
  10. package/fixtures/typescript/services/movies/platformatic.db.json +33 -0
  11. package/fixtures/typescript/services/movies/plugin.ts +5 -0
  12. package/fixtures/typescript/services/movies/tsconfig.json +21 -0
  13. package/fixtures/typescript/services/movies/types/Movie.d.ts +9 -0
  14. package/fixtures/typescript/services/movies/types/index.d.ts +7 -0
  15. package/fixtures/typescript/services/titles/client/client.d.ts +141 -0
  16. package/fixtures/typescript/services/titles/client/client.openapi.json +630 -0
  17. package/fixtures/typescript/services/titles/client/package.json +4 -0
  18. package/fixtures/typescript/services/titles/platformatic.service.json +31 -0
  19. package/fixtures/typescript/services/titles/plugins/example.ts +6 -0
  20. package/fixtures/typescript/services/titles/routes/root.ts +21 -0
  21. package/fixtures/typescript/services/titles/tsconfig.json +21 -0
  22. package/help/compile.txt +8 -0
  23. package/index.js +7 -7
  24. package/lib/api-client.js +91 -0
  25. package/lib/api.js +26 -77
  26. package/lib/app.js +6 -2
  27. package/lib/compile.js +43 -0
  28. package/lib/config.js +77 -14
  29. package/lib/message-port-writable.js +42 -0
  30. package/lib/start.js +38 -19
  31. package/lib/unified-api.js +2 -0
  32. package/lib/worker.js +25 -26
  33. package/package.json +12 -8
  34. package/runtime.mjs +4 -0
  35. package/test/api.test.js +12 -1
  36. package/test/app.test.js +1 -1
  37. package/test/cli/compile.test.mjs +65 -0
  38. package/test/cli/start.test.mjs +56 -0
  39. package/test/cli/validations.test.mjs +2 -1
  40. package/test/cli/watch.test.mjs +15 -12
  41. package/test/config.test.js +137 -1
  42. package/test/start.test.js +57 -0
@@ -0,0 +1,21 @@
1
+ {
2
+ "compilerOptions": {
3
+ "module": "commonjs",
4
+ "esModuleInterop": true,
5
+ "target": "es2019",
6
+ "sourceMap": true,
7
+ "pretty": true,
8
+ "noEmitOnError": true,
9
+ "outDir": "dist"
10
+ },
11
+ "watchOptions": {
12
+ "watchFile": "fixedPollingInterval",
13
+ "watchDirectory": "fixedPollingInterval",
14
+ "fallbackPolling": "dynamicPriority",
15
+ "synchronousWatchDirectory": true,
16
+ "excludeDirectories": [
17
+ "**/node_modules",
18
+ "dist"
19
+ ]
20
+ }
21
+ }
@@ -0,0 +1,8 @@
1
+ Compile all typescript plugins for all services.
2
+
3
+ ``` bash
4
+ $ platformatic runtime compile
5
+ ```
6
+
7
+ This command will compile the TypeScript
8
+ plugins for each services registered in the runtime.
package/index.js CHANGED
@@ -3,11 +3,11 @@ const { buildServer } = require('./lib/build-server')
3
3
  const { platformaticRuntime } = require('./lib/config')
4
4
  const { start } = require('./lib/start')
5
5
  const unifiedApi = require('./lib/unified-api')
6
+ const RuntimeApi = require('./lib/api')
6
7
 
7
- module.exports = {
8
- buildServer,
9
- platformaticRuntime,
10
- schema: platformaticRuntime.schema,
11
- start,
12
- unifiedApi
13
- }
8
+ module.exports.buildServer = buildServer
9
+ module.exports.platformaticRuntime = platformaticRuntime
10
+ module.exports.schema = platformaticRuntime.schema
11
+ module.exports.RuntimeApi = RuntimeApi
12
+ module.exports.start = start
13
+ module.exports.unifiedApi = unifiedApi
@@ -0,0 +1,91 @@
1
+ 'use strict'
2
+
3
+ const { once, EventEmitter } = require('node:events')
4
+ const { randomUUID } = require('node:crypto')
5
+
6
+ const MAX_LISTENERS_COUNT = 100
7
+
8
+ class RuntimeApiClient extends EventEmitter {
9
+ #exitCode
10
+ #exitPromise
11
+
12
+ constructor (worker) {
13
+ super()
14
+ this.setMaxListeners(MAX_LISTENERS_COUNT)
15
+
16
+ this.worker = worker
17
+ this.#exitPromise = this.#exitHandler()
18
+ this.worker.on('message', (message) => {
19
+ if (message.operationId) {
20
+ this.emit(message.operationId, message)
21
+ }
22
+ })
23
+ }
24
+
25
+ async start () {
26
+ return this.#sendCommand('plt:start-services')
27
+ }
28
+
29
+ async close () {
30
+ await this.#sendCommand('plt:stop-services')
31
+ await this.#exitPromise
32
+ }
33
+
34
+ async restart () {
35
+ return this.#sendCommand('plt:restart-services')
36
+ }
37
+
38
+ async getServices () {
39
+ return this.#sendCommand('plt:get-services')
40
+ }
41
+
42
+ async getServiceDetails (id) {
43
+ return this.#sendCommand('plt:get-service-details', { id })
44
+ }
45
+
46
+ async getServiceConfig (id) {
47
+ return this.#sendCommand('plt:get-service-config', { id })
48
+ }
49
+
50
+ async startService (id) {
51
+ return this.#sendCommand('plt:start-service', { id })
52
+ }
53
+
54
+ async stopService (id) {
55
+ return this.#sendCommand('plt:stop-service', { id })
56
+ }
57
+
58
+ async inject (id, injectParams) {
59
+ return this.#sendCommand('plt:inject', { id, injectParams })
60
+ }
61
+
62
+ async #sendCommand (command, params = {}) {
63
+ const operationId = randomUUID()
64
+
65
+ this.worker.postMessage({ operationId, command, params })
66
+ const [message] = await Promise.race(
67
+ [once(this, operationId), this.#exitPromise]
68
+ )
69
+
70
+ if (this.#exitCode !== undefined) {
71
+ throw new Error('The runtime exited before the operation completed')
72
+ }
73
+
74
+ const { error, data } = message
75
+ if (error !== null) {
76
+ throw new Error(error)
77
+ }
78
+
79
+ return JSON.parse(data)
80
+ }
81
+
82
+ async #exitHandler () {
83
+ this.#exitCode = undefined
84
+ return once(this.worker, 'exit').then((msg) => {
85
+ this.#exitCode = msg[0]
86
+ return msg
87
+ })
88
+ }
89
+ }
90
+
91
+ module.exports = RuntimeApiClient
package/lib/api.js CHANGED
@@ -1,84 +1,33 @@
1
1
  'use strict'
2
2
 
3
- const { once, EventEmitter } = require('node:events')
4
- const { randomUUID } = require('node:crypto')
3
+ const FastifyUndiciDispatcher = require('fastify-undici-dispatcher')
4
+ const { Agent, setGlobalDispatcher } = require('undici')
5
+ const { PlatformaticApp } = require('./app')
5
6
 
6
- const MAX_LISTENERS_COUNT = 100
7
-
8
- class RuntimeApiClient extends EventEmitter {
9
- #worker
10
-
11
- constructor (worker) {
12
- super()
13
- this.setMaxListeners(MAX_LISTENERS_COUNT)
14
-
15
- this.#worker = worker
16
- this.#worker.on('message', (message) => {
17
- if (message.operationId) {
18
- this.emit(message.operationId, message)
19
- }
20
- })
21
- }
22
-
23
- async start () {
24
- return this.#sendCommand('plt:start-services')
25
- }
26
-
27
- async close () {
28
- await this.#sendCommand('plt:stop-services')
29
- await once(this.#worker, 'exit')
30
- }
31
-
32
- async restart () {
33
- return this.#sendCommand('plt:restart-services')
34
- }
35
-
36
- async getServices () {
37
- return this.#sendCommand('plt:get-services')
38
- }
39
-
40
- async getServiceDetails (id) {
41
- return this.#sendCommand('plt:get-service-details', { id })
42
- }
43
-
44
- async getServiceConfig (id) {
45
- return this.#sendCommand('plt:get-service-config', { id })
46
- }
47
-
48
- async startService (id) {
49
- return this.#sendCommand('plt:start-service', { id })
50
- }
51
-
52
- async stopService (id) {
53
- return this.#sendCommand('plt:stop-service', { id })
54
- }
55
-
56
- async inject (id, injectParams) {
57
- return this.#sendCommand('plt:inject', { id, injectParams })
58
- }
7
+ class RuntimeApi {
8
+ #services
9
+ #dispatcher
10
+ #loaderPort
59
11
 
60
- async #sendCommand (command, params = {}) {
61
- const operationId = randomUUID()
12
+ constructor (config, logger, loaderPort) {
13
+ this.#services = new Map()
62
14
 
63
- this.#worker.postMessage({ operationId, command, params })
64
- const [message] = await once(this, operationId)
15
+ for (let i = 0; i < config.services.length; ++i) {
16
+ const service = config.services[i]
17
+ const app = new PlatformaticApp(service, loaderPort, logger)
65
18
 
66
- const { error, data } = message
67
- if (error !== null) {
68
- throw new Error(error)
19
+ this.#services.set(service.id, app)
69
20
  }
70
21
 
71
- return JSON.parse(data)
72
- }
73
- }
74
-
75
- class RuntimeApi {
76
- #services
77
- #dispatcher
22
+ const globalAgent = new Agent()
23
+ const globalDispatcher = new FastifyUndiciDispatcher({
24
+ dispatcher: globalAgent,
25
+ // setting the domain here allows for fail-fast scenarios
26
+ domain: '.plt.local'
27
+ })
78
28
 
79
- constructor (services, dispatcher) {
80
- this.#services = services
81
- this.#dispatcher = dispatcher
29
+ setGlobalDispatcher(globalDispatcher)
30
+ this.#dispatcher = globalDispatcher
82
31
  }
83
32
 
84
33
  async startListening (parentPort) {
@@ -116,9 +65,9 @@ class RuntimeApi {
116
65
  async #runCommandHandler (command, params) {
117
66
  switch (command) {
118
67
  case 'plt:start-services':
119
- return this.#startServices(params)
68
+ return this.startServices(params)
120
69
  case 'plt:stop-services':
121
- return this.#stopServices(params)
70
+ return this.stopServices(params)
122
71
  case 'plt:restart-services':
123
72
  return this.#restartServices(params)
124
73
  case 'plt:get-services':
@@ -139,7 +88,7 @@ class RuntimeApi {
139
88
  }
140
89
  }
141
90
 
142
- async #startServices () {
91
+ async startServices () {
143
92
  let entrypointUrl = null
144
93
  for (const service of this.#services.values()) {
145
94
  await service.start()
@@ -154,7 +103,7 @@ class RuntimeApi {
154
103
  return entrypointUrl
155
104
  }
156
105
 
157
- async #stopServices () {
106
+ async stopServices () {
158
107
  for (const service of this.#services.values()) {
159
108
  const serviceStatus = service.getStatus()
160
109
  if (serviceStatus === 'started') {
@@ -254,4 +203,4 @@ class RuntimeApi {
254
203
  }
255
204
  }
256
205
 
257
- module.exports = { RuntimeApi, RuntimeApiClient }
206
+ module.exports = RuntimeApi
package/lib/app.js CHANGED
@@ -118,6 +118,7 @@ class PlatformaticApp {
118
118
  }
119
119
 
120
120
  async handleProcessLevelEvent ({ signal, err }) {
121
+ /* c8 ignore next 3 */
121
122
  if (!this.server) {
122
123
  return false
123
124
  }
@@ -223,6 +224,7 @@ class PlatformaticApp {
223
224
  watchIgnore: [...(this.#originalWatch?.ignore || []), basename(configManager.fullPath)]
224
225
  })
225
226
 
227
+ /* c8 ignore next 4 */
226
228
  fileWatcher.on('update', async () => {
227
229
  this.server.log.debug('files changed')
228
230
  this.restart()
@@ -236,12 +238,14 @@ class PlatformaticApp {
236
238
 
237
239
  async #stopFileWatching () {
238
240
  const watcher = this.server.platformatic.fileWatcher
241
+ // The configManager automatically watches for the config file changes
242
+ // therefore we need to stop it all the times.
243
+ await this.config.configManager.stopWatching()
239
244
 
240
245
  if (watcher) {
241
- await watcher.stopWatching()
242
246
  this.server.log.debug('stop watching files')
247
+ await watcher.stopWatching()
243
248
  this.server.platformatic.fileWatcher = undefined
244
- this.server.platformatic.configManager.stopWatching()
245
249
  }
246
250
  }
247
251
 
package/lib/compile.js ADDED
@@ -0,0 +1,43 @@
1
+ 'use strict'
2
+
3
+ const { loadConfig, tsCompiler } = require('@platformatic/service')
4
+ const { access } = require('node:fs/promises')
5
+ const { join } = require('path')
6
+ const pino = require('pino')
7
+ const pretty = require('pino-pretty')
8
+ const { isatty } = require('node:tty')
9
+
10
+ const { platformaticRuntime } = require('./config')
11
+
12
+ async function compile (argv) {
13
+ const { configManager } = await loadConfig({}, argv, platformaticRuntime, {
14
+ watch: false
15
+ })
16
+
17
+ let stream
18
+
19
+ /* c8 ignore next 6 */
20
+ if (isatty(process.stdout.fd)) {
21
+ stream = pretty({
22
+ translateTime: 'SYS:HH:MM:ss',
23
+ ignore: 'hostname,pid'
24
+ })
25
+ }
26
+
27
+ const logger = pino(stream)
28
+
29
+ for (const service of configManager.current.services) {
30
+ const tsconfig = join(service.path, 'tsconfig.json')
31
+
32
+ try {
33
+ await access(tsconfig)
34
+ } catch {
35
+ logger.trace(`No tsconfig.json found in ${service.path}, skipping...`)
36
+ continue
37
+ }
38
+
39
+ await tsCompiler.compile(service.path, {}, logger.child({ name: service.id }))
40
+ }
41
+ }
42
+
43
+ module.exports.compile = compile
package/lib/config.js CHANGED
@@ -37,6 +37,7 @@ async function _transformConfig (configManager) {
37
37
 
38
38
  configManager.current.allowCycles = !!configManager.current.allowCycles
39
39
  configManager.current.serviceMap = new Map()
40
+ configManager.current.inspectorOptions = null
40
41
 
41
42
  let hasValidEntrypoint = false
42
43
 
@@ -121,22 +122,28 @@ async function parseClientsAndComposer (configManager) {
121
122
  let clientName = client.serviceId ?? ''
122
123
  let clientUrl
123
124
  let missingKey
124
-
125
- try {
126
- clientUrl = await cm.replaceEnv(client.url)
127
- /* c8 ignore next 2 - unclear why c8 is unhappy here */
128
- } catch (err) {
129
- if (err.name !== 'MissingValueError') {
130
- /* c8 ignore next 3 */
131
- reject(err)
132
- return
125
+ let isLocal = false
126
+
127
+ if (clientName === '' || client.url !== undefined) {
128
+ try {
129
+ clientUrl = await cm.replaceEnv(client.url)
130
+ /* c8 ignore next 2 - unclear why c8 is unhappy here */
131
+ } catch (err) {
132
+ if (err.name !== 'MissingValueError') {
133
+ /* c8 ignore next 3 */
134
+ reject(err)
135
+ return
136
+ }
137
+
138
+ missingKey = err.key
133
139
  }
134
-
135
- missingKey = err.key
140
+ isLocal = missingKey && client.url === `{${missingKey}}`
141
+ /* c8 ignore next 3 */
142
+ } else {
143
+ /* c8 ignore next 2 */
144
+ isLocal = true
136
145
  }
137
146
 
138
- const isLocal = missingKey && client.url === `{${missingKey}}`
139
-
140
147
  /* c8 ignore next 20 - unclear why c8 is unhappy for nearly 20 lines here */
141
148
  if (!clientName) {
142
149
  const clientAbsolutePath = pathResolve(service.path, client.path)
@@ -234,4 +241,60 @@ async function wrapConfigInRuntimeConfig ({ configManager, args }) {
234
241
  return cm
235
242
  }
236
243
 
237
- module.exports = { platformaticRuntime, wrapConfigInRuntimeConfig }
244
+ function parseInspectorOptions (configManager) {
245
+ const { current, args } = configManager
246
+ const hasInspect = 'inspect' in args
247
+ const hasInspectBrk = 'inspect-brk' in args
248
+ let inspectFlag
249
+
250
+ if (hasInspect) {
251
+ inspectFlag = args.inspect
252
+
253
+ if (hasInspectBrk) {
254
+ throw new Error('--inspect and --inspect-brk cannot be used together')
255
+ }
256
+ } else if (hasInspectBrk) {
257
+ inspectFlag = args['inspect-brk']
258
+ }
259
+
260
+ if (inspectFlag !== undefined) {
261
+ let host = '127.0.0.1'
262
+ let port = 9229
263
+
264
+ if (typeof inspectFlag === 'string' && inspectFlag.length > 0) {
265
+ const splitAt = inspectFlag.lastIndexOf(':')
266
+
267
+ if (splitAt === -1) {
268
+ port = inspectFlag
269
+ } else {
270
+ host = inspectFlag.substring(0, splitAt)
271
+ port = inspectFlag.substring(splitAt + 1)
272
+ }
273
+
274
+ port = Number.parseInt(port, 10)
275
+
276
+ if (!(port === 0 || (port >= 1024 && port <= 65535))) {
277
+ throw new Error('inspector port must be 0 or in range 1024 to 65535')
278
+ }
279
+
280
+ if (!host) {
281
+ throw new Error('inspector host cannot be empty')
282
+ }
283
+ }
284
+
285
+ current.inspectorOptions = {
286
+ host,
287
+ port,
288
+ breakFirstLine: hasInspectBrk,
289
+ hotReloadDisabled: !!current.hotReload
290
+ }
291
+
292
+ current.hotReload = false
293
+ }
294
+ }
295
+
296
+ module.exports = {
297
+ parseInspectorOptions,
298
+ platformaticRuntime,
299
+ wrapConfigInRuntimeConfig
300
+ }
@@ -0,0 +1,42 @@
1
+ 'use strict'
2
+ const assert = require('node:assert')
3
+ const { Writable } = require('node:stream')
4
+
5
+ class MessagePortWritable extends Writable {
6
+ constructor (options) {
7
+ const opts = { ...options, objectMode: true }
8
+
9
+ super(opts)
10
+ this.port = opts.port
11
+ this.metadata = opts.metadata
12
+ }
13
+
14
+ _writev (chunks, callback) {
15
+ // Process the logs here before trying to send them across the thread
16
+ // boundary. Sometimes the chunks have an undocumented method on them
17
+ // which will prevent sending the chunk itself across threads.
18
+ const logs = chunks.map((chunk) => {
19
+ // pino should always produce a string here.
20
+ assert(typeof chunk.chunk === 'string')
21
+ return chunk.chunk.trim()
22
+ })
23
+
24
+ this.port.postMessage({
25
+ metadata: this.metadata,
26
+ logs
27
+ })
28
+ setImmediate(callback)
29
+ }
30
+
31
+ _final (callback) {
32
+ this.port.close()
33
+ callback()
34
+ }
35
+
36
+ _destroy (err, callback) {
37
+ this.port.close()
38
+ callback(err)
39
+ }
40
+ }
41
+
42
+ module.exports = { MessagePortWritable }
package/lib/start.js CHANGED
@@ -1,12 +1,13 @@
1
1
  'use strict'
2
2
  const { once } = require('node:events')
3
+ const inspector = require('node:inspector')
3
4
  const { join } = require('node:path')
4
5
  const { pathToFileURL } = require('node:url')
5
6
  const { Worker } = require('node:worker_threads')
6
7
  const closeWithGrace = require('close-with-grace')
7
8
  const { loadConfig } = require('@platformatic/service')
8
- const { platformaticRuntime } = require('./config')
9
- const { RuntimeApiClient } = require('./api.js')
9
+ const { parseInspectorOptions, platformaticRuntime } = require('./config')
10
+ const RuntimeApiClient = require('./api-client.js')
10
11
  const kLoaderFile = pathToFileURL(join(__dirname, 'loader.mjs')).href
11
12
  const kWorkerFile = join(__dirname, 'worker.js')
12
13
  const kWorkerExecArgv = [
@@ -16,21 +17,33 @@ const kWorkerExecArgv = [
16
17
  ]
17
18
 
18
19
  async function start (argv) {
19
- const { configManager } = await loadConfig({}, argv, platformaticRuntime, {
20
+ const config = await loadConfig({}, argv, platformaticRuntime, {
20
21
  watch: true
21
22
  })
22
- const app = await startWithConfig(configManager)
23
23
 
24
+ config.configManager.args = config.args
25
+ const app = await startWithConfig(config.configManager)
24
26
  await app.start()
25
27
  return app
26
28
  }
27
29
 
28
- async function startWithConfig (configManager) {
30
+ async function startWithConfig (configManager, env = process.env) {
29
31
  const config = configManager.current
32
+
33
+ if (inspector.url()) {
34
+ throw new Error('The Node.js inspector flags are not supported. Please use \'platformatic start --inspect\' instead.')
35
+ }
36
+
37
+ if (configManager.args) {
38
+ parseInspectorOptions(configManager)
39
+ }
40
+
30
41
  const worker = new Worker(kWorkerFile, {
31
42
  /* c8 ignore next */
32
43
  execArgv: config.hotReload ? kWorkerExecArgv : [],
33
- workerData: { config }
44
+ transferList: config.loggingPort ? [config.loggingPort] : [],
45
+ workerData: { config },
46
+ env
34
47
  })
35
48
 
36
49
  worker.on('exit', () => {
@@ -38,23 +51,29 @@ async function startWithConfig (configManager) {
38
51
  })
39
52
 
40
53
  worker.on('error', () => {
41
- // the error is logged in the worker
42
- process.exit(1)
54
+ // If this is the only 'error' handler, then exit the process as the default
55
+ // behavior. If anything else is listening for errors, then don't exit.
56
+ if (worker.listenerCount('error') === 1) {
57
+ // The error is logged in the worker.
58
+ process.exit(1)
59
+ }
43
60
  })
44
61
 
45
- /* c8 ignore next 3 */
46
- process.on('SIGUSR2', () => {
47
- worker.postMessage({ signal: 'SIGUSR2' })
48
- })
62
+ if (config.hotReload) {
63
+ /* c8 ignore next 3 */
64
+ process.on('SIGUSR2', () => {
65
+ worker.postMessage({ signal: 'SIGUSR2' })
66
+ })
49
67
 
50
- closeWithGrace((event) => {
51
- worker.postMessage(event)
52
- })
68
+ closeWithGrace((event) => {
69
+ worker.postMessage(event)
70
+ })
53
71
 
54
- /* c8 ignore next 3 */
55
- configManager.on('update', () => {
56
- // TODO(cjihrig): Need to clean up and restart the worker.
57
- })
72
+ /* c8 ignore next 3 */
73
+ configManager.on('update', () => {
74
+ // TODO(cjihrig): Need to clean up and restart the worker.
75
+ })
76
+ }
58
77
 
59
78
  await once(worker, 'message') // plt:init
60
79
 
@@ -163,9 +163,11 @@ async function startCommandInRuntime (args) {
163
163
  let runtime
164
164
 
165
165
  if (configType === 'runtime') {
166
+ config.configManager.args = config.args
166
167
  runtime = await runtimeStartWithConfig(config.configManager)
167
168
  } else {
168
169
  const wrappedConfig = await wrapConfigInRuntimeConfig(config)
170
+ wrappedConfig.args = config.args
169
171
  runtime = await runtimeStartWithConfig(wrappedConfig)
170
172
  }
171
173