sails-hook-quest 0.0.4 → 0.0.6
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/CHANGELOG.md +27 -0
- package/CONTRACT.md +97 -0
- package/README.md +14 -1
- package/lib/core/child-result.js +80 -0
- package/lib/core/execution-child.js +14 -0
- package/lib/core/executor.js +346 -143
- package/lib/core/global-sails.js +16 -0
- package/lib/core/job-control.js +130 -34
- package/lib/core/loader.js +60 -28
- package/lib/core/result.js +91 -0
- package/lib/core/runtime.js +240 -0
- package/lib/core/scheduler.js +95 -46
- package/lib/index.js +94 -17
- package/lib/types.js +301 -0
- package/package.json +16 -2
- package/.claude/settings.local.json +0 -7
- package/.commitlintrc.js +0 -1
- package/.github/FUNDING.yml +0 -1
- package/.github/workflows/prettier.yml +0 -22
- package/.husky/pre-commit +0 -1
- package/.prettierrc.js +0 -5
- package/sails-hook-quest-0.0.0.tgz +0 -0
package/lib/core/job-control.js
CHANGED
|
@@ -1,9 +1,22 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* core/job-control.js
|
|
3
5
|
*
|
|
4
6
|
* Functions for controlling job lifecycle (start, stop, pause, resume)
|
|
5
7
|
*/
|
|
6
8
|
|
|
9
|
+
const { getGlobalSails } = require('./global-sails')
|
|
10
|
+
|
|
11
|
+
/** @typedef {import('../types').AnyRecord} AnyRecord */
|
|
12
|
+
/** @typedef {import('../types').QuestExecutionResult} QuestExecutionResult */
|
|
13
|
+
/** @typedef {import('../types').QuestJobNameInput} QuestJobNameInput */
|
|
14
|
+
/** @typedef {import('../types').QuestJobsMap} QuestJobsMap */
|
|
15
|
+
/** @typedef {import('../types').QuestRunContext} QuestRunContext */
|
|
16
|
+
/** @typedef {import('../types').QuestScheduleContext} QuestScheduleContext */
|
|
17
|
+
/** @typedef {import('../types').QuestStartContext} QuestStartContext */
|
|
18
|
+
/** @typedef {import('../types').QuestStopContext} QuestStopContext */
|
|
19
|
+
|
|
7
20
|
// setTimeout uses a 32-bit signed integer internally.
|
|
8
21
|
// Delays larger than this overflow and fire immediately (~1ms), causing infinite loops.
|
|
9
22
|
const MAX_SAFE_TIMEOUT = 2_147_483_647 // 2^31 - 1, ~24.8 days
|
|
@@ -11,31 +24,60 @@ const MAX_SAFE_TIMEOUT = 2_147_483_647 // 2^31 - 1, ~24.8 days
|
|
|
11
24
|
/**
|
|
12
25
|
* Schedule a job to run at its next scheduled time
|
|
13
26
|
* @param {String} name - Job name
|
|
14
|
-
* @param {
|
|
27
|
+
* @param {QuestScheduleContext} [context] - Context with jobs, timers maps and helper functions
|
|
28
|
+
* @param {Date} [targetTime] - Original target for a long-delay intermediate timer
|
|
15
29
|
*/
|
|
16
|
-
function scheduleJob(name, context = {}) {
|
|
17
|
-
const {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
30
|
+
function scheduleJob(name, context = {}, targetTime) {
|
|
31
|
+
const { jobs = new Map(), timers = new Map() } = context
|
|
32
|
+
const getNextRunTime =
|
|
33
|
+
/** @type {NonNullable<QuestScheduleContext['getNextRunTime']>} */ (
|
|
34
|
+
context.getNextRunTime
|
|
35
|
+
)
|
|
36
|
+
const executeJob =
|
|
37
|
+
/** @type {NonNullable<QuestScheduleContext['executeJob']>} */ (
|
|
38
|
+
context.executeJob
|
|
39
|
+
)
|
|
23
40
|
|
|
24
41
|
const job = jobs.get(name)
|
|
25
42
|
if (!job) {
|
|
26
|
-
|
|
43
|
+
const sails = getGlobalSails()
|
|
44
|
+
if (sails) {
|
|
27
45
|
sails.log.warn(`Job "${name}" not found in jobs Map`)
|
|
28
46
|
}
|
|
29
47
|
return
|
|
30
48
|
}
|
|
31
49
|
|
|
32
50
|
// Clear any existing timer
|
|
33
|
-
stopJob(name,
|
|
51
|
+
stopJob(name, context)
|
|
34
52
|
|
|
35
53
|
// Get the next run time
|
|
36
|
-
const
|
|
54
|
+
const states = context.scheduleStates
|
|
55
|
+
const update = (registration) =>
|
|
56
|
+
states?.set(name, {
|
|
57
|
+
validation: 'not_checked',
|
|
58
|
+
validationErrors: [],
|
|
59
|
+
reason: null,
|
|
60
|
+
...states.get(name),
|
|
61
|
+
registration
|
|
62
|
+
})
|
|
63
|
+
if (states && !targetTime)
|
|
64
|
+
states.set(name, {
|
|
65
|
+
validation: 'not_checked',
|
|
66
|
+
validationErrors: [],
|
|
67
|
+
reason: null,
|
|
68
|
+
lastAttemptAt: new Date().toISOString(),
|
|
69
|
+
registration: 'not_registered'
|
|
70
|
+
})
|
|
71
|
+
let nextRun
|
|
72
|
+
try {
|
|
73
|
+
nextRun = targetTime || getNextRunTime(job)
|
|
74
|
+
} catch (err) {
|
|
75
|
+
update('failed')
|
|
76
|
+
throw err
|
|
77
|
+
}
|
|
37
78
|
if (!nextRun) {
|
|
38
|
-
|
|
79
|
+
const sails = getGlobalSails()
|
|
80
|
+
if (sails) {
|
|
39
81
|
sails.log.warn(
|
|
40
82
|
`Job "${name}" has no valid schedule (interval: ${job.interval}, cron: ${job.cron}, timeout: ${job.timeout})`
|
|
41
83
|
)
|
|
@@ -43,12 +85,32 @@ function scheduleJob(name, context = {}) {
|
|
|
43
85
|
return
|
|
44
86
|
}
|
|
45
87
|
|
|
88
|
+
if (!Number.isFinite(nextRun.getTime())) {
|
|
89
|
+
states?.set(name, {
|
|
90
|
+
...states.get(name),
|
|
91
|
+
registration: 'failed',
|
|
92
|
+
validation: 'invalid',
|
|
93
|
+
reason: null,
|
|
94
|
+
validationErrors: [
|
|
95
|
+
{
|
|
96
|
+
code: 'E_SCHEDULE_TARGET',
|
|
97
|
+
message: 'Schedule target must be a finite date.'
|
|
98
|
+
}
|
|
99
|
+
]
|
|
100
|
+
})
|
|
101
|
+
return
|
|
102
|
+
}
|
|
46
103
|
const delay = nextRun.getTime() - Date.now()
|
|
104
|
+
context.dueTimes?.set(name, nextRun)
|
|
47
105
|
|
|
48
106
|
// If delay is negative (past time), run immediately
|
|
49
107
|
if (delay <= 0) {
|
|
50
|
-
|
|
51
|
-
|
|
108
|
+
timers.delete(name)
|
|
109
|
+
context.dueTimes?.delete(name)
|
|
110
|
+
update('consumed')
|
|
111
|
+
executeJob(name, undefined, 'scheduled').catch((err) => {
|
|
112
|
+
const sails = getGlobalSails()
|
|
113
|
+
if (sails) {
|
|
52
114
|
sails.log.error(`Error running job "${name}":`, err)
|
|
53
115
|
}
|
|
54
116
|
})
|
|
@@ -64,12 +126,14 @@ function scheduleJob(name, context = {}) {
|
|
|
64
126
|
// that rechecks when we're closer to the target time
|
|
65
127
|
if (delay > MAX_SAFE_TIMEOUT) {
|
|
66
128
|
const timer = setTimeout(() => {
|
|
67
|
-
scheduleJob(name, context)
|
|
129
|
+
scheduleJob(name, context, nextRun)
|
|
68
130
|
}, MAX_SAFE_TIMEOUT)
|
|
69
131
|
|
|
70
132
|
timers.set(name, timer)
|
|
133
|
+
update('registered')
|
|
71
134
|
|
|
72
|
-
|
|
135
|
+
const sails = getGlobalSails()
|
|
136
|
+
if (sails) {
|
|
73
137
|
sails.log.verbose(
|
|
74
138
|
`Job "${name}" scheduled for ${nextRun.toISOString()} (delay exceeds 24.8d, re-checking later)`
|
|
75
139
|
)
|
|
@@ -79,8 +143,12 @@ function scheduleJob(name, context = {}) {
|
|
|
79
143
|
|
|
80
144
|
// Set timer for the next execution
|
|
81
145
|
const timer = setTimeout(() => {
|
|
82
|
-
|
|
83
|
-
|
|
146
|
+
timers.delete(name)
|
|
147
|
+
context.dueTimes?.delete(name)
|
|
148
|
+
update('consumed')
|
|
149
|
+
executeJob(name, undefined, 'scheduled').catch((err) => {
|
|
150
|
+
const sails = getGlobalSails()
|
|
151
|
+
if (sails) {
|
|
84
152
|
sails.log.error(`Error running job "${name}":`, err)
|
|
85
153
|
}
|
|
86
154
|
})
|
|
@@ -92,8 +160,10 @@ function scheduleJob(name, context = {}) {
|
|
|
92
160
|
}, delay)
|
|
93
161
|
|
|
94
162
|
timers.set(name, timer)
|
|
163
|
+
update('registered')
|
|
95
164
|
|
|
96
|
-
|
|
165
|
+
const sails = getGlobalSails()
|
|
166
|
+
if (sails) {
|
|
97
167
|
sails.log.verbose(`Job "${name}" scheduled for ${nextRun.toISOString()}`)
|
|
98
168
|
}
|
|
99
169
|
}
|
|
@@ -101,17 +171,31 @@ function scheduleJob(name, context = {}) {
|
|
|
101
171
|
/**
|
|
102
172
|
* Stop a single job
|
|
103
173
|
* @param {String} name - Job name
|
|
104
|
-
* @param {
|
|
174
|
+
* @param {QuestStopContext} [context] - Context with timers map
|
|
105
175
|
*/
|
|
106
176
|
function stopJob(name, context = {}) {
|
|
107
177
|
const { timers = new Map() } = context
|
|
108
178
|
|
|
179
|
+
context.dueTimes?.delete(name)
|
|
180
|
+
if (
|
|
181
|
+
context.scheduleStates &&
|
|
182
|
+
(context.scheduleStates.has(name) || context.jobs?.has(name))
|
|
183
|
+
)
|
|
184
|
+
context.scheduleStates.set(name, {
|
|
185
|
+
validation: 'not_checked',
|
|
186
|
+
validationErrors: [],
|
|
187
|
+
reason: null,
|
|
188
|
+
lastAttemptAt: null,
|
|
189
|
+
...context.scheduleStates.get(name),
|
|
190
|
+
registration: 'stopped'
|
|
191
|
+
})
|
|
109
192
|
const timer = timers.get(name)
|
|
110
193
|
if (timer) {
|
|
111
194
|
clearTimeout(timer)
|
|
112
195
|
clearInterval(timer)
|
|
113
196
|
timers.delete(name)
|
|
114
|
-
|
|
197
|
+
const sails = getGlobalSails()
|
|
198
|
+
if (sails) {
|
|
115
199
|
sails.log.verbose(`Job "${name}" stopped`)
|
|
116
200
|
}
|
|
117
201
|
}
|
|
@@ -119,11 +203,16 @@ function stopJob(name, context = {}) {
|
|
|
119
203
|
|
|
120
204
|
/**
|
|
121
205
|
* Start scheduling jobs
|
|
122
|
-
* @param {
|
|
123
|
-
* @param {
|
|
206
|
+
* @param {QuestJobNameInput} jobNames - Job names to start (optional)
|
|
207
|
+
* @param {QuestStartContext} [context] - Context with jobs map and scheduleJob function
|
|
208
|
+
* @returns {Promise<void>}
|
|
124
209
|
*/
|
|
125
210
|
async function startJobs(jobNames, context = {}) {
|
|
126
|
-
const { jobs = new Map()
|
|
211
|
+
const { jobs = new Map() } = context
|
|
212
|
+
const scheduleJob =
|
|
213
|
+
/** @type {NonNullable<QuestStartContext['scheduleJob']>} */ (
|
|
214
|
+
context.scheduleJob
|
|
215
|
+
)
|
|
127
216
|
|
|
128
217
|
const names = !jobNames
|
|
129
218
|
? Array.from(jobs.keys())
|
|
@@ -138,8 +227,9 @@ async function startJobs(jobNames, context = {}) {
|
|
|
138
227
|
|
|
139
228
|
/**
|
|
140
229
|
* Stop scheduling jobs
|
|
141
|
-
* @param {
|
|
142
|
-
* @param {
|
|
230
|
+
* @param {QuestJobNameInput} jobNames - Job names to stop (optional)
|
|
231
|
+
* @param {QuestStopContext} [context] - Context with jobs and timers maps
|
|
232
|
+
* @returns {void}
|
|
143
233
|
*/
|
|
144
234
|
function stopJobs(jobNames, context = {}) {
|
|
145
235
|
const { jobs = new Map(), timers = new Map() } = context
|
|
@@ -151,18 +241,22 @@ function stopJobs(jobNames, context = {}) {
|
|
|
151
241
|
: [jobNames]
|
|
152
242
|
|
|
153
243
|
for (const name of names) {
|
|
154
|
-
stopJob(name,
|
|
244
|
+
stopJob(name, context)
|
|
155
245
|
}
|
|
156
246
|
}
|
|
157
247
|
|
|
158
248
|
/**
|
|
159
249
|
* Run jobs immediately
|
|
160
|
-
* @param {
|
|
161
|
-
* @param {
|
|
162
|
-
* @param {
|
|
250
|
+
* @param {QuestJobNameInput} jobNames - Job names to run
|
|
251
|
+
* @param {AnyRecord} [inputs] - Custom inputs
|
|
252
|
+
* @param {QuestRunContext} [context] - Context with executeJob function
|
|
253
|
+
* @returns {Promise<QuestExecutionResult[]>}
|
|
163
254
|
*/
|
|
164
255
|
async function runJobs(jobNames, inputs, context = {}) {
|
|
165
|
-
const { jobs = new Map()
|
|
256
|
+
const { jobs = new Map() } = context
|
|
257
|
+
const executeJob = /** @type {NonNullable<QuestRunContext['executeJob']>} */ (
|
|
258
|
+
context.executeJob
|
|
259
|
+
)
|
|
166
260
|
|
|
167
261
|
const names = !jobNames
|
|
168
262
|
? Array.from(jobs.keys())
|
|
@@ -170,14 +264,15 @@ async function runJobs(jobNames, inputs, context = {}) {
|
|
|
170
264
|
? jobNames
|
|
171
265
|
: [jobNames]
|
|
172
266
|
|
|
173
|
-
const promises = names.map((name) => executeJob(name, inputs))
|
|
267
|
+
const promises = names.map((name) => executeJob(name, inputs, 'manual'))
|
|
174
268
|
return Promise.all(promises)
|
|
175
269
|
}
|
|
176
270
|
|
|
177
271
|
/**
|
|
178
272
|
* Pause a job
|
|
179
273
|
* @param {String} name - Job name
|
|
180
|
-
* @param {
|
|
274
|
+
* @param {QuestJobsMap} [jobs] - Jobs map
|
|
275
|
+
* @returns {boolean}
|
|
181
276
|
*/
|
|
182
277
|
function pauseJob(name, jobs = new Map()) {
|
|
183
278
|
const job = jobs.get(name)
|
|
@@ -191,7 +286,8 @@ function pauseJob(name, jobs = new Map()) {
|
|
|
191
286
|
/**
|
|
192
287
|
* Resume a job
|
|
193
288
|
* @param {String} name - Job name
|
|
194
|
-
* @param {
|
|
289
|
+
* @param {QuestJobsMap} [jobs] - Jobs map
|
|
290
|
+
* @returns {boolean}
|
|
195
291
|
*/
|
|
196
292
|
function resumeJob(name, jobs = new Map()) {
|
|
197
293
|
const job = jobs.get(name)
|
package/lib/core/loader.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* core/loader.js
|
|
3
5
|
*
|
|
@@ -6,13 +8,24 @@
|
|
|
6
8
|
|
|
7
9
|
const path = require('path')
|
|
8
10
|
const includeAll = require('include-all')
|
|
11
|
+
const { getGlobalSails } = require('./global-sails')
|
|
12
|
+
|
|
13
|
+
/** @typedef {import('../types').AnyRecord} AnyRecord */
|
|
14
|
+
/** @typedef {import('../types').QuestConfig} QuestConfig */
|
|
15
|
+
/** @typedef {import('../types').QuestJob} QuestJob */
|
|
16
|
+
/** @typedef {import('../types').QuestJobDefinition} QuestJobDefinition */
|
|
17
|
+
/** @typedef {import('../types').QuestJobsMap} QuestJobsMap */
|
|
18
|
+
/** @typedef {import('../types').QuestMachineInputs} QuestMachineInputs */
|
|
19
|
+
/** @typedef {import('../types').QuestScriptDefinition} QuestScriptDefinition */
|
|
20
|
+
/** @typedef {import('../types').QuestTimersMap} QuestTimersMap */
|
|
9
21
|
|
|
10
22
|
/**
|
|
11
23
|
* Extract default values from a script's inputs schema
|
|
12
|
-
* @param {
|
|
13
|
-
* @returns {
|
|
24
|
+
* @param {QuestMachineInputs | null | undefined} inputs - Script's inputs definition (Sails machine format)
|
|
25
|
+
* @returns {AnyRecord} Object with input names and their defaultsTo values
|
|
14
26
|
*/
|
|
15
27
|
function extractScriptInputDefaults(inputs) {
|
|
28
|
+
/** @type {AnyRecord} */
|
|
16
29
|
const defaults = {}
|
|
17
30
|
if (!inputs || typeof inputs !== 'object') return defaults
|
|
18
31
|
|
|
@@ -26,9 +39,9 @@ function extractScriptInputDefaults(inputs) {
|
|
|
26
39
|
|
|
27
40
|
/**
|
|
28
41
|
* Load jobs from scripts directory and config
|
|
29
|
-
* @param {
|
|
30
|
-
* @param {
|
|
31
|
-
* @returns {Promise<
|
|
42
|
+
* @param {QuestConfig} config - Quest configuration
|
|
43
|
+
* @param {QuestJobsMap} [jobs] - Jobs map to populate
|
|
44
|
+
* @returns {Promise<QuestJobsMap>} Populated jobs map
|
|
32
45
|
*/
|
|
33
46
|
async function loadJobs(config, jobs = new Map()) {
|
|
34
47
|
// First, load scripts from the scripts directory
|
|
@@ -36,17 +49,21 @@ async function loadJobs(config, jobs = new Map()) {
|
|
|
36
49
|
const appPath = config.appPath || process.cwd()
|
|
37
50
|
const fullPath = path.resolve(appPath, scriptsDir)
|
|
38
51
|
|
|
52
|
+
/** @type {Record<string, QuestScriptDefinition>} */
|
|
39
53
|
let scripts = {}
|
|
40
54
|
|
|
41
55
|
try {
|
|
42
|
-
scripts =
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
56
|
+
scripts = /** @type {Record<string, QuestScriptDefinition>} */ (
|
|
57
|
+
includeAll({
|
|
58
|
+
dirname: fullPath,
|
|
59
|
+
filter: /(.+)\.js$/,
|
|
60
|
+
excludeDirs: /^\.(git|svn)$/,
|
|
61
|
+
flatten: true
|
|
62
|
+
})
|
|
63
|
+
)
|
|
48
64
|
} catch (e) {
|
|
49
|
-
|
|
65
|
+
const sails = getGlobalSails()
|
|
66
|
+
if (sails) {
|
|
50
67
|
sails.log.verbose('No scripts directory found, skipping script jobs')
|
|
51
68
|
}
|
|
52
69
|
}
|
|
@@ -57,22 +74,28 @@ async function loadJobs(config, jobs = new Map()) {
|
|
|
57
74
|
|
|
58
75
|
for (const jobDef of config.jobs) {
|
|
59
76
|
// Handle string shorthand (just job name)
|
|
60
|
-
|
|
77
|
+
/** @type {QuestJobDefinition} */
|
|
78
|
+
const job = typeof jobDef === 'string' ? { name: jobDef } : { ...jobDef }
|
|
79
|
+
const jobName = job.name
|
|
61
80
|
|
|
62
81
|
// Check for duplicate job names in config
|
|
63
|
-
if (configJobNames.has(
|
|
82
|
+
if (jobName && configJobNames.has(jobName)) {
|
|
64
83
|
throw new Error(
|
|
65
|
-
`Duplicate job name "${
|
|
84
|
+
`Duplicate job name "${jobName}" in config/quest.js. Each job must have a unique name.`
|
|
66
85
|
)
|
|
67
86
|
}
|
|
68
|
-
|
|
87
|
+
if (jobName) {
|
|
88
|
+
configJobNames.add(jobName)
|
|
89
|
+
}
|
|
69
90
|
|
|
70
91
|
// Try to get script inputs if the script exists
|
|
71
|
-
const scriptDef = scripts[job.
|
|
92
|
+
const scriptDef = jobName ? scripts[job.script || jobName] : undefined
|
|
72
93
|
if (scriptDef && !job.scriptInputs) {
|
|
73
94
|
job.scriptInputs = extractScriptInputDefaults(scriptDef.inputs)
|
|
74
95
|
}
|
|
75
96
|
|
|
97
|
+
job.script = job.script || jobName
|
|
98
|
+
job.inputSchema = scriptDef ? scriptDef.inputs || {} : undefined
|
|
76
99
|
addJobDefinition(job, jobs, config)
|
|
77
100
|
}
|
|
78
101
|
}
|
|
@@ -92,15 +115,18 @@ async function loadJobs(config, jobs = new Map()) {
|
|
|
92
115
|
const existingJob = jobs.get(jobName)
|
|
93
116
|
|
|
94
117
|
// Merge: config as base, script quest config takes priority
|
|
118
|
+
/** @type {QuestJobDefinition} */
|
|
95
119
|
const jobDef = {
|
|
96
120
|
name: jobName,
|
|
97
121
|
friendlyName: scriptDef.friendlyName,
|
|
98
122
|
description: scriptDef.description,
|
|
99
123
|
// Preserve config's withoutOverlapping if script doesn't specify
|
|
100
124
|
withoutOverlapping: existingJob?.withoutOverlapping,
|
|
101
|
-
inputs: { ...existingJob?.inputs, ...questConfig.inputs },
|
|
125
|
+
inputs: { ...(existingJob?.inputs || {}), ...(questConfig.inputs || {}) },
|
|
102
126
|
...questConfig,
|
|
103
|
-
scriptInputs
|
|
127
|
+
scriptInputs,
|
|
128
|
+
script: scriptName,
|
|
129
|
+
inputSchema: scriptDef.inputs || {}
|
|
104
130
|
}
|
|
105
131
|
|
|
106
132
|
addJobDefinition(jobDef, jobs, config)
|
|
@@ -111,23 +137,27 @@ async function loadJobs(config, jobs = new Map()) {
|
|
|
111
137
|
|
|
112
138
|
/**
|
|
113
139
|
* Add a job definition to the jobs Map
|
|
114
|
-
* @param {
|
|
115
|
-
* @param {
|
|
116
|
-
* @param {
|
|
117
|
-
* @returns {
|
|
140
|
+
* @param {QuestJobDefinition} jobDef - Job definition
|
|
141
|
+
* @param {QuestJobsMap} [jobs] - Jobs map
|
|
142
|
+
* @param {QuestConfig} [config] - Quest configuration
|
|
143
|
+
* @returns {QuestJob | null} Normalized job
|
|
118
144
|
*/
|
|
119
145
|
function addJobDefinition(jobDef, jobs = new Map(), config = {}) {
|
|
120
146
|
const name = jobDef.name
|
|
121
147
|
if (!name) {
|
|
122
|
-
|
|
148
|
+
const sails = getGlobalSails()
|
|
149
|
+
if (sails) {
|
|
123
150
|
sails.log.warn('Job definition missing name, skipping:', jobDef)
|
|
124
151
|
}
|
|
125
152
|
return null
|
|
126
153
|
}
|
|
127
154
|
|
|
128
155
|
// Parse and normalize the job definition
|
|
156
|
+
/** @type {QuestJob} */
|
|
129
157
|
const job = {
|
|
130
158
|
name,
|
|
159
|
+
script: jobDef.script || name,
|
|
160
|
+
inputSchema: jobDef.inputSchema,
|
|
131
161
|
friendlyName: jobDef.friendlyName || name,
|
|
132
162
|
description: jobDef.description,
|
|
133
163
|
|
|
@@ -151,7 +181,8 @@ function addJobDefinition(jobDef, jobs = new Map(), config = {}) {
|
|
|
151
181
|
|
|
152
182
|
jobs.set(name, job)
|
|
153
183
|
|
|
154
|
-
|
|
184
|
+
const sails = getGlobalSails()
|
|
185
|
+
if (sails) {
|
|
155
186
|
const schedule = {}
|
|
156
187
|
if (job.interval !== undefined) schedule.interval = job.interval
|
|
157
188
|
if (job.cron !== undefined) schedule.cron = job.cron
|
|
@@ -165,8 +196,8 @@ function addJobDefinition(jobDef, jobs = new Map(), config = {}) {
|
|
|
165
196
|
/**
|
|
166
197
|
* Remove a job from the jobs map
|
|
167
198
|
* @param {String} name - Job name
|
|
168
|
-
* @param {
|
|
169
|
-
* @param {
|
|
199
|
+
* @param {QuestJobsMap} [jobs] - Jobs map
|
|
200
|
+
* @param {QuestTimersMap} [timers] - Timers map
|
|
170
201
|
* @returns {Boolean} Success
|
|
171
202
|
*/
|
|
172
203
|
function removeJob(name, jobs = new Map(), timers = new Map()) {
|
|
@@ -181,7 +212,8 @@ function removeJob(name, jobs = new Map(), timers = new Map()) {
|
|
|
181
212
|
// Remove from jobs map
|
|
182
213
|
const existed = jobs.delete(name)
|
|
183
214
|
|
|
184
|
-
|
|
215
|
+
const sails = getGlobalSails()
|
|
216
|
+
if (existed && sails) {
|
|
185
217
|
sails.log.verbose(`Job "${name}" removed`)
|
|
186
218
|
}
|
|
187
219
|
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
const DEFAULT_RESULT_BYTES = 64 * 1024
|
|
4
|
+
const MAX_RESULT_BYTES = 1024 * 1024
|
|
5
|
+
|
|
6
|
+
/** @param {number} [value] */
|
|
7
|
+
function byteLimit(value) {
|
|
8
|
+
return Number.isFinite(value) && value > 0
|
|
9
|
+
? Math.min(MAX_RESULT_BYTES, Math.floor(value))
|
|
10
|
+
: DEFAULT_RESULT_BYTES
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Encode plain JSON without executing getters/toJSON or allocating an unbounded
|
|
15
|
+
* intermediate JSON string. Depth/node limits bound even tiny nested values.
|
|
16
|
+
* @param {any} value
|
|
17
|
+
* @param {number} [requestedBytes]
|
|
18
|
+
* @returns {{status: string, value?: any, json?: string, exit?: string}}
|
|
19
|
+
*/
|
|
20
|
+
function encodeResult(value, requestedBytes) {
|
|
21
|
+
if (value === undefined) return { status: 'undefined' }
|
|
22
|
+
const limit = byteLimit(requestedBytes)
|
|
23
|
+
const parts = []
|
|
24
|
+
const ancestors = new Set()
|
|
25
|
+
let bytes = 0
|
|
26
|
+
let nodes = 0
|
|
27
|
+
function append(text) {
|
|
28
|
+
bytes += Buffer.byteLength(text)
|
|
29
|
+
if (bytes > limit) throw new RangeError('too_large')
|
|
30
|
+
parts.push(text)
|
|
31
|
+
}
|
|
32
|
+
function visit(item, depth) {
|
|
33
|
+
if (++nodes > 10000 || depth > 64) throw new RangeError('too_large')
|
|
34
|
+
if (item === null) return append('null')
|
|
35
|
+
if (typeof item === 'boolean') return append(String(item))
|
|
36
|
+
if (typeof item === 'number' && Number.isFinite(item))
|
|
37
|
+
return append(String(item))
|
|
38
|
+
if (typeof item === 'string') {
|
|
39
|
+
if (
|
|
40
|
+
item.length > limit - bytes ||
|
|
41
|
+
Buffer.byteLength(item) > limit - bytes
|
|
42
|
+
)
|
|
43
|
+
throw new RangeError('too_large')
|
|
44
|
+
return append(JSON.stringify(item))
|
|
45
|
+
}
|
|
46
|
+
if (typeof item !== 'object') throw new TypeError('non_json')
|
|
47
|
+
if (ancestors.has(item)) throw new TypeError('circular')
|
|
48
|
+
const array = Array.isArray(item)
|
|
49
|
+
if (
|
|
50
|
+
!array &&
|
|
51
|
+
Object.getPrototypeOf(item) !== Object.prototype &&
|
|
52
|
+
Object.getPrototypeOf(item) !== null
|
|
53
|
+
) {
|
|
54
|
+
throw new TypeError('non_plain_object')
|
|
55
|
+
}
|
|
56
|
+
ancestors.add(item)
|
|
57
|
+
append(array ? '[' : '{')
|
|
58
|
+
const keys = array ? null : Object.keys(item)
|
|
59
|
+
const count = array ? item.length : keys.length
|
|
60
|
+
if (count > 10000) throw new RangeError('too_large')
|
|
61
|
+
for (let i = 0; i < count; i++) {
|
|
62
|
+
if (i) append(',')
|
|
63
|
+
const key = array ? String(i) : keys[i]
|
|
64
|
+
if (!array) {
|
|
65
|
+
if (
|
|
66
|
+
key.length > limit - bytes ||
|
|
67
|
+
Buffer.byteLength(key) > limit - bytes
|
|
68
|
+
)
|
|
69
|
+
throw new RangeError('too_large')
|
|
70
|
+
append(JSON.stringify(key) + ':')
|
|
71
|
+
}
|
|
72
|
+
const descriptor = Object.getOwnPropertyDescriptor(item, key)
|
|
73
|
+
if (!descriptor || !('value' in descriptor))
|
|
74
|
+
throw new TypeError('non_json_property')
|
|
75
|
+
visit(descriptor.value, depth + 1)
|
|
76
|
+
}
|
|
77
|
+
append(array ? ']' : '}')
|
|
78
|
+
ancestors.delete(item)
|
|
79
|
+
}
|
|
80
|
+
try {
|
|
81
|
+
visit(value, 0)
|
|
82
|
+
const json = parts.join('')
|
|
83
|
+
return { status: 'available', value: JSON.parse(json), json }
|
|
84
|
+
} catch (error) {
|
|
85
|
+
return {
|
|
86
|
+
status: error instanceof RangeError ? 'too_large' : 'serialization_error'
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
module.exports = { encodeResult, byteLimit }
|