velocious 1.0.692 → 1.0.694

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.
Files changed (99) hide show
  1. package/README.md +5 -0
  2. package/build/background-jobs/scheduled-enqueue-bound.js +18 -0
  3. package/build/background-jobs/scheduled-enqueue-timeout-error.js +25 -0
  4. package/build/background-jobs/scheduler.js +121 -4
  5. package/build/cli/commands/mcp.js +7 -0
  6. package/build/cli/index.js +1 -1
  7. package/build/configuration-types.js +9 -0
  8. package/build/configuration.js +77 -2
  9. package/build/database/advisory-lock-bound.js +16 -0
  10. package/build/database/drivers/base.js +23 -2
  11. package/build/database/drivers/pgsql/index.js +27 -4
  12. package/build/environment-handlers/base.js +9 -0
  13. package/build/environment-handlers/node/cli/commands/mcp.js +87 -0
  14. package/build/environment-handlers/node.js +10 -0
  15. package/build/mcp/dispatcher.js +271 -0
  16. package/build/mcp/json-rpc-server.js +167 -0
  17. package/build/mcp/manifest-tools.js +310 -0
  18. package/build/routes/built-in/mcp/controller.js +63 -0
  19. package/build/routes/catch-all-route.js +58 -0
  20. package/build/routes/resolver.js +21 -8
  21. package/build/routes/root-route.js +26 -1
  22. package/build/src/background-jobs/scheduled-enqueue-bound.d.ts +15 -0
  23. package/build/src/background-jobs/scheduled-enqueue-bound.d.ts.map +1 -0
  24. package/build/src/background-jobs/scheduled-enqueue-bound.js +17 -0
  25. package/build/src/background-jobs/scheduled-enqueue-timeout-error.d.ts +24 -0
  26. package/build/src/background-jobs/scheduled-enqueue-timeout-error.d.ts.map +1 -0
  27. package/build/src/background-jobs/scheduled-enqueue-timeout-error.js +23 -0
  28. package/build/src/background-jobs/scheduler.d.ts +40 -0
  29. package/build/src/background-jobs/scheduler.d.ts.map +1 -1
  30. package/build/src/background-jobs/scheduler.js +111 -5
  31. package/build/src/cli/commands/mcp.d.ts +5 -0
  32. package/build/src/cli/commands/mcp.d.ts.map +1 -0
  33. package/build/src/cli/commands/mcp.js +7 -0
  34. package/build/src/cli/index.js +2 -2
  35. package/build/src/configuration-types.d.ts +30 -0
  36. package/build/src/configuration-types.d.ts.map +1 -1
  37. package/build/src/configuration-types.js +9 -1
  38. package/build/src/configuration.d.ts +24 -1
  39. package/build/src/configuration.d.ts.map +1 -1
  40. package/build/src/configuration.js +69 -3
  41. package/build/src/database/advisory-lock-bound.d.ts +15 -0
  42. package/build/src/database/advisory-lock-bound.d.ts.map +1 -0
  43. package/build/src/database/advisory-lock-bound.js +16 -0
  44. package/build/src/database/drivers/base.d.ts +13 -1
  45. package/build/src/database/drivers/base.d.ts.map +1 -1
  46. package/build/src/database/drivers/base.js +23 -3
  47. package/build/src/database/drivers/pgsql/index.d.ts +15 -3
  48. package/build/src/database/drivers/pgsql/index.d.ts.map +1 -1
  49. package/build/src/database/drivers/pgsql/index.js +23 -5
  50. package/build/src/environment-handlers/base.d.ts +6 -0
  51. package/build/src/environment-handlers/base.d.ts.map +1 -1
  52. package/build/src/environment-handlers/base.js +9 -1
  53. package/build/src/environment-handlers/node/cli/commands/mcp.d.ts +19 -0
  54. package/build/src/environment-handlers/node/cli/commands/mcp.d.ts.map +1 -0
  55. package/build/src/environment-handlers/node/cli/commands/mcp.js +70 -0
  56. package/build/src/environment-handlers/node.d.ts +6 -0
  57. package/build/src/environment-handlers/node.d.ts.map +1 -1
  58. package/build/src/environment-handlers/node.js +10 -1
  59. package/build/src/mcp/dispatcher.d.ts +133 -0
  60. package/build/src/mcp/dispatcher.d.ts.map +1 -0
  61. package/build/src/mcp/dispatcher.js +232 -0
  62. package/build/src/mcp/json-rpc-server.d.ts +47 -0
  63. package/build/src/mcp/json-rpc-server.d.ts.map +1 -0
  64. package/build/src/mcp/json-rpc-server.js +142 -0
  65. package/build/src/mcp/manifest-tools.d.ts +76 -0
  66. package/build/src/mcp/manifest-tools.d.ts.map +1 -0
  67. package/build/src/mcp/manifest-tools.js +273 -0
  68. package/build/src/routes/built-in/mcp/controller.d.ts +14 -0
  69. package/build/src/routes/built-in/mcp/controller.d.ts.map +1 -0
  70. package/build/src/routes/built-in/mcp/controller.js +55 -0
  71. package/build/src/routes/catch-all-route.d.ts +48 -0
  72. package/build/src/routes/catch-all-route.d.ts.map +1 -0
  73. package/build/src/routes/catch-all-route.js +50 -0
  74. package/build/src/routes/resolver.d.ts.map +1 -1
  75. package/build/src/routes/resolver.js +23 -9
  76. package/build/src/routes/root-route.d.ts +15 -1
  77. package/build/src/routes/root-route.d.ts.map +1 -1
  78. package/build/src/routes/root-route.js +24 -2
  79. package/package.json +1 -1
  80. package/src/background-jobs/scheduled-enqueue-bound.js +18 -0
  81. package/src/background-jobs/scheduled-enqueue-timeout-error.js +25 -0
  82. package/src/background-jobs/scheduler.js +121 -4
  83. package/src/cli/commands/mcp.js +7 -0
  84. package/src/cli/index.js +1 -1
  85. package/src/configuration-types.js +9 -0
  86. package/src/configuration.js +77 -2
  87. package/src/database/advisory-lock-bound.js +16 -0
  88. package/src/database/drivers/base.js +23 -2
  89. package/src/database/drivers/pgsql/index.js +27 -4
  90. package/src/environment-handlers/base.js +9 -0
  91. package/src/environment-handlers/node/cli/commands/mcp.js +87 -0
  92. package/src/environment-handlers/node.js +10 -0
  93. package/src/mcp/dispatcher.js +271 -0
  94. package/src/mcp/json-rpc-server.js +167 -0
  95. package/src/mcp/manifest-tools.js +310 -0
  96. package/src/routes/built-in/mcp/controller.js +63 -0
  97. package/src/routes/catch-all-route.js +58 -0
  98. package/src/routes/resolver.js +21 -8
  99. package/src/routes/root-route.js +26 -1
