@platformatic/runtime 3.29.1 → 3.31.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.
package/config.d.ts CHANGED
@@ -78,6 +78,18 @@ export type PlatformaticRuntimeConfig = {
78
78
  )[];
79
79
  [k: string]: unknown;
80
80
  };
81
+ compileCache?:
82
+ | boolean
83
+ | {
84
+ /**
85
+ * Enable Node.js module compile cache for faster startup
86
+ */
87
+ enabled?: boolean;
88
+ /**
89
+ * Directory to store compile cache. Defaults to .plt/compile-cache in app root
90
+ */
91
+ directory?: string;
92
+ };
81
93
  };
82
94
  };
83
95
  };
@@ -108,7 +120,7 @@ export type PlatformaticRuntimeConfig = {
108
120
  };
109
121
  workersRestartDelay?: number | string;
110
122
  logger?: {
111
- level: (
123
+ level?: (
112
124
  | ("fatal" | "error" | "warn" | "info" | "debug" | "trace" | "silent")
113
125
  | {
114
126
  [k: string]: unknown;
@@ -501,4 +513,16 @@ export type PlatformaticRuntimeConfig = {
501
513
  [k: string]: string | [string, ...string[]];
502
514
  };
503
515
  };
516
+ compileCache?:
517
+ | boolean
518
+ | {
519
+ /**
520
+ * Enable Node.js module compile cache for faster startup
521
+ */
522
+ enabled?: boolean;
523
+ /**
524
+ * Directory to store compile cache. Defaults to .plt/compile-cache in app root
525
+ */
526
+ directory?: string;
527
+ };
504
528
  };
package/lib/runtime.js CHANGED
@@ -10,6 +10,7 @@ import {
10
10
  parseMemorySize
11
11
  } from '@platformatic/foundation'
12
12
  import { ITC } from '@platformatic/itc'
13
+ import { client as metricsClient, collectProcessMetrics } from '@platformatic/metrics'
13
14
  import fastify from 'fastify'
14
15
  import { EventEmitter, once } from 'node:events'
15
16
  import { existsSync } from 'node:fs'
@@ -119,6 +120,8 @@ export class Runtime extends EventEmitter {
119
120
 
120
121
  #channelCreationHook
121
122
 
123
+ #processMetricsRegistry
124
+
122
125
  constructor (config, context) {
123
126
  super()
124
127
  this.setMaxListeners(MAX_LISTENERS_COUNT)
@@ -197,6 +200,14 @@ export class Runtime extends EventEmitter {
197
200
  this.#metricsLabelName = 'applicationId'
198
201
  }
199
202
 
203
+ // Initialize process-level metrics registry in the main thread if metrics or management API is enabled
204
+ // These metrics are the same across all workers and only need to be collected once
205
+ // We need this for management API as it can request metrics even without explicit metrics config
206
+ if (config.metrics || config.managementApi) {
207
+ this.#processMetricsRegistry = new metricsClient.Registry()
208
+ collectProcessMetrics(this.#processMetricsRegistry)
209
+ }
210
+
200
211
  // Create the logger
201
212
  const [logger, destination, context] = await createLogger(config)
202
213
  this.logger = logger
@@ -346,6 +357,12 @@ export class Runtime extends EventEmitter {
346
357
  await this.#prometheusServer.close()
347
358
  }
348
359
 
360
+ // Clean up process metrics registry
361
+ if (this.#processMetricsRegistry) {
362
+ this.#processMetricsRegistry.clear()
363
+ this.#processMetricsRegistry = null
364
+ }
365
+
349
366
  if (this.#sharedHttpCache?.close) {
350
367
  await this.#sharedHttpCache.close()
351
368
  }
@@ -710,6 +727,56 @@ export class Runtime extends EventEmitter {
710
727
  }
711
728
  }
712
729
 
730
+ /**
731
+ * Updates the metrics configuration at runtime without restarting the runtime or workers.
732
+ *
733
+ * This method allows you to:
734
+ * - Enable or disable metrics collection
735
+ * - Change Prometheus server settings (port, endpoint, authentication)
736
+ * - Update custom labels for metrics
737
+ *
738
+ * @example
739
+ * // Enable metrics with custom port
740
+ * await runtime.updateMetricsConfig({
741
+ * enabled: true,
742
+ * port: 9091,
743
+ * labels: { environment: 'production' }
744
+ * })
745
+ *
746
+ * // Disable metrics
747
+ * await runtime.updateMetricsConfig({ enabled: false })
748
+ */
749
+ async updateMetricsConfig (metricsConfig) {
750
+ if (this.#prometheusServer) {
751
+ await this.#prometheusServer.close()
752
+ this.#prometheusServer = null
753
+ }
754
+
755
+ this.#config.metrics = metricsConfig
756
+ this.#metricsLabelName = metricsConfig?.applicationLabel || 'applicationId'
757
+
758
+ if (metricsConfig.enabled !== false) {
759
+ this.#prometheusServer = await startPrometheusServer(this, metricsConfig)
760
+ }
761
+
762
+ const promises = []
763
+ for (const worker of this.#workers.values()) {
764
+ if (worker[kWorkerStatus] === 'started') {
765
+ promises.push(sendViaITC(worker, 'updateMetricsConfig', metricsConfig))
766
+ }
767
+ }
768
+
769
+ const results = await Promise.allSettled(promises)
770
+ for (const result of results) {
771
+ if (result.status === 'rejected') {
772
+ this.logger.error({ err: result.reason }, 'Cannot update metrics config on worker')
773
+ }
774
+ }
775
+
776
+ this.logger.info({ metricsConfig }, 'Metrics configuration updated')
777
+ return { success: true, config: metricsConfig }
778
+ }
779
+
713
780
  // TODO: Remove in next major version
714
781
  startCollectingMetrics () {
715
782
  this.logger.warn(
@@ -1009,6 +1076,12 @@ export class Runtime extends EventEmitter {
1009
1076
  async getMetrics (format = 'json') {
1010
1077
  let metrics = null
1011
1078
 
1079
+ // Get process-level metrics once from main thread registry (if available)
1080
+ let processMetricsJson = null
1081
+ if (this.#processMetricsRegistry) {
1082
+ processMetricsJson = await this.#processMetricsRegistry.getMetricsAsJSON()
1083
+ }
1084
+
1012
1085
  for (const worker of this.#workers.values()) {
1013
1086
  try {
1014
1087
  // The application might be temporarily unavailable
@@ -1016,6 +1089,7 @@ export class Runtime extends EventEmitter {
1016
1089
  continue
1017
1090
  }
1018
1091
 
1092
+ // Get thread-specific metrics from worker
1019
1093
  const applicationMetrics = await executeWithTimeout(
1020
1094
  sendViaITC(worker, 'getMetrics', format),
1021
1095
  this.#config.metrics?.timeout ?? 10000
@@ -1026,9 +1100,30 @@ export class Runtime extends EventEmitter {
1026
1100
  metrics = format === 'json' ? [] : ''
1027
1101
  }
1028
1102
 
1103
+ // Build worker labels including custom labels from metrics config
1104
+ const workerLabels = {
1105
+ ...this.#config.metrics?.labels,
1106
+ [this.#metricsLabelName]: worker[kApplicationId]
1107
+ }
1108
+ const workerId = worker[kWorkerId]
1109
+ if (workerId >= 0) {
1110
+ workerLabels.workerId = workerId
1111
+ }
1112
+
1029
1113
  if (format === 'json') {
1030
- metrics.push(...applicationMetrics)
1114
+ // Duplicate process metrics with worker labels and add to output
1115
+ if (processMetricsJson) {
1116
+ this.#applyLabelsToMetrics(processMetricsJson, workerLabels, metrics)
1117
+ }
1118
+ // Add worker's thread-specific metrics
1119
+ for (let i = 0; i < applicationMetrics.length; i++) {
1120
+ metrics.push(applicationMetrics[i])
1121
+ }
1031
1122
  } else {
1123
+ // Text format: format process metrics with worker labels
1124
+ if (processMetricsJson) {
1125
+ metrics += this.#formatProcessMetricsText(processMetricsJson, workerLabels)
1126
+ }
1032
1127
  metrics += applicationMetrics
1033
1128
  }
1034
1129
  }
@@ -1049,6 +1144,65 @@ export class Runtime extends EventEmitter {
1049
1144
  return { metrics }
1050
1145
  }
1051
1146
 
1147
+ // Apply labels to process metrics and push to output array (for JSON format)
1148
+ #applyLabelsToMetrics (processMetrics, labels, outputArray) {
1149
+ for (let i = 0; i < processMetrics.length; i++) {
1150
+ const metric = processMetrics[i]
1151
+ const newValues = []
1152
+ const values = metric.values
1153
+ for (let j = 0; j < values.length; j++) {
1154
+ const v = values[j]
1155
+ newValues.push({
1156
+ value: v.value,
1157
+ labels: { ...labels, ...v.labels },
1158
+ metricName: v.metricName
1159
+ })
1160
+ }
1161
+ outputArray.push({
1162
+ name: metric.name,
1163
+ help: metric.help,
1164
+ type: metric.type,
1165
+ aggregator: metric.aggregator,
1166
+ values: newValues
1167
+ })
1168
+ }
1169
+ }
1170
+
1171
+ // Format process metrics as Prometheus text format with labels
1172
+ #formatProcessMetricsText (processMetricsJson, labels) {
1173
+ let output = ''
1174
+
1175
+ for (let i = 0; i < processMetricsJson.length; i++) {
1176
+ const metric = processMetricsJson[i]
1177
+ const name = metric.name
1178
+ const help = metric.help
1179
+ const type = metric.type
1180
+
1181
+ // Add HELP and TYPE lines
1182
+ output += `# HELP ${name} ${help}\n`
1183
+ output += `# TYPE ${name} ${type}\n`
1184
+
1185
+ const values = metric.values
1186
+ for (let j = 0; j < values.length; j++) {
1187
+ const v = values[j]
1188
+ const combinedLabels = { ...labels, ...v.labels }
1189
+ const labelParts = []
1190
+
1191
+ for (const [key, val] of Object.entries(combinedLabels)) {
1192
+ // Escape label values for Prometheus format
1193
+ const escapedVal = String(val).replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n')
1194
+ labelParts.push(`${key}="${escapedVal}"`)
1195
+ }
1196
+
1197
+ const labelStr = labelParts.length > 0 ? `{${labelParts.join(',')}}` : ''
1198
+ const metricName = v.metricName || name
1199
+ output += `${metricName}${labelStr} ${v.value}\n`
1200
+ }
1201
+ }
1202
+
1203
+ return output
1204
+ }
1205
+
1052
1206
  async getFormattedMetrics () {
1053
1207
  try {
1054
1208
  const { metrics } = await this.getMetrics()
@@ -1514,7 +1668,8 @@ export class Runtime extends EventEmitter {
1514
1668
  codeRangeSizeMb
1515
1669
  },
1516
1670
  stdout: true,
1517
- stderr: true
1671
+ stderr: true,
1672
+ name: workerId
1518
1673
  })
1519
1674
 
1520
1675
  this.#handleWorkerStandardStreams(worker, applicationId, index)
@@ -92,6 +92,13 @@ export class Controller extends EventEmitter {
92
92
  }
93
93
  }
94
94
 
95
+ async updateMetricsConfig (metricsConfig) {
96
+ this.#context.metricsConfig = metricsConfig
97
+ if (this.capability && typeof this.capability.updateMetricsConfig === 'function') {
98
+ await this.capability.updateMetricsConfig(metricsConfig)
99
+ }
100
+ }
101
+
95
102
  // Note: capability's init() is executed within start
96
103
  async init () {
97
104
  try {
package/lib/worker/itc.js CHANGED
@@ -2,8 +2,8 @@ import { ensureLoggableError, executeInParallel, executeWithTimeout, kTimeout }
2
2
  import { ITC } from '@platformatic/itc'
3
3
  import { Unpromise } from '@watchable/unpromise'
4
4
  import { once } from 'node:events'
5
- import repl from 'node:repl'
6
5
  import { Duplex } from 'node:stream'
6
+ import { createRequire } from 'node:module'
7
7
  import { parentPort, workerData } from 'node:worker_threads'
8
8
  import {
9
9
  ApplicationExitedError,
@@ -176,6 +176,13 @@ export function setupITC (controller, application, dispatcher, sharedContext) {
176
176
  await updateUndiciInterceptors(undiciConfig)
177
177
  },
178
178
 
179
+ async updateMetricsConfig (metricsConfig) {
180
+ if (controller && typeof controller.updateMetricsConfig === 'function') {
181
+ await controller.updateMetricsConfig(metricsConfig)
182
+ }
183
+ return { success: true }
184
+ },
185
+
179
186
  async updateWorkersCount (data) {
180
187
  const { workers } = data
181
188
  workerData.applicationConfig.workers = workers
@@ -266,6 +273,14 @@ export function setupITC (controller, application, dispatcher, sharedContext) {
266
273
  },
267
274
 
268
275
  startRepl (port) {
276
+ // We are loading the repl module dynamically here to avoid loading it
277
+ // when not needed (since it pulls in domain, which is quite expensive
278
+ // as it monkey patches EventEmitter).
279
+ // We must use local require() instead of import
280
+ // because dynamic import() is async and the
281
+ // startRepl handler is sync.
282
+ const repl = createRequire(import.meta.url)('node:repl')
283
+
269
284
  // Create a duplex stream that wraps the MessagePort
270
285
  const replStream = new Duplex({
271
286
  read () {},
@@ -9,7 +9,7 @@ import { EventEmitter } from 'node:events'
9
9
  import { ServerResponse } from 'node:http'
10
10
  import inspector from 'node:inspector'
11
11
  import { hostname } from 'node:os'
12
- import { resolve } from 'node:path'
12
+ import { join, resolve } from 'node:path'
13
13
  import { pathToFileURL } from 'node:url'
14
14
  import { threadId, workerData } from 'node:worker_threads'
15
15
  import pino from 'pino'
@@ -85,6 +85,57 @@ async function performPreloading (...sources) {
85
85
  }
86
86
  }
87
87
 
88
+ // Enable compile cache if configured (Node.js 22.1.0+)
89
+ async function setupCompileCache (runtimeConfig, applicationConfig, logger) {
90
+ // Normalize boolean shorthand: true -> { enabled: true }
91
+ const normalizeConfig = cfg => {
92
+ if (cfg === true) return { enabled: true }
93
+ if (cfg === false) return { enabled: false }
94
+ return cfg
95
+ }
96
+
97
+ // Merge runtime and app-level config (app overrides runtime)
98
+ const runtimeCache = normalizeConfig(runtimeConfig.compileCache)
99
+ const appCache = normalizeConfig(applicationConfig.compileCache)
100
+ const config = { ...runtimeCache, ...appCache }
101
+
102
+ if (!config.enabled) {
103
+ return
104
+ }
105
+
106
+ // Check if API is available (Node.js 22.1.0+)
107
+ let moduleApi
108
+ try {
109
+ moduleApi = await import('node:module')
110
+ if (typeof moduleApi.enableCompileCache !== 'function') {
111
+ return
112
+ }
113
+ } catch {
114
+ return
115
+ }
116
+
117
+ // Determine cache directory - use applicationConfig.path for the app root
118
+ const cacheDir = config.directory ?? join(applicationConfig.path, '.plt', 'compile-cache')
119
+
120
+ try {
121
+ const result = moduleApi.enableCompileCache(cacheDir)
122
+
123
+ const { compileCacheStatus } = moduleApi.constants ?? {}
124
+
125
+ if (result.status === compileCacheStatus?.ENABLED) {
126
+ logger.debug({ directory: result.directory }, 'Module compile cache enabled')
127
+ } else if (result.status === compileCacheStatus?.ALREADY_ENABLED) {
128
+ logger.debug({ directory: result.directory }, 'Module compile cache already enabled')
129
+ } else if (result.status === compileCacheStatus?.FAILED) {
130
+ logger.warn({ message: result.message }, 'Failed to enable module compile cache')
131
+ } else if (result.status === compileCacheStatus?.DISABLED) {
132
+ logger.debug('Module compile cache disabled via NODE_DISABLE_COMPILE_CACHE')
133
+ }
134
+ } catch (err) {
135
+ logger.warn({ err }, 'Error enabling module compile cache')
136
+ }
137
+ }
138
+
88
139
  async function main () {
89
140
  globalThis.fetch = fetch
90
141
  globalThis[kId] = threadId
@@ -94,10 +145,12 @@ async function main () {
94
145
  })
95
146
 
96
147
  const runtimeConfig = workerData.config
148
+ const applicationConfig = workerData.applicationConfig
97
149
 
98
- await performPreloading(runtimeConfig, workerData.applicationConfig)
150
+ // Enable compile cache early before loading user modules
151
+ await setupCompileCache(runtimeConfig, applicationConfig, globalThis.platformatic.logger)
99
152
 
100
- const applicationConfig = workerData.applicationConfig
153
+ await performPreloading(runtimeConfig, applicationConfig)
101
154
 
102
155
  // Load env file and mixin env vars from application config
103
156
  let envfile
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@platformatic/runtime",
3
- "version": "3.29.1",
3
+ "version": "3.31.0",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -35,14 +35,14 @@
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": "3.29.1",
39
- "@platformatic/db": "3.29.1",
40
- "@platformatic/node": "3.29.1",
41
- "@platformatic/gateway": "3.29.1",
42
- "@platformatic/service": "3.29.1",
43
- "@platformatic/sql-graphql": "3.29.1",
44
- "@platformatic/sql-mapper": "3.29.1",
45
- "@platformatic/wattpm-pprof-capture": "3.29.1"
38
+ "@platformatic/composer": "3.31.0",
39
+ "@platformatic/db": "3.31.0",
40
+ "@platformatic/gateway": "3.31.0",
41
+ "@platformatic/node": "3.31.0",
42
+ "@platformatic/sql-graphql": "3.31.0",
43
+ "@platformatic/service": "3.31.0",
44
+ "@platformatic/wattpm-pprof-capture": "3.31.0",
45
+ "@platformatic/sql-mapper": "3.31.0"
46
46
  },
47
47
  "dependencies": {
48
48
  "@fastify/accepts": "^5.0.0",
@@ -58,7 +58,7 @@
58
58
  "cron": "^4.1.0",
59
59
  "debounce": "^2.0.0",
60
60
  "fastest-levenshtein": "^1.0.16",
61
- "fastify": "^5.0.0",
61
+ "fastify": "^5.7.0",
62
62
  "graphql": "^16.8.1",
63
63
  "help-me": "^5.0.0",
64
64
  "minimist": "^1.2.8",
@@ -71,12 +71,12 @@
71
71
  "undici": "^7.0.0",
72
72
  "undici-thread-interceptor": "^1.0.0",
73
73
  "ws": "^8.16.0",
74
- "@platformatic/basic": "3.29.1",
75
- "@platformatic/generators": "3.29.1",
76
- "@platformatic/itc": "3.29.1",
77
- "@platformatic/foundation": "3.29.1",
78
- "@platformatic/metrics": "3.29.1",
79
- "@platformatic/telemetry": "3.29.1"
74
+ "@platformatic/generators": "3.31.0",
75
+ "@platformatic/basic": "3.31.0",
76
+ "@platformatic/foundation": "3.31.0",
77
+ "@platformatic/itc": "3.31.0",
78
+ "@platformatic/metrics": "3.31.0",
79
+ "@platformatic/telemetry": "3.31.0"
80
80
  },
81
81
  "engines": {
82
82
  "node": ">=22.19.0"
package/schema.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "$id": "https://schemas.platformatic.dev/@platformatic/runtime/3.29.1.json",
2
+ "$id": "https://schemas.platformatic.dev/@platformatic/runtime/3.31.0.json",
3
3
  "$schema": "http://json-schema.org/draft-07/schema#",
4
4
  "title": "Platformatic Runtime Config",
5
5
  "type": "object",
@@ -332,6 +332,28 @@
332
332
  }
333
333
  }
334
334
  }
335
+ },
336
+ "compileCache": {
337
+ "anyOf": [
338
+ {
339
+ "type": "boolean"
340
+ },
341
+ {
342
+ "type": "object",
343
+ "properties": {
344
+ "enabled": {
345
+ "type": "boolean",
346
+ "default": true,
347
+ "description": "Enable Node.js module compile cache for faster startup"
348
+ },
349
+ "directory": {
350
+ "type": "string",
351
+ "description": "Directory to store compile cache. Defaults to .plt/compile-cache in app root"
352
+ }
353
+ },
354
+ "additionalProperties": false
355
+ }
356
+ ]
335
357
  }
336
358
  }
337
359
  }
@@ -646,6 +668,28 @@
646
668
  }
647
669
  }
648
670
  }
671
+ },
672
+ "compileCache": {
673
+ "anyOf": [
674
+ {
675
+ "type": "boolean"
676
+ },
677
+ {
678
+ "type": "object",
679
+ "properties": {
680
+ "enabled": {
681
+ "type": "boolean",
682
+ "default": true,
683
+ "description": "Enable Node.js module compile cache for faster startup"
684
+ },
685
+ "directory": {
686
+ "type": "string",
687
+ "description": "Directory to store compile cache. Defaults to .plt/compile-cache in app root"
688
+ }
689
+ },
690
+ "additionalProperties": false
691
+ }
692
+ ]
649
693
  }
650
694
  }
651
695
  }
@@ -958,6 +1002,28 @@
958
1002
  }
959
1003
  }
