@platformatic/runtime 2.5.5-alpha.3 → 2.5.6-alpha.2

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 HttpsSchemasPlatformaticDevPlatformaticRuntime255Alpha3Json = {
8
+ export type HttpsSchemasPlatformaticDevPlatformaticRuntime256Alpha2Json = {
9
9
  [k: string]: unknown;
10
10
  } & {
11
11
  $schema?: string;
@@ -109,6 +109,17 @@ export type HttpsSchemasPlatformaticDevPlatformaticRuntime255Alpha3Json = {
109
109
  };
110
110
  [k: string]: unknown;
111
111
  };
112
+ httpCache?:
113
+ | boolean
114
+ | {
115
+ store?: string;
116
+ /**
117
+ * @minItems 1
118
+ */
119
+ methods?: [string, ...string[]];
120
+ cacheTagsHeader?: string;
121
+ [k: string]: unknown;
122
+ };
112
123
  watch?: boolean | string;
113
124
  managementApi?:
114
125
  | boolean
@@ -22,6 +22,11 @@ async function managementApiPlugin (app, opts) {
22
22
 
23
23
  const runtime = opts.runtime
24
24
 
25
+ app.get('/status', async () => {
26
+ const status = runtime.getRuntimeStatus()
27
+ return { status }
28
+ })
29
+
25
30
  app.get('/metadata', async () => {
26
31
  return runtime.getRuntimeMetadata()
27
32
  })
@@ -188,6 +193,15 @@ async function managementApiPlugin (app, opts) {
188
193
  const logFileStream = await runtime.getLogFileStream(logId, runtimePID)
189
194
  return logFileStream
190
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
+ })
191
205
  }
192
206
 
193
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 { kId, kITC, kConfig } = require('./worker/symbols')
@@ -53,6 +54,7 @@ class Runtime extends EventEmitter {
53
54
  #bootstrapAttempts
54
55
  #inspectors
55
56
  #inspectorServer
57
+ #sharedHttpCache
56
58
 
57
59
  constructor (configManager, runtimeLogsDir, env) {
58
60
  super()
@@ -72,6 +74,7 @@ class Runtime extends EventEmitter {
72
74
  this.#restartPromises = new Map()
73
75
  this.#bootstrapAttempts = new Map()
74
76
  this.#inspectors = []
77
+ this.#sharedHttpCache = null
75
78
  }
76
79
 
77
80
  async init () {
@@ -116,6 +119,11 @@ class Runtime extends EventEmitter {
116
119
  throw e
117
120
  }
118
121
 
122
+ this.#sharedHttpCache = createSharedStore(
123
+ this.#configManager.dirname,
124
+ config.httpCache
125
+ )
126
+
119
127
  this.#updateStatus('init')
120
128
  }
121
129
 
@@ -202,6 +210,10 @@ class Runtime extends EventEmitter {
202
210
  return this.#url
203
211
  }
204
212
 
213
+ getRuntimeStatus () {
214
+ return this.#status
215
+ }
216
+
205
217
  async close (fromManagementApi = false, silent = false) {
206
218
  this.#updateStatus('closing')
207
219
 
@@ -751,6 +763,24 @@ class Runtime extends EventEmitter {
751
763
  return createReadStream(filePath)
752
764
  }
753
765
 
766
+ async getCachedHttpRequests () {
767
+ return this.#sharedHttpCache.getRoutes()
768
+ }
769
+
770
+ async invalidateHttpCache (options = {}) {
771
+ const { origin, routes, tags } = options
772
+
773
+ if (!this.#sharedHttpCache) return
774
+
775
+ if (routes && routes.length > 0) {
776
+ await this.#sharedHttpCache.deleteRoutes(routes)
777
+ }
778
+
779
+ if (tags && tags.length > 0) {
780
+ await this.#sharedHttpCache.deleteByCacheTags(origin, tags)
781
+ }
782
+ }
783
+
754
784
  #updateStatus (status) {
755
785
  this.#status = status
756
786
  this.emit(status)
@@ -861,7 +891,18 @@ class Runtime extends EventEmitter {
861
891
  port: service,
862
892
  handlers: {
863
893
  getServiceMeta: this.getServiceMeta.bind(this),
864
- getServices: this.getServices.bind(this)
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),
865
906
  }
866
907
  })
867
908
  service[kITC].listen()
package/lib/schema.js CHANGED
@@ -142,6 +142,32 @@ const platformaticRuntimeSchema = {
142
142
  }
143
143
  }
