@platformatic/runtime 2.19.0-alpha.3 → 2.19.0-alpha.5

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 HttpsSchemasPlatformaticDevPlatformaticRuntime2190Alpha3Json = {
8
+ export type HttpsSchemasPlatformaticDevPlatformaticRuntime2190Alpha5Json = {
9
9
  [k: string]: unknown;
10
10
  } & {
11
11
  $schema?: string;
@@ -133,6 +133,17 @@ export type HttpsSchemasPlatformaticDevPlatformaticRuntime2190Alpha3Json = {
133
133
  };
134
134
  [k: string]: unknown;
135
135
  };
136
+ httpCache?:
137
+ | boolean
138
+ | {
139
+ store?: string;
140
+ /**
141
+ * @minItems 1
142
+ */
143
+ methods?: [string, ...string[]];
144
+ cacheTagsHeader?: string;
145
+ [k: string]: unknown;
146
+ };
136
147
  watch?: boolean | string;
137
148
  managementApi?:
138
149
  | boolean
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')
@@ -45,7 +46,7 @@ const COLLECT_METRICS_TIMEOUT = 1000
45
46
  const MAX_BOOTSTRAP_ATTEMPTS = 5
46
47
 
47
48
  const telemetryPath = require.resolve('@platformatic/telemetry')
48
- const openTelemetrySetupPath = join(telemetryPath, '..', 'lib', 'node-http-telemetry.js')
49
+ const openTelemetrySetupPath = join(telemetryPath, '..', 'lib', 'node-telemetry.js')
49
50
 
50
51
  class Runtime extends EventEmitter {
51
52
  #configManager
@@ -66,6 +67,7 @@ class Runtime extends EventEmitter {
66
67
  #inspectorServer
67
68
  #workers
68
69
  #restartingWorkers
70
+ #sharedHttpCache
69
71
 
70
72
  constructor (configManager, runtimeLogsDir, env) {
71
73
  super()
@@ -85,6 +87,7 @@ class Runtime extends EventEmitter {
85
87
  })
86
88
  this.#status = undefined
87
89
  this.#restartingWorkers = new Map()
90
+ this.#sharedHttpCache = null
88
91
  }
89
92
 
90
93
  async init () {
@@ -139,6 +142,11 @@ class Runtime extends EventEmitter {
139
142
  throw e
140
143
  }
141
144
 
145
+ this.#sharedHttpCache = createSharedStore(
146
+ this.#configManager.dirname,
147
+ config.httpCache
148
+ )
149
+
142
150
  this.#updateStatus('init')
143
151
  }
144
152
 
@@ -267,6 +275,10 @@ class Runtime extends EventEmitter {
267
275
  this.#loggerDestination = null
268
276
  }
269
277
 
278
+ if (this.#sharedHttpCache?.close) {
279
+ await this.#sharedHttpCache.close()
280
+ }
281
+
270
282
  this.#updateStatus('closed')
271
283
  }
272
284
 
@@ -729,6 +741,23 @@ class Runtime extends EventEmitter {
729
741
  return createReadStream(filePath)
730
742
  }
731
743
 
744
+ async invalidateHttpCache (options = {}) {
745
+ const { keys, tags } = options
746
+
747
+ if (!this.#sharedHttpCache) return
748
+
749
+ const promises = []
750
+ if (keys && keys.length > 0) {
751
+ promises.push(this.#sharedHttpCache.deleteKeys(keys))
752
+ }
753
+
754
+ if (tags && tags.length > 0) {
755
+ promises.push(this.#sharedHttpCache.deleteTags(tags))
756
+ }
757
+
758
+ await Promise.all(promises)
759
+ }
760
+
732
761
  async sendCommandToService (id, name, message) {
733
762
  const service = await this.#getServiceById(id)
734
763
 
@@ -885,9 +914,18 @@ class Runtime extends EventEmitter {
885
914
  port: worker,
886
915
  handlers: {
887
916
  getServiceMeta: this.getServiceMeta.bind(this),
888
- listServices: () => {
889
- return this.#servicesIds
890
- }
917
+ listServices: () => this.#servicesIds,
918
+ getServices: this.getServices.bind(this),
919
+ getHttpCacheValue: opts => this.#sharedHttpCache.getValue(opts.request),
920
+ setHttpCacheValue: opts => this.#sharedHttpCache.setValue(
921
+ opts.request,
922
+ opts.response,
923
+ opts.payload
924
+ ),
925
+ deleteHttpCacheValue: opts => this.#sharedHttpCache.delete(
926
+ opts.request
927
+ ),
928
+ invalidateHttpCache: opts => this.invalidateHttpCache(opts),
891
929
  }
892
930
  })
893
931
  worker[kITC].listen()
package/lib/schema.js CHANGED
@@ -194,6 +194,32 @@ const platformaticRuntimeSchema = {
194
194
  }
195
195
  }
