@platformatic/next 2.19.0 → 2.20.0-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 +6 -0
- package/index.js +16 -2
- package/lib/caching/valkey.js +229 -0
- package/lib/loader.js +34 -12
- package/lib/schema.js +32 -1
- package/package.json +8 -6
- package/schema.json +36 -1
package/config.d.ts
CHANGED
package/index.js
CHANGED
|
@@ -21,6 +21,8 @@ import { packageJson, schema } from './lib/schema.js'
|
|
|
21
21
|
|
|
22
22
|
const supportedVersions = ['^14.0.0', '^15.0.0']
|
|
23
23
|
|
|
24
|
+
export * as cachingValkey from './lib/caching/valkey.js'
|
|
25
|
+
|
|
24
26
|
export class NextStackable extends BaseStackable {
|
|
25
27
|
#basePath
|
|
26
28
|
#next
|
|
@@ -61,8 +63,10 @@ export class NextStackable extends BaseStackable {
|
|
|
61
63
|
return this.stopCommand()
|
|
62
64
|
}
|
|
63
65
|
|
|
66
|
+
globalThis.platformatic.events.emit('plt:next:close')
|
|
67
|
+
|
|
64
68
|
if (this.isProduction) {
|
|
65
|
-
|
|
69
|
+
await new Promise((resolve, reject) => {
|
|
66
70
|
this.#server.close(error => {
|
|
67
71
|
/* c8 ignore next 3 */
|
|
68
72
|
if (error) {
|
|
@@ -72,6 +76,8 @@ export class NextStackable extends BaseStackable {
|
|
|
72
76
|
resolve()
|
|
73
77
|
})
|
|
74
78
|
})
|
|
79
|
+
|
|
80
|
+
await this.childManager.close()
|
|
75
81
|
} else {
|
|
76
82
|
const exitPromise = once(this.#child, 'exit')
|
|
77
83
|
await this.childManager.close()
|
|
@@ -102,7 +108,8 @@ export class NextStackable extends BaseStackable {
|
|
|
102
108
|
/* c8 ignore next 5 */
|
|
103
109
|
async getWatchConfig () {
|
|
104
110
|
return {
|
|
105
|
-
enabled: false
|
|
111
|
+
enabled: false,
|
|
112
|
+
path: this.root
|
|
106
113
|
}
|
|
107
114
|
}
|
|
108
115
|
|
|
@@ -137,6 +144,7 @@ export class NextStackable extends BaseStackable {
|
|
|
137
144
|
this.childManager = new ChildManager({
|
|
138
145
|
loader: loaderUrl,
|
|
139
146
|
context: {
|
|
147
|
+
config: this.configManager.current,
|
|
140
148
|
serviceId: this.serviceId,
|
|
141
149
|
workerId: this.workerId,
|
|
142
150
|
// Always use URL to avoid serialization problem in Windows
|
|
@@ -200,6 +208,7 @@ export class NextStackable extends BaseStackable {
|
|
|
200
208
|
this.childManager = new ChildManager({
|
|
201
209
|
loader: loaderUrl,
|
|
202
210
|
context: {
|
|
211
|
+
config: this.configManager.current,
|
|
203
212
|
serviceId: this.serviceId,
|
|
204
213
|
workerId: this.workerId,
|
|
205
214
|
// Always use URL to avoid serialization problem in Windows
|
|
@@ -220,6 +229,7 @@ export class NextStackable extends BaseStackable {
|
|
|
220
229
|
|
|
221
230
|
async #startProductionNext () {
|
|
222
231
|
try {
|
|
232
|
+
globalThis.platformatic.config = this.configManager.current
|
|
223
233
|
await this.childManager.inject()
|
|
224
234
|
const { nextStart } = await importFile(pathResolve(this.#next, './dist/cli/next-start.js'))
|
|
225
235
|
|
|
@@ -278,6 +288,10 @@ function transformConfig () {
|
|
|
278
288
|
this.current.watch = { enabled: this.current.watch || false }
|
|
279
289
|
}
|
|
280
290
|
|
|
291
|
+
if (this.current.cache?.adapter === 'redis') {
|
|
292
|
+
this.current.cache.adapter = 'valkey'
|
|
293
|
+
}
|
|
294
|
+
|
|
281
295
|
basicTransformConfig.call(this)
|
|
282
296
|
}
|
|
283
297
|
|
|
@@ -0,0 +1,229 @@
|
|
|
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
|
+
export 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
|
package/lib/loader.js
CHANGED
|
@@ -11,10 +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'
|
|
14
15
|
import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
15
16
|
|
|
16
17
|
const originalId = '__pltOriginalNextConfig'
|
|
17
18
|
|
|
19
|
+
let config
|
|
18
20
|
let candidates
|
|
19
21
|
let basePath = ''
|
|
20
22
|
|
|
@@ -35,6 +37,11 @@ function parseSingleExpression (expr) {
|
|
|
35
37
|
__pltOriginalNextConfig.basePath = basePath
|
|
36
38
|
}
|
|
37
39
|
|
|
40
|
+
if(typeof __pltOriginalNextConfig.cacheHandler === 'undefined') {
|
|
41
|
+
__pltOriginalNextConfig.cacheHandler = $PATH
|
|
42
|
+
__pltOriginalNextConfig.cacheMaxMemorySize = 0
|
|
43
|
+
}
|
|
44
|
+
|
|
38
45
|
// This is to send the configuraion when Next is executed in a child process (development)
|
|
39
46
|
globalThis[Symbol.for('plt.children.itc')]?.notify('config', __pltOriginalNextConfig)
|
|
40
47
|
|
|
@@ -45,21 +52,35 @@ function parseSingleExpression (expr) {
|
|
|
45
52
|
}
|
|
46
53
|
*/
|
|
47
54
|
function createEvaluatorWrapperFunction (original) {
|
|
55
|
+
const cacheHandler = config?.cache?.adapter
|
|
56
|
+
? fileURLToPath(new URL(`./caching/${config.cache.adapter}.js`, import.meta.url)).replaceAll(sep, '/')
|
|
57
|
+
: undefined
|
|
58
|
+
|
|
48
59
|
return functionDeclaration(
|
|
49
60
|
null,
|
|
50
61
|
[restElement(identifier('args'))],
|
|
51
|
-
blockStatement(
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
+
),
|
|
63
84
|
false,
|
|
64
85
|
true
|
|
65
86
|
)
|
|
@@ -125,6 +146,7 @@ export async function initialize (data) {
|
|
|
125
146
|
// Keep in sync with https://github.com/vercel/next.js/blob/main/packages/next/src/shared/lib/constants.ts
|
|
126
147
|
candidates = ['next.config.js', 'next.config.mjs'].map(c => new URL(c, realRoot).toString())
|
|
127
148
|
basePath = data.basePath ?? ''
|
|
149
|
+
config = data.config
|
|
128
150
|
}
|
|
129
151
|
|
|
130
152
|
export async function load (url, context, nextLoad) {
|
package/lib/schema.js
CHANGED
|
@@ -4,6 +4,36 @@ 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
|
+
|
|
7
37
|
export const schema = {
|
|
8
38
|
$id: `https://schemas.platformatic.dev/@platformatic/next/${packageJson.version}.json`,
|
|
9
39
|
$schema: 'http://json-schema.org/draft-07/schema#',
|
|
@@ -16,7 +46,8 @@ export const schema = {
|
|
|
16
46
|
logger: utilsSchemaComponents.logger,
|
|
17
47
|
server: utilsSchemaComponents.server,
|
|
18
48
|
watch: schemaComponents.watch,
|
|
19
|
-
application: schemaComponents.application
|
|
49
|
+
application: schemaComponents.application,
|
|
50
|
+
cache
|
|
20
51
|
},
|
|
21
52
|
additionalProperties: false
|
|
22
53
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@platformatic/next",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.20.0-alpha.1",
|
|
4
4
|
"description": "Platformatic Next.js Stackable",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -20,10 +20,12 @@
|
|
|
20
20
|
"@babel/traverse": "^7.25.3",
|
|
21
21
|
"@babel/types": "^7.25.2",
|
|
22
22
|
"amaro": "^0.2.0",
|
|
23
|
+
"iovalkey": "^0.2.1",
|
|
24
|
+
"msgpackr": "^1.11.2",
|
|
23
25
|
"semver": "^7.6.3",
|
|
24
|
-
"@platformatic/basic": "2.
|
|
25
|
-
"@platformatic/config": "2.
|
|
26
|
-
"@platformatic/utils": "2.
|
|
26
|
+
"@platformatic/basic": "2.20.0-alpha.1",
|
|
27
|
+
"@platformatic/config": "2.20.0-alpha.1",
|
|
28
|
+
"@platformatic/utils": "2.20.0-alpha.1"
|
|
27
29
|
},
|
|
28
30
|
"devDependencies": {
|
|
29
31
|
"@fastify/reply-from": "^11.0.0",
|
|
@@ -39,8 +41,8 @@
|
|
|
39
41
|
"react-dom": "^18.3.1",
|
|
40
42
|
"typescript": "^5.5.4",
|
|
41
43
|
"ws": "^8.18.0",
|
|
42
|
-
"@platformatic/composer": "2.
|
|
43
|
-
"@platformatic/service": "2.
|
|
44
|
+
"@platformatic/composer": "2.20.0-alpha.1",
|
|
45
|
+
"@platformatic/service": "2.20.0-alpha.1"
|
|
44
46
|
},
|
|
45
47
|
"scripts": {
|
|
46
48
|
"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.
|
|
2
|
+
"$id": "https://schemas.platformatic.dev/@platformatic/next/2.20.0-alpha.1.json",
|
|
3
3
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
4
4
|
"title": "Platformatic Next.js Stackable",
|
|
5
5
|
"type": "object",
|
|
@@ -296,6 +296,41 @@
|
|
|
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
|
|
299
334
|
}
|
|
300
335
|
},
|
|
301
336
|
"additionalProperties": false
|