sails-hook-quest 0.0.5 → 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/lib/core/child-result.js +80 -0
- package/lib/core/execution-child.js +14 -0
- package/lib/core/executor.js +295 -181
- package/lib/core/job-control.js +69 -8
- package/lib/core/loader.js +9 -3
- package/lib/core/result.js +91 -0
- package/lib/core/runtime.js +240 -0
- package/lib/core/scheduler.js +83 -44
- package/lib/index.js +52 -11
- package/lib/types.js +75 -5
- package/package.json +10 -1
- 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/jsconfig.json +0 -17
- package/test/executor.test.js +0 -95
- package/typecheck/public-api-smoke.js +0 -84
package/lib/index.js
CHANGED
|
@@ -11,6 +11,8 @@ const scheduler = require('./core/scheduler')
|
|
|
11
11
|
const executor = require('./core/executor')
|
|
12
12
|
const loader = require('./core/loader')
|
|
13
13
|
const jobControl = require('./core/job-control')
|
|
14
|
+
const resident = require('./core/runtime')
|
|
15
|
+
const { isExecutionChild } = require('./core/execution-child')
|
|
14
16
|
|
|
15
17
|
/** @typedef {import('./types').AnyRecord} AnyRecord */
|
|
16
18
|
/** @typedef {import('./types').QuestApi} QuestApi */
|
|
@@ -44,6 +46,10 @@ function defineQuestHook(sails) {
|
|
|
44
46
|
jobs,
|
|
45
47
|
timers,
|
|
46
48
|
running,
|
|
49
|
+
sails,
|
|
50
|
+
runtime: resident.createRuntime(),
|
|
51
|
+
dueTimes: new Map(),
|
|
52
|
+
scheduleStates: new Map(),
|
|
47
53
|
config: null, // Will be set after sails.config is available
|
|
48
54
|
scheduleJob: null, // Will be set after function is defined
|
|
49
55
|
executeJob: null, // Will be set after function is defined
|
|
@@ -74,6 +80,9 @@ function defineQuestHook(sails) {
|
|
|
74
80
|
// Maximum child output retained on failure (live output remains unbounded)
|
|
75
81
|
diagnosticTailBytes: 64 * 1024,
|
|
76
82
|
|
|
83
|
+
resultBytes: 64 * 1024,
|
|
84
|
+
inputBytes: 64 * 1024,
|
|
85
|
+
|
|
77
86
|
// Jobs defined in config
|
|
78
87
|
jobs: []
|
|
79
88
|
}
|
|
@@ -82,15 +91,33 @@ function defineQuestHook(sails) {
|
|
|
82
91
|
initialize: async function () {
|
|
83
92
|
sails.log.info('Initializing Quest job scheduler')
|
|
84
93
|
|
|
85
|
-
|
|
86
|
-
|
|
94
|
+
// Register cleanup before awaiting dependencies so failed lift also stops timers.
|
|
95
|
+
sails.on('lower', async () => {
|
|
96
|
+
sails.log.info('Stopping Quest jobs...')
|
|
97
|
+
await jobControl.stopJobs(null, context)
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
const initializeAfterOrm = async () => {
|
|
101
|
+
// The owned CLI child needs the app's hooks/context, but must not start
|
|
102
|
+
// another copy of its resident schedules, even if source config enables it.
|
|
103
|
+
if (isExecutionChild()) sails.config.quest.autoStart = false
|
|
104
|
+
const questConfig = {
|
|
105
|
+
...sails.config.quest,
|
|
106
|
+
appPath:
|
|
107
|
+
sails.config.quest.appPath || sails.config.appPath || process.cwd()
|
|
108
|
+
}
|
|
87
109
|
|
|
88
110
|
// Set up context with config
|
|
89
111
|
context.config = questConfig
|
|
90
112
|
context.getNextRunTime = (job) =>
|
|
91
|
-
scheduler.getNextRunTime(job, questConfig)
|
|
113
|
+
scheduler.getNextRunTime(job, questConfig, (assessment) => {
|
|
114
|
+
context.scheduleStates.set(job.name, {
|
|
115
|
+
...context.scheduleStates.get(job.name),
|
|
116
|
+
...assessment
|
|
117
|
+
})
|
|
118
|
+
})
|
|
92
119
|
context.scheduleJob = (name) => jobControl.scheduleJob(name, context)
|
|
93
|
-
context.executeJob = (name, customInputs) => {
|
|
120
|
+
context.executeJob = (name, customInputs, trigger = 'manual') => {
|
|
94
121
|
const job = jobs.get(name)
|
|
95
122
|
if (!job) {
|
|
96
123
|
// Try to run as a regular script without quest config
|
|
@@ -100,9 +127,15 @@ function defineQuestHook(sails) {
|
|
|
100
127
|
withoutOverlapping: false,
|
|
101
128
|
inputs: {}
|
|
102
129
|
}
|
|
103
|
-
return executor.executeJob(name, minimalJob, customInputs,
|
|
130
|
+
return executor.executeJob(name, minimalJob, customInputs, {
|
|
131
|
+
...context,
|
|
132
|
+
trigger
|
|
133
|
+
})
|
|
104
134
|
}
|
|
105
|
-
return executor.executeJob(name, job, customInputs,
|
|
135
|
+
return executor.executeJob(name, job, customInputs, {
|
|
136
|
+
...context,
|
|
137
|
+
trigger
|
|
138
|
+
})
|
|
106
139
|
}
|
|
107
140
|
|
|
108
141
|
// Load jobs from scripts and config
|
|
@@ -129,6 +162,7 @@ function defineQuestHook(sails) {
|
|
|
129
162
|
/** @type {QuestNamedJobDefinition} */
|
|
130
163
|
const job = typeof def === 'string' ? { name: def } : def
|
|
131
164
|
loader.addJobDefinition(job, jobs, context.config || {})
|
|
165
|
+
context.scheduleStates.delete(job.name)
|
|
132
166
|
added.push(job.name)
|
|
133
167
|
if (context.config?.autoStart) {
|
|
134
168
|
jobControl.scheduleJob(job.name, context)
|
|
@@ -141,12 +175,17 @@ function defineQuestHook(sails) {
|
|
|
141
175
|
const removed = []
|
|
142
176
|
for (const name of names) {
|
|
143
177
|
if (loader.removeJob(name, jobs, timers)) {
|
|
178
|
+
context.dueTimes.delete(name)
|
|
179
|
+
context.scheduleStates.delete(name)
|
|
144
180
|
removed.push(name)
|
|
145
181
|
}
|
|
146
182
|
}
|
|
147
183
|
return removed
|
|
148
184
|
},
|
|
149
185
|
|
|
186
|
+
getRuntime: () => resident.runtimeInfo(context),
|
|
187
|
+
metadata: (name) => resident.metadata(context, name),
|
|
188
|
+
|
|
150
189
|
// Job information
|
|
151
190
|
jobs: Array.from(jobs.values()),
|
|
152
191
|
|
|
@@ -162,12 +201,14 @@ function defineQuestHook(sails) {
|
|
|
162
201
|
|
|
163
202
|
sails.quest = questApi
|
|
164
203
|
sails.log.info(`Quest started with ${jobs.size} scheduled job(s)`)
|
|
165
|
-
}
|
|
204
|
+
}
|
|
166
205
|
|
|
167
|
-
//
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
206
|
+
// Sails waits for this Promise before marking Quest loaded. ORM initializes
|
|
207
|
+
// independently; after() also handles an ORM event that already occurred.
|
|
208
|
+
await new Promise((resolve, reject) => {
|
|
209
|
+
sails.after('hook:orm:loaded', () => {
|
|
210
|
+
initializeAfterOrm().then(resolve, reject)
|
|
211
|
+
})
|
|
171
212
|
})
|
|
172
213
|
}
|
|
173
214
|
}
|
package/lib/types.js
CHANGED
|
@@ -10,11 +10,11 @@
|
|
|
10
10
|
*
|
|
11
11
|
* @typedef {JsonPrimitive | any[] | AnyRecord} JsonValue
|
|
12
12
|
*
|
|
13
|
-
* @typedef {string | number} QuestInterval
|
|
13
|
+
* @typedef {string | number | false} QuestInterval
|
|
14
14
|
*
|
|
15
15
|
* @typedef {string | number | false} QuestTimeout
|
|
16
16
|
*
|
|
17
|
-
* @typedef {string | number | Date} QuestDateInput
|
|
17
|
+
* @typedef {string | number | Date | false} QuestDateInput
|
|
18
18
|
*
|
|
19
19
|
* @typedef {Record<string, any>} QuestCronOptions
|
|
20
20
|
*
|
|
@@ -52,6 +52,8 @@
|
|
|
52
52
|
*
|
|
53
53
|
* @typedef {{
|
|
54
54
|
* name: string,
|
|
55
|
+
* script?: string,
|
|
56
|
+
* inputSchema?: QuestMachineInputs,
|
|
55
57
|
* friendlyName: string,
|
|
56
58
|
* description?: string,
|
|
57
59
|
* interval?: QuestInterval,
|
|
@@ -68,6 +70,8 @@
|
|
|
68
70
|
*
|
|
69
71
|
* @typedef {{
|
|
70
72
|
* name: string,
|
|
73
|
+
* script?: string,
|
|
74
|
+
* inputSchema?: QuestMachineInputs,
|
|
71
75
|
* inputs?: AnyRecord,
|
|
72
76
|
* scriptInputs?: AnyRecord,
|
|
73
77
|
* paused?: boolean,
|
|
@@ -101,23 +105,66 @@
|
|
|
101
105
|
* @typedef {{
|
|
102
106
|
* skipped: true,
|
|
103
107
|
* reason: 'already_running' | 'paused',
|
|
108
|
+
* runId?: string,
|
|
109
|
+
* runtimeId?: string,
|
|
110
|
+
* startedAt?: Date,
|
|
111
|
+
* trigger?: 'manual' | 'scheduled',
|
|
112
|
+
* finishedAt?: Date,
|
|
113
|
+
* exitCode?: number | null,
|
|
114
|
+
* signal?: string | null,
|
|
104
115
|
* }} QuestSkippedResult
|
|
105
116
|
*
|
|
106
117
|
* @typedef {{
|
|
107
118
|
* success: true,
|
|
108
119
|
* duration: number,
|
|
120
|
+
* runId?: string,
|
|
121
|
+
* runtimeId?: string,
|
|
122
|
+
* sequence?: number,
|
|
123
|
+
* startedAt?: Date,
|
|
124
|
+
* trigger?: 'manual' | 'scheduled',
|
|
125
|
+
* finishedAt?: Date,
|
|
126
|
+
* exitCode?: number | null,
|
|
127
|
+
* signal?: string | null,
|
|
128
|
+
* result?: QuestBusinessResult,
|
|
129
|
+
* logs?: QuestLogs,
|
|
109
130
|
* }} QuestSuccessResult
|
|
110
131
|
*
|
|
132
|
+
* @typedef {{status: string, value?: any, reason?: string, exit?: string}} QuestBusinessResult
|
|
133
|
+
*
|
|
134
|
+
* @typedef {{stdout: string, stderr: string, stdoutTruncated: boolean, stderrTruncated: boolean, limitBytes: number}} QuestLogs
|
|
135
|
+
*
|
|
136
|
+
* @typedef {{runtimeId: string, sequence: number, active: Map<string, Map<string, number>>}} QuestRuntime
|
|
137
|
+
*
|
|
111
138
|
* @typedef {QuestSkippedResult | QuestSuccessResult} QuestExecutionResult
|
|
112
139
|
*
|
|
113
140
|
* @typedef {{
|
|
114
141
|
* name: string,
|
|
142
|
+
* runId?: string,
|
|
143
|
+
* runtimeId?: string,
|
|
144
|
+
* sequence?: number,
|
|
145
|
+
* startedAt?: Date,
|
|
146
|
+
* trigger?: 'manual' | 'scheduled',
|
|
147
|
+
* finishedAt?: Date,
|
|
148
|
+
* exitCode?: number | null,
|
|
149
|
+
* signal?: string | null,
|
|
150
|
+
* result?: QuestBusinessResult,
|
|
151
|
+
* logs?: QuestLogs,
|
|
115
152
|
* inputs: AnyRecord,
|
|
116
153
|
* timestamp: Date,
|
|
117
154
|
* }} QuestJobStartEvent
|
|
118
155
|
*
|
|
119
156
|
* @typedef {{
|
|
120
157
|
* name: string,
|
|
158
|
+
* runId?: string,
|
|
159
|
+
* runtimeId?: string,
|
|
160
|
+
* sequence?: number,
|
|
161
|
+
* startedAt?: Date,
|
|
162
|
+
* trigger?: 'manual' | 'scheduled',
|
|
163
|
+
* finishedAt?: Date,
|
|
164
|
+
* exitCode?: number | null,
|
|
165
|
+
* signal?: string | null,
|
|
166
|
+
* result?: QuestBusinessResult,
|
|
167
|
+
* logs?: QuestLogs,
|
|
121
168
|
* inputs: AnyRecord,
|
|
122
169
|
* duration: number,
|
|
123
170
|
* timestamp: Date,
|
|
@@ -132,6 +179,16 @@
|
|
|
132
179
|
*
|
|
133
180
|
* @typedef {{
|
|
134
181
|
* name: string,
|
|
182
|
+
* runId?: string,
|
|
183
|
+
* runtimeId?: string,
|
|
184
|
+
* sequence?: number,
|
|
185
|
+
* startedAt?: Date,
|
|
186
|
+
* trigger?: 'manual' | 'scheduled',
|
|
187
|
+
* finishedAt?: Date,
|
|
188
|
+
* exitCode?: number | null,
|
|
189
|
+
* signal?: string | null,
|
|
190
|
+
* result?: QuestBusinessResult,
|
|
191
|
+
* logs?: QuestLogs,
|
|
135
192
|
* inputs: AnyRecord,
|
|
136
193
|
* error: QuestJobErrorDetails,
|
|
137
194
|
* duration: number,
|
|
@@ -144,6 +201,8 @@
|
|
|
144
201
|
* run(jobNames?: QuestJobNameInput, inputs?: AnyRecord): Promise<QuestExecutionResult[]>,
|
|
145
202
|
* add(jobDefs: QuestJobDefinitionInput | QuestJobDefinitionInput[]): string[],
|
|
146
203
|
* remove(jobNames: QuestJobNameInput): string[],
|
|
204
|
+
* getRuntime(): AnyRecord,
|
|
205
|
+
* metadata(name?: string): AnyRecord | AnyRecord[] | undefined,
|
|
147
206
|
* jobs: QuestJob[],
|
|
148
207
|
* list(): QuestJob[],
|
|
149
208
|
* get(name: string): QuestJob | undefined,
|
|
@@ -162,17 +221,23 @@
|
|
|
162
221
|
* jobs: QuestJobsMap,
|
|
163
222
|
* timers: QuestTimersMap,
|
|
164
223
|
* running: QuestRunningMap,
|
|
224
|
+
* sails: QuestSailsApp,
|
|
225
|
+
* runtime: QuestRuntime,
|
|
226
|
+
* dueTimes: Map<string, Date>,
|
|
227
|
+
* scheduleStates?: Map<string, AnyRecord>,
|
|
165
228
|
* config: QuestConfig | null,
|
|
166
229
|
* scheduleJob: ((name: string) => void) | null,
|
|
167
|
-
* executeJob: ((name: string, customInputs?: AnyRecord) => Promise<QuestExecutionResult>) | null,
|
|
230
|
+
* executeJob: ((name: string, customInputs?: AnyRecord, trigger?: 'manual' | 'scheduled') => Promise<QuestExecutionResult>) | null,
|
|
168
231
|
* getNextRunTime: ((job: QuestJob) => Date | null) | null,
|
|
169
232
|
* }} QuestContext
|
|
170
233
|
*
|
|
171
234
|
* @typedef {{
|
|
172
235
|
* jobs?: QuestJobsMap,
|
|
173
236
|
* timers?: QuestTimersMap,
|
|
237
|
+
* dueTimes?: Map<string, Date>,
|
|
238
|
+
* scheduleStates?: Map<string, AnyRecord>,
|
|
174
239
|
* getNextRunTime?: (job: QuestJob) => Date | null,
|
|
175
|
-
* executeJob?: (name: string, customInputs?: AnyRecord) => Promise<QuestExecutionResult>,
|
|
240
|
+
* executeJob?: (name: string, customInputs?: AnyRecord, trigger?: 'manual' | 'scheduled') => Promise<QuestExecutionResult>,
|
|
176
241
|
* }} QuestScheduleContext
|
|
177
242
|
*
|
|
178
243
|
* @typedef {{
|
|
@@ -183,15 +248,20 @@
|
|
|
183
248
|
* @typedef {{
|
|
184
249
|
* jobs?: QuestJobsMap,
|
|
185
250
|
* timers?: QuestTimersMap,
|
|
251
|
+
* dueTimes?: Map<string, Date>,
|
|
252
|
+
* scheduleStates?: Map<string, AnyRecord>,
|
|
186
253
|
* }} QuestStopContext
|
|
187
254
|
*
|
|
188
255
|
* @typedef {{
|
|
189
256
|
* jobs?: QuestJobsMap,
|
|
190
|
-
* executeJob?: (name: string, customInputs?: AnyRecord) => Promise<QuestExecutionResult>,
|
|
257
|
+
* executeJob?: (name: string, customInputs?: AnyRecord, trigger?: 'manual' | 'scheduled') => Promise<QuestExecutionResult>,
|
|
191
258
|
* }} QuestRunContext
|
|
192
259
|
*
|
|
193
260
|
* @typedef {{
|
|
261
|
+
* trigger?: 'manual' | 'scheduled',
|
|
194
262
|
* running?: QuestRunningMap,
|
|
263
|
+
* sails?: QuestSailsApp,
|
|
264
|
+
* runtime?: QuestRuntime,
|
|
195
265
|
* config?: QuestConfig | null,
|
|
196
266
|
* stdout?: NodeJS.WritableStream,
|
|
197
267
|
* stderr?: NodeJS.WritableStream,
|
package/package.json
CHANGED
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sails-hook-quest",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.6",
|
|
4
4
|
"description": "Elegant job scheduling for Sails.js applications with human-readable intervals, cron expressions, and full Sails context",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
|
+
"files": [
|
|
7
|
+
"lib",
|
|
8
|
+
"README.md",
|
|
9
|
+
"CONTRACT.md",
|
|
10
|
+
"CHANGELOG.md",
|
|
11
|
+
"LICENSE"
|
|
12
|
+
],
|
|
6
13
|
"scripts": {
|
|
7
14
|
"test": "sounding test",
|
|
8
15
|
"lint": "prettier --check .",
|
|
@@ -56,6 +63,8 @@
|
|
|
56
63
|
"husky": "^9.1.6",
|
|
57
64
|
"lint-staged": "^15.2.10",
|
|
58
65
|
"prettier": "^3.3.3",
|
|
66
|
+
"sails": "^1.5.18",
|
|
67
|
+
"sails-hook-orm": "^4.0.3",
|
|
59
68
|
"sounding": "^0.2.0",
|
|
60
69
|
"typescript": "^5.6.2"
|
|
61
70
|
}
|
package/.commitlintrc.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
module.exports = { extends: ['@commitlint/config-conventional'] }
|
package/.github/FUNDING.yml
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
github: DominusKelvin
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
name: Prettier
|
|
2
|
-
|
|
3
|
-
on: [push, pull_request]
|
|
4
|
-
|
|
5
|
-
jobs:
|
|
6
|
-
prettier:
|
|
7
|
-
runs-on: ubuntu-latest
|
|
8
|
-
|
|
9
|
-
steps:
|
|
10
|
-
- name: Checkout code
|
|
11
|
-
uses: actions/checkout@v3
|
|
12
|
-
|
|
13
|
-
- name: Setup Node.js
|
|
14
|
-
uses: actions/setup-node@v3
|
|
15
|
-
with:
|
|
16
|
-
node-version: 18
|
|
17
|
-
|
|
18
|
-
- name: Run npm ci
|
|
19
|
-
run: npm ci
|
|
20
|
-
|
|
21
|
-
- name: Run Prettier
|
|
22
|
-
run: npx prettier --config ./.prettierrc.js --write .
|
package/.husky/pre-commit
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
npx lint-staged
|
package/.prettierrc.js
DELETED
package/jsconfig.json
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"allowJs": true,
|
|
4
|
-
"checkJs": true,
|
|
5
|
-
"noEmit": true,
|
|
6
|
-
"target": "ES2022",
|
|
7
|
-
"module": "NodeNext",
|
|
8
|
-
"moduleResolution": "NodeNext",
|
|
9
|
-
"lib": ["ES2022"],
|
|
10
|
-
"types": ["node"],
|
|
11
|
-
"maxNodeModuleJsDepth": 0,
|
|
12
|
-
"strict": false,
|
|
13
|
-
"skipLibCheck": true
|
|
14
|
-
},
|
|
15
|
-
"include": ["lib/**/*.js", "typecheck/**/*.js"],
|
|
16
|
-
"exclude": ["node_modules"]
|
|
17
|
-
}
|
package/test/executor.test.js
DELETED
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
const { EventEmitter } = require('node:events')
|
|
2
|
-
const fs = require('node:fs')
|
|
3
|
-
const os = require('node:os')
|
|
4
|
-
const path = require('node:path')
|
|
5
|
-
const { Writable } = require('node:stream')
|
|
6
|
-
const { createRuntime, createTestApi } = require('sounding')
|
|
7
|
-
|
|
8
|
-
const { createDiagnosticTail, executeJob } = require('../lib/core/executor')
|
|
9
|
-
|
|
10
|
-
const sails = new EventEmitter()
|
|
11
|
-
sails.config = {
|
|
12
|
-
appPath: process.cwd(),
|
|
13
|
-
environment: 'test',
|
|
14
|
-
datastores: {}
|
|
15
|
-
}
|
|
16
|
-
sails.hooks = {}
|
|
17
|
-
sails.helpers = {}
|
|
18
|
-
sails.models = {}
|
|
19
|
-
sails.log = { info() {}, warn() {}, error() {}, verbose() {} }
|
|
20
|
-
|
|
21
|
-
const test = createTestApi({ runtime: createRuntime(sails) })
|
|
22
|
-
|
|
23
|
-
test('failed jobs keep live output and emit bounded diagnostics', async ({
|
|
24
|
-
expect,
|
|
25
|
-
t
|
|
26
|
-
}) => {
|
|
27
|
-
const appPath = fs.mkdtempSync(path.join(os.tmpdir(), 'quest-diagnostics-'))
|
|
28
|
-
const scriptsPath = path.join(appPath, 'scripts')
|
|
29
|
-
const runnerPath = path.join(appPath, 'fake-sails')
|
|
30
|
-
fs.mkdirSync(scriptsPath)
|
|
31
|
-
fs.writeFileSync(path.join(scriptsPath, 'send-issue-notifications.js'), '')
|
|
32
|
-
fs.writeFileSync(
|
|
33
|
-
runnerPath,
|
|
34
|
-
[
|
|
35
|
-
'#!/usr/bin/env node',
|
|
36
|
-
"process.stdout.write('Preparing notifications\\n')",
|
|
37
|
-
"process.stderr.write('Error: database exploded\\n at sendIssueNotifications (/app/scripts/send-issue-notifications.js:12:3)\\n')",
|
|
38
|
-
'process.exitCode = 1'
|
|
39
|
-
].join('\n')
|
|
40
|
-
)
|
|
41
|
-
fs.chmodSync(runnerPath, 0o755)
|
|
42
|
-
|
|
43
|
-
const previousSails = global.sails
|
|
44
|
-
global.sails = sails
|
|
45
|
-
|
|
46
|
-
let liveOutput = ''
|
|
47
|
-
const output = new Writable({
|
|
48
|
-
write(chunk, encoding, callback) {
|
|
49
|
-
liveOutput += chunk.toString()
|
|
50
|
-
callback()
|
|
51
|
-
}
|
|
52
|
-
})
|
|
53
|
-
const failure = new Promise((resolve) =>
|
|
54
|
-
sails.once('quest:job:error', resolve)
|
|
55
|
-
)
|
|
56
|
-
|
|
57
|
-
t.after(() => {
|
|
58
|
-
global.sails = previousSails
|
|
59
|
-
fs.rmSync(appPath, { recursive: true, force: true })
|
|
60
|
-
})
|
|
61
|
-
|
|
62
|
-
let executionError
|
|
63
|
-
try {
|
|
64
|
-
await executeJob(
|
|
65
|
-
'send-issue-notifications',
|
|
66
|
-
{ name: 'send-issue-notifications' },
|
|
67
|
-
{},
|
|
68
|
-
{
|
|
69
|
-
config: { appPath, sailsPath: runnerPath, diagnosticTailBytes: 1024 },
|
|
70
|
-
stdout: output,
|
|
71
|
-
stderr: output
|
|
72
|
-
}
|
|
73
|
-
)
|
|
74
|
-
} catch (error) {
|
|
75
|
-
executionError = error
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
const event = await failure
|
|
79
|
-
expect(executionError.message).toContain('exited with code 1')
|
|
80
|
-
expect(liveOutput).toContain('Preparing notifications')
|
|
81
|
-
expect(liveOutput).toContain('database exploded')
|
|
82
|
-
expect(event.error.diagnostic).toContain('sendIssueNotifications')
|
|
83
|
-
expect(event.error.stack).toContain('executor.js')
|
|
84
|
-
})
|
|
85
|
-
|
|
86
|
-
test('diagnostic tails discard old output at the configured byte limit', ({
|
|
87
|
-
expect
|
|
88
|
-
}) => {
|
|
89
|
-
const tail = createDiagnosticTail(12)
|
|
90
|
-
tail.append('discard-this-')
|
|
91
|
-
tail.append('keep-this')
|
|
92
|
-
|
|
93
|
-
expect(Buffer.byteLength(tail.value())).toBe(12)
|
|
94
|
-
expect(tail.value()).toBe('is-keep-this')
|
|
95
|
-
})
|
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
// @ts-check
|
|
2
|
-
|
|
3
|
-
const defineQuestHook = require('../lib')
|
|
4
|
-
const scheduler = require('../lib/core/scheduler')
|
|
5
|
-
const loader = require('../lib/core/loader')
|
|
6
|
-
|
|
7
|
-
/** @typedef {import('../lib/types').QuestSailsApp} QuestSailsApp */
|
|
8
|
-
/** @typedef {import('../lib/types').QuestNamedJobDefinition} QuestNamedJobDefinition */
|
|
9
|
-
|
|
10
|
-
/** @type {QuestSailsApp} */
|
|
11
|
-
const sails = {
|
|
12
|
-
config: {
|
|
13
|
-
quest: {
|
|
14
|
-
autoStart: false,
|
|
15
|
-
timezone: 'UTC',
|
|
16
|
-
withoutOverlapping: true,
|
|
17
|
-
jobs: [
|
|
18
|
-
'cleanup-sessions',
|
|
19
|
-
{
|
|
20
|
-
name: 'health-check',
|
|
21
|
-
interval: '5 minutes',
|
|
22
|
-
inputs: { url: 'https://example.com/health' }
|
|
23
|
-
}
|
|
24
|
-
]
|
|
25
|
-
}
|
|
26
|
-
},
|
|
27
|
-
log: {
|
|
28
|
-
info() {},
|
|
29
|
-
warn() {},
|
|
30
|
-
error() {},
|
|
31
|
-
verbose() {}
|
|
32
|
-
},
|
|
33
|
-
after(_eventName, handler) {
|
|
34
|
-
handler()
|
|
35
|
-
},
|
|
36
|
-
on() {},
|
|
37
|
-
emit() {}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
const hook = defineQuestHook(sails)
|
|
41
|
-
hook.defaults.quest.jobs.push({
|
|
42
|
-
name: 'daily-digest',
|
|
43
|
-
cron: '0 9 * * *',
|
|
44
|
-
timezone: 'Africa/Lagos'
|
|
45
|
-
})
|
|
46
|
-
|
|
47
|
-
/** @type {QuestNamedJobDefinition} */
|
|
48
|
-
const dynamicJob = {
|
|
49
|
-
name: 'dynamic-report',
|
|
50
|
-
interval: '1 hour',
|
|
51
|
-
inputs: { format: 'csv' }
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
sails.quest?.add(dynamicJob)
|
|
55
|
-
sails.quest?.start(['health-check', 'dynamic-report'])
|
|
56
|
-
sails.quest?.run('health-check', { url: 'https://example.com/ping' })
|
|
57
|
-
sails.quest?.pause('dynamic-report')
|
|
58
|
-
sails.quest?.resume('dynamic-report')
|
|
59
|
-
sails.quest?.remove(['dynamic-report'])
|
|
60
|
-
|
|
61
|
-
const jobs = sails.quest?.list() || []
|
|
62
|
-
const firstJob = jobs[0]
|
|
63
|
-
if (firstJob) {
|
|
64
|
-
scheduler.getNextRunTime(firstJob, sails.config.quest)
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
loader.addJobDefinition(
|
|
68
|
-
{
|
|
69
|
-
name: 'warm-cache',
|
|
70
|
-
timeout: '10 minutes',
|
|
71
|
-
withoutOverlapping: false
|
|
72
|
-
},
|
|
73
|
-
new Map(),
|
|
74
|
-
sails.config.quest
|
|
75
|
-
)
|
|
76
|
-
|
|
77
|
-
// @ts-expect-error Job names must be strings or string arrays.
|
|
78
|
-
sails.quest?.start(123)
|
|
79
|
-
|
|
80
|
-
// @ts-expect-error Dynamic jobs need a string name when provided as objects.
|
|
81
|
-
sails.quest?.add({ name: 123, interval: '5 minutes' })
|
|
82
|
-
|
|
83
|
-
// @ts-expect-error Dynamic job objects need an explicit name.
|
|
84
|
-
sails.quest?.add({ interval: '5 minutes' })
|