@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,302 @@
1
+ import {
2
+ ensureLoggableError,
3
+ FileWatcher,
4
+ kHandledError,
5
+ listRecognizedConfigurationFiles,
6
+ loadConfiguration,
7
+ loadConfigurationModule
8
+ } from '@platformatic/foundation'
9
+ import debounce from 'debounce'
10
+ import { EventEmitter } from 'node:events'
11
+ import { existsSync } from 'node:fs'
12
+ import { resolve } from 'node:path'
13
+ import { getActiveResourcesInfo } from 'node:process'
14
+ import { workerData } from 'node:worker_threads'
15
+ import { getGlobalDispatcher, setGlobalDispatcher } from 'undici'
16
+ import { ApplicationAlreadyStartedError, RuntimeNotStartedError } from '../errors.js'
17
+ import { getApplicationUrl } from '../utils.js'
18
+
19
+ function fetchApplicationUrl (application, key) {
20
+ if (!key.endsWith('_URL') || !application.id) {
21
+ return null
22
+ }
23
+
24
+ return getApplicationUrl(application.id)
25
+ }
26
+
27
+ export class Controller extends EventEmitter {
28
+ #starting
29
+ #started
30
+ #listening
31
+ #watch
32
+ #fileWatcher
33
+ #debouncedRestart
34
+ #context
35
+ #lastELU
36
+
37
+ constructor (
38
+ appConfig,
39
+ workerId,
40
+ telemetryConfig,
41
+ loggerConfig,
42
+ serverConfig,
43
+ metricsConfig,
44
+ hasManagementApi,
45
+ watch
46
+ ) {
47
+ super()
48
+ this.appConfig = appConfig
49
+ this.applicationId = this.appConfig.id
50
+ this.workerId = workerId
51
+ this.#watch = watch
52
+ this.#starting = false
53
+ this.#started = false
54
+ this.#listening = false
55
+ this.capability = null
56
+ this.#fileWatcher = null
57
+ this.#lastELU = performance.eventLoopUtilization()
58
+
59
+ this.#context = {
60
+ controller: this,
61
+ applicationId: this.applicationId,
62
+ workerId: this.workerId,
63
+ directory: this.appConfig.path,
64
+ dependencies: this.appConfig.dependencies,
65
+ isEntrypoint: this.appConfig.entrypoint,
66
+ isProduction: this.appConfig.isProduction,
67
+ telemetryConfig,
68
+ metricsConfig,
69
+ loggerConfig,
70
+ serverConfig,
71
+ worker: workerData?.worker,
72
+ hasManagementApi: !!hasManagementApi,
73
+ fetchApplicationUrl: fetchApplicationUrl.bind(null, appConfig)
74
+ }
75
+ }
76
+
77
+ getStatus () {
78
+ if (this.#starting) return 'starting'
79
+ if (this.#started) return 'started'
80
+ return 'stopped'
81
+ }
82
+
83
+ async updateContext (context) {
84
+ this.#context = { ...this.#context, ...context }
85
+ if (this.capability) {
86
+ await this.capability.updateContext(context)
87
+ }
88
+ }
89
+
90
+ // Note: capability's init() is executed within start
91
+ async init () {
92
+ try {
93
+ const appConfig = this.appConfig
94
+
95
+ if (appConfig.isProduction && !process.env.NODE_ENV) {
96
+ process.env.NODE_ENV = 'production'
97
+ }
98
+
99
+ // Before returning the base application, check if there is any file we recognize
100
+ // and the user just forgot to specify in the configuration.
101
+ if (!appConfig.config) {
102
+ const candidate = listRecognizedConfigurationFiles().find(f => existsSync(resolve(appConfig.path, f)))
103
+
104
+ if (candidate) {
105
+ appConfig.config = resolve(appConfig.path, candidate)
106
+ }
107
+ }
108
+
109
+ if (appConfig.config) {
110
+ // Parse the configuration file the first time to obtain the schema
111
+ const unvalidatedConfig = await loadConfiguration(appConfig.config, null, {
112
+ onMissingEnv: this.#context.fetchApplicationUrl
113
+ })
114
+ const pkg = await loadConfigurationModule(appConfig.path, unvalidatedConfig)
115
+ this.capability = await pkg.create(appConfig.path, appConfig.config, this.#context)
116
+ // We could not find a configuration file, we use the bundle @platformatic/basic with the runtime to load it
117
+ } else {
118
+ const pkg = await loadConfigurationModule(resolve(import.meta.dirname, '../..'), {}, '@platformatic/basic')
119
+ this.capability = await pkg.create(appConfig.path, {}, this.#context)
120
+ }
121
+
122
+ this.#updateDispatcher()
123
+ } catch (err) {
124
+ if (err.validationErrors) {
125
+ globalThis.platformatic.logger.error(
126
+ { err: ensureLoggableError(err) },
127
+ 'The application threw a validation error.'
128
+ )
129
+
130
+ throw err
131
+ } else {
132
+ this.#logAndThrow(err)
133
+ }
134
+ }
135
+ }
136
+
137
+ async start () {
138
+ if (this.#starting || this.#started) {
139
+ throw new ApplicationAlreadyStartedError()
140
+ }
141
+
142
+ this.#starting = true
143
+
144
+ try {
145
+ await this.capability.init?.()
146
+ this.emit('init')
147
+ } catch (err) {
148
+ this.#logAndThrow(err)
149
+ }
150
+
151
+ this.emit('starting')
152
+
153
+ if (this.capability.status === 'stopped') {
154
+ return
155
+ }
156
+
157
+ if (this.#watch) {
158
+ const watchConfig = await this.capability.getWatchConfig()
159
+
160
+ if (watchConfig.enabled !== false) {
161
+ /* c8 ignore next 4 */
162
+ this.#debouncedRestart = debounce(() => {
163
+ this.capability.log({ message: 'files changed', level: 'debug' })
164
+ this.emit('changed')
165
+ }, 100) // debounce restart for 100ms
166
+
167
+ this.#startFileWatching(watchConfig)
168
+ }
169
+ }
170
+
171
+ const listen = !!this.appConfig.useHttp
172
+
173
+ try {
174
+ await this.capability.start({ listen })
175
+ this.#listening = listen
176
+ /* c8 ignore next 5 */
177
+ } catch (err) {
178
+ this.capability.log({ message: err.message, level: 'debug' })
179
+ this.#starting = false
180
+ throw err
181
+ }
182
+
183
+ this.#started = true
184
+ this.#starting = false
185
+ this.emit('started')
186
+ }
187
+
188
+ async stop (force = false, dependents = []) {
189
+ if (!force && (!this.#started || this.#starting)) {
190
+ throw new RuntimeNotStartedError()
191
+ }
192
+
193
+ this.emit('stopping')
194
+ await this.#stopFileWatching()
195
+ await this.capability.waitForDependentsStop(dependents)
196
+ await this.capability.stop()
197
+
198
+ this.#started = false
199
+ this.#starting = false
200
+ this.#listening = false
201
+ this.emit('stopped')
202
+ }
203
+
204
+ async listen () {
205
+ // This server is not an entrypoint or already listened in start. Behave as no-op.
206
+ if (!this.appConfig.entrypoint || this.appConfig.useHttp || this.#listening) {
207
+ return
208
+ }
209
+
210
+ await this.capability.start({ listen: true })
211
+ }
212
+
213
+ async getMetrics ({ format }) {
214
+ const dispatcher = getGlobalDispatcher()
215
+ if (globalThis.platformatic?.onHttpStatsFree && dispatcher?.stats) {
216
+ for (const url in dispatcher.stats) {
217
+ const { free, connected, pending, queued, running, size } = dispatcher.stats[url]
218
+ globalThis.platformatic.onHttpStatsFree(url, free || 0)
219
+ globalThis.platformatic.onHttpStatsConnected(url, connected || 0)
220
+ globalThis.platformatic.onHttpStatsPending(url, pending || 0)
221
+ globalThis.platformatic.onHttpStatsQueued(url, queued || 0)
222
+ globalThis.platformatic.onHttpStatsRunning(url, running || 0)
223
+ globalThis.platformatic.onHttpStatsSize(url, size || 0)
224
+ }
225
+ }
226
+ globalThis.platformatic.onActiveResourcesEventLoop(getActiveResourcesInfo().length)
227
+ return this.capability.getMetrics({ format })
228
+ }
229
+
230
+ async getHealth () {
231
+ const currentELU = performance.eventLoopUtilization()
232
+ const elu = performance.eventLoopUtilization(currentELU, this.#lastELU).utilization
233
+ this.#lastELU = currentELU
234
+
235
+ const { heapUsed, heapTotal } = process.memoryUsage()
236
+
237
+ return {
238
+ elu,
239
+ heapUsed,
240
+ heapTotal
241
+ }
242
+ }
243
+
244
+ #startFileWatching (watch) {
245
+ if (this.#fileWatcher) {
246
+ return
247
+ }
248
+
249
+ const fileWatcher = new FileWatcher({
250
+ path: watch.path,
251
+ /* c8 ignore next 2 */
252
+ allowToWatch: watch?.allow,
253
+ watchIgnore: watch?.ignore || []
254
+ })
255
+
256
+ fileWatcher.on('update', this.#debouncedRestart)
257
+
258
+ fileWatcher.startWatching()
259
+ this.capability.log({ message: 'start watching files', level: 'debug' })
260
+ this.#fileWatcher = fileWatcher
261
+ }
262
+
263
+ async #stopFileWatching () {
264
+ const watcher = this.#fileWatcher
265
+
266
+ if (watcher) {
267
+ this.capability.log({ message: 'stop watching files', level: 'debug' })
268
+ await watcher.stopWatching()
269
+ this.#fileWatcher = null
270
+ }
271
+ }
272
+
273
+ #logAndThrow (err) {
274
+ globalThis.platformatic.logger.error(
275
+ { err: ensureLoggableError(err) },
276
+ err[kHandledError] ? err.message : 'The application threw an error.'
277
+ )
278
+
279
+ throw err
280
+ }
281
+
282
+ #updateDispatcher () {
283
+ const telemetryConfig = this.#context.telemetryConfig
284
+ const telemetryId = telemetryConfig?.applicationName
285
+
286
+ const interceptor = dispatch => {
287
+ return function InterceptedDispatch (opts, handler) {
288
+ if (telemetryId) {
289
+ opts.headers = {
290
+ ...opts.headers,
291
+ 'x-plt-telemetry-id': telemetryId
292
+ }
293
+ }
294
+ return dispatch(opts, handler)
295
+ }
296
+ }
297
+
298
+ const dispatcher = getGlobalDispatcher().compose(interceptor)
299
+
300
+ setGlobalDispatcher(dispatcher)
301
+ }
302
+ }
@@ -0,0 +1,171 @@
1
+ import { randomUUID } from 'node:crypto'
2
+ import { Readable, Writable } from 'node:stream'
3
+ import { interceptors } from 'undici'
4
+ import { kITC } from './symbols.js'
5
+
6
+ const kCacheIdHeader = Symbol('cacheIdHeader')
7
+ const CACHE_ID_HEADER = 'x-plt-http-cache-id'
8
+
9
+ const noop = () => {}
10
+
11
+ export class RemoteCacheStore {
12
+ #onRequest
13
+ #onCacheHit
14
+ #onCacheMiss
15
+ #logger
16
+
17
+ constructor (opts = {}) {
18
+ this.#onRequest = opts.onRequest ?? noop
19
+ this.#onCacheHit = opts.onCacheHit ?? noop
20
+ this.#onCacheMiss = opts.onCacheMiss ?? noop
21
+ this.#logger = opts.logger
22
+ }
23
+
24
+ async get (request) {
25
+ try {
26
+ this.#onRequest(request)
27
+ } catch (err) {
28
+ this.#logger.error(err, 'Error in onRequest http cache hook')
29
+ }
30
+
31
+ const itc = globalThis[kITC]
32
+ if (!itc) return
33
+
34
+ const sanitizedRequest = this.#sanitizeRequest(request)
35
+
36
+ const cachedValue = await itc.send('getHttpCacheValue', {
37
+ request: sanitizedRequest
38
+ })
39
+ if (!cachedValue) {
40
+ try {
41
+ this.#onCacheMiss(request)
42
+ } catch (err) {
43
+ this.#logger.error(err, 'Error in onCacheMiss http cache hook')
44
+ }
45
+ return
46
+ }
47
+
48
+ const readable = new Readable({ read () {} })
49
+ readable.push(cachedValue.payload)
50
+ readable.push(null)
51
+
52
+ try {
53
+ this.#onCacheHit(request, cachedValue.response)
54
+ } catch (err) {
55
+ this.#logger.error(err, 'Error in onCacheHit http cache hook')
56
+ }
57
+
58
+ return {
59
+ ...cachedValue.response,
60
+ body: readable
61
+ }
62
+ }
63
+
64
+ createWriteStream (key, value) {
65
+ const cacheEntryId = value.headers?.[kCacheIdHeader]
66
+ if (cacheEntryId) {
67
+ key = { ...key, id: cacheEntryId }
68
+ value.headers = { ...value.headers, [CACHE_ID_HEADER]: cacheEntryId }
69
+ }
70
+
71
+ const itc = globalThis[kITC]
72
+ if (!itc) throw new Error('Cannot write to cache without an ITC instance')
73
+
74
+ const acc = []
75
+
76
+ key = this.#sanitizeRequest(key)
77
+
78
+ return new Writable({
79
+ write (chunk, encoding, callback) {
80
+ acc.push(chunk)
81
+ callback()
82
+ },
83
+ final (callback) {
84
+ let payload
85
+ if (acc.length > 0 && typeof acc[0] === 'string') {
86
+ payload = acc.join('')
87
+ } else {
88
+ payload = Buffer.concat(acc)
89
+ }
90
+ itc
91
+ .send('setHttpCacheValue', { request: key, response: value, payload })
92
+ .then(() => callback())
93
+ .catch(err => callback(err))
94
+ }
95
+ })
96
+ }
97
+
98
+ delete (request) {
99
+ const itc = globalThis[kITC]
100
+ if (!itc) throw new Error('Cannot delete from cache without an ITC instance')
101
+
102
+ request = this.#sanitizeRequest(request)
103
+ itc.send('deleteHttpCacheValue', { request })
104
+ // TODO: return a Promise
105
+ }
106
+
107
+ #sanitizeRequest (request) {
108
+ return {
109
+ id: request.id,
110
+ origin: request.origin,
111
+ method: request.method,
112
+ path: request.path,
113
+ headers: request.headers
114
+ }
115
+ }
116
+ }
117
+
118
+ export function httpCacheInterceptor (interceptorOpts) {
119
+ const originalInterceptor = interceptors.cache(interceptorOpts)
120
+
121
+ // AsyncLocalStorage that contains a client http request span
122
+ // Exists only when the nodejs capability telemetry is enabled
123
+ const clientSpansAls = globalThis.platformatic.clientSpansAls
124
+
125
+ return originalDispatch => {
126
+ const dispatch = (opts, handler) => {
127
+ const originOnResponseStart = handler.onResponseStart.bind(handler)
128
+ handler.onResponseStart = (ac, statusCode, headers, statusMessage) => {
129
+ // Setting a potentially cache entry id when cache miss happens
130
+ headers[kCacheIdHeader] = randomUUID()
131
+ return originOnResponseStart(ac, statusCode, headers, statusMessage)
132
+ }
133
+
134
+ return originalDispatch(opts, handler)
135
+ }
136
+
137
+ const dispatcher = originalInterceptor(dispatch)
138
+
139
+ return (opts, handler) => {
140
+ const originOnResponseStart = handler.onResponseStart.bind(handler)
141
+ handler.onResponseStart = (ac, statusCode, headers, statusMessage) => {
142
+ const cacheEntryId = headers[kCacheIdHeader] ?? headers[CACHE_ID_HEADER]
143
+ const isCacheHit = headers.age !== undefined
144
+
145
+ if (cacheEntryId) {
146
+ // Setting a cache id header on cache hit
147
+ headers[CACHE_ID_HEADER] = cacheEntryId
148
+ delete headers[kCacheIdHeader]
149
+
150
+ if (clientSpansAls) {
151
+ try {
152
+ const { span } = clientSpansAls.getStore()
153
+ if (span) {
154
+ span.setAttribute('http.cache.id', cacheEntryId)
155
+ span.setAttribute('http.cache.hit', isCacheHit.toString())
156
+ }
157
+ } catch (err) {
158
+ interceptorOpts.logger.error(err, 'Error setting cache id on span')
159
+ }
160
+ }
161
+ }
162
+ return originOnResponseStart(ac, statusCode, headers, statusMessage)
163
+ }
164
+
165
+ if (!clientSpansAls) {
166
+ return dispatcher(opts, handler)
167
+ }
168
+ return clientSpansAls.run({ span: null }, () => dispatcher(opts, handler))
169
+ }
170
+ }
171
+ }
@@ -1,17 +1,197 @@
1
- 'use strict'
1
+ import { createTelemetryThreadInterceptorHooks } from '@platformatic/telemetry'
2
+ import { createRequire } from 'node:module'
3
+ import { join } from 'node:path'
4
+ import { pathToFileURL } from 'node:url'
5
+ import { parentPort, workerData } from 'node:worker_threads'
6
+ import { Agent, Client, Pool, setGlobalDispatcher } from 'undici'
7
+ import { wire } from 'undici-thread-interceptor'
8
+ import { RemoteCacheStore, httpCacheInterceptor } from './http-cache.js'
9
+ import { kInterceptors } from './symbols.js'
2
10
 
