@coji/durably-react 0.11.0 → 0.13.0

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/README.md CHANGED
@@ -28,10 +28,10 @@ export const durably = createDurably<typeof durably>({
28
28
  })
29
29
 
30
30
  function MyComponent() {
31
- const { trigger, isRunning, isCompleted, output } = durably.myJob.useJob()
31
+ const { trigger, isLeased, isCompleted, output } = durably.myJob.useJob()
32
32
 
33
33
  return (
34
- <button onClick={() => trigger({ id: '123' })} disabled={isRunning}>
34
+ <button onClick={() => trigger({ id: '123' })} disabled={isLeased}>
35
35
  Run
36
36
  </button>
37
37
  )
@@ -79,9 +79,9 @@ function App() {
79
79
  }
80
80
 
81
81
  function MyComponent() {
82
- const { trigger, isRunning, isCompleted } = useJob(myJob)
82
+ const { trigger, isLeased, isCompleted } = useJob(myJob)
83
83
  return (
84
- <button onClick={() => trigger({ id: '123' })} disabled={isRunning}>
84
+ <button onClick={() => trigger({ id: '123' })} disabled={isLeased}>
85
85
  Run
86
86
  </button>
87
87
  )
@@ -33,16 +33,14 @@ var initialSubscriptionState = {
33
33
  };
34
34
  function subscriptionReducer(state, action) {
35
35
  switch (action.type) {
36
- case "run:start":
37
- return { ...state, status: "running" };
36
+ case "run:leased":
37
+ return { ...state, status: "leased" };
38
38
  case "run:complete":
39
39
  return { ...state, status: "completed", output: action.output };
40
40
  case "run:fail":
41
41
  return { ...state, status: "failed", error: action.error };
42
42
  case "run:cancel":
43
43
  return { ...state, status: "cancelled" };
44
- case "run:retry":
45
- return { ...state, status: "pending", error: null };
46
44
  case "run:progress":
47
45
  return { ...state, progress: action.progress };
48
46
  case "log:write": {
@@ -88,9 +86,8 @@ function useSubscription(subscriber, runId, options) {
88
86
  const unsubscribe = subscriber.subscribe(runId, (event) => {
89
87
  if (runIdRef.current !== runId) return;
90
88
  switch (event.type) {
91
- case "run:start":
89
+ case "run:leased":
92
90
  case "run:cancel":
93
- case "run:retry":
94
91
  dispatch({ type: event.type });
95
92
  break;
96
93
  case "run:complete":
@@ -139,4 +136,4 @@ export {
139
136
  useSubscription,
140
137
  isJobDefinition
141
138
  };
142
- //# sourceMappingURL=chunk-XRJEZWAV.js.map
139
+ //# sourceMappingURL=chunk-33VIIDHK.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/types.ts","../src/shared/create-log-entry.ts","../src/shared/subscription-reducer.ts","../src/shared/use-subscription.ts"],"sourcesContent":["// Shared type definitions for @coji/durably-react\n\nimport type { ClientRun, JobDefinition, Run } from '@coji/durably'\n\n// Type inference utilities for extracting Input/Output types from JobDefinition\nexport type InferInput<T> =\n T extends JobDefinition<string, infer TInput, unknown>\n ? TInput extends Record<string, unknown>\n ? TInput\n : Record<string, unknown>\n : T extends { trigger: (input: infer TInput) => unknown }\n ? TInput extends Record<string, unknown>\n ? TInput\n : Record<string, unknown>\n : Record<string, unknown>\n\nexport type InferOutput<T> =\n T extends JobDefinition<string, unknown, infer TOutput>\n ? TOutput extends Record<string, unknown>\n ? TOutput\n : Record<string, unknown>\n : T extends {\n trigger: (input: unknown) => Promise<{ output?: infer TOutput }>\n }\n ? TOutput extends Record<string, unknown>\n ? TOutput\n : Record<string, unknown>\n : Record<string, unknown>\n\nexport type RunStatus =\n | 'pending'\n | 'leased'\n | 'completed'\n | 'failed'\n | 'cancelled'\n\nexport interface Progress {\n current: number\n total?: number\n message?: string\n}\n\nexport interface LogEntry {\n id: string\n runId: string\n stepName: string | null\n level: 'info' | 'warn' | 'error'\n message: string\n data: unknown\n timestamp: string\n}\n\n// Shared subscription state (used by both direct and SSE subscriptions)\nexport interface SubscriptionState<TOutput = unknown> {\n status: RunStatus | null\n output: TOutput | null\n error: string | null\n logs: LogEntry[]\n progress: Progress | null\n}\n\n// SSE event types (sent from server).\n// Note: Unlike core DurablyEvent, these omit timestamp/sequence because\n// the SSE handler in server.ts sends only the fields needed by the UI.\nexport type DurablyEvent =\n | { type: 'run:leased'; runId: string; jobName: string; input: unknown }\n | {\n type: 'run:complete'\n runId: string\n jobName: string\n output: unknown\n duration: number\n }\n | { type: 'run:fail'; runId: string; jobName: string; error: string }\n | { type: 'run:cancel'; runId: string; jobName: string }\n | { type: 'run:delete'; runId: string; jobName: string }\n | { type: 'run:trigger'; runId: string; jobName: string; input: unknown }\n | {\n type: 'run:progress'\n runId: string\n jobName: string\n progress: Progress\n }\n | {\n type: 'step:start'\n runId: string\n jobName: string\n stepName: string\n stepIndex: number\n }\n | {\n type: 'step:complete'\n runId: string\n jobName: string\n stepName: string\n stepIndex: number\n output: unknown\n }\n | {\n type: 'step:cancel'\n runId: string\n jobName: string\n stepName: string\n stepIndex: number\n labels: Record<string, string>\n }\n | {\n type: 'log:write'\n runId: string\n jobName: string\n stepName: string | null\n labels: Record<string, string>\n level: 'info' | 'warn' | 'error'\n message: string\n data: unknown\n }\n\n// =============================================================================\n// Typed Run types for useRuns hooks\n// =============================================================================\n\n/**\n * A typed version of Run with generic input/output types.\n * Used by browser hooks (direct durably access).\n */\nexport type TypedRun<\n TInput extends Record<string, unknown> = Record<string, unknown>,\n TOutput extends Record<string, unknown> | undefined =\n | Record<string, unknown>\n | undefined,\n> = Omit<Run, 'input' | 'output'> & {\n input: TInput\n output: TOutput | null\n}\n\n// ClientRun is imported from '@coji/durably' and re-exported for consumers.\nexport type { ClientRun } from '@coji/durably'\n\n/**\n * A typed version of ClientRun with generic input/output types.\n * Used by client hooks (HTTP/SSE connection).\n */\nexport type TypedClientRun<\n TInput extends Record<string, unknown> = Record<string, unknown>,\n TOutput extends Record<string, unknown> | undefined =\n | Record<string, unknown>\n | undefined,\n> = Omit<ClientRun, 'input' | 'output'> & {\n input: TInput\n output: TOutput | null\n}\n\n/**\n * Type guard to check if an object is a JobDefinition.\n * Used to distinguish between JobDefinition and options objects in overloaded functions.\n */\nexport function isJobDefinition<\n TName extends string = string,\n TInput extends Record<string, unknown> = Record<string, unknown>,\n TOutput extends Record<string, unknown> | undefined = undefined,\n>(obj: unknown): obj is JobDefinition<TName, TInput, TOutput> {\n return (\n typeof obj === 'object' &&\n obj !== null &&\n 'name' in obj &&\n 'run' in obj &&\n typeof (obj as { run: unknown }).run === 'function'\n )\n}\n","import type { LogEntry } from '../types'\n\nexport interface CreateLogEntryParams {\n runId: string\n stepName: string | null\n level: 'info' | 'warn' | 'error'\n message: string\n data: unknown\n}\n\n/**\n * Creates a LogEntry with auto-generated id and timestamp.\n * Extracted to eliminate duplication between subscription hooks.\n */\nexport function createLogEntry(params: CreateLogEntryParams): LogEntry {\n return {\n id: crypto.randomUUID(),\n runId: params.runId,\n stepName: params.stepName,\n level: params.level,\n message: params.message,\n data: params.data,\n timestamp: new Date().toISOString(),\n }\n}\n\n/**\n * Appends a log entry to the array, respecting maxLogs limit.\n */\nexport function appendLog(\n logs: LogEntry[],\n newLog: LogEntry,\n maxLogs: number,\n): LogEntry[] {\n const newLogs = [...logs, newLog]\n if (maxLogs > 0 && newLogs.length > maxLogs) {\n return newLogs.slice(-maxLogs)\n }\n return newLogs\n}\n","import type { Progress, SubscriptionState } from '../types'\nimport { appendLog, createLogEntry } from './create-log-entry'\n\n// Action types for subscription state transitions\nexport type SubscriptionAction<TOutput = unknown> =\n | { type: 'run:leased' }\n | { type: 'run:complete'; output: TOutput }\n | { type: 'run:fail'; error: string }\n | { type: 'run:cancel' }\n | { type: 'run:progress'; progress: Progress }\n | {\n type: 'log:write'\n runId: string\n stepName: string | null\n level: 'info' | 'warn' | 'error'\n message: string\n data: unknown\n maxLogs: number\n }\n | { type: 'reset' }\n | { type: 'clear_logs' }\n | { type: 'connection_error'; error: string }\n\nexport const initialSubscriptionState: SubscriptionState<unknown> = {\n status: null,\n output: null,\n error: null,\n logs: [],\n progress: null,\n}\n\n/**\n * Pure reducer for subscription state transitions.\n * Extracted to eliminate duplication between useRunSubscription and useSSESubscription.\n */\nexport function subscriptionReducer<TOutput = unknown>(\n state: SubscriptionState<TOutput>,\n action: SubscriptionAction<TOutput>,\n): SubscriptionState<TOutput> {\n switch (action.type) {\n case 'run:leased':\n return { ...state, status: 'leased' }\n\n case 'run:complete':\n return { ...state, status: 'completed', output: action.output }\n\n case 'run:fail':\n return { ...state, status: 'failed', error: action.error }\n\n case 'run:cancel':\n return { ...state, status: 'cancelled' }\n\n case 'run:progress':\n return { ...state, progress: action.progress }\n\n case 'log:write': {\n const newLog = createLogEntry({\n runId: action.runId,\n stepName: action.stepName,\n level: action.level,\n message: action.message,\n data: action.data,\n })\n return { ...state, logs: appendLog(state.logs, newLog, action.maxLogs) }\n }\n\n case 'reset':\n return initialSubscriptionState as SubscriptionState<TOutput>\n\n case 'clear_logs':\n return { ...state, logs: [] }\n\n case 'connection_error':\n return { ...state, error: action.error }\n\n default:\n return state\n }\n}\n","import { useCallback, useEffect, useReducer, useRef } from 'react'\nimport type { SubscriptionState } from '../types'\nimport type { EventSubscriber } from './event-subscriber'\nimport {\n initialSubscriptionState,\n subscriptionReducer,\n} from './subscription-reducer'\n\nexport interface UseSubscriptionOptions {\n /**\n * Maximum number of logs to keep (0 = unlimited)\n */\n maxLogs?: number\n}\n\nexport interface UseSubscriptionResult<\n TOutput = unknown,\n> extends SubscriptionState<TOutput> {\n /**\n * Clear all logs\n */\n clearLogs: () => void\n /**\n * Reset all state\n */\n reset: () => void\n}\n\n/**\n * Core subscription hook that works with any EventSubscriber implementation.\n * This unifies the subscription logic between Durably.on and SSE.\n */\nexport function useSubscription<TOutput = unknown>(\n subscriber: EventSubscriber | null,\n runId: string | null,\n options?: UseSubscriptionOptions,\n): UseSubscriptionResult<TOutput> {\n const [state, dispatch] = useReducer(\n subscriptionReducer<TOutput>,\n initialSubscriptionState as SubscriptionState<TOutput>,\n )\n\n const runIdRef = useRef<string | null>(runId)\n const prevRunIdRef = useRef<string | null>(null)\n\n const maxLogs = options?.maxLogs ?? 0\n\n // Reset state when runId changes\n if (prevRunIdRef.current !== runId) {\n prevRunIdRef.current = runId\n if (runIdRef.current !== runId) {\n dispatch({ type: 'reset' })\n }\n }\n runIdRef.current = runId\n\n useEffect(() => {\n if (!subscriber || !runId) return\n\n const unsubscribe = subscriber.subscribe<TOutput>(runId, (event) => {\n // Verify runId hasn't changed during async operation\n if (runIdRef.current !== runId) return\n\n switch (event.type) {\n case 'run:leased':\n case 'run:cancel':\n dispatch({ type: event.type })\n break\n case 'run:complete':\n dispatch({ type: 'run:complete', output: event.output })\n break\n case 'run:fail':\n dispatch({ type: 'run:fail', error: event.error })\n break\n case 'run:progress':\n dispatch({ type: 'run:progress', progress: event.progress })\n break\n case 'log:write':\n dispatch({\n type: 'log:write',\n runId: event.runId,\n stepName: event.stepName,\n level: event.level,\n message: event.message,\n data: event.data,\n maxLogs,\n })\n break\n case 'connection_error':\n dispatch({ type: 'connection_error', error: event.error })\n break\n }\n })\n\n return unsubscribe\n }, [subscriber, runId, maxLogs])\n\n const clearLogs = useCallback(() => {\n dispatch({ type: 'clear_logs' })\n }, [])\n\n const reset = useCallback(() => {\n dispatch({ type: 'reset' })\n }, [])\n\n return {\n ...state,\n clearLogs,\n reset,\n }\n}\n"],"mappings":"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","names":[]}
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { R as RunStatus, L as LogEntry, P as Progress, I as InferInput, a as InferOutput, T as TypedClientRun } from './types-JIBwGTm6.js';
2
- export { D as DurablyEvent } from './types-JIBwGTm6.js';
1
+ import { R as RunStatus, L as LogEntry, P as Progress, I as InferInput, a as InferOutput, T as TypedClientRun } from './types-DMtqQ6Wp.js';
2
+ export { D as DurablyEvent } from './types-DMtqQ6Wp.js';
3
3
  import { JobDefinition, ClientRun } from '@coji/durably';
4
4
  export { ClientRun } from '@coji/durably';
5
5
 
@@ -18,7 +18,7 @@ interface UseJobClientOptions {
18
18
  */
19
19
  initialRunId?: string;
20
20
  /**
21
- * Automatically resume tracking a running/pending job on mount
21
+ * Automatically resume tracking a leased/pending job on mount
22
22
  * @default true
23
23
  */
24
24
  autoResume?: boolean;
@@ -68,7 +68,7 @@ interface UseJobClientResult<TInput, TOutput> {
68
68
  /**
69
69
  * Whether a run is currently running
70
70
  */
71
- isRunning: boolean;
71
+ isLeased: boolean;
72
72
  /**
73
73
  * Whether a run is pending
74
74
  */
@@ -182,7 +182,7 @@ interface UseJobRunClientResult<TOutput = unknown> {
182
182
  /**
183
183
  * Whether a run is currently running
184
184
  */
185
- isRunning: boolean;
185
+ isLeased: boolean;
186
186
  /**
187
187
  * Whether a run is pending
188
188
  */
@@ -254,7 +254,7 @@ interface JobHooks<TInput, TOutput> {
254
254
  *
255
255
  * // In your component - fully type-safe
256
256
  * function CsvImporter() {
257
- * const { trigger, output, progress, isRunning } = importCsv.useJob()
257
+ * const { trigger, output, progress, isLeased } = importCsv.useJob()
258
258
  *
259
259
  * return (
260
260
  * <button onClick={() => trigger({ rows: [...] })}>
@@ -282,11 +282,11 @@ interface UseRunActionsClientOptions {
282
282
  }
283
283
  interface UseRunActionsClientResult {
284
284
  /**
285
- * Retry a failed or cancelled run
285
+ * Create a fresh run from a completed, failed, or cancelled run
286
286
  */
287
- retry: (runId: string) => Promise<void>;
287
+ retrigger: (runId: string) => Promise<string>;
288
288
  /**
289
- * Cancel a pending or running run
289
+ * Cancel a pending or leased run
290
290
  */
291
291
  cancel: (runId: string) => Promise<void>;
292
292
  /**
@@ -311,23 +311,23 @@ interface UseRunActionsClientResult {
311
311
  error: string | null;
312
312
  }
313
313
  /**
314
- * Hook for run actions (retry, cancel) via server API.
314
+ * Hook for run actions via server API.
315
315
  *
316
316
  * @example
317
317
  * ```tsx
318
318
  * function RunActions({ runId, status }: { runId: string; status: string }) {
319
- * const { retry, cancel, isLoading, error } = useRunActions({
319
+ * const { retrigger, cancel, isLoading, error } = useRunActions({
320
320
  * api: '/api/durably',
321
321
  * })
322
322
  *
323
323
  * return (
324
324
  * <div>
325
325
  * {status === 'failed' && (
326
- * <button onClick={() => retry(runId)} disabled={isLoading}>
327
- * Retry
326
+ * <button onClick={() => retrigger(runId)} disabled={isLoading}>
327
+ * Run Again
328
328
  * </button>
329
329
  * )}
330
- * {(status === 'pending' || status === 'running') && (
330
+ * {(status === 'pending' || status === 'leased') && (
331
331
  * <button onClick={() => cancel(runId)} disabled={isLoading}>
332
332
  * Cancel
333
333
  * </button>
@@ -472,7 +472,7 @@ type DurablyClient<T> = {
472
472
  */
473
473
  useRuns: <TInput extends Record<string, unknown> = Record<string, unknown>, TOutput extends Record<string, unknown> | undefined = Record<string, unknown> | undefined>(options?: Omit<UseRunsClientOptions, 'api'>) => UseRunsClientResult<TInput, TOutput>;
474
474
  /**
475
- * Run actions: retry, cancel, delete, getRun, getSteps (cross-job).
475
+ * Run actions: retrigger, cancel, delete, getRun, getSteps (cross-job).
476
476
  * The `api` option is pre-configured.
477
477
  */
478
478
  useRunActions: () => UseRunActionsClientResult;
@@ -504,14 +504,14 @@ type DurablyClient<T> = {
504
504
  *
505
505
  * // In your component — fully type-safe with autocomplete
506
506
  * function CsvImporter() {
507
- * const { trigger, output, isRunning } = durably.importCsv.useJob()
507
+ * const { trigger, output, isLeased } = durably.importCsv.useJob()
508
508
  * return <button onClick={() => trigger({ rows: [...] })}>Import</button>
509
509
  * }
510
510
  *
511
511
  * // Cross-job hooks
512
512
  * function Dashboard() {
513
513
  * const { runs, nextPage } = durably.useRuns({ pageSize: 10 })
514
- * const { retry, cancel } = durably.useRunActions()
514
+ * const { retrigger, cancel } = durably.useRunActions()
515
515
  * }
516
516
  * ```
517
517
  */
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  isJobDefinition,
3
3
  useSubscription
4
- } from "./chunk-XRJEZWAV.js";
4
+ } from "./chunk-33VIIDHK.js";
5
5
 
6
6
  // src/client/use-job.ts
7
7
  import { useCallback, useEffect, useRef, useState } from "react";
@@ -20,8 +20,8 @@ function createSSEEventSubscriber(apiBaseUrl) {
20
20
  const data = JSON.parse(messageEvent.data);
21
21
  if (data.runId !== runId) return;
22
22
  switch (data.type) {
23
- case "run:start":
24
- onEvent({ type: "run:start" });
23
+ case "run:leased":
24
+ onEvent({ type: "run:leased" });
25
25
  break;
26
26
  case "run:complete":
27
27
  onEvent({
@@ -35,9 +35,6 @@ function createSSEEventSubscriber(apiBaseUrl) {
35
35
  case "run:cancel":
36
36
  onEvent({ type: "run:cancel" });
37
37
  break;
38
- case "run:retry":
39
- onEvent({ type: "run:retry" });
40
- break;
41
38
  case "run:progress":
42
39
  onEvent({ type: "run:progress", progress: data.progress });
43
40
  break;
@@ -56,8 +53,9 @@ function createSSEEventSubscriber(apiBaseUrl) {
56
53
  }
57
54
  };
58
55
  eventSource.onerror = () => {
59
- onEvent({ type: "connection_error", error: "Connection failed" });
60
- eventSource.close();
56
+ if (eventSource.readyState === EventSource.CLOSED) {
57
+ onEvent({ type: "connection_error", error: "Connection failed" });
58
+ }
61
59
  };
62
60
  return () => {
63
61
  eventSource.close();
@@ -99,9 +97,9 @@ function useJob(options) {
99
97
  const abortController = new AbortController();
100
98
  const findActiveRun = async () => {
101
99
  const signal = abortController.signal;
102
- const [runningRes, pendingRes] = await Promise.all([
100
+ const [leasedRes, pendingRes] = await Promise.all([
103
101
  fetch(
104
- `${api}/runs?${new URLSearchParams({ jobName, status: "running", limit: "1" })}`,
102
+ `${api}/runs?${new URLSearchParams({ jobName, status: "leased", limit: "1" })}`,
105
103
  { signal }
106
104
  ),
107
105
  fetch(
@@ -110,8 +108,8 @@ function useJob(options) {
110
108
  )
111
109
  ]);
112
110
  if (hasUserTriggered.current) return;
113
- if (runningRes.ok) {
114
- const runs = await runningRes.json();
111
+ if (leasedRes.ok) {
112
+ const runs = await leasedRes.json();
115
113
  if (runs.length > 0) {
116
114
  setCurrentRunId(runs[0].id);
117
115
  return;
@@ -140,7 +138,7 @@ function useJob(options) {
140
138
  eventSource.onmessage = (event) => {
141
139
  try {
142
140
  const data = JSON.parse(event.data);
143
- if ((data.type === "run:trigger" || data.type === "run:start") && data.runId) {
141
+ if ((data.type === "run:trigger" || data.type === "run:leased") && data.runId) {
144
142
  setCurrentRunId(data.runId);
145
143
  }
146
144
  } catch {
@@ -229,7 +227,7 @@ function useJob(options) {
229
227
  error: subscription.error,
230
228
  logs: subscription.logs,
231
229
  progress: subscription.progress,
232
- isRunning: effectiveStatus === "running",
230
+ isLeased: effectiveStatus === "leased",
233
231
  isPending: effectiveStatus === "pending",
234
232
  isCompleted: effectiveStatus === "completed",
235
233
  isFailed: effectiveStatus === "failed",
@@ -258,14 +256,14 @@ function useJobRun(options) {
258
256
  const isCompleted = effectiveStatus === "completed";
259
257
  const isFailed = effectiveStatus === "failed";
260
258
  const isPending = effectiveStatus === "pending";
261
- const isRunning = effectiveStatus === "running";
259
+ const isLeased = effectiveStatus === "leased";
262
260
  const isCancelled = effectiveStatus === "cancelled";
263
261
  const prevStatusRef = useRef2(null);
264
262
  useEffect2(() => {
265
263
  const prevStatus = prevStatusRef.current;
266
264
  prevStatusRef.current = effectiveStatus;
267
265
  if (prevStatus !== effectiveStatus) {
268
- if (prevStatus === null && (isPending || isRunning) && onStart) {
266
+ if (prevStatus === null && (isPending || isLeased) && onStart) {
269
267
  onStart();
270
268
  }
271
269
  if (isCompleted && onComplete) {
@@ -278,7 +276,7 @@ function useJobRun(options) {
278
276
  }, [
279
277
  effectiveStatus,
280
278
  isPending,
281
- isRunning,
279
+ isLeased,
282
280
  isCompleted,
283
281
  isFailed,
284
282
  onStart,
@@ -291,7 +289,7 @@ function useJobRun(options) {
291
289
  error: subscription.error,
292
290
  logs: subscription.logs,
293
291
  progress: subscription.progress,
294
- isRunning,
292
+ isLeased,
295
293
  isPending,
296
294
  isCompleted,
297
295
  isFailed,
@@ -321,15 +319,14 @@ function useRunActions(options) {
321
319
  const { api } = options;
322
320
  const [isLoading, setIsLoading] = useState2(false);
323
321
  const [error, setError] = useState2(null);
324
- const retry = useCallback2(
325
- async (runId) => {
322
+ const executeAction = useCallback2(
323
+ async (url, actionName, init) => {
326
324
  setIsLoading(true);
327
325
  setError(null);
328
326
  try {
329
- const url = `${api}/retry?runId=${encodeURIComponent(runId)}`;
330
- const response = await fetch(url, { method: "POST" });
327
+ const response = await fetch(url, init);
331
328
  if (!response.ok) {
332
- let errorMessage = `Failed to retry: ${response.statusText}`;
329
+ let errorMessage = `Failed to ${actionName}: ${response.statusText}`;
333
330
  try {
334
331
  const data = await response.json();
335
332
  if (data.error) {
@@ -339,6 +336,7 @@ function useRunActions(options) {
339
336
  }
340
337
  throw new Error(errorMessage);
341
338
  }
339
+ return await response.json();
342
340
  } catch (err) {
343
341
  const message = err instanceof Error ? err.message : "Unknown error";
344
342
  setError(message);
@@ -347,71 +345,50 @@ function useRunActions(options) {
347
345
  setIsLoading(false);
348
346
  }
349
347
  },
350
- [api]
348
+ []
351
349
  );
352
- const cancel = useCallback2(
350
+ const retrigger = useCallback2(
353
351
  async (runId) => {
354
- setIsLoading(true);
355
- setError(null);
356
- try {
357
- const url = `${api}/cancel?runId=${encodeURIComponent(runId)}`;
358
- const response = await fetch(url, { method: "POST" });
359
- if (!response.ok) {
360
- let errorMessage = `Failed to cancel: ${response.statusText}`;
361
- try {
362
- const data = await response.json();
363
- if (data.error) {
364
- errorMessage = data.error;
365
- }
366
- } catch {
367
- }
368
- throw new Error(errorMessage);
369
- }
370
- } catch (err) {
371
- const message = err instanceof Error ? err.message : "Unknown error";
352
+ const enc = encodeURIComponent(runId);
353
+ const data = await executeAction(
354
+ `${api}/retrigger?runId=${enc}`,
355
+ "retrigger",
356
+ { method: "POST" }
357
+ );
358
+ if (!data.runId) {
359
+ const message = "Failed to retrigger: missing runId in response";
372
360
  setError(message);
373
- throw err;
374
- } finally {
375
- setIsLoading(false);
361
+ throw new Error(message);
376
362
  }
363
+ return data.runId;
377
364
  },
378
- [api]
365
+ [api, executeAction]
366
+ );
367
+ const cancel = useCallback2(
368
+ async (runId) => {
369
+ const enc = encodeURIComponent(runId);
370
+ await executeAction(`${api}/cancel?runId=${enc}`, "cancel", {
371
+ method: "POST"
372
+ });
373
+ },
374
+ [api, executeAction]
379
375
  );
380
376
  const deleteRun = useCallback2(
381
377
  async (runId) => {
382
- setIsLoading(true);
383
- setError(null);
384
- try {
385
- const url = `${api}/run?runId=${encodeURIComponent(runId)}`;
386
- const response = await fetch(url, { method: "DELETE" });
387
- if (!response.ok) {
388
- let errorMessage = `Failed to delete: ${response.statusText}`;
389
- try {
390
- const data = await response.json();
391
- if (data.error) {
392
- errorMessage = data.error;
393
- }
394
- } catch {
395
- }
396
- throw new Error(errorMessage);
397
- }
398
- } catch (err) {
399
- const message = err instanceof Error ? err.message : "Unknown error";
400
- setError(message);
401
- throw err;
402
- } finally {
403
- setIsLoading(false);
404
- }
378
+ const enc = encodeURIComponent(runId);
379
+ await executeAction(`${api}/run?runId=${enc}`, "delete", {
380
+ method: "DELETE"
381
+ });
405
382
  },
406
- [api]
383
+ [api, executeAction]
407
384
  );
408
385
  const getRun = useCallback2(
409
386
  async (runId) => {
410
387
  setIsLoading(true);
411
388
  setError(null);
412
389
  try {
413
- const url = `${api}/run?runId=${encodeURIComponent(runId)}`;
414
- const response = await fetch(url);
390
+ const enc = encodeURIComponent(runId);
391
+ const response = await fetch(`${api}/run?runId=${enc}`);
415
392
  if (response.status === 404) {
416
393
  return null;
417
394
  }
@@ -439,35 +416,16 @@ function useRunActions(options) {
439
416
  );
440
417
  const getSteps = useCallback2(
441
418
  async (runId) => {
442
- setIsLoading(true);
443
- setError(null);
444
- try {
445
- const url = `${api}/steps?runId=${encodeURIComponent(runId)}`;
446
- const response = await fetch(url);
447
- if (!response.ok) {
448
- let errorMessage = `Failed to get steps: ${response.statusText}`;
449
- try {
450
- const data = await response.json();
451
- if (data.error) {
452
- errorMessage = data.error;
453
- }
454
- } catch {
455
- }
456
- throw new Error(errorMessage);
457
- }
458
- return await response.json();
459
- } catch (err) {
460
- const message = err instanceof Error ? err.message : "Unknown error";
461
- setError(message);
462
- throw err;
463
- } finally {
464
- setIsLoading(false);
465
- }
419
+ const enc = encodeURIComponent(runId);
420
+ return executeAction(
421
+ `${api}/steps?runId=${enc}`,
422
+ "get steps"
423
+ );
466
424
  },
467
- [api]
425
+ [api, executeAction]
468
426
  );
469
427
  return {
470
- retry,
428
+ retrigger,
471
429
  cancel,
472
430
  deleteRun,
473
431
  getRun,
@@ -555,7 +513,7 @@ function useRuns(jobDefinitionOrOptions, optionsArg) {
555
513
  eventSource.onmessage = (event) => {
556
514
  try {
557
515
  const data = JSON.parse(event.data);
558
- if (data.type === "run:trigger" || data.type === "run:start" || data.type === "run:complete" || data.type === "run:fail" || data.type === "run:cancel" || data.type === "run:delete" || data.type === "run:retry") {
516
+ if (data.type === "run:trigger" || data.type === "run:leased" || data.type === "run:complete" || data.type === "run:fail" || data.type === "run:cancel" || data.type === "run:delete") {
559
517
  refresh();
560
518
  }
561
519
  if (data.type === "run:progress") {