@platformatic/runtime 2.8.1 → 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 HttpsSchemasPlatformaticDevPlatformaticRuntime281Json = {
8
+ export type HttpsSchemasPlatformaticDevPlatformaticRuntime282Alpha1Json = {
9
9
  [k: string]: unknown;
10
10
  } & {
11
11
  $schema?: string;
@@ -115,6 +115,17 @@ export type HttpsSchemasPlatformaticDevPlatformaticRuntime281Json = {
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
@@ -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')
@@ -65,6 +66,7 @@ class Runtime extends EventEmitter {
65
66
  #inspectorServer
66
67
  #workers
67
68
  #restartingWorkers
69
+ #sharedHttpCache
68
70
 
69
71
  constructor (configManager, runtimeLogsDir, env) {
70
72
  super()
@@ -81,6 +83,7 @@ class Runtime extends EventEmitter {
81
83
  this.#interceptor = createThreadInterceptor({ domain: '.plt.local', timeout: true })
82
84
  this.#status = undefined
83
85
  this.#restartingWorkers = new Map()
86
+ this.#sharedHttpCache = null
84
87
  }
85
88
 
86
89
  async init () {
@@ -127,6 +130,16 @@ class Runtime extends EventEmitter {
127
130
  throw e
128
131
  }
129
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
+
130
143
  this.#updateStatus('init')
131
144
  }
132
145
 
@@ -255,6 +268,10 @@ class Runtime extends EventEmitter {
255
268
  this.#loggerDestination = null
256
269
  }
257
270
 
271
+ if (this.#sharedHttpCache?.close) {
272
+ await this.#sharedHttpCache.close()
273
+ }
274
+
258
275
  this.#updateStatus('closed')
259
276
  }
260
277
 
@@ -717,6 +734,24 @@ class Runtime extends EventEmitter {
717
734
  return createReadStream(filePath)
718
735
  }
719
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
+
720
755
  #updateStatus (status) {
721
756
  this.#status = status
722
757
  this.emit(status)
@@ -855,9 +890,19 @@ class Runtime extends EventEmitter {
855
890
  port: worker,
856
891
  handlers: {
857
892
  getServiceMeta: this.getServiceMeta.bind(this),
858
- listServices: () => {
859
- return this.#servicesIds
860
- }
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),
861
906
  }
862
907
  })
863
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) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@platformatic/runtime",
3
- "version": "2.8.1",
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.1",
39
- "@platformatic/db": "2.8.1",
40
- "@platformatic/node": "2.8.1",
41
- "@platformatic/sql-graphql": "2.8.1",
42
- "@platformatic/service": "2.8.1",
43
- "@platformatic/sql-mapper": "2.8.1"
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.1",
75
- "@platformatic/config": "2.8.1",
76
- "@platformatic/generators": "2.8.1",
77
- "@platformatic/telemetry": "2.8.1",
78
- "@platformatic/itc": "2.8.1",
79
- "@platformatic/utils": "2.8.1",
80
- "@platformatic/ts-compiler": "2.8.1"
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.1.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
  {