@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 +0 -6
- package/index.js +48 -24
- package/lib/loader-next-15.cjs +52 -0
- package/lib/loader.js +15 -37
- package/lib/schema.js +1 -32
- package/package.json +10 -9
- package/schema.json +1 -36
- package/lib/caching/valkey.js +0 -229
package/config.d.ts
CHANGED
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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.
|
|
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
|
-
"
|
|
23
|
-
"msgpackr": "^1.11.2",
|
|
22
|
+
"amaro": "^0.2.0",
|
|
24
23
|
"semver": "^7.6.3",
|
|
25
|
-
"@platformatic/basic": "2.19.0-alpha.
|
|
26
|
-
"@platformatic/
|
|
27
|
-
"@platformatic/
|
|
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": "^
|
|
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.
|
|
42
|
-
"@platformatic/service": "2.19.0-alpha.
|
|
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.
|
|
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
|
package/lib/caching/valkey.js
DELETED
|
@@ -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
|