@platformatic/basic 2.22.0 → 2.24.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/index.js CHANGED
@@ -1,6 +1,6 @@
1
+ import { createRequire } from '@platformatic/utils'
1
2
  import { existsSync } from 'node:fs'
2
3
  import { readFile } from 'node:fs/promises'
3
- import { createRequire } from 'node:module'
4
4
  import { relative, resolve } from 'node:path'
5
5
  import { workerData } from 'node:worker_threads'
6
6
  import pino from 'pino'
@@ -37,7 +37,7 @@ function isImportFailedError (error, pkg) {
37
37
  return match?.[1] === pkg || error.requireStack?.[0].endsWith(importStackablePackageMarker)
38
38
  }
39
39
 
40
- async function importStackablePackage (opts, pkg, autodetectDescription) {
40
+ async function importStackablePackage (opts, pkg) {
41
41
  try {
42
42
  try {
43
43
  // Try regular import
@@ -105,7 +105,7 @@ async function buildStackable (opts) {
105
105
  toImport = '@platformatic/astro'
106
106
  }
107
107
 
108
- const imported = await importStackablePackage(opts, toImport, autodetectDescription)
108
+ const imported = await importStackablePackage(opts, toImport)
109
109
 
110
110
  const serviceRoot = relative(process.cwd(), opts.context.directory)
111
111
  if (
package/lib/base.js CHANGED
@@ -1,4 +1,4 @@
1
- import { collectMetrics } from '@platformatic/metrics'
1
+ import { client, collectMetrics } from '@platformatic/metrics'
2
2
  import { deepmerge, executeWithTimeout } from '@platformatic/utils'
3
3
  import { parseCommandString } from 'execa'
4
4
  import { spawn } from 'node:child_process'
@@ -19,6 +19,7 @@ export class BaseStackable {
19
19
  childManager
20
20
  subprocess
21
21
  #subprocessStarted
22
+ #metricsCollected
22
23
 
23
24
  constructor (type, version, options, root, configManager) {
24
25
  options.context.worker ??= { count: 1, index: 0 }
@@ -38,7 +39,8 @@ export class BaseStackable {
38
39
  this.basePath = null
39
40
  this.isEntrypoint = options.context.isEntrypoint
40
41
  this.isProduction = options.context.isProduction
41
- this.metricsRegistry = null
42
+ this.metricsRegistry = new client.Registry()
43
+ this.#metricsCollected = false
42
44
  this.startHttpTimer = null
43
45
  this.endHttpTimer = null
44
46
  this.clientWs = null
@@ -71,7 +73,8 @@ export class BaseStackable {
71
73
  setConnectionString: this.setConnectionString.bind(this),
72
74
  setBasePath: this.setBasePath.bind(this),
73
75
  runtimeBasePath: this.runtimeConfig?.basePath ?? null,
74
- invalidateHttpCache: this.#invalidateHttpCache.bind(this)
76
+ invalidateHttpCache: this.#invalidateHttpCache.bind(this),
77
+ prometheus: { client, registry: this.metricsRegistry }
75
78
  })
76
79
  }
77
80
 
@@ -263,7 +266,7 @@ export class BaseStackable {
263
266
 
264
267
  this.#subprocessStarted = true
265
268
  } catch (e) {
266
- this.childManager.close('SIGKILL')
269
+ this.childManager.close()
267
270
  throw new Error(`Cannot execute command "${command}": executable not found`)
268
271
  } finally {
269
272
  await this.childManager.eject()
@@ -272,7 +275,7 @@ export class BaseStackable {
272
275
  // If the process exits prematurely, terminate the thread with the same code
273
276
  this.subprocess.on('exit', code => {
274
277
  if (this.#subprocessStarted && typeof code === 'number' && code !== 0) {
275
- this.childManager.close('SIGKILL')
278
+ this.childManager.close()
276
279
  process.exit(code)
277
280
  }
278
281
  })
@@ -280,10 +283,12 @@ export class BaseStackable {
280
283
  const [url, clientWs] = await once(this.childManager, 'url')
281
284
  this.url = url
282
285
  this.clientWs = clientWs
286
+
287
+ await this._collectMetrics()
283
288
  }
284
289
 
285
290
  async stopCommand () {
286
- const exitTimeout = this.runtimeConfig.gracefulShutdown.runtime
291
+ const exitTimeout = this.runtimeConfig.gracefulShutdown.service
287
292
 
288
293
  this.#subprocessStarted = false
289
294
  const exitPromise = once(this.subprocess, 'exit')
@@ -307,7 +312,7 @@ export class BaseStackable {
307
312
  await exitPromise
308
313
 
309
314
  // Close the manager
310
- this.childManager.close()
315
+ await this.childManager.close()
311
316
  }
312
317
 
313
318
  getChildManager () {
@@ -323,7 +328,16 @@ export class BaseStackable {
323
328
  : spawn(executable, args, { cwd: this.root })
324
329
  }
325
330
 
326
- async collectMetrics () {
331
+ async _collectMetrics () {
332
+ if (this.#metricsCollected) {
333
+ return
334
+ }
335
+
336
+ this.#metricsCollected = true
337
+ await this.#collectMetrics()
338
+ }
339
+
340
+ async #collectMetrics () {
327
341
  let metricsConfig = this.options.context.metricsConfig
328
342
  if (metricsConfig !== false) {
329
343
  metricsConfig = {
@@ -341,13 +355,13 @@ export class BaseStackable {
341
355
  return
342
356
  }
343
357
 
344
- const { registry, startHttpTimer, endHttpTimer } = await collectMetrics(
358
+ const { startHttpTimer, endHttpTimer } = await collectMetrics(
345
359
  this.serviceId,
346
360
  this.workerId,
347
- metricsConfig
361
+ metricsConfig,
362
+ this.metricsRegistry
348
363
  )
349
364
 
350
- this.metricsRegistry = registry
351
365
  this.startHttpTimer = startHttpTimer
352
366
  this.endHttpTimer = endHttpTimer
353
367
  }
@@ -358,8 +372,6 @@ export class BaseStackable {
358
372
  return this.childManager.send(this.clientWs, 'getMetrics', { format })
359
373
  }
360
374
 
361
- if (!this.metricsRegistry) return null
362
-
363
375
  return format === 'json' ? await this.metricsRegistry.getMetricsAsJSON() : await this.metricsRegistry.metrics()
364
376
  }
365
377
 
package/lib/utils.js CHANGED
@@ -1,4 +1,4 @@
1
- import { createRequire } from 'node:module'
1
+ import { createRequire } from '@platformatic/utils'
2
2
  import { pathToFileURL } from 'node:url'
3
3
  import { request } from 'undici'
4
4
 
@@ -1,9 +1,9 @@
1
1
  import { ITC } from '@platformatic/itc'
2
- import { createDirectory, ensureLoggableError } from '@platformatic/utils'
2
+ import { createDirectory, createRequire, ensureLoggableError } from '@platformatic/utils'
3
3
  import { once } from 'node:events'
4
4
  import { rm, writeFile } from 'node:fs/promises'
5
5
  import { createServer } from 'node:http'
6
- import { createRequire, register } from 'node:module'
6
+ import { register } from 'node:module'
7
7
  import { platform, tmpdir } from 'node:os'
8
8
  import { dirname, join, resolve } from 'node:path'
9
9
  import { pathToFileURL } from 'node:url'
@@ -43,6 +43,7 @@ export class ChildManager extends ITC {
43
43
  #scripts
44
44
  #logger
45
45
  #server
46
+ #websocketServer
46
47
  #socketPath
47
48
  #clients
48
49
  #requests
@@ -93,9 +94,9 @@ export class ChildManager extends ITC {
93
94
  await createDirectory(dirname(this.#socketPath))
94
95
  }
95
96
 
96
- const wssServer = new WebSocketServer({ server: this.#server })
97
+ this.#websocketServer = new WebSocketServer({ server: this.#server })
97
98
 
98
- wssServer.on('connection', ws => {
99
+ this.#websocketServer.on('connection', ws => {
99
100
  this.#clients.add(ws)
100
101
 
101
102
  ws.on('message', raw => {
@@ -135,8 +136,14 @@ export class ChildManager extends ITC {
135
136
  await rm(this.#dataPath, { force: true })
136
137
  }
137
138
 
138
- this.#server?.close()
139
- super.close()
139
+ for (const client of this.#clients) {
140
+ client.close()
141
+ await once(client, 'close')
142
+ }
143
+
144
+ await this.#closeServer(this.#websocketServer)
145
+ await this.#closeServer(this.#server)
146
+ await super.close()
140
147
  }
141
148
 
142
149
  async inject () {
@@ -252,4 +259,22 @@ export class ChildManager extends ITC {
252
259
  this.#logger.error({ err: ensureLoggableError(error) }, message)
253
260
  process.exit(exitCode)
254
261
  }
262
+
263
+ #closeServer (server) {
264
+ return new Promise((resolve, reject) => {
265
+ if (!server || server.listening === false) {
266
+ resolve()
267
+ return
268
+ }
269
+
270
+ server.close(err => {
271
+ if (err) {
272
+ reject(err)
273
+ return
274
+ }
275
+
276
+ resolve()
277
+ })
278
+ })
279
+ }
255
280
  }
@@ -1,6 +1,6 @@
1
1
  import { ITC } from '@platformatic/itc'
2
- import { collectMetrics } from '@platformatic/metrics'
3
- import { createPinoWritable, ensureLoggableError } from '@platformatic/utils'
2
+ import { client, collectMetrics } from '@platformatic/metrics'
3
+ import { createPinoWritable, ensureLoggableError, features } from '@platformatic/utils'
4
4
  import diagnosticChannel, { tracingChannel } from 'node:diagnostics_channel'
5
5
  import { EventEmitter, once } from 'node:events'
6
6
  import { readFile } from 'node:fs/promises'
@@ -105,6 +105,7 @@ export class ChildProcess extends ITC {
105
105
  const protocol = platform() === 'win32' ? 'ws+unix:' : 'ws+unix://'
106
106
  this.#socket = new WebSocket(`${protocol}${getSocketPath(process.env.PLT_MANAGER_ID)}`)
107
107
  this.#pendingMessages = []
108
+ this.#metricsRegistry = new client.Registry()
108
109
 
109
110
  this.listen()
110
111
  this.#setupLogger()
@@ -124,6 +125,7 @@ export class ChildProcess extends ITC {
124
125
  setGraphqlSchema: this.setGraphqlSchema.bind(this),
125
126
  setConnectionString: this.setConnectionString.bind(this),
126
127
  setBasePath: this.setBasePath.bind(this),
128
+ prometheus: { client, registry: this.#metricsRegistry }
127
129
  })
128
130
  }
129
131
 
@@ -177,13 +179,10 @@ export class ChildProcess extends ITC {
177
179
  }
178
180
 
179
181
  async #collectMetrics ({ serviceId, workerId, metricsConfig }) {
180
- const { registry } = await collectMetrics(serviceId, workerId, metricsConfig)
181
- this.#metricsRegistry = registry
182
+ await collectMetrics(serviceId, workerId, metricsConfig, this.#metricsRegistry)
182
183
  }
183
184
 
184
185
  async #getMetrics ({ format } = {}) {
185
- if (!this.#metricsRegistry) return null
186
-
187
186
  const res =
188
187
  format === 'json' ? await this.#metricsRegistry.getMetricsAsJSON() : await this.#metricsRegistry.metrics()
189
188
 
@@ -232,8 +231,14 @@ export class ChildProcess extends ITC {
232
231
  const host = globalThis.platformatic.host
233
232
 
234
233
  if (port !== false) {
235
- options.port = typeof port === 'number' ? port : 0
234
+ const hasFixedPort = typeof port === 'number'
235
+ options.port = hasFixedPort ? port : 0
236
+
237
+ if (hasFixedPort && features.node.reusePort) {
238
+ options.reusePort = true
239
+ }
236
240
  }
241
+
237
242
  if (typeof host === 'string') {
238
243
  options.host = host
239
244
  }
@@ -1,5 +1,12 @@
1
- import { withResolvers } from '@platformatic/utils'
1
+ import { features, withResolvers } from '@platformatic/utils'
2
2
  import { subscribe, tracingChannel, unsubscribe } from 'node:diagnostics_channel'
3
+ import { AsyncLocalStorage } from 'node:async_hooks'
4
+
5
+ const snapshots = new WeakMap()
6
+
7
+ export function getAsyncLocalStorageSnapshot (server) {
8
+ return snapshots.get(server)
9
+ }
3
10
 
4
11
  export function createServerListener (overridePort = true, overrideHost) {
5
12
  const { promise, resolve, reject } = withResolvers()
@@ -12,14 +19,21 @@ export function createServerListener (overridePort = true, overrideHost) {
12
19
  }
13
20
 
14
21
  if (overridePort !== false) {
15
- options.port = typeof overridePort === 'number' ? overridePort : 0
22
+ const hasFixedPort = typeof overridePort === 'number'
23
+ options.port = hasFixedPort ? overridePort : 0
24
+
25
+ if (hasFixedPort && features.node.reusePort) {
26
+ options.reusePort = true
27
+ }
16
28
  }
29
+
17
30
  if (typeof overrideHost === 'string') {
18
31
  options.host = overrideHost
19
32
  }
20
33
  },
21
34
  asyncEnd ({ server }) {
22
35
  cancel()
36
+ snapshots.set(server, AsyncLocalStorage.snapshot())
23
37
  resolve(server)
24
38
  },
25
39
  error ({ error }) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@platformatic/basic",
3
- "version": "2.22.0",
3
+ "version": "2.24.0",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -23,19 +23,20 @@
23
23
  "split2": "^4.2.0",
24
24
  "undici": "^7.0.0",
25
25
  "ws": "^8.18.0",
26
- "@platformatic/config": "2.22.0",
27
- "@platformatic/itc": "2.22.0",
28
- "@platformatic/metrics": "2.22.0",
29
- "@platformatic/utils": "2.22.0",
30
- "@platformatic/telemetry": "2.22.0"
26
+ "@platformatic/config": "2.24.0",
27
+ "@platformatic/itc": "2.24.0",
28
+ "@platformatic/telemetry": "2.24.0",
29
+ "@platformatic/metrics": "2.24.0",
30
+ "@platformatic/utils": "2.24.0"
31
31
  },
32
32
  "devDependencies": {
33
33
  "borp": "^0.19.0",
34
34
  "eslint": "9",
35
35
  "express": "^4.19.2",
36
36
  "fastify": "^5.0.0",
37
+ "get-port": "^7.1.0",
37
38
  "json-schema-to-typescript": "^15.0.0",
38
- "neostandard": "^0.11.1",
39
+ "neostandard": "^0.12.0",
39
40
  "next": "^15.0.0",
40
41
  "react": "^18.3.1",
41
42
  "react-dom": "^18.3.1",
package/schema.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "$id": "https://schemas.platformatic.dev/@platformatic/basic/2.22.0.json",
2
+ "$id": "https://schemas.platformatic.dev/@platformatic/basic/2.24.0.json",
3
3
  "$schema": "http://json-schema.org/draft-07/schema#",
4
4
  "title": "Platformatic Stackable",
5
5
  "type": "object",