3
- const { pathToFileURL } = require('node:url')
11
+ export async function setDispatcher (runtimeConfig) {
12
+ const threadDispatcher = createThreadInterceptor(runtimeConfig)
13
+ const threadInterceptor = threadDispatcher.interceptor
14
+
15
+ let cacheInterceptor = null
16
+ if (runtimeConfig.httpCache) {
17
+ cacheInterceptor = createHttpCacheInterceptor(runtimeConfig)
18
+ }
19
+
20
+ let userInterceptors = []
21
+ if (Array.isArray(runtimeConfig.undici?.interceptors)) {
22
+ const _require = createRequire(join(workerData.dirname, 'package.json'))
23
+ userInterceptors = await loadInterceptors(_require, runtimeConfig.undici.interceptors)
24
+ }
25
+
26
+ const dispatcherOpts = await getDispatcherOpts(runtimeConfig.undici)
27
+
28
+ setGlobalDispatcher(
29
+ new Agent(dispatcherOpts).compose([threadInterceptor, ...userInterceptors, cacheInterceptor].filter(Boolean))
30
+ )
31
+
32
+ return { threadDispatcher }
33
+ }
34
+
35
+ export async function updateUndiciInterceptors (undiciConfig) {
36
+ const updatableInterceptors = globalThis[kInterceptors]
37
+ if (!updatableInterceptors) return
38
+
39
+ if (Array.isArray(undiciConfig?.interceptors)) {
40
+ for (const interceptorConfig of undiciConfig.interceptors) {
41
+ const { module, options } = interceptorConfig
42
+
43
+ const interceptorCtx = updatableInterceptors[module]
44
+ if (!interceptorCtx) continue
45
+
46
+ const { createInterceptor, updateInterceptor } = interceptorCtx
47
+ updateInterceptor(createInterceptor(options))
48
+ }
49
+ } else {
50
+ for (const key of ['Agent', 'Pool', 'Client']) {
51
+ const interceptorConfigs = undiciConfig.interceptors[key]
52
+ if (!interceptorConfigs) continue
53
+
54
+ for (const interceptorConfig of interceptorConfigs) {
55
+ const { module, options } = interceptorConfig
56
+
57
+ const interceptorCtx = updatableInterceptors[key][module]
58
+ if (!interceptorCtx) continue
59
+
60
+ const { createInterceptor, updateInterceptor } = interceptorCtx
61
+ updateInterceptor(createInterceptor(options))
62
+ }
63
+ }
64
+ }
65
+ }
66
+
67
+ function createUpdatableInterceptor (originInterceptor) {
68
+ let originalDispatcher = null
69
+ let originalDispatch = null
70
+
71
+ function updatableInterceptor (dispatch) {
72
+ originalDispatch = dispatch
73
+ originalDispatcher = originInterceptor(dispatch)
74
+
75
+ return function dispatcher (opts, handler) {
76
+ return originalDispatcher(opts, handler)
77
+ }
78
+ }
79
+
80
+ function updateInterceptor (newInterceptor) {
81
+ originalDispatcher = newInterceptor(originalDispatch)
82
+ }
83
+
84
+ return { updatableInterceptor, updateInterceptor }
85
+ }
86
+
87
+ async function loadInterceptors (_require, interceptorsConfigs, key) {
88
+ return Promise.all(
89
+ interceptorsConfigs.map(async interceptorConfig => {
90
+ return loadInterceptor(_require, interceptorConfig, key)
91
+ })
92
+ )
93
+ }
94
+
95
+ async function loadInterceptor (_require, interceptorConfig, key) {
96
+ let updatableInterceptors = globalThis[kInterceptors]
97
+ if (!updatableInterceptors) {
98
+ updatableInterceptors = {}
99
+ globalThis[kInterceptors] = updatableInterceptors
100
+ }
101
+
102
+ const { module, options } = interceptorConfig
4
103
 
5
- async function loadInterceptor (_require, module, options) {
6
104
  const url = pathToFileURL(_require.resolve(module))
7
- const interceptor = (await import(url)).default
8
- return interceptor(options)
105
+ const createInterceptor = (await import(url)).default
106
+ const interceptor = createInterceptor(options)
107
+
108
+ const { updatableInterceptor, updateInterceptor } = createUpdatableInterceptor(interceptor)
109
+
110
+ const interceptorCtx = { createInterceptor, updateInterceptor }
111
+
112
+ if (key !== undefined) {
113
+ if (!updatableInterceptors[key]) {
114
+ updatableInterceptors[key] = {}
115
+ }
116
+ updatableInterceptors[key][module] = interceptorCtx
117
+ } else {
118
+ updatableInterceptors[module] = interceptorCtx
119
+ }
120
+
121
+ return updatableInterceptor
122
+ }
123
+
124
+ async function getDispatcherOpts (undiciConfig) {
125
+ const dispatcherOpts = { ...undiciConfig }
126
+
127
+ const interceptorsConfigs = undiciConfig?.interceptors
128
+ if (!interceptorsConfigs || Array.isArray(interceptorsConfigs)) {
129
+ return dispatcherOpts
130
+ }
131
+
132
+ const _require = createRequire(join(workerData.dirname, 'package.json'))
133
+
134
+ const clientInterceptors = []
135
+ const poolInterceptors = []
136
+
137
+ for (const key of ['Agent', 'Pool', 'Client']) {
138
+ const interceptorConfig = undiciConfig.interceptors[key]
139
+ if (!interceptorConfig) continue
140
+
141
+ const interceptors = await loadInterceptors(_require, interceptorConfig, key)
142
+ if (key === 'Agent') {
143
+ clientInterceptors.push(...interceptors)
144
+ poolInterceptors.push(...interceptors)
145
+ }
146
+ if (key === 'Pool') {
147
+ poolInterceptors.push(...interceptors)
148
+ }
149
+ if (key === 'Client') {
150
+ clientInterceptors.push(...interceptors)
151
+ }
152
+ }
153
+
154
+ dispatcherOpts.factory = (origin, opts) => {
155
+ return opts && opts.connections === 1
156
+ ? new Client(origin, opts).compose(clientInterceptors)
157
+ : new Pool(origin, opts).compose(poolInterceptors)
158
+ }
159
+
160
+ return dispatcherOpts
9
161
  }
10
162
 
11
- function loadInterceptors (_require, interceptors) {
12
- return Promise.all(interceptors.map(async ({ module, options }) => {
13
- return loadInterceptor(_require, module, options)
14
- }))
163
+ function createThreadInterceptor (runtimeConfig) {
164
+ const telemetry = runtimeConfig.telemetry
165
+
166
+ const telemetryHooks = telemetry ? createTelemetryThreadInterceptorHooks() : {}
167
+
168
+ const threadDispatcher = wire({
169
+ // Specifying the domain is critical to avoid flooding the DNS
170
+ // with requests for a domain that's never going to exist.
171
+ domain: '.plt.local',
172
+ port: parentPort,
173
+ timeout: runtimeConfig.applicationTimeout,
174
+ ...telemetryHooks
175
+ })
176
+ return threadDispatcher
15
177
  }
16
178
 
17
- module.exports = loadInterceptors
179
+ function createHttpCacheInterceptor (runtimeConfig) {
180
+ const cacheInterceptor = httpCacheInterceptor({
181
+ store: new RemoteCacheStore({
182
+ onRequest: opts => {
183
+ globalThis.platformatic?.onHttpCacheRequest?.(opts)
184
+ },
185
+ onCacheHit: opts => {
186
+ globalThis.platformatic?.onHttpCacheHit?.(opts)
187
+ },
188
+ onCacheMiss: opts => {
189
+ globalThis.platformatic?.onHttpCacheMiss?.(opts)
190
+ },
191
+ logger: globalThis.platformatic.logger
192
+ }),
193
+ methods: runtimeConfig.httpCache.methods ?? ['GET', 'HEAD'],
194
+ logger: globalThis.platformatic.logger
195
+ })
196
+ return cacheInterceptor
197
+ }