@coji/durably-react 0.7.0 → 0.8.1

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
@@ -74,12 +74,20 @@ For full-stack apps, use hooks from `@coji/durably-react/client`:
74
74
  import { useJob } from '@coji/durably-react/client'
75
75
 
76
76
  function MyComponent() {
77
- const { trigger, status, output, isRunning } = useJob<
78
- { id: string },
79
- { result: number }
80
- >({
77
+ const {
78
+ trigger,
79
+ status,
80
+ output,
81
+ isRunning,
82
+ isPending,
83
+ isCompleted,
84
+ isFailed,
85
+ isCancelled,
86
+ } = useJob<{ id: string }, { result: number }>({
81
87
  api: '/api/durably',
82
88
  jobName: 'my-job',
89
+ autoResume: true, // Auto-resume running/pending jobs on mount (default)
90
+ followLatest: true, // Switch to tracking new runs via SSE (default)
83
91
  })
84
92
 
85
93
  return (
package/dist/client.d.ts CHANGED
@@ -16,6 +16,16 @@ interface UseJobClientOptions {
16
16
  * When provided, the hook will immediately start subscribing to this run
17
17
  */
18
18
  initialRunId?: string;
19
+ /**
20
+ * Automatically resume tracking a running/pending job on mount
21
+ * @default true
22
+ */
23
+ autoResume?: boolean;
24
+ /**
25
+ * Automatically switch to tracking the latest triggered job
26
+ * @default true
27
+ */
28
+ followLatest?: boolean;
19
29
  }
20
30
  interface UseJobClientResult<TInput, TOutput> {
21
31
  /**
@@ -70,6 +80,10 @@ interface UseJobClientResult<TInput, TOutput> {
70
80
  * Whether the run failed
71
81
  */
72
82
  isFailed: boolean;
83
+ /**
84
+ * Whether the run was cancelled
85
+ */
86
+ isCancelled: boolean;
73
87
  /**
74
88
  * Current run ID
75
89
  */
package/dist/client.js CHANGED
@@ -4,7 +4,7 @@ import {
4
4
  } from "./chunk-42AVE35N.js";
5
5
 
6
6
  // src/client/use-job.ts
7
- import { useCallback, useEffect, useState } from "react";
7
+ import { useCallback, useEffect, useRef, useState } from "react";
8
8
 
9
9
  // src/client/use-sse-subscription.ts
10
10
  import { useMemo } from "react";
@@ -77,14 +77,87 @@ function useSSESubscription(api, runId, options) {
77
77
 
78
78
  // src/client/use-job.ts
79
79
  function useJob(options) {
80
- const { api, jobName, initialRunId } = options;
80
+ const {
81
+ api,
82
+ jobName,
83
+ initialRunId,
84
+ autoResume = true,
85
+ followLatest = true
86
+ } = options;
81
87
  const [currentRunId, setCurrentRunId] = useState(
82
88
  initialRunId ?? null
83
89
  );
84
90
  const [isPending, setIsPending] = useState(false);
91
+ const hasUserTriggered = useRef(false);
85
92
  const subscription = useSSESubscription(api, currentRunId);
93
+ useEffect(() => {
94
+ if (!autoResume) return;
95
+ if (initialRunId) return;
96
+ const abortController = new AbortController();
97
+ const findActiveRun = async () => {
98
+ const runningParams = new URLSearchParams({
99
+ jobName,
100
+ status: "running",
101
+ limit: "1"
102
+ });
103
+ const runningRes = await fetch(`${api}/runs?${runningParams}`, {
104
+ signal: abortController.signal
105
+ });
106
+ if (runningRes.ok) {
107
+ const runs = await runningRes.json();
108
+ if (runs.length > 0) {
109
+ if (hasUserTriggered.current) return;
110
+ setCurrentRunId(runs[0].id);
111
+ return;
112
+ }
113
+ }
114
+ const pendingParams = new URLSearchParams({
115
+ jobName,
116
+ status: "pending",
117
+ limit: "1"
118
+ });
119
+ const pendingRes = await fetch(`${api}/runs?${pendingParams}`, {
120
+ signal: abortController.signal
121
+ });
122
+ if (pendingRes.ok) {
123
+ const runs = await pendingRes.json();
124
+ if (runs.length > 0) {
125
+ if (hasUserTriggered.current) return;
126
+ setCurrentRunId(runs[0].id);
127
+ }
128
+ }
129
+ };
130
+ findActiveRun().catch((err) => {
131
+ if (err.name !== "AbortError") {
132
+ console.error("autoResume error:", err);
133
+ }
134
+ });
135
+ return () => {
136
+ abortController.abort();
137
+ };
138
+ }, [api, jobName, autoResume, initialRunId]);
139
+ useEffect(() => {
140
+ if (!followLatest) return;
141
+ const params = new URLSearchParams({ jobName });
142
+ const eventSource = new EventSource(`${api}/runs/subscribe?${params}`);
143
+ eventSource.onmessage = (event) => {
144
+ try {
145
+ const data = JSON.parse(event.data);
146
+ if ((data.type === "run:trigger" || data.type === "run:start") && data.runId) {
147
+ setCurrentRunId(data.runId);
148
+ }
149
+ } catch {
150
+ }
151
+ };
152
+ eventSource.onerror = () => {
153
+ };
154
+ return () => {
155
+ eventSource.close();
156
+ };
157
+ }, [api, jobName, followLatest]);
86
158
  const trigger = useCallback(
87
159
  async (input) => {
160
+ hasUserTriggered.current = true;
88
161
  subscription.reset();
89
162
  setIsPending(true);
90
163
  const response = await fetch(`${api}/trigger`, {
@@ -148,6 +221,7 @@ function useJob(options) {
148
221
  isPending: effectiveStatus === "pending",
149
222
  isCompleted: effectiveStatus === "completed",
150
223
  isFailed: effectiveStatus === "failed",
224
+ isCancelled: effectiveStatus === "cancelled",
151
225
  currentRunId,
152
226
  reset
153
227
  };
@@ -164,7 +238,7 @@ function useJobLogs(options) {
164
238
  }
165
239
 
166
240
  // src/client/use-job-run.ts
167
- import { useEffect as useEffect2, useRef } from "react";
241
+ import { useEffect as useEffect2, useRef as useRef2 } from "react";
168
242
  function useJobRun(options) {
169
243
  const { api, runId, onStart, onComplete, onFail } = options;
170
244
  const subscription = useSSESubscription(api, runId);
@@ -174,7 +248,7 @@ function useJobRun(options) {
174
248
  const isPending = effectiveStatus === "pending";
175
249
  const isRunning = effectiveStatus === "running";
176
250
  const isCancelled = effectiveStatus === "cancelled";
177
- const prevStatusRef = useRef(null);
251
+ const prevStatusRef = useRef2(null);
178
252
  useEffect2(() => {
179
253
  const prevStatus = prevStatusRef.current;
180
254
  prevStatusRef.current = effectiveStatus;
@@ -250,7 +324,7 @@ function createJobHooks(options) {
250
324
  }
251
325
 
252
326
  // src/client/use-runs.ts
253
- import { useCallback as useCallback2, useEffect as useEffect3, useRef as useRef2, useState as useState2 } from "react";
327
+ import { useCallback as useCallback2, useEffect as useEffect3, useRef as useRef3, useState as useState2 } from "react";
254
328
  function useRuns(jobDefinitionOrOptions, optionsArg) {
255
329
  const isJob = isJobDefinition(jobDefinitionOrOptions);
256
330
  const jobName = isJob ? jobDefinitionOrOptions.name : jobDefinitionOrOptions.jobName;
@@ -261,8 +335,8 @@ function useRuns(jobDefinitionOrOptions, optionsArg) {
261
335
  const [hasMore, setHasMore] = useState2(false);
262
336
  const [isLoading, setIsLoading] = useState2(false);
263
337
  const [error, setError] = useState2(null);
264
- const isMountedRef = useRef2(true);
265
- const eventSourceRef = useRef2(null);
338
+ const isMountedRef = useRef3(true);
339
+ const eventSourceRef = useRef3(null);
266
340
  const refresh = useCallback2(async () => {
267
341
  setIsLoading(true);
268
342
  setError(null);
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/client/use-job.ts","../src/client/use-sse-subscription.ts","../src/shared/sse-event-subscriber.ts","../src/client/use-job-logs.ts","../src/client/use-job-run.ts","../src/client/create-durably-client.ts","../src/client/create-job-hooks.ts","../src/client/use-runs.ts","../src/client/use-run-actions.ts"],"sourcesContent":["import { useCallback, useEffect, useState } from 'react'\nimport type { LogEntry, Progress, RunStatus } from '../types'\nimport { useSSESubscription } from './use-sse-subscription'\n\nexport interface UseJobClientOptions {\n /**\n * API endpoint URL (e.g., '/api/durably')\n */\n api: string\n /**\n * Job name to trigger\n */\n jobName: string\n /**\n * Initial Run ID to subscribe to (for reconnection scenarios)\n * When provided, the hook will immediately start subscribing to this run\n */\n initialRunId?: string\n}\n\nexport interface UseJobClientResult<TInput, TOutput> {\n /**\n * Whether the hook is ready (always true for client mode)\n */\n /**\n * Trigger the job with the given input\n */\n trigger: (input: TInput) => Promise<{ runId: string }>\n /**\n * Trigger and wait for completion\n */\n triggerAndWait: (input: TInput) => Promise<{ runId: string; output: TOutput }>\n /**\n * Current run status\n */\n status: RunStatus | null\n /**\n * Output from completed run\n */\n output: TOutput | null\n /**\n * Error message from failed run\n */\n error: string | null\n /**\n * Logs collected during execution\n */\n logs: LogEntry[]\n /**\n * Current progress\n */\n progress: Progress | null\n /**\n * Whether a run is currently running\n */\n isRunning: boolean\n /**\n * Whether a run is pending\n */\n isPending: boolean\n /**\n * Whether the run completed successfully\n */\n isCompleted: boolean\n /**\n * Whether the run failed\n */\n isFailed: boolean\n /**\n * Current run ID\n */\n currentRunId: string | null\n /**\n * Reset all state\n */\n reset: () => void\n}\n\n/**\n * Hook for triggering and subscribing to jobs via server API.\n * Uses fetch for triggering and EventSource for SSE subscription.\n */\nexport function useJob<\n TInput extends Record<string, unknown> = Record<string, unknown>,\n TOutput extends Record<string, unknown> = Record<string, unknown>,\n>(options: UseJobClientOptions): UseJobClientResult<TInput, TOutput> {\n const { api, jobName, initialRunId } = options\n\n const [currentRunId, setCurrentRunId] = useState<string | null>(\n initialRunId ?? null,\n )\n const [isPending, setIsPending] = useState(false)\n\n const subscription = useSSESubscription<TOutput>(api, currentRunId)\n\n const trigger = useCallback(\n async (input: TInput): Promise<{ runId: string }> => {\n // Reset state\n subscription.reset()\n setIsPending(true)\n\n const response = await fetch(`${api}/trigger`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({ jobName, input }),\n })\n\n if (!response.ok) {\n setIsPending(false)\n const errorText = await response.text()\n throw new Error(errorText || `HTTP ${response.status}`)\n }\n\n const { runId } = (await response.json()) as { runId: string }\n setCurrentRunId(runId)\n\n return { runId }\n },\n [api, jobName, subscription.reset],\n )\n\n const triggerAndWait = useCallback(\n async (input: TInput): Promise<{ runId: string; output: TOutput }> => {\n const { runId } = await trigger(input)\n\n return new Promise((resolve, reject) => {\n const checkInterval = setInterval(() => {\n if (subscription.status === 'completed' && subscription.output) {\n clearInterval(checkInterval)\n resolve({ runId, output: subscription.output })\n } else if (subscription.status === 'failed') {\n clearInterval(checkInterval)\n reject(new Error(subscription.error ?? 'Job failed'))\n } else if (subscription.status === 'cancelled') {\n clearInterval(checkInterval)\n reject(new Error('Job cancelled'))\n }\n }, 50)\n })\n },\n [trigger, subscription.status, subscription.output, subscription.error],\n )\n\n const reset = useCallback(() => {\n subscription.reset()\n setCurrentRunId(null)\n setIsPending(false)\n }, [subscription.reset])\n\n // Compute effective status (pending overrides null when we've triggered but SSE hasn't started)\n const effectiveStatus = subscription.status ?? (isPending ? 'pending' : null)\n\n // Clear pending when we get a real status\n useEffect(() => {\n if (subscription.status && isPending) {\n setIsPending(false)\n }\n }, [subscription.status, isPending])\n\n return {\n trigger,\n triggerAndWait,\n status: effectiveStatus,\n output: subscription.output,\n error: subscription.error,\n logs: subscription.logs,\n progress: subscription.progress,\n isRunning: effectiveStatus === 'running',\n isPending: effectiveStatus === 'pending',\n isCompleted: effectiveStatus === 'completed',\n isFailed: effectiveStatus === 'failed',\n currentRunId,\n reset,\n }\n}\n","import { useMemo } from 'react'\nimport { createSSEEventSubscriber } from '../shared/sse-event-subscriber'\nimport {\n useSubscription,\n type UseSubscriptionOptions,\n type UseSubscriptionResult,\n} from '../shared/use-subscription'\nimport type { SubscriptionState } from '../types'\n\n/** @deprecated Use SubscriptionState from '../types' instead */\nexport type SSESubscriptionState<TOutput = unknown> = SubscriptionState<TOutput>\n\n/** @deprecated Use UseSubscriptionOptions from '../shared/use-subscription' instead */\nexport type UseSSESubscriptionOptions = UseSubscriptionOptions\n\n/** @deprecated Use UseSubscriptionResult from '../shared/use-subscription' instead */\nexport type UseSSESubscriptionResult<TOutput = unknown> =\n UseSubscriptionResult<TOutput>\n\n/**\n * Internal hook for subscribing to run events via SSE.\n * Used by client-mode hooks (useJob, useJobRun, useJobLogs).\n *\n * @deprecated Consider using useSubscription with createSSEEventSubscriber directly.\n */\nexport function useSSESubscription<TOutput = unknown>(\n api: string | null,\n runId: string | null,\n options?: UseSSESubscriptionOptions,\n): UseSSESubscriptionResult<TOutput> {\n const subscriber = useMemo(\n () => (api ? createSSEEventSubscriber(api) : null),\n [api],\n )\n\n return useSubscription<TOutput>(subscriber, runId, options)\n}\n","import type { DurablyEvent } from '../types'\nimport type { EventSubscriber, SubscriptionEvent } from './event-subscriber'\n\n/**\n * EventSubscriber implementation using Server-Sent Events (SSE).\n * Used in client environments that communicate with a Durably server via HTTP.\n */\nexport function createSSEEventSubscriber(apiBaseUrl: string): EventSubscriber {\n return {\n subscribe<TOutput = unknown>(\n runId: string,\n onEvent: (event: SubscriptionEvent<TOutput>) => void,\n ): () => void {\n const url = `${apiBaseUrl}/subscribe?runId=${encodeURIComponent(runId)}`\n const eventSource = new EventSource(url)\n\n eventSource.onmessage = (messageEvent) => {\n try {\n const data = JSON.parse(messageEvent.data) as DurablyEvent\n if (data.runId !== runId) return\n\n switch (data.type) {\n case 'run:start':\n onEvent({ type: 'run:start' })\n break\n case 'run:complete':\n onEvent({\n type: 'run:complete',\n output: data.output as TOutput,\n })\n break\n case 'run:fail':\n onEvent({ type: 'run:fail', error: data.error })\n break\n case 'run:cancel':\n onEvent({ type: 'run:cancel' })\n break\n case 'run:retry':\n onEvent({ type: 'run:retry' })\n break\n case 'run:progress':\n onEvent({ type: 'run:progress', progress: data.progress })\n break\n case 'log:write':\n onEvent({\n type: 'log:write',\n runId: data.runId,\n stepName: null,\n level: data.level,\n message: data.message,\n data: data.data,\n })\n break\n }\n } catch {\n // Ignore parse errors\n }\n }\n\n eventSource.onerror = () => {\n onEvent({ type: 'connection_error', error: 'Connection failed' })\n eventSource.close()\n }\n\n return () => {\n eventSource.close()\n }\n },\n }\n}\n","import type { LogEntry } from '../types'\nimport { useSSESubscription } from './use-sse-subscription'\n\nexport interface UseJobLogsClientOptions {\n /**\n * API endpoint URL (e.g., '/api/durably')\n */\n api: string\n /**\n * The run ID to subscribe to logs for\n */\n runId: string | null\n /**\n * Maximum number of logs to keep (default: unlimited)\n */\n maxLogs?: number\n}\n\nexport interface UseJobLogsClientResult {\n /**\n * Whether the hook is ready (always true for client mode)\n */\n /**\n * Logs collected during execution\n */\n logs: LogEntry[]\n /**\n * Clear all logs\n */\n clearLogs: () => void\n}\n\n/**\n * Hook for subscribing to logs from a run via server API.\n * Uses EventSource for SSE subscription.\n */\nexport function useJobLogs(\n options: UseJobLogsClientOptions,\n): UseJobLogsClientResult {\n const { api, runId, maxLogs } = options\n\n const subscription = useSSESubscription(api, runId, { maxLogs })\n\n return {\n logs: subscription.logs,\n clearLogs: subscription.clearLogs,\n }\n}\n","import { useEffect, useRef } from 'react'\nimport type { LogEntry, Progress, RunStatus } from '../types'\nimport { useSSESubscription } from './use-sse-subscription'\n\nexport interface UseJobRunClientOptions {\n /**\n * API endpoint URL (e.g., '/api/durably')\n */\n api: string\n /**\n * The run ID to subscribe to\n */\n runId: string | null\n /**\n * Callback when run starts (transitions to pending/running)\n */\n onStart?: () => void\n /**\n * Callback when run completes successfully\n */\n onComplete?: () => void\n /**\n * Callback when run fails\n */\n onFail?: () => void\n}\n\nexport interface UseJobRunClientResult<TOutput = unknown> {\n /**\n * Whether the hook is ready (always true for client mode)\n */\n /**\n * Current run status\n */\n status: RunStatus | null\n /**\n * Output from completed run\n */\n output: TOutput | null\n /**\n * Error message from failed run\n */\n error: string | null\n /**\n * Logs collected during execution\n */\n logs: LogEntry[]\n /**\n * Current progress\n */\n progress: Progress | null\n /**\n * Whether a run is currently running\n */\n isRunning: boolean\n /**\n * Whether a run is pending\n */\n isPending: boolean\n /**\n * Whether the run completed successfully\n */\n isCompleted: boolean\n /**\n * Whether the run failed\n */\n isFailed: boolean\n /**\n * Whether the run was cancelled\n */\n isCancelled: boolean\n}\n\n/**\n * Hook for subscribing to an existing run via server API.\n * Uses EventSource for SSE subscription.\n */\nexport function useJobRun<TOutput = unknown>(\n options: UseJobRunClientOptions,\n): UseJobRunClientResult<TOutput> {\n const { api, runId, onStart, onComplete, onFail } = options\n\n const subscription = useSSESubscription<TOutput>(api, runId)\n\n // If we have a runId but no status yet, treat as pending\n const effectiveStatus = subscription.status ?? (runId ? 'pending' : null)\n\n const isCompleted = effectiveStatus === 'completed'\n const isFailed = effectiveStatus === 'failed'\n const isPending = effectiveStatus === 'pending'\n const isRunning = effectiveStatus === 'running'\n const isCancelled = effectiveStatus === 'cancelled'\n\n // Track previous status to detect transitions (use effectiveStatus, not subscription.status)\n const prevStatusRef = useRef<RunStatus | null>(null)\n\n useEffect(() => {\n const prevStatus = prevStatusRef.current\n prevStatusRef.current = effectiveStatus\n\n // Only fire callbacks on status transitions\n if (prevStatus !== effectiveStatus) {\n // Fire onStart when transitioning from null to pending/running\n if (prevStatus === null && (isPending || isRunning) && onStart) {\n onStart()\n }\n if (isCompleted && onComplete) {\n onComplete()\n }\n if (isFailed && onFail) {\n onFail()\n }\n }\n }, [\n effectiveStatus,\n isPending,\n isRunning,\n isCompleted,\n isFailed,\n onStart,\n onComplete,\n onFail,\n ])\n\n return {\n status: effectiveStatus,\n output: subscription.output,\n error: subscription.error,\n logs: subscription.logs,\n progress: subscription.progress,\n isRunning,\n isPending,\n isCompleted,\n isFailed,\n isCancelled,\n }\n}\n","import type { InferInput, InferOutput } from '../types'\nimport { useJob, type UseJobClientResult } from './use-job'\nimport { useJobLogs, type UseJobLogsClientResult } from './use-job-logs'\nimport { useJobRun, type UseJobRunClientResult } from './use-job-run'\n\n/**\n * Type-safe hooks for a specific job\n */\nexport interface JobClient<TInput, TOutput> {\n /**\n * Hook for triggering and monitoring the job\n */\n useJob: () => UseJobClientResult<TInput, TOutput>\n\n /**\n * Hook for subscribing to an existing run by ID\n */\n useRun: (runId: string | null) => UseJobRunClientResult<TOutput>\n\n /**\n * Hook for subscribing to logs from a run\n */\n useLogs: (\n runId: string | null,\n options?: { maxLogs?: number },\n ) => UseJobLogsClientResult\n}\n\n/**\n * Options for createDurablyClient\n */\nexport interface CreateDurablyClientOptions {\n /**\n * API endpoint URL (e.g., '/api/durably')\n */\n api: string\n}\n\n/**\n * A type-safe client with hooks for each registered job\n */\nexport type DurablyClient<TJobs extends Record<string, unknown>> = {\n [K in keyof TJobs]: JobClient<InferInput<TJobs[K]>, InferOutput<TJobs[K]>>\n}\n\n/**\n * Create a type-safe Durably client with hooks for all registered jobs.\n *\n * @example\n * ```tsx\n * // Server: register jobs\n * // app/lib/durably.server.ts\n * export const jobs = durably.register({\n * importCsv: importCsvJob,\n * syncUsers: syncUsersJob,\n * })\n *\n * // Client: create typed client\n * // app/lib/durably.client.ts\n * import type { jobs } from '~/lib/durably.server'\n * import { createDurablyClient } from '@coji/durably-react/client'\n *\n * export const durably = createDurablyClient<typeof jobs>({\n * api: '/api/durably',\n * })\n *\n * // In your component - fully type-safe with autocomplete\n * function CsvImporter() {\n * const { trigger, output, isRunning } = durably.importCsv.useJob()\n *\n * return (\n * <button onClick={() => trigger({ rows: [...] })}>\n * Import\n * </button>\n * )\n * }\n * ```\n */\nexport function createDurablyClient<TJobs extends Record<string, unknown>>(\n options: CreateDurablyClientOptions,\n): DurablyClient<TJobs> {\n const { api } = options\n\n // Create a proxy that generates job clients on demand\n return new Proxy({} as DurablyClient<TJobs>, {\n get(_target, jobKey: string) {\n return {\n useJob: () => {\n return useJob({ api, jobName: jobKey })\n },\n\n useRun: (runId: string | null) => {\n return useJobRun({ api, runId })\n },\n\n useLogs: (runId: string | null, logsOptions?: { maxLogs?: number }) => {\n return useJobLogs({ api, runId, maxLogs: logsOptions?.maxLogs })\n },\n }\n },\n })\n}\n","import type { JobDefinition } from '@coji/durably'\nimport type { InferInput, InferOutput } from '../types'\nimport { useJob, type UseJobClientResult } from './use-job'\nimport { useJobLogs, type UseJobLogsClientResult } from './use-job-logs'\nimport { useJobRun, type UseJobRunClientResult } from './use-job-run'\n\n/**\n * Options for createJobHooks\n */\nexport interface CreateJobHooksOptions {\n /**\n * API endpoint URL (e.g., '/api/durably')\n */\n api: string\n /**\n * Job name (must match the server-side job name)\n */\n jobName: string\n}\n\n/**\n * Type-safe hooks for a specific job\n */\nexport interface JobHooks<TInput, TOutput> {\n /**\n * Hook for triggering and monitoring the job\n */\n useJob: () => UseJobClientResult<TInput, TOutput>\n\n /**\n * Hook for subscribing to an existing run by ID\n */\n useRun: (runId: string | null) => UseJobRunClientResult<TOutput>\n\n /**\n * Hook for subscribing to logs from a run\n */\n useLogs: (\n runId: string | null,\n options?: { maxLogs?: number },\n ) => UseJobLogsClientResult\n}\n\n/**\n * Create type-safe hooks for a specific job.\n *\n * @example\n * ```tsx\n * // Import job type from server (type-only import is safe)\n * import type { importCsvJob } from '~/lib/durably.server'\n * import { createJobHooks } from '@coji/durably-react/client'\n *\n * const importCsv = createJobHooks<typeof importCsvJob>({\n * api: '/api/durably',\n * jobName: 'import-csv',\n * })\n *\n * // In your component - fully type-safe\n * function CsvImporter() {\n * const { trigger, output, progress, isRunning } = importCsv.useJob()\n *\n * return (\n * <button onClick={() => trigger({ rows: [...] })}>\n * Import\n * </button>\n * )\n * }\n * ```\n */\nexport function createJobHooks<\n // biome-ignore lint/suspicious/noExplicitAny: TJob needs to accept any JobDefinition\n TJob extends JobDefinition<string, any, any>,\n>(\n options: CreateJobHooksOptions,\n): JobHooks<InferInput<TJob>, InferOutput<TJob>> {\n const { api, jobName } = options\n\n return {\n useJob: () => {\n return useJob<InferInput<TJob>, InferOutput<TJob>>({ api, jobName })\n },\n\n useRun: (runId: string | null) => {\n return useJobRun<InferOutput<TJob>>({ api, runId })\n },\n\n useLogs: (runId: string | null, logsOptions?: { maxLogs?: number }) => {\n return useJobLogs({ api, runId, maxLogs: logsOptions?.maxLogs })\n },\n }\n}\n","import type { JobDefinition } from '@coji/durably'\nimport { useCallback, useEffect, useRef, useState } from 'react'\nimport {\n type Progress,\n type RunStatus,\n type TypedClientRun,\n isJobDefinition,\n} from '../types'\n\n// Re-export types for convenience\nexport type { ClientRun, TypedClientRun } from '../types'\n\n/**\n * SSE notification event from /runs/subscribe\n */\ntype RunUpdateEvent =\n | {\n type:\n | 'run:trigger'\n | 'run:start'\n | 'run:complete'\n | 'run:fail'\n | 'run:cancel'\n | 'run:retry'\n runId: string\n jobName: string\n }\n | { type: 'run:progress'; runId: string; jobName: string; progress: Progress }\n | {\n type: 'step:start' | 'step:complete'\n runId: string\n jobName: string\n stepName: string\n stepIndex: number\n }\n | {\n type: 'step:fail'\n runId: string\n jobName: string\n stepName: string\n stepIndex: number\n error: string\n }\n | {\n type: 'log:write'\n runId: string\n stepName: string | null\n level: 'info' | 'warn' | 'error'\n message: string\n data: unknown\n }\n\nexport interface UseRunsClientOptions {\n /**\n * API endpoint URL (e.g., '/api/durably')\n */\n api: string\n /**\n * Filter by job name\n */\n jobName?: string\n /**\n * Filter by status\n */\n status?: RunStatus\n /**\n * Number of runs per page\n * @default 10\n */\n pageSize?: number\n}\n\nexport interface UseRunsClientResult<\n TInput extends Record<string, unknown> = Record<string, unknown>,\n TOutput extends Record<string, unknown> | undefined =\n | Record<string, unknown>\n | undefined,\n> {\n /**\n * List of runs for the current page\n */\n runs: TypedClientRun<TInput, TOutput>[]\n /**\n * Current page (0-indexed)\n */\n page: number\n /**\n * Whether there are more pages\n */\n hasMore: boolean\n /**\n * Whether data is being loaded\n */\n isLoading: boolean\n /**\n * Error message if fetch failed\n */\n error: string | null\n /**\n * Go to the next page\n */\n nextPage: () => void\n /**\n * Go to the previous page\n */\n prevPage: () => void\n /**\n * Go to a specific page\n */\n goToPage: (page: number) => void\n /**\n * Refresh the current page\n */\n refresh: () => Promise<void>\n}\n\n/**\n * Hook for listing runs via server API with pagination.\n * First page (page 0) automatically subscribes to SSE for real-time updates.\n * Other pages are static and require manual refresh.\n *\n * @example With generic type parameter (dashboard with multiple job types)\n * ```tsx\n * type DashboardRun = TypedClientRun<ImportInput, ImportOutput> | TypedClientRun<SyncInput, SyncOutput>\n *\n * function Dashboard() {\n * const { runs } = useRuns<DashboardRun>({ api: '/api/durably', pageSize: 10 })\n * // runs are typed as DashboardRun[]\n * }\n * ```\n *\n * @example With JobDefinition (single job, auto-filters by jobName)\n * ```tsx\n * const myJob = defineJob({ name: 'my-job', ... })\n *\n * function RunHistory() {\n * const { runs } = useRuns(myJob, { api: '/api/durably' })\n * // runs[0].output is typed!\n * return <div>{runs[0]?.output?.someField}</div>\n * }\n * ```\n *\n * @example With options only (untyped)\n * ```tsx\n * function RunHistory() {\n * const { runs } = useRuns({ api: '/api/durably', pageSize: 10 })\n * // runs[0].output is unknown\n * }\n * ```\n */\n// Overload 1: With generic type parameter\nexport function useRuns<\n TRun extends TypedClientRun<\n Record<string, unknown>,\n Record<string, unknown> | undefined\n >,\n>(\n options: UseRunsClientOptions,\n): UseRunsClientResult<\n TRun extends TypedClientRun<infer I, infer _O> ? I : Record<string, unknown>,\n TRun extends TypedClientRun<infer _I, infer O> ? O : Record<string, unknown>\n>\n\n// Overload 2: With JobDefinition for type inference (auto-filters by jobName)\nexport function useRuns<\n TName extends string,\n TInput extends Record<string, unknown>,\n TOutput extends Record<string, unknown> | undefined,\n>(\n jobDefinition: JobDefinition<TName, TInput, TOutput>,\n options: Omit<UseRunsClientOptions, 'jobName'>,\n): UseRunsClientResult<TInput, TOutput>\n\n// Overload 3: Without type parameter (untyped, backward compatible)\nexport function useRuns(options: UseRunsClientOptions): UseRunsClientResult\n\n// Implementation\nexport function useRuns<\n TName extends string,\n TInput extends Record<string, unknown>,\n TOutput extends Record<string, unknown> | undefined,\n>(\n jobDefinitionOrOptions:\n | JobDefinition<TName, TInput, TOutput>\n | UseRunsClientOptions,\n optionsArg?: Omit<UseRunsClientOptions, 'jobName'>,\n): UseRunsClientResult<TInput, TOutput> {\n // Determine if first argument is a JobDefinition using type guard\n const isJob = isJobDefinition(jobDefinitionOrOptions)\n\n const jobName = isJob\n ? jobDefinitionOrOptions.name\n : (jobDefinitionOrOptions as UseRunsClientOptions).jobName\n\n const options = isJob\n ? (optionsArg as Omit<UseRunsClientOptions, 'jobName'>)\n : (jobDefinitionOrOptions as UseRunsClientOptions)\n\n const { api, status, pageSize = 10 } = options\n\n const [runs, setRuns] = useState<TypedClientRun<TInput, TOutput>[]>([])\n const [page, setPage] = useState(0)\n const [hasMore, setHasMore] = useState(false)\n const [isLoading, setIsLoading] = useState(false)\n const [error, setError] = useState<string | null>(null)\n\n const isMountedRef = useRef(true)\n const eventSourceRef = useRef<EventSource | null>(null)\n\n const refresh = useCallback(async () => {\n setIsLoading(true)\n setError(null)\n\n try {\n const params = new URLSearchParams()\n if (jobName) params.set('jobName', jobName)\n if (status) params.set('status', status)\n params.set('limit', String(pageSize + 1))\n params.set('offset', String(page * pageSize))\n\n const url = `${api}/runs?${params.toString()}`\n const response = await fetch(url)\n\n if (!response.ok) {\n throw new Error(`Failed to fetch runs: ${response.statusText}`)\n }\n\n const data = (await response.json()) as TypedClientRun<TInput, TOutput>[]\n\n if (isMountedRef.current) {\n setHasMore(data.length > pageSize)\n setRuns(data.slice(0, pageSize))\n }\n } catch (err) {\n if (isMountedRef.current) {\n setError(err instanceof Error ? err.message : 'Unknown error')\n }\n } finally {\n if (isMountedRef.current) {\n setIsLoading(false)\n }\n }\n }, [api, jobName, status, pageSize, page])\n\n // Initial fetch\n useEffect(() => {\n isMountedRef.current = true\n refresh()\n\n return () => {\n isMountedRef.current = false\n }\n }, [refresh])\n\n // SSE subscription for first page only\n useEffect(() => {\n // Only subscribe to SSE on first page\n if (page !== 0) {\n // Clean up any existing connection when navigating away from first page\n if (eventSourceRef.current) {\n eventSourceRef.current.close()\n eventSourceRef.current = null\n }\n return\n }\n\n // Build SSE URL\n const params = new URLSearchParams()\n if (jobName) params.set('jobName', jobName)\n const sseUrl = `${api}/runs/subscribe${params.toString() ? `?${params.toString()}` : ''}`\n\n const eventSource = new EventSource(sseUrl)\n eventSourceRef.current = eventSource\n\n eventSource.onmessage = (event) => {\n try {\n const data = JSON.parse(event.data) as RunUpdateEvent\n // On run lifecycle events, refresh the list\n if (\n data.type === 'run:trigger' ||\n data.type === 'run:start' ||\n data.type === 'run:complete' ||\n data.type === 'run:fail' ||\n data.type === 'run:cancel' ||\n data.type === 'run:retry'\n ) {\n refresh()\n }\n // On progress update, update the run in place\n if (data.type === 'run:progress') {\n setRuns((prev) =>\n prev.map((run) =>\n run.id === data.runId ? { ...run, progress: data.progress } : run,\n ),\n )\n }\n // On step complete, update currentStepIndex\n if (data.type === 'step:complete') {\n setRuns((prev) =>\n prev.map((run) =>\n run.id === data.runId\n ? { ...run, currentStepIndex: data.stepIndex + 1 }\n : run,\n ),\n )\n }\n // On step start or fail, refresh to get latest state\n if (data.type === 'step:start' || data.type === 'step:fail') {\n refresh()\n }\n // log:write is handled by useJobLogs, not useRuns\n } catch {\n // Ignore parse errors\n }\n }\n\n eventSource.onerror = () => {\n // EventSource will automatically reconnect\n }\n\n return () => {\n eventSource.close()\n eventSourceRef.current = null\n }\n }, [api, jobName, page, refresh])\n\n const nextPage = useCallback(() => {\n if (hasMore) {\n setPage((p) => p + 1)\n }\n }, [hasMore])\n\n const prevPage = useCallback(() => {\n setPage((p) => Math.max(0, p - 1))\n }, [])\n\n const goToPage = useCallback((newPage: number) => {\n setPage(Math.max(0, newPage))\n }, [])\n\n return {\n runs,\n page,\n hasMore,\n isLoading,\n error,\n nextPage,\n prevPage,\n goToPage,\n refresh,\n }\n}\n","import { useCallback, useState } from 'react'\n\n/**\n * Run record returned from the server API\n */\nexport interface RunRecord {\n id: string\n jobName: string\n status: 'pending' | 'running' | 'completed' | 'failed' | 'cancelled'\n payload: unknown\n output: unknown | null\n error: string | null\n progress: { current: number; total?: number; message?: string } | null\n currentStepIndex: number\n stepCount: number\n createdAt: string\n startedAt: string | null\n completedAt: string | null\n}\n\n/**\n * Step record returned from the server API\n */\nexport interface StepRecord {\n name: string\n status: 'completed' | 'failed'\n output: unknown\n}\n\nexport interface UseRunActionsClientOptions {\n /**\n * API endpoint URL (e.g., '/api/durably')\n */\n api: string\n}\n\nexport interface UseRunActionsClientResult {\n /**\n * Retry a failed or cancelled run\n */\n retry: (runId: string) => Promise<void>\n /**\n * Cancel a pending or running run\n */\n cancel: (runId: string) => Promise<void>\n /**\n * Delete a run (only completed, failed, or cancelled runs)\n */\n deleteRun: (runId: string) => Promise<void>\n /**\n * Get a single run by ID\n */\n getRun: (runId: string) => Promise<RunRecord | null>\n /**\n * Get steps for a run\n */\n getSteps: (runId: string) => Promise<StepRecord[]>\n /**\n * Whether an action is in progress\n */\n isLoading: boolean\n /**\n * Error message from last action\n */\n error: string | null\n}\n\n/**\n * Hook for run actions (retry, cancel) via server API.\n *\n * @example\n * ```tsx\n * function RunActions({ runId, status }: { runId: string; status: string }) {\n * const { retry, cancel, isLoading, error } = useRunActions({\n * api: '/api/durably',\n * })\n *\n * return (\n * <div>\n * {status === 'failed' && (\n * <button onClick={() => retry(runId)} disabled={isLoading}>\n * Retry\n * </button>\n * )}\n * {(status === 'pending' || status === 'running') && (\n * <button onClick={() => cancel(runId)} disabled={isLoading}>\n * Cancel\n * </button>\n * )}\n * {error && <span className=\"error\">{error}</span>}\n * </div>\n * )\n * }\n * ```\n */\nexport function useRunActions(\n options: UseRunActionsClientOptions,\n): UseRunActionsClientResult {\n const { api } = options\n\n const [isLoading, setIsLoading] = useState(false)\n const [error, setError] = useState<string | null>(null)\n\n const retry = useCallback(\n async (runId: string) => {\n setIsLoading(true)\n setError(null)\n\n try {\n const url = `${api}/retry?runId=${encodeURIComponent(runId)}`\n const response = await fetch(url, { method: 'POST' })\n\n if (!response.ok) {\n let errorMessage = `Failed to retry: ${response.statusText}`\n try {\n const data = await response.json()\n if (data.error) {\n errorMessage = data.error\n }\n } catch {\n // Response is not JSON, use statusText\n }\n throw new Error(errorMessage)\n }\n } catch (err) {\n const message = err instanceof Error ? err.message : 'Unknown error'\n setError(message)\n throw err\n } finally {\n setIsLoading(false)\n }\n },\n [api],\n )\n\n const cancel = useCallback(\n async (runId: string) => {\n setIsLoading(true)\n setError(null)\n\n try {\n const url = `${api}/cancel?runId=${encodeURIComponent(runId)}`\n const response = await fetch(url, { method: 'POST' })\n\n if (!response.ok) {\n let errorMessage = `Failed to cancel: ${response.statusText}`\n try {\n const data = await response.json()\n if (data.error) {\n errorMessage = data.error\n }\n } catch {\n // Response is not JSON, use statusText\n }\n throw new Error(errorMessage)\n }\n } catch (err) {\n const message = err instanceof Error ? err.message : 'Unknown error'\n setError(message)\n throw err\n } finally {\n setIsLoading(false)\n }\n },\n [api],\n )\n\n const deleteRun = useCallback(\n async (runId: string) => {\n setIsLoading(true)\n setError(null)\n\n try {\n const url = `${api}/run?runId=${encodeURIComponent(runId)}`\n const response = await fetch(url, { method: 'DELETE' })\n\n if (!response.ok) {\n let errorMessage = `Failed to delete: ${response.statusText}`\n try {\n const data = await response.json()\n if (data.error) {\n errorMessage = data.error\n }\n } catch {\n // Response is not JSON, use statusText\n }\n throw new Error(errorMessage)\n }\n } catch (err) {\n const message = err instanceof Error ? err.message : 'Unknown error'\n setError(message)\n throw err\n } finally {\n setIsLoading(false)\n }\n },\n [api],\n )\n\n const getRun = useCallback(\n async (runId: string): Promise<RunRecord | null> => {\n setIsLoading(true)\n setError(null)\n\n try {\n const url = `${api}/run?runId=${encodeURIComponent(runId)}`\n const response = await fetch(url)\n\n if (response.status === 404) {\n return null\n }\n\n if (!response.ok) {\n let errorMessage = `Failed to get run: ${response.statusText}`\n try {\n const data = await response.json()\n if (data.error) {\n errorMessage = data.error\n }\n } catch {\n // Response is not JSON, use statusText\n }\n throw new Error(errorMessage)\n }\n\n return (await response.json()) as RunRecord\n } catch (err) {\n const message = err instanceof Error ? err.message : 'Unknown error'\n setError(message)\n throw err\n } finally {\n setIsLoading(false)\n }\n },\n [api],\n )\n\n const getSteps = useCallback(\n async (runId: string): Promise<StepRecord[]> => {\n setIsLoading(true)\n setError(null)\n\n try {\n const url = `${api}/steps?runId=${encodeURIComponent(runId)}`\n const response = await fetch(url)\n\n if (!response.ok) {\n let errorMessage = `Failed to get steps: ${response.statusText}`\n try {\n const data = await response.json()\n if (data.error) {\n errorMessage = data.error\n }\n } catch {\n // Response is not JSON, use statusText\n }\n throw new Error(errorMessage)\n }\n\n return (await response.json()) as StepRecord[]\n } catch (err) {\n const message = err instanceof Error ? err.message : 'Unknown error'\n setError(message)\n throw err\n } finally {\n setIsLoading(false)\n }\n },\n [api],\n )\n\n return {\n retry,\n cancel,\n deleteRun,\n getRun,\n getSteps,\n isLoading,\n error,\n }\n}\n"],"mappings":"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","names":["useEffect","useEffect","useCallback","useEffect","useRef","useState","useState","useRef","useCallback","useEffect","useCallback","useState"]}
1
+ {"version":3,"sources":["../src/client/use-job.ts","../src/client/use-sse-subscription.ts","../src/shared/sse-event-subscriber.ts","../src/client/use-job-logs.ts","../src/client/use-job-run.ts","../src/client/create-durably-client.ts","../src/client/create-job-hooks.ts","../src/client/use-runs.ts","../src/client/use-run-actions.ts"],"sourcesContent":["import { useCallback, useEffect, useRef, useState } from 'react'\nimport type { LogEntry, Progress, RunStatus } from '../types'\nimport { useSSESubscription } from './use-sse-subscription'\n\nexport interface UseJobClientOptions {\n /**\n * API endpoint URL (e.g., '/api/durably')\n */\n api: string\n /**\n * Job name to trigger\n */\n jobName: string\n /**\n * Initial Run ID to subscribe to (for reconnection scenarios)\n * When provided, the hook will immediately start subscribing to this run\n */\n initialRunId?: string\n /**\n * Automatically resume tracking a running/pending job on mount\n * @default true\n */\n autoResume?: boolean\n /**\n * Automatically switch to tracking the latest triggered job\n * @default true\n */\n followLatest?: boolean\n}\n\nexport interface UseJobClientResult<TInput, TOutput> {\n /**\n * Whether the hook is ready (always true for client mode)\n */\n /**\n * Trigger the job with the given input\n */\n trigger: (input: TInput) => Promise<{ runId: string }>\n /**\n * Trigger and wait for completion\n */\n triggerAndWait: (input: TInput) => Promise<{ runId: string; output: TOutput }>\n /**\n * Current run status\n */\n status: RunStatus | null\n /**\n * Output from completed run\n */\n output: TOutput | null\n /**\n * Error message from failed run\n */\n error: string | null\n /**\n * Logs collected during execution\n */\n logs: LogEntry[]\n /**\n * Current progress\n */\n progress: Progress | null\n /**\n * Whether a run is currently running\n */\n isRunning: boolean\n /**\n * Whether a run is pending\n */\n isPending: boolean\n /**\n * Whether the run completed successfully\n */\n isCompleted: boolean\n /**\n * Whether the run failed\n */\n isFailed: boolean\n /**\n * Whether the run was cancelled\n */\n isCancelled: boolean\n /**\n * Current run ID\n */\n currentRunId: string | null\n /**\n * Reset all state\n */\n reset: () => void\n}\n\n/**\n * Hook for triggering and subscribing to jobs via server API.\n * Uses fetch for triggering and EventSource for SSE subscription.\n */\nexport function useJob<\n TInput extends Record<string, unknown> = Record<string, unknown>,\n TOutput extends Record<string, unknown> = Record<string, unknown>,\n>(options: UseJobClientOptions): UseJobClientResult<TInput, TOutput> {\n const {\n api,\n jobName,\n initialRunId,\n autoResume = true,\n followLatest = true,\n } = options\n\n const [currentRunId, setCurrentRunId] = useState<string | null>(\n initialRunId ?? null,\n )\n const [isPending, setIsPending] = useState(false)\n\n // Track if user has triggered a run (to prevent autoResume from overwriting)\n const hasUserTriggered = useRef(false)\n\n const subscription = useSSESubscription<TOutput>(api, currentRunId)\n\n // Auto-resume: fetch running/pending job on mount\n useEffect(() => {\n if (!autoResume) return\n if (initialRunId) return // Skip if initialRunId is provided\n\n const abortController = new AbortController()\n\n const findActiveRun = async () => {\n // Try running first\n const runningParams = new URLSearchParams({\n jobName,\n status: 'running',\n limit: '1',\n })\n const runningRes = await fetch(`${api}/runs?${runningParams}`, {\n signal: abortController.signal,\n })\n if (runningRes.ok) {\n const runs = (await runningRes.json()) as Array<{ id: string }>\n if (runs.length > 0) {\n // Don't overwrite if user already triggered a run\n if (hasUserTriggered.current) return\n setCurrentRunId(runs[0].id)\n return\n }\n }\n\n // Try pending\n const pendingParams = new URLSearchParams({\n jobName,\n status: 'pending',\n limit: '1',\n })\n const pendingRes = await fetch(`${api}/runs?${pendingParams}`, {\n signal: abortController.signal,\n })\n if (pendingRes.ok) {\n const runs = (await pendingRes.json()) as Array<{ id: string }>\n if (runs.length > 0) {\n // Don't overwrite if user already triggered a run\n if (hasUserTriggered.current) return\n setCurrentRunId(runs[0].id)\n }\n }\n }\n\n findActiveRun().catch((err) => {\n // Ignore abort errors\n if (err.name !== 'AbortError') {\n console.error('autoResume error:', err)\n }\n })\n\n return () => {\n abortController.abort()\n }\n }, [api, jobName, autoResume, initialRunId])\n\n // Follow latest: subscribe to job-level SSE for run:trigger/run:start events\n useEffect(() => {\n if (!followLatest) return\n\n const params = new URLSearchParams({ jobName })\n const eventSource = new EventSource(`${api}/runs/subscribe?${params}`)\n\n eventSource.onmessage = (event) => {\n try {\n const data = JSON.parse(event.data) as {\n type: string\n runId?: string\n }\n if (\n (data.type === 'run:trigger' || data.type === 'run:start') &&\n data.runId\n ) {\n setCurrentRunId(data.runId)\n }\n } catch {\n // Ignore parse errors\n }\n }\n\n eventSource.onerror = () => {\n // SSE connection error - could reconnect or log for debugging\n // No need to surface error to user as this is a background subscription\n }\n\n return () => {\n eventSource.close()\n }\n }, [api, jobName, followLatest])\n\n const trigger = useCallback(\n async (input: TInput): Promise<{ runId: string }> => {\n // Mark that user has triggered (prevents autoResume from overwriting)\n hasUserTriggered.current = true\n\n // Reset state\n subscription.reset()\n setIsPending(true)\n\n const response = await fetch(`${api}/trigger`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({ jobName, input }),\n })\n\n if (!response.ok) {\n setIsPending(false)\n const errorText = await response.text()\n throw new Error(errorText || `HTTP ${response.status}`)\n }\n\n const { runId } = (await response.json()) as { runId: string }\n setCurrentRunId(runId)\n\n return { runId }\n },\n [api, jobName, subscription.reset],\n )\n\n const triggerAndWait = useCallback(\n async (input: TInput): Promise<{ runId: string; output: TOutput }> => {\n const { runId } = await trigger(input)\n\n return new Promise((resolve, reject) => {\n const checkInterval = setInterval(() => {\n if (subscription.status === 'completed' && subscription.output) {\n clearInterval(checkInterval)\n resolve({ runId, output: subscription.output })\n } else if (subscription.status === 'failed') {\n clearInterval(checkInterval)\n reject(new Error(subscription.error ?? 'Job failed'))\n } else if (subscription.status === 'cancelled') {\n clearInterval(checkInterval)\n reject(new Error('Job cancelled'))\n }\n }, 50)\n })\n },\n [trigger, subscription.status, subscription.output, subscription.error],\n )\n\n const reset = useCallback(() => {\n subscription.reset()\n setCurrentRunId(null)\n setIsPending(false)\n }, [subscription.reset])\n\n // Compute effective status (pending overrides null when we've triggered but SSE hasn't started)\n const effectiveStatus = subscription.status ?? (isPending ? 'pending' : null)\n\n // Clear pending when we get a real status\n useEffect(() => {\n if (subscription.status && isPending) {\n setIsPending(false)\n }\n }, [subscription.status, isPending])\n\n return {\n trigger,\n triggerAndWait,\n status: effectiveStatus,\n output: subscription.output,\n error: subscription.error,\n logs: subscription.logs,\n progress: subscription.progress,\n isRunning: effectiveStatus === 'running',\n isPending: effectiveStatus === 'pending',\n isCompleted: effectiveStatus === 'completed',\n isFailed: effectiveStatus === 'failed',\n isCancelled: effectiveStatus === 'cancelled',\n currentRunId,\n reset,\n }\n}\n","import { useMemo } from 'react'\nimport { createSSEEventSubscriber } from '../shared/sse-event-subscriber'\nimport {\n useSubscription,\n type UseSubscriptionOptions,\n type UseSubscriptionResult,\n} from '../shared/use-subscription'\nimport type { SubscriptionState } from '../types'\n\n/** @deprecated Use SubscriptionState from '../types' instead */\nexport type SSESubscriptionState<TOutput = unknown> = SubscriptionState<TOutput>\n\n/** @deprecated Use UseSubscriptionOptions from '../shared/use-subscription' instead */\nexport type UseSSESubscriptionOptions = UseSubscriptionOptions\n\n/** @deprecated Use UseSubscriptionResult from '../shared/use-subscription' instead */\nexport type UseSSESubscriptionResult<TOutput = unknown> =\n UseSubscriptionResult<TOutput>\n\n/**\n * Internal hook for subscribing to run events via SSE.\n * Used by client-mode hooks (useJob, useJobRun, useJobLogs).\n *\n * @deprecated Consider using useSubscription with createSSEEventSubscriber directly.\n */\nexport function useSSESubscription<TOutput = unknown>(\n api: string | null,\n runId: string | null,\n options?: UseSSESubscriptionOptions,\n): UseSSESubscriptionResult<TOutput> {\n const subscriber = useMemo(\n () => (api ? createSSEEventSubscriber(api) : null),\n [api],\n )\n\n return useSubscription<TOutput>(subscriber, runId, options)\n}\n","import type { DurablyEvent } from '../types'\nimport type { EventSubscriber, SubscriptionEvent } from './event-subscriber'\n\n/**\n * EventSubscriber implementation using Server-Sent Events (SSE).\n * Used in client environments that communicate with a Durably server via HTTP.\n */\nexport function createSSEEventSubscriber(apiBaseUrl: string): EventSubscriber {\n return {\n subscribe<TOutput = unknown>(\n runId: string,\n onEvent: (event: SubscriptionEvent<TOutput>) => void,\n ): () => void {\n const url = `${apiBaseUrl}/subscribe?runId=${encodeURIComponent(runId)}`\n const eventSource = new EventSource(url)\n\n eventSource.onmessage = (messageEvent) => {\n try {\n const data = JSON.parse(messageEvent.data) as DurablyEvent\n if (data.runId !== runId) return\n\n switch (data.type) {\n case 'run:start':\n onEvent({ type: 'run:start' })\n break\n case 'run:complete':\n onEvent({\n type: 'run:complete',\n output: data.output as TOutput,\n })\n break\n case 'run:fail':\n onEvent({ type: 'run:fail', error: data.error })\n break\n case 'run:cancel':\n onEvent({ type: 'run:cancel' })\n break\n case 'run:retry':\n onEvent({ type: 'run:retry' })\n break\n case 'run:progress':\n onEvent({ type: 'run:progress', progress: data.progress })\n break\n case 'log:write':\n onEvent({\n type: 'log:write',\n runId: data.runId,\n stepName: null,\n level: data.level,\n message: data.message,\n data: data.data,\n })\n break\n }\n } catch {\n // Ignore parse errors\n }\n }\n\n eventSource.onerror = () => {\n onEvent({ type: 'connection_error', error: 'Connection failed' })\n eventSource.close()\n }\n\n return () => {\n eventSource.close()\n }\n },\n }\n}\n","import type { LogEntry } from '../types'\nimport { useSSESubscription } from './use-sse-subscription'\n\nexport interface UseJobLogsClientOptions {\n /**\n * API endpoint URL (e.g., '/api/durably')\n */\n api: string\n /**\n * The run ID to subscribe to logs for\n */\n runId: string | null\n /**\n * Maximum number of logs to keep (default: unlimited)\n */\n maxLogs?: number\n}\n\nexport interface UseJobLogsClientResult {\n /**\n * Whether the hook is ready (always true for client mode)\n */\n /**\n * Logs collected during execution\n */\n logs: LogEntry[]\n /**\n * Clear all logs\n */\n clearLogs: () => void\n}\n\n/**\n * Hook for subscribing to logs from a run via server API.\n * Uses EventSource for SSE subscription.\n */\nexport function useJobLogs(\n options: UseJobLogsClientOptions,\n): UseJobLogsClientResult {\n const { api, runId, maxLogs } = options\n\n const subscription = useSSESubscription(api, runId, { maxLogs })\n\n return {\n logs: subscription.logs,\n clearLogs: subscription.clearLogs,\n }\n}\n","import { useEffect, useRef } from 'react'\nimport type { LogEntry, Progress, RunStatus } from '../types'\nimport { useSSESubscription } from './use-sse-subscription'\n\nexport interface UseJobRunClientOptions {\n /**\n * API endpoint URL (e.g., '/api/durably')\n */\n api: string\n /**\n * The run ID to subscribe to\n */\n runId: string | null\n /**\n * Callback when run starts (transitions to pending/running)\n */\n onStart?: () => void\n /**\n * Callback when run completes successfully\n */\n onComplete?: () => void\n /**\n * Callback when run fails\n */\n onFail?: () => void\n}\n\nexport interface UseJobRunClientResult<TOutput = unknown> {\n /**\n * Whether the hook is ready (always true for client mode)\n */\n /**\n * Current run status\n */\n status: RunStatus | null\n /**\n * Output from completed run\n */\n output: TOutput | null\n /**\n * Error message from failed run\n */\n error: string | null\n /**\n * Logs collected during execution\n */\n logs: LogEntry[]\n /**\n * Current progress\n */\n progress: Progress | null\n /**\n * Whether a run is currently running\n */\n isRunning: boolean\n /**\n * Whether a run is pending\n */\n isPending: boolean\n /**\n * Whether the run completed successfully\n */\n isCompleted: boolean\n /**\n * Whether the run failed\n */\n isFailed: boolean\n /**\n * Whether the run was cancelled\n */\n isCancelled: boolean\n}\n\n/**\n * Hook for subscribing to an existing run via server API.\n * Uses EventSource for SSE subscription.\n */\nexport function useJobRun<TOutput = unknown>(\n options: UseJobRunClientOptions,\n): UseJobRunClientResult<TOutput> {\n const { api, runId, onStart, onComplete, onFail } = options\n\n const subscription = useSSESubscription<TOutput>(api, runId)\n\n // If we have a runId but no status yet, treat as pending\n const effectiveStatus = subscription.status ?? (runId ? 'pending' : null)\n\n const isCompleted = effectiveStatus === 'completed'\n const isFailed = effectiveStatus === 'failed'\n const isPending = effectiveStatus === 'pending'\n const isRunning = effectiveStatus === 'running'\n const isCancelled = effectiveStatus === 'cancelled'\n\n // Track previous status to detect transitions (use effectiveStatus, not subscription.status)\n const prevStatusRef = useRef<RunStatus | null>(null)\n\n useEffect(() => {\n const prevStatus = prevStatusRef.current\n prevStatusRef.current = effectiveStatus\n\n // Only fire callbacks on status transitions\n if (prevStatus !== effectiveStatus) {\n // Fire onStart when transitioning from null to pending/running\n if (prevStatus === null && (isPending || isRunning) && onStart) {\n onStart()\n }\n if (isCompleted && onComplete) {\n onComplete()\n }\n if (isFailed && onFail) {\n onFail()\n }\n }\n }, [\n effectiveStatus,\n isPending,\n isRunning,\n isCompleted,\n isFailed,\n onStart,\n onComplete,\n onFail,\n ])\n\n return {\n status: effectiveStatus,\n output: subscription.output,\n error: subscription.error,\n logs: subscription.logs,\n progress: subscription.progress,\n isRunning,\n isPending,\n isCompleted,\n isFailed,\n isCancelled,\n }\n}\n","import type { InferInput, InferOutput } from '../types'\nimport { useJob, type UseJobClientResult } from './use-job'\nimport { useJobLogs, type UseJobLogsClientResult } from './use-job-logs'\nimport { useJobRun, type UseJobRunClientResult } from './use-job-run'\n\n/**\n * Type-safe hooks for a specific job\n */\nexport interface JobClient<TInput, TOutput> {\n /**\n * Hook for triggering and monitoring the job\n */\n useJob: () => UseJobClientResult<TInput, TOutput>\n\n /**\n * Hook for subscribing to an existing run by ID\n */\n useRun: (runId: string | null) => UseJobRunClientResult<TOutput>\n\n /**\n * Hook for subscribing to logs from a run\n */\n useLogs: (\n runId: string | null,\n options?: { maxLogs?: number },\n ) => UseJobLogsClientResult\n}\n\n/**\n * Options for createDurablyClient\n */\nexport interface CreateDurablyClientOptions {\n /**\n * API endpoint URL (e.g., '/api/durably')\n */\n api: string\n}\n\n/**\n * A type-safe client with hooks for each registered job\n */\nexport type DurablyClient<TJobs extends Record<string, unknown>> = {\n [K in keyof TJobs]: JobClient<InferInput<TJobs[K]>, InferOutput<TJobs[K]>>\n}\n\n/**\n * Create a type-safe Durably client with hooks for all registered jobs.\n *\n * @example\n * ```tsx\n * // Server: register jobs\n * // app/lib/durably.server.ts\n * export const jobs = durably.register({\n * importCsv: importCsvJob,\n * syncUsers: syncUsersJob,\n * })\n *\n * // Client: create typed client\n * // app/lib/durably.client.ts\n * import type { jobs } from '~/lib/durably.server'\n * import { createDurablyClient } from '@coji/durably-react/client'\n *\n * export const durably = createDurablyClient<typeof jobs>({\n * api: '/api/durably',\n * })\n *\n * // In your component - fully type-safe with autocomplete\n * function CsvImporter() {\n * const { trigger, output, isRunning } = durably.importCsv.useJob()\n *\n * return (\n * <button onClick={() => trigger({ rows: [...] })}>\n * Import\n * </button>\n * )\n * }\n * ```\n */\nexport function createDurablyClient<TJobs extends Record<string, unknown>>(\n options: CreateDurablyClientOptions,\n): DurablyClient<TJobs> {\n const { api } = options\n\n // Create a proxy that generates job clients on demand\n return new Proxy({} as DurablyClient<TJobs>, {\n get(_target, jobKey: string) {\n return {\n useJob: () => {\n return useJob({ api, jobName: jobKey })\n },\n\n useRun: (runId: string | null) => {\n return useJobRun({ api, runId })\n },\n\n useLogs: (runId: string | null, logsOptions?: { maxLogs?: number }) => {\n return useJobLogs({ api, runId, maxLogs: logsOptions?.maxLogs })\n },\n }\n },\n })\n}\n","import type { JobDefinition } from '@coji/durably'\nimport type { InferInput, InferOutput } from '../types'\nimport { useJob, type UseJobClientResult } from './use-job'\nimport { useJobLogs, type UseJobLogsClientResult } from './use-job-logs'\nimport { useJobRun, type UseJobRunClientResult } from './use-job-run'\n\n/**\n * Options for createJobHooks\n */\nexport interface CreateJobHooksOptions {\n /**\n * API endpoint URL (e.g., '/api/durably')\n */\n api: string\n /**\n * Job name (must match the server-side job name)\n */\n jobName: string\n}\n\n/**\n * Type-safe hooks for a specific job\n */\nexport interface JobHooks<TInput, TOutput> {\n /**\n * Hook for triggering and monitoring the job\n */\n useJob: () => UseJobClientResult<TInput, TOutput>\n\n /**\n * Hook for subscribing to an existing run by ID\n */\n useRun: (runId: string | null) => UseJobRunClientResult<TOutput>\n\n /**\n * Hook for subscribing to logs from a run\n */\n useLogs: (\n runId: string | null,\n options?: { maxLogs?: number },\n ) => UseJobLogsClientResult\n}\n\n/**\n * Create type-safe hooks for a specific job.\n *\n * @example\n * ```tsx\n * // Import job type from server (type-only import is safe)\n * import type { importCsvJob } from '~/lib/durably.server'\n * import { createJobHooks } from '@coji/durably-react/client'\n *\n * const importCsv = createJobHooks<typeof importCsvJob>({\n * api: '/api/durably',\n * jobName: 'import-csv',\n * })\n *\n * // In your component - fully type-safe\n * function CsvImporter() {\n * const { trigger, output, progress, isRunning } = importCsv.useJob()\n *\n * return (\n * <button onClick={() => trigger({ rows: [...] })}>\n * Import\n * </button>\n * )\n * }\n * ```\n */\nexport function createJobHooks<\n // biome-ignore lint/suspicious/noExplicitAny: TJob needs to accept any JobDefinition\n TJob extends JobDefinition<string, any, any>,\n>(\n options: CreateJobHooksOptions,\n): JobHooks<InferInput<TJob>, InferOutput<TJob>> {\n const { api, jobName } = options\n\n return {\n useJob: () => {\n return useJob<InferInput<TJob>, InferOutput<TJob>>({ api, jobName })\n },\n\n useRun: (runId: string | null) => {\n return useJobRun<InferOutput<TJob>>({ api, runId })\n },\n\n useLogs: (runId: string | null, logsOptions?: { maxLogs?: number }) => {\n return useJobLogs({ api, runId, maxLogs: logsOptions?.maxLogs })\n },\n }\n}\n","import type { JobDefinition } from '@coji/durably'\nimport { useCallback, useEffect, useRef, useState } from 'react'\nimport {\n type Progress,\n type RunStatus,\n type TypedClientRun,\n isJobDefinition,\n} from '../types'\n\n// Re-export types for convenience\nexport type { ClientRun, TypedClientRun } from '../types'\n\n/**\n * SSE notification event from /runs/subscribe\n */\ntype RunUpdateEvent =\n | {\n type:\n | 'run:trigger'\n | 'run:start'\n | 'run:complete'\n | 'run:fail'\n | 'run:cancel'\n | 'run:retry'\n runId: string\n jobName: string\n }\n | { type: 'run:progress'; runId: string; jobName: string; progress: Progress }\n | {\n type: 'step:start' | 'step:complete'\n runId: string\n jobName: string\n stepName: string\n stepIndex: number\n }\n | {\n type: 'step:fail'\n runId: string\n jobName: string\n stepName: string\n stepIndex: number\n error: string\n }\n | {\n type: 'log:write'\n runId: string\n stepName: string | null\n level: 'info' | 'warn' | 'error'\n message: string\n data: unknown\n }\n\nexport interface UseRunsClientOptions {\n /**\n * API endpoint URL (e.g., '/api/durably')\n */\n api: string\n /**\n * Filter by job name\n */\n jobName?: string\n /**\n * Filter by status\n */\n status?: RunStatus\n /**\n * Number of runs per page\n * @default 10\n */\n pageSize?: number\n}\n\nexport interface UseRunsClientResult<\n TInput extends Record<string, unknown> = Record<string, unknown>,\n TOutput extends Record<string, unknown> | undefined =\n | Record<string, unknown>\n | undefined,\n> {\n /**\n * List of runs for the current page\n */\n runs: TypedClientRun<TInput, TOutput>[]\n /**\n * Current page (0-indexed)\n */\n page: number\n /**\n * Whether there are more pages\n */\n hasMore: boolean\n /**\n * Whether data is being loaded\n */\n isLoading: boolean\n /**\n * Error message if fetch failed\n */\n error: string | null\n /**\n * Go to the next page\n */\n nextPage: () => void\n /**\n * Go to the previous page\n */\n prevPage: () => void\n /**\n * Go to a specific page\n */\n goToPage: (page: number) => void\n /**\n * Refresh the current page\n */\n refresh: () => Promise<void>\n}\n\n/**\n * Hook for listing runs via server API with pagination.\n * First page (page 0) automatically subscribes to SSE for real-time updates.\n * Other pages are static and require manual refresh.\n *\n * @example With generic type parameter (dashboard with multiple job types)\n * ```tsx\n * type DashboardRun = TypedClientRun<ImportInput, ImportOutput> | TypedClientRun<SyncInput, SyncOutput>\n *\n * function Dashboard() {\n * const { runs } = useRuns<DashboardRun>({ api: '/api/durably', pageSize: 10 })\n * // runs are typed as DashboardRun[]\n * }\n * ```\n *\n * @example With JobDefinition (single job, auto-filters by jobName)\n * ```tsx\n * const myJob = defineJob({ name: 'my-job', ... })\n *\n * function RunHistory() {\n * const { runs } = useRuns(myJob, { api: '/api/durably' })\n * // runs[0].output is typed!\n * return <div>{runs[0]?.output?.someField}</div>\n * }\n * ```\n *\n * @example With options only (untyped)\n * ```tsx\n * function RunHistory() {\n * const { runs } = useRuns({ api: '/api/durably', pageSize: 10 })\n * // runs[0].output is unknown\n * }\n * ```\n */\n// Overload 1: With generic type parameter\nexport function useRuns<\n TRun extends TypedClientRun<\n Record<string, unknown>,\n Record<string, unknown> | undefined\n >,\n>(\n options: UseRunsClientOptions,\n): UseRunsClientResult<\n TRun extends TypedClientRun<infer I, infer _O> ? I : Record<string, unknown>,\n TRun extends TypedClientRun<infer _I, infer O> ? O : Record<string, unknown>\n>\n\n// Overload 2: With JobDefinition for type inference (auto-filters by jobName)\nexport function useRuns<\n TName extends string,\n TInput extends Record<string, unknown>,\n TOutput extends Record<string, unknown> | undefined,\n>(\n jobDefinition: JobDefinition<TName, TInput, TOutput>,\n options: Omit<UseRunsClientOptions, 'jobName'>,\n): UseRunsClientResult<TInput, TOutput>\n\n// Overload 3: Without type parameter (untyped, backward compatible)\nexport function useRuns(options: UseRunsClientOptions): UseRunsClientResult\n\n// Implementation\nexport function useRuns<\n TName extends string,\n TInput extends Record<string, unknown>,\n TOutput extends Record<string, unknown> | undefined,\n>(\n jobDefinitionOrOptions:\n | JobDefinition<TName, TInput, TOutput>\n | UseRunsClientOptions,\n optionsArg?: Omit<UseRunsClientOptions, 'jobName'>,\n): UseRunsClientResult<TInput, TOutput> {\n // Determine if first argument is a JobDefinition using type guard\n const isJob = isJobDefinition(jobDefinitionOrOptions)\n\n const jobName = isJob\n ? jobDefinitionOrOptions.name\n : (jobDefinitionOrOptions as UseRunsClientOptions).jobName\n\n const options = isJob\n ? (optionsArg as Omit<UseRunsClientOptions, 'jobName'>)\n : (jobDefinitionOrOptions as UseRunsClientOptions)\n\n const { api, status, pageSize = 10 } = options\n\n const [runs, setRuns] = useState<TypedClientRun<TInput, TOutput>[]>([])\n const [page, setPage] = useState(0)\n const [hasMore, setHasMore] = useState(false)\n const [isLoading, setIsLoading] = useState(false)\n const [error, setError] = useState<string | null>(null)\n\n const isMountedRef = useRef(true)\n const eventSourceRef = useRef<EventSource | null>(null)\n\n const refresh = useCallback(async () => {\n setIsLoading(true)\n setError(null)\n\n try {\n const params = new URLSearchParams()\n if (jobName) params.set('jobName', jobName)\n if (status) params.set('status', status)\n params.set('limit', String(pageSize + 1))\n params.set('offset', String(page * pageSize))\n\n const url = `${api}/runs?${params.toString()}`\n const response = await fetch(url)\n\n if (!response.ok) {\n throw new Error(`Failed to fetch runs: ${response.statusText}`)\n }\n\n const data = (await response.json()) as TypedClientRun<TInput, TOutput>[]\n\n if (isMountedRef.current) {\n setHasMore(data.length > pageSize)\n setRuns(data.slice(0, pageSize))\n }\n } catch (err) {\n if (isMountedRef.current) {\n setError(err instanceof Error ? err.message : 'Unknown error')\n }\n } finally {\n if (isMountedRef.current) {\n setIsLoading(false)\n }\n }\n }, [api, jobName, status, pageSize, page])\n\n // Initial fetch\n useEffect(() => {\n isMountedRef.current = true\n refresh()\n\n return () => {\n isMountedRef.current = false\n }\n }, [refresh])\n\n // SSE subscription for first page only\n useEffect(() => {\n // Only subscribe to SSE on first page\n if (page !== 0) {\n // Clean up any existing connection when navigating away from first page\n if (eventSourceRef.current) {\n eventSourceRef.current.close()\n eventSourceRef.current = null\n }\n return\n }\n\n // Build SSE URL\n const params = new URLSearchParams()\n if (jobName) params.set('jobName', jobName)\n const sseUrl = `${api}/runs/subscribe${params.toString() ? `?${params.toString()}` : ''}`\n\n const eventSource = new EventSource(sseUrl)\n eventSourceRef.current = eventSource\n\n eventSource.onmessage = (event) => {\n try {\n const data = JSON.parse(event.data) as RunUpdateEvent\n // On run lifecycle events, refresh the list\n if (\n data.type === 'run:trigger' ||\n data.type === 'run:start' ||\n data.type === 'run:complete' ||\n data.type === 'run:fail' ||\n data.type === 'run:cancel' ||\n data.type === 'run:retry'\n ) {\n refresh()\n }\n // On progress update, update the run in place\n if (data.type === 'run:progress') {\n setRuns((prev) =>\n prev.map((run) =>\n run.id === data.runId ? { ...run, progress: data.progress } : run,\n ),\n )\n }\n // On step complete, update currentStepIndex\n if (data.type === 'step:complete') {\n setRuns((prev) =>\n prev.map((run) =>\n run.id === data.runId\n ? { ...run, currentStepIndex: data.stepIndex + 1 }\n : run,\n ),\n )\n }\n // On step start or fail, refresh to get latest state\n if (data.type === 'step:start' || data.type === 'step:fail') {\n refresh()\n }\n // log:write is handled by useJobLogs, not useRuns\n } catch {\n // Ignore parse errors\n }\n }\n\n eventSource.onerror = () => {\n // EventSource will automatically reconnect\n }\n\n return () => {\n eventSource.close()\n eventSourceRef.current = null\n }\n }, [api, jobName, page, refresh])\n\n const nextPage = useCallback(() => {\n if (hasMore) {\n setPage((p) => p + 1)\n }\n }, [hasMore])\n\n const prevPage = useCallback(() => {\n setPage((p) => Math.max(0, p - 1))\n }, [])\n\n const goToPage = useCallback((newPage: number) => {\n setPage(Math.max(0, newPage))\n }, [])\n\n return {\n runs,\n page,\n hasMore,\n isLoading,\n error,\n nextPage,\n prevPage,\n goToPage,\n refresh,\n }\n}\n","import { useCallback, useState } from 'react'\n\n/**\n * Run record returned from the server API\n */\nexport interface RunRecord {\n id: string\n jobName: string\n status: 'pending' | 'running' | 'completed' | 'failed' | 'cancelled'\n payload: unknown\n output: unknown | null\n error: string | null\n progress: { current: number; total?: number; message?: string } | null\n currentStepIndex: number\n stepCount: number\n createdAt: string\n startedAt: string | null\n completedAt: string | null\n}\n\n/**\n * Step record returned from the server API\n */\nexport interface StepRecord {\n name: string\n status: 'completed' | 'failed'\n output: unknown\n}\n\nexport interface UseRunActionsClientOptions {\n /**\n * API endpoint URL (e.g., '/api/durably')\n */\n api: string\n}\n\nexport interface UseRunActionsClientResult {\n /**\n * Retry a failed or cancelled run\n */\n retry: (runId: string) => Promise<void>\n /**\n * Cancel a pending or running run\n */\n cancel: (runId: string) => Promise<void>\n /**\n * Delete a run (only completed, failed, or cancelled runs)\n */\n deleteRun: (runId: string) => Promise<void>\n /**\n * Get a single run by ID\n */\n getRun: (runId: string) => Promise<RunRecord | null>\n /**\n * Get steps for a run\n */\n getSteps: (runId: string) => Promise<StepRecord[]>\n /**\n * Whether an action is in progress\n */\n isLoading: boolean\n /**\n * Error message from last action\n */\n error: string | null\n}\n\n/**\n * Hook for run actions (retry, cancel) via server API.\n *\n * @example\n * ```tsx\n * function RunActions({ runId, status }: { runId: string; status: string }) {\n * const { retry, cancel, isLoading, error } = useRunActions({\n * api: '/api/durably',\n * })\n *\n * return (\n * <div>\n * {status === 'failed' && (\n * <button onClick={() => retry(runId)} disabled={isLoading}>\n * Retry\n * </button>\n * )}\n * {(status === 'pending' || status === 'running') && (\n * <button onClick={() => cancel(runId)} disabled={isLoading}>\n * Cancel\n * </button>\n * )}\n * {error && <span className=\"error\">{error}</span>}\n * </div>\n * )\n * }\n * ```\n */\nexport function useRunActions(\n options: UseRunActionsClientOptions,\n): UseRunActionsClientResult {\n const { api } = options\n\n const [isLoading, setIsLoading] = useState(false)\n const [error, setError] = useState<string | null>(null)\n\n const retry = useCallback(\n async (runId: string) => {\n setIsLoading(true)\n setError(null)\n\n try {\n const url = `${api}/retry?runId=${encodeURIComponent(runId)}`\n const response = await fetch(url, { method: 'POST' })\n\n if (!response.ok) {\n let errorMessage = `Failed to retry: ${response.statusText}`\n try {\n const data = await response.json()\n if (data.error) {\n errorMessage = data.error\n }\n } catch {\n // Response is not JSON, use statusText\n }\n throw new Error(errorMessage)\n }\n } catch (err) {\n const message = err instanceof Error ? err.message : 'Unknown error'\n setError(message)\n throw err\n } finally {\n setIsLoading(false)\n }\n },\n [api],\n )\n\n const cancel = useCallback(\n async (runId: string) => {\n setIsLoading(true)\n setError(null)\n\n try {\n const url = `${api}/cancel?runId=${encodeURIComponent(runId)}`\n const response = await fetch(url, { method: 'POST' })\n\n if (!response.ok) {\n let errorMessage = `Failed to cancel: ${response.statusText}`\n try {\n const data = await response.json()\n if (data.error) {\n errorMessage = data.error\n }\n } catch {\n // Response is not JSON, use statusText\n }\n throw new Error(errorMessage)\n }\n } catch (err) {\n const message = err instanceof Error ? err.message : 'Unknown error'\n setError(message)\n throw err\n } finally {\n setIsLoading(false)\n }\n },\n [api],\n )\n\n const deleteRun = useCallback(\n async (runId: string) => {\n setIsLoading(true)\n setError(null)\n\n try {\n const url = `${api}/run?runId=${encodeURIComponent(runId)}`\n const response = await fetch(url, { method: 'DELETE' })\n\n if (!response.ok) {\n let errorMessage = `Failed to delete: ${response.statusText}`\n try {\n const data = await response.json()\n if (data.error) {\n errorMessage = data.error\n }\n } catch {\n // Response is not JSON, use statusText\n }\n throw new Error(errorMessage)\n }\n } catch (err) {\n const message = err instanceof Error ? err.message : 'Unknown error'\n setError(message)\n throw err\n } finally {\n setIsLoading(false)\n }\n },\n [api],\n )\n\n const getRun = useCallback(\n async (runId: string): Promise<RunRecord | null> => {\n setIsLoading(true)\n setError(null)\n\n try {\n const url = `${api}/run?runId=${encodeURIComponent(runId)}`\n const response = await fetch(url)\n\n if (response.status === 404) {\n return null\n }\n\n if (!response.ok) {\n let errorMessage = `Failed to get run: ${response.statusText}`\n try {\n const data = await response.json()\n if (data.error) {\n errorMessage = data.error\n }\n } catch {\n // Response is not JSON, use statusText\n }\n throw new Error(errorMessage)\n }\n\n return (await response.json()) as RunRecord\n } catch (err) {\n const message = err instanceof Error ? err.message : 'Unknown error'\n setError(message)\n throw err\n } finally {\n setIsLoading(false)\n }\n },\n [api],\n )\n\n const getSteps = useCallback(\n async (runId: string): Promise<StepRecord[]> => {\n setIsLoading(true)\n setError(null)\n\n try {\n const url = `${api}/steps?runId=${encodeURIComponent(runId)}`\n const response = await fetch(url)\n\n if (!response.ok) {\n let errorMessage = `Failed to get steps: ${response.statusText}`\n try {\n const data = await response.json()\n if (data.error) {\n errorMessage = data.error\n }\n } catch {\n // Response is not JSON, use statusText\n }\n throw new Error(errorMessage)\n }\n\n return (await response.json()) as StepRecord[]\n } catch (err) {\n const message = err instanceof Error ? err.message : 'Unknown error'\n setError(message)\n throw err\n } finally {\n setIsLoading(false)\n }\n },\n [api],\n )\n\n return {\n retry,\n cancel,\n deleteRun,\n getRun,\n getSteps,\n isLoading,\n error,\n }\n}\n"],"mappings":"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","names":["useEffect","useRef","useRef","useEffect","useCallback","useEffect","useRef","useState","useState","useRef","useCallback","useEffect","useCallback","useState"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coji/durably-react",
3
- "version": "0.7.0",
3
+ "version": "0.8.1",
4
4
  "description": "React bindings for Durably - step-oriented resumable batch execution",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -49,7 +49,7 @@
49
49
  }
50
50
  },
51
51
  "devDependencies": {
52
- "@biomejs/biome": "^2.3.10",
52
+ "@biomejs/biome": "^2.3.11",
53
53
  "@testing-library/react": "^16.3.1",
54
54
  "@types/react": "^19.2.7",
55
55
  "@types/react-dom": "^19.2.3",
@@ -66,8 +66,8 @@
66
66
  "tsup": "^8.5.1",
67
67
  "typescript": "^5.9.3",
68
68
  "vitest": "^4.0.16",
69
- "zod": "^4.3.4",
70
- "@coji/durably": "0.7.0"
69
+ "zod": "^4.3.5",
70
+ "@coji/durably": "0.8.1"
71
71
  },
72
72
  "scripts": {
73
73
  "build": "tsup",