144
144
  },
145
+ httpCache: {
146
+ oneOf: [
147
+ {
148
+ type: 'boolean'
149
+ },
150
+ {
151
+ type: 'object',
152
+ properties: {
153
+ store: {
154
+ type: 'string'
155
+ },
156
+ methods: {
157
+ type: 'array',
158
+ items: {
159
+ type: 'string'
160
+ },
161
+ default: ['GET', 'HEAD'],
162
+ minItems: 1
163
+ },
164
+ cacheTagsHeader: {
165
+ type: 'string'
166
+ }
167
+ }
168
+ }
169
+ ]
170
+ },
145
171
  watch: {
146
172
  anyOf: [
147
173
  {
@@ -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
@@ -7,9 +7,11 @@ const { pathToFileURL } = require('node:url')
7
7
  const inspector = require('node:inspector')
8
8
 
9
9
  const pino = require('pino')
10
- const { fetch, setGlobalDispatcher, Agent } = require('undici')
10
+ const { fetch, setGlobalDispatcher, getGlobalDispatcher, Agent } = require('undici')
11
11
  const { wire } = require('undici-thread-interceptor')
12
+ const undici = require('undici')
12
13
 
14
+ const RemoteCacheStore = require('./http-cache')
13
15
  const { PlatformaticApp } = require('./app')
14
16
  const { setupITC } = require('./itc')
15
17
  const loadInterceptors = require('./interceptors')
@@ -78,10 +80,21 @@ async function main () {
78
80
  }
79
81
  }
80
82
 
81
- const globalDispatcher = new Agent({
82
- ...config.undici,
83
- interceptors
84
- }).compose(composedInterceptors)
83
+ const dispatcherOpts = { ...config.undici }
84
+
85
+ if (Object.keys(interceptors).length > 0) {
86
+ const clientInterceptors = [...interceptors.Agent, ...interceptors.Client]
87
+ const poolInterceptors = [...interceptors.Agent, ...interceptors.Pool]
88
+
89
+ dispatcherOpts.factory = (origin, opts) => {
90
+ return opts && opts.connections === 1
91
+ ? new undici.Client(origin, opts).compose(clientInterceptors)
92
+ : new undici.Pool(origin, opts).compose(poolInterceptors)
93
+ }
94
+ }
95
+
96
+ const globalDispatcher = new Agent(dispatcherOpts)
97
+ .compose(composedInterceptors)
85
98
 
86
99
  setGlobalDispatcher(globalDispatcher)
87
100
 
@@ -90,6 +103,15 @@ async function main () {
90
103
  // TODO: make this configurable
91
104
  const threadDispatcher = wire({ port: parentPort, useNetwork: service.useHttp, timeout: 5 * 60 * 1000 })
92
105
 
106
+ if (config.httpCache) {
107
+ setGlobalDispatcher(
108
+ getGlobalDispatcher().compose(undici.interceptors.cache({
109
+ store: new RemoteCacheStore(),
110
+ methods: config.httpCache.methods ?? ['GET', 'HEAD']
111
+ }))
112
+ )
113
+ }
114
+
93
115
  // If the service is an entrypoint and runtime server config is defined, use it.
94
116
  let serverConfig = null
95
117
  if (config.server && service.entrypoint) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@platformatic/runtime",
3
- "version": "2.5.5-alpha.3",
3
+ "version": "2.5.6-alpha.2",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -19,7 +19,7 @@
19
19
  "devDependencies": {
20
20
  "@fastify/express": "^4.0.0",
21
21
  "@fastify/formbody": "^8.0.0",
22
- "borp": "^0.17.0",
22
+ "borp": "^0.18.0",
23
23
  "c8": "^10.0.0",
24
24
  "eslint": "9",
25
25
  "execa": "^8.0.1",
@@ -35,17 +35,18 @@
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.5.5-alpha.3",
39
- "@platformatic/db": "2.5.5-alpha.3",
40
- "@platformatic/service": "2.5.5-alpha.3",
41
- "@platformatic/sql-graphql": "2.5.5-alpha.3",
42
- "@platformatic/sql-mapper": "2.5.5-alpha.3"
38
+ "@platformatic/composer": "2.5.6-alpha.2",
39
+ "@platformatic/service": "2.5.6-alpha.2",
40
+ "@platformatic/db": "2.5.6-alpha.2",
41
+ "@platformatic/sql-graphql": "2.5.6-alpha.2",
42
+ "@platformatic/sql-mapper": "2.5.6-alpha.2"
43
43
  },
44
44
  "dependencies": {
45
45
  "@fastify/error": "^4.0.0",
46
46
  "@fastify/websocket": "^11.0.0",
47
47
  "@hapi/topo": "^6.0.2",
48
48
  "@platformatic/http-metrics": "^0.2.1",
49
+ "@platformatic/undici-cache-memory": "^0.3.0",
49
50
  "@watchable/unpromise": "^1.0.2",
50
51
  "boring-name-generator": "^1.0.3",
51
52
  "change-case-all": "^2.1.0",
@@ -67,16 +68,16 @@
67
68
  "semgrator": "^0.3.0",
68
69
  "tail-file-stream": "^0.2.0",
69
70
  "thread-cpu-usage": "^0.2.0",
70
- "undici": "^6.9.0",
71
+ "undici": "7.0.0-alpha.3",
71
72
  "undici-thread-interceptor": "^0.7.0",
72
73
  "ws": "^8.16.0",
73
- "@platformatic/basic": "2.5.5-alpha.3",
74
- "@platformatic/config": "2.5.5-alpha.3",
75
- "@platformatic/generators": "2.5.5-alpha.3",
76
- "@platformatic/itc": "2.5.5-alpha.3",
77
- "@platformatic/telemetry": "2.5.5-alpha.3",
78
- "@platformatic/ts-compiler": "2.5.5-alpha.3",
79
- "@platformatic/utils": "2.5.5-alpha.3"
74
+ "@platformatic/basic": "2.5.6-alpha.2",
75
+ "@platformatic/config": "2.5.6-alpha.2",
76
+ "@platformatic/generators": "2.5.6-alpha.2",
77
+ "@platformatic/telemetry": "2.5.6-alpha.2",
78
+ "@platformatic/itc": "2.5.6-alpha.2",
79
+ "@platformatic/ts-compiler": "2.5.6-alpha.2",
80
+ "@platformatic/utils": "2.5.6-alpha.2"
80
81
  },
81
82
  "scripts": {
82
83
  "test": "npm run lint && borp --concurrency=1 --timeout=180000 && tsd",
package/schema.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "$id": "https://schemas.platformatic.dev/@platformatic/runtime/2.5.5-alpha.3.json",
2
+ "$id": "https://schemas.platformatic.dev/@platformatic/runtime/2.5.6-alpha.2.json",
3
3
  "$schema": "http://json-schema.org/draft-07/schema#",
4
4
  "type": "object",
5
5
  "properties": {
@@ -379,6 +379,35 @@
379
379
  }
380
380
  }
381
381
  },
382
+ "httpCache": {
383
+ "oneOf": [
384
+ {
385
+ "type": "boolean"
386
+ },
387
+ {
388
+ "type": "object",
389
+ "properties": {
390
+ "store": {
391
+ "type": "string"
392
+ },
393
+ "methods": {
394
+ "type": "array",
395
+ "items": {
396
+ "type": "string"
397
+ },
398
+ "default": [
399
+ "GET",
400
+ "HEAD"
401
+ ],
402
+ "minItems": 1
403
+ },
404
+ "cacheTagsHeader": {
405
+ "type": "string"
406
+ }
407
+ }
408
+ }
409
+ ]
410
+ },
382
411
  "watch": {
383
412
  "anyOf": [
384
413
  {