@goodandready/dsh-cron 0.3.24 → 0.3.25

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-item.js CHANGED
@@ -21,7 +21,8 @@ export async function handleTaskItem({ store, scheduler, req, res, url, id, acti
21
21
  }
22
22
 
23
23
  if (req.method === 'GET') {
24
- if (action === 'stream') {
24
+ // Both /dsh-cron/tasks/:id/stream and /dsh-cron/tasks/:id/logs/stream (#392)
25
+ if (action === 'stream' || (action === 'logs' && subAction === 'stream')) {
25
26
  logStreamer.handleSseRequest(id, req, res);
26
27
  return;
27
28
  }
package/lib/client.js CHANGED
@@ -2736,14 +2736,46 @@ if (window.__dshLiveLogsEvtSource) {
2736
2736
  try { window.__dshLiveLogsEvtSource.close(); } catch (e) { void e; }
2737
2737
  window.__dshLiveLogsEvtSource = null;
2738
2738
  }
2739
+ if (window.__dshLiveLogsPollTimer) {
2740
+ clearInterval(window.__dshLiveLogsPollTimer);
2741
+ window.__dshLiveLogsPollTimer = null;
2742
+ }
2739
2743
  setLiveLogsTaskId(task.id);
2740
2744
  setLiveLogsTaskTitle(task.title || task.id);
2741
2745
  setLiveLogsLines([]);
2742
2746
  setLiveLogsStatus(task.running ? 'connecting' : 'idle');
2743
2747
  setLiveLogsCompleted(false);
2744
2748
  setLiveLogsModalOpen(true);
2749
+ const startPollingFallback = () => {
2750
+ if (window.__dshLiveLogsPollTimer) return;
2751
+ const pollFn = () => {
2752
+ fetchJsonWithTimeout('/dsh-cron/tasks/' + encodeURIComponent(task.id) + '/history?limit=1')
2753
+ .then((data) => {
2754
+ if (data && data.history && data.history.length > 0) {
2755
+ const lastRun = data.history[0];
2756
+ if (lastRun.output || lastRun.error) {
2757
+ const text = lastRun.output || lastRun.error;
2758
+ setLiveLogsLines([{ chunk: text, stream: lastRun.error ? 'stderr' : 'stdout', ts: lastRun.at }]);
2759
+ }
2760
+ if (lastRun.status) {
2761
+ setLiveLogsStatus(lastRun.status);
2762
+ setLiveLogsCompleted(true);
2763
+ if (window.__dshLiveLogsPollTimer) {
2764
+ clearInterval(window.__dshLiveLogsPollTimer);
2765
+ window.__dshLiveLogsPollTimer = null;
2766
+ }
2767
+ }
2768
+ }
2769
+ })
2770
+ .catch(() => {});
2771
+ };
2772
+ pollFn();
2773
+ window.__dshLiveLogsPollTimer = setInterval(pollFn, 2000);
2774
+ };
2775
+ if (typeof EventSource !== 'undefined') {
2745
2776
  try {
2746
- const es = new EventSource('/dsh-cron/tasks/' + encodeURIComponent(task.id) + '/stream');
2777
+ const streamUrl = '/dsh-cron/tasks/' + encodeURIComponent(task.id) + '/logs/stream';
2778
+ const es = new EventSource(streamUrl);
2747
2779
  window.__dshLiveLogsEvtSource = es;
2748
2780
  es.addEventListener('init', (e) => {
2749
2781
  try {
@@ -2767,12 +2799,21 @@ try { es.close(); } catch (e) { void e; }
2767
2799
  window.__dshLiveLogsEvtSource = null;
2768
2800
  });
2769
2801
  es.onerror = () => {
2770
- setLiveLogsStatus((curr) => curr === 'running' ? 'completed' : 'closed');
2771
2802
  try { es.close(); } catch (e) { void e; }
2772
2803
  window.__dshLiveLogsEvtSource = null;
2804
+ setLiveLogsStatus((curr) => {
2805
+ if (curr === 'running' || curr === 'connecting') {
2806
+ startPollingFallback();
2807
+ return curr;
2808
+ }
2809
+ return 'completed';
2810
+ });
2773
2811
  };
2774
2812
  } catch (err) {
2775
- setLiveLogsStatus('error');
2813
+ startPollingFallback();
2814
+ }
2815
+ } else {
2816
+ startPollingFallback();
2776
2817
  }
2777
2818
  }
2778
2819
  function handleCloseLiveLogs() {
@@ -2780,6 +2821,10 @@ if (window.__dshLiveLogsEvtSource) {
2780
2821
  try { window.__dshLiveLogsEvtSource.close(); } catch (e) { void e; }
2781
2822
  window.__dshLiveLogsEvtSource = null;
2782
2823
  }
2824
+ if (window.__dshLiveLogsPollTimer) {
2825
+ clearInterval(window.__dshLiveLogsPollTimer);
2826
+ window.__dshLiveLogsPollTimer = null;
2827
+ }
2783
2828
  setLiveLogsModalOpen(false);
2784
2829
  }
2785
2830
  const openAnalyticsDigest = async (days = 7) => {
@@ -50,12 +50,15 @@ export class LogStreamer extends EventEmitter {
50
50
  ts: Date.now(),
51
51
  };
52
52
 
53
+ entry.eventSeq = (entry.eventSeq || 0) + 1;
54
+ item.id = entry.eventSeq;
55
+
53
56
  entry.buffer.push(item);
54
57
  if (entry.buffer.length > MAX_BUFFER_LINES) {
55
58
  entry.buffer.shift();
56
59
  }
57
60
 
58
- const ssePayload = `event: log\ndata: ${JSON.stringify(item)}\n\n`;
61
+ const ssePayload = `id: ${item.id}\nevent: log\ndata: ${JSON.stringify(item)}\n\n`;
59
62
  for (const res of entry.subscribers) {
60
63
  try {
61
64
  res.write(ssePayload);
@@ -111,12 +114,17 @@ export class LogStreamer extends EventEmitter {
111
114
 
112
115
  const entry = this.streams.get(taskId);
113
116
 
117
+ // Support W3C Last-Event-ID for reconnection (#392)
118
+ const lastEventIdStr = req?.headers ? req.headers['last-event-id'] : undefined;
119
+ const lastEventId = lastEventIdStr ? parseInt(lastEventIdStr, 10) : 0;
120
+ const effectiveLogs = (entry?.buffer || []).filter((item) => !lastEventId || (item.id && item.id > lastEventId));
121
+
114
122
  // Initial event with current state and buffered logs
115
123
  const initData = {
116
124
  taskId,
117
125
  runId: entry?.runId || null,
118
126
  status: entry ? (entry.completed ? 'completed' : 'running') : 'idle',
119
- bufferedLogs: entry?.buffer || [],
127
+ bufferedLogs: effectiveLogs,
120
128
  };
121
129
  res.write(`event: init\ndata: ${JSON.stringify(initData)}\n\n`);
122
130
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-cron",
3
- "version": "0.3.24",
3
+ "version": "0.3.25",
4
4
  "description": "Background automation runner for DSH: isolated agent runs, script/HTTP/SSH/Docker runtimes, cost guard, notifications, heartbeats.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -23,7 +23,8 @@
23
23
  "build:client": "node scripts/build-client.mjs",
24
24
  "pretest": "node scripts/build-client.mjs",
25
25
  "preflight": "node scripts/ci-preflight.mjs",
26
- "test:engine": "node scripts/test-engines.mjs"
26
+ "test:engine": "node scripts/test-engines.mjs",
27
+ "build:client:check": "node scripts/build-client.mjs --check"
27
28
  },
28
29
  "keywords": [
29
30
  "dsh",
@@ -108,6 +109,7 @@
108
109
  "@deepseek-ai/dsh-session": ">=0.1.0-rc.6 <0.1.1 || >=0.1.1-rc.1 <0.1.2 || >=0.1.2-alpha.1 <1.0.0",
109
110
  "@deepseek-ai/dsh-settings": ">=0.1.0-rc.6 <0.1.1 || >=0.1.1-rc.1 <0.1.2 || >=0.1.2-alpha.1 <1.0.0",
110
111
  "@deepseek-ai/dsh-tools": ">=0.1.0-rc.6 <0.1.1 || >=0.1.1-rc.1 <0.1.2 || >=0.1.2-alpha.1 <1.0.0",
111
- "@deepseek-ai/schemastery": "^3.18.1"
112
+ "@deepseek-ai/schemastery": "^3.18.1",
113
+ "esbuild": "^0.28.2"
112
114
  }
113
115
  }