196
196
  },
197
+ httpCache: {
198
+ oneOf: [
199
+ {
200
+ type: 'boolean'
201
+ },
202
+ {
203
+ type: 'object',
204
+ properties: {
205
+ store: {
206
+ type: 'string'
207
+ },
208
+ methods: {
209
+ type: 'array',
210
+ items: {
211
+ type: 'string'
212
+ },
213
+ default: ['GET', 'HEAD'],
214
+ minItems: 1
215
+ },
216
+ cacheTagsHeader: {
217
+ type: 'string'
218
+ }
219
+ }
220
+ }
221
+ ]
222
+ },
197
223
  watch: {
198
224
  anyOf: [
199
225
  {
@@ -0,0 +1,39 @@
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 cachedValue = await this.get(req)
16
+ if (!cachedValue) return null
17
+
18
+ const { body, ...response } = cachedValue
19
+
20
+ let payload = ''
21
+ for await (const chunk of body) {
22
+ payload += chunk
23
+ }
24
+
25
+ return { response, payload }
26
+ }
27
+
28
+ setValue (req, opts, data) {
29
+ const writeStream = this.createWriteStream(req, opts)
30
+ writeStream.write(data)
31
+ writeStream.end()
32
+ return null
33
+ }
34
+ }
35
+
36
+ return new SharedCacheStore(storeConfig)
37
+ }
38
+
39
+ module.exports = { createSharedStore }
@@ -0,0 +1,73 @@
1
+ 'use strict'
2
+
3
+ const { Readable, Writable } = require('node:stream')
4
+ const { kITC } = require('./symbols')
5
+
6
+ class RemoteCacheStore {
7
+ async get (request) {
8
+ const itc = globalThis[kITC]
9
+ if (!itc) return
10
+
11
+ const cachedValue = await itc.send('getHttpCacheValue', {
12
+ request: this.#sanitizeRequest(request)
13
+ })
14
+ if (!cachedValue) return
15
+
16
+ const readable = new Readable({
17
+ read () {}
18
+ })
19
+
20
+ Object.defineProperty(readable, 'value', {
21
+ get () { return cachedValue.response }
22
+ })
23
+
24
+ readable.push(cachedValue.payload)
25
+ readable.push(null)
26
+
27
+ return {
28
+ ...cachedValue.response,
29
+ body: readable
30
+ }
31
+ }
32
+
33
+ createWriteStream (request, response) {
34
+ const itc = globalThis[kITC]
35
+ if (!itc) throw new Error('Cannot write to cache without an ITC instance')
36
+
37
+ let payload = ''
38
+
39
+ request = this.#sanitizeRequest(request)
40
+
41
+ return new Writable({
42
+ write (chunk, encoding, callback) {
43
+ payload += chunk
44
+ callback()
45
+ },
46
+ final (callback) {
47
+ itc.send('setHttpCacheValue', { request, response, payload })
48
+ .then(() => callback())
49
+ .catch((err) => callback(err))
50
+ }
51
+ })
52
+ }
53
+
54
+ delete (request) {
55
+ const itc = globalThis[kITC]
56
+ if (!itc) throw new Error('Cannot delete from cache without an ITC instance')
57
+
58
+ request = this.#sanitizeRequest(request)
59
+ itc.send('deleteHttpCacheValue', { request })
60
+ // TODO: return a Promise
61
+ }
62
+
63
+ #sanitizeRequest (request) {
64
+ return {
65
+ origin: request.origin,
66
+ method: request.method,
67
+ path: request.path,
68
+ headers: request.headers
69
+ }
70
+ }
71
+ }
72
+
73
+ module.exports = RemoteCacheStore
@@ -1,6 +1,5 @@
1
1
  'use strict'
