@platformatic/runtime 2.7.0 → 2.7.1-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 HttpsSchemasPlatformaticDevPlatformaticRuntime270Json = {
8
+ export type HttpsSchemasPlatformaticDevPlatformaticRuntime271Alpha2Json = {
9
9
  [k: string]: unknown;
10
10
  } & {
11
11
  $schema?: string;
@@ -110,6 +110,17 @@ export type HttpsSchemasPlatformaticDevPlatformaticRuntime270Json = {
110
110
  };
111
111
  [k: string]: unknown;
112
112
  };
113
+ httpCache?:
114
+ | boolean
115
+ | {
116
+ store?: string;
117
+ /**
118
+ * @minItems 1
119
+ */
120
+ methods?: [string, ...string[]];
121
+ cacheTagsHeader?: string;
122
+ [k: string]: unknown;
123
+ };
113
124
  watch?: boolean | string;
114
125
  managementApi?:
115
126
  | 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 { 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
 
@@ -235,6 +243,10 @@ class Runtime extends EventEmitter {
235
243
  this.#loggerDestination = null
236
244
  }
237
245
 
246
+ if (this.#sharedHttpCache?.close) {
247
+ await this.#sharedHttpCache.close()
248
+ }
249
+
238
250
  this.#updateStatus('closed')
239
251
  }
240
252
 
@@ -755,6 +767,24 @@ class Runtime extends EventEmitter {
755
767
  return createReadStream(filePath)
756
768
  }
757
769
 
770
+ async getCachedHttpRequests () {
771
+ return this.#sharedHttpCache.getRoutes()
772
+ }
773
+
774
+ async invalidateHttpCache (options = {}) {
775
+ const { origin, routes, tags } = options
776
+
777
+ if (!this.#sharedHttpCache) return
778
+
779
+ if (routes && routes.length > 0) {
780
+ await this.#sharedHttpCache.deleteRoutes(routes)
781
+ }
782
+
783
+ if (tags && tags.length > 0) {
784
+ await this.#sharedHttpCache.deleteByCacheTags(origin, tags)
785
+ }
786
+ }
787
+
758
788
  #updateStatus (status) {
759
789
  this.#status = status
760
790
  this.emit(status)
@@ -865,7 +895,18 @@ class Runtime extends EventEmitter {
865
895
  port: service,
866
896
  handlers: {
867
897
  getServiceMeta: this.getServiceMeta.bind(this),
868
- getServices: this.getServices.bind(this)
898
+ getServices: this.getServices.bind(this),
899
+ isHttpCacheFull: () => this.#sharedHttpCache.isFull(),
900
+ getHttpCacheValue: opts => this.#sharedHttpCache.getValue(opts.request),
901
+ setHttpCacheValue: opts => this.#sharedHttpCache.setValue(
902
+ opts.request,
903
+ opts.response,
904
+ opts.payload
905
+ ),
906
+ deleteHttpCacheValue: opts => this.#sharedHttpCache.deleteByOrigin(
907
+ opts.origin
908
+ ),
909
+ invalidateHttpCache: opts => this.invalidateHttpCache(opts),
869
910
  }
870
911
  })
871
912
  service[kITC].listen()
package/lib/schema.js CHANGED
@@ -145,6 +145,32 @@ const platformaticRuntimeSchema = {
145
145
  }
146
146
  }
147
147
  },
