@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 +12 -1
- package/lib/management-api.js +9 -0
- package/lib/runtime.js +48 -3
- package/lib/schema.js +26 -0
- package/lib/shared-http-cache.js +45 -0
- package/lib/worker/http-cache.js +83 -0
- package/lib/worker/main.js +40 -5
- package/package.json +16 -15
- package/schema.json +30 -1
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
|
|
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
|
package/lib/management-api.js
CHANGED
|
@@ -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
|
-
|
|
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
|
package/lib/worker/main.js
CHANGED
|
@@ -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
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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/
|
|
41
|
-
"@platformatic/
|
|
42
|
-
"@platformatic/
|
|
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": "
|
|
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/
|
|
77
|
-
"@platformatic/
|
|
78
|
-
"@platformatic/
|
|
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
|
{
|