alchemymvc 1.3.15 → 1.3.17
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/lib/app/behaviour/sluggable_behaviour.js +5 -1
- package/lib/app/conduit/socket_conduit.js +13 -0
- package/lib/app/datasource/mongo_datasource.js +86 -69
- package/lib/app/element/time_ago.js +19 -36
- package/lib/app/helper/alchemy_helper.js +2 -2
- package/lib/app/helper/cron.js +1502 -0
- package/lib/app/helper_datasource/00-nosql_datasource.js +16 -8
- package/lib/app/helper_field/schema_field.js +106 -7
- package/lib/app/helper_model/document.js +1 -0
- package/lib/app/helper_model/field_set.js +13 -0
- package/lib/app/helper_model/model.js +2 -2
- package/lib/app/model/alchemy_task_history_model.js +109 -0
- package/lib/app/model/alchemy_task_model.js +141 -18
- package/lib/bootstrap.js +3 -0
- package/lib/class/conduit.js +78 -21
- package/lib/class/datasource.js +18 -13
- package/lib/class/inode_file.js +5 -1
- package/lib/class/model.js +30 -2
- package/lib/class/route.js +5 -1
- package/lib/class/router.js +7 -2
- package/lib/class/schema_client.js +8 -11
- package/lib/class/session.js +14 -3
- package/lib/class/task.js +297 -145
- package/lib/class/task_service.js +933 -0
- package/lib/core/base.js +23 -1
- package/lib/core/client_alchemy.js +9 -5
- package/lib/core/middleware.js +2 -2
- package/lib/init/alchemy.js +149 -10
- package/lib/init/functions.js +53 -3
- package/package.json +25 -25
|
@@ -0,0 +1,933 @@
|
|
|
1
|
+
let TIMEOUT_UPDATE_INTERVAL = 7200000; // 2 hours
|
|
2
|
+
let MAX_TIMEOUT_DURATION = 86400000; // 24 hours
|
|
3
|
+
|
|
4
|
+
let running_task_menu,
|
|
5
|
+
start_task_menu,
|
|
6
|
+
singleton;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The TaskService class
|
|
10
|
+
*
|
|
11
|
+
* @author Jelle De Loecker <jelle@elevenways.be>
|
|
12
|
+
* @since 1.3.17
|
|
13
|
+
* @version 1.3.17
|
|
14
|
+
*/
|
|
15
|
+
const Service = Function.inherits('Alchemy.Base', 'Alchemy.Task', function TaskService() {
|
|
16
|
+
|
|
17
|
+
if (singleton) {
|
|
18
|
+
return singleton;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Get the AlchemyTask model
|
|
22
|
+
this.AlchemyTask = Model.get('AlchemyTask');
|
|
23
|
+
|
|
24
|
+
// Keep track of all running tasks
|
|
25
|
+
this.running_tasks = [];
|
|
26
|
+
|
|
27
|
+
// Keep track of next schedule per task
|
|
28
|
+
this.schedules = new Map();
|
|
29
|
+
|
|
30
|
+
// Has the task service finished loading?
|
|
31
|
+
this.has_loaded = false;
|
|
32
|
+
|
|
33
|
+
this.initSchedules();
|
|
34
|
+
|
|
35
|
+
singleton = this;
|
|
36
|
+
|
|
37
|
+
if (alchemy.settings.janeway_task_menu) {
|
|
38
|
+
this.createJanewayTaskMenu();
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Get a TaskSchedule instance for the given taks
|
|
44
|
+
*
|
|
45
|
+
* @author Jelle De Loecker <jelle@elevenways.be>
|
|
46
|
+
* @since 1.3.17
|
|
47
|
+
* @version 1.3.17
|
|
48
|
+
*
|
|
49
|
+
* @return {TaskSchedules|String}
|
|
50
|
+
*/
|
|
51
|
+
Service.setMethod(function getTaskSchedules(constructor) {
|
|
52
|
+
|
|
53
|
+
if (typeof constructor == 'string') {
|
|
54
|
+
constructor = Classes.Alchemy.Task.Task.getMember(constructor);
|
|
55
|
+
|
|
56
|
+
if (!constructor) {
|
|
57
|
+
console.warn('Could not find task constructor', constructor);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (!constructor) {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
let result = this.schedules.get(constructor);
|
|
66
|
+
|
|
67
|
+
if (!result) {
|
|
68
|
+
result = new TaskSchedules(constructor);
|
|
69
|
+
this.schedules.set(constructor, result);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return result;
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Checksum the given cron schedule & settings
|
|
77
|
+
*
|
|
78
|
+
* @author Jelle De Loecker <jelle@elevenways.be>
|
|
79
|
+
* @since 1.3.17
|
|
80
|
+
* @version 1.3.17
|
|
81
|
+
*
|
|
82
|
+
* @param {String} type_path
|
|
83
|
+
* @param {Cron} cron
|
|
84
|
+
* @param {Object} settings
|
|
85
|
+
*
|
|
86
|
+
* @return {String}
|
|
87
|
+
*/
|
|
88
|
+
Service.setMethod(function checksumSystemSchedule(type_path, cron, settings) {
|
|
89
|
+
|
|
90
|
+
if (!cron) {
|
|
91
|
+
return '';
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (!settings) {
|
|
95
|
+
settings = {};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
let result = Object.checksum([type_path, cron.toDry(), settings]);
|
|
99
|
+
|
|
100
|
+
return result;
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Initialize the schedules on boot
|
|
105
|
+
*
|
|
106
|
+
* @author Jelle De Loecker <jelle@elevenways.be>
|
|
107
|
+
* @since 1.3.17
|
|
108
|
+
* @version 1.3.17
|
|
109
|
+
*/
|
|
110
|
+
Service.setMethod(async function initSchedules() {
|
|
111
|
+
|
|
112
|
+
if (this.has_loaded) {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
let crit = this.AlchemyTask.find();
|
|
117
|
+
crit.where('schedule_type').in(['system_forced', 'system_fallback']);
|
|
118
|
+
|
|
119
|
+
let existing_system_records = new Map(),
|
|
120
|
+
required_system_schedules = new Map();
|
|
121
|
+
|
|
122
|
+
// Iterate over all the Task classes & see if they have any forced schedules
|
|
123
|
+
for (let task_class of Classes.Alchemy.Task.Task.getLiveDescendantsMap()) {
|
|
124
|
+
|
|
125
|
+
let forced_schedules = task_class.forced_cron_schedules,
|
|
126
|
+
fallback_schedules = task_class.fallback_cron_schedules;
|
|
127
|
+
|
|
128
|
+
// Add the hard-coded forced schedules
|
|
129
|
+
if (forced_schedules?.length) {
|
|
130
|
+
for (let entry of forced_schedules) {
|
|
131
|
+
let checksum = this.checksumSystemSchedule(task_class.type_path, entry.cron_schedule, entry.settings);
|
|
132
|
+
required_system_schedules.set(checksum, {
|
|
133
|
+
schedule_type : 'system_forced',
|
|
134
|
+
task_type : task_class.type_path,
|
|
135
|
+
cron : entry.cron_schedule,
|
|
136
|
+
settings : entry.settings,
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// And the fallback schedules
|
|
142
|
+
if (fallback_schedules?.length) {
|
|
143
|
+
for (let entry of fallback_schedules) {
|
|
144
|
+
let checksum = this.checksumSystemSchedule(task_class.type_path, entry.cron_schedule, entry.settings);
|
|
145
|
+
required_system_schedules.set(checksum, {
|
|
146
|
+
schedule_type : 'system_fallback',
|
|
147
|
+
task_type : task_class.type_path,
|
|
148
|
+
cron : entry.cron_schedule,
|
|
149
|
+
settings : entry.settings,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Iterate over all the existing system schedules in the database
|
|
156
|
+
for await (let record of crit) {
|
|
157
|
+
|
|
158
|
+
if (!record.schedule_type || record.schedule_type == 'user') {
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
try {
|
|
163
|
+
|
|
164
|
+
const checksum = record.forced_schedule_checksum;
|
|
165
|
+
|
|
166
|
+
// If (for any reason) this system schedule does not have a checksum,
|
|
167
|
+
// just remove it
|
|
168
|
+
if (!checksum) {
|
|
169
|
+
await record.remove();
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
let entry = required_system_schedules.get(checksum);
|
|
173
|
+
|
|
174
|
+
// If the checksum is not in the required system schedules, remove it
|
|
175
|
+
if (!entry) {
|
|
176
|
+
await record.remove();
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// If this checksum has already been seen, also remove it
|
|
180
|
+
if (existing_system_records.has(checksum)) {
|
|
181
|
+
await record.remove();
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Update the record
|
|
185
|
+
record.frequency = entry.cron.input;
|
|
186
|
+
record.settings = entry.settings;
|
|
187
|
+
record.schedule_type = entry.schedule_type;
|
|
188
|
+
|
|
189
|
+
await record.save();
|
|
190
|
+
|
|
191
|
+
} catch (err) {
|
|
192
|
+
alchemy.registerError(err);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Iterate over all the required system schedules and create new ones
|
|
197
|
+
for (let [checksum, entry] of required_system_schedules) {
|
|
198
|
+
|
|
199
|
+
if (existing_system_records.has(checksum)) {
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
try {
|
|
204
|
+
let record = this.AlchemyTask.createDocument();
|
|
205
|
+
record.frequency = entry.cron.input;
|
|
206
|
+
record.settings = entry.settings;
|
|
207
|
+
record.schedule_type = entry.schedule_type;
|
|
208
|
+
record.forced_schedule_checksum = checksum;
|
|
209
|
+
record.type = entry.task_type;
|
|
210
|
+
record.enabled = true;
|
|
211
|
+
await record.save();
|
|
212
|
+
|
|
213
|
+
existing_system_records.set(checksum, record);
|
|
214
|
+
} catch (err) {
|
|
215
|
+
alchemy.registerError(err);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Now we can schedule all the tasks
|
|
220
|
+
await this.rescheduleAllTasks();
|
|
221
|
+
|
|
222
|
+
// Check all the timeout calls every 2 hours
|
|
223
|
+
setInterval(() => this.updateTimeoutCalls(), TIMEOUT_UPDATE_INTERVAL);
|
|
224
|
+
|
|
225
|
+
this.has_loaded = true;
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Create the Janeway task menu
|
|
230
|
+
*
|
|
231
|
+
* @author Jelle De Loecker <jelle@elevenways.be>
|
|
232
|
+
* @since 1.3.17
|
|
233
|
+
* @version 1.3.17
|
|
234
|
+
*/
|
|
235
|
+
Service.setMethod(function createJanewayTaskMenu() {
|
|
236
|
+
|
|
237
|
+
if (!alchemy.Janeway || running_task_menu) {
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
running_task_menu = alchemy.Janeway.addIndicator('⌚︎', {
|
|
242
|
+
sorter: function sortItems(a, b) {
|
|
243
|
+
|
|
244
|
+
let result = 0;
|
|
245
|
+
|
|
246
|
+
if (!a.task_schedule) {
|
|
247
|
+
// Things without schedules are titles
|
|
248
|
+
result = -10;
|
|
249
|
+
} else if (!b.task_schedule) {
|
|
250
|
+
// Things without schedules are titles
|
|
251
|
+
result = 10;
|
|
252
|
+
} else if (a.task_schedule.is_running) {
|
|
253
|
+
// If it's running, it should be at the top
|
|
254
|
+
result = -5;
|
|
255
|
+
} else if (a.task_schedule.has_ended) {
|
|
256
|
+
// If it has ended, it should be at the bottom
|
|
257
|
+
result = -3;
|
|
258
|
+
} else if (!a.task_schedule.next_scheduled_date) {
|
|
259
|
+
// If it has no scheduled date, it should be at the bottom
|
|
260
|
+
result = 1;
|
|
261
|
+
} else {
|
|
262
|
+
result = a.task_schedule.next_scheduled_date - b.task_schedule.next_scheduled_date;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return result;
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
if (!running_task_menu.addItem) {
|
|
270
|
+
return running_task_menu.remove();
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
running_task_menu.addItem('Currently scheduled tasks:', () => {
|
|
274
|
+
console.log('Currently scheduled tasks:', this.schedules);
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
alchemy.Janeway.running_task_menu = running_task_menu;
|
|
278
|
+
this.janeway_running_task_menu = running_task_menu;
|
|
279
|
+
this.janeway_running_task_menu_entries = [];
|
|
280
|
+
|
|
281
|
+
start_task_menu = alchemy.Janeway.addIndicator('▶︎', {
|
|
282
|
+
area: 'left',
|
|
283
|
+
sorter: function(a, b) {
|
|
284
|
+
|
|
285
|
+
if (!isFinite(a.weight) || !isFinite(b.weight)) {
|
|
286
|
+
return b.weight - a.weight;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
return a.title.toLowerCase().localeCompare(b.title.toLowerCase());
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
start_task_menu.addItem('Start a task:', {weight: Infinity});
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Update all the schedule `setTimeout` calls that need it
|
|
298
|
+
*
|
|
299
|
+
* @author Jelle De Loecker <jelle@elevenways.be>
|
|
300
|
+
* @since 1.3.17
|
|
301
|
+
* @version 1.3.17
|
|
302
|
+
*/
|
|
303
|
+
Service.setMethod(function updateTimeoutCalls() {
|
|
304
|
+
for (let task_schedule of this.schedules.values()) {
|
|
305
|
+
task_schedule.updateTimeoutCalls();
|
|
306
|
+
}
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Schedule forced tasks
|
|
311
|
+
*
|
|
312
|
+
* @author Jelle De Loecker <jelle@elevenways.be>
|
|
313
|
+
* @since 1.3.17
|
|
314
|
+
* @version 1.3.17
|
|
315
|
+
*/
|
|
316
|
+
Service.setMethod(async function rescheduleAllTasks() {
|
|
317
|
+
|
|
318
|
+
// Iterate over all the existing schedules & clear them
|
|
319
|
+
for (let existing_schedule of this.schedules.values()) {
|
|
320
|
+
existing_schedule.clearAll();
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Iterate over all the Task classes & reschedule them
|
|
324
|
+
for (let task_class of Classes.Alchemy.Task.Task.getLiveDescendantsMap()) {
|
|
325
|
+
await this.rescheduleTasksOfType(task_class);
|
|
326
|
+
}
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Get the constructor of the given task type
|
|
331
|
+
*
|
|
332
|
+
* @author Jelle De Loecker <jelle@elevenways.be>
|
|
333
|
+
* @since 1.3.17
|
|
334
|
+
* @version 1.3.17
|
|
335
|
+
*
|
|
336
|
+
* @param {String|Function} task_type_path
|
|
337
|
+
*
|
|
338
|
+
* @return {Function|Boolean}
|
|
339
|
+
*/
|
|
340
|
+
Service.setMethod(function getTaskConstructor(task_type_path) {
|
|
341
|
+
|
|
342
|
+
let task_class;
|
|
343
|
+
|
|
344
|
+
if (typeof task_type_path == 'string') {
|
|
345
|
+
task_class = Classes.Alchemy.Task.Task.getMember(task_type_path);
|
|
346
|
+
|
|
347
|
+
if (!task_class) {
|
|
348
|
+
console.warn('Could not find task constructor', task_type_path);
|
|
349
|
+
return false;
|
|
350
|
+
}
|
|
351
|
+
} else if (typeof task_type_path == 'function') {
|
|
352
|
+
task_class = task_type_path;
|
|
353
|
+
} else {
|
|
354
|
+
task_class = false;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
return task_class;
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Reschedule all tasks of the given type
|
|
362
|
+
*
|
|
363
|
+
* @author Jelle De Loecker <jelle@elevenways.be>
|
|
364
|
+
* @since 1.3.17
|
|
365
|
+
* @version 1.3.17
|
|
366
|
+
*
|
|
367
|
+
* @param {String|Function} task_class
|
|
368
|
+
*/
|
|
369
|
+
Service.setMethod(async function rescheduleTasksOfType(task_class) {
|
|
370
|
+
|
|
371
|
+
task_class = this.getTaskConstructor(task_class);
|
|
372
|
+
|
|
373
|
+
if (!task_class) {
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
let schedules = this.getTaskSchedules(task_class);
|
|
378
|
+
|
|
379
|
+
if (!schedules) {
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
schedules.clearAll();
|
|
384
|
+
|
|
385
|
+
const Task = Model.get('AlchemyTask');
|
|
386
|
+
const crit = Task.find();
|
|
387
|
+
crit.where('type').equals(task_class.type_path);
|
|
388
|
+
const records = await Task.find('all', crit);
|
|
389
|
+
|
|
390
|
+
// Do the user & forced tasks first
|
|
391
|
+
for (let task_record of records) {
|
|
392
|
+
|
|
393
|
+
schedules.createStartMenuEntry(task_record);
|
|
394
|
+
|
|
395
|
+
// Skip records without a valid frequency or that are disabled
|
|
396
|
+
if (!task_record.frequency || !task_record.enabled) {
|
|
397
|
+
continue;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
if (!task_record.schedule_type || task_record.schedule_type == 'system_fallback') {
|
|
401
|
+
continue;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
try {
|
|
405
|
+
let cron = new Classes.Alchemy.Cron(task_record.frequency);
|
|
406
|
+
let settings = task_record.settings;
|
|
407
|
+
schedules.add(cron, settings, task_record);
|
|
408
|
+
} catch (err) {
|
|
409
|
+
alchemy.registerError(err);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// Do the fallback tasks now
|
|
414
|
+
if (!schedules.length) {
|
|
415
|
+
for (let task_record of records) {
|
|
416
|
+
|
|
417
|
+
// Skip records without a valid frequency or that are disabled
|
|
418
|
+
if (!task_record.frequency || !task_record.enabled) {
|
|
419
|
+
continue;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// Skip records that are not fallbacks
|
|
423
|
+
if (task_record.schedule_type != 'system_fallback') {
|
|
424
|
+
continue;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
try {
|
|
428
|
+
let cron = new Classes.Alchemy.Cron(task_record.frequency);
|
|
429
|
+
let settings = task_record.settings;
|
|
430
|
+
schedules.add(cron, settings, task_record);
|
|
431
|
+
} catch (err) {
|
|
432
|
+
alchemy.registerError(err);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Simple class to keep track of a task's schedule
|
|
440
|
+
*
|
|
441
|
+
* @author Jelle De Loecker <jelle@elevenways.be>
|
|
442
|
+
* @since 1.3.17
|
|
443
|
+
* @version 1.3.17
|
|
444
|
+
*/
|
|
445
|
+
class TaskSchedules {
|
|
446
|
+
task_constructor = null;
|
|
447
|
+
schedules = [];
|
|
448
|
+
start_task_menu_items = new Map();
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* Initialize the instance
|
|
452
|
+
*
|
|
453
|
+
* @author Jelle De Loecker <jelle@elevenways.be>
|
|
454
|
+
* @since 1.3.17
|
|
455
|
+
* @version 1.3.17
|
|
456
|
+
*/
|
|
457
|
+
constructor(task_constructor) {
|
|
458
|
+
this.task_constructor = task_constructor;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* Getter for the amount of schedules
|
|
463
|
+
*
|
|
464
|
+
* @author Jelle De Loecker <jelle@elevenways.be>
|
|
465
|
+
* @since 1.3.17
|
|
466
|
+
* @version 1.3.17
|
|
467
|
+
*/
|
|
468
|
+
get length() {
|
|
469
|
+
return this.schedules.length;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* Add a schedule (with the given settings)
|
|
474
|
+
*
|
|
475
|
+
* @author Jelle De Loecker <jelle@elevenways.be>
|
|
476
|
+
* @since 1.3.17
|
|
477
|
+
* @version 1.3.17
|
|
478
|
+
*
|
|
479
|
+
* @param {Cron} cron The actual schedule
|
|
480
|
+
* @param {Object} settings The settings
|
|
481
|
+
* @param {Document.AlchemyTask} task_document The task record
|
|
482
|
+
*/
|
|
483
|
+
add(cron, settings, task_document) {
|
|
484
|
+
let schedule = new TaskSchedule(this, cron, settings, task_document);
|
|
485
|
+
schedule.calculateNextScheduledDate();
|
|
486
|
+
this.schedules.push(schedule);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* Update all the timeout calls that need it.
|
|
491
|
+
*
|
|
492
|
+
* @author Jelle De Loecker <jelle@elevenways.be>
|
|
493
|
+
* @since 1.3.17
|
|
494
|
+
* @version 1.3.17
|
|
495
|
+
*/
|
|
496
|
+
updateTimeoutCalls() {
|
|
497
|
+
for (let task_schedule of this.schedules.values()) {
|
|
498
|
+
task_schedule.updateTimeoutCall();
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* Create a start menu entry for the given record
|
|
504
|
+
*
|
|
505
|
+
* @author Jelle De Loecker <jelle@elevenways.be>
|
|
506
|
+
* @since 1.3.17
|
|
507
|
+
* @version 1.3.17
|
|
508
|
+
*/
|
|
509
|
+
createStartMenuEntry(task_record) {
|
|
510
|
+
|
|
511
|
+
if (!start_task_menu) {
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
let title = task_record.title || task_record.type;
|
|
516
|
+
|
|
517
|
+
let entry = start_task_menu.addItem('▪︎ ' + title, () => {
|
|
518
|
+
this.#startFromMenu(task_record);
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
this.start_task_menu_items.set(task_record, entry);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/**
|
|
525
|
+
* Start a task from the menu
|
|
526
|
+
*
|
|
527
|
+
* @author Jelle De Loecker <jelle@elevenways.be>
|
|
528
|
+
* @since 1.3.17
|
|
529
|
+
* @version 1.3.17
|
|
530
|
+
*/
|
|
531
|
+
#startFromMenu(task_record) {
|
|
532
|
+
console.log('Manually starting', task_record.type, 'task:', task_record);
|
|
533
|
+
let schedule = new TaskSchedule(this, null, task_record.settings, task_record);
|
|
534
|
+
schedule.startManually();
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
/**
|
|
538
|
+
* Clear all the schedules
|
|
539
|
+
*
|
|
540
|
+
* @author Jelle De Loecker <jelle@elevenways.be>
|
|
541
|
+
* @since 1.3.17
|
|
542
|
+
* @version 1.3.17
|
|
543
|
+
*/
|
|
544
|
+
clearAll() {
|
|
545
|
+
|
|
546
|
+
for (let entry of this.start_task_menu_items.values()) {
|
|
547
|
+
entry.remove();
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
this.start_task_menu_items.clear();
|
|
551
|
+
|
|
552
|
+
for (let schedule of this.schedules) {
|
|
553
|
+
schedule.clearTimer();
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
this.schedules = [];
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* Simple class to keep track of a task's schedule
|
|
562
|
+
*
|
|
563
|
+
* @author Jelle De Loecker <jelle@elevenways.be>
|
|
564
|
+
* @since 1.3.17
|
|
565
|
+
* @version 1.3.17
|
|
566
|
+
*/
|
|
567
|
+
class TaskSchedule {
|
|
568
|
+
task_schedules = null;
|
|
569
|
+
task_document = null;
|
|
570
|
+
cron = null;
|
|
571
|
+
settings = null;
|
|
572
|
+
|
|
573
|
+
next_scheduled_date = null;
|
|
574
|
+
timer_id = null;
|
|
575
|
+
is_running = false;
|
|
576
|
+
has_ended = false;
|
|
577
|
+
task_instance = null;
|
|
578
|
+
history_document = null;
|
|
579
|
+
janeway_menu_item = null;
|
|
580
|
+
|
|
581
|
+
#change_counter = 0;
|
|
582
|
+
|
|
583
|
+
/**
|
|
584
|
+
* Initialize the instance
|
|
585
|
+
*
|
|
586
|
+
* @author Jelle De Loecker <jelle@elevenways.be>
|
|
587
|
+
* @since 1.3.17
|
|
588
|
+
* @version 1.3.17
|
|
589
|
+
*
|
|
590
|
+
* @param {TaskSchedules} task_schedules
|
|
591
|
+
* @param {Cron} cron
|
|
592
|
+
* @param {Object} settings
|
|
593
|
+
* @param {Document.AlchemyTask} task_document
|
|
594
|
+
*/
|
|
595
|
+
constructor(task_schedules, cron, settings, task_document) {
|
|
596
|
+
this.task_schedules = task_schedules;
|
|
597
|
+
this.task_document = task_document;
|
|
598
|
+
this.cron = cron;
|
|
599
|
+
this.settings = settings;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
/**
|
|
603
|
+
* Create a new instance with the current settings
|
|
604
|
+
*
|
|
605
|
+
* @author Jelle De Loecker <jelle@elevenways.be>
|
|
606
|
+
* @since 1.3.17
|
|
607
|
+
* @version 1.3.17
|
|
608
|
+
*/
|
|
609
|
+
createInstance() {
|
|
610
|
+
let settings = this.settings ? JSON.clone(this.settings) : null;
|
|
611
|
+
let result = new this.task_schedules.task_constructor();
|
|
612
|
+
result.setPayload(settings);
|
|
613
|
+
result.setAlchemyTaskDocument(this.task_document);
|
|
614
|
+
result.setAlchemyTaskHistoryDocument(this.history_document);
|
|
615
|
+
return result;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
/**
|
|
619
|
+
* Calculate the next scheduled date
|
|
620
|
+
*
|
|
621
|
+
* @author Jelle De Loecker <jelle@elevenways.be>
|
|
622
|
+
* @since 1.3.17
|
|
623
|
+
* @version 1.3.17
|
|
624
|
+
*/
|
|
625
|
+
calculateNextScheduledDate() {
|
|
626
|
+
let next_date = this.cron.getNextDate();
|
|
627
|
+
this.proposeDate(next_date);
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
/**
|
|
631
|
+
* With the current scheduled date,
|
|
632
|
+
* check if the timeout call needs to be updated.
|
|
633
|
+
*
|
|
634
|
+
* @author Jelle De Loecker <jelle@elevenways.be>
|
|
635
|
+
* @since 1.3.17
|
|
636
|
+
* @version 1.3.17
|
|
637
|
+
*/
|
|
638
|
+
updateTimeoutCall() {
|
|
639
|
+
|
|
640
|
+
// If it already has a timer_id, or it's running, do nothing
|
|
641
|
+
if (this.timer_id != null || this.is_running) {
|
|
642
|
+
return;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
// If there is no next scheduled date, or it's in the past, calculate a new one
|
|
646
|
+
if (!this.next_scheduled_date || this.next_scheduled_date < Date.now()) {
|
|
647
|
+
return this.calculateNextScheduledDate();
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
// Propose the wanted next scheduled date again
|
|
651
|
+
if (this.next_scheduled_date) {
|
|
652
|
+
this.#setDate(this.next_scheduled_date);
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
/**
|
|
657
|
+
* Propose a new scheduled date.
|
|
658
|
+
* If another date is already set, the closest one will be used.
|
|
659
|
+
*
|
|
660
|
+
* @author Jelle De Loecker <jelle@elevenways.be>
|
|
661
|
+
* @since 1.3.17
|
|
662
|
+
* @version 1.3.17
|
|
663
|
+
*/
|
|
664
|
+
proposeDate(scheduled_date) {
|
|
665
|
+
|
|
666
|
+
if (this.is_running) {
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
if (!scheduled_date) {
|
|
671
|
+
return;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
if (!this.next_scheduled_date || scheduled_date < this.next_scheduled_date) {
|
|
675
|
+
this.#setDate(scheduled_date);
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
/**
|
|
680
|
+
* Get the AlchemyTaskHistory document for the given date.
|
|
681
|
+
* If it does not exist yet, it will be created.
|
|
682
|
+
*
|
|
683
|
+
* @author Jelle De Loecker <jelle@elevenways.be>
|
|
684
|
+
* @since 1.3.17
|
|
685
|
+
* @version 1.3.17
|
|
686
|
+
*/
|
|
687
|
+
async getHistoryDocumentForDate(date) {
|
|
688
|
+
|
|
689
|
+
const TaskHistory = Model.get('AlchemyTaskHistory');
|
|
690
|
+
const crit = TaskHistory.find();
|
|
691
|
+
|
|
692
|
+
crit.where('scheduled_at').equals(date);
|
|
693
|
+
crit.where('alchemy_task_id').equals(this.task_document.$pk);
|
|
694
|
+
crit.where('type').equals(this.task_schedules.task_constructor.type_path);
|
|
695
|
+
crit.where('started_at').isEmpty();
|
|
696
|
+
|
|
697
|
+
let result = await TaskHistory.find('first', crit);
|
|
698
|
+
|
|
699
|
+
if (!result) {
|
|
700
|
+
result = TaskHistory.createDocument();
|
|
701
|
+
|
|
702
|
+
// Try to set the possible AlchemyTask document id first
|
|
703
|
+
result.alchemy_task_id = this.task_document?.$pk;
|
|
704
|
+
|
|
705
|
+
// Set when this task is supposed to run
|
|
706
|
+
result.scheduled_at = date;
|
|
707
|
+
|
|
708
|
+
// Set our PID
|
|
709
|
+
result.process_id = process.pid;
|
|
710
|
+
|
|
711
|
+
// Set the type of task
|
|
712
|
+
result.type = this.task_schedules.task_constructor.type_path;
|
|
713
|
+
|
|
714
|
+
// Set the settings
|
|
715
|
+
result.settings = this.settings;
|
|
716
|
+
|
|
717
|
+
// It is not yet running
|
|
718
|
+
result.is_running = false;
|
|
719
|
+
|
|
720
|
+
await result.save();
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
return result;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
/**
|
|
727
|
+
* Set the next scheduled date.
|
|
728
|
+
* Current date will be overridden no matter what.
|
|
729
|
+
*
|
|
730
|
+
* @author Jelle De Loecker <jelle@elevenways.be>
|
|
731
|
+
* @since 1.3.17
|
|
732
|
+
* @version 1.3.17
|
|
733
|
+
*/
|
|
734
|
+
#setDate(scheduled_date) {
|
|
735
|
+
|
|
736
|
+
this.clearTimer();
|
|
737
|
+
|
|
738
|
+
if (!scheduled_date) {
|
|
739
|
+
return;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
if (running_task_menu) {
|
|
743
|
+
let title = this.task_document?.title || this.task_schedules.task_constructor.type_name;
|
|
744
|
+
title += ' @ ' + scheduled_date.format('Y-m-d H:i:s');
|
|
745
|
+
this.janeway_menu_item = running_task_menu.addItem(title, () => this.clickedJanewayMenuItem());
|
|
746
|
+
this.janeway_menu_item.task_schedule = this;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
// Calculate the timeout that should be set
|
|
750
|
+
let timeout = scheduled_date.getTime() - Date.now();
|
|
751
|
+
|
|
752
|
+
if (timeout < 0) {
|
|
753
|
+
timeout = 0;
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
// `setTimeout` can only handle 32-bit integers,
|
|
757
|
+
// so we can't set timeouts for longer than 24.8 days
|
|
758
|
+
// We've even lowered this limit a bit further.
|
|
759
|
+
if (timeout >= MAX_TIMEOUT_DURATION) {
|
|
760
|
+
return;
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
return this.#setDateAndTimeout(scheduled_date, timeout);
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
/**
|
|
767
|
+
* Do something when the Janeway menu item is clicked.
|
|
768
|
+
*
|
|
769
|
+
* @author Jelle De Loecker <jelle@elevenways.be>
|
|
770
|
+
* @since 1.3.17
|
|
771
|
+
* @version 1.3.17
|
|
772
|
+
*/
|
|
773
|
+
clickedJanewayMenuItem() {
|
|
774
|
+
console.log('Clicked on scheduled task:', this);
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
/**
|
|
778
|
+
* Actually set the date & timeout (and history document)
|
|
779
|
+
*
|
|
780
|
+
* @author Jelle De Loecker <jelle@elevenways.be>
|
|
781
|
+
* @since 1.3.17
|
|
782
|
+
* @version 1.3.17
|
|
783
|
+
*/
|
|
784
|
+
async #setDateAndTimeout(scheduled_date, timeout) {
|
|
785
|
+
|
|
786
|
+
let current_counter = this.#change_counter;
|
|
787
|
+
|
|
788
|
+
this.next_scheduled_date = scheduled_date;
|
|
789
|
+
|
|
790
|
+
let history_document = await this.getHistoryDocumentForDate(scheduled_date);
|
|
791
|
+
|
|
792
|
+
// If something else triggered a change, do nothing
|
|
793
|
+
if (current_counter != this.#change_counter) {
|
|
794
|
+
return;
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
this.history_document = history_document;
|
|
798
|
+
|
|
799
|
+
this.timer_id = setTimeout(() => this.#run(), timeout);
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
/**
|
|
803
|
+
* Does the current process own this task?
|
|
804
|
+
*
|
|
805
|
+
* @author Jelle De Loecker <jelle@elevenways.be>
|
|
806
|
+
* @since 1.3.17
|
|
807
|
+
* @version 1.3.17
|
|
808
|
+
*/
|
|
809
|
+
async ownsScheduledTask() {
|
|
810
|
+
|
|
811
|
+
if (!this.history_document || !this.next_scheduled_date) {
|
|
812
|
+
return true;
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
// Get the document again!
|
|
816
|
+
let doc = await this.getHistoryDocumentForDate(this.next_scheduled_date);
|
|
817
|
+
|
|
818
|
+
if (!doc) {
|
|
819
|
+
return true;
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
this.history_document = doc;
|
|
823
|
+
|
|
824
|
+
// If it has already ended, return false
|
|
825
|
+
if (doc.ended_at || doc.had_error) {
|
|
826
|
+
return false;
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
// If we own the process, return true
|
|
830
|
+
if (doc.process_id == process.pid || !doc.process_id) {
|
|
831
|
+
return true;
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
// If the process is no longer running, claim it!
|
|
835
|
+
if (!alchemy.isProcessRunning(doc.process_id)) {
|
|
836
|
+
doc.process_id = process.pid;
|
|
837
|
+
return true;
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
return false;
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
/**
|
|
844
|
+
* Do a manual start
|
|
845
|
+
*
|
|
846
|
+
* @author Jelle De Loecker <jelle@elevenways.be>
|
|
847
|
+
* @since 1.3.17
|
|
848
|
+
* @version 1.3.17
|
|
849
|
+
*/
|
|
850
|
+
async startManually() {
|
|
851
|
+
return this.#run(true);
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
/**
|
|
855
|
+
* Actually run the task
|
|
856
|
+
*
|
|
857
|
+
* @author Jelle De Loecker <jelle@elevenways.be>
|
|
858
|
+
* @since 1.3.17
|
|
859
|
+
* @version 1.3.17
|
|
860
|
+
*/
|
|
861
|
+
async #run(started_manually = false) {
|
|
862
|
+
|
|
863
|
+
if (this.is_running) {
|
|
864
|
+
return false;
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
this.is_running = true;
|
|
868
|
+
let janeway_item;
|
|
869
|
+
let janeway_title;
|
|
870
|
+
|
|
871
|
+
let postfix = '';
|
|
872
|
+
|
|
873
|
+
if (started_manually) {
|
|
874
|
+
postfix += ' (manually)';
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
try {
|
|
878
|
+
if (running_task_menu) {
|
|
879
|
+
janeway_title = this.task_document?.title || this.task_schedules.task_constructor.type_name;
|
|
880
|
+
let title = 'Currently running: ' + janeway_title + postfix;
|
|
881
|
+
janeway_item = running_task_menu.addItem(title, () => this.clickedJanewayMenuItem());
|
|
882
|
+
janeway_item.task_schedule = this;
|
|
883
|
+
}
|
|
884
|
+
} catch (err) {
|
|
885
|
+
alchemy.registerError(err);
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
try {
|
|
889
|
+
if (started_manually || await this.ownsScheduledTask()) {
|
|
890
|
+
let instance = this.createInstance();
|
|
891
|
+
this.task_instance = instance;
|
|
892
|
+
await instance.start();
|
|
893
|
+
}
|
|
894
|
+
} catch (err) {
|
|
895
|
+
alchemy.registerError(err);
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
this.history_document = null;
|
|
899
|
+
this.next_scheduled_date = null;
|
|
900
|
+
this.task_instance = null;
|
|
901
|
+
this.is_running = false;
|
|
902
|
+
this.has_ended = true;
|
|
903
|
+
|
|
904
|
+
if (!started_manually) {
|
|
905
|
+
this.calculateNextScheduledDate();
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
if (janeway_item) {
|
|
909
|
+
let title = 'Finished: ' + janeway_title + postfix;
|
|
910
|
+
janeway_item.setContent(title);
|
|
911
|
+
setTimeout(() => janeway_item.remove(), 5000);
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
/**
|
|
916
|
+
* Clear the timer
|
|
917
|
+
*
|
|
918
|
+
* @author Jelle De Loecker <jelle@elevenways.be>
|
|
919
|
+
* @since 1.3.17
|
|
920
|
+
* @version 1.3.17
|
|
921
|
+
*/
|
|
922
|
+
clearTimer() {
|
|
923
|
+
clearTimeout(this.timer_id);
|
|
924
|
+
this.timer_id = null;
|
|
925
|
+
this.next_scheduled_date = null;
|
|
926
|
+
this.#change_counter++;
|
|
927
|
+
|
|
928
|
+
if (this.janeway_menu_item) {
|
|
929
|
+
this.janeway_menu_item.remove();
|
|
930
|
+
this.janeway_menu_item = null;
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
}
|