@platformatic/runtime 2.6.1 → 2.8.0-alpha.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.
- package/config.d.ts +3 -1
- package/eslint.config.js +1 -1
- package/lib/config.js +1 -1
- package/lib/dependencies.js +15 -13
- package/lib/errors.js +3 -1
- package/lib/logger.js +22 -7
- package/lib/management-api.js +1 -1
- package/lib/runtime.js +317 -204
- package/lib/schema.js +12 -0
- package/lib/start.js +15 -9
- package/lib/worker/app.js +16 -2
- package/lib/worker/itc.js +7 -2
- package/lib/worker/main.js +75 -7
- package/lib/worker/round-robin-map.js +61 -0
- package/lib/worker/symbols.js +6 -1
- package/package.json +16 -15
- package/schema.json +17 -1
package/lib/schema.js
CHANGED
|
@@ -27,6 +27,10 @@ const services = {
|
|
|
27
27
|
},
|
|
28
28
|
useHttp: {
|
|
29
29
|
type: 'boolean'
|
|
30
|
+
},
|
|
31
|
+
workers: {
|
|
32
|
+
type: 'number',
|
|
33
|
+
minimum: 1
|
|
30
34
|
}
|
|
31
35
|
}
|
|
32
36
|
}
|
|
@@ -49,6 +53,9 @@ const platformaticRuntimeSchema = {
|
|
|
49
53
|
entrypoint: {
|
|
50
54
|
type: 'string'
|
|
51
55
|
},
|
|
56
|
+
basePath: {
|
|
57
|
+
type: 'string'
|
|
58
|
+
},
|
|
52
59
|
autoload: {
|
|
53
60
|
type: 'object',
|
|
54
61
|
additionalProperties: false,
|
|
@@ -87,6 +94,11 @@ const platformaticRuntimeSchema = {
|
|
|
87
94
|
}
|
|
88
95
|
},
|
|
89
96
|
services,
|
|
97
|
+
workers: {
|
|
98
|
+
type: 'number',
|
|
99
|
+
minimum: 1,
|
|
100
|
+
default: 1
|
|
101
|
+
},
|
|
90
102
|
web: services,
|
|
91
103
|
logger,
|
|
92
104
|
server,
|
package/lib/start.js
CHANGED
|
@@ -18,6 +18,16 @@ const { Runtime } = require('./runtime')
|
|
|
18
18
|
const errors = require('./errors')
|
|
19
19
|
const { getRuntimeLogsDir, loadConfig } = require('./utils')
|
|
20
20
|
|
|
21
|
+
async function restartRuntime (runtime) {
|
|
22
|
+
runtime.logger.info('Received SIGUSR2, restarting all services ...')
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
await runtime.restart()
|
|
26
|
+
} catch (err) {
|
|
27
|
+
runtime.logger.error({ err: ensureLoggableError(err) }, 'Failed to restart services.')
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
21
31
|
async function buildRuntime (configManager, env) {
|
|
22
32
|
env = env || process.env
|
|
23
33
|
|
|
@@ -35,14 +45,10 @@ async function buildRuntime (configManager, env) {
|
|
|
35
45
|
const runtime = new Runtime(configManager, runtimeLogsDir, env)
|
|
36
46
|
|
|
37
47
|
/* c8 ignore next 3 */
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
await runtime.restart()
|
|
43
|
-
} catch (err) {
|
|
44
|
-
runtime.logger.error({ err: ensureLoggableError(err) }, 'Failed to restart services.')
|
|
45
|
-
}
|
|
48
|
+
const restartListener = restartRuntime.bind(null, runtime)
|
|
49
|
+
process.on('SIGUSR2', restartListener)
|
|
50
|
+
runtime.on('closed', () => {
|
|
51
|
+
process.removeListener('SIGUSR2', restartListener)
|
|
46
52
|
})
|
|
47
53
|
|
|
48
54
|
try {
|
|
@@ -109,7 +115,7 @@ async function setupAndStartRuntime (config) {
|
|
|
109
115
|
})
|
|
110
116
|
)
|
|
111
117
|
logger.warn(`Port: ${originalPort} is already in use!`)
|
|
112
|
-
logger.warn(`
|
|
118
|
+
logger.warn(`Changing the port to ${runtimeConfig.current.server.port}`)
|
|
113
119
|
}
|
|
114
120
|
return { address, runtime }
|
|
115
121
|
}
|
package/lib/worker/app.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
const { existsSync } = require('node:fs')
|
|
4
4
|
const { EventEmitter } = require('node:events')
|
|
5
5
|
const { resolve } = require('node:path')
|
|
6
|
+
const { workerData } = require('node:worker_threads')
|
|
6
7
|
const { ConfigManager } = require('@platformatic/config')
|
|
7
8
|
const { FileWatcher } = require('@platformatic/utils')
|
|
8
9
|
const { getGlobalDispatcher, setGlobalDispatcher } = require('undici')
|
|
@@ -21,9 +22,20 @@ class PlatformaticApp extends EventEmitter {
|
|
|
21
22
|
#debouncedRestart
|
|
22
23
|
#context
|
|
23
24
|
|
|
24
|
-
constructor (
|
|
25
|
+
constructor (
|
|
26
|
+
appConfig,
|
|
27
|
+
workerId,
|
|
28
|
+
telemetryConfig,
|
|
29
|
+
loggerConfig,
|
|
30
|
+
serverConfig,
|
|
31
|
+
metricsConfig,
|
|
32
|
+
hasManagementApi,
|
|
33
|
+
watch
|
|
34
|
+
) {
|
|
25
35
|
super()
|
|
26
36
|
this.appConfig = appConfig
|
|
37
|
+
this.serviceId = this.appConfig.id
|
|
38
|
+
this.workerId = workerId
|
|
27
39
|
this.#watch = watch
|
|
28
40
|
this.#starting = false
|
|
29
41
|
this.#started = false
|
|
@@ -32,7 +44,8 @@ class PlatformaticApp extends EventEmitter {
|
|
|
32
44
|
this.#fileWatcher = null
|
|
33
45
|
|
|
34
46
|
this.#context = {
|
|
35
|
-
serviceId: this.
|
|
47
|
+
serviceId: this.serviceId,
|
|
48
|
+
workerId: this.workerId,
|
|
36
49
|
directory: this.appConfig.path,
|
|
37
50
|
isEntrypoint: this.appConfig.entrypoint,
|
|
38
51
|
isProduction: this.appConfig.isProduction,
|
|
@@ -40,6 +53,7 @@ class PlatformaticApp extends EventEmitter {
|
|
|
40
53
|
metricsConfig,
|
|
41
54
|
loggerConfig,
|
|
42
55
|
serverConfig,
|
|
56
|
+
worker: workerData?.worker,
|
|
43
57
|
hasManagementApi: !!hasManagementApi,
|
|
44
58
|
localServiceEnvVars: this.appConfig.localServiceEnvVars
|
|
45
59
|
}
|
package/lib/worker/itc.js
CHANGED
|
@@ -7,7 +7,7 @@ const { ITC } = require('@platformatic/itc')
|
|
|
7
7
|
const { Unpromise } = require('@watchable/unpromise')
|
|
8
8
|
|
|
9
9
|
const errors = require('../errors')
|
|
10
|
-
const { kITC, kId } = require('./symbols')
|
|
10
|
+
const { kITC, kId, kServiceId, kWorkerId } = require('./symbols')
|
|
11
11
|
|
|
12
12
|
async function safeHandleInITC (worker, fn) {
|
|
13
13
|
try {
|
|
@@ -23,7 +23,11 @@ async function safeHandleInITC (worker, fn) {
|
|
|
23
23
|
])
|
|
24
24
|
|
|
25
25
|
if (typeof exitCode === 'number') {
|
|
26
|
-
|
|
26
|
+
if (typeof worker[kWorkerId] !== 'undefined') {
|
|
27
|
+
throw new errors.WorkerExitedError(worker[kWorkerId], worker[kServiceId], exitCode)
|
|
28
|
+
} else {
|
|
29
|
+
throw new errors.ServiceExitedError(worker[kId], exitCode)
|
|
30
|
+
}
|
|
27
31
|
} else {
|
|
28
32
|
ac.abort()
|
|
29
33
|
}
|
|
@@ -156,6 +160,7 @@ function setupITC (app, service, dispatcher) {
|
|
|
156
160
|
itc.notify('changed')
|
|
157
161
|
})
|
|
158
162
|
|
|
163
|
+
itc.listen()
|
|
159
164
|
return itc
|
|
160
165
|
}
|
|
161
166
|
|
package/lib/worker/main.js
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
3
|
const { createRequire } = require('node:module')
|
|
4
|
+
const { hostname } = require('node:os')
|
|
4
5
|
const { join } = require('node:path')
|
|
5
6
|
const { parentPort, workerData, threadId } = require('node:worker_threads')
|
|
6
7
|
const { pathToFileURL } = require('node:url')
|
|
7
8
|
const inspector = require('node:inspector')
|
|
9
|
+
const diagnosticChannel = require('node:diagnostics_channel')
|
|
10
|
+
const { ServerResponse } = require('node:http')
|
|
8
11
|
|
|
9
12
|
const pino = require('pino')
|
|
10
13
|
const { fetch, setGlobalDispatcher, Agent } = require('undici')
|
|
@@ -28,14 +31,17 @@ globalThis.fetch = fetch
|
|
|
28
31
|
globalThis[kId] = threadId
|
|
29
32
|
|
|
30
33
|
let app
|
|
34
|
+
|
|
31
35
|
const config = workerData.config
|
|
32
36
|
globalThis.platformatic = Object.assign(globalThis.platformatic ?? {}, { logger: createLogger() })
|
|
33
37
|
|
|
34
38
|
function handleUnhandled (type, err) {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
+
const label =
|
|
40
|
+
workerData.worker.count > 1
|
|
41
|
+
? `worker ${workerData.worker.index} of the service "${workerData.serviceConfig.id}"`
|
|
42
|
+
: `service "${workerData.serviceConfig.id}"`
|
|
43
|
+
|
|
44
|
+
globalThis.platformatic.logger.error({ err: ensureLoggableError(err) }, `The ${label} threw an ${type}.`)
|
|
39
45
|
|
|
40
46
|
executeWithTimeout(app?.stop(), 1000)
|
|
41
47
|
.catch()
|
|
@@ -46,7 +52,13 @@ function handleUnhandled (type, err) {
|
|
|
46
52
|
|
|
47
53
|
function createLogger () {
|
|
48
54
|
const destination = new MessagePortWritable({ port: workerData.loggingPort })
|
|
49
|
-
const
|
|
55
|
+
const pinoOptions = { level: 'trace', name: workerData.serviceConfig.id }
|
|
56
|
+
|
|
57
|
+
if (typeof workerData.worker?.index !== 'undefined') {
|
|
58
|
+
pinoOptions.base = { pid: process.pid, hostname: hostname(), worker: workerData.worker.index }
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const loggerInstance = pino(pinoOptions, destination)
|
|
50
62
|
|
|
51
63
|
Reflect.defineProperty(process, 'stdout', { value: createPinoWritable(loggerInstance, 'info') })
|
|
52
64
|
Reflect.defineProperty(process, 'stderr', { value: createPinoWritable(loggerInstance, 'error') })
|
|
@@ -131,6 +143,7 @@ async function main () {
|
|
|
131
143
|
// Create the application
|
|
132
144
|
app = new PlatformaticApp(
|
|
133
145
|
service,
|
|
146
|
+
workerData.worker.count > 1 ? workerData.worker.index : undefined,
|
|
134
147
|
telemetryConfig,
|
|
135
148
|
config.logger,
|
|
136
149
|
serverConfig,
|
|
@@ -141,15 +154,70 @@ async function main () {
|
|
|
141
154
|
|
|
142
155
|
await app.init()
|
|
143
156
|
|
|
157
|
+
if (service.entrypoint && config.basePath) {
|
|
158
|
+
const meta = await app.stackable.getMeta()
|
|
159
|
+
if (!meta.wantsAbsoluteUrls) {
|
|
160
|
+
stripBasePath(config.basePath)
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
144
164
|
// Setup interaction with parent port
|
|
145
165
|
const itc = setupITC(app, service, threadDispatcher)
|
|
166
|
+
globalThis[kITC] = itc
|
|
146
167
|
|
|
147
168
|
// Get the dependencies
|
|
148
169
|
const dependencies = config.autoload ? await app.getBootstrapDependencies() : []
|
|
149
170
|
itc.notify('init', { dependencies })
|
|
150
|
-
|
|
171
|
+
}
|
|
151
172
|
|
|
152
|
-
|
|
173
|
+
function stripBasePath (basePath) {
|
|
174
|
+
const kBasePath = Symbol('kBasePath')
|
|
175
|
+
|
|
176
|
+
diagnosticChannel.subscribe('http.server.request.start', ({ request, response }) => {
|
|
177
|
+
if (request.url.startsWith(basePath)) {
|
|
178
|
+
request.url = request.url.slice(basePath.length)
|
|
179
|
+
|
|
180
|
+
if (request.url.charAt(0) !== '/') {
|
|
181
|
+
request.url = '/' + request.url
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
response[kBasePath] = basePath
|
|
185
|
+
}
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
const originWriteHead = ServerResponse.prototype.writeHead
|
|
189
|
+
const originSetHeader = ServerResponse.prototype.setHeader
|
|
190
|
+
|
|
191
|
+
ServerResponse.prototype.writeHead = function (statusCode, statusMessage, headers) {
|
|
192
|
+
if (this[kBasePath] !== undefined) {
|
|
193
|
+
if (headers === undefined && typeof statusMessage === 'object') {
|
|
194
|
+
headers = statusMessage
|
|
195
|
+
statusMessage = undefined
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (headers) {
|
|
199
|
+
for (const key in headers) {
|
|
200
|
+
if (
|
|
201
|
+
key.toLowerCase() === 'location' &&
|
|
202
|
+
!headers[key].startsWith(basePath)
|
|
203
|
+
) {
|
|
204
|
+
headers[key] = basePath + headers[key]
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return originWriteHead.call(this, statusCode, statusMessage, headers)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
ServerResponse.prototype.setHeader = function (name, value) {
|
|
214
|
+
if (this[kBasePath]) {
|
|
215
|
+
if (name.toLowerCase() === 'location' && !value.startsWith(basePath)) {
|
|
216
|
+
value = basePath + value
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
originSetHeader.call(this, name, value)
|
|
220
|
+
}
|
|
153
221
|
}
|
|
154
222
|
|
|
155
223
|
// No need to catch this because there is the unhadledRejection handler on top.
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
class RoundRobinMap extends Map {
|
|
4
|
+
#instances
|
|
5
|
+
|
|
6
|
+
constructor (iterable, instances) {
|
|
7
|
+
super(iterable)
|
|
8
|
+
this.#instances = instances
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
get configuration () {
|
|
12
|
+
return { ...this.#instances }
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// In development or for the entrypoint always use 1 worker
|
|
16
|
+
configure (services, defaultInstances, production) {
|
|
17
|
+
this.#instances = {}
|
|
18
|
+
|
|
19
|
+
for (const service of services) {
|
|
20
|
+
let count = service.workers ?? defaultInstances
|
|
21
|
+
|
|
22
|
+
if (service.entrypoint || !production) {
|
|
23
|
+
count = 1
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
this.#instances[service.id] = { next: 0, count }
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
getCount (service) {
|
|
31
|
+
return this.#instances[service].count
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
next (service) {
|
|
35
|
+
if (!this.#instances[service]) {
|
|
36
|
+
return undefined
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
let worker
|
|
40
|
+
let { next, count } = this.#instances[service]
|
|
41
|
+
|
|
42
|
+
// Try count times to get the next worker. This is to handle the case where a worker is being restarted.
|
|
43
|
+
for (let i = 0; i < count; i++) {
|
|
44
|
+
const current = next++
|
|
45
|
+
if (next >= count) {
|
|
46
|
+
next = 0
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
worker = this.get(`${service}:${current}`)
|
|
50
|
+
|
|
51
|
+
if (worker) {
|
|
52
|
+
break
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
this.#instances[service].next = next
|
|
57
|
+
return worker
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
module.exports = { RoundRobinMap }
|
package/lib/worker/symbols.js
CHANGED
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
const kConfig = Symbol.for('plt.runtime.config')
|
|
4
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 kServiceId = Symbol.for('plt.runtime.service.id')
|
|
6
|
+
const kWorkerId = Symbol.for('plt.runtime.worker.id')
|
|
5
7
|
const kITC = Symbol.for('plt.runtime.itc')
|
|
8
|
+
const kLoggerDestination = Symbol.for('plt.runtime.loggerDestination')
|
|
9
|
+
const kLoggingPort = Symbol.for('plt.runtime.logginPort')
|
|
10
|
+
const kWorkerStatus = Symbol('plt.runtime.worker.status')
|
|
6
11
|
|
|
7
|
-
module.exports = { kConfig, kId, kITC }
|
|
12
|
+
module.exports = { kConfig, kId, kServiceId, kWorkerId, kITC, kLoggerDestination, kLoggingPort, kWorkerStatus }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@platformatic/runtime",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.8.0-alpha.1",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
@@ -35,11 +35,12 @@
|
|
|
35
35
|
"typescript": "^5.5.4",
|
|
36
36
|
"undici-oidc-interceptor": "^0.5.0",
|
|
37
37
|
"why-is-node-running": "^2.2.2",
|
|
38
|
-
"@platformatic/composer": "2.
|
|
39
|
-
"@platformatic/db": "2.
|
|
40
|
-
"@platformatic/
|
|
41
|
-
"@platformatic/
|
|
42
|
-
"@platformatic/sql-graphql": "2.
|
|
38
|
+
"@platformatic/composer": "2.8.0-alpha.1",
|
|
39
|
+
"@platformatic/db": "2.8.0-alpha.1",
|
|
40
|
+
"@platformatic/node": "2.8.0-alpha.1",
|
|
41
|
+
"@platformatic/service": "2.8.0-alpha.1",
|
|
42
|
+
"@platformatic/sql-graphql": "2.8.0-alpha.1",
|
|
43
|
+
"@platformatic/sql-mapper": "2.8.0-alpha.1"
|
|
43
44
|
},
|
|
44
45
|
"dependencies": {
|
|
45
46
|
"@fastify/error": "^4.0.0",
|
|
@@ -70,17 +71,17 @@
|
|
|
70
71
|
"undici": "^6.9.0",
|
|
71
72
|
"undici-thread-interceptor": "^0.7.0",
|
|
72
73
|
"ws": "^8.16.0",
|
|
73
|
-
"@platformatic/basic": "2.
|
|
74
|
-
"@platformatic/
|
|
75
|
-
"@platformatic/
|
|
76
|
-
"@platformatic/
|
|
77
|
-
"@platformatic/
|
|
78
|
-
"@platformatic/
|
|
79
|
-
"@platformatic/utils": "2.
|
|
74
|
+
"@platformatic/basic": "2.8.0-alpha.1",
|
|
75
|
+
"@platformatic/generators": "2.8.0-alpha.1",
|
|
76
|
+
"@platformatic/config": "2.8.0-alpha.1",
|
|
77
|
+
"@platformatic/telemetry": "2.8.0-alpha.1",
|
|
78
|
+
"@platformatic/itc": "2.8.0-alpha.1",
|
|
79
|
+
"@platformatic/ts-compiler": "2.8.0-alpha.1",
|
|
80
|
+
"@platformatic/utils": "2.8.0-alpha.1"
|
|
80
81
|
},
|
|
81
82
|
"scripts": {
|
|
82
|
-
"test": "npm run lint && borp --concurrency=1 --timeout=
|
|
83
|
-
"coverage": "npm run lint && borp -X fixtures -X test -C --concurrency=1 --timeout=
|
|
83
|
+
"test": "npm run lint && borp --concurrency=1 --timeout=300000 && tsd",
|
|
84
|
+
"coverage": "npm run lint && borp -X fixtures -X test -C --concurrency=1 --timeout=300000 && tsd",
|
|
84
85
|
"gen-schema": "node lib/schema.js > schema.json",
|
|
85
86
|
"gen-types": "json2ts > config.d.ts < schema.json",
|
|
86
87
|
"build": "pnpm run gen-schema && pnpm run gen-types",
|
package/schema.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"$id": "https://schemas.platformatic.dev/@platformatic/runtime/2.
|
|
2
|
+
"$id": "https://schemas.platformatic.dev/@platformatic/runtime/2.8.0-alpha.1.json",
|
|
3
3
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
4
4
|
"type": "object",
|
|
5
5
|
"properties": {
|
|
@@ -13,6 +13,9 @@
|
|
|
13
13
|
"entrypoint": {
|
|
14
14
|
"type": "string"
|
|
15
15
|
},
|
|
16
|
+
"basePath": {
|
|
17
|
+
"type": "string"
|
|
18
|
+
},
|
|
16
19
|
"autoload": {
|
|
17
20
|
"type": "object",
|
|
18
21
|
"additionalProperties": false,
|
|
@@ -88,10 +91,19 @@
|
|
|
88
91
|
},
|
|
89
92
|
"useHttp": {
|
|
90
93
|
"type": "boolean"
|
|
94
|
+
},
|
|
95
|
+
"workers": {
|
|
96
|
+
"type": "number",
|
|
97
|
+
"minimum": 1
|
|
91
98
|
}
|
|
92
99
|
}
|
|
93
100
|
}
|
|
94
101
|
},
|
|
102
|
+
"workers": {
|
|
103
|
+
"type": "number",
|
|
104
|
+
"minimum": 1,
|
|
105
|
+
"default": 1
|
|
106
|
+
},
|
|
95
107
|
"web": {
|
|
96
108
|
"type": "array",
|
|
97
109
|
"items": {
|
|
@@ -126,6 +138,10 @@
|
|
|
126
138
|
},
|
|
127
139
|
"useHttp": {
|
|
128
140
|
"type": "boolean"
|
|
141
|
+
},
|
|
142
|
+
"workers": {
|
|
143
|
+
"type": "number",
|
|
144
|
+
"minimum": 1
|
|
129
145
|
}
|
|
130
146
|
}
|
|
131
147
|
}
|