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.
- package/README.md +5 -0
- package/build/background-jobs/scheduled-enqueue-bound.js +18 -0
- package/build/background-jobs/scheduled-enqueue-timeout-error.js +25 -0
- package/build/background-jobs/scheduler.js +121 -4
- package/build/cli/commands/mcp.js +7 -0
- package/build/cli/index.js +1 -1
- package/build/configuration-types.js +9 -0
- package/build/configuration.js +77 -2
- package/build/database/advisory-lock-bound.js +16 -0
- package/build/database/drivers/base.js +23 -2
- package/build/database/drivers/pgsql/index.js +27 -4
- package/build/environment-handlers/base.js +9 -0
- package/build/environment-handlers/node/cli/commands/mcp.js +87 -0
- package/build/environment-handlers/node.js +10 -0
- package/build/mcp/dispatcher.js +271 -0
- package/build/mcp/json-rpc-server.js +167 -0
- package/build/mcp/manifest-tools.js +310 -0
- package/build/routes/built-in/mcp/controller.js +63 -0
- package/build/routes/catch-all-route.js +58 -0
- package/build/routes/resolver.js +21 -8
- package/build/routes/root-route.js +26 -1
- package/build/src/background-jobs/scheduled-enqueue-bound.d.ts +15 -0
- package/build/src/background-jobs/scheduled-enqueue-bound.d.ts.map +1 -0
- package/build/src/background-jobs/scheduled-enqueue-bound.js +17 -0
- package/build/src/background-jobs/scheduled-enqueue-timeout-error.d.ts +24 -0
- package/build/src/background-jobs/scheduled-enqueue-timeout-error.d.ts.map +1 -0
- package/build/src/background-jobs/scheduled-enqueue-timeout-error.js +23 -0
- package/build/src/background-jobs/scheduler.d.ts +40 -0
- package/build/src/background-jobs/scheduler.d.ts.map +1 -1
- package/build/src/background-jobs/scheduler.js +111 -5
- package/build/src/cli/commands/mcp.d.ts +5 -0
- package/build/src/cli/commands/mcp.d.ts.map +1 -0
- package/build/src/cli/commands/mcp.js +7 -0
- package/build/src/cli/index.js +2 -2
- package/build/src/configuration-types.d.ts +30 -0
- package/build/src/configuration-types.d.ts.map +1 -1
- package/build/src/configuration-types.js +9 -1
- package/build/src/configuration.d.ts +24 -1
- package/build/src/configuration.d.ts.map +1 -1
- package/build/src/configuration.js +69 -3
- package/build/src/database/advisory-lock-bound.d.ts +15 -0
- package/build/src/database/advisory-lock-bound.d.ts.map +1 -0
- package/build/src/database/advisory-lock-bound.js +16 -0
- package/build/src/database/drivers/base.d.ts +13 -1
- package/build/src/database/drivers/base.d.ts.map +1 -1
- package/build/src/database/drivers/base.js +23 -3
- package/build/src/database/drivers/pgsql/index.d.ts +15 -3
- package/build/src/database/drivers/pgsql/index.d.ts.map +1 -1
- package/build/src/database/drivers/pgsql/index.js +23 -5
- package/build/src/environment-handlers/base.d.ts +6 -0
- package/build/src/environment-handlers/base.d.ts.map +1 -1
- package/build/src/environment-handlers/base.js +9 -1
- package/build/src/environment-handlers/node/cli/commands/mcp.d.ts +19 -0
- package/build/src/environment-handlers/node/cli/commands/mcp.d.ts.map +1 -0
- package/build/src/environment-handlers/node/cli/commands/mcp.js +70 -0
- package/build/src/environment-handlers/node.d.ts +6 -0
- package/build/src/environment-handlers/node.d.ts.map +1 -1
- package/build/src/environment-handlers/node.js +10 -1
- package/build/src/mcp/dispatcher.d.ts +133 -0
- package/build/src/mcp/dispatcher.d.ts.map +1 -0
- package/build/src/mcp/dispatcher.js +232 -0
- package/build/src/mcp/json-rpc-server.d.ts +47 -0
- package/build/src/mcp/json-rpc-server.d.ts.map +1 -0
- package/build/src/mcp/json-rpc-server.js +142 -0
- package/build/src/mcp/manifest-tools.d.ts +76 -0
- package/build/src/mcp/manifest-tools.d.ts.map +1 -0
- package/build/src/mcp/manifest-tools.js +273 -0
- package/build/src/routes/built-in/mcp/controller.d.ts +14 -0
- package/build/src/routes/built-in/mcp/controller.d.ts.map +1 -0
- package/build/src/routes/built-in/mcp/controller.js +55 -0
- package/build/src/routes/catch-all-route.d.ts +48 -0
- package/build/src/routes/catch-all-route.d.ts.map +1 -0
- package/build/src/routes/catch-all-route.js +50 -0
- package/build/src/routes/resolver.d.ts.map +1 -1
- package/build/src/routes/resolver.js +23 -9
- package/build/src/routes/root-route.d.ts +15 -1
- package/build/src/routes/root-route.d.ts.map +1 -1
- package/build/src/routes/root-route.js +24 -2
- package/package.json +1 -1
- package/src/background-jobs/scheduled-enqueue-bound.js +18 -0
- package/src/background-jobs/scheduled-enqueue-timeout-error.js +25 -0
- package/src/background-jobs/scheduler.js +121 -4
- package/src/cli/commands/mcp.js +7 -0
- package/src/cli/index.js +1 -1
- package/src/configuration-types.js +9 -0
- package/src/configuration.js +77 -2
- package/src/database/advisory-lock-bound.js +16 -0
- package/src/database/drivers/base.js +23 -2
- package/src/database/drivers/pgsql/index.js +27 -4
- package/src/environment-handlers/base.js +9 -0
- package/src/environment-handlers/node/cli/commands/mcp.js +87 -0
- package/src/environment-handlers/node.js +10 -0
- package/src/mcp/dispatcher.js +271 -0
- package/src/mcp/json-rpc-server.js +167 -0
- package/src/mcp/manifest-tools.js +310 -0
- package/src/routes/built-in/mcp/controller.js +63 -0
- package/src/routes/catch-all-route.js +58 -0
- package/src/routes/resolver.js +21 -8
- 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
|
+
}
|
package/build/routes/resolver.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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
|
-
|
|
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).
|