@goodandready/dsh-cron 0.2.10 → 0.2.11

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/api.js CHANGED
@@ -6,7 +6,7 @@
6
6
  */
7
7
 
8
8
  import { randomUUID, createHash } from 'node:crypto';
9
- import { parseScheduleExpression } from './scheduler.js';
9
+ import { parseScheduleExpression, previewSchedule } from './scheduler.js';
10
10
  import { normalizeTaskType, CODE_EXECUTING_TYPES } from './runtimes.js';
11
11
  import { CHANNEL_IDS, unknownChannelIds } from './channels.js';
12
12
  import { applyTaskPatch } from './task-patch.js';
@@ -60,6 +60,16 @@ export function createCronApiHandler(store, scheduler, collection) {
60
60
  return;
61
61
  }
62
62
 
63
+ if (scope === 'heartbeat' && id) {
64
+ handleHeartbeatPing({ store, req, res, taskId: id });
65
+ return;
66
+ }
67
+
68
+ if (scope === 'schedule' && id === 'preview') {
69
+ await handleSchedulePreview({ req, res, url });
70
+ return;
71
+ }
72
+
63
73
  if (!id) {
64
74
  await handleTaskCollection({ store, scheduler, collection, req, res, url, scope });
65
75
  return;
@@ -352,6 +362,16 @@ function applyImportPlan({ store, scheduler, plan }) {
352
362
  // ------------------------------------------------------------ single task
353
363
 
354
364
  async function handleTaskItem({ store, scheduler, req, res, url, id, action }) {
365
+ if ((req.method === 'GET' || req.method === 'POST') && action === 'heartbeat') {
366
+ handleHeartbeatPing({ store, req, res, taskId: id });
367
+ return;
368
+ }
369
+
370
+ if (req.method === 'POST' && action === 'dry-run') {
371
+ await handleDryRunTask({ store, scheduler, req, res, id });
372
+ return;
373
+ }
374
+
355
375
  if (req.method === 'GET' && action === 'archive') {
356
376
  const limit = parseInt(url.searchParams.get('limit') || '50', 10);
357
377
  const offset = parseInt(url.searchParams.get('offset') || '0', 10);
@@ -532,7 +552,15 @@ function mergeExecutionFields(current, body, parsed) {
532
552
  * tasks and the config-owned refusals are the same code, not a second copy.
533
553
  */
534
554
  export async function handleExternalTaskRequest({ store, scheduler, req, res, url }) {
535
- const parts = url.pathname.split('/').filter(Boolean); // dsh-cron, api, tasks, :id?, :action?
555
+ const parts = url.pathname.split('/').filter(Boolean); // dsh-cron, api, [tasks|heartbeat|schedule], :id?, :action?
556
+ if (parts[2] === 'heartbeat' && parts[3]) {
557
+ handleHeartbeatPing({ store, req, res, taskId: parts[3] });
558
+ return;
559
+ }
560
+ if (parts[2] === 'schedule' && parts[3] === 'preview') {
561
+ await handleSchedulePreview({ req, res, url });
562
+ return;
563
+ }
536
564
  const id = parts[3];
537
565
  const action = parts[4];
538
566
 
@@ -681,3 +709,66 @@ export async function handleTelegramWebhook({ store, scheduler, req, res }) {
681
709
 
682
710
  sendJson(res, 200, { ok: true, verb, taskId });
683
711
  }
712
+
713
+ export function handleHeartbeatPing({ store, req, res, taskId }) {
714
+ const task = store.get(taskId);
715
+ if (!task) {
716
+ sendJson(res, 404, { ok: false, error: 'Task not found' });
717
+ return;
718
+ }
719
+ const result = typeof store.recordHeartbeat === 'function' ? store.recordHeartbeat(taskId) : null;
720
+ if (!result) {
721
+ sendJson(res, 500, { ok: false, error: 'Could not record heartbeat' });
722
+ return;
723
+ }
724
+ sendJson(res, 200, result);
725
+ }
726
+
727
+ export async function handleSchedulePreview({ req, res, url }) {
728
+ let schedule = '';
729
+ let timezone = '';
730
+ let count = 5;
731
+
732
+ if (req.method === 'POST') {
733
+ const { body, error } = await readBody(req, res);
734
+ if (error) return;
735
+ schedule = body.schedule || '';
736
+ timezone = body.timezone || '';
737
+ count = body.count || 5;
738
+ } else if (req.method === 'GET') {
739
+ schedule = url.searchParams.get('schedule') || '';
740
+ timezone = url.searchParams.get('timezone') || '';
741
+ count = parseInt(url.searchParams.get('count') || '5', 10);
742
+ } else {
743
+ sendJson(res, 405, NOT_ALLOWED);
744
+ return;
745
+ }
746
+
747
+ if (!schedule) {
748
+ sendJson(res, 400, { ok: false, error: 'Field "schedule" is required' });
749
+ return;
750
+ }
751
+
752
+ try {
753
+ const preview = previewSchedule(schedule, { timezone, count });
754
+ sendJson(res, 200, { ok: true, ...preview });
755
+ } catch (err) {
756
+ sendJson(res, 400, { ok: false, error: err.message });
757
+ }
758
+ }
759
+
760
+ export async function handleDryRunTask({ store, scheduler, req, res, id }) {
761
+ if (rejectCrossOrigin(req, res)) return;
762
+ const task = store.get(id);
763
+ if (!task) {
764
+ sendJson(res, 404, { ok: false, error: 'Task not found' });
765
+ return;
766
+ }
767
+
768
+ try {
769
+ const outcome = await scheduler.runTask(id, { dryRun: true });
770
+ sendJson(res, 200, outcome || { ok: true, status: 'completed' });
771
+ } catch (err) {
772
+ sendJson(res, 500, { ok: false, error: err.message });
773
+ }
774
+ }