2
2
 
3
- const { EventEmitter } = require('node:events')
4
3
  const { createRequire } = require('node:module')
5
4
  const { hostname } = require('node:os')
6
5
  const { join } = require('node:path')
@@ -11,9 +10,11 @@ const diagnosticChannel = require('node:diagnostics_channel')
11
10
  const { ServerResponse } = require('node:http')
12
11
 
13
12
  const pino = require('pino')
14
- const { fetch, setGlobalDispatcher, Agent } = require('undici')
13
+ const { fetch, setGlobalDispatcher, getGlobalDispatcher, Agent } = require('undici')
15
14
  const { wire } = require('undici-thread-interceptor')
15
+ const undici = require('undici')
16
16
 
17
+ const RemoteCacheStore = require('./http-cache')
17
18
  const { PlatformaticApp } = require('./app')
18
19
  const { setupITC } = require('./itc')
19
20
  const loadInterceptors = require('./interceptors')
@@ -36,10 +37,7 @@ globalThis[kId] = threadId
36
37
  let app
37
38
 
38
39
  const config = workerData.config
39
- globalThis.platformatic = Object.assign(globalThis.platformatic ?? {}, {
40
- logger: createLogger(),
41
- events: new EventEmitter()
42
- })
40
+ globalThis.platformatic = Object.assign(globalThis.platformatic ?? {}, { logger: createLogger() })
43
41
 
44
42
  function handleUnhandled (type, err) {
45
43
  const label =
@@ -96,22 +94,50 @@ async function main () {
96
94
  }
97
95
  }
98
96
 
99
- const globalDispatcher = new Agent({
100
- ...config.undici,
101
- interceptors
102
- }).compose(composedInterceptors)
97
+ const dispatcherOpts = { ...config.undici }
98
+
99
+ if (Object.keys(interceptors).length > 0) {
100
+ const clientInterceptors = []
101
+ const poolInterceptors = []
102
+
103
+ if (interceptors.Agent) {
104
+ clientInterceptors.push(...interceptors.Agent)
105
+ poolInterceptors.push(...interceptors.Agent)
106
+ }
107
+
108
+ if (interceptors.Pool) {
109
+ poolInterceptors.push(...interceptors.Pool)
110
+ }
111
+
112
+ if (interceptors.Client) {
113
+ clientInterceptors.push(...interceptors.Client)
114
+ }
115
+
116
+ dispatcherOpts.factory = (origin, opts) => {
117
+ return opts && opts.connections === 1
118
+ ? new undici.Client(origin, opts).compose(clientInterceptors)
119
+ : new undici.Pool(origin, opts).compose(poolInterceptors)
120
+ }
121
+ }
122
+
123
+ const globalDispatcher = new Agent(dispatcherOpts)
124
+ .compose(composedInterceptors)
103
125
 
104
126
  setGlobalDispatcher(globalDispatcher)
105
127
 
106
128
  const { telemetry } = service
107
129
  const hooks = telemetry ? createTelemetryThreadInterceptorHooks() : {}
108
130
  // Setup mesh networker
109
- const threadDispatcher = wire({
110
- port: parentPort,
111
- useNetwork: service.useHttp,
112
- timeout: config.serviceTimeout,
113
- ...hooks
114
- })
131
+ const threadDispatcher = wire({ port: parentPort, useNetwork: service.useHttp, timeout: config.serviceTimeout, ...hooks })
132
+
133
+ if (config.httpCache) {
134
+ setGlobalDispatcher(
135
+ getGlobalDispatcher().compose(undici.interceptors.cache({
136
+ store: new RemoteCacheStore(),
137
+ methods: config.httpCache.methods ?? ['GET', 'HEAD']
138
+ }))
139
+ )
140
+ }
115
141
 
116
142
  // If the service is an entrypoint and runtime server config is defined, use it.
117
143
  let serverConfig = null
