alchemymvc 1.3.16 → 1.3.18

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.
@@ -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
+ }