960
1004
  }
1005
+ },
1006
+ "compileCache": {
1007
+ "anyOf": [
1008
+ {
1009
+ "type": "boolean"
1010
+ },
1011
+ {
1012
+ "type": "object",
1013
+ "properties": {
1014
+ "enabled": {
1015
+ "type": "boolean",
1016
+ "default": true,
1017
+ "description": "Enable Node.js module compile cache for faster startup"
1018
+ },
1019
+ "directory": {
1020
+ "type": "string",
1021
+ "description": "Directory to store compile cache. Defaults to .plt/compile-cache in app root"
1022
+ }
1023
+ },
1024
+ "additionalProperties": false
1025
+ }
1026
+ ]
961
1027
  }
962
1028
  }
963
1029
  }
@@ -1270,6 +1336,28 @@
1270
1336
  }
1271
1337
  }
1272
1338
  }
1339
+ },
1340
+ "compileCache": {
1341
+ "anyOf": [
1342
+ {
1343
+ "type": "boolean"
1344
+ },
1345
+ {
1346
+ "type": "object",
1347
+ "properties": {
1348
+ "enabled": {
1349
+ "type": "boolean",
1350
+ "default": true,
1351
+ "description": "Enable Node.js module compile cache for faster startup"
1352
+ },
1353
+ "directory": {
1354
+ "type": "string",
1355
+ "description": "Directory to store compile cache. Defaults to .plt/compile-cache in app root"
1356
+ }
1357
+ },
1358
+ "additionalProperties": false
1359
+ }
1360
+ ]
1273
1361
  }