@@ -0,0 +1,310 @@
1
+ // @ts-check
2
+
3
+ /**
4
+ * @typedef {object} ManifestCommandEntry
5
+ * @property {Array<{name: string, type: string}>} [args] - Declared typed command arguments.
6
+ * @property {string} methodName - Command method name.
7
+ * @property {string} [path] - Manifest command path (member paths carry `<id>`).
8
+ * @property {string | null} [returnType] - Declared JSDoc return type.
9
+ * @property {"collection" | "member"} scope - Command scope.
10
+ */
11
+
12
+ /**
13
+ * @typedef {object} ManifestResourceEntry
14
+ * @property {Record<string, ManifestCommandEntry[]>} [commands] - Scope-keyed custom command entries.
15
+ * @property {Record<string, Record<string, string>>} [builtInCommands] - Scope-keyed built-in command maps (methodName → slug).
16
+ * @property {string} modelName - Backend model name.
17
+ * @property {string | string[]} [primaryKey] - Primary key attribute name(s).
18
+ */
19
+
20
+ /**
21
+ * @typedef {Record<string, unknown> & {formatVersion?: number, resources?: Record<string, unknown>}} ApiManifest
22
+ */
23
+
24
+ import inflection from "inflection"
25
+
26
+ const MCP_TOOL_NAME_PATTERN = /^[a-z0-9_]+$/
27
+
28
+ /** @type {Record<string, string>} */
29
+ const BUILT_IN_COMMAND_DESCRIPTIONS = {
30
+ attach: "Attach a file attachment to one record.",
31
+ attachmentList: "List the attachments of one record.",
32
+ create: "Create one record with `attributes` (and optional `nestedAttributes`).",
33
+ destroy: "Destroy one record by primary key `id`.",
34
+ download: "Download one attachment of one record.",
35
+ find: "Find one record by primary key `id`.",
36
+ index: "List records with optional `where`, `searches`, `sort`, `limit`, `offset`, `page`, `perPage`, `preload`, `select`, `count`, `pluck`, `distinct`, `group`, `joins`, and `queryData`.",
37
+ update: "Update one record by primary key `id` with `attributes` (and optional `nestedAttributes`).",
38
+ url: "Resolve the download URL of one attachment of one record."
39
+ }
40
+
41
+ const META_TOOL_NAME = "frontend_model_api_manifest"
42
+ const META_TOOL_DESCRIPTION = "Return the full frontend-model API manifest (all resources, attributes, abilities, and commands) so callers can discover the available model commands."
43
+
44
+ /**
45
+ * Maps one JSDoc type string from command metadata to a JSON Schema fragment.
46
+ * @param {string} type - JSDoc type string (for example `string`, `number`, `{id: number}`).
47
+ * @returns {{items?: {type: string}, type: string}} - JSON Schema type fragment.
48
+ */
49
+ function jsonSchemaTypeForJSDocType(type) {
50
+ const normalizedType = type.trim()
51
+
52
+ if (normalizedType === "number" || normalizedType === "integer") return {type: "number"}
53
+ if (normalizedType === "boolean") return {type: "boolean"}
54
+ if (normalizedType.startsWith("Array<") || normalizedType.endsWith("[]")) return {items: {type: "string"}, type: "array"}
55
+
56
+ return {type: "string"}
57
+ }
58
+
59
+ /**
60
+ * Builds the MCP tool name for one model command.
61
+ * @param {object} args - Arguments.
62
+ * @param {string} args.modelKey - Manifest resource key (configured model name).
63
+ * @param {string} args.commandName - Command method name.
64
+ * @param {"collection" | "member"} args.scope - Command scope.
65
+ * @returns {string} - MCP tool name.
66
+ */
67
+ function mcpToolName({commandName, modelKey, scope}) {
68
+ const modelPart = inflection.underscore(modelKey).toLowerCase()
69
+ const commandPart = inflection.underscore(commandName).toLowerCase()
70
+ const scopePart = scope === "member" ? "_by_id" : ""
71
+
72
+ return `${modelPart}_${commandPart}${scopePart}`
73
+ }
74
+
75
+ /**
76
+ * Builds the primary key input properties for one manifest resource.
77
+ * @param {string | string[] | undefined} primaryKey - Resource primary key attribute name(s).
78
+ * @returns {Record<string, {description: string, type: string}>} - Primary key JSON Schema properties.
79
+ */
80
+ function primaryKeyProperties(primaryKey) {
81
+ const keyAttributes = Array.isArray(primaryKey) ? primaryKey : [primaryKey || "id"]
82
+ /** @type {Record<string, {description: string, type: string}>} */
83
+ const properties = {}
84
+
85
+ for (const keyAttribute of keyAttributes) {
86
+ properties[keyAttribute] = {description: `Primary key attribute \`${keyAttribute}\` of the record.`, type: "string"}
87
+ }
88
+
89
+ return properties
90
+ }
91
+
92
+ /**
93
+ * Builds the fixed input schema for one built-in command.
94
+ * @param {object} args - Arguments.
95
+ * @param {string} args.commandName - Built-in command name.
96
+ * @param {string | string[] | undefined} args.primaryKey - Resource primary key attribute name(s).
97
+ * @returns {Record<string, unknown>} - JSON Schema object for the tool input.
98
+ */
99
+ function builtInCommandInputSchema({commandName, primaryKey}) {
100
+ if (commandName === "index") {
101
+ return {
102
+ additionalProperties: false,
103
+ properties: {
104
+ count: {description: "Return only the count of matching records.", type: "boolean"},
105
+ distinct: {description: "Apply DISTINCT to the index query.", type: "boolean"},
106
+ group: {description: "Group the index query by attribute names or relationship-path descriptors.", type: "object"},
107
+ joins: {description: "Relationship-path join descriptors.", type: "object"},
108
+ limit: {description: "Maximum number of records to return.", type: "number"},
109
+ offset: {description: "Number of records to skip.", type: "number"},
110
+ page: {description: "1-based page number (combine with `perPage`).", type: "number"},
111
+ perPage: {description: "Records per page.", type: "number"},
112
+ pluck: {description: "Attribute names or relationship-path descriptors to pluck instead of full records.", type: "array"},
113
+ preload: {description: "Relationship preload descriptors.", type: "object"},
114
+ queryData: {description: "Structured query-data descriptor.", type: "object"},
115
+ searches: {description: "Search descriptors (path/column/operator/value).", type: "array"},
116
+ select: {description: "Selected attribute names per model.", type: "object"},
117
+ selectsExtra: {description: "Extra selected attributes.", type: "object"},
118
+ sort: {description: "Sort definitions (column or relationship-path descriptors).", type: "array"},
119
+ where: {description: "Attribute filter hash.", type: "object"},
120
+ withCount: {description: "Include the total count alongside records.", type: "boolean"}
121
+ },
122
+ type: "object"
123
+ }
124
+ }
125
+
126
+ if (commandName === "create") {
127
+ return {
128
+ additionalProperties: false,
129
+ properties: {
130
+ attributes: {description: "Writable attribute values for the new record.", type: "object"},
131
+ nestedAttributes: {description: "Nested relationship attribute payloads.", type: "object"}
132
+ },
133
+ required: ["attributes"],
134
+ type: "object"
135
+ }
136
+ }
137
+
138
+ if (commandName === "update") {
139
+ const idProperties = primaryKeyProperties(primaryKey)
140
+
141
+ return {
142
+ additionalProperties: false,
143
+ properties: {
144
+ attributes: {description: "Writable attribute values to apply.", type: "object"},
145
+ ...idProperties,
146
+ nestedAttributes: {description: "Nested relationship attribute payloads.", type: "object"}
147
+ },
148
+ required: [...Object.keys(idProperties), "attributes"],
149
+ type: "object"
150
+ }
151
+ }
152
+
153
+ // find / destroy / attach / download / url / attachmentList are id-addressed
154
+ const idProperties = primaryKeyProperties(primaryKey)
155
+
156
+ return {
157
+ additionalProperties: false,
158
+ properties: {...idProperties},
159
+ required: [...Object.keys(idProperties)],
160
+ type: "object"
161
+ }
162
+ }
163
+
164
+ /**
165
+ * Builds the input schema for one custom command from its manifest argument metadata.
166
+ * @param {object} args - Arguments.
167
+ * @param {ManifestCommandEntry} args.entry - Manifest command entry.
168
+ * @param {string | string[] | undefined} args.primaryKey - Resource primary key attribute name(s).
169
+ * @returns {Record<string, unknown>} - JSON Schema object for the tool input.
170
+ */
171
+ function customCommandInputSchema({entry, primaryKey}) {
172
+ const commandArgs = entry.args || []
173
+ /** @type {Record<string, {description?: string, items?: {items?: {type: string}, type: string}, type: string}>} */
174
+ const properties = {}
175
+
176
+ for (const commandArg of commandArgs) {
177
+ properties[commandArg.name] = jsonSchemaTypeForJSDocType(commandArg.type)
178
+ }
179
+
180
+ if (entry.scope === "member") {
181
+ for (const [keyAttribute, keySchema] of Object.entries(primaryKeyProperties(primaryKey))) {
182
+ if (!properties[keyAttribute]) properties[keyAttribute] = keySchema
183
+ }
184
+ }
185
+
186
+ return {
187
+ additionalProperties: true,
188
+ properties,
189
+ type: "object"
190
+ }
191
+ }
192
+
193
+ /**
194
+ * Generates MCP tool definitions from a frontend-model API manifest.
195
+ * Every built-in and custom command of every manifest resource becomes one tool;
196
+ * one meta tool exposes the raw manifest for discovery.
197
+ * @param {ApiManifest} manifest - Manifest from `configuration.getApiManifest()`.
198
+ * @returns {Record<string, unknown>[]} - MCP `tools/list` tool definitions.
199
+ */
200
+ export function generateMcpTools(manifest) {
201
+ const resources = manifest.resources
202
+ const tools = []
203
+
204
+ if (!resources) return [buildMetaTool()]
205
+
206
+ for (const modelKey of Object.keys(resources).sort()) {
207
+ const resource = /** @type {ManifestResourceEntry} */ (resources[modelKey])
208
+ const commands = resource.commands || {}
209
+ const builtInCommands = resource.builtInCommands || {}
210
+
211
+ for (const scope of /** @type {Array<"collection" | "member">} */ (["collection", "member"])) {
212
+ for (const entry of scopeCommandEntries({builtInCommands, commands, scope})) {
213
+ const commandName = entry.methodName
214
+ const isBuiltIn = entry.isBuiltIn
215
+ const name = mcpToolName({commandName, modelKey, scope})
216
+
217
+ if (!MCP_TOOL_NAME_PATTERN.test(name)) continue
218
+
219
+ tools.push({
220
+ description: isBuiltIn
221
+ ? `${BUILT_IN_COMMAND_DESCRIPTIONS[commandName] || `Run the built-in \`${commandName}\` command on \`${resource.modelName}\`.`} Model: \`${resource.modelName}\`.`
222
+ : `Custom command \`${commandName}\` on model \`${resource.modelName}\`${entry.returnType ? ` (returns ${entry.returnType})` : ""}.`,
223
+ inputSchema: isBuiltIn
224
+ ? builtInCommandInputSchema({commandName, primaryKey: resource.primaryKey})
225
+ : customCommandInputSchema({entry, primaryKey: resource.primaryKey}),
226
+ name
227
+ })
228
+ }
229
+ }
230
+ }
231
+
232
+ tools.push(buildMetaTool())
233
+
234
+ return tools
235
+ }
236
+
237
+ /**
238
+ * Merges the manifest's built-in command names and custom command entries for
239
+ * one scope into a single deterministic command-entry list.
240
+ * @param {object} args - Arguments.
241
+ * @param {Record<string, Record<string, string>>} args.builtInCommands - Scope-keyed built-in command maps.
242
+ * @param {Record<string, ManifestCommandEntry[]>} args.commands - Scope-keyed custom command entries.
243
+ * @param {"collection" | "member"} args.scope - Command scope.
244
+ * @returns {Array<ManifestCommandEntry & {isBuiltIn: boolean}>} - Combined command entries.
245
+ */
246
+ function scopeCommandEntries({builtInCommands, commands, scope}) {
247
+ /** @type {Array<ManifestCommandEntry & {isBuiltIn: boolean}>} */
248
+ const entries = []
249
+
250
+ for (const commandName of Object.keys(builtInCommands[scope] || {})) {
251
+ entries.push({isBuiltIn: true, methodName: commandName, scope})
252
+ }
253
+
254
+ for (const entry of commands[scope] || []) {
255
+ entries.push({...entry, isBuiltIn: false})
256
+ }
257
+
258
+ return entries.sort((a, b) => a.methodName.localeCompare(b.methodName))
259
+ }
260
+
261
+ /**
262
+ * Builds the manifest discovery meta tool.
263
+ * @returns {Record<string, unknown>} - MCP tool definition.
264
+ */
265
+ function buildMetaTool() {
266
+ return {
267
+ description: META_TOOL_DESCRIPTION,
268
+ inputSchema: {additionalProperties: false, properties: {}, type: "object"},
269
+ name: META_TOOL_NAME
270
+ }
271
+ }
272
+
273
+ /**
274
+ * Parses one MCP tool name back into its manifest model key and command method name.
275
+ * @param {object} args - Arguments.
276
+ * @param {string} args.toolName - MCP tool name.
277
+ * @param {ApiManifest} args.manifest - Manifest from `configuration.getApiManifest()`.
278
+ * @returns {{commandName: string, modelKey: string, scope: "collection" | "member"} | null} - Command identity, or null when unknown.
279
+ */
280
+ export function parseMcpToolName({manifest, toolName}) {
281
+ if (toolName === META_TOOL_NAME) return null
282
+
283
+ const resources = manifest.resources
284
+
285
+ if (!resources) return null
286
+
287
+ for (const modelKey of Object.keys(resources)) {
288
+ const resource = /** @type {ManifestResourceEntry} */ (resources[modelKey])
289
+ const commands = resource.commands || {}
290
+ const builtInCommands = resource.builtInCommands || {}
291
+
292
+ for (const scope of /** @type {Array<"collection" | "member">} */ (["collection", "member"])) {
293
+ const commandNames = [...Object.keys(builtInCommands[scope] || {}), ...(commands[scope] || []).map((entry) => entry.methodName)]
294
+
295
+ for (const commandName of commandNames) {
296
+ if (mcpToolName({commandName, modelKey, scope}) === toolName) {
297
+ return {commandName, modelKey, scope}
298
+ }
299
+ }
300
+ }
301
+ }
302
+
303
+ return null
304
+ }
305
+
306
+ /**
307
+ * Runs the metaToolName helper.
308
+ * @returns {string} - The meta tool name.
309
+ */
310
+ export function metaToolName() { return META_TOOL_NAME }
@@ -0,0 +1,63 @@
1
+ import Controller from "../../../controller.js"
2
+ import {handleMcpRequest} from "../../../mcp/json-rpc-server.js"
3
+
4
+ export default class BuiltInMcpController extends Controller {
5
+ /**
6
+ * Runs one JSON-RPC message of the MCP protocol over the HTTP transport.
7
+ * The request body is parsed into params by the HTTP request parser; the
8
+ * JSON-RPC envelope is extracted from it and answered exactly like the stdio
9
+ * transport. The request's `Authorization` header is forwarded as the
10
+ * request-context `authorization` key, so tool calls authenticate through the
11
+ * app's own tenant/ability resolvers just like regular frontend-model calls.
12
+ * @returns {Promise<void>} - Resolves when the JSON-RPC response has been rendered.
13
+ */
14
+ async handle() {
15
+ const params = this.params()
16
+ const requestContext = buildRequestContextFromHeaders({request: this.request()})
17
+ const response = await handleMcpRequest({
18
+ configuration: this.getConfiguration(),
19
+ request: {
20
+ id: params.id ?? null,
21
+ jsonrpc: params.jsonrpc,
22
+ method: params.method,
23
+ params: typeof params.params === "object" && params.params !== null && !Array.isArray(params.params) ? params.params : undefined
24
+ },
25
+ requestContext
26
+ })
27
+
28
+ this._response.setHeader("Content-Type", "application/json; charset=UTF-8")
29
+
30
+ if (response === null) {
31
+ // Notifications never carry a JSON-RPC response id; 202 Accepted with an
32
+ // empty body keeps the HTTP transport honest about that.
33
+ this._response.setStatus(202)
34
+ this._response.setBody("")
35
+
36
+ return
37
+ }
38
+
39
+ this._response.setBody(`${JSON.stringify(response)}\n`)
40
+ }
41
+ }
42
+
43
+ /**
44
+ * Builds the remote request context forwarded to every tool call from the
45
+ * incoming HTTP request headers. The `Authorization` header becomes the
46
+ * request-context `authorization` key, which the MCP dispatcher forwards as
47
+ * the Authorization header of the synthetic in-process frontend-model request.
48
+ * @param {object} args - Arguments.
49
+ * @param {import("../../../http-server/client/request.js").default} args.request - Incoming HTTP request.
50
+ * @returns {Record<string, string | number | boolean> | undefined} - Request context, or undefined when no headers carry context.
51
+ */
52
+ function buildRequestContextFromHeaders({request}) {
53
+ /** @type {Record<string, string | number | boolean>} */
54
+ const context = {}
55
+
56
+ const authorization = request.header("authorization")
57
+
58
+ if (typeof authorization === "string" && authorization.trim().length > 0) {
59
+ context.authorization = authorization.trim()
60
+ }
61
+
62
+ return Object.keys(context).length > 0 ? context : undefined
63
+ }
@@ -0,0 +1,58 @@
1
+ // @ts-check
2
+
3
+ import restArgsError from "../utils/rest-args-error.js"
4
+
5
+ /**
6
+ * Catch-all route options.
7
+ * @typedef {object} CatchAllRouteOptions
8
+ * @property {string} [controller] - Controller name unmatched requests resolve to.
9
+ * @property {string} [action] - Action name unmatched requests resolve to.
10
+ */
11
+
12
+ /**
13
+ * Application-declared catch-all route created with `route.catchAll(...)` in
14
+ * the routes file. It is not a path matcher: the resolver applies it only
15
+ * when no other route in the tree matched, replacing the built-in errors
16
+ * not-found response with the application's own controller/action (for
17
+ * example a JSON error envelope) without hand-wiring a route resolver hook.
18
+ */
19
+ export default class VelociousRouteCatchAllRoute {
20
+ /**
21
+ * Controller name unmatched requests resolve to.
22
+ * @type {string} */
23
+ controller
24
+
25
+ /**
26
+ * Action name unmatched requests resolve to.
27
+ * @type {string} */
28
+ action
29
+
30
+ /**
31
+ * Runs constructor.
32
+ * @param {CatchAllRouteOptions} [options] - Catch-all options.
33
+ */
34
+ constructor({controller = "_root", action = "notFound", ...restArgs} = {}) {
35
+ restArgsError(restArgs)
36
+
37
+ if (typeof controller !== "string" || controller.length < 1) {
38
+ throw new Error(`Expected catch-all 'controller' to be a non-empty string, got: ${String(controller)}`)
39
+ }
40
+
41
+ if (typeof action !== "string" || action.length < 1) {
42
+ throw new Error(`Expected catch-all 'action' to be a non-empty string, got: ${String(action)}`)
43
+ }
44
+
45
+ this.controller = controller
46
+ this.action = action
47
+ }
48
+
49
+ /**
50
+ * Runs get human paths.
51
+ * @returns {Array<{action: string | null, method: string, path: string}>} - Route definitions for this route.
52
+ */
53
+ getHumanPaths() {
54
+ return [
55
+ {method: "ANY", action: this.action, path: "*"}
56
+ ]
57
+ }
58
+ }
@@ -126,7 +126,10 @@ export default class VelociousRoutesResolver {
126
126
  const __filename = fileURLToPath(import.meta.url)
127
127
  const __dirname = dirname(__filename)
128
128
  const requestedPath = currentPath.replace(/^\//, "") || "_root"
129
- const attemptedControllerPath = `${this.configuration.getDirectory()}/src/routes/${requestedPath}/controller.js`
129
+ const catchAllRoute = currentRoute?.catchAllRoute
130
+ const attemptedControllerPath = catchAllRoute
131
+ ? `${this.configuration.getDirectory()}/src/routes/${catchAllRoute.controller}/controller.js`
132
+ : `${this.configuration.getDirectory()}/src/routes/${requestedPath}/controller.js`
130
133
 
131
134
  const logger = this.logger
132
135
 
@@ -136,13 +139,23 @@ export default class VelociousRoutesResolver {
136
139
 
137
140
  await logger.warn(`No route matched for ${loggedPath}. Tried controller at ${attemptedControllerPath}`)
138
141
 
139
- controller = "errors"
140
- controllerPath = "./built-in/errors/controller.js"
141
- action = "notFound"
142
- skipAbilityResolution = true
143
- skipControllerConnections = true
144
- skipTenantResolution = true
145
- viewPath = await fs.realpath(`${__dirname}/built-in/errors`)
142
+ if (catchAllRoute) {
143
+ controller = catchAllRoute.controller
144
+ action = normalizeActionName(catchAllRoute.action)
145
+ controllerPath = `${this.configuration.getDirectory()}/src/routes/${controller}/controller.js`
146
+ viewPath = `${this.configuration.getDirectory()}/src/routes/${controller}`
147
+ skipAbilityResolution = true
148
+ skipControllerConnections = true
149
+ skipTenantResolution = true
150
+ } else {
151
+ controller = "errors"
152
+ controllerPath = "./built-in/errors/controller.js"
153
+ action = "notFound"
154
+ skipAbilityResolution = true
155
+ skipControllerConnections = true
156
+ skipTenantResolution = true
157
+ viewPath = await fs.realpath(`${__dirname}/built-in/errors`)
158
+ }
146
159
  } else if (action) {
147
160
  if (!controller) controller = "_root"
148
161
 
@@ -1,11 +1,36 @@
1
1
  // @ts-check
2
2
 
3
3
  import BasicRoute from "./basic-route.js"
4
+ import CatchAllRoute from "./catch-all-route.js"
4
5
 
5
6
  export default class VelociousRootRoute extends BasicRoute {
7
+ /**
8
+ * Application catch-all route declared with `route.catchAll(...)`, or null
9
+ * when the application did not declare one.
10
+ * @type {CatchAllRoute | null} */
11
+ catchAllRoute = null
12
+
13
+ /**
14
+ * Runs catch all. Declares the application's catch-all route: unmatched
15
+ * requests resolve to the given controller/action instead of the built-in
16
+ * errors not-found response.
17
+ * @param {import("./catch-all-route.js").CatchAllRouteOptions} [options] - Catch-all options.
18
+ * @returns {void} - No return value.
19
+ */
20
+ catchAll(options = {}) {
21
+ if (this.catchAllRoute) throw new Error("Only one catchAll route can be declared")
22
+
23
+ this.catchAllRoute = new CatchAllRoute(options)
24
+ }
25
+
6
26
  getHumanPaths() {
7
- return [
27
+ /** @type {Array<{action: string | null, method: string, path: string}>} */
28
+ const humanPaths = [
8
29
  {method: "GET", action: "index", path: "/"}
9
30
  ]
31
+
32
+ if (this.catchAllRoute) humanPaths.push(...this.catchAllRoute.getHumanPaths())
33
+
34
+ return humanPaths
10
35
  }
11
36
  }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Default bound for one scheduled background-job enqueue attempt.
3
+ *
4
+ * A durable enqueue that never settles (wedged DB connection, advisory lock held by a dead process)
5
+ * used to poison its schedule key forever: `BackgroundJobsScheduler` kept the in-flight promise in
6
+ * `pendingEnqueuesByJobKey`, every later tick returned early, and shutdown could never drain. The
7
+ * bound makes that stall observable and self-healing instead of silent.
8
+ *
9
+ * It is deliberately larger than the advisory-lock acquisition bound, because a scheduled enqueue
10
+ * waits for a lock *and then* inserts. Keeping the enqueue watchdog outside the lock bound lets the
11
+ * precise `AdvisoryLockTimeoutError` surface first — naming the lock that is actually stuck —
12
+ * instead of a generic enqueue timeout that hides which lock caused the stall.
13
+ * @type {number} */
14
+ export declare const DEFAULT_SCHEDULED_ENQUEUE_TIMEOUT_MS: number;
15
+ //# sourceMappingURL=scheduled-enqueue-bound.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scheduled-enqueue-bound.d.ts","sourceRoot":"","sources":["../../../src/background-jobs/scheduled-enqueue-bound.js"],"names":[],"mappings":"AAIA;;;;;;;;;;;;oBAYoB;AACpB,eAAO,MAAM,oCAAoC,EADvC,MACkG,CAAA"}
@@ -0,0 +1,17 @@
1
+ // @ts-check
2
+ import { DEFAULT_ADVISORY_LOCK_ACQUIRE_TIMEOUT_MS } from "../database/advisory-lock-bound.js";
3
+ /**
4
+ * Default bound for one scheduled background-job enqueue attempt.
5
+ *
6
+ * A durable enqueue that never settles (wedged DB connection, advisory lock held by a dead process)
7
+ * used to poison its schedule key forever: `BackgroundJobsScheduler` kept the in-flight promise in
8
+ * `pendingEnqueuesByJobKey`, every later tick returned early, and shutdown could never drain. The
9
+ * bound makes that stall observable and self-healing instead of silent.
10
+ *
11
+ * It is deliberately larger than the advisory-lock acquisition bound, because a scheduled enqueue
12
+ * waits for a lock *and then* inserts. Keeping the enqueue watchdog outside the lock bound lets the
13
+ * precise `AdvisoryLockTimeoutError` surface first — naming the lock that is actually stuck —
14
+ * instead of a generic enqueue timeout that hides which lock caused the stall.
15
+ * @type {number} */
16
+ export const DEFAULT_SCHEDULED_ENQUEUE_TIMEOUT_MS = DEFAULT_ADVISORY_LOCK_ACQUIRE_TIMEOUT_MS + 5 * 60 * 1000;
17
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic2NoZWR1bGVkLWVucXVldWUtYm91bmQuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi9zcmMvYmFja2dyb3VuZC1qb2JzL3NjaGVkdWxlZC1lbnF1ZXVlLWJvdW5kLmpzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLFlBQVk7QUFFWixPQUFPLEVBQUMsd0NBQXdDLEVBQUMsTUFBTSxvQ0FBb0MsQ0FBQTtBQUUzRjs7Ozs7Ozs7Ozs7O29CQVlvQjtBQUNwQixNQUFNLENBQUMsTUFBTSxvQ0FBb0MsR0FBRyx3Q0FBd0MsR0FBRyxDQUFDLEdBQUcsRUFBRSxHQUFHLElBQUksQ0FBQSIsInNvdXJjZXNDb250ZW50IjpbIi8vIEB0cy1jaGVja1xuXG5pbXBvcnQge0RFRkFVTFRfQURWSVNPUllfTE9DS19BQ1FVSVJFX1RJTUVPVVRfTVN9IGZyb20gXCIuLi9kYXRhYmFzZS9hZHZpc29yeS1sb2NrLWJvdW5kLmpzXCJcblxuLyoqXG4gKiBEZWZhdWx0IGJvdW5kIGZvciBvbmUgc2NoZWR1bGVkIGJhY2tncm91bmQtam9iIGVucXVldWUgYXR0ZW1wdC5cbiAqXG4gKiBBIGR1cmFibGUgZW5xdWV1ZSB0aGF0IG5ldmVyIHNldHRsZXMgKHdlZGdlZCBEQiBjb25uZWN0aW9uLCBhZHZpc29yeSBsb2NrIGhlbGQgYnkgYSBkZWFkIHByb2Nlc3MpXG4gKiB1c2VkIHRvIHBvaXNvbiBpdHMgc2NoZWR1bGUga2V5IGZvcmV2ZXI6IGBCYWNrZ3JvdW5kSm9ic1NjaGVkdWxlcmAga2VwdCB0aGUgaW4tZmxpZ2h0IHByb21pc2UgaW5cbiAqIGBwZW5kaW5nRW5xdWV1ZXNCeUpvYktleWAsIGV2ZXJ5IGxhdGVyIHRpY2sgcmV0dXJuZWQgZWFybHksIGFuZCBzaHV0ZG93biBjb3VsZCBuZXZlciBkcmFpbi4gVGhlXG4gKiBib3VuZCBtYWtlcyB0aGF0IHN0YWxsIG9ic2VydmFibGUgYW5kIHNlbGYtaGVhbGluZyBpbnN0ZWFkIG9mIHNpbGVudC5cbiAqXG4gKiBJdCBpcyBkZWxpYmVyYXRlbHkgbGFyZ2VyIHRoYW4gdGhlIGFkdmlzb3J5LWxvY2sgYWNxdWlzaXRpb24gYm91bmQsIGJlY2F1c2UgYSBzY2hlZHVsZWQgZW5xdWV1ZVxuICogd2FpdHMgZm9yIGEgbG9jayAqYW5kIHRoZW4qIGluc2VydHMuIEtlZXBpbmcgdGhlIGVucXVldWUgd2F0Y2hkb2cgb3V0c2lkZSB0aGUgbG9jayBib3VuZCBsZXRzIHRoZVxuICogcHJlY2lzZSBgQWR2aXNvcnlMb2NrVGltZW91dEVycm9yYCBzdXJmYWNlIGZpcnN0IOKAlCBuYW1pbmcgdGhlIGxvY2sgdGhhdCBpcyBhY3R1YWxseSBzdHVjayDigJRcbiAqIGluc3RlYWQgb2YgYSBnZW5lcmljIGVucXVldWUgdGltZW91dCB0aGF0IGhpZGVzIHdoaWNoIGxvY2sgY2F1c2VkIHRoZSBzdGFsbC5cbiAqIEB0eXBlIHtudW1iZXJ9ICovXG5leHBvcnQgY29uc3QgREVGQVVMVF9TQ0hFRFVMRURfRU5RVUVVRV9USU1FT1VUX01TID0gREVGQVVMVF9BRFZJU09SWV9MT0NLX0FDUVVJUkVfVElNRU9VVF9NUyArIDUgKiA2MCAqIDEwMDBcbiJdfQ==
@@ -0,0 +1,24 @@
1
+ import { TimeoutError } from "awaitery/build/timeout.js";
2
+ /** Typed failure for a scheduled background-job enqueue that never settled within its bound. */
3
+ export default class ScheduledBackgroundJobEnqueueTimeoutError extends TimeoutError {
4
+ /** @type {"BACKGROUND_JOB_SCHEDULED_ENQUEUE_TIMEOUT"} */
5
+ code: "BACKGROUND_JOB_SCHEDULED_ENQUEUE_TIMEOUT";
6
+ jobKey: string;
7
+ jobName: string;
8
+ scheduledEnqueueTimeoutMs: number;
9
+ /**
10
+ * Builds a scheduled-enqueue timeout error without job arguments or connection details.
11
+ * @param {object} args - Safe timeout context.
12
+ * @param {string} args.jobKey - Schedule key whose enqueue stalled.
13
+ * @param {string} args.jobName - Resolved job class name.
14
+ * @param {number} args.scheduledEnqueueTimeoutMs - Configured bound that expired.
15
+ * @param {TimeoutError} [args.cause] - Original watchdog timeout.
16
+ */
17
+ constructor({ jobKey, jobName, scheduledEnqueueTimeoutMs, cause }: {
18
+ jobKey: string;
19
+ jobName: string;
20
+ scheduledEnqueueTimeoutMs: number;
21
+ cause?: TimeoutError;
22
+ });
23
+ }
24
+ //# sourceMappingURL=scheduled-enqueue-timeout-error.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scheduled-enqueue-timeout-error.d.ts","sourceRoot":"","sources":["../../../src/background-jobs/scheduled-enqueue-timeout-error.js"],"names":[],"mappings":"AAEA,OAAO,EAAC,YAAY,EAAC,MAAM,2BAA2B,CAAA;AAEtD,gGAAgG;AAChG,MAAM,CAAC,OAAO,OAAO,yCAA0C,SAAQ,YAAY;IAa/E,yDAAyD;IACpD,IAAI,EADE,0CAA0C;IAEhD,MAAM;IACN,OAAO;IACP,yBAAyB;IAhBhC;;;;;;;OAOG;IACH,YAAY,EAAC,MAAM,EAAE,OAAO,EAAE,yBAAyB,EAAE,KAAK,EAAC,EAL5D;QAAqB,MAAM,EAAnB,MAAM,CACd;QAAqB,OAAO,EAApB,MAAM,CACd;QAAqB,yBAAyB,EAAtC,MAAM,CACd;QAA4B,KAAK,AAAjC,CACF,EADU,YAAY,CACtB;KAC8D,EAS9D;CACF"}
@@ -0,0 +1,23 @@
1
+ // @ts-check
2
+ import { TimeoutError } from "awaitery/build/timeout.js";
3
+ /** Typed failure for a scheduled background-job enqueue that never settled within its bound. */
4
+ export default class ScheduledBackgroundJobEnqueueTimeoutError extends TimeoutError {
5
+ /**
6
+ * Builds a scheduled-enqueue timeout error without job arguments or connection details.
7
+ * @param {object} args - Safe timeout context.
8
+ * @param {string} args.jobKey - Schedule key whose enqueue stalled.
9
+ * @param {string} args.jobName - Resolved job class name.
10
+ * @param {number} args.scheduledEnqueueTimeoutMs - Configured bound that expired.
11
+ * @param {TimeoutError} [args.cause] - Original watchdog timeout.
12
+ */
13
+ constructor({ jobKey, jobName, scheduledEnqueueTimeoutMs, cause }) {
14
+ super(`Scheduled background job enqueue did not settle within ${scheduledEnqueueTimeoutMs}ms`, cause ? { cause } : undefined);
15
+ this.name = "ScheduledBackgroundJobEnqueueTimeoutError";
16
+ /** @type {"BACKGROUND_JOB_SCHEDULED_ENQUEUE_TIMEOUT"} */
17
+ this.code = "BACKGROUND_JOB_SCHEDULED_ENQUEUE_TIMEOUT";
18
+ this.jobKey = jobKey;
19
+ this.jobName = jobName;
20
+ this.scheduledEnqueueTimeoutMs = scheduledEnqueueTimeoutMs;
21
+ }
22
+ }
23
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic2NoZWR1bGVkLWVucXVldWUtdGltZW91dC1lcnJvci5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uL3NyYy9iYWNrZ3JvdW5kLWpvYnMvc2NoZWR1bGVkLWVucXVldWUtdGltZW91dC1lcnJvci5qcyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxZQUFZO0FBRVosT0FBTyxFQUFDLFlBQVksRUFBQyxNQUFNLDJCQUEyQixDQUFBO0FBRXRELGdHQUFnRztBQUNoRyxNQUFNLENBQUMsT0FBTyxPQUFPLHlDQUEwQyxTQUFRLFlBQVk7SUFDakY7Ozs7Ozs7T0FPRztJQUNILFlBQVksRUFBQyxNQUFNLEVBQUUsT0FBTyxFQUFFLHlCQUF5QixFQUFFLEtBQUssRUFBQztRQUM3RCxLQUFLLENBQUMsMERBQTBELHlCQUF5QixJQUFJLEVBQUUsS0FBSyxDQUFDLENBQUMsQ0FBQyxFQUFDLEtBQUssRUFBQyxDQUFDLENBQUMsQ0FBQyxTQUFTLENBQUMsQ0FBQTtRQUUzSCxJQUFJLENBQUMsSUFBSSxHQUFHLDJDQUEyQyxDQUFBO1FBQ3ZELHlEQUF5RDtRQUN6RCxJQUFJLENBQUMsSUFBSSxHQUFHLDBDQUEwQyxDQUFBO1FBQ3RELElBQUksQ0FBQyxNQUFNLEdBQUcsTUFBTSxDQUFBO1FBQ3BCLElBQUksQ0FBQyxPQUFPLEdBQUcsT0FBTyxDQUFBO1FBQ3RCLElBQUksQ0FBQyx5QkFBeUIsR0FBRyx5QkFBeUIsQ0FBQTtJQUM1RCxDQUFDO0NBQ0YiLCJzb3VyY2VzQ29udGVudCI6WyIvLyBAdHMtY2hlY2tcblxuaW1wb3J0IHtUaW1lb3V0RXJyb3J9IGZyb20gXCJhd2FpdGVyeS9idWlsZC90aW1lb3V0LmpzXCJcblxuLyoqIFR5cGVkIGZhaWx1cmUgZm9yIGEgc2NoZWR1bGVkIGJhY2tncm91bmQtam9iIGVucXVldWUgdGhhdCBuZXZlciBzZXR0bGVkIHdpdGhpbiBpdHMgYm91bmQuICovXG5leHBvcnQgZGVmYXVsdCBjbGFzcyBTY2hlZHVsZWRCYWNrZ3JvdW5kSm9iRW5xdWV1ZVRpbWVvdXRFcnJvciBleHRlbmRzIFRpbWVvdXRFcnJvciB7XG4gIC8qKlxuICAgKiBCdWlsZHMgYSBzY2hlZHVsZWQtZW5xdWV1ZSB0aW1lb3V0IGVycm9yIHdpdGhvdXQgam9iIGFyZ3VtZW50cyBvciBjb25uZWN0aW9uIGRldGFpbHMuXG4gICAqIEBwYXJhbSB7b2JqZWN0fSBhcmdzIC0gU2FmZSB0aW1lb3V0IGNvbnRleHQuXG4gICAqIEBwYXJhbSB7c3RyaW5nfSBhcmdzLmpvYktleSAtIFNjaGVkdWxlIGtleSB3aG9zZSBlbnF1ZXVlIHN0YWxsZWQuXG4gICAqIEBwYXJhbSB7c3RyaW5nfSBhcmdzLmpvYk5hbWUgLSBSZXNvbHZlZCBqb2IgY2xhc3MgbmFtZS5cbiAgICogQHBhcmFtIHtudW1iZXJ9IGFyZ3Muc2NoZWR1bGVkRW5xdWV1ZVRpbWVvdXRNcyAtIENvbmZpZ3VyZWQgYm91bmQgdGhhdCBleHBpcmVkLlxuICAgKiBAcGFyYW0ge1RpbWVvdXRFcnJvcn0gW2FyZ3MuY2F1c2VdIC0gT3JpZ2luYWwgd2F0Y2hkb2cgdGltZW91dC5cbiAgICovXG4gIGNvbnN0cnVjdG9yKHtqb2JLZXksIGpvYk5hbWUsIHNjaGVkdWxlZEVucXVldWVUaW1lb3V0TXMsIGNhdXNlfSkge1xuICAgIHN1cGVyKGBTY2hlZHVsZWQgYmFja2dyb3VuZCBqb2IgZW5xdWV1ZSBkaWQgbm90IHNldHRsZSB3aXRoaW4gJHtzY2hlZHVsZWRFbnF1ZXVlVGltZW91dE1zfW1zYCwgY2F1c2UgPyB7Y2F1c2V9IDogdW5kZWZpbmVkKVxuXG4gICAgdGhpcy5uYW1lID0gXCJTY2hlZHVsZWRCYWNrZ3JvdW5kSm9iRW5xdWV1ZVRpbWVvdXRFcnJvclwiXG4gICAgLyoqIEB0eXBlIHtcIkJBQ0tHUk9VTkRfSk9CX1NDSEVEVUxFRF9FTlFVRVVFX1RJTUVPVVRcIn0gKi9cbiAgICB0aGlzLmNvZGUgPSBcIkJBQ0tHUk9VTkRfSk9CX1NDSEVEVUxFRF9FTlFVRVVFX1RJTUVPVVRcIlxuICAgIHRoaXMuam9iS2V5ID0gam9iS2V5XG4gICAgdGhpcy5qb2JOYW1lID0gam9iTmFtZVxuICAgIHRoaXMuc2NoZWR1bGVkRW5xdWV1ZVRpbWVvdXRNcyA9IHNjaGVkdWxlZEVucXVldWVUaW1lb3V0TXNcbiAgfVxufVxuIl19
@@ -38,6 +38,8 @@ export default class BackgroundJobsScheduler {
38
38
  options: import("./types.js").BackgroundJobOptions;
39
39
  }) => Promise<void>;
40
40
  logger: Logger;
41
+ /** @type {number | null} - Bound (ms) for one scheduled enqueue attempt; `null` disables the bound. */
42
+ scheduledEnqueueTimeoutMs: number | null;
41
43
  /**
42
44
  * Narrows the runtime value to the documented type.
43
45
  * @type {Array<ReturnType<typeof setInterval>>} */
@@ -135,6 +137,44 @@ export default class BackgroundJobsScheduler {
135
137
  jobConfiguration: import("../configuration-types.js").ScheduledBackgroundJobConfiguration;
136
138
  jobKey: string;
137
139
  }): Promise<void>;
140
+ /**
141
+ * Runs one durable enqueue attempt under the scheduled-enqueue bound.
142
+ *
143
+ * The bound is applied with a local race rather than `awaitery`'s `timeout()`:
144
+ * that helper starts its callback through `Promise.resolve().then(...)`, which
145
+ * would defer every scheduled enqueue by a microtask and change the tick
146
+ * semantics callers rely on. Here the attempt starts synchronously, and the
147
+ * watchdog uses the module-captured timers so it is never observable as a
148
+ * schedule timer. Only the watchdog raises the typed timeout error, so a
149
+ * timeout raised inside the enqueue itself keeps its own error path.
150
+ * @param {object} args - Options.
151
+ * @param {string} args.jobKey - Schedule key.
152
+ * @param {string} args.jobName - Resolved job class name.
153
+ * @param {() => Promise<void>} args.run - Enqueue attempt.
154
+ * @returns {Promise<void>} - Resolves when the attempt settles within the bound.
155
+ */
156
+ _withScheduledEnqueueTimeout({ jobKey, jobName, run }: {
157
+ jobKey: string;
158
+ jobName: string;
159
+ run: () => Promise<void>;
160
+ }): Promise<void>;
161
+ /**
162
+ * Surfaces a scheduled enqueue whose store call never settled. Without this report the only
163
+ * symptom is a schedule that silently stops enqueueing after one wedged call.
164
+ * @param {object} args - Options.
165
+ * @param {Error} args.error - Timeout error.
166
+ * @param {string} args.jobKey - Schedule key.
167
+ * @returns {void}
168
+ */
169
+ _reportScheduledEnqueueStall({ error, jobKey }: {
170
+ error: Error;
171
+ jobKey: string;
172
+ }): void;
173
+ /**
174
+ * Resolves the scheduled-enqueue bound from configuration.
175
+ * @returns {number | null} - Bound in milliseconds, or `null` for an unbounded wait.
176
+ */
177
+ _resolveScheduledEnqueueTimeoutMs(): number | null;
138
178
  /**
139
179
  * Runs normalize every.
140
180
  * @param {NonNullable<import("../configuration-types.js").ScheduledBackgroundJobConfiguration["every"]>} every - Every config (caller must guarantee not undefined).