@platformatic/next 2.19.0-alpha.6 → 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
@@ -93,10 +93,4 @@ export interface PlatformaticNextJsStackable {
93
93
  production?: string;
94
94
  };
95
95
  };
96
- cache?: {
97
- adapter: "redis" | "valkey";
98
- url: string;
99
- prefix?: string;
100
- maxTTL?: number | string;
101
- };
102
96
  }
package/index.js CHANGED
@@ -16,14 +16,15 @@ import { once } from 'node:events'
16
16
  import { readFile } from 'node:fs/promises'
17
17
  import { dirname, resolve as pathResolve } from 'node:path'
18
18
  import { pathToFileURL } from 'node:url'
19
- import { satisfies } from 'semver'
19
+ import { parse, satisfies } from 'semver'
20
20
  import { packageJson, schema } from './lib/schema.js'
21
21
 
22
- const supportedVersions = '^14.0.0'
22
+ const supportedVersions = ['^14.0.0', '^15.0.0']
23
23
 
24
24
  export class NextStackable extends BaseStackable {
25
25
  #basePath
26
26
  #next
27
+ #nextVersion
27
28
  #child
28
29
  #server
29
30
 
@@ -34,9 +35,10 @@ export class NextStackable extends BaseStackable {
34
35
  async init () {
35
36
  this.#next = pathResolve(dirname(resolvePackage(this.root, 'next')), '../..')
36
37
  const nextPackage = JSON.parse(await readFile(pathResolve(this.#next, 'package.json'), 'utf-8'))
38
+ this.#nextVersion = parse(nextPackage.version)
37
39
 
38
40
  /* c8 ignore next 3 */
39
- if (!satisfies(nextPackage.version, supportedVersions)) {
41
+ if (!supportedVersions.some(v => satisfies(nextPackage.version, v))) {
40
42
  throw new errors.UnsupportedVersion('next', nextPackage.version, supportedVersions)
41
43
  }
42
44
  }
@@ -59,10 +61,8 @@ export class NextStackable extends BaseStackable {
59
61
  return this.stopCommand()
60
62
  }
61
63
 
62
- globalThis.platformatic.events.emit('plt:next:close')
63
-
64
64
  if (this.isProduction) {
65
- await new Promise((resolve, reject) => {
65
+ return new Promise((resolve, reject) => {
66
66
  this.#server.close(error => {
67
67
  /* c8 ignore next 3 */
68
68
  if (error) {
@@ -72,8 +72,6 @@ export class NextStackable extends BaseStackable {
72
72
  resolve()
73
73
  })
74
74
  })
75
-
76
- await this.childManager.close()
77
75
  } else {
78
76
  const exitPromise = once(this.#child, 'exit')
79
77
  await this.childManager.close()
@@ -83,6 +81,10 @@ export class NextStackable extends BaseStackable {
83
81
  }
84
82
 
85
83
  async build () {
84
+ if (!this.#nextVersion) {
85
+ await this.init()
86
+ }
87
+
86
88
  const config = this.configManager.current
87
89
  const loader = new URL('./lib/loader.js', import.meta.url)
88
90
  this.#basePath = config.application?.basePath ? cleanBasePath(config.application?.basePath) : ''
@@ -94,14 +96,13 @@ export class NextStackable extends BaseStackable {
94
96
  command = ['node', pathResolve(this.#next, './dist/bin/next'), 'build', this.root]
95
97
  }
96
98
 
97
- return this.buildWithCommand(command, this.#basePath, loader)
99
+ return this.buildWithCommand(command, this.#basePath, loader, this.#getChildManagerScripts())
98
100
  }
99
101
 
100
102
  /* c8 ignore next 5 */
101
103
  async getWatchConfig () {
102
104
  return {
103
- enabled: false,
104
- path: this.root
105
+ enabled: false
105
106
  }
106
107
  }
107
108
 
@@ -124,7 +125,7 @@ export class NextStackable extends BaseStackable {
124
125
  this.#basePath = config.application?.basePath ? cleanBasePath(config.application?.basePath) : ''
125
126
 
126
127
  if (command) {
127
- return this.startWithCommand(command, loaderUrl)
128
+ return this.startWithCommand(command, loaderUrl, this.#getChildManagerScripts())
128
129
  }
129
130
 
130
131
  const { hostname, port } = this.serverConfig ?? {}
@@ -136,7 +137,6 @@ export class NextStackable extends BaseStackable {
136
137
  this.childManager = new ChildManager({
137
138
  loader: loaderUrl,
138
139
  context: {
139
- config: this.configManager.current,
140
140
  serviceId: this.serviceId,
141
141
  workerId: this.workerId,
142
142
  // Always use URL to avoid serialization problem in Windows
@@ -148,7 +148,8 @@ export class NextStackable extends BaseStackable {
148
148
  runtimeBasePath: this.runtimeConfig.basePath,
149
149
  wantsAbsoluteUrls: true,
150
150
  telemetryConfig: this.telemetryConfig
151
- }
151
+ },
152
+ scripts: this.#getChildManagerScripts()
152
153
  })
153
154
 
154
155
  const promise = once(this.childManager, 'url')
@@ -168,7 +169,17 @@ export class NextStackable extends BaseStackable {
168
169
  try {
169
170
  await this.childManager.inject()
170
171
  const childPromise = createChildProcessListener()
171
- await nextDev(serverOptions, 'default', this.root)
172
+
173
+ if (this.#nextVersion.major === 14 && this.#nextVersion.minor < 2) {
174
+ await nextDev({
175
+ '--hostname': serverOptions.host,
176
+ '--port': serverOptions.port,
177
+ _: [this.root]
178
+ })
179
+ } else {
180
+ await nextDev(serverOptions, 'default', this.root)
181
+ }
182
+
172
183
  this.#child = await childPromise
173
184
  } finally {
174
185
  await this.childManager.eject()
@@ -183,13 +194,12 @@ export class NextStackable extends BaseStackable {
183
194
  this.#basePath = config.application?.basePath ? cleanBasePath(config.application?.basePath) : ''
184
195
 
185
196
  if (command) {
186
- return this.startWithCommand(command, loaderUrl)
197
+ return this.startWithCommand(command, loaderUrl, this.#getChildManagerScripts())
187
198
  }
188
199
 
189
200
  this.childManager = new ChildManager({
190
201
  loader: loaderUrl,
191
202
  context: {
192
- config: this.configManager.current,
193
203
  serviceId: this.serviceId,
194
204
  workerId: this.workerId,
195
205
  // Always use URL to avoid serialization problem in Windows
@@ -200,7 +210,8 @@ export class NextStackable extends BaseStackable {
200
210
  runtimeBasePath: this.runtimeConfig.basePath,
201
211
  wantsAbsoluteUrls: true,
202
212
  telemetryConfig: this.telemetryConfig
203
- }
213
+ },
214
+ scripts: this.#getChildManagerScripts()
204
215
  })
205
216
 
206
217
  this.verifyOutputDirectory(pathResolve(this.root, '.next'))
@@ -209,7 +220,6 @@ export class NextStackable extends BaseStackable {
209
220
 
210
221
  async #startProductionNext () {
211
222
  try {
212
- globalThis.platformatic.config = this.configManager.current
213
223
  await this.childManager.inject()
214
224
  const { nextStart } = await importFile(pathResolve(this.#next, './dist/cli/next-start.js'))
215
225
 
@@ -230,7 +240,15 @@ export class NextStackable extends BaseStackable {
230
240
  (this.isEntrypoint ? serverOptions?.hostname : undefined) ?? true
231
241
  )
232
242
 
233
- await nextStart(serverOptions, this.root)
243
+ if (this.#nextVersion.major === 14 && this.#nextVersion.minor < 2) {
244
+ await nextStart({
245
+ '--hostname': serverOptions.host,
246
+ '--port': serverOptions.port,
247
+ _: [this.root]
248
+ })
249
+ } else {
250
+ await nextStart(serverOptions, this.root)
251
+ }
234
252
 
235
253
  this.#server = await serverPromise
236
254
  this.url = getServerUrl(this.#server)
@@ -238,6 +256,16 @@ export class NextStackable extends BaseStackable {
238
256
  await this.childManager.eject()
239
257
  }
240
258
  }
259
+
260
+ #getChildManagerScripts () {
261
+ const scripts = []
262
+
263
+ if (this.#nextVersion.major === 15) {
264
+ scripts.push(new URL('./lib/loader-next-15.cjs', import.meta.url))
265
+ }
266
+
267
+ return scripts
268
+ }
241
269
  }
242
270
 
243
271
  /* c8 ignore next 9 */
@@ -250,10 +278,6 @@ function transformConfig () {
250
278
  this.current.watch = { enabled: this.current.watch || false }
251
279
  }
252
280
 
253
- if (this.current.cache?.adapter === 'redis') {
254
- this.current.cache.adapter = 'valkey'
255
- }
256
-
257
281
  basicTransformConfig.call(this)
258
282
  }
259
283
 
@@ -0,0 +1,52 @@
1
+ const { resolve } = require('node:path')
2
+ const { fileURLToPath } = require('node:url')
3
+ const fsPromises = require('node:fs').promises
4
+ const { transformSync } = require('amaro')
5
+ const { parse } = require('@babel/parser')
6
+ const traverse = require('@babel/traverse')
7
+
8
+ const originalReadFile = fsPromises.readFile
9
+ const targetFile = resolve(fileURLToPath(globalThis.platformatic.root), 'next.config.ts')
10
+
11
+ function detectFormat (code) {
12
+ let format = 'esm'
13
+
14
+ const ast = parse(code, { sourceType: 'module' })
15
+
16
+ // Manipulate the AST
17
+ traverse.default(ast, {
18
+ AssignmentExpression (path) {
19
+ const { left } = path.node
20
+
21
+ // module.exports = $EXPRESSION
22
+ if (left.object.name === 'module' && left.property.name === 'exports') {
23
+ format = 'cjs'
24
+ path.stop()
25
+ }
26
+ }
27
+ })
28
+
29
+ return format
30
+ }
31
+
32
+ fsPromises.readFile = async function WTF (url, options) {
33
+ if (url.startsWith('file://')) {
34
+ url = fileURLToPath(url)
35
+ }
36
+
37
+ const contents = await originalReadFile(url, options)
38
+
39
+ if (url !== targetFile) {
40
+ return contents
41
+ }
42
+
43
+ const { code } = transformSync(contents.toString('utf-8'), { mode: 'strip-only' })
44
+
45
+ const { transformESM, transformCJS } = await import('./loader.js')
46
+ const transformer = detectFormat(code) === 'esm' ? transformESM : transformCJS
47
+ const transformed = transformer(code)
48
+
49
+ // Restore the original method
50
+ fsPromises.readFile = originalReadFile
51
+ return transformed
52
+ }
package/lib/loader.js CHANGED
@@ -11,14 +11,12 @@ import {
11
11
  variableDeclarator
12
12
  } from '@babel/types'
13
13
  import { readFile, realpath } from 'node:fs/promises'
14
- import { sep } from 'node:path'
15
14
  import { fileURLToPath, pathToFileURL } from 'node:url'
16
15
 
17
16
  const originalId = '__pltOriginalNextConfig'
18
17
 
19
- let config
20
18
  let candidates
21
- let basePath
19
+ let basePath = ''
22
20
 
23
21
  function parseSingleExpression (expr) {
24
22
  return parse(expr, { allowAwaitOutsideFunction: true }).program.body[0]
@@ -37,11 +35,6 @@ function parseSingleExpression (expr) {
37
35
  __pltOriginalNextConfig.basePath = basePath
38
36
  }
39
37
 
40
- if(typeof __pltOriginalNextConfig.cacheHandler === 'undefined') {
41
- __pltOriginalNextConfig.cacheHandler = $PATH
42
- __pltOriginalNextConfig.cacheMaxMemorySize = 0
43
- }
44
-
45
38
  // This is to send the configuraion when Next is executed in a child process (development)
46
39
  globalThis[Symbol.for('plt.children.itc')]?.notify('config', __pltOriginalNextConfig)
47
40
 
@@ -52,41 +45,27 @@ function parseSingleExpression (expr) {
52
45
  }
53
46
  */
54
47
  function createEvaluatorWrapperFunction (original) {
55
- const cacheHandler = config?.cache
56
- ? fileURLToPath(new URL(`./caching/${config.cache.adapter ?? 'foo'}.js`, import.meta.url)).replaceAll(sep, '/')
57
- : undefined
58
-
59
48
  return functionDeclaration(
60
49
  null,
61
50
  [restElement(identifier('args'))],
62
- blockStatement(
63
- [
64
- variableDeclaration('let', [variableDeclarator(identifier(originalId), original)]),
65
- parseSingleExpression(
66
- `if (typeof ${originalId} === 'function') { ${originalId} = await ${originalId}(...args) }`
67
- ),
68
- parseSingleExpression(
69
- `if (typeof ${originalId}.basePath === 'undefined') { ${originalId}.basePath = "${basePath}" }`
70
- ),
71
- cacheHandler
72
- ? parseSingleExpression(`
73
- if (typeof ${originalId}.cacheHandler === 'undefined') {
74
- ${originalId}.cacheHandler = '${cacheHandler}'
75
- ${originalId}.cacheMaxMemorySize = 0
76
- }
77
- `)
78
- : undefined,
79
- parseSingleExpression(`globalThis[Symbol.for('plt.children.itc')]?.notify('config', ${originalId})`),
80
- parseSingleExpression(`process.emit('plt:next:config', ${originalId})`),
81
- returnStatement(identifier(originalId))
82
- ].filter(e => e)
83
- ),
51
+ blockStatement([
52
+ variableDeclaration('let', [variableDeclarator(identifier(originalId), original)]),
53
+ parseSingleExpression(
54
+ `if (typeof ${originalId} === 'function') { ${originalId} = await ${originalId}(...args) }`
55
+ ),
56
+ parseSingleExpression(
57
+ `if (typeof ${originalId}.basePath === 'undefined') { ${originalId}.basePath = "${basePath}" }`
58
+ ),
59
+ parseSingleExpression(`globalThis[Symbol.for('plt.children.itc')]?.notify('config', ${originalId})`),
60
+ parseSingleExpression(`process.emit('plt:next:config', ${originalId})`),
61
+ returnStatement(identifier(originalId))
62
+ ]),
84
63
  false,
85
64
  true
86
65
  )
87
66
  }
88
67
 
89
- function transformCJS (source) {
68
+ export function transformCJS (source) {
90
69
  const ast = parse(source.toString(), { sourceType: 'module' })
91
70
 
92
71
  // Manipulate the AST
@@ -105,7 +84,7 @@ function transformCJS (source) {
105
84
  return generate.default(ast).code
106
85
  }
107
86
 
108
- function transformESM (source) {
87
+ export function transformESM (source) {
109
88
  const ast = parse(source.toString(), { sourceType: 'module' })
110
89
 
111
90
  // Manipulate the AST
@@ -146,7 +125,6 @@ export async function initialize (data) {
146
125
  // Keep in sync with https://github.com/vercel/next.js/blob/main/packages/next/src/shared/lib/constants.ts
147
126
  candidates = ['next.config.js', 'next.config.mjs'].map(c => new URL(c, realRoot).toString())
148
127
  basePath = data.basePath ?? ''
149
- config = data.config
150
128
  }
151
129
 
152
130
  export async function load (url, context, nextLoad) {
package/lib/schema.js CHANGED
@@ -4,36 +4,6 @@ import { readFileSync } from 'node:fs'
4
4
 
5
5
  export const packageJson = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf-8'))
6
6
 
7
- export const cache = {
8
- type: 'object',
9
- properties: {
10
- adapter: {
11
- type: 'string',
12
- enum: ['redis', 'valkey']
13
- },
14
- url: {
15
- type: 'string'
16
- },
17
- prefix: {
18
- type: 'string'
19
- },
20
- maxTTL: {
21
- default: 86400 * 7, // One week
22
- anyOf: [
23
- {
24
- type: 'number',
25
- minimum: 0
26
- },
27
- {
28
- type: 'string'
29
- }
30
- ]
31
- }
32
- },
33
- required: ['adapter', 'url'],
34
- additionalProperties: false
35
- }
36
-
37
7
  export const schema = {
38
8
  $id: `https://schemas.platformatic.dev/@platformatic/next/${packageJson.version}.json`,
39
9
  $schema: 'http://json-schema.org/draft-07/schema#',
@@ -46,8 +16,7 @@ export const schema = {
46
16
  logger: utilsSchemaComponents.logger,
47
17
  server: utilsSchemaComponents.server,
48
18
  watch: schemaComponents.watch,
49
- application: schemaComponents.application,
50
- cache
19
+ application: schemaComponents.application
51
20
  },
52
21
  additionalProperties: false
53
22
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@platformatic/next",
3
- "version": "2.19.0-alpha.6",
3
+ "version": "2.19.0-alpha.8",
4
4
  "description": "Platformatic Next.js Stackable",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -19,27 +19,28 @@
19
19
  "@babel/parser": "^7.25.3",
20
20
  "@babel/traverse": "^7.25.3",
21
21
  "@babel/types": "^7.25.2",
22
- "iovalkey": "^0.2.1",
23
- "msgpackr": "^1.11.2",
22
+ "amaro": "^0.2.0",
24
23
  "semver": "^7.6.3",
25
- "@platformatic/basic": "2.19.0-alpha.6",
26
- "@platformatic/utils": "2.19.0-alpha.6",
27
- "@platformatic/config": "2.19.0-alpha.6"
24
+ "@platformatic/basic": "2.19.0-alpha.8",
25
+ "@platformatic/config": "2.19.0-alpha.8",
26
+ "@platformatic/utils": "2.19.0-alpha.8"
28
27
  },
29
28
  "devDependencies": {
30
29
  "@fastify/reply-from": "^11.0.0",
30
+ "@types/node": "^22.5.0",
31
31
  "borp": "^0.19.0",
32
32
  "eslint": "9",
33
+ "execa": "^9.5.1",
33
34
  "fastify": "^5.0.0",
34
35
  "json-schema-to-typescript": "^15.0.1",
35
36
  "neostandard": "^0.11.1",
36
- "next": "^14.2.5",
37
+ "next": "^15.0.0",
37
38
  "react": "^18.3.1",
38
39
  "react-dom": "^18.3.1",
39
40
  "typescript": "^5.5.4",
40
41
  "ws": "^8.18.0",
41
- "@platformatic/composer": "2.19.0-alpha.6",
42
- "@platformatic/service": "2.19.0-alpha.6"
42
+ "@platformatic/composer": "2.19.0-alpha.8",
43
+ "@platformatic/service": "2.19.0-alpha.8"
43
44
  },
44
45
  "scripts": {
45
46
  "test": "npm run lint && borp --concurrency=1 --no-timeout",
package/schema.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "$id": "https://schemas.platformatic.dev/@platformatic/next/2.19.0-alpha.6.json",
2
+ "$id": "https://schemas.platformatic.dev/@platformatic/next/2.19.0-alpha.8.json",
3
3
  "$schema": "http://json-schema.org/draft-07/schema#",
4
4
  "title": "Platformatic Next.js Stackable",
5
5
  "type": "object",
@@ -296,41 +296,6 @@
296
296
  },
297
297
  "additionalProperties": false,
298
298
  "default": {}
299
- },
300
- "cache": {
301
- "type": "object",
302
- "properties": {
303
- "adapter": {
304
- "type": "string",
305
- "enum": [
306
- "redis",
307
- "valkey"
308
- ]
309
- },
310
- "url": {
311
- "type": "string"
312
- },
313
- "prefix": {
314
- "type": "string"
315
- },
316
- "maxTTL": {
317
- "default": 604800,
318
- "anyOf": [
319
- {
320
- "type": "number",
321
- "minimum": 0
322
- },
323
- {
324
- "type": "string"
325
- }
326
- ]
327
- }
328
- },
329
- "required": [
330
- "adapter",
331
- "url"
332
- ],
333
- "additionalProperties": false
334
299
  }
335
300
  },
336
301
  "additionalProperties": false
@@ -1,229 +0,0 @@
1
- import { ensureLoggableError } from '@platformatic/utils'
2
- import { Redis } from 'iovalkey'
3
- import { pack, unpack } from 'msgpackr'
4
- import { existsSync, readFileSync } from 'node:fs'
5
- import { hostname } from 'node:os'
6
- import { resolve } from 'node:path'
7
- import { fileURLToPath } from 'node:url'
8
- import { pino } from 'pino'
9
-
10
- export const MAX_BATCH_SIZE = 100
11
-
12
- const sections = {
13
- values: 'values',
14
- tags: 'tags'
15
- }
16
-
17
- const clients = new Map()
18
-
19
- export function keyFor (prefix, subprefix, section, key) {
20
- return [prefix, 'cache:next', subprefix, section, key ? Buffer.from(key).toString('base64url') : undefined]
21
- .filter(c => c)
22
- .join(':')
23
- }
24
-
25
- export function getConnection (url) {
26
- let client = clients.get(url)
27
-
28
- if (!client) {
29
- client = new Redis(url, { enableAutoPipelining: true })
30
- clients.set(url, client)
31
-
32
- globalThis.platformatic.events.on('plt:next:close', () => {
33
- client.disconnect(false)
34
- })
35
- }
36
-
37
- return client
38
- }
39
-
40
- export class CacheHandler {
41
- #config
42
- #logger
43
- #store
44
- #subprefix
45
- #maxTTL
46
-
47
- constructor () {
48
- this.#logger = this.#createLogger()
49
- this.#config = globalThis.platformatic.config.cache
50
- this.#store = getConnection(this.#config.url)
51
- this.#maxTTL = this.#config.maxTTL
52
- this.#subprefix = this.#getSubprefix()
53
- }
54
-
55
- async get (cacheKey) {
56
- this.#logger.trace({ key: cacheKey }, 'get')
57
-
58
- const key = this.#keyFor(cacheKey, sections.values)
59
-
60
- let rawValue
61
- try {
62
- rawValue = await this.#store.get(key)
63
-
64
- if (!rawValue) {
65
- return
66
- }
67
- } catch (e) {
68
- this.#logger.error({ err: ensureLoggableError(e) }, 'Cannot read cache value from Valkey')
69
- throw new Error('Cannot read cache value from Valkey', { cause: e })
70
- }
71
-
72
- let value
73
- try {
74
- value = this.#deserialize(rawValue)
75
- } catch (e) {
76
- this.#logger.error({ err: ensureLoggableError(e) }, 'Cannot deserialize cache value from Valkey')
77
-
78
- // Avoid useless reads the next time
79
- // Note that since the value was unserializable, we don't know its tags and thus
80
- // we cannot remove it from the tags sets. TTL will take care of them.
81
- await this.#store.del(key)
82
-
83
- throw new Error('Cannot deserialize cache value from Valkey', { cause: e })
84
- }
85
-
86
- if (this.#maxTTL < value.revalidate) {
87
- try {
88
- await this.#refreshKey(key, value)
89
- } catch (e) {
90
- this.#logger.error({ err: ensureLoggableError(e) }, 'Cannot refresh cache key expiration in Valkey')
91
-
92
- // We don't throw here since we want to use the cached value anyway
93
- }
94
- }
95
-
96
- return value
97
- }
98
-
99
- async set (cacheKey, value, { tags, revalidate }) {
100
- this.#logger.trace({ key: cacheKey, value, tags, revalidate }, 'set')
101
-
102
- try {
103
- // Compute the parameters to save
104
- const key = this.#keyFor(cacheKey, sections.values)
105
- const data = this.#serialize({ value, tags, lastModified: Date.now(), revalidate, maxTTL: this.#maxTTL })
106
- const expire = Math.min(revalidate, this.#maxTTL)
107
-
108
- if (expire < 1) {
109
- return
110
- }
111
-
112
- // Enqueue all the operations to perform in Valkey
113
-
114
- const promises = []
115
- promises.push(this.#store.set(key, data, 'EX', expire))
116
-
117
- // As Next.js limits tags to 64, we don't need to manage batches here
118
- if (Array.isArray(tags)) {
119
- for (const tag of tags) {
120
- const tagsKey = this.#keyFor(tag, sections.tags)
121
- promises.push(this.#store.sadd(tagsKey, key))
122
- promises.push(this.#store.expire(tagsKey, expire))
123
- }
124
- }
125
-
126
- // Execute all the operations
127
- await Promise.all(promises)
128
- } catch (e) {
129
- this.#logger.error({ err: ensureLoggableError(e) }, 'Cannot write cache value in Valkey')
130
- throw new Error('Cannot write cache value in Valkey', { cause: e })
131
- }
132
- }
133
-
134
- async revalidateTag (tags) {
135
- this.#logger.trace({ tags }, 'revalidateTag')
136
-
137
- if (typeof tags === 'string') {
138
- tags = [tags]
139
- }
140
-
141
- try {
142
- let promises = []
143
-
144
- for (const tag of tags) {
145
- const tagsKey = this.#keyFor(tag, sections.tags)
146
-
147
- // For each key in the tag set, expire the key
148
- for await (const keys of this.#store.sscanStream(tagsKey)) {
149
- for (const key of keys) {
150
- promises.push(this.#store.del(key))
151
-
152
- // Batch full, execute it
153
- if (promises.length >= MAX_BATCH_SIZE) {
154
- await Promise.all(promises)
155
- promises = []
156
- }
157
- }
158
- }
159
-
160
- // Delete the set, this will also take care of executing pending operation for a non full batch
161
- promises.push(this.#store.del(tagsKey))
162
- await Promise.all(promises)
163
- promises = []
164
- }
165
- } catch (e) {
166
- this.#logger.error({ err: ensureLoggableError(e) }, 'Cannot expire cache tags in Valkey')
167
- throw new Error('Cannot expire cache tags in Valkey', { cause: e })
168
- }
169
- }
170
-
171
- async #refreshKey (key, value) {
172
- const life = Math.round((Date.now() - value.lastModified) / 1000)
173
- const expire = Math.min(value.revalidate - life, this.#maxTTL)
174
-
175
- if (expire < 1) {
176
- return
177
- }
178
-
179
- const promises = []
180
- promises.push(this.#store.expire(key, expire, 'gt'))
181
-
182
- if (Array.isArray(value.tags)) {
183
- for (const tag of value.tags) {
184
- const tagsKey = this.#keyFor(tag, sections.tags)
185
- promises.push(this.#store.expire(tagsKey, expire, 'gt'))
186
- }
187
- }
188
-
189
- await Promise.all(promises)
190
- }
191
-
192
- #createLogger () {
193
- const pinoOptions = {
194
- level: globalThis.platformatic?.logLevel ?? 'info'
195
- }
196
-
197
- if (this.serviceId) {
198
- pinoOptions.name = `cache:${this.serviceId}`
199
- }
200
-
201
- if (typeof globalThis.platformatic.workerId !== 'undefined') {
202
- pinoOptions.base = { pid: process.pid, hostname: hostname(), worker: this.workerId }
203
- }
204
-
205
- return pino(pinoOptions)
206
- }
207
-
208
- #getSubprefix () {
209
- const root = fileURLToPath(globalThis.platformatic.root)
210
-
211
- return existsSync(resolve(root, '.next/BUILD_ID'))
212
- ? (this.#subprefix = readFileSync(resolve(root, '.next/BUILD_ID'), 'utf-8').trim())
213
- : 'development'
214
- }
215
-
216
- #keyFor (key, section) {
217
- return keyFor(this.#config.prefix, this.#subprefix, section, key)
218
- }
219
-
220
- #serialize (data) {
221
- return pack(data).toString('base64url')
222
- }
223
-
224
- #deserialize (data) {
225
- return unpack(Buffer.from(data, 'base64url'))
226
- }
227
- }
228
-
229
- export default CacheHandler