1274
1362
  }
1275
1363
  }
@@ -1349,7 +1437,6 @@
1349
1437
  "properties": {
1350
1438
  "level": {
1351
1439
  "type": "string",
1352
- "default": "info",
1353
1440
  "oneOf": [
1354
1441
  {
1355
1442
  "enum": [
@@ -1496,9 +1583,6 @@
1496
1583
  "default": true
1497
1584
  }
1498
1585
  },
1499
- "required": [
1500
- "level"
1501
- ],
1502
1586
  "default": {},
1503
1587
  "additionalProperties": true
1504
1588
  },
@@ -2570,6 +2654,28 @@
2570
2654
  "deny"
2571
2655
  ],
2572
2656
  "additionalProperties": false
2657
+ },
2658
+ "compileCache": {
2659
+ "anyOf": [
2660
+ {
2661
+ "type": "boolean"
2662
+ },
2663
+ {
2664
+ "type": "object",
2665
+ "properties": {
2666
+ "enabled": {
2667
+ "type": "boolean",
2668
+ "default": true,
2669
+ "description": "Enable Node.js module compile cache for faster startup"
2670
+ },
2671
+ "directory": {
2672
+ "type": "string",
2673
+ "description": "Directory to store compile cache. Defaults to .plt/compile-cache in app root"
2674
+ }
2675
+ },
2676
+ "additionalProperties": false
2677
+ }
2678
+ ]
2573
2679
  }
2574
2680
  },
2575
2681
  "anyOf": [