@@ -200,7 +226,10 @@ function stripBasePath (basePath) {
200
226
 
201
227
  if (headers) {
202
228
  for (const key in headers) {
203
- if (key.toLowerCase() === 'location' && !headers[key].startsWith(basePath)) {
229
+ if (
230
+ key.toLowerCase() === 'location' &&
231
+ !headers[key].startsWith(basePath)
232
+ ) {
204
233
  headers[key] = basePath + headers[key]
205
234
  }
206
235
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@platformatic/runtime",
3
- "version": "2.19.0-alpha.3",
3
+ "version": "2.19.0-alpha.5",
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.19.0-alpha.3",
39
- "@platformatic/db": "2.19.0-alpha.3",
40
- "@platformatic/node": "2.19.0-alpha.3",
41
- "@platformatic/service": "2.19.0-alpha.3",
42
- "@platformatic/sql-graphql": "2.19.0-alpha.3",
43
- "@platformatic/sql-mapper": "2.19.0-alpha.3"
38
+ "@platformatic/composer": "2.19.0-alpha.5",
39
+ "@platformatic/db": "2.19.0-alpha.5",
40
+ "@platformatic/node": "2.19.0-alpha.5",
41
+ "@platformatic/sql-graphql": "2.19.0-alpha.5",
42
+ "@platformatic/service": "2.19.0-alpha.5",
43
+ "@platformatic/sql-mapper": "2.19.0-alpha.5"
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.8.0",
50
51
  "@watchable/unpromise": "^1.0.2",
51
52
  "boring-name-generator": "^1.0.3",
52
53
  "change-case-all": "^2.1.0",
@@ -67,16 +68,17 @@
67
68
  "prom-client": "^15.1.2",
68
69
  "semgrator": "^0.3.0",
69
70
  "tail-file-stream": "^0.2.0",
70
- "undici": "^6.9.0",
71
- "undici-thread-interceptor": "^0.9.0",
71
+ "thread-cpu-usage": "^0.2.0",
72
+ "undici": "7.0.0",
73
+ "undici-thread-interceptor": "^0.10.0",
72
74
  "ws": "^8.16.0",
73
- "@platformatic/basic": "2.19.0-alpha.3",
74
- "@platformatic/config": "2.19.0-alpha.3",
75
- "@platformatic/generators": "2.19.0-alpha.3",
76
- "@platformatic/telemetry": "2.19.0-alpha.3",
77
- "@platformatic/itc": "2.19.0-alpha.3",
78
- "@platformatic/utils": "2.19.0-alpha.3",
79
- "@platformatic/ts-compiler": "2.19.0-alpha.3"
75
+ "@platformatic/basic": "2.19.0-alpha.5",
76
+ "@platformatic/config": "2.19.0-alpha.5",
77
+ "@platformatic/generators": "2.19.0-alpha.5",
78
+ "@platformatic/itc": "2.19.0-alpha.5",
79
+ "@platformatic/telemetry": "2.19.0-alpha.5",
80
+ "@platformatic/utils": "2.19.0-alpha.5",
81
+ "@platformatic/ts-compiler": "2.19.0-alpha.5"
80
82
  },
81
83
  "scripts": {
82
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.19.0-alpha.3.json",
2
+ "$id": "https://schemas.platformatic.dev/@platformatic/runtime/2.19.0-alpha.5.json",
3
3
  "$schema": "http://json-schema.org/draft-07/schema#",
4
4
  "type": "object",
5
5
  "properties": {
@@ -840,6 +840,35 @@
840
840
  }
841
841
  }
842
842
  },
843
+ "httpCache": {
844
+ "oneOf": [
845
+ {
846
+ "type": "boolean"
847
+ },
848
+ {
849
+ "type": "object",
850
+ "properties": {
851
+ "store": {
852
+ "type": "string"
853
+ },
854
+ "methods": {
855
+ "type": "array",
856
+ "items": {
857
+ "type": "string"
858
+ },
859
+ "default": [
860
+ "GET",
861
+ "HEAD"
862
+ ],
863
+ "minItems": 1
864
+ },
865
+ "cacheTagsHeader": {
866
+ "type": "string"
867
+ }
868
+ }
869
+ }
870
+ ]
871
+ },
843
872
  "watch": {
844
873
  "anyOf": [
845
874
  {