@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,182 @@
1
+ import { executeWithTimeout, kTimeout } from '@platformatic/foundation'
2
+ import { ITC, generateResponse, sanitize } from '@platformatic/itc'
3
+ import { MessagingError } from '../errors.js'
4
+ import { RoundRobinMap } from './round-robin-map.js'
5
+ import { kITC, kWorkersBroadcast } from './symbols.js'
6
+
7
+ const kPendingResponses = Symbol('plt.messaging.pendingResponses')
8
+
9
+ export class MessagingITC extends ITC {
10
+ #timeout
11
+ #listener
12
+ #closeResolvers
13
+ #broadcastChannel
14
+ #workers
15
+ #sources
16
+
17
+ constructor (id, runtimeConfig) {
18
+ super({
19
+ throwOnMissingHandler: true,
20
+ name: `${id}-messaging`
21
+ })
22
+
23
+ this.#timeout = runtimeConfig.messagingTimeout
24
+ this.#workers = new RoundRobinMap()
25
+ this.#sources = new Set()
26
+
27
+ // Start listening on the BroadcastChannel for the list of applications
28
+ this.#broadcastChannel = new BroadcastChannel(kWorkersBroadcast)
29
+ this.#broadcastChannel.onmessage = this.#updateWorkers.bind(this)
30
+
31
+ this.listen()
32
+ }
33
+
34
+ _setupListener (listener) {
35
+ this.#listener = listener
36
+ }
37
+
38
+ handle (message, handler) {
39
+ if (typeof message === 'object') {
40
+ for (const [name, fn] of Object.entries(message)) {
41
+ super.handle(name, fn)
42
+ }
43
+ } else {
44
+ super.handle(message, handler)
45
+ }
46
+ }
47
+
48
+ async send (application, name, message, options) {
49
+ // Get the next worker for the application
50
+ const worker = this.#workers.next(application)
51
+
52
+ if (!worker) {
53
+ throw new MessagingError(application, 'No workers available')
54
+ }
55
+
56
+ if (!worker.channel) {
57
+ // Use twice the value here as a fallback measure. The target handler in the main thread is forwarding
58
+ // the request to the worker, using executeWithTimeout with the user set timeout value.
59
+ const channel = await executeWithTimeout(
60
+ globalThis[kITC].send('getWorkerMessagingChannel', { application: worker.application, worker: worker.worker }),
61
+ this.#timeout * 2
62
+ )
63
+
64
+ /* c8 ignore next 3 - Hard to test */
65
+ if (channel === kTimeout) {
66
+ throw new MessagingError(application, 'Timeout while waiting for a communication channel.')
67
+ }
68
+
69
+ worker.channel = channel
70
+ this.#setupChannel(channel)
71
+
72
+ channel[kPendingResponses] = new Map()
73
+ channel.on('close', this.#handlePendingResponse.bind(this, channel))
74
+ }
75
+
76
+ const context = { ...options }
77
+ context.channel = worker.channel
78
+ context.application = worker.application
79
+ context.trackResponse = true
80
+
81
+ const response = await executeWithTimeout(super.send(name, message, context), this.#timeout)
82
+
83
+ if (response === kTimeout) {
84
+ throw new MessagingError(application, 'Timeout while waiting for a response.')
85
+ }
86
+
87
+ return response
88
+ }
89
+
90
+ async addSource (channel) {
91
+ this.#sources.add(channel)
92
+ this.#setupChannel(channel)
93
+
94
+ // This has been closed on the other side.
95
+ // Pending messages will be silently discarded by Node (as postMessage does not throw) so we don't need to handle them.
96
+ channel.on('close', () => {
97
+ this.#sources.delete(channel)
98
+ })
99
+ }
100
+
101
+ _send (request, context) {
102
+ const { channel, transferList } = context
103
+
104
+ if (context.trackResponse) {
105
+ const application = context.application
106
+ channel[kPendingResponses].set(request.reqId, { application, request })
107
+ }
108
+
109
+ channel.postMessage(sanitize(request, transferList), { transferList })
110
+ }
111
+
112
+ _createClosePromise () {
113
+ const { promise, resolve, reject } = Promise.withResolvers()
114
+ this.#closeResolvers = { resolve, reject }
115
+ return promise
116
+ }
117
+
118
+ _close () {
119
+ this.#closeResolvers.resolve()
120
+ this.#broadcastChannel.close()
121
+
122
+ for (const source of this.#sources) {
123
+ source.close()
124
+ }
125
+
126
+ for (const worker of this.#workers.values()) {
127
+ worker.channel?.close()
128
+ }
129
+
130
+ this.#sources.clear()
131
+ }
132
+
133
+ #setupChannel (channel) {
134
+ // Setup the message for processing
135
+ channel.on('message', event => {
136
+ this.#listener(event, { channel })
137
+ })
138
+ }
139
+
140
+ #updateWorkers (event) {
141
+ // Gather all existing channels by thread, it will make them reusable
142
+ const existingChannels = new Map()
143
+ for (const source of this.#workers.values()) {
144
+ existingChannels.set(source.thread, source.channel)
145
+ }
146
+
147
+ // Create a brand new map
148
+ this.#workers = new RoundRobinMap()
149
+
150
+ const instances = []
151
+ for (const [application, workers] of event.data) {
152
+ const count = workers.length
153
+ const next = Math.floor(Math.random() * count)
154
+
155
+ instances.push({ id: application, next, workers: count })
156
+
157
+ for (let i = 0; i < count; i++) {
158
+ const worker = workers[i]
159
+ const channel = existingChannels.get(worker.thread)
160
+
161
+ // Note i is not the worker index as in runtime, but the index in the list of current alive workers for the application
162
+ this.#workers.set(`${application}:${i}`, { ...worker, channel })
163
+ }
164
+ }
165
+
166
+ this.#workers.configure(instances)
167
+ }
168
+
169
+ #handlePendingResponse (channel) {
170
+ for (const { application, request } of channel[kPendingResponses].values()) {
171
+ this._emitResponse(
172
+ generateResponse(
173
+ request,
174
+ new MessagingError(application, 'The communication channel was closed before receiving a response.'),
175
+ null
176
+ )
177
+ )
178
+ }
179
+
180
+ channel[kPendingResponses].clear()
181
+ }
182
+ }
@@ -0,0 +1,62 @@
1
+ export class RoundRobinMap extends Map {
2
+ #instances
3
+
4
+ constructor (iterable, instances = {}) {
5
+ super(iterable)
6
+ this.#instances = instances
7
+ }
8
+
9
+ get configuration () {
10
+ return { ...this.#instances }
11
+ }
12
+
13
+ configure (applications) {
14
+ this.#instances = {}
15
+
16
+ for (const application of applications) {
17
+ this.#instances[application.id] = { next: application.next ?? 0, count: application.workers }
18
+ }
19
+ }
20
+
21
+ getCount (application) {
22
+ if (!this.#instances[application]) {
23
+ return null
24
+ }
25
+
26
+ return this.#instances[application].count
27
+ }
28
+
29
+ setCount (application, count) {
30
+ if (!this.#instances[application]) {
31
+ throw new Error(`Application ${application} is not configured.`)
32
+ }
33
+
34
+ this.#instances[application].count = count
35
+ }
36
+
37
+ next (application) {
38
+ if (!this.#instances[application]) {
39
+ return null
40
+ }
41
+
42
+ let worker
43
+ let { next, count } = this.#instances[application]
44
+
45
+ // Try count times to get the next worker. This is to handle the case where a worker is being restarted.
46
+ for (let i = 0; i < count; i++) {
47
+ const current = next++
48
+ if (next >= count) {
49
+ next = 0
50
+ }
51
+
52
+ worker = this.get(`${application}:${current}`)
53
+
54
+ if (worker) {
55
+ break
56
+ }
57
+ }
58
+
59
+ this.#instances[application].next = next
60
+ return worker
61
+ }
62
+ }
@@ -0,0 +1,22 @@
1
+ import { kITC } from './symbols.js'
2
+
3
+ export class SharedContext {
4
+ constructor () {
5
+ this.sharedContext = null
6
+ }
7
+
8
+ update (context, options = {}) {
9
+ return globalThis[kITC].send('updateSharedContext', { ...options, context })
10
+ }
11
+
12
+ get () {
13
+ if (this.sharedContext === null) {
14
+ this.sharedContext = globalThis[kITC].send('getSharedContext')
15
+ }
16
+ return this.sharedContext
17
+ }
18
+
19
+ _set (context) {
20
+ this.sharedContext = context
21
+ }
22
+ }
@@ -1,7 +1,16 @@
1
- 'use strict'
1
+ export const kConfig = Symbol.for('plt.runtime.config')
2
+ export const kId = Symbol.for('plt.runtime.id') // This is also used to detect if we are running in a Platformatic runtime thread
3
+ export const kFullId = Symbol.for('plt.runtime.fullId')
4
+ export const kApplicationId = Symbol.for('plt.runtime.application.id')
5
+ export const kWorkerId = Symbol.for('plt.runtime.worker.id')
6
+ export const kITC = Symbol.for('plt.runtime.itc')
7
+ export const kHealthCheckTimer = Symbol.for('plt.runtime.worker.healthCheckTimer')
8
+ export const kWorkerStatus = Symbol('plt.runtime.worker.status')
9
+ export const kInterceptors = Symbol.for('plt.runtime.worker.interceptors')
10
+ export const kLastELU = Symbol.for('plt.runtime.worker.lastELU')
2
11
 
3
- const kConfig = Symbol.for('plt.runtime.config')
4
- const kId = Symbol.for('plt.runtime.id') // This is also used to detect if we are running in a Platformatic runtime thread
5
- const kITC = Symbol.for('plt.runtime.itc')
12
+ // This string marker should be safe to use since it belongs to Unicode private area
13
+ export const kStderrMarker = '\ue002'
6
14
 
7
- module.exports = { kConfig, kId, kITC }
15
+ // Note that this is used to create a BroadcastChannel so it must be a string
16
+ export const kWorkersBroadcast = 'plt.runtime.workers'
package/package.json CHANGED
@@ -1,12 +1,10 @@
1
1
  {
2
2
  "name": "@platformatic/runtime",
3
- "version": "3.4.1",
3
+ "version": "3.5.1",
4
4
  "description": "",
5
5
  "main": "index.js",
6
- "bin": {
7
- "plt-runtime": "./runtime.mjs"
8
- },
9
- "author": "Matteo Collina <hello@matteocollina.com>",
6
+ "type": "module",
7
+ "author": "Platformatic Inc. <oss@platformatic.dev> (https://platformatic.dev)",
10
8
  "repository": {
11
9
  "type": "git",
12
10
  "url": "git+https://github.com/platformatic/platformatic.git"
@@ -17,71 +15,82 @@
17
15
  },
18
16
  "homepage": "https://github.com/platformatic/platformatic#readme",
19
17
  "devDependencies": {
18
+ "@fastify/compress": "^8.0.0",
20
19
  "@fastify/express": "^4.0.0",
21
20
  "@fastify/formbody": "^8.0.0",
22
- "borp": "^0.17.0",
21
+ "autocannon": "^8.0.0",
23
22
  "c8": "^10.0.0",
23
+ "cleaner-spec-reporter": "^0.5.0",
24
24
  "eslint": "9",
25
- "execa": "^8.0.1",
25
+ "execa": "^9.0.0",
26
26
  "express": "^4.18.3",
27
- "fast-jwt": "^4.0.0",
27
+ "fast-jwt": "^5.0.0",
28
28
  "get-port": "^7.1.0",
29
+ "inspector-client": "^0.2.0",
29
30
  "json-schema-to-typescript": "^15.0.0",
30
- "neostandard": "^0.11.1",
31
+ "neostandard": "^0.12.0",
31
32
  "pino-abstract-transport": "^2.0.0",
32
33
  "split2": "^4.2.0",
33
- "tsd": "^0.31.0",
34
34
  "typescript": "^5.5.4",
35
35
  "undici-oidc-interceptor": "^0.5.0",
36
36
  "why-is-node-running": "^2.2.2",
37
- "@platformatic/composer": "3.4.1",
38
- "@platformatic/db": "3.4.1",
39
- "@platformatic/service": "3.4.1",
40
- "@platformatic/sql-graphql": "3.4.1",
41
- "@platformatic/sql-mapper": "3.4.1"
37
+ "@platformatic/db": "3.5.1",
38
+ "@platformatic/gateway": "3.5.1",
39
+ "@platformatic/composer": "3.5.1",
40
+ "@platformatic/node": "3.5.1",
41
+ "@platformatic/service": "3.5.1",
42
+ "@platformatic/sql-graphql": "3.5.1",
43
+ "@platformatic/wattpm-pprof-capture": "3.5.1",
44
+ "@platformatic/sql-mapper": "3.5.1"
42
45
  },
43
46
  "dependencies": {
47
+ "@fastify/accepts": "^5.0.0",
48
+ "@fastify/basic-auth": "^6.0.0",
44
49
  "@fastify/error": "^4.0.0",
45
50
  "@fastify/websocket": "^11.0.0",
46
- "@hapi/topo": "^6.0.2",
47
- "@platformatic/http-metrics": "^0.2.0",
51
+ "@opentelemetry/api": "^1.9.0",
52
+ "@platformatic/undici-cache-memory": "^0.8.1",
48
53
  "@watchable/unpromise": "^1.0.2",
49
- "boring-name-generator": "^1.0.3",
50
54
  "change-case-all": "^2.1.0",
51
- "close-with-grace": "^2.0.0",
52
- "commist": "^3.2.0",
55
+ "close-with-grace": "^2.2.0",
56
+ "colorette": "^2.0.20",
57
+ "cron": "^4.1.0",
53
58
  "debounce": "^2.0.0",
54
- "desm": "^1.3.1",
59
+ "dotenv": "^16.4.5",
55
60
  "dotenv-tool": "^0.1.1",
56
- "es-main": "^1.3.0",
57
61
  "fastest-levenshtein": "^1.0.16",
58
62
  "fastify": "^5.0.0",
59
63
  "graphql": "^16.8.1",
60
64
  "help-me": "^5.0.0",
61
65
  "minimist": "^1.2.8",
62
- "pino": "^8.19.0",
63
- "pino-pretty": "^11.0.0",
64
- "pino-roll": "^2.0.0",
66
+ "pino": "^9.9.0",
67
+ "pino-pretty": "^13.0.0",
65
68
  "prom-client": "^15.1.2",
66
69
  "semgrator": "^0.3.0",
67
- "tail-file-stream": "^0.2.0",
68
- "undici": "^6.9.0",
69
- "undici-thread-interceptor": "^0.7.0",
70
+ "sonic-boom": "^4.2.0",
71
+ "undici": "^7.0.0",
72
+ "undici-thread-interceptor": "^0.14.0",
70
73
  "ws": "^8.16.0",
71
- "@platformatic/generators": "3.4.1",
72
- "@platformatic/basic": "3.4.1",
73
- "@platformatic/config": "3.4.1",
74
- "@platformatic/itc": "3.4.1",
75
- "@platformatic/ts-compiler": "3.4.1",
76
- "@platformatic/telemetry": "3.4.1",
77
- "@platformatic/utils": "3.4.1"
74
+ "@platformatic/basic": "3.5.1",
75
+ "@platformatic/generators": "3.5.1",
76
+ "@platformatic/foundation": "3.5.1",
77
+ "@platformatic/itc": "3.5.1",
78
+ "@platformatic/telemetry": "3.5.1",
79
+ "@platformatic/metrics": "3.5.1"
80
+ },
81
+ "engines": {
82
+ "node": ">=22.19.0"
78
83
  },
79
84
  "scripts": {
80
- "test": "npm run lint && borp --concurrency=1 --timeout=180000 && tsd",
81
- "coverage": "npm run lint && borp -X fixtures -X test -C --concurrency=1 --timeout=180000 && tsd",
85
+ "test": "npm run test:main && npm run test:api && npm run test:cli && npm run test:start && npm run test:multiple-workers",
86
+ "test:main": "node --test --test-reporter=cleaner-spec-reporter --test-concurrency=1 --test-timeout=2000000 test/*.test.js test/versions/*.test.js",
87
+ "test:api": "node --test --test-reporter=cleaner-spec-reporter --test-concurrency=1 --test-timeout=2000000 test/api/*.test.js test/management-api/*.test.js",
88
+ "test:cli": "node --test --test-reporter=cleaner-spec-reporter --test-concurrency=1 --test-timeout=2000000 test/cli/*.test.js test/cli/**/*.test.js",
89
+ "test:start": "node --test --test-reporter=cleaner-spec-reporter --test-concurrency=1 --test-timeout=2000000 test/start/*.test.js",
90
+ "test:multiple-workers": "node --test --test-reporter=cleaner-spec-reporter --test-concurrency=1 --test-timeout=2000000 test/multiple-workers/*.test.js",
82
91
  "gen-schema": "node lib/schema.js > schema.json",
83
92
  "gen-types": "json2ts > config.d.ts < schema.json",
84
- "build": "pnpm run gen-schema && pnpm run gen-types",
93
+ "build": "npm run gen-schema && npm run gen-types",
85
94
  "lint": "eslint"
86
95
  }
87
96
  }