@platformatic/runtime 2.8.0 → 2.8.2-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 CHANGED
@@ -5,7 +5,7 @@
5
5
  * and run json-schema-to-typescript to regenerate this file.
6
6
  */
7
7
 
8
- export type HttpsSchemasPlatformaticDevPlatformaticRuntime280Json = {
8
+ export type HttpsSchemasPlatformaticDevPlatformaticRuntime282Alpha1Json = {
9
9
  [k: string]: unknown;
10
10
  } & {
11
11
  $schema?: string;
@@ -115,6 +115,17 @@ export type HttpsSchemasPlatformaticDevPlatformaticRuntime280Json = {
115
115
  };
116
116
  [k: string]: unknown;
117
117
  };
118
+ httpCache?:
119
+ | boolean
120
+ | {
121
+ store?: string;
122
+ /**
123
+ * @minItems 1
124
+ */
125
+ methods?: [string, ...string[]];
126
+ cacheTagsHeader?: string;
127
+ [k: string]: unknown;
128
+ };
118
129
  watch?: boolean | string;
119
130
  managementApi?:
120
131
  | boolean
package/lib/config.js CHANGED
@@ -1,9 +1,10 @@
1
1
  'use strict'
2
2
 
3
3
  const { readdir } = require('node:fs/promises')
4
- const { join, resolve: pathResolve } = require('node:path')
4
+ const { join, resolve: pathResolve, isAbsolute } = require('node:path')
5
5
 
6
6
  const ConfigManager = require('@platformatic/config')
7
+ const { Store } = require('@platformatic/config')
7
8
 
8
9
  const errors = require('./errors')
9
10
  const { schema } = require('./schema')
@@ -86,9 +87,40 @@ async function _transformConfig (configManager, args) {
86
87
  for (let i = 0; i < services.length; ++i) {
87
88
  const service = services[i]
88
89
 
90
+ // We need to have absolut paths here, ot the `loadConfig` will fail
91
+ if (!isAbsolute(service.path)) {
92
+ service.path = pathResolve(configManager.dirname, service.path)
93
+ }
94
+
89
95
  if (configManager._fixPaths && service.config) {
90
96
  service.config = pathResolve(service.path, service.config)
91
97
  }
98
+
99
+ if (service.config) {
100
+ try {
101
+ const store = new Store({ cwd: service.path })
102
+ const serviceConfig = await store.loadConfig(service)
103
+ service.isPLTService = !!serviceConfig.app.isPLTService
104
+ service.type = serviceConfig.app.configType
105
+ } catch (err) {
106
+ // Fallback if for any reason a dependency is not found
107
+ try {
108
+ const manager = new ConfigManager({ source: pathResolve(service.path, service.config) })
109
+ await manager.parse()
110
+ const config = manager.current
111
+ const type = config.$schema ? ConfigManager.matchKnownSchema(config.$schema) : undefined
112
+ service.type = type
113
+ service.isPLTService = !!config.isPLTService
114
+ } catch (err) {
115
+ // This should not happen, it happens on running some unit tests if we prepare the runtime
116
+ // when not all the services configs are available. Given that we are running this only
117
+ // to ddetermine the type of the service, it's safe to ignore this error and default to unknown
118
+ service.type = 'unknown'
119
+ service.isPLTService = false
120
+ }
121
+ }
122
+ }
123
+
92
124
  service.entrypoint = service.id === config.entrypoint
93
125
  service.dependencies = []
94
126
  service.localServiceEnvVars = new Map()
@@ -121,12 +153,7 @@ async function _transformConfig (configManager, args) {
121
153
  continue
122
154
  }
123
155
 
124
- const manager = new ConfigManager({ source: pathResolve(service.path, service.config) })
125
- await manager.parse()
126
- const config = manager.current
127
- const type = config.$schema ? ConfigManager.matchKnownSchema(config.$schema) : undefined
128
-
129
- if (type === 'composer') {
156
+ if (service.type === 'composer') {
130
157
  composers.push(service.id)
131
158
  }
132
159
  }
@@ -193,6 +193,15 @@ async function managementApiPlugin (app, opts) {
193
193
  const logFileStream = await runtime.getLogFileStream(logId, runtimePID)
194
194
  return logFileStream
195
195
  })
196
+
197
+ app.get('/http-cache/requests', async () => {
198
+ return runtime.getCachedHttpRequests()
199
+ })
200
+
201
+ app.post('/http-cache/invalidate', async (req) => {
202
+ const { origin, routes, tags } = req.body
203
+ await runtime.invalidateHttpCache({ origin, routes, tags })
204
+ })
196
205
  }
197
206
 
198
207
  async function startManagementApi (runtime, configManager) {
package/lib/runtime.js CHANGED
@@ -16,6 +16,7 @@ const errors = require('./errors')
16
16
  const { createLogger } = require('./logger')
17
17
  const { startManagementApi } = require('./management-api')
18
18
  const { startPrometheusServer } = require('./prom-server')
19
+ const { createSharedStore } = require('./shared-http-cache')
19
20
  const { getRuntimeTmpDir } = require('./utils')
20
21
  const { sendViaITC, waitEventFromITC } = require('./worker/itc')
21
22
  const { RoundRobinMap } = require('./worker/round-robin-map.js')
@@ -43,6 +44,9 @@ const COLLECT_METRICS_TIMEOUT = 1000
43
44
 
44
45
  const MAX_BOOTSTRAP_ATTEMPTS = 5
45
46
 
47
+ const telemetryPath = require.resolve('@platformatic/telemetry')
48
+ const openTelemetrySetupPath = join(telemetryPath, '..', 'lib', 'node-http-telemetry.js')
49
+
46
50
  class Runtime extends EventEmitter {
47
51
  #configManager
48
52
  #isProduction
@@ -62,6 +66,7 @@ class Runtime extends EventEmitter {
62
66
  #inspectorServer
63
67
  #workers
64
68
  #restartingWorkers
69
+ #sharedHttpCache
65
70
 
66
71
  constructor (configManager, runtimeLogsDir, env) {
67
72
  super()
@@ -78,6 +83,7 @@ class Runtime extends EventEmitter {
78
83
  this.#interceptor = createThreadInterceptor({ domain: '.plt.local', timeout: true })
79
84
  this.#status = undefined
80
85
  this.#restartingWorkers = new Map()
86
+ this.#sharedHttpCache = null
81
87
  }
82
88
 
83
89
  async init () {
@@ -124,6 +130,16 @@ class Runtime extends EventEmitter {
124
130
  throw e
125
131
  }
126
132
 
133
+ this.#sharedHttpCache = createSharedStore(
134
+ this.#configManager.dirname,
135
+ {
136
+ ...config.httpCache,
137
+ errorCallback: (err) => {
138
+ this.logger.error(err, 'Error in shared HTTP cache store')
139
+ }
140
+ }
141
+ )
142
+
127
143
  this.#updateStatus('init')
128
144
  }
129
145
 
@@ -252,6 +268,10 @@ class Runtime extends EventEmitter {
252
268
  this.#loggerDestination = null
253
269
  }
254
270
 
271
+ if (this.#sharedHttpCache?.close) {
272
+ await this.#sharedHttpCache.close()
273
+ }
274
+
255
275
  this.#updateStatus('closed')
256
276
  }
257
277
 
@@ -714,6 +734,24 @@ class Runtime extends EventEmitter {
714
734
  return createReadStream(filePath)
715
735
  }
716
736
 
737
+ async getCachedHttpRequests () {
738
+ return this.#sharedHttpCache.getRoutes()
739
+ }
740
+
741
+ async invalidateHttpCache (options = {}) {
742
+ const { origin, routes, tags } = options
743
+
744
+ if (!this.#sharedHttpCache) return
745
+
746
+ if (routes && routes.length > 0) {
747
+ await this.#sharedHttpCache.deleteRoutes(routes)
748
+ }
749
+
750
+ if (tags && tags.length > 0) {
751
+ await this.#sharedHttpCache.deleteByCacheTags(origin, tags)
752
+ }
753
+ }
754
+
717
755
  #updateStatus (status) {
718
756
  this.#status = status
719
757
  this.emit(status)
@@ -753,6 +791,13 @@ class Runtime extends EventEmitter {
753
791
  inspectorOptions.port = inspectorOptions.port + this.#workers.size + 1
754
792
  }
755
793
 
794
+ if (config.telemetry) {
795
+ serviceConfig.telemetry = {
796
+ ...config.telemetry,
797
+ serviceName: `${config.telemetry.serviceName}-${serviceConfig.id}`
798
+ }
799
+ }
800
+
756
801
  const worker = new Worker(kWorkerFile, {
757
802
  workerData: {
758
803
  config,
@@ -770,7 +815,7 @@ class Runtime extends EventEmitter {
770
815
  runtimeLogsDir: this.#runtimeLogsDir,
771
816
  loggingPort
772
817
  },
773
- execArgv: [], // Avoid side effects
818
+ execArgv: serviceConfig.isPLTService ? [] : ['--require', openTelemetrySetupPath],
774
819
  env: this.#env,
775
820
  transferList: [loggingPort],
776
821
  /*
@@ -845,9 +890,19 @@ class Runtime extends EventEmitter {
845
890
  port: worker,
846
891
  handlers: {
847
892
  getServiceMeta: this.getServiceMeta.bind(this),
848
- listServices: () => {
849
- return this.#servicesIds
850
- }
893
+ listServices: () => this.#servicesIds,
894
+ getServices: this.getServices.bind(this),
895
+ isHttpCacheFull: () => this.#sharedHttpCache.isFull(),
896
+ getHttpCacheValue: opts => this.#sharedHttpCache.getValue(opts.request),
897
+ setHttpCacheValue: opts => this.#sharedHttpCache.setValue(
898
+ opts.request,
899
+ opts.response,
900
+ opts.payload
901
+ ),
902
+ deleteHttpCacheValue: opts => this.#sharedHttpCache.deleteByOrigin(
903
+ opts.origin
904
+ ),
905
+ invalidateHttpCache: opts => this.invalidateHttpCache(opts),
851
906
  }
852
907
  })
853
908
  worker[kITC].listen()
package/lib/schema.js CHANGED
@@ -185,6 +185,32 @@ const platformaticRuntimeSchema = {
185
185
  }
186
186
  }
187
187
  },
188
+ httpCache: {
189
+ oneOf: [
190
+ {
191
+ type: 'boolean'
192
+ },
193
+ {
194
+ type: 'object',
195
+ properties: {
196
+ store: {
197
+ type: 'string'
198
+ },
199
+ methods: {
200
+ type: 'array',
201
+ items: {
202
+ type: 'string'
203
+ },
204
+ default: ['GET', 'HEAD'],
205
+ minItems: 1
206
+ },
207
+ cacheTagsHeader: {
208
+ type: 'string'
209
+ }
210
+ }
211
+ }
212
+ ]
213
+ },
188
214
  watch: {
189
215
  anyOf: [
190
216
  {
@@ -0,0 +1,45 @@
1
+ 'use strict'
2
+
3
+ const { join } = require('node:path')
4
+ const { createRequire } = require('node:module')
5
+ const MemoryCacheStore = require('@platformatic/undici-cache-memory')
6
+
7
+ function createSharedStore (projectDir, httpCacheConfig = {}) {
8
+ const runtimeRequire = createRequire(join(projectDir, 'file'))
9
+
10
+ const { store, ...storeConfig } = httpCacheConfig
11
+ const CacheStore = store ? runtimeRequire(store) : MemoryCacheStore
12
+
13
+ class SharedCacheStore extends CacheStore {
14
+ async getValue (req) {
15
+ const readStream = await this.createReadStream(req)
16
+ if (!readStream) return null
17
+
18
+ let payload = ''
19
+ for await (const chunk of readStream) {
20
+ payload += chunk
21
+ }
22
+
23
+ const response = this.#sanitizeResponse(readStream.value)
24
+ return { response, payload }
25
+ }
26
+
27
+ setValue (req, opts, data) {
28
+ const writeStream = this.createWriteStream(req, opts)
29
+ writeStream.write(data)
30
+ writeStream.end()
31
+ return null
32
+ }
33
+
34
+ #sanitizeResponse (response) {
35
+ return {
36
+ ...response,
37
+ rawHeaders: response.rawHeaders.map(header => header.toString())
38
+ }
39
+ }
40
+ }
41
+
42
+ return new SharedCacheStore(storeConfig)
43
+ }
44
+
45
+ module.exports = { createSharedStore }
@@ -0,0 +1,83 @@
1
+ 'use strict'
2
+
3
+ const { Readable, Writable } = require('node:stream')
4
+ const { kITC } = require('./symbols')
5
+
6
+ class RemoteCacheStore {
7
+ get isFull () {
8
+ // TODO: make an itc call to the shared cache when interceptor supports
9
+ // an async isFull method
10
+ return false
11
+ }
12
+
13
+ async createReadStream (request) {
14
+ const itc = globalThis[kITC]
15
+ if (!itc) return
16
+
17
+ const cachedValue = await itc.send('getHttpCacheValue', {
18
+ request: this.#sanitizeRequest(request)
19
+ })
20
+ if (!cachedValue) return
21
+
22
+ const readable = new Readable({
23
+ read () {}
24
+ })
25
+
26
+ Object.defineProperty(readable, 'value', {
27
+ get () { return cachedValue.response }
28
+ })
29
+
30
+ readable.push(cachedValue.payload)
31
+ readable.push(null)
32
+
33
+ return readable
34
+ }
35
+
36
+ createWriteStream (request, response) {
37
+ const itc = globalThis[kITC]
38
+ if (!itc) throw new Error('Cannot write to cache without an ITC instance')
39
+
40
+ let payload = ''
41
+
42
+ request = this.#sanitizeRequest(request)
43
+ response = this.#sanitizeResponse(response)
44
+
45
+ return new Writable({
46
+ write (chunk, encoding, callback) {
47
+ payload += chunk
48
+ callback()
49
+ },
50
+ final (callback) {
51
+ itc.send('setHttpCacheValue', { request, response, payload })
52
+ .then(() => callback())
53
+ .catch((err) => callback(err))
54
+ }
55
+ })
56
+ }
57
+
58
+ deleteByOrigin (origin) {
59
+ const itc = globalThis[kITC]
60
+ if (!itc) throw new Error('Cannot delete from cache without an ITC instance')
61
+
62
+ itc.send('deleteHttpCacheValue', { origin })
63
+ // TODO: return a Promise
64
+ }
65
+
66
+ #sanitizeRequest (request) {
67
+ return {
68
+ origin: request.origin,
69
+ method: request.method,
70
+ path: request.path,
71
+ headers: request.headers
72
+ }
73
+ }
74
+
75
+ #sanitizeResponse (response) {
76
+ return {
77
+ ...response,
78
+ rawHeaders: response.rawHeaders.map(header => header.toString())
79
+ }
80
+ }
81
+ }
82
+
83
+ module.exports = RemoteCacheStore
@@ -10,9 +10,11 @@ const diagnosticChannel = require('node:diagnostics_channel')
10
10
  const { ServerResponse } = require('node:http')
11
11
 
12
12
  const pino = require('pino')
13
- const { fetch, setGlobalDispatcher, Agent } = require('undici')
13
+ const { fetch, setGlobalDispatcher, getGlobalDispatcher, Agent } = require('undici')
14
14
  const { wire } = require('undici-thread-interceptor')
15
+ const undici = require('undici')
15
16
 
17
+ const RemoteCacheStore = require('./http-cache')
16
18
  const { PlatformaticApp } = require('./app')
17
19
  const { setupITC } = require('./itc')
18
20
  const loadInterceptors = require('./interceptors')
@@ -90,10 +92,34 @@ async function main () {
90
92
  }
91
93
  }
92
94
 
93
- const globalDispatcher = new Agent({
94
- ...config.undici,
95
- interceptors
96
- }).compose(composedInterceptors)
95
+ const dispatcherOpts = { ...config.undici }
96
+
97
+ if (Object.keys(interceptors).length > 0) {
98
+ const clientInterceptors = []
99
+ const poolInterceptors = []
100
+
101
+ if (interceptors.Agent) {
102
+ clientInterceptors.push(...interceptors.Agent)
103
+ poolInterceptors.push(...interceptors.Agent)
104
+ }
105
+
106
+ if (interceptors.Pool) {
107
+ poolInterceptors.push(...interceptors.Pool)
108
+ }
109
+
110
+ if (interceptors.Client) {
111
+ clientInterceptors.push(...interceptors.Client)
112
+ }
113
+
114
+ dispatcherOpts.factory = (origin, opts) => {
115
+ return opts && opts.connections === 1
116
+ ? new undici.Client(origin, opts).compose(clientInterceptors)
117
+ : new undici.Pool(origin, opts).compose(poolInterceptors)
118
+ }
119
+ }
120
+
121
+ const globalDispatcher = new Agent(dispatcherOpts)
122
+ .compose(composedInterceptors)
97
123
 
98
124
  setGlobalDispatcher(globalDispatcher)
99
125
 
@@ -102,6 +128,15 @@ async function main () {
102
128
  // TODO: make this configurable
103
129
  const threadDispatcher = wire({ port: parentPort, useNetwork: service.useHttp, timeout: 5 * 60 * 1000 })
104
130
 
131
+ if (config.httpCache) {
132
+ setGlobalDispatcher(
133
+ getGlobalDispatcher().compose(undici.interceptors.cache({
134
+ store: new RemoteCacheStore(),
135
+ methods: config.httpCache.methods ?? ['GET', 'HEAD']
136
+ }))
137
+ )
138
+ }
139
+
105
140
  // If the service is an entrypoint and runtime server config is defined, use it.
106
141
  let serverConfig = null
107
142
  if (config.server && service.entrypoint) {
@@ -114,14 +149,6 @@ async function main () {
114
149
  }
115
150
  }
116
151
 
117
- let telemetryConfig = config.telemetry
118
- if (telemetryConfig) {
119
- telemetryConfig = {
120
- ...telemetryConfig,
121
- serviceName: `${telemetryConfig.serviceName}-${service.id}`
122
- }
123
- }
124
-
125
152
  const inspectorOptions = workerData.inspectorOptions
126
153
 
127
154
  if (inspectorOptions) {
@@ -144,7 +171,7 @@ async function main () {
144
171
  app = new PlatformaticApp(
145
172
  service,
146
173
  workerData.worker.count > 1 ? workerData.worker.index : undefined,
147
- telemetryConfig,
174
+ service.telemetry,
148
175
  config.logger,
149
176
  serverConfig,
150
177
  config.metrics,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@platformatic/runtime",
3
- "version": "2.8.0",
3
+ "version": "2.8.2-alpha.1",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -35,18 +35,19 @@
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.8.0",
39
- "@platformatic/db": "2.8.0",
40
- "@platformatic/node": "2.8.0",
41
- "@platformatic/service": "2.8.0",
42
- "@platformatic/sql-graphql": "2.8.0",
43
- "@platformatic/sql-mapper": "2.8.0"
38
+ "@platformatic/composer": "2.8.2-alpha.1",
39
+ "@platformatic/db": "2.8.2-alpha.1",
40
+ "@platformatic/service": "2.8.2-alpha.1",
41
+ "@platformatic/node": "2.8.2-alpha.1",
42
+ "@platformatic/sql-graphql": "2.8.2-alpha.1",
43
+ "@platformatic/sql-mapper": "2.8.2-alpha.1"
44
44
  },
45
45
  "dependencies": {
46
46
  "@fastify/error": "^4.0.0",
47
47
  "@fastify/websocket": "^11.0.0",
48
48
  "@hapi/topo": "^6.0.2",
49
49
  "@platformatic/http-metrics": "^0.2.1",
50
+ "@platformatic/undici-cache-memory": "^0.3.0",
50
51
  "@watchable/unpromise": "^1.0.2",
51
52
  "boring-name-generator": "^1.0.3",
52
53
  "change-case-all": "^2.1.0",
@@ -68,16 +69,16 @@
68
69
  "semgrator": "^0.3.0",
69
70
  "tail-file-stream": "^0.2.0",
70
71
  "thread-cpu-usage": "^0.2.0",
71
- "undici": "^6.9.0",
72
+ "undici": "7.0.0-alpha.3",
72
73
  "undici-thread-interceptor": "^0.7.0",
73
74
  "ws": "^8.16.0",
74
- "@platformatic/basic": "2.8.0",
75
- "@platformatic/generators": "2.8.0",
76
- "@platformatic/config": "2.8.0",
77
- "@platformatic/itc": "2.8.0",
78
- "@platformatic/telemetry": "2.8.0",
79
- "@platformatic/ts-compiler": "2.8.0",
80
- "@platformatic/utils": "2.8.0"
75
+ "@platformatic/basic": "2.8.2-alpha.1",
76
+ "@platformatic/config": "2.8.2-alpha.1",
77
+ "@platformatic/itc": "2.8.2-alpha.1",
78
+ "@platformatic/generators": "2.8.2-alpha.1",
79
+ "@platformatic/telemetry": "2.8.2-alpha.1",
80
+ "@platformatic/utils": "2.8.2-alpha.1",
81
+ "@platformatic/ts-compiler": "2.8.2-alpha.1"
81
82
  },
82
83
  "scripts": {
83
84
  "test": "npm run lint && borp --concurrency=1 --timeout=300000 && tsd",
package/schema.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "$id": "https://schemas.platformatic.dev/@platformatic/runtime/2.8.0.json",
2
+ "$id": "https://schemas.platformatic.dev/@platformatic/runtime/2.8.2-alpha.1.json",
3
3
  "$schema": "http://json-schema.org/draft-07/schema#",
4
4
  "type": "object",
5
5
  "properties": {
@@ -451,6 +451,35 @@
451
451
  }
452
452
  }
453
453
  },
454
+ "httpCache": {
455
+ "oneOf": [
456
+ {
457
+ "type": "boolean"
458
+ },
459
+ {
460
+ "type": "object",
461
+ "properties": {
462
+ "store": {
463
+ "type": "string"
464
+ },
465
+ "methods": {
466
+ "type": "array",
467
+ "items": {
468
+ "type": "string"
469
+ },
470
+ "default": [
471
+ "GET",
472
+ "HEAD"
473
+ ],
474
+ "minItems": 1
475
+ },
476
+ "cacheTagsHeader": {
477
+ "type": "string"
478
+ }
479
+ }
480
+ }
481
+ ]
482
+ },
454
483
  "watch": {
455
484
  "anyOf": [
456
485
  {