@platformatic/next 3.39.0 → 3.40.0

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
@@ -332,6 +332,12 @@ export interface PlatformaticNextJsConfig {
332
332
  */
333
333
  socket?: string;
334
334
  };
335
+ management?:
336
+ | boolean
337
+ | {
338
+ enabled?: boolean;
339
+ operations?: string[];
340
+ };
335
341
  metrics?:
336
342
  | boolean
337
343
  | {
@@ -662,6 +668,26 @@ export interface PlatformaticNextJsConfig {
662
668
  cert?: string;
663
669
  ca?: string;
664
670
  };
671
+ imageOptimizer?: {
672
+ enabled: boolean;
673
+ fallback: string;
674
+ storage?:
675
+ | {
676
+ type: "memory";
677
+ }
678
+ | {
679
+ type: "filesystem";
680
+ path?: string;
681
+ }
682
+ | {
683
+ type: "redis" | "valkey";
684
+ url: string;
685
+ prefix?: string;
686
+ db?: number | string;
687
+ };
688
+ timeout?: number | string;
689
+ maxAttempts?: number | string;
690
+ };
665
691
  };
666
692
  cache?: {
667
693
  enabled?: boolean | string;
package/index.js CHANGED
@@ -2,6 +2,7 @@ import { transform as basicTransform, resolve, validationOptions } from '@platfo
2
2
  import { kMetadata, loadConfiguration as utilsLoadConfiguration } from '@platformatic/foundation'
3
3
  import { resolve as resolvePath } from 'node:path'
4
4
  import { getCacheHandlerPath, NextCapability } from './lib/capability.js'
5
+ import { NextImageOptimizerCapability } from './lib/image-optimizer.js'
5
6
  import { schema } from './lib/schema.js'
6
7
 
7
8
  /* c8 ignore next 9 */
@@ -103,9 +104,12 @@ export async function loadConfiguration (configOrRoot, sourceOrConfig, context)
103
104
 
104
105
  export async function create (configOrRoot, sourceOrConfig, context) {
105
106
  const config = await loadConfiguration(configOrRoot, sourceOrConfig, context)
106
- return new NextCapability(config[kMetadata].root, config, context)
107
+
108
+ const Capability = config.next?.imageOptimizer?.enabled ? NextImageOptimizerCapability : NextCapability
109
+ return new Capability(config[kMetadata].root, config, context)
107
110
  }
108
111
 
109
112
  export * from './lib/capability.js'
110
113
  export * as errors from './lib/errors.js'
114
+ export * from './lib/image-optimizer.js'
111
115
  export { packageJson, schema, schemaComponents, version } from './lib/schema.js'
@@ -1,6 +1,6 @@
1
1
  import { buildPinoFormatters, buildPinoTimestamp } from '@platformatic/foundation'
2
- import { createRequire } from 'node:module'
3
2
  import { existsSync, readFileSync } from 'node:fs'
3
+ import { createRequire } from 'node:module'
4
4
  import { hostname } from 'node:os'
5
5
  import { resolve } from 'node:path'
6
6
  import { fileURLToPath } from 'node:url'
@@ -12,13 +12,6 @@ const require = createRequire(import.meta.url)
12
12
  let Redis
13
13
  let msgpackr
14
14
 
15
- function loadMsgpackr () {
16
- if (!msgpackr) {
17
- msgpackr = require('msgpackr')
18
- }
19
- return msgpackr
20
- }
21
-
22
15
  globalThis.platformatic ??= {}
23
16
  globalThis.platformatic.valkeyClients = new Map()
24
17
 
@@ -42,14 +35,22 @@ export function keyFor (prefix, subprefix, section, key) {
42
35
  return result
43
36
  }
44
37
 
38
+ export function ensureRedis () {
39
+ if (!Redis) {
40
+ Redis = require('iovalkey').Redis
41
+ }
42
+ }
43
+
44
+ export function ensureMsgpackr () {
45
+ if (!msgpackr) {
46
+ msgpackr = require('msgpackr')
47
+ }
48
+ }
49
+
45
50
  export function getConnection (url) {
46
51
  let client = globalThis.platformatic.valkeyClients.get(url)
47
52
 
48
53
  if (!client) {
49
- if (!Redis) {
50
- Redis = require('iovalkey').Redis
51
- loadMsgpackr()
52
- }
53
54
  client = new Redis(url, { enableAutoPipelining: true })
54
55
  globalThis.platformatic.valkeyClients.set(url, client)
55
56
 
@@ -109,9 +110,9 @@ export function getPlatformaticMeta () {
109
110
  }
110
111
 
111
112
  export function serialize (data) {
112
- return loadMsgpackr().pack(data).toString('base64url')
113
+ return msgpackr.pack(data).toString('base64url')
113
114
  }
114
115
 
115
116
  export function deserialize (data) {
116
- return loadMsgpackr().unpack(Buffer.from(data, 'base64url'))
117
+ return msgpackr.unpack(Buffer.from(data, 'base64url'))
117
118
  }
@@ -3,6 +3,8 @@ import { ReadableStream } from 'node:stream/web'
3
3
  import {
4
4
  createPlatformaticLogger,
5
5
  deserialize,
6
+ ensureMsgpackr,
7
+ ensureRedis,
6
8
  getConnection,
7
9
  getPlatformaticMeta,
8
10
  getPlatformaticSubprefix,
@@ -37,6 +39,9 @@ export class CacheHandler {
37
39
  #cacheMissMetric
38
40
 
39
41
  constructor () {
42
+ ensureRedis()
43
+ ensureMsgpackr()
44
+
40
45
  this.#config ??= globalThis.platformatic.config.cache
41
46
  this.#logger ??= createPlatformaticLogger()
42
47
  this.#store ??= getConnection(this.#config.url)
@@ -2,6 +2,8 @@ import { ensureLoggableError } from '@platformatic/foundation'
2
2
  import {
3
3
  createPlatformaticLogger,
4
4
  deserialize,
5
+ ensureMsgpackr,
6
+ ensureRedis,
5
7
  getConnection,
6
8
  getPlatformaticMeta,
7
9
  getPlatformaticSubprefix,
@@ -33,6 +35,9 @@ export class CacheHandler {
33
35
  constructor (options) {
34
36
  options ??= {}
35
37
 
38
+ ensureRedis()
39
+ ensureMsgpackr()
40
+
36
41
  this.#standalone = options.standalone
37
42
  this.#config = options.config
38
43
  this.#logger = options.logger
package/lib/capability.js CHANGED
@@ -20,7 +20,7 @@ import { parse, satisfies } from 'semver'
20
20
  import * as errors from './errors.js'
21
21
  import { version } from './schema.js'
22
22
 
23
- const supportedVersions = ['^14.0.0', '^15.0.0', '^16.0.0']
23
+ export const supportedVersions = ['^14.0.0', '^15.0.0', '^16.0.0']
24
24
 
25
25
  export function getCacheHandlerPath (name) {
26
26
  return fileURLToPath(new URL(`./caching/${name}.js`, import.meta.url)).replaceAll(sep, '/')
@@ -0,0 +1,283 @@
1
+ import {
2
+ BaseCapability,
3
+ errors as basicErrors,
4
+ createServerListener,
5
+ getServerUrl,
6
+ importFile,
7
+ resolvePackageViaCJS
8
+ } from '@platformatic/basic'
9
+ import { cleanBasePath } from '@platformatic/basic/lib/utils.js'
10
+ import { ensureLoggableError } from '@platformatic/foundation/lib/errors.js'
11
+ import { createQueue } from '@platformatic/image-optimizer'
12
+ import { FileStorage, MemoryStorage, RedisStorage } from '@platformatic/job-queue'
13
+ import inject from 'light-my-request'
14
+ import { readFile } from 'node:fs/promises'
15
+ import { createServer } from 'node:http'
16
+ import { dirname, resolve as resolvePath } from 'node:path'
17
+ import { satisfies } from 'semver'
18
+ import { supportedVersions } from './capability.js'
19
+ import { version } from './schema.js'
20
+
21
+ export class NextImageOptimizerCapability extends BaseCapability {
22
+ #basePath
23
+ #fallbackDomain
24
+ #next
25
+ #nextConfig
26
+ #validateParams
27
+ #app
28
+ #server
29
+ #dispatcher
30
+ #queue
31
+ #fetchTimeout
32
+
33
+ constructor (root, config, context) {
34
+ super('next-image', version, root, config, context)
35
+ }
36
+
37
+ async init () {
38
+ await super.init()
39
+
40
+ // This is needed to avoid Next.js to throw an error when the lockfile is not correct
41
+ // and the user is using npm but has pnpm in its $PATH.
42
+ //
43
+ // See: https://github.com/platformatic/composer-next-node-fastify/pull/3
44
+ //
45
+ // PS by Paolo: Sob.
46
+ process.env.NEXT_IGNORE_INCORRECT_LOCKFILE = 'true'
47
+
48
+ this.#next = resolvePath(dirname(await resolvePackageViaCJS(this.root, 'next')), '../..')
49
+ const nextPackage = JSON.parse(await readFile(resolvePath(this.#next, 'package.json'), 'utf-8'))
50
+
51
+ /* c8 ignore next 3 */
52
+ if (!this.isProduction && !supportedVersions.some(v => satisfies(nextPackage.version, v))) {
53
+ throw new basicErrors.UnsupportedVersion('next', nextPackage.version, supportedVersions)
54
+ }
55
+
56
+ const imageOptimizerConfig = this.config.next?.imageOptimizer ?? {}
57
+
58
+ this.#fetchTimeout = imageOptimizerConfig?.timeout ?? 30000
59
+ this.#fallbackDomain = this.config.next?.imageOptimizer?.fallback
60
+
61
+ // If it's not a full URL, it's a local service
62
+ if (!this.#fallbackDomain.startsWith('http://') && !this.#fallbackDomain.startsWith('https://')) {
63
+ this.#fallbackDomain = `http://${this.#fallbackDomain}.plt.local`
64
+ }
65
+
66
+ if (this.#fallbackDomain.endsWith('/')) {
67
+ this.#fallbackDomain = this.#fallbackDomain.slice(0, -1)
68
+ }
69
+
70
+ this.#queue = await this.#createQueue(imageOptimizerConfig)
71
+ }
72
+
73
+ async start ({ listen }) {
74
+ // Make this idempotent
75
+ if (this.url) {
76
+ return this.url
77
+ }
78
+
79
+ const config = this.config
80
+ this.#basePath = config.application?.basePath ? cleanBasePath(config.application?.basePath) : ''
81
+ await super._start({ listen })
82
+
83
+ if (this.#app && listen) {
84
+ const serverOptions = this.serverConfig
85
+ const listenOptions = { host: serverOptions?.hostname || '127.0.0.1', port: serverOptions?.port || 0 }
86
+
87
+ if (typeof serverOptions?.backlog === 'number') {
88
+ createServerListener(false, false, { backlog: serverOptions.backlog })
89
+ listenOptions.backlog = serverOptions.backlog
90
+ }
91
+
92
+ this.#server = await new Promise((resolve, reject) => {
93
+ return this.#app
94
+ .listen(listenOptions, function () {
95
+ resolve(this)
96
+ })
97
+ .on('error', reject)
98
+ })
99
+
100
+ this.url = getServerUrl(this.#server)
101
+
102
+ return this.url
103
+ }
104
+
105
+ const { ImageOptimizerCache } = await importFile(resolvePath(this.#next, './dist/server/image-optimizer.js'))
106
+ this.#validateParams = ImageOptimizerCache.validateParams.bind(ImageOptimizerCache)
107
+
108
+ try {
109
+ const { default: loadConfigAPI } = await importFile(resolvePath(this.#next, './dist/server/config.js'))
110
+ this.#nextConfig = await loadConfigAPI.default('production', this.root)
111
+ } catch (error) {
112
+ this.logger.error({ err: ensureLoggableError(error) }, 'Error loading Next.js configuration.')
113
+ throw new Error('Failed to load Next.js configuration.', { cause: error })
114
+ }
115
+
116
+ this.#app = createServer(this.#handleRequest.bind(this))
117
+ this.#dispatcher = this.#app.listeners('request')[0]
118
+ await this.#queue.start()
119
+ }
120
+
121
+ async stop () {
122
+ await super.stop()
123
+ await this.#queue?.stop()
124
+
125
+ globalThis.platformatic.events.emit('plt:next:close')
126
+
127
+ if (!this.#app || !this.#server?.listening) {
128
+ return
129
+ }
130
+ const { promise, resolve, reject } = Promise.withResolvers()
131
+
132
+ this.#server.close(error => {
133
+ if (error) {
134
+ return reject(error)
135
+ }
136
+
137
+ resolve()
138
+ })
139
+
140
+ return promise
141
+ }
142
+
143
+ /* c8 ignore next 5 */
144
+ async getWatchConfig () {
145
+ return {
146
+ enabled: false,
147
+ path: this.root
148
+ }
149
+ }
150
+
151
+ getMeta () {
152
+ const gateway = { prefix: this.basePath ?? this.#basePath, wantsAbsoluteUrls: true, needsRootTrailingSlash: false }
153
+
154
+ if (this.url) {
155
+ gateway.tcp = true
156
+ gateway.url = this.url
157
+ }
158
+
159
+ return { gateway }
160
+ }
161
+
162
+ async inject (injectParams, onInject) {
163
+ this.logger.trace({ injectParams }, 'injecting via light-my-request')
164
+ const res = inject(this.#dispatcher ?? this.#app, injectParams, onInject)
165
+
166
+ /* c8 ignore next 3 */
167
+ if (onInject) {
168
+ return
169
+ }
170
+
171
+ // Since inject might be called from the main thread directly via ITC, let's clean it up
172
+ const { statusCode, headers, body, payload, rawPayload } = res
173
+
174
+ return { statusCode, headers, body, payload, rawPayload }
175
+ }
176
+
177
+ async #createQueue (imageOptimizerConfig) {
178
+ const queueOptions = {
179
+ visibilityTimeout: imageOptimizerConfig.timeout,
180
+ maxRetries: imageOptimizerConfig.maxAttempts,
181
+ logger: this.logger
182
+ }
183
+
184
+ if (imageOptimizerConfig.storage.type === 'memory') {
185
+ queueOptions.storage = new MemoryStorage()
186
+ } else if (imageOptimizerConfig.storage.type === 'filesystem') {
187
+ queueOptions.storage = new FileStorage({
188
+ basePath: imageOptimizerConfig.storage.path ?? resolvePath(this.root, '.next/cache/image-optimizer')
189
+ })
190
+ } else {
191
+ const redisStorageOptions = {
192
+ url: this.#buildRedisStorageUrl(imageOptimizerConfig.storage.url, imageOptimizerConfig.storage.db)
193
+ }
194
+
195
+ if (typeof imageOptimizerConfig.storage.prefix === 'string') {
196
+ redisStorageOptions.keyPrefix = imageOptimizerConfig.storage.prefix
197
+ }
198
+
199
+ queueOptions.storage = new RedisStorage(redisStorageOptions)
200
+ }
201
+
202
+ return createQueue(queueOptions)
203
+ }
204
+
205
+ #buildRedisStorageUrl (url, db) {
206
+ if (typeof db === 'undefined') {
207
+ return url
208
+ }
209
+
210
+ const parsedUrl = new URL(url)
211
+ parsedUrl.pathname = `/${db}`
212
+ return parsedUrl.toString()
213
+ }
214
+
215
+ async #handleRequest (request, response) {
216
+ const { pathname, searchParams } = new URL(request.url, 'http://localhost')
217
+ const imagePath = `${this.#basePath}/_next/image`
218
+
219
+ if (request.method !== 'GET' || pathname !== imagePath) {
220
+ response.statusCode = 404
221
+ response.end('Not Found')
222
+ return
223
+ }
224
+
225
+ const query = {}
226
+
227
+ for (const [key, value] of searchParams.entries()) {
228
+ if (typeof query[key] === 'undefined') {
229
+ query[key] = value
230
+ } else if (Array.isArray(query[key])) {
231
+ query[key].push(value)
232
+ } else {
233
+ query[key] = [query[key], value]
234
+ }
235
+ }
236
+
237
+ try {
238
+ // Extract and validate the parameters
239
+ const params = this.#validateParams(request, query, this.#nextConfig, false)
240
+
241
+ if (params.errorMessage) {
242
+ const error = new Error('Invalid optimization parameters.')
243
+ error.reason = params.errorMessage
244
+ throw error
245
+ }
246
+
247
+ let url
248
+ if (params.isAbsolute) {
249
+ url = params.href
250
+ } else {
251
+ url = `${this.#fallbackDomain}${params.href.startsWith('/') ? '' : '/'}${params.href}`
252
+ }
253
+
254
+ const result = await this.#queue.fetchAndOptimize(
255
+ url,
256
+ params.width,
257
+ params.quality,
258
+ this.#nextConfig.images.dangerouslyAllowSVG,
259
+ { timeout: this.#fetchTimeout }
260
+ )
261
+ const buffer = Buffer.from(result.buffer, 'base64')
262
+
263
+ response.statusCode = 200
264
+ response.setHeader('Content-Type', result.contentType)
265
+ response.setHeader('Cache-Control', result.cacheControl)
266
+ response.end(buffer)
267
+ } catch (error) {
268
+ response.statusCode = 502
269
+ response.setHeader('Content-Type', 'application/json; charset=utf-8')
270
+ response.end(
271
+ JSON.stringify({
272
+ error: 'Bad Gateway',
273
+ message: 'An error occurred while optimizing the image.',
274
+ statusCode: 502,
275
+ cause: {
276
+ ...ensureLoggableError(error.originalError ? JSON.parse(error.originalError) : error),
277
+ stack: undefined
278
+ }
279
+ })
280
+ )
281
+ }
282
+ }
283
+ }
package/lib/schema.js CHANGED
@@ -97,6 +97,107 @@ const next = {
97
97
  }
98
98
  },
99
99
  additionalProperties: false
100
+ },
101
+ imageOptimizer: {
102
+ type: 'object',
103
+ properties: {
104
+ enabled: {
105
+ type: 'boolean',
106
+ default: false
107
+ },
108
+ fallback: {
109
+ type: 'string'
110
+ },
111
+ storage: {
112
+ oneOf: [
113
+ {
114
+ type: 'object',
115
+ properties: {
116
+ type: {
117
+ const: 'memory'
118
+ }
119
+ },
120
+ required: ['type'],
121
+ additionalProperties: false
122
+ },
123
+ {
124
+ type: 'object',
125
+ properties: {
126
+ type: {
127
+ const: 'filesystem'
128
+ },
129
+ path: {
130
+ type: 'string',
131
+ resolvePath: true
132
+ }
133
+ },
134
+ required: ['type'],
135
+ additionalProperties: false
136
+ },
137
+ {
138
+ type: 'object',
139
+ properties: {
140
+ type: {
141
+ type: 'string',
142
+ enum: ['redis', 'valkey']
143
+ },
144
+ url: {
145
+ type: 'string'
146
+ },
147
+ prefix: {
148
+ type: 'string'
149
+ },
150
+ db: {
151
+ anyOf: [
152
+ {
153
+ type: 'number',
154
+ minimum: 0,
155
+ default: 0
156
+ },
157
+ {
158
+ type: 'string',
159
+ pattern: '^[0-9]+$'
160
+ }
161
+ ]
162
+ }
163
+ },
164
+ required: ['type', 'url'],
165
+ additionalProperties: false
166
+ }
167
+ ],
168
+ default: {
169
+ type: 'memory'
170
+ }
171
+ },
172
+ timeout: {
173
+ default: 30000,
174
+ anyOf: [
175
+ {
176
+ type: 'number',
177
+ minimum: 0
178
+ },
179
+ {
180
+ type: 'string',
181
+ pattern: '^[0-9]+$'
182
+ }
183
+ ]
184
+ },
185
+ maxAttempts: {
186
+ default: 3,
187
+ anyOf: [
188
+ {
189
+ type: 'number',
190
+ minimum: 1
191
+ },
192
+ {
193
+ type: 'string',
194
+ pattern: '^[1-9][0-9]*$'
195
+ }
196
+ ]
197
+ }
198
+ },
199
+ required: ['enabled', 'fallback'],
200
+ additionalProperties: false
100
201
  }
101
202
  },
102
203
  default: {},
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@platformatic/next",
3
- "version": "3.39.0",
3
+ "version": "3.40.0",
4
4
  "description": "Platformatic Next.js Capability",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -19,12 +19,14 @@
19
19
  "@babel/parser": "^7.25.3",
20
20
  "@babel/traverse": "^7.25.3",
21
21
  "@babel/types": "^7.25.2",
22
+ "@platformatic/image-optimizer": "^0.2.0",
22
23
  "amaro": "^0.3.0",
23
24
  "iovalkey": "^0.3.0",
25
+ "light-my-request": "^6.0.0",
24
26
  "msgpackr": "^1.11.2",
25
27
  "semver": "^7.6.3",
26
- "@platformatic/foundation": "3.39.0",
27
- "@platformatic/basic": "3.39.0"
28
+ "@platformatic/basic": "3.40.0",
29
+ "@platformatic/foundation": "3.40.0"
28
30
  },
29
31
  "devDependencies": {
30
32
  "@fastify/reply-from": "^12.0.0",
@@ -34,14 +36,14 @@
34
36
  "cleaner-spec-reporter": "^0.5.0",
35
37
  "eslint": "9",
36
38
  "execa": "^9.5.1",
37
- "fastify": "^5.7.0",
39
+ "fastify": "^5.7.3",
38
40
  "json-schema-to-typescript": "^15.0.1",
39
41
  "neostandard": "^0.12.0",
40
42
  "next": "^16.1.0",
41
43
  "typescript": "^5.5.4",
42
44
  "ws": "^8.18.0",
43
- "@platformatic/gateway": "3.39.0",
44
- "@platformatic/service": "3.39.0"
45
+ "@platformatic/gateway": "3.40.0",
46
+ "@platformatic/service": "3.40.0"
45
47
  },
46
48
  "engines": {
47
49
  "node": ">=22.19.0"
package/schema.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "$id": "https://schemas.platformatic.dev/@platformatic/next/3.39.0.json",
2
+ "$id": "https://schemas.platformatic.dev/@platformatic/next/3.40.0.json",
3
3
  "$schema": "http://json-schema.org/draft-07/schema#",
4
4
  "title": "Platformatic Next.js Config",
5
5
  "type": "object",
@@ -736,6 +736,29 @@
736
736
  "additionalProperties": false
737
737
  }
738
738
  ]
739
+ },
740
+ "management": {
741
+ "anyOf": [
742
+ {
743
+ "type": "boolean"
744
+ },
745
+ {
746
+ "type": "object",
747
+ "properties": {
748
+ "enabled": {
749
+ "type": "boolean",
750
+ "default": true
751
+ },
752
+ "operations": {
753
+ "type": "array",
754
+ "items": {
755
+ "type": "string"
756
+ }
757
+ }
758
+ },
759
+ "additionalProperties": false
760
+ }
761
+ ]
739
762
  }
740
763
  }
741
764
  }
@@ -1475,6 +1498,29 @@
1475
1498
  ],
1476
1499
  "default": true
1477
1500
  },
1501
+ "management": {
1502
+ "anyOf": [
1503
+ {
1504
+ "type": "boolean"
1505
+ },
1506
+ {
1507
+ "type": "object",
1508
+ "properties": {
1509
+ "enabled": {
1510
+ "type": "boolean",
1511
+ "default": true
1512
+ },
1513
+ "operations": {
1514
+ "type": "array",
1515
+ "items": {
1516
+ "type": "string"
1517
+ }
1518
+ }
1519
+ },
1520
+ "additionalProperties": false
1521
+ }
1522
+ ]
1523
+ },
1478
1524
  "metrics": {
1479
1525
  "anyOf": [
1480
1526
  {
@@ -2437,6 +2483,120 @@
2437
2483
  }
2438
2484
  },
2439
2485
  "additionalProperties": false
2486
+ },
2487
+ "imageOptimizer": {
2488
+ "type": "object",
2489
+ "properties": {
2490
+ "enabled": {
2491
+ "type": "boolean",
2492
+ "default": false
2493
+ },
2494
+ "fallback": {
2495
+ "type": "string"
2496
+ },
2497
+ "storage": {
2498
+ "oneOf": [
2499
+ {
2500
+ "type": "object",
2501
+ "properties": {
2502
+ "type": {
2503
+ "const": "memory"
2504
+ }
2505
+ },
2506
+ "required": [
2507
+ "type"
2508
+ ],
2509
+ "additionalProperties": false
2510
+ },
2511
+ {
2512
+ "type": "object",
2513
+ "properties": {
2514
+ "type": {
2515
+ "const": "filesystem"
2516
+ },
2517
+ "path": {
2518
+ "type": "string",
2519
+ "resolvePath": true
2520
+ }
2521
+ },
2522
+ "required": [
2523
+ "type"
2524
+ ],
2525
+ "additionalProperties": false
2526
+ },
2527
+ {
2528
+ "type": "object",
2529
+ "properties": {
2530
+ "type": {
2531
+ "type": "string",
2532
+ "enum": [
2533
+ "redis",
2534
+ "valkey"
2535
+ ]
2536
+ },
2537
+ "url": {
2538
+ "type": "string"
2539
+ },
2540
+ "prefix": {
2541
+ "type": "string"
2542
+ },
2543
+ "db": {
2544
+ "anyOf": [
2545
+ {
2546
+ "type": "number",
2547
+ "minimum": 0,
2548
+ "default": 0
2549
+ },
2550
+ {
2551
+ "type": "string",
2552
+ "pattern": "^[0-9]+$"
2553
+ }
2554
+ ]
2555
+ }
2556
+ },
2557
+ "required": [
2558
+ "type",
2559
+ "url"
2560
+ ],
2561
+ "additionalProperties": false
2562
+ }
2563
+ ],
2564
+ "default": {
2565
+ "type": "memory"
2566
+ }
2567
+ },
2568
+ "timeout": {
2569
+ "default": 30000,
2570
+ "anyOf": [
2571
+ {
2572
+ "type": "number",
2573
+ "minimum": 0
2574
+ },
2575
+ {
2576
+ "type": "string",
2577
+ "pattern": "^[0-9]+$"
2578
+ }
2579
+ ]
2580
+ },
2581
+ "maxAttempts": {
2582
+ "default": 3,
2583
+ "anyOf": [
2584
+ {
2585
+ "type": "number",
2586
+ "minimum": 1
2587
+ },
2588
+ {
2589
+ "type": "string",
2590
+ "pattern": "^[1-9][0-9]*$"
2591
+ }
2592
+ ]
2593
+ }
2594
+ },
2595
+ "required": [
2596
+ "enabled",
2597
+ "fallback"
2598
+ ],
2599
+ "additionalProperties": false
2440
2600
  }
2441
2601
  },
2442
2602
  "default": {},