148
+ httpCache: {
149
+ oneOf: [
150
+ {
151
+ type: 'boolean'
152
+ },
153
+ {
154
+ type: 'object',
155
+ properties: {
156
+ store: {
157
+ type: 'string'
158
+ },
159
+ methods: {
160
+ type: 'array',
161
+ items: {
162
+ type: 'string'
163
+ },
164
+ default: ['GET', 'HEAD'],
165
+ minItems: 1
166
+ },
167
+ cacheTagsHeader: {
168
+ type: 'string'
169
+ }
170
+ }
171
+ }
172
+ ]
173
+ },
148
174
  watch: {
149
175
  anyOf: [
150
176
  {
@@ -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
@@ -9,9 +9,11 @@ const diagnosticChannel = require('node:diagnostics_channel')
9
9
  const { ServerResponse } = require('node:http')
10
10
 
11
11
  const pino = require('pino')
12
- const { fetch, setGlobalDispatcher, Agent } = require('undici')
12
+ const { fetch, setGlobalDispatcher, getGlobalDispatcher, Agent } = require('undici')
13
13
  const { wire } = require('undici-thread-interceptor')
14
+ const undici = require('undici')
14
15
 
16
+ const RemoteCacheStore = require('./http-cache')
15
17
  const { PlatformaticApp } = require('./app')
16
18
  const { setupITC } = require('./itc')
17
19
  const loadInterceptors = require('./interceptors')
@@ -80,10 +82,34 @@ async function main () {
80
82
  }
81
83
  }
82
84
 
83
- const globalDispatcher = new Agent({
84
- ...config.undici,
85
- interceptors
86
- }).compose(composedInterceptors)
85
+ const dispatcherOpts = { ...config.undici }
86
+
87
+ if (Object.keys(interceptors).length > 0) {
88
+ const clientInterceptors = []
89
+ const poolInterceptors = []
90
+
91
+ if (interceptors.Agent) {
92
+ clientInterceptors.push(...interceptors.Agent)
93
+ poolInterceptors.push(...interceptors.Agent)
94
+ }
95
+
96
+ if (interceptors.Pool) {
97
+ poolInterceptors.push(...interceptors.Pool)
98
+ }
99
+
100
+ if (interceptors.Client) {
101
+ clientInterceptors.push(...interceptors.Client)
102
+ }
103
+
104
+ dispatcherOpts.factory = (origin, opts) => {
105
+ return opts && opts.connections === 1
106
+ ? new undici.Client(origin, opts).compose(clientInterceptors)
107
+ : new undici.Pool(origin, opts).compose(poolInterceptors)
108
+ }
109
+ }
110
+
111
+ const globalDispatcher = new Agent(dispatcherOpts)
112
+ .compose(composedInterceptors)
87
113
 
88
114
  setGlobalDispatcher(globalDispatcher)
89
115
 
@@ -92,6 +118,15 @@ async function main () {
92
118
  // TODO: make this configurable
93
119
  const threadDispatcher = wire({ port: parentPort, useNetwork: service.useHttp, timeout: 5 * 60 * 1000 })
94
120
 
121
+ if (config.httpCache) {
122
+ setGlobalDispatcher(
123
+ getGlobalDispatcher().compose(undici.interceptors.cache({
124
+ store: new RemoteCacheStore(),
125
+ methods: config.httpCache.methods ?? ['GET', 'HEAD']
126
+ }))
127
+ )
128
+ }
129
+
95
130
  // If the service is an entrypoint and runtime server config is defined, use it.
96
131
  let serverConfig = null
97
132
  if (config.server && service.entrypoint) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@platformatic/runtime",
3
- "version": "2.7.0",
3
+ "version": "2.7.1-alpha.2",
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.7.0",
39
- "@platformatic/db": "2.7.0",
40
- "@platformatic/service": "2.7.0",
41
- "@platformatic/node": "2.7.0",
42
- "@platformatic/sql-graphql": "2.7.0",
43
- "@platformatic/sql-mapper": "2.7.0"
38
+ "@platformatic/db": "2.7.1-alpha.2",
39
+ "@platformatic/composer": "2.7.1-alpha.2",
40
+ "@platformatic/service": "2.7.1-alpha.2",
41
+ "@platformatic/node": "2.7.1-alpha.2",
42
+ "@platformatic/sql-graphql": "2.7.1-alpha.2",
43
+ "@platformatic/sql-mapper": "2.7.1-alpha.2"
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.7.0",
75
- "@platformatic/config": "2.7.0",
76
- "@platformatic/generators": "2.7.0",
77
- "@platformatic/ts-compiler": "2.7.0",
78
- "@platformatic/itc": "2.7.0",
79
- "@platformatic/telemetry": "2.7.0",
80
- "@platformatic/utils": "2.7.0"
75
+ "@platformatic/basic": "2.7.1-alpha.2",
76
+ "@platformatic/config": "2.7.1-alpha.2",
77
+ "@platformatic/itc": "2.7.1-alpha.2",
78
+ "@platformatic/ts-compiler": "2.7.1-alpha.2",
79
+ "@platformatic/telemetry": "2.7.1-alpha.2",
80
+ "@platformatic/generators": "2.7.1-alpha.2",
81
+ "@platformatic/utils": "2.7.1-alpha.2"
81
82
  },
82
83
  "scripts": {
83
84
  "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.7.0.json",
2
+ "$id": "https://schemas.platformatic.dev/@platformatic/runtime/2.7.1-alpha.2.json",
3
3
  "$schema": "http://json-schema.org/draft-07/schema#",
4
4
  "type": "object",
5
5
  "properties": {
@@ -382,6 +382,35 @@
382
382
  }
383
383
  }
384
384
  },
385
+ "httpCache": {
386
+ "oneOf": [
387
+ {
388
+ "type": "boolean"
389
+ },
390
+ {
391
+ "type": "object",
392
+ "properties": {
393
+ "store": {
394
+ "type": "string"
395
+ },
396
+ "methods": {
397
+ "type": "array",
398
+ "items": {
399
+ "type": "string"
400
+ },
401
+ "default": [
402
+ "GET",
403
+ "HEAD"
404
+ ],
405
+ "minItems": 1
406
+ },
407
+ "cacheTagsHeader": {
408
+ "type": "string"
409
+ }
410
+ }
411
+ }
412
+ ]
413
+ },
385
414
  "watch": {
386
415
  "anyOf": [
387
416
  {