@platformatic/runtime 2.19.0-alpha.7 → 2.19.0-alpha.8

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 HttpsSchemasPlatformaticDevPlatformaticRuntime2190Alpha7Json = {
8
+ export type HttpsSchemasPlatformaticDevPlatformaticRuntime2190Alpha8Json = {
9
9
  [k: string]: unknown;
10
10
  } & {
11
11
  $schema?: string;
@@ -133,6 +133,17 @@ export type HttpsSchemasPlatformaticDevPlatformaticRuntime2190Alpha7Json = {
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')
@@ -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 () {
@@ -134,26 +137,16 @@ class Runtime extends EventEmitter {
134
137
 
135
138
  // Recompute the list of services after sorting
136
139
  this.#servicesIds = config.services.map(service => service.id)
137
-
138
- // When autoloading is disabled, add a warning if a service is defined before its dependencies
139
- if (!autoloadEnabled) {
140
- for (let i = 0; i < config.services.length; i++) {
141
- const current = config.services[i]
142
-
143
- for (const dep of current.dependencies ?? []) {
144
- if (config.services.findIndex(s => s.id === dep.id) > i) {
145
- this.logger.warn(
146
- `Service "${current.id}" depends on service "${dep.id}", but it is defined and it will be started before it. Please check your configuration file.`
147
- )
148
- }
149
- }
150
- }
151
- }
152
140
  } catch (e) {
153
141
  await this.close()
154
142
  throw e
155
143
  }
156
144
 
145
+ this.#sharedHttpCache = createSharedStore(
146
+ this.#configManager.dirname,
147
+ config.httpCache
148
+ )
149
+
157
150
  this.#updateStatus('init')
158
151
  }
159
152
 
@@ -282,6 +275,10 @@ class Runtime extends EventEmitter {
282
275
  this.#loggerDestination = null
283
276
  }
284
277
 
278
+ if (this.#sharedHttpCache?.close) {
279
+ await this.#sharedHttpCache.close()
280
+ }
281
+
285
282
  this.#updateStatus('closed')
286
283
  }
287
284
 
@@ -744,6 +741,23 @@ class Runtime extends EventEmitter {
744
741
  return createReadStream(filePath)
745
742
  }
746
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
+
747
761
  async sendCommandToService (id, name, message) {
748
762
  const service = await this.#getServiceById(id)
749
763
 
@@ -781,7 +795,7 @@ class Runtime extends EventEmitter {
781
795
  }
782
796
 
783
797
  async #setupWorker (config, serviceConfig, workersCount, serviceId, index) {
784
- const { restartOnError } = config
798
+ const { autoload, restartOnError } = config
785
799
  const workerId = `${serviceId}:${index}`
786
800
 
787
801
  const { port1: loggerDestination, port2: loggingPort } = new MessageChannel()
@@ -900,9 +914,18 @@ class Runtime extends EventEmitter {
900
914
  port: worker,
901
915
  handlers: {
902
916
  getServiceMeta: this.getServiceMeta.bind(this),
903
- listServices: () => {
904
- return this.#servicesIds
905
- }
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),
906
929
  }
907
930
  })
908
931
  worker[kITC].listen()
@@ -946,10 +969,12 @@ class Runtime extends EventEmitter {
946
969
  // Store dependencies
947
970
  const [{ dependencies }] = await waitEventFromITC(worker, 'init')
948
971
 
949
- serviceConfig.dependencies = dependencies
950
- for (const { envVar, url } of dependencies) {
951
- if (envVar) {
952
- serviceConfig.localServiceEnvVars.set(envVar, url)
972
+ if (autoload) {
973
+ serviceConfig.dependencies = dependencies
974
+ for (const { envVar, url } of dependencies) {
975
+ if (envVar) {
976
+ serviceConfig.localServiceEnvVars.set(envVar, url)
977
+ }
953
978
  }
954
979
  }
955
980
 
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
@@ -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')
@@ -92,10 +94,34 @@ async function main () {
92
94
  }
93
95
  }
94
96
 
95
- const globalDispatcher = new Agent({
96
- ...config.undici,
97
- interceptors
98
- }).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)
99
125
 
100
126
  setGlobalDispatcher(globalDispatcher)
101
127
 
@@ -109,6 +135,15 @@ async function main () {
109
135
  ...hooks
110
136
  })
111
137
 
138
+ if (config.httpCache) {
139
+ setGlobalDispatcher(
140
+ getGlobalDispatcher().compose(undici.interceptors.cache({
141
+ store: new RemoteCacheStore(),
142
+ methods: config.httpCache.methods ?? ['GET', 'HEAD']
143
+ }))
144
+ )
145
+ }
146
+
112
147
  // If the service is an entrypoint and runtime server config is defined, use it.
113
148
  let serverConfig = null
114
149
  if (config.server && service.entrypoint) {
@@ -165,7 +200,7 @@ async function main () {
165
200
  globalThis[kITC] = itc
166
201
 
167
202
  // Get the dependencies
168
- const dependencies = await app.getBootstrapDependencies()
203
+ const dependencies = config.autoload ? await app.getBootstrapDependencies() : []
169
204
  itc.notify('init', { dependencies })
170
205
  }
171
206
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@platformatic/runtime",
3
- "version": "2.19.0-alpha.7",
3
+ "version": "2.19.0-alpha.8",
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.7",
39
- "@platformatic/db": "2.19.0-alpha.7",
40
- "@platformatic/node": "2.19.0-alpha.7",
41
- "@platformatic/service": "2.19.0-alpha.7",
42
- "@platformatic/sql-graphql": "2.19.0-alpha.7",
43
- "@platformatic/sql-mapper": "2.19.0-alpha.7"
38
+ "@platformatic/composer": "2.19.0-alpha.8",
39
+ "@platformatic/db": "2.19.0-alpha.8",
40
+ "@platformatic/node": "2.19.0-alpha.8",
41
+ "@platformatic/service": "2.19.0-alpha.8",
42
+ "@platformatic/sql-graphql": "2.19.0-alpha.8",
43
+ "@platformatic/sql-mapper": "2.19.0-alpha.8"
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.1",
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.7",
74
- "@platformatic/generators": "2.19.0-alpha.7",
75
- "@platformatic/ts-compiler": "2.19.0-alpha.7",
76
- "@platformatic/utils": "2.19.0-alpha.7",
77
- "@platformatic/config": "2.19.0-alpha.7",
78
- "@platformatic/itc": "2.19.0-alpha.7",
79
- "@platformatic/telemetry": "2.19.0-alpha.7"
75
+ "@platformatic/config": "2.19.0-alpha.8",
76
+ "@platformatic/basic": "2.19.0-alpha.8",
77
+ "@platformatic/itc": "2.19.0-alpha.8",
78
+ "@platformatic/generators": "2.19.0-alpha.8",
79
+ "@platformatic/telemetry": "2.19.0-alpha.8",
80
+ "@platformatic/ts-compiler": "2.19.0-alpha.8",
81
+ "@platformatic/utils": "2.19.0-alpha.8"
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.7.json",
2
+ "$id": "https://schemas.platformatic.dev/@platformatic/runtime/2.19.0-alpha.8.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
  {