@coji/durably-react 0.6.1 → 0.8.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
@@ -80,6 +80,8 @@ function MyComponent() {
80
80
  >({
81
81
  api: '/api/durably',
82
82
  jobName: 'my-job',
83
+ autoResume: true, // Auto-resume running/pending jobs on mount (default)
84
+ followLatest: true, // Switch to tracking new runs via SSE (default)
83
85
  })
84
86
 
85
87
  return (
@@ -1,3 +1,8 @@
1
+ // src/types.ts
2
+ function isJobDefinition(obj) {
3
+ return typeof obj === "object" && obj !== null && "name" in obj && "run" in obj && typeof obj.run === "function";
4
+ }
5
+
1
6
  // src/shared/create-log-entry.ts
2
7
  function createLogEntry(params) {
3
8
  return {
@@ -131,6 +136,7 @@ function useSubscription(subscriber, runId, options) {
131
136
  export {
132
137
  initialSubscriptionState,
133
138
  subscriptionReducer,
134
- useSubscription
139
+ useSubscription,
140
+ isJobDefinition
135
141
  };
136
- //# sourceMappingURL=chunk-M3YA2EA4.js.map
142
+ //# sourceMappingURL=chunk-42AVE35N.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 { 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 | 'running'\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)\nexport type DurablyEvent =\n | { type: 'run:start'; runId: string; jobName: string; payload: 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:retry'; runId: string; jobName: string }\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: 'log:write'\n runId: string\n jobName: 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 payload/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, 'payload' | 'output'> & {\n payload: TInput\n output: TOutput | null\n}\n\n/**\n * Run type for client mode (matches server response).\n * Used by client hooks (HTTP/SSE connection).\n */\nexport interface ClientRun {\n id: string\n jobName: string\n status: RunStatus\n input: unknown\n output: unknown | null\n error: string | null\n currentStepIndex: number\n stepCount: number\n progress: Progress | null\n createdAt: string\n startedAt: string | null\n completedAt: string | null\n}\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:start' }\n | { type: 'run:complete'; output: TOutput }\n | { type: 'run:fail'; error: string }\n | { type: 'run:cancel' }\n | { type: 'run:retry' }\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:start':\n return { ...state, status: 'running' }\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:retry':\n return { ...state, status: 'pending', error: null }\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:start':\n case 'run:cancel':\n case 'run:retry':\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/client.d.ts CHANGED
@@ -1,4 +1,5 @@
1
- import { R as RunStatus, L as LogEntry, P as Progress, I as InferInput, a as InferOutput } from './types-C7h7nZv3.js';
1
+ import { R as RunStatus, L as LogEntry, P as Progress, I as InferInput, a as InferOutput, b as TypedClientRun } from './types-DY4Y6ggJ.js';
2
+ export { C as ClientRun } from './types-DY4Y6ggJ.js';
2
3
  import { JobDefinition } from '@coji/durably';
3
4
 
4
5
  interface UseJobClientOptions {
@@ -15,6 +16,16 @@ interface UseJobClientOptions {
15
16
  * When provided, the hook will immediately start subscribing to this run
16
17
  */
17
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;
18
29
  }
19
30
  interface UseJobClientResult<TInput, TOutput> {
20
31
  /**
@@ -319,23 +330,6 @@ interface JobHooks<TInput, TOutput> {
319
330
  */
320
331
  declare function createJobHooks<TJob extends JobDefinition<string, any, any>>(options: CreateJobHooksOptions): JobHooks<InferInput<TJob>, InferOutput<TJob>>;
321
332
 
322
- /**
323
- * Run type for client mode (matches server response)
324
- */
325
- interface ClientRun {
326
- id: string;
327
- jobName: string;
328
- status: RunStatus;
329
- input: unknown;
330
- output: unknown | null;
331
- error: string | null;
332
- currentStepIndex: number;
333
- stepCount: number;
334
- progress: Progress | null;
335
- createdAt: string;
336
- startedAt: string | null;
337
- completedAt: string | null;
338
- }
339
333
  interface UseRunsClientOptions {
340
334
  /**
341
335
  * API endpoint URL (e.g., '/api/durably')
@@ -355,11 +349,11 @@ interface UseRunsClientOptions {
355
349
  */
356
350
  pageSize?: number;
357
351
  }
358
- interface UseRunsClientResult {
352
+ interface UseRunsClientResult<TInput extends Record<string, unknown> = Record<string, unknown>, TOutput extends Record<string, unknown> | undefined = Record<string, unknown> | undefined> {
359
353
  /**
360
354
  * List of runs for the current page
361
355
  */
362
- runs: ClientRun[];
356
+ runs: TypedClientRun<TInput, TOutput>[];
363
357
  /**
364
358
  * Current page (0-indexed)
365
359
  */
@@ -398,28 +392,37 @@ interface UseRunsClientResult {
398
392
  * First page (page 0) automatically subscribes to SSE for real-time updates.
399
393
  * Other pages are static and require manual refresh.
400
394
  *
401
- * @example
395
+ * @example With generic type parameter (dashboard with multiple job types)
402
396
  * ```tsx
397
+ * type DashboardRun = TypedClientRun<ImportInput, ImportOutput> | TypedClientRun<SyncInput, SyncOutput>
398
+ *
399
+ * function Dashboard() {
400
+ * const { runs } = useRuns<DashboardRun>({ api: '/api/durably', pageSize: 10 })
401
+ * // runs are typed as DashboardRun[]
402
+ * }
403
+ * ```
404
+ *
405
+ * @example With JobDefinition (single job, auto-filters by jobName)
406
+ * ```tsx
407
+ * const myJob = defineJob({ name: 'my-job', ... })
408
+ *
403
409
  * function RunHistory() {
404
- * const { runs, page, hasMore, nextPage, prevPage, refresh } = useRuns({
405
- * api: '/api/durably',
406
- * jobName: 'import-csv',
407
- * pageSize: 10,
408
- * })
410
+ * const { runs } = useRuns(myJob, { api: '/api/durably' })
411
+ * // runs[0].output is typed!
412
+ * return <div>{runs[0]?.output?.someField}</div>
413
+ * }
414
+ * ```
409
415
  *
410
- * return (
411
- * <div>
412
- * {runs.map(run => (
413
- * <div key={run.id}>{run.jobName}: {run.status}</div>
414
- * ))}
415
- * <button onClick={prevPage} disabled={page === 0}>Prev</button>
416
- * <button onClick={nextPage} disabled={!hasMore}>Next</button>
417
- * <button onClick={refresh}>Refresh</button>
418
- * </div>
419
- * )
416
+ * @example With options only (untyped)
417
+ * ```tsx
418
+ * function RunHistory() {
419
+ * const { runs } = useRuns({ api: '/api/durably', pageSize: 10 })
420
+ * // runs[0].output is unknown
420
421
  * }
421
422
  * ```
422
423
  */
424
+ declare function useRuns<TRun extends TypedClientRun<Record<string, unknown>, Record<string, unknown> | undefined>>(options: UseRunsClientOptions): UseRunsClientResult<TRun extends TypedClientRun<infer I, infer _O> ? I : Record<string, unknown>, TRun extends TypedClientRun<infer _I, infer O> ? O : Record<string, unknown>>;
425
+ declare function useRuns<TName extends string, TInput extends Record<string, unknown>, TOutput extends Record<string, unknown> | undefined>(jobDefinition: JobDefinition<TName, TInput, TOutput>, options: Omit<UseRunsClientOptions, 'jobName'>): UseRunsClientResult<TInput, TOutput>;
423
426
  declare function useRuns(options: UseRunsClientOptions): UseRunsClientResult;
424
427
 
425
428
  /**
@@ -517,4 +520,4 @@ interface UseRunActionsClientResult {
517
520
  */
518
521
  declare function useRunActions(options: UseRunActionsClientOptions): UseRunActionsClientResult;
519
522
 
520
- export { type ClientRun, type CreateDurablyClientOptions, type CreateJobHooksOptions, type DurablyClient, type JobClient, type JobHooks, LogEntry, Progress, type RunRecord, RunStatus, type StepRecord, type UseJobClientOptions, type UseJobClientResult, type UseJobLogsClientOptions, type UseJobLogsClientResult, type UseJobRunClientOptions, type UseJobRunClientResult, type UseRunActionsClientOptions, type UseRunActionsClientResult, type UseRunsClientOptions, type UseRunsClientResult, createDurablyClient, createJobHooks, useJob, useJobLogs, useJobRun, useRunActions, useRuns };
523
+ export { type CreateDurablyClientOptions, type CreateJobHooksOptions, type DurablyClient, type JobClient, type JobHooks, LogEntry, Progress, type RunRecord, RunStatus, type StepRecord, TypedClientRun, type UseJobClientOptions, type UseJobClientResult, type UseJobLogsClientOptions, type UseJobLogsClientResult, type UseJobRunClientOptions, type UseJobRunClientResult, type UseRunActionsClientOptions, type UseRunActionsClientResult, type UseRunsClientOptions, type UseRunsClientResult, createDurablyClient, createJobHooks, useJob, useJobLogs, useJobRun, useRunActions, useRuns };
package/dist/client.js CHANGED
@@ -1,9 +1,10 @@
1
1
  import {
2
+ isJobDefinition,
2
3
  useSubscription
3
- } from "./chunk-M3YA2EA4.js";
4
+ } from "./chunk-42AVE35N.js";
4
5
 
5
6
  // src/client/use-job.ts
6
- import { useCallback, useEffect, useState } from "react";
7
+ import { useCallback, useEffect, useRef, useState } from "react";
7
8
 
8
9
  // src/client/use-sse-subscription.ts
9
10
  import { useMemo } from "react";
@@ -76,14 +77,87 @@ function useSSESubscription(api, runId, options) {
76
77
 
77
78
  // src/client/use-job.ts
78
79
  function useJob(options) {
79
- const { api, jobName, initialRunId } = options;
80
+ const {
81
+ api,
82
+ jobName,
83
+ initialRunId,
84
+ autoResume = true,
85
+ followLatest = true
86
+ } = options;
80
87
  const [currentRunId, setCurrentRunId] = useState(
81
88
  initialRunId ?? null
82
89
  );
83
90
  const [isPending, setIsPending] = useState(false);
91
+ const hasUserTriggered = useRef(false);
84
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]);
85
158
  const trigger = useCallback(
86
159
  async (input) => {
160
+ hasUserTriggered.current = true;
87
161
  subscription.reset();
88
162
  setIsPending(true);
89
163
  const response = await fetch(`${api}/trigger`, {
@@ -163,7 +237,7 @@ function useJobLogs(options) {
163
237
  }
164
238
 
165
239
  // src/client/use-job-run.ts
166
- import { useEffect as useEffect2, useRef } from "react";
240
+ import { useEffect as useEffect2, useRef as useRef2 } from "react";
167
241
  function useJobRun(options) {
168
242
  const { api, runId, onStart, onComplete, onFail } = options;
169
243
  const subscription = useSSESubscription(api, runId);
@@ -173,7 +247,7 @@ function useJobRun(options) {
173
247
  const isPending = effectiveStatus === "pending";
174
248
  const isRunning = effectiveStatus === "running";
175
249
  const isCancelled = effectiveStatus === "cancelled";
176
- const prevStatusRef = useRef(null);
250
+ const prevStatusRef = useRef2(null);
177
251
  useEffect2(() => {
178
252
  const prevStatus = prevStatusRef.current;
179
253
  prevStatusRef.current = effectiveStatus;
@@ -249,16 +323,19 @@ function createJobHooks(options) {
249
323
  }
250
324
 
251
325
  // src/client/use-runs.ts
252
- import { useCallback as useCallback2, useEffect as useEffect3, useRef as useRef2, useState as useState2 } from "react";
253
- function useRuns(options) {
254
- const { api, jobName, status, pageSize = 10 } = options;
326
+ import { useCallback as useCallback2, useEffect as useEffect3, useRef as useRef3, useState as useState2 } from "react";
327
+ function useRuns(jobDefinitionOrOptions, optionsArg) {
328
+ const isJob = isJobDefinition(jobDefinitionOrOptions);
329
+ const jobName = isJob ? jobDefinitionOrOptions.name : jobDefinitionOrOptions.jobName;
330
+ const options = isJob ? optionsArg : jobDefinitionOrOptions;
331
+ const { api, status, pageSize = 10 } = options;
255
332
  const [runs, setRuns] = useState2([]);
256
333
  const [page, setPage] = useState2(0);
257
334
  const [hasMore, setHasMore] = useState2(false);
258
335
  const [isLoading, setIsLoading] = useState2(false);
259
336
  const [error, setError] = useState2(null);
260
- const isMountedRef = useRef2(true);
261
- const eventSourceRef = useRef2(null);
337
+ const isMountedRef = useRef3(true);
338
+ const eventSourceRef = useRef3(null);
262
339
  const refresh = useCallback2(async () => {
263
340
  setIsLoading(true);
264
341
  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 { useCallback, useEffect, useRef, useState } from 'react'\nimport type { Progress, RunStatus } from '../types'\n\n/**\n * Run type for client mode (matches server response)\n */\nexport interface ClientRun {\n id: string\n jobName: string\n status: RunStatus\n input: unknown\n output: unknown | null\n error: string | null\n currentStepIndex: number\n stepCount: number\n progress: Progress | null\n createdAt: string\n startedAt: string | null\n completedAt: string | null\n}\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 /**\n * List of runs for the current page\n */\n runs: ClientRun[]\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\n * ```tsx\n * function RunHistory() {\n * const { runs, page, hasMore, nextPage, prevPage, refresh } = useRuns({\n * api: '/api/durably',\n * jobName: 'import-csv',\n * pageSize: 10,\n * })\n *\n * return (\n * <div>\n * {runs.map(run => (\n * <div key={run.id}>{run.jobName}: {run.status}</div>\n * ))}\n * <button onClick={prevPage} disabled={page === 0}>Prev</button>\n * <button onClick={nextPage} disabled={!hasMore}>Next</button>\n * <button onClick={refresh}>Refresh</button>\n * </div>\n * )\n * }\n * ```\n */\nexport function useRuns(options: UseRunsClientOptions): UseRunsClientResult {\n const { api, jobName, status, pageSize = 10 } = options\n\n const [runs, setRuns] = useState<ClientRun[]>([])\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 ClientRun[]\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":";;;;;AAAA,SAAS,aAAa,WAAW,gBAAgB;;;ACAjD,SAAS,eAAe;;;ACOjB,SAAS,yBAAyB,YAAqC;AAC5E,SAAO;AAAA,IACL,UACE,OACA,SACY;AACZ,YAAM,MAAM,GAAG,UAAU,oBAAoB,mBAAmB,KAAK,CAAC;AACtE,YAAM,cAAc,IAAI,YAAY,GAAG;AAEvC,kBAAY,YAAY,CAAC,iBAAiB;AACxC,YAAI;AACF,gBAAM,OAAO,KAAK,MAAM,aAAa,IAAI;AACzC,cAAI,KAAK,UAAU,MAAO;AAE1B,kBAAQ,KAAK,MAAM;AAAA,YACjB,KAAK;AACH,sBAAQ,EAAE,MAAM,YAAY,CAAC;AAC7B;AAAA,YACF,KAAK;AACH,sBAAQ;AAAA,gBACN,MAAM;AAAA,gBACN,QAAQ,KAAK;AAAA,cACf,CAAC;AACD;AAAA,YACF,KAAK;AACH,sBAAQ,EAAE,MAAM,YAAY,OAAO,KAAK,MAAM,CAAC;AAC/C;AAAA,YACF,KAAK;AACH,sBAAQ,EAAE,MAAM,aAAa,CAAC;AAC9B;AAAA,YACF,KAAK;AACH,sBAAQ,EAAE,MAAM,YAAY,CAAC;AAC7B;AAAA,YACF,KAAK;AACH,sBAAQ,EAAE,MAAM,gBAAgB,UAAU,KAAK,SAAS,CAAC;AACzD;AAAA,YACF,KAAK;AACH,sBAAQ;AAAA,gBACN,MAAM;AAAA,gBACN,OAAO,KAAK;AAAA,gBACZ,UAAU;AAAA,gBACV,OAAO,KAAK;AAAA,gBACZ,SAAS,KAAK;AAAA,gBACd,MAAM,KAAK;AAAA,cACb,CAAC;AACD;AAAA,UACJ;AAAA,QACF,QAAQ;AAAA,QAER;AAAA,MACF;AAEA,kBAAY,UAAU,MAAM;AAC1B,gBAAQ,EAAE,MAAM,oBAAoB,OAAO,oBAAoB,CAAC;AAChE,oBAAY,MAAM;AAAA,MACpB;AAEA,aAAO,MAAM;AACX,oBAAY,MAAM;AAAA,MACpB;AAAA,IACF;AAAA,EACF;AACF;;;AD5CO,SAAS,mBACd,KACA,OACA,SACmC;AACnC,QAAM,aAAa;AAAA,IACjB,MAAO,MAAM,yBAAyB,GAAG,IAAI;AAAA,IAC7C,CAAC,GAAG;AAAA,EACN;AAEA,SAAO,gBAAyB,YAAY,OAAO,OAAO;AAC5D;;;AD8CO,SAAS,OAGd,SAAmE;AACnE,QAAM,EAAE,KAAK,SAAS,aAAa,IAAI;AAEvC,QAAM,CAAC,cAAc,eAAe,IAAI;AAAA,IACtC,gBAAgB;AAAA,EAClB;AACA,QAAM,CAAC,WAAW,YAAY,IAAI,SAAS,KAAK;AAEhD,QAAM,eAAe,mBAA4B,KAAK,YAAY;AAElE,QAAM,UAAU;AAAA,IACd,OAAO,UAA8C;AAEnD,mBAAa,MAAM;AACnB,mBAAa,IAAI;AAEjB,YAAM,WAAW,MAAM,MAAM,GAAG,GAAG,YAAY;AAAA,QAC7C,QAAQ;AAAA,QACR,SAAS;AAAA,UACP,gBAAgB;AAAA,QAClB;AAAA,QACA,MAAM,KAAK,UAAU,EAAE,SAAS,MAAM,CAAC;AAAA,MACzC,CAAC;AAED,UAAI,CAAC,SAAS,IAAI;AAChB,qBAAa,KAAK;AAClB,cAAM,YAAY,MAAM,SAAS,KAAK;AACtC,cAAM,IAAI,MAAM,aAAa,QAAQ,SAAS,MAAM,EAAE;AAAA,MACxD;AAEA,YAAM,EAAE,MAAM,IAAK,MAAM,SAAS,KAAK;AACvC,sBAAgB,KAAK;AAErB,aAAO,EAAE,MAAM;AAAA,IACjB;AAAA,IACA,CAAC,KAAK,SAAS,aAAa,KAAK;AAAA,EACnC;AAEA,QAAM,iBAAiB;AAAA,IACrB,OAAO,UAA+D;AACpE,YAAM,EAAE,MAAM,IAAI,MAAM,QAAQ,KAAK;AAErC,aAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACtC,cAAM,gBAAgB,YAAY,MAAM;AACtC,cAAI,aAAa,WAAW,eAAe,aAAa,QAAQ;AAC9D,0BAAc,aAAa;AAC3B,oBAAQ,EAAE,OAAO,QAAQ,aAAa,OAAO,CAAC;AAAA,UAChD,WAAW,aAAa,WAAW,UAAU;AAC3C,0BAAc,aAAa;AAC3B,mBAAO,IAAI,MAAM,aAAa,SAAS,YAAY,CAAC;AAAA,UACtD,WAAW,aAAa,WAAW,aAAa;AAC9C,0BAAc,aAAa;AAC3B,mBAAO,IAAI,MAAM,eAAe,CAAC;AAAA,UACnC;AAAA,QACF,GAAG,EAAE;AAAA,MACP,CAAC;AAAA,IACH;AAAA,IACA,CAAC,SAAS,aAAa,QAAQ,aAAa,QAAQ,aAAa,KAAK;AAAA,EACxE;AAEA,QAAM,QAAQ,YAAY,MAAM;AAC9B,iBAAa,MAAM;AACnB,oBAAgB,IAAI;AACpB,iBAAa,KAAK;AAAA,EACpB,GAAG,CAAC,aAAa,KAAK,CAAC;AAGvB,QAAM,kBAAkB,aAAa,WAAW,YAAY,YAAY;AAGxE,YAAU,MAAM;AACd,QAAI,aAAa,UAAU,WAAW;AACpC,mBAAa,KAAK;AAAA,IACpB;AAAA,EACF,GAAG,CAAC,aAAa,QAAQ,SAAS,CAAC;AAEnC,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA,QAAQ;AAAA,IACR,QAAQ,aAAa;AAAA,IACrB,OAAO,aAAa;AAAA,IACpB,MAAM,aAAa;AAAA,IACnB,UAAU,aAAa;AAAA,IACvB,WAAW,oBAAoB;AAAA,IAC/B,WAAW,oBAAoB;AAAA,IAC/B,aAAa,oBAAoB;AAAA,IACjC,UAAU,oBAAoB;AAAA,IAC9B;AAAA,IACA;AAAA,EACF;AACF;;;AG5IO,SAAS,WACd,SACwB;AACxB,QAAM,EAAE,KAAK,OAAO,QAAQ,IAAI;AAEhC,QAAM,eAAe,mBAAmB,KAAK,OAAO,EAAE,QAAQ,CAAC;AAE/D,SAAO;AAAA,IACL,MAAM,aAAa;AAAA,IACnB,WAAW,aAAa;AAAA,EAC1B;AACF;;;AC/CA,SAAS,aAAAA,YAAW,cAAc;AA6E3B,SAAS,UACd,SACgC;AAChC,QAAM,EAAE,KAAK,OAAO,SAAS,YAAY,OAAO,IAAI;AAEpD,QAAM,eAAe,mBAA4B,KAAK,KAAK;AAG3D,QAAM,kBAAkB,aAAa,WAAW,QAAQ,YAAY;AAEpE,QAAM,cAAc,oBAAoB;AACxC,QAAM,WAAW,oBAAoB;AACrC,QAAM,YAAY,oBAAoB;AACtC,QAAM,YAAY,oBAAoB;AACtC,QAAM,cAAc,oBAAoB;AAGxC,QAAM,gBAAgB,OAAyB,IAAI;AAEnD,EAAAC,WAAU,MAAM;AACd,UAAM,aAAa,cAAc;AACjC,kBAAc,UAAU;AAGxB,QAAI,eAAe,iBAAiB;AAElC,UAAI,eAAe,SAAS,aAAa,cAAc,SAAS;AAC9D,gBAAQ;AAAA,MACV;AACA,UAAI,eAAe,YAAY;AAC7B,mBAAW;AAAA,MACb;AACA,UAAI,YAAY,QAAQ;AACtB,eAAO;AAAA,MACT;AAAA,IACF;AAAA,EACF,GAAG;AAAA,IACD;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF,CAAC;AAED,SAAO;AAAA,IACL,QAAQ;AAAA,IACR,QAAQ,aAAa;AAAA,IACrB,OAAO,aAAa;AAAA,IACpB,MAAM,aAAa;AAAA,IACnB,UAAU,aAAa;AAAA,IACvB;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;;;AC1DO,SAAS,oBACd,SACsB;AACtB,QAAM,EAAE,IAAI,IAAI;AAGhB,SAAO,IAAI,MAAM,CAAC,GAA2B;AAAA,IAC3C,IAAI,SAAS,QAAgB;AAC3B,aAAO;AAAA,QACL,QAAQ,MAAM;AACZ,iBAAO,OAAO,EAAE,KAAK,SAAS,OAAO,CAAC;AAAA,QACxC;AAAA,QAEA,QAAQ,CAAC,UAAyB;AAChC,iBAAO,UAAU,EAAE,KAAK,MAAM,CAAC;AAAA,QACjC;AAAA,QAEA,SAAS,CAAC,OAAsB,gBAAuC;AACrE,iBAAO,WAAW,EAAE,KAAK,OAAO,SAAS,aAAa,QAAQ,CAAC;AAAA,QACjE;AAAA,MACF;AAAA,IACF;AAAA,EACF,CAAC;AACH;;;AChCO,SAAS,eAId,SAC+C;AAC/C,QAAM,EAAE,KAAK,QAAQ,IAAI;AAEzB,SAAO;AAAA,IACL,QAAQ,MAAM;AACZ,aAAO,OAA4C,EAAE,KAAK,QAAQ,CAAC;AAAA,IACrE;AAAA,IAEA,QAAQ,CAAC,UAAyB;AAChC,aAAO,UAA6B,EAAE,KAAK,MAAM,CAAC;AAAA,IACpD;AAAA,IAEA,SAAS,CAAC,OAAsB,gBAAuC;AACrE,aAAO,WAAW,EAAE,KAAK,OAAO,SAAS,aAAa,QAAQ,CAAC;AAAA,IACjE;AAAA,EACF;AACF;;;AC1FA,SAAS,eAAAC,cAAa,aAAAC,YAAW,UAAAC,SAAQ,YAAAC,iBAAgB;AAmJlD,SAAS,QAAQ,SAAoD;AAC1E,QAAM,EAAE,KAAK,SAAS,QAAQ,WAAW,GAAG,IAAI;AAEhD,QAAM,CAAC,MAAM,OAAO,IAAIA,UAAsB,CAAC,CAAC;AAChD,QAAM,CAAC,MAAM,OAAO,IAAIA,UAAS,CAAC;AAClC,QAAM,CAAC,SAAS,UAAU,IAAIA,UAAS,KAAK;AAC5C,QAAM,CAAC,WAAW,YAAY,IAAIA,UAAS,KAAK;AAChD,QAAM,CAAC,OAAO,QAAQ,IAAIA,UAAwB,IAAI;AAEtD,QAAM,eAAeD,QAAO,IAAI;AAChC,QAAM,iBAAiBA,QAA2B,IAAI;AAEtD,QAAM,UAAUF,aAAY,YAAY;AACtC,iBAAa,IAAI;AACjB,aAAS,IAAI;AAEb,QAAI;AACF,YAAM,SAAS,IAAI,gBAAgB;AACnC,UAAI,QAAS,QAAO,IAAI,WAAW,OAAO;AAC1C,UAAI,OAAQ,QAAO,IAAI,UAAU,MAAM;AACvC,aAAO,IAAI,SAAS,OAAO,WAAW,CAAC,CAAC;AACxC,aAAO,IAAI,UAAU,OAAO,OAAO,QAAQ,CAAC;AAE5C,YAAM,MAAM,GAAG,GAAG,SAAS,OAAO,SAAS,CAAC;AAC5C,YAAM,WAAW,MAAM,MAAM,GAAG;AAEhC,UAAI,CAAC,SAAS,IAAI;AAChB,cAAM,IAAI,MAAM,yBAAyB,SAAS,UAAU,EAAE;AAAA,MAChE;AAEA,YAAM,OAAQ,MAAM,SAAS,KAAK;AAElC,UAAI,aAAa,SAAS;AACxB,mBAAW,KAAK,SAAS,QAAQ;AACjC,gBAAQ,KAAK,MAAM,GAAG,QAAQ,CAAC;AAAA,MACjC;AAAA,IACF,SAAS,KAAK;AACZ,UAAI,aAAa,SAAS;AACxB,iBAAS,eAAe,QAAQ,IAAI,UAAU,eAAe;AAAA,MAC/D;AAAA,IACF,UAAE;AACA,UAAI,aAAa,SAAS;AACxB,qBAAa,KAAK;AAAA,MACpB;AAAA,IACF;AAAA,EACF,GAAG,CAAC,KAAK,SAAS,QAAQ,UAAU,IAAI,CAAC;AAGzC,EAAAC,WAAU,MAAM;AACd,iBAAa,UAAU;AACvB,YAAQ;AAER,WAAO,MAAM;AACX,mBAAa,UAAU;AAAA,IACzB;AAAA,EACF,GAAG,CAAC,OAAO,CAAC;AAGZ,EAAAA,WAAU,MAAM;AAEd,QAAI,SAAS,GAAG;AAEd,UAAI,eAAe,SAAS;AAC1B,uBAAe,QAAQ,MAAM;AAC7B,uBAAe,UAAU;AAAA,MAC3B;AACA;AAAA,IACF;AAGA,UAAM,SAAS,IAAI,gBAAgB;AACnC,QAAI,QAAS,QAAO,IAAI,WAAW,OAAO;AAC1C,UAAM,SAAS,GAAG,GAAG,kBAAkB,OAAO,SAAS,IAAI,IAAI,OAAO,SAAS,CAAC,KAAK,EAAE;AAEvF,UAAM,cAAc,IAAI,YAAY,MAAM;AAC1C,mBAAe,UAAU;AAEzB,gBAAY,YAAY,CAAC,UAAU;AACjC,UAAI;AACF,cAAM,OAAO,KAAK,MAAM,MAAM,IAAI;AAElC,YACE,KAAK,SAAS,iBACd,KAAK,SAAS,eACd,KAAK,SAAS,kBACd,KAAK,SAAS,cACd,KAAK,SAAS,gBACd,KAAK,SAAS,aACd;AACA,kBAAQ;AAAA,QACV;AAEA,YAAI,KAAK,SAAS,gBAAgB;AAChC;AAAA,YAAQ,CAAC,SACP,KAAK;AAAA,cAAI,CAAC,QACR,IAAI,OAAO,KAAK,QAAQ,EAAE,GAAG,KAAK,UAAU,KAAK,SAAS,IAAI;AAAA,YAChE;AAAA,UACF;AAAA,QACF;AAEA,YAAI,KAAK,SAAS,iBAAiB;AACjC;AAAA,YAAQ,CAAC,SACP,KAAK;AAAA,cAAI,CAAC,QACR,IAAI,OAAO,KAAK,QACZ,EAAE,GAAG,KAAK,kBAAkB,KAAK,YAAY,EAAE,IAC/C;AAAA,YACN;AAAA,UACF;AAAA,QACF;AAEA,YAAI,KAAK,SAAS,gBAAgB,KAAK,SAAS,aAAa;AAC3D,kBAAQ;AAAA,QACV;AAAA,MAEF,QAAQ;AAAA,MAER;AAAA,IACF;AAEA,gBAAY,UAAU,MAAM;AAAA,IAE5B;AAEA,WAAO,MAAM;AACX,kBAAY,MAAM;AAClB,qBAAe,UAAU;AAAA,IAC3B;AAAA,EACF,GAAG,CAAC,KAAK,SAAS,MAAM,OAAO,CAAC;AAEhC,QAAM,WAAWD,aAAY,MAAM;AACjC,QAAI,SAAS;AACX,cAAQ,CAAC,MAAM,IAAI,CAAC;AAAA,IACtB;AAAA,EACF,GAAG,CAAC,OAAO,CAAC;AAEZ,QAAM,WAAWA,aAAY,MAAM;AACjC,YAAQ,CAAC,MAAM,KAAK,IAAI,GAAG,IAAI,CAAC,CAAC;AAAA,EACnC,GAAG,CAAC,CAAC;AAEL,QAAM,WAAWA,aAAY,CAAC,YAAoB;AAChD,YAAQ,KAAK,IAAI,GAAG,OAAO,CAAC;AAAA,EAC9B,GAAG,CAAC,CAAC;AAEL,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;;;AC7SA,SAAS,eAAAI,cAAa,YAAAC,iBAAgB;AA+F/B,SAAS,cACd,SAC2B;AAC3B,QAAM,EAAE,IAAI,IAAI;AAEhB,QAAM,CAAC,WAAW,YAAY,IAAIA,UAAS,KAAK;AAChD,QAAM,CAAC,OAAO,QAAQ,IAAIA,UAAwB,IAAI;AAEtD,QAAM,QAAQD;AAAA,IACZ,OAAO,UAAkB;AACvB,mBAAa,IAAI;AACjB,eAAS,IAAI;AAEb,UAAI;AACF,cAAM,MAAM,GAAG,GAAG,gBAAgB,mBAAmB,KAAK,CAAC;AAC3D,cAAM,WAAW,MAAM,MAAM,KAAK,EAAE,QAAQ,OAAO,CAAC;AAEpD,YAAI,CAAC,SAAS,IAAI;AAChB,cAAI,eAAe,oBAAoB,SAAS,UAAU;AAC1D,cAAI;AACF,kBAAM,OAAO,MAAM,SAAS,KAAK;AACjC,gBAAI,KAAK,OAAO;AACd,6BAAe,KAAK;AAAA,YACtB;AAAA,UACF,QAAQ;AAAA,UAER;AACA,gBAAM,IAAI,MAAM,YAAY;AAAA,QAC9B;AAAA,MACF,SAAS,KAAK;AACZ,cAAM,UAAU,eAAe,QAAQ,IAAI,UAAU;AACrD,iBAAS,OAAO;AAChB,cAAM;AAAA,MACR,UAAE;AACA,qBAAa,KAAK;AAAA,MACpB;AAAA,IACF;AAAA,IACA,CAAC,GAAG;AAAA,EACN;AAEA,QAAM,SAASA;AAAA,IACb,OAAO,UAAkB;AACvB,mBAAa,IAAI;AACjB,eAAS,IAAI;AAEb,UAAI;AACF,cAAM,MAAM,GAAG,GAAG,iBAAiB,mBAAmB,KAAK,CAAC;AAC5D,cAAM,WAAW,MAAM,MAAM,KAAK,EAAE,QAAQ,OAAO,CAAC;AAEpD,YAAI,CAAC,SAAS,IAAI;AAChB,cAAI,eAAe,qBAAqB,SAAS,UAAU;AAC3D,cAAI;AACF,kBAAM,OAAO,MAAM,SAAS,KAAK;AACjC,gBAAI,KAAK,OAAO;AACd,6BAAe,KAAK;AAAA,YACtB;AAAA,UACF,QAAQ;AAAA,UAER;AACA,gBAAM,IAAI,MAAM,YAAY;AAAA,QAC9B;AAAA,MACF,SAAS,KAAK;AACZ,cAAM,UAAU,eAAe,QAAQ,IAAI,UAAU;AACrD,iBAAS,OAAO;AAChB,cAAM;AAAA,MACR,UAAE;AACA,qBAAa,KAAK;AAAA,MACpB;AAAA,IACF;AAAA,IACA,CAAC,GAAG;AAAA,EACN;AAEA,QAAM,YAAYA;AAAA,IAChB,OAAO,UAAkB;AACvB,mBAAa,IAAI;AACjB,eAAS,IAAI;AAEb,UAAI;AACF,cAAM,MAAM,GAAG,GAAG,cAAc,mBAAmB,KAAK,CAAC;AACzD,cAAM,WAAW,MAAM,MAAM,KAAK,EAAE,QAAQ,SAAS,CAAC;AAEtD,YAAI,CAAC,SAAS,IAAI;AAChB,cAAI,eAAe,qBAAqB,SAAS,UAAU;AAC3D,cAAI;AACF,kBAAM,OAAO,MAAM,SAAS,KAAK;AACjC,gBAAI,KAAK,OAAO;AACd,6BAAe,KAAK;AAAA,YACtB;AAAA,UACF,QAAQ;AAAA,UAER;AACA,gBAAM,IAAI,MAAM,YAAY;AAAA,QAC9B;AAAA,MACF,SAAS,KAAK;AACZ,cAAM,UAAU,eAAe,QAAQ,IAAI,UAAU;AACrD,iBAAS,OAAO;AAChB,cAAM;AAAA,MACR,UAAE;AACA,qBAAa,KAAK;AAAA,MACpB;AAAA,IACF;AAAA,IACA,CAAC,GAAG;AAAA,EACN;AAEA,QAAM,SAASA;AAAA,IACb,OAAO,UAA6C;AAClD,mBAAa,IAAI;AACjB,eAAS,IAAI;AAEb,UAAI;AACF,cAAM,MAAM,GAAG,GAAG,cAAc,mBAAmB,KAAK,CAAC;AACzD,cAAM,WAAW,MAAM,MAAM,GAAG;AAEhC,YAAI,SAAS,WAAW,KAAK;AAC3B,iBAAO;AAAA,QACT;AAEA,YAAI,CAAC,SAAS,IAAI;AAChB,cAAI,eAAe,sBAAsB,SAAS,UAAU;AAC5D,cAAI;AACF,kBAAM,OAAO,MAAM,SAAS,KAAK;AACjC,gBAAI,KAAK,OAAO;AACd,6BAAe,KAAK;AAAA,YACtB;AAAA,UACF,QAAQ;AAAA,UAER;AACA,gBAAM,IAAI,MAAM,YAAY;AAAA,QAC9B;AAEA,eAAQ,MAAM,SAAS,KAAK;AAAA,MAC9B,SAAS,KAAK;AACZ,cAAM,UAAU,eAAe,QAAQ,IAAI,UAAU;AACrD,iBAAS,OAAO;AAChB,cAAM;AAAA,MACR,UAAE;AACA,qBAAa,KAAK;AAAA,MACpB;AAAA,IACF;AAAA,IACA,CAAC,GAAG;AAAA,EACN;AAEA,QAAM,WAAWA;AAAA,IACf,OAAO,UAAyC;AAC9C,mBAAa,IAAI;AACjB,eAAS,IAAI;AAEb,UAAI;AACF,cAAM,MAAM,GAAG,GAAG,gBAAgB,mBAAmB,KAAK,CAAC;AAC3D,cAAM,WAAW,MAAM,MAAM,GAAG;AAEhC,YAAI,CAAC,SAAS,IAAI;AAChB,cAAI,eAAe,wBAAwB,SAAS,UAAU;AAC9D,cAAI;AACF,kBAAM,OAAO,MAAM,SAAS,KAAK;AACjC,gBAAI,KAAK,OAAO;AACd,6BAAe,KAAK;AAAA,YACtB;AAAA,UACF,QAAQ;AAAA,UAER;AACA,gBAAM,IAAI,MAAM,YAAY;AAAA,QAC9B;AAEA,eAAQ,MAAM,SAAS,KAAK;AAAA,MAC9B,SAAS,KAAK;AACZ,cAAM,UAAU,eAAe,QAAQ,IAAI,UAAU;AACrD,iBAAS,OAAO;AAChB,cAAM;AAAA,MACR,UAAE;AACA,qBAAa,KAAK;AAAA,MACpB;AAAA,IACF;AAAA,IACA,CAAC,GAAG;AAAA,EACN;AAEA,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;","names":["useEffect","useEffect","useCallback","useEffect","useRef","useState","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 * 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 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/dist/index.d.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
- import { Durably, JobDefinition, Run } from '@coji/durably';
2
+ import { Durably, JobDefinition } from '@coji/durably';
3
3
  import { ReactNode } from 'react';
4
- import { R as RunStatus, L as LogEntry, P as Progress } from './types-C7h7nZv3.js';
5
- export { D as DurablyEvent } from './types-C7h7nZv3.js';
4
+ import { R as RunStatus, L as LogEntry, P as Progress, T as TypedRun } from './types-DY4Y6ggJ.js';
5
+ export { D as DurablyEvent } from './types-DY4Y6ggJ.js';
6
6
 
7
7
  interface DurablyContextValue {
8
8
  durably: Durably;
@@ -223,11 +223,11 @@ interface UseRunsOptions {
223
223
  */
224
224
  realtime?: boolean;
225
225
  }
226
- interface UseRunsResult {
226
+ interface UseRunsResult<TInput extends Record<string, unknown> = Record<string, unknown>, TOutput extends Record<string, unknown> | undefined = Record<string, unknown> | undefined> {
227
227
  /**
228
228
  * List of runs for the current page
229
229
  */
230
- runs: Run[];
230
+ runs: TypedRun<TInput, TOutput>[];
231
231
  /**
232
232
  * Current page (0-indexed)
233
233
  */
@@ -260,25 +260,37 @@ interface UseRunsResult {
260
260
  /**
261
261
  * Hook for listing runs with pagination and real-time updates.
262
262
  *
263
- * @example
263
+ * @example With generic type parameter (dashboard with multiple job types)
264
264
  * ```tsx
265
+ * type DashboardRun = TypedRun<ImportInput, ImportOutput> | TypedRun<SyncInput, SyncOutput>
266
+ *
265
267
  * function Dashboard() {
266
- * const { runs, page, hasMore, nextPage, prevPage, isLoading } = useRuns({
267
- * pageSize: 20,
268
- * })
268
+ * const { runs } = useRuns<DashboardRun>({ pageSize: 10 })
269
+ * // runs are typed as DashboardRun[]
270
+ * }
271
+ * ```
269
272
  *
270
- * return (
271
- * <div>
272
- * {runs.map(run => (
273
- * <div key={run.id}>{run.jobName}: {run.status}</div>
274
- * ))}
275
- * <button onClick={prevPage} disabled={page === 0}>Prev</button>
276
- * <button onClick={nextPage} disabled={!hasMore}>Next</button>
277
- * </div>
278
- * )
273
+ * @example With JobDefinition (single job, auto-filters by jobName)
274
+ * ```tsx
275
+ * const myJob = defineJob({ name: 'my-job', ... })
276
+ *
277
+ * function Dashboard() {
278
+ * const { runs } = useRuns(myJob)
279
+ * // runs[0].output is typed!
280
+ * return <div>{runs[0]?.output?.someField}</div>
281
+ * }
282
+ * ```
283
+ *
284
+ * @example With options only (untyped)
285
+ * ```tsx
286
+ * function Dashboard() {
287
+ * const { runs } = useRuns({ pageSize: 20 })
288
+ * // runs[0].output is unknown
279
289
  * }
280
290
  * ```
281
291
  */
292
+ declare function useRuns<TRun extends TypedRun<Record<string, unknown>, Record<string, unknown> | undefined>>(options?: UseRunsOptions): UseRunsResult<TRun extends TypedRun<infer I, infer _O> ? I : Record<string, unknown>, TRun extends TypedRun<infer _I, infer O> ? O : Record<string, unknown>>;
293
+ declare function useRuns<TName extends string, TInput extends Record<string, unknown>, TOutput extends Record<string, unknown> | undefined>(jobDefinition: JobDefinition<TName, TInput, TOutput>, options?: Omit<UseRunsOptions, 'jobName'>): UseRunsResult<TInput, TOutput>;
282
294
  declare function useRuns(options?: UseRunsOptions): UseRunsResult;
283
295
 
284
- export { DurablyProvider, type DurablyProviderProps, LogEntry, Progress, RunStatus, type UseJobLogsOptions, type UseJobLogsResult, type UseJobOptions, type UseJobResult, type UseJobRunOptions, type UseJobRunResult, type UseRunsOptions, type UseRunsResult, useDurably, useJob, useJobLogs, useJobRun, useRuns };
296
+ export { DurablyProvider, type DurablyProviderProps, LogEntry, Progress, RunStatus, TypedRun, type UseJobLogsOptions, type UseJobLogsResult, type UseJobOptions, type UseJobResult, type UseJobRunOptions, type UseJobRunResult, type UseRunsOptions, type UseRunsResult, useDurably, useJob, useJobLogs, useJobRun, useRuns };
package/dist/index.js CHANGED
@@ -1,8 +1,9 @@
1
1
  import {
2
2
  initialSubscriptionState,
3
+ isJobDefinition,
3
4
  subscriptionReducer,
4
5
  useSubscription
5
- } from "./chunk-M3YA2EA4.js";
6
+ } from "./chunk-42AVE35N.js";
6
7
 
7
8
  // src/context.tsx
8
9
  import { Suspense, createContext, use, useContext } from "react";
@@ -227,13 +228,7 @@ function useJob(jobDefinition, options) {
227
228
  );
228
229
  useEffect3(() => {
229
230
  if (!durably || !options?.initialRunId) return;
230
- const jobHandle = jobHandleRef.current;
231
- if (!jobHandle) return;
232
231
  subscription.setCurrentRunId(options.initialRunId);
233
- jobHandle.getRun(options.initialRunId).then((run) => {
234
- if (run) {
235
- }
236
- });
237
232
  }, [durably, options?.initialRunId, subscription.setCurrentRunId]);
238
233
  const trigger = useCallback2(
239
234
  async (input) => {
@@ -410,10 +405,14 @@ function useJobRun(options) {
410
405
 
411
406
  // src/hooks/use-runs.ts
412
407
  import { useCallback as useCallback3, useEffect as useEffect5, useState } from "react";
413
- function useRuns(options) {
408
+ function useRuns(jobDefinitionOrOptions, optionsArg) {
414
409
  const { durably } = useDurably();
410
+ const isJob = isJobDefinition(jobDefinitionOrOptions);
411
+ const jobName = isJob ? jobDefinitionOrOptions.name : jobDefinitionOrOptions?.jobName;
412
+ const options = isJob ? optionsArg : jobDefinitionOrOptions;
415
413
  const pageSize = options?.pageSize ?? 10;
416
414
  const realtime = options?.realtime ?? true;
415
+ const status = options?.status;
417
416
  const [runs, setRuns] = useState([]);
418
417
  const [page, setPage] = useState(0);
419
418
  const [hasMore, setHasMore] = useState(false);
@@ -423,8 +422,8 @@ function useRuns(options) {
423
422
  setIsLoading(true);
424
423
  try {
425
424
  const data = await durably.getRuns({
426
- jobName: options?.jobName,
427
- status: options?.status,
425
+ jobName,
426
+ status,
428
427
  limit: pageSize + 1,
429
428
  offset: page * pageSize
430
429
  });
@@ -433,7 +432,7 @@ function useRuns(options) {
433
432
  } finally {
434
433
  setIsLoading(false);
435
434
  }
436
- }, [durably, options?.jobName, options?.status, pageSize, page]);
435
+ }, [durably, jobName, status, pageSize, page]);
437
436
  useEffect5(() => {
438
437
  if (!durably) return;
439
438
  refresh();
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/context.tsx","../src/hooks/use-job.ts","../src/hooks/use-auto-resume.ts","../src/hooks/use-job-subscription.ts","../src/hooks/use-run-subscription.ts","../src/shared/durably-event-subscriber.ts","../src/hooks/use-job-logs.ts","../src/hooks/use-job-run.ts","../src/hooks/use-runs.ts"],"sourcesContent":["import type { Durably } from '@coji/durably'\nimport { Suspense, createContext, use, useContext, type ReactNode } from 'react'\n\ninterface DurablyContextValue {\n durably: Durably\n}\n\nconst DurablyContext = createContext<DurablyContextValue | null>(null)\n\nexport interface DurablyProviderProps {\n /**\n * Durably instance or Promise that resolves to one.\n * The instance should already be initialized via `await durably.init()`.\n *\n * When passing a Promise, wrap the provider with Suspense or use the fallback prop.\n *\n * @example\n * // With Suspense (recommended)\n * <Suspense fallback={<Loading />}>\n * <DurablyProvider durably={durablyPromise}>\n * <App />\n * </DurablyProvider>\n * </Suspense>\n *\n * @example\n * // With fallback prop\n * <DurablyProvider durably={durablyPromise} fallback={<Loading />}>\n * <App />\n * </DurablyProvider>\n */\n durably: Durably | Promise<Durably>\n /**\n * Fallback to show while waiting for the Durably Promise to resolve.\n * This wraps the provider content in a Suspense boundary automatically.\n */\n fallback?: ReactNode\n children: ReactNode\n}\n\n/**\n * Internal component that uses the `use()` hook to resolve the Promise\n */\nfunction DurablyProviderInner({\n durably: durablyOrPromise,\n children,\n}: Omit<DurablyProviderProps, 'fallback'>) {\n const durably =\n durablyOrPromise instanceof Promise\n ? use(durablyOrPromise)\n : durablyOrPromise\n\n return (\n <DurablyContext.Provider value={{ durably }}>\n {children}\n </DurablyContext.Provider>\n )\n}\n\nexport function DurablyProvider({\n durably,\n fallback,\n children,\n}: DurablyProviderProps) {\n const inner = (\n <DurablyProviderInner durably={durably}>{children}</DurablyProviderInner>\n )\n\n if (fallback !== undefined) {\n return <Suspense fallback={fallback}>{inner}</Suspense>\n }\n\n return inner\n}\n\nexport function useDurably(): DurablyContextValue {\n const context = useContext(DurablyContext)\n if (!context) {\n throw new Error('useDurably must be used within a DurablyProvider')\n }\n return context\n}\n","import type { JobDefinition, JobHandle } from '@coji/durably'\nimport { useCallback, useEffect, useMemo, useRef } from 'react'\nimport { useDurably } from '../context'\nimport type { LogEntry, Progress, RunStatus } from '../types'\nimport { useAutoResume } from './use-auto-resume'\nimport { useJobSubscription } from './use-job-subscription'\n\nexport interface UseJobOptions {\n /**\n * Initial Run ID to subscribe to (for reconnection scenarios)\n */\n initialRunId?: string\n /**\n * Automatically resume tracking any pending or running job on initialization.\n * If a pending or running run exists for this job, the hook will subscribe to it.\n * @default true\n */\n autoResume?: boolean\n /**\n * Automatically switch to tracking the latest running job when a new run starts.\n * When true, the hook will update to track any new run for this job as soon as it starts running.\n * When false, the hook will only track the run that was triggered or explicitly set.\n * @default true\n */\n followLatest?: boolean\n}\n\nexport interface UseJobResult<TInput, TOutput> {\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\nexport function useJob<\n TName extends string,\n TInput extends Record<string, unknown>,\n // biome-ignore lint/suspicious/noConfusingVoidType: TOutput can be void for jobs without return value\n TOutput extends Record<string, unknown> | void,\n>(\n jobDefinition: JobDefinition<TName, TInput, TOutput>,\n options?: UseJobOptions,\n): UseJobResult<TInput, TOutput> {\n const { durably } = useDurably()\n\n const jobHandleRef = useRef<JobHandle<TName, TInput, TOutput> | null>(null)\n\n // Register job\n useEffect(() => {\n if (!durably) return\n\n const d = durably.register({\n _job: jobDefinition,\n })\n jobHandleRef.current = d.jobs._job\n }, [durably, jobDefinition])\n\n // Use the extracted job subscription hook\n const subscription = useJobSubscription<TOutput>(\n durably,\n jobDefinition.name,\n {\n followLatest: options?.followLatest,\n },\n )\n\n // Auto-resume callbacks - stable reference\n const autoResumeCallbacks = useMemo(\n () => ({\n onRunFound: (runId: string, _status: RunStatus) => {\n subscription.setCurrentRunId(runId)\n },\n }),\n [subscription.setCurrentRunId],\n )\n\n // Use the extracted auto-resume hook\n useAutoResume(\n jobHandleRef.current,\n {\n enabled: options?.autoResume,\n initialRunId: options?.initialRunId,\n },\n autoResumeCallbacks,\n )\n\n // Handle initialRunId - set it and fetch current state\n useEffect(() => {\n if (!durably || !options?.initialRunId) return\n\n const jobHandle = jobHandleRef.current\n if (!jobHandle) return\n\n subscription.setCurrentRunId(options.initialRunId)\n\n // Fetch initial state for the run\n jobHandle.getRun(options.initialRunId).then((run) => {\n if (run) {\n // State will be updated via subscription events or we could\n // dispatch initial state here if needed\n }\n })\n }, [durably, options?.initialRunId, subscription.setCurrentRunId])\n\n const trigger = useCallback(\n async (input: TInput): Promise<{ runId: string }> => {\n const jobHandle = jobHandleRef.current\n if (!jobHandle) {\n throw new Error('Job not ready')\n }\n\n // Reset state before triggering\n subscription.reset()\n\n const run = await jobHandle.trigger(input)\n subscription.setCurrentRunId(run.id)\n\n return { runId: run.id }\n },\n [subscription],\n )\n\n const triggerAndWait = useCallback(\n async (input: TInput): Promise<{ runId: string; output: TOutput }> => {\n const jobHandle = jobHandleRef.current\n if (!jobHandle || !durably) {\n throw new Error('Job not ready')\n }\n\n // Reset state before triggering\n subscription.reset()\n\n const run = await jobHandle.trigger(input)\n subscription.setCurrentRunId(run.id)\n\n // Wait for completion by polling\n return new Promise((resolve, reject) => {\n const checkCompletion = async () => {\n const updatedRun = await jobHandle.getRun(run.id)\n if (!updatedRun) {\n reject(new Error('Run not found'))\n return\n }\n\n if (updatedRun.status === 'completed') {\n resolve({ runId: run.id, output: updatedRun.output as TOutput })\n } else if (updatedRun.status === 'failed') {\n reject(new Error(updatedRun.error ?? 'Job failed'))\n } else if (updatedRun.status === 'cancelled') {\n reject(new Error('Job cancelled'))\n } else {\n // Still running, check again\n setTimeout(checkCompletion, 50)\n }\n }\n checkCompletion()\n })\n },\n [durably, subscription],\n )\n\n return {\n trigger,\n triggerAndWait,\n status: subscription.status,\n output: subscription.output,\n error: subscription.error,\n logs: subscription.logs,\n progress: subscription.progress,\n isRunning: subscription.status === 'running',\n isPending: subscription.status === 'pending',\n isCompleted: subscription.status === 'completed',\n isFailed: subscription.status === 'failed',\n isCancelled: subscription.status === 'cancelled',\n currentRunId: subscription.currentRunId,\n reset: subscription.reset,\n }\n}\n","import type { JobHandle } from '@coji/durably'\nimport { useEffect } from 'react'\nimport type { RunStatus } from '../types'\n\nexport interface UseAutoResumeOptions {\n /**\n * Whether to automatically resume tracking pending/running runs\n * @default true\n */\n enabled?: boolean\n /**\n * Skip auto-resume if an initial run ID is provided\n */\n skipIfInitialRunId?: boolean\n /**\n * Initial run ID (if provided, auto-resume is skipped)\n */\n initialRunId?: string\n}\n\nexport interface UseAutoResumeCallbacks {\n /**\n * Called when a run is found to resume\n */\n onRunFound: (runId: string, status: RunStatus) => void\n}\n\n/**\n * Hook that automatically finds and resumes tracking of pending/running runs.\n * Extracted from useJob to separate the auto-resume concern.\n */\nexport function useAutoResume<\n TName extends string,\n TInput extends Record<string, unknown>,\n TOutput,\n>(\n jobHandle: JobHandle<TName, TInput, TOutput> | null,\n options: UseAutoResumeOptions,\n callbacks: UseAutoResumeCallbacks,\n): void {\n const enabled = options.enabled !== false\n const skipIfInitialRunId = options.skipIfInitialRunId !== false\n const initialRunId = options.initialRunId\n\n useEffect(() => {\n if (!jobHandle) return\n if (!enabled) return\n if (skipIfInitialRunId && initialRunId) return\n\n let cancelled = false\n\n const findActiveRun = async () => {\n // First check for running runs\n const runningRuns = await jobHandle.getRuns({ status: 'running' })\n if (cancelled) return\n\n if (runningRuns.length > 0) {\n const run = runningRuns[0]\n callbacks.onRunFound(run.id, run.status as RunStatus)\n return\n }\n\n // Then check for pending runs\n const pendingRuns = await jobHandle.getRuns({ status: 'pending' })\n if (cancelled) return\n\n if (pendingRuns.length > 0) {\n const run = pendingRuns[0]\n callbacks.onRunFound(run.id, run.status as RunStatus)\n }\n }\n\n findActiveRun()\n\n return () => {\n cancelled = true\n }\n }, [jobHandle, enabled, skipIfInitialRunId, initialRunId, callbacks])\n}\n","import type { Durably } from '@coji/durably'\nimport { useCallback, useEffect, useReducer, useRef } from 'react'\nimport {\n initialSubscriptionState,\n subscriptionReducer,\n type SubscriptionAction,\n} from '../shared/subscription-reducer'\nimport type { SubscriptionState } from '../types'\n\nexport interface UseJobSubscriptionOptions {\n /**\n * Automatically switch to tracking the latest running job when a new run starts.\n * @default true\n */\n followLatest?: boolean\n /**\n * Maximum number of logs to keep (0 = unlimited)\n */\n maxLogs?: number\n}\n\nexport interface UseJobSubscriptionResult<\n TOutput = unknown,\n> extends SubscriptionState<TOutput> {\n /**\n * Current run ID being tracked\n */\n currentRunId: string | null\n /**\n * Set the current run ID to track\n */\n setCurrentRunId: (runId: string | null) => void\n /**\n * Clear all logs\n */\n clearLogs: () => void\n /**\n * Reset all state including currentRunId\n */\n reset: () => void\n}\n\n// Extended state for job subscription (includes currentRunId)\ninterface JobSubscriptionState<\n TOutput = unknown,\n> extends SubscriptionState<TOutput> {\n currentRunId: string | null\n}\n\n// Extended actions for job subscription\ntype JobSubscriptionAction<TOutput = unknown> =\n | SubscriptionAction<TOutput>\n | { type: 'set_run_id'; runId: string | null }\n | {\n type: 'switch_to_run'\n runId: string\n }\n\nfunction jobSubscriptionReducer<TOutput = unknown>(\n state: JobSubscriptionState<TOutput>,\n action: JobSubscriptionAction<TOutput>,\n): JobSubscriptionState<TOutput> {\n switch (action.type) {\n case 'set_run_id':\n return { ...state, currentRunId: action.runId }\n\n case 'switch_to_run':\n // Switch to a new run, resetting state\n return {\n ...initialSubscriptionState,\n currentRunId: action.runId,\n status: 'running',\n } as JobSubscriptionState<TOutput>\n\n case 'reset':\n return {\n ...(initialSubscriptionState as SubscriptionState<TOutput>),\n currentRunId: null,\n }\n\n default:\n // Delegate to base subscription reducer\n return {\n ...subscriptionReducer(state, action as SubscriptionAction<TOutput>),\n currentRunId: state.currentRunId,\n }\n }\n}\n\n/**\n * Hook for subscribing to job events with followLatest support.\n * This is a specialized version of useSubscription for job-level tracking.\n */\nexport function useJobSubscription<TOutput = unknown>(\n durably: Durably | null,\n jobName: string,\n options?: UseJobSubscriptionOptions,\n): UseJobSubscriptionResult<TOutput> {\n const initialState: JobSubscriptionState<TOutput> = {\n ...(initialSubscriptionState as SubscriptionState<TOutput>),\n currentRunId: null,\n }\n\n const [state, dispatch] = useReducer(\n jobSubscriptionReducer<TOutput>,\n initialState,\n )\n\n const currentRunIdRef = useRef<string | null>(null)\n currentRunIdRef.current = state.currentRunId\n\n const followLatest = options?.followLatest !== false\n const maxLogs = options?.maxLogs ?? 0\n\n useEffect(() => {\n if (!durably) return\n\n const unsubscribes: (() => void)[] = []\n\n unsubscribes.push(\n durably.on('run:start', (event) => {\n if (event.jobName !== jobName) return\n\n if (followLatest) {\n // Switch to tracking the new run\n dispatch({ type: 'switch_to_run', runId: event.runId })\n currentRunIdRef.current = event.runId\n } else {\n // Only update if this is our current run\n if (event.runId !== currentRunIdRef.current) return\n dispatch({ type: 'run:start' })\n }\n }),\n )\n\n unsubscribes.push(\n durably.on('run:complete', (event) => {\n if (event.runId !== currentRunIdRef.current) return\n dispatch({ type: 'run:complete', output: event.output as TOutput })\n }),\n )\n\n unsubscribes.push(\n durably.on('run:fail', (event) => {\n if (event.runId !== currentRunIdRef.current) return\n dispatch({ type: 'run:fail', error: event.error })\n }),\n )\n\n unsubscribes.push(\n durably.on('run:cancel', (event) => {\n if (event.runId !== currentRunIdRef.current) return\n dispatch({ type: 'run:cancel' })\n }),\n )\n\n unsubscribes.push(\n durably.on('run:retry', (event) => {\n if (event.runId !== currentRunIdRef.current) return\n dispatch({ type: 'run:retry' })\n }),\n )\n\n unsubscribes.push(\n durably.on('run:progress', (event) => {\n if (event.runId !== currentRunIdRef.current) return\n dispatch({ type: 'run:progress', progress: event.progress })\n }),\n )\n\n unsubscribes.push(\n durably.on('log:write', (event) => {\n if (event.runId !== currentRunIdRef.current) return\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 }),\n )\n\n return () => {\n for (const unsubscribe of unsubscribes) {\n unsubscribe()\n }\n }\n }, [durably, jobName, followLatest, maxLogs])\n\n const setCurrentRunId = useCallback((runId: string | null) => {\n dispatch({ type: 'set_run_id', runId })\n currentRunIdRef.current = runId\n }, [])\n\n const clearLogs = useCallback(() => {\n dispatch({ type: 'clear_logs' })\n }, [])\n\n const reset = useCallback(() => {\n dispatch({ type: 'reset' })\n currentRunIdRef.current = null\n }, [])\n\n return {\n ...state,\n setCurrentRunId,\n clearLogs,\n reset,\n }\n}\n","import type { Durably } from '@coji/durably'\nimport { useMemo } from 'react'\nimport { createDurablyEventSubscriber } from '../shared/durably-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 RunSubscriptionState<TOutput = unknown> = SubscriptionState<TOutput>\n\n/** @deprecated Use UseSubscriptionOptions from '../shared/use-subscription' instead */\nexport type UseRunSubscriptionOptions = UseSubscriptionOptions\n\n/** @deprecated Use UseSubscriptionResult from '../shared/use-subscription' instead */\nexport type UseRunSubscriptionResult<TOutput = unknown> =\n UseSubscriptionResult<TOutput>\n\n/**\n * Internal hook for subscribing to run events via Durably.on().\n * Shared by useJob, useJobRun, and useJobLogs.\n *\n * @deprecated Consider using useSubscription with createDurablyEventSubscriber directly.\n */\nexport function useRunSubscription<TOutput = unknown>(\n durably: Durably | null,\n runId: string | null,\n options?: UseRunSubscriptionOptions,\n): UseRunSubscriptionResult<TOutput> {\n const subscriber = useMemo(\n () => (durably ? createDurablyEventSubscriber(durably) : null),\n [durably],\n )\n\n return useSubscription<TOutput>(subscriber, runId, options)\n}\n","import type { Durably } from '@coji/durably'\nimport type { EventSubscriber, SubscriptionEvent } from './event-subscriber'\n\n/**\n * EventSubscriber implementation using Durably.on() for direct subscriptions.\n * Used in browser environments where Durably instance is available.\n */\nexport function createDurablyEventSubscriber(\n durably: Durably,\n): EventSubscriber {\n return {\n subscribe<TOutput = unknown>(\n runId: string,\n onEvent: (event: SubscriptionEvent<TOutput>) => void,\n ): () => void {\n const unsubscribes: (() => void)[] = []\n\n unsubscribes.push(\n durably.on('run:start', (event) => {\n if (event.runId !== runId) return\n onEvent({ type: 'run:start' })\n }),\n )\n\n unsubscribes.push(\n durably.on('run:complete', (event) => {\n if (event.runId !== runId) return\n onEvent({ type: 'run:complete', output: event.output as TOutput })\n }),\n )\n\n unsubscribes.push(\n durably.on('run:fail', (event) => {\n if (event.runId !== runId) return\n onEvent({ type: 'run:fail', error: event.error })\n }),\n )\n\n unsubscribes.push(\n durably.on('run:cancel', (event) => {\n if (event.runId !== runId) return\n onEvent({ type: 'run:cancel' })\n }),\n )\n\n unsubscribes.push(\n durably.on('run:retry', (event) => {\n if (event.runId !== runId) return\n onEvent({ type: 'run:retry' })\n }),\n )\n\n unsubscribes.push(\n durably.on('run:progress', (event) => {\n if (event.runId !== runId) return\n onEvent({ type: 'run:progress', progress: event.progress })\n }),\n )\n\n unsubscribes.push(\n durably.on('log:write', (event) => {\n if (event.runId !== runId) return\n onEvent({\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 })\n }),\n )\n\n return () => {\n for (const unsubscribe of unsubscribes) {\n unsubscribe()\n }\n }\n },\n }\n}\n","import { useDurably } from '../context'\nimport type { LogEntry } from '../types'\nimport { useRunSubscription } from './use-run-subscription'\n\nexport interface UseJobLogsOptions {\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 UseJobLogsResult {\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.\n * Use this when you only need logs, not full run status.\n */\nexport function useJobLogs(options: UseJobLogsOptions): UseJobLogsResult {\n const { durably } = useDurably()\n const { runId, maxLogs } = options\n\n const subscription = useRunSubscription(durably, runId, { maxLogs })\n\n return {\n logs: subscription.logs,\n clearLogs: subscription.clearLogs,\n }\n}\n","import { useEffect, useRef } from 'react'\nimport { useDurably } from '../context'\nimport type { LogEntry, Progress, RunStatus } from '../types'\nimport { useRunSubscription } from './use-run-subscription'\n\nexport interface UseJobRunOptions {\n /**\n * The run ID to subscribe to\n */\n runId: string | null\n}\n\nexport interface UseJobRunResult<TOutput = unknown> {\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 by ID.\n * Use this when you have a runId and want to track its status.\n */\nexport function useJobRun<TOutput = unknown>(\n options: UseJobRunOptions,\n): UseJobRunResult<TOutput> {\n const { durably } = useDurably()\n const { runId } = options\n\n const subscription = useRunSubscription<TOutput>(durably, runId)\n\n // Fetch initial state when runId changes\n const fetchedRef = useRef<Set<string>>(new Set())\n\n useEffect(() => {\n if (!durably || !runId || fetchedRef.current.has(runId)) return\n\n // Mark as fetched to avoid duplicate fetches\n fetchedRef.current.add(runId)\n\n // Try to fetch current run state\n // Note: We need to use internal APIs or polling here\n // For now, we rely on event-based updates\n }, [durably, runId])\n\n return {\n status: subscription.status,\n output: subscription.output,\n error: subscription.error,\n logs: subscription.logs,\n progress: subscription.progress,\n isRunning: subscription.status === 'running',\n isPending: subscription.status === 'pending',\n isCompleted: subscription.status === 'completed',\n isFailed: subscription.status === 'failed',\n isCancelled: subscription.status === 'cancelled',\n }\n}\n","import type { Run } from '@coji/durably'\nimport { useCallback, useEffect, useState } from 'react'\nimport { useDurably } from '../context'\n\nexport interface UseRunsOptions {\n /**\n * Filter by job name\n */\n jobName?: string\n /**\n * Filter by status\n */\n status?: 'pending' | 'running' | 'completed' | 'failed' | 'cancelled'\n /**\n * Number of runs per page\n * @default 10\n */\n pageSize?: number\n /**\n * Subscribe to real-time updates\n * @default true\n */\n realtime?: boolean\n}\n\nexport interface UseRunsResult {\n /**\n * List of runs for the current page\n */\n runs: Run[]\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 * 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 with pagination and real-time updates.\n *\n * @example\n * ```tsx\n * function Dashboard() {\n * const { runs, page, hasMore, nextPage, prevPage, isLoading } = useRuns({\n * pageSize: 20,\n * })\n *\n * return (\n * <div>\n * {runs.map(run => (\n * <div key={run.id}>{run.jobName}: {run.status}</div>\n * ))}\n * <button onClick={prevPage} disabled={page === 0}>Prev</button>\n * <button onClick={nextPage} disabled={!hasMore}>Next</button>\n * </div>\n * )\n * }\n * ```\n */\nexport function useRuns(options?: UseRunsOptions): UseRunsResult {\n const { durably } = useDurably()\n const pageSize = options?.pageSize ?? 10\n const realtime = options?.realtime ?? true\n\n const [runs, setRuns] = useState<Run[]>([])\n const [page, setPage] = useState(0)\n const [hasMore, setHasMore] = useState(false)\n const [isLoading, setIsLoading] = useState(false)\n\n const refresh = useCallback(async () => {\n if (!durably) return\n\n setIsLoading(true)\n try {\n const data = await durably.getRuns({\n jobName: options?.jobName,\n status: options?.status,\n limit: pageSize + 1,\n offset: page * pageSize,\n })\n setHasMore(data.length > pageSize)\n setRuns(data.slice(0, pageSize))\n } finally {\n setIsLoading(false)\n }\n }, [durably, options?.jobName, options?.status, pageSize, page])\n\n // Initial fetch and subscribe to events\n useEffect(() => {\n if (!durably) return\n\n refresh()\n\n if (!realtime) return\n\n const unsubscribes = [\n durably.on('run:trigger', refresh),\n durably.on('run:start', refresh),\n durably.on('run:complete', refresh),\n durably.on('run:fail', refresh),\n durably.on('run:cancel', refresh),\n durably.on('run:retry', refresh),\n durably.on('run:progress', refresh),\n durably.on('step:start', refresh),\n durably.on('step:complete', refresh),\n ]\n\n return () => {\n for (const unsubscribe of unsubscribes) {\n unsubscribe()\n }\n }\n }, [durably, refresh, realtime])\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 nextPage,\n prevPage,\n goToPage,\n refresh,\n }\n}\n"],"mappings":"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","names":["useCallback","useEffect","useRef","useEffect","useEffect","useRef","useEffect","useCallback","useMemo","useMemo","useEffect","useRef","useRef","useEffect","useCallback","useEffect","useCallback","useEffect"]}
1
+ {"version":3,"sources":["../src/context.tsx","../src/hooks/use-job.ts","../src/hooks/use-auto-resume.ts","../src/hooks/use-job-subscription.ts","../src/hooks/use-run-subscription.ts","../src/shared/durably-event-subscriber.ts","../src/hooks/use-job-logs.ts","../src/hooks/use-job-run.ts","../src/hooks/use-runs.ts"],"sourcesContent":["import type { Durably } from '@coji/durably'\nimport { Suspense, createContext, use, useContext, type ReactNode } from 'react'\n\ninterface DurablyContextValue {\n durably: Durably\n}\n\nconst DurablyContext = createContext<DurablyContextValue | null>(null)\n\nexport interface DurablyProviderProps {\n /**\n * Durably instance or Promise that resolves to one.\n * The instance should already be initialized via `await durably.init()`.\n *\n * When passing a Promise, wrap the provider with Suspense or use the fallback prop.\n *\n * @example\n * // With Suspense (recommended)\n * <Suspense fallback={<Loading />}>\n * <DurablyProvider durably={durablyPromise}>\n * <App />\n * </DurablyProvider>\n * </Suspense>\n *\n * @example\n * // With fallback prop\n * <DurablyProvider durably={durablyPromise} fallback={<Loading />}>\n * <App />\n * </DurablyProvider>\n */\n durably: Durably | Promise<Durably>\n /**\n * Fallback to show while waiting for the Durably Promise to resolve.\n * This wraps the provider content in a Suspense boundary automatically.\n */\n fallback?: ReactNode\n children: ReactNode\n}\n\n/**\n * Internal component that uses the `use()` hook to resolve the Promise\n */\nfunction DurablyProviderInner({\n durably: durablyOrPromise,\n children,\n}: Omit<DurablyProviderProps, 'fallback'>) {\n const durably =\n durablyOrPromise instanceof Promise\n ? use(durablyOrPromise)\n : durablyOrPromise\n\n return (\n <DurablyContext.Provider value={{ durably }}>\n {children}\n </DurablyContext.Provider>\n )\n}\n\nexport function DurablyProvider({\n durably,\n fallback,\n children,\n}: DurablyProviderProps) {\n const inner = (\n <DurablyProviderInner durably={durably}>{children}</DurablyProviderInner>\n )\n\n if (fallback !== undefined) {\n return <Suspense fallback={fallback}>{inner}</Suspense>\n }\n\n return inner\n}\n\nexport function useDurably(): DurablyContextValue {\n const context = useContext(DurablyContext)\n if (!context) {\n throw new Error('useDurably must be used within a DurablyProvider')\n }\n return context\n}\n","import type { JobDefinition, JobHandle } from '@coji/durably'\nimport { useCallback, useEffect, useMemo, useRef } from 'react'\nimport { useDurably } from '../context'\nimport type { LogEntry, Progress, RunStatus } from '../types'\nimport { useAutoResume } from './use-auto-resume'\nimport { useJobSubscription } from './use-job-subscription'\n\nexport interface UseJobOptions {\n /**\n * Initial Run ID to subscribe to (for reconnection scenarios)\n */\n initialRunId?: string\n /**\n * Automatically resume tracking any pending or running job on initialization.\n * If a pending or running run exists for this job, the hook will subscribe to it.\n * @default true\n */\n autoResume?: boolean\n /**\n * Automatically switch to tracking the latest running job when a new run starts.\n * When true, the hook will update to track any new run for this job as soon as it starts running.\n * When false, the hook will only track the run that was triggered or explicitly set.\n * @default true\n */\n followLatest?: boolean\n}\n\nexport interface UseJobResult<TInput, TOutput> {\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\nexport function useJob<\n TName extends string,\n TInput extends Record<string, unknown>,\n // biome-ignore lint/suspicious/noConfusingVoidType: TOutput can be void for jobs without return value\n TOutput extends Record<string, unknown> | void,\n>(\n jobDefinition: JobDefinition<TName, TInput, TOutput>,\n options?: UseJobOptions,\n): UseJobResult<TInput, TOutput> {\n const { durably } = useDurably()\n\n const jobHandleRef = useRef<JobHandle<TName, TInput, TOutput> | null>(null)\n\n // Register job\n useEffect(() => {\n if (!durably) return\n\n const d = durably.register({\n _job: jobDefinition,\n })\n jobHandleRef.current = d.jobs._job\n }, [durably, jobDefinition])\n\n // Use the extracted job subscription hook\n const subscription = useJobSubscription<TOutput>(\n durably,\n jobDefinition.name,\n {\n followLatest: options?.followLatest,\n },\n )\n\n // Auto-resume callbacks - stable reference\n const autoResumeCallbacks = useMemo(\n () => ({\n onRunFound: (runId: string, _status: RunStatus) => {\n subscription.setCurrentRunId(runId)\n },\n }),\n [subscription.setCurrentRunId],\n )\n\n // Use the extracted auto-resume hook\n useAutoResume(\n jobHandleRef.current,\n {\n enabled: options?.autoResume,\n initialRunId: options?.initialRunId,\n },\n autoResumeCallbacks,\n )\n\n // Handle initialRunId - set it to start tracking\n useEffect(() => {\n if (!durably || !options?.initialRunId) return\n\n subscription.setCurrentRunId(options.initialRunId)\n }, [durably, options?.initialRunId, subscription.setCurrentRunId])\n\n const trigger = useCallback(\n async (input: TInput): Promise<{ runId: string }> => {\n const jobHandle = jobHandleRef.current\n if (!jobHandle) {\n throw new Error('Job not ready')\n }\n\n // Reset state before triggering\n subscription.reset()\n\n const run = await jobHandle.trigger(input)\n subscription.setCurrentRunId(run.id)\n\n return { runId: run.id }\n },\n [subscription],\n )\n\n const triggerAndWait = useCallback(\n async (input: TInput): Promise<{ runId: string; output: TOutput }> => {\n const jobHandle = jobHandleRef.current\n if (!jobHandle || !durably) {\n throw new Error('Job not ready')\n }\n\n // Reset state before triggering\n subscription.reset()\n\n const run = await jobHandle.trigger(input)\n subscription.setCurrentRunId(run.id)\n\n // Wait for completion by polling\n return new Promise((resolve, reject) => {\n const checkCompletion = async () => {\n const updatedRun = await jobHandle.getRun(run.id)\n if (!updatedRun) {\n reject(new Error('Run not found'))\n return\n }\n\n if (updatedRun.status === 'completed') {\n resolve({ runId: run.id, output: updatedRun.output as TOutput })\n } else if (updatedRun.status === 'failed') {\n reject(new Error(updatedRun.error ?? 'Job failed'))\n } else if (updatedRun.status === 'cancelled') {\n reject(new Error('Job cancelled'))\n } else {\n // Still running, check again\n setTimeout(checkCompletion, 50)\n }\n }\n checkCompletion()\n })\n },\n [durably, subscription],\n )\n\n return {\n trigger,\n triggerAndWait,\n status: subscription.status,\n output: subscription.output,\n error: subscription.error,\n logs: subscription.logs,\n progress: subscription.progress,\n isRunning: subscription.status === 'running',\n isPending: subscription.status === 'pending',\n isCompleted: subscription.status === 'completed',\n isFailed: subscription.status === 'failed',\n isCancelled: subscription.status === 'cancelled',\n currentRunId: subscription.currentRunId,\n reset: subscription.reset,\n }\n}\n","import type { JobHandle } from '@coji/durably'\nimport { useEffect } from 'react'\nimport type { RunStatus } from '../types'\n\nexport interface UseAutoResumeOptions {\n /**\n * Whether to automatically resume tracking pending/running runs\n * @default true\n */\n enabled?: boolean\n /**\n * Skip auto-resume if an initial run ID is provided\n */\n skipIfInitialRunId?: boolean\n /**\n * Initial run ID (if provided, auto-resume is skipped)\n */\n initialRunId?: string\n}\n\nexport interface UseAutoResumeCallbacks {\n /**\n * Called when a run is found to resume\n */\n onRunFound: (runId: string, status: RunStatus) => void\n}\n\n/**\n * Hook that automatically finds and resumes tracking of pending/running runs.\n * Extracted from useJob to separate the auto-resume concern.\n */\nexport function useAutoResume<\n TName extends string,\n TInput extends Record<string, unknown>,\n TOutput,\n>(\n jobHandle: JobHandle<TName, TInput, TOutput> | null,\n options: UseAutoResumeOptions,\n callbacks: UseAutoResumeCallbacks,\n): void {\n const enabled = options.enabled !== false\n const skipIfInitialRunId = options.skipIfInitialRunId !== false\n const initialRunId = options.initialRunId\n\n useEffect(() => {\n if (!jobHandle) return\n if (!enabled) return\n if (skipIfInitialRunId && initialRunId) return\n\n let cancelled = false\n\n const findActiveRun = async () => {\n // First check for running runs\n const runningRuns = await jobHandle.getRuns({ status: 'running' })\n if (cancelled) return\n\n if (runningRuns.length > 0) {\n const run = runningRuns[0]\n callbacks.onRunFound(run.id, run.status as RunStatus)\n return\n }\n\n // Then check for pending runs\n const pendingRuns = await jobHandle.getRuns({ status: 'pending' })\n if (cancelled) return\n\n if (pendingRuns.length > 0) {\n const run = pendingRuns[0]\n callbacks.onRunFound(run.id, run.status as RunStatus)\n }\n }\n\n findActiveRun()\n\n return () => {\n cancelled = true\n }\n }, [jobHandle, enabled, skipIfInitialRunId, initialRunId, callbacks])\n}\n","import type { Durably } from '@coji/durably'\nimport { useCallback, useEffect, useReducer, useRef } from 'react'\nimport {\n initialSubscriptionState,\n subscriptionReducer,\n type SubscriptionAction,\n} from '../shared/subscription-reducer'\nimport type { SubscriptionState } from '../types'\n\nexport interface UseJobSubscriptionOptions {\n /**\n * Automatically switch to tracking the latest running job when a new run starts.\n * @default true\n */\n followLatest?: boolean\n /**\n * Maximum number of logs to keep (0 = unlimited)\n */\n maxLogs?: number\n}\n\nexport interface UseJobSubscriptionResult<\n TOutput = unknown,\n> extends SubscriptionState<TOutput> {\n /**\n * Current run ID being tracked\n */\n currentRunId: string | null\n /**\n * Set the current run ID to track\n */\n setCurrentRunId: (runId: string | null) => void\n /**\n * Clear all logs\n */\n clearLogs: () => void\n /**\n * Reset all state including currentRunId\n */\n reset: () => void\n}\n\n// Extended state for job subscription (includes currentRunId)\ninterface JobSubscriptionState<\n TOutput = unknown,\n> extends SubscriptionState<TOutput> {\n currentRunId: string | null\n}\n\n// Extended actions for job subscription\ntype JobSubscriptionAction<TOutput = unknown> =\n | SubscriptionAction<TOutput>\n | { type: 'set_run_id'; runId: string | null }\n | {\n type: 'switch_to_run'\n runId: string\n }\n\nfunction jobSubscriptionReducer<TOutput = unknown>(\n state: JobSubscriptionState<TOutput>,\n action: JobSubscriptionAction<TOutput>,\n): JobSubscriptionState<TOutput> {\n switch (action.type) {\n case 'set_run_id':\n return { ...state, currentRunId: action.runId }\n\n case 'switch_to_run':\n // Switch to a new run, resetting state\n return {\n ...initialSubscriptionState,\n currentRunId: action.runId,\n status: 'running',\n } as JobSubscriptionState<TOutput>\n\n case 'reset':\n return {\n ...(initialSubscriptionState as SubscriptionState<TOutput>),\n currentRunId: null,\n }\n\n default:\n // Delegate to base subscription reducer\n return {\n ...subscriptionReducer(state, action as SubscriptionAction<TOutput>),\n currentRunId: state.currentRunId,\n }\n }\n}\n\n/**\n * Hook for subscribing to job events with followLatest support.\n * This is a specialized version of useSubscription for job-level tracking.\n */\nexport function useJobSubscription<TOutput = unknown>(\n durably: Durably | null,\n jobName: string,\n options?: UseJobSubscriptionOptions,\n): UseJobSubscriptionResult<TOutput> {\n const initialState: JobSubscriptionState<TOutput> = {\n ...(initialSubscriptionState as SubscriptionState<TOutput>),\n currentRunId: null,\n }\n\n const [state, dispatch] = useReducer(\n jobSubscriptionReducer<TOutput>,\n initialState,\n )\n\n const currentRunIdRef = useRef<string | null>(null)\n currentRunIdRef.current = state.currentRunId\n\n const followLatest = options?.followLatest !== false\n const maxLogs = options?.maxLogs ?? 0\n\n useEffect(() => {\n if (!durably) return\n\n const unsubscribes: (() => void)[] = []\n\n unsubscribes.push(\n durably.on('run:start', (event) => {\n if (event.jobName !== jobName) return\n\n if (followLatest) {\n // Switch to tracking the new run\n dispatch({ type: 'switch_to_run', runId: event.runId })\n currentRunIdRef.current = event.runId\n } else {\n // Only update if this is our current run\n if (event.runId !== currentRunIdRef.current) return\n dispatch({ type: 'run:start' })\n }\n }),\n )\n\n unsubscribes.push(\n durably.on('run:complete', (event) => {\n if (event.runId !== currentRunIdRef.current) return\n dispatch({ type: 'run:complete', output: event.output as TOutput })\n }),\n )\n\n unsubscribes.push(\n durably.on('run:fail', (event) => {\n if (event.runId !== currentRunIdRef.current) return\n dispatch({ type: 'run:fail', error: event.error })\n }),\n )\n\n unsubscribes.push(\n durably.on('run:cancel', (event) => {\n if (event.runId !== currentRunIdRef.current) return\n dispatch({ type: 'run:cancel' })\n }),\n )\n\n unsubscribes.push(\n durably.on('run:retry', (event) => {\n if (event.runId !== currentRunIdRef.current) return\n dispatch({ type: 'run:retry' })\n }),\n )\n\n unsubscribes.push(\n durably.on('run:progress', (event) => {\n if (event.runId !== currentRunIdRef.current) return\n dispatch({ type: 'run:progress', progress: event.progress })\n }),\n )\n\n unsubscribes.push(\n durably.on('log:write', (event) => {\n if (event.runId !== currentRunIdRef.current) return\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 }),\n )\n\n return () => {\n for (const unsubscribe of unsubscribes) {\n unsubscribe()\n }\n }\n }, [durably, jobName, followLatest, maxLogs])\n\n const setCurrentRunId = useCallback((runId: string | null) => {\n dispatch({ type: 'set_run_id', runId })\n currentRunIdRef.current = runId\n }, [])\n\n const clearLogs = useCallback(() => {\n dispatch({ type: 'clear_logs' })\n }, [])\n\n const reset = useCallback(() => {\n dispatch({ type: 'reset' })\n currentRunIdRef.current = null\n }, [])\n\n return {\n ...state,\n setCurrentRunId,\n clearLogs,\n reset,\n }\n}\n","import type { Durably } from '@coji/durably'\nimport { useMemo } from 'react'\nimport { createDurablyEventSubscriber } from '../shared/durably-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 RunSubscriptionState<TOutput = unknown> = SubscriptionState<TOutput>\n\n/** @deprecated Use UseSubscriptionOptions from '../shared/use-subscription' instead */\nexport type UseRunSubscriptionOptions = UseSubscriptionOptions\n\n/** @deprecated Use UseSubscriptionResult from '../shared/use-subscription' instead */\nexport type UseRunSubscriptionResult<TOutput = unknown> =\n UseSubscriptionResult<TOutput>\n\n/**\n * Internal hook for subscribing to run events via Durably.on().\n * Shared by useJob, useJobRun, and useJobLogs.\n *\n * @deprecated Consider using useSubscription with createDurablyEventSubscriber directly.\n */\nexport function useRunSubscription<TOutput = unknown>(\n durably: Durably | null,\n runId: string | null,\n options?: UseRunSubscriptionOptions,\n): UseRunSubscriptionResult<TOutput> {\n const subscriber = useMemo(\n () => (durably ? createDurablyEventSubscriber(durably) : null),\n [durably],\n )\n\n return useSubscription<TOutput>(subscriber, runId, options)\n}\n","import type { Durably } from '@coji/durably'\nimport type { EventSubscriber, SubscriptionEvent } from './event-subscriber'\n\n/**\n * EventSubscriber implementation using Durably.on() for direct subscriptions.\n * Used in browser environments where Durably instance is available.\n */\nexport function createDurablyEventSubscriber(\n durably: Durably,\n): EventSubscriber {\n return {\n subscribe<TOutput = unknown>(\n runId: string,\n onEvent: (event: SubscriptionEvent<TOutput>) => void,\n ): () => void {\n const unsubscribes: (() => void)[] = []\n\n unsubscribes.push(\n durably.on('run:start', (event) => {\n if (event.runId !== runId) return\n onEvent({ type: 'run:start' })\n }),\n )\n\n unsubscribes.push(\n durably.on('run:complete', (event) => {\n if (event.runId !== runId) return\n onEvent({ type: 'run:complete', output: event.output as TOutput })\n }),\n )\n\n unsubscribes.push(\n durably.on('run:fail', (event) => {\n if (event.runId !== runId) return\n onEvent({ type: 'run:fail', error: event.error })\n }),\n )\n\n unsubscribes.push(\n durably.on('run:cancel', (event) => {\n if (event.runId !== runId) return\n onEvent({ type: 'run:cancel' })\n }),\n )\n\n unsubscribes.push(\n durably.on('run:retry', (event) => {\n if (event.runId !== runId) return\n onEvent({ type: 'run:retry' })\n }),\n )\n\n unsubscribes.push(\n durably.on('run:progress', (event) => {\n if (event.runId !== runId) return\n onEvent({ type: 'run:progress', progress: event.progress })\n }),\n )\n\n unsubscribes.push(\n durably.on('log:write', (event) => {\n if (event.runId !== runId) return\n onEvent({\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 })\n }),\n )\n\n return () => {\n for (const unsubscribe of unsubscribes) {\n unsubscribe()\n }\n }\n },\n }\n}\n","import { useDurably } from '../context'\nimport type { LogEntry } from '../types'\nimport { useRunSubscription } from './use-run-subscription'\n\nexport interface UseJobLogsOptions {\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 UseJobLogsResult {\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.\n * Use this when you only need logs, not full run status.\n */\nexport function useJobLogs(options: UseJobLogsOptions): UseJobLogsResult {\n const { durably } = useDurably()\n const { runId, maxLogs } = options\n\n const subscription = useRunSubscription(durably, runId, { maxLogs })\n\n return {\n logs: subscription.logs,\n clearLogs: subscription.clearLogs,\n }\n}\n","import { useEffect, useRef } from 'react'\nimport { useDurably } from '../context'\nimport type { LogEntry, Progress, RunStatus } from '../types'\nimport { useRunSubscription } from './use-run-subscription'\n\nexport interface UseJobRunOptions {\n /**\n * The run ID to subscribe to\n */\n runId: string | null\n}\n\nexport interface UseJobRunResult<TOutput = unknown> {\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 by ID.\n * Use this when you have a runId and want to track its status.\n */\nexport function useJobRun<TOutput = unknown>(\n options: UseJobRunOptions,\n): UseJobRunResult<TOutput> {\n const { durably } = useDurably()\n const { runId } = options\n\n const subscription = useRunSubscription<TOutput>(durably, runId)\n\n // Fetch initial state when runId changes\n const fetchedRef = useRef<Set<string>>(new Set())\n\n useEffect(() => {\n if (!durably || !runId || fetchedRef.current.has(runId)) return\n\n // Mark as fetched to avoid duplicate fetches\n fetchedRef.current.add(runId)\n\n // Try to fetch current run state\n // Note: We need to use internal APIs or polling here\n // For now, we rely on event-based updates\n }, [durably, runId])\n\n return {\n status: subscription.status,\n output: subscription.output,\n error: subscription.error,\n logs: subscription.logs,\n progress: subscription.progress,\n isRunning: subscription.status === 'running',\n isPending: subscription.status === 'pending',\n isCompleted: subscription.status === 'completed',\n isFailed: subscription.status === 'failed',\n isCancelled: subscription.status === 'cancelled',\n }\n}\n","import type { JobDefinition } from '@coji/durably'\nimport { useCallback, useEffect, useState } from 'react'\nimport { useDurably } from '../context'\nimport { type TypedRun, isJobDefinition } from '../types'\n\n// Re-export TypedRun for convenience\nexport type { TypedRun } from '../types'\n\nexport interface UseRunsOptions {\n /**\n * Filter by job name\n */\n jobName?: string\n /**\n * Filter by status\n */\n status?: 'pending' | 'running' | 'completed' | 'failed' | 'cancelled'\n /**\n * Number of runs per page\n * @default 10\n */\n pageSize?: number\n /**\n * Subscribe to real-time updates\n * @default true\n */\n realtime?: boolean\n}\n\nexport interface UseRunsResult<\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: TypedRun<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 * 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 with pagination and real-time updates.\n *\n * @example With generic type parameter (dashboard with multiple job types)\n * ```tsx\n * type DashboardRun = TypedRun<ImportInput, ImportOutput> | TypedRun<SyncInput, SyncOutput>\n *\n * function Dashboard() {\n * const { runs } = useRuns<DashboardRun>({ 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 Dashboard() {\n * const { runs } = useRuns(myJob)\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 Dashboard() {\n * const { runs } = useRuns({ pageSize: 20 })\n * // runs[0].output is unknown\n * }\n * ```\n */\n// Overload 1: With generic type parameter\nexport function useRuns<\n TRun extends TypedRun<\n Record<string, unknown>,\n Record<string, unknown> | undefined\n >,\n>(\n options?: UseRunsOptions,\n): UseRunsResult<\n TRun extends TypedRun<infer I, infer _O> ? I : Record<string, unknown>,\n TRun extends TypedRun<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<UseRunsOptions, 'jobName'>,\n): UseRunsResult<TInput, TOutput>\n\n// Overload 3: Without type parameter (untyped, backward compatible)\nexport function useRuns(options?: UseRunsOptions): UseRunsResult\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 | UseRunsOptions,\n optionsArg?: Omit<UseRunsOptions, 'jobName'>,\n): UseRunsResult<TInput, TOutput> {\n const { durably } = useDurably()\n\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 UseRunsOptions | undefined)?.jobName\n\n const options = isJob\n ? optionsArg\n : (jobDefinitionOrOptions as UseRunsOptions | undefined)\n\n const pageSize = options?.pageSize ?? 10\n const realtime = options?.realtime ?? true\n const status = options?.status\n\n const [runs, setRuns] = useState<TypedRun<TInput, TOutput>[]>([])\n const [page, setPage] = useState(0)\n const [hasMore, setHasMore] = useState(false)\n const [isLoading, setIsLoading] = useState(false)\n\n const refresh = useCallback(async () => {\n if (!durably) return\n\n setIsLoading(true)\n try {\n const data = await durably.getRuns({\n jobName,\n status,\n limit: pageSize + 1,\n offset: page * pageSize,\n })\n setHasMore(data.length > pageSize)\n setRuns(data.slice(0, pageSize) as TypedRun<TInput, TOutput>[])\n } finally {\n setIsLoading(false)\n }\n }, [durably, jobName, status, pageSize, page])\n\n // Initial fetch and subscribe to events\n useEffect(() => {\n if (!durably) return\n\n refresh()\n\n if (!realtime) return\n\n const unsubscribes = [\n durably.on('run:trigger', refresh),\n durably.on('run:start', refresh),\n durably.on('run:complete', refresh),\n durably.on('run:fail', refresh),\n durably.on('run:cancel', refresh),\n durably.on('run:retry', refresh),\n durably.on('run:progress', refresh),\n durably.on('step:start', refresh),\n durably.on('step:complete', refresh),\n ]\n\n return () => {\n for (const unsubscribe of unsubscribes) {\n unsubscribe()\n }\n }\n }, [durably, refresh, realtime])\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 nextPage,\n prevPage,\n goToPage,\n refresh,\n }\n}\n"],"mappings":"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","names":["useCallback","useEffect","useRef","useEffect","useEffect","useRef","useEffect","useCallback","useMemo","useMemo","useEffect","useRef","useRef","useEffect","useCallback","useEffect","useCallback","useEffect"]}
@@ -1,4 +1,4 @@
1
- import { JobDefinition } from '@coji/durably';
1
+ import { Run, JobDefinition } from '@coji/durably';
2
2
 
3
3
  type InferInput<T> = T extends JobDefinition<string, infer TInput, unknown> ? TInput extends Record<string, unknown> ? TInput : Record<string, unknown> : T extends {
4
4
  trigger: (input: infer TInput) => unknown;
@@ -73,5 +73,39 @@ type DurablyEvent = {
73
73
  message: string;
74
74
  data: unknown;
75
75
  };
76
+ /**
77
+ * A typed version of Run with generic payload/output types.
78
+ * Used by browser hooks (direct durably access).
79
+ */
80
+ type TypedRun<TInput extends Record<string, unknown> = Record<string, unknown>, TOutput extends Record<string, unknown> | undefined = Record<string, unknown> | undefined> = Omit<Run, 'payload' | 'output'> & {
81
+ payload: TInput;
82
+ output: TOutput | null;
83
+ };
84
+ /**
85
+ * Run type for client mode (matches server response).
86
+ * Used by client hooks (HTTP/SSE connection).
87
+ */
88
+ interface ClientRun {
89
+ id: string;
90
+ jobName: string;
91
+ status: RunStatus;
92
+ input: unknown;
93
+ output: unknown | null;
94
+ error: string | null;
95
+ currentStepIndex: number;
96
+ stepCount: number;
97
+ progress: Progress | null;
98
+ createdAt: string;
99
+ startedAt: string | null;
100
+ completedAt: string | null;
101
+ }
102
+ /**
103
+ * A typed version of ClientRun with generic input/output types.
104
+ * Used by client hooks (HTTP/SSE connection).
105
+ */
106
+ type TypedClientRun<TInput extends Record<string, unknown> = Record<string, unknown>, TOutput extends Record<string, unknown> | undefined = Record<string, unknown> | undefined> = Omit<ClientRun, 'input' | 'output'> & {
107
+ input: TInput;
108
+ output: TOutput | null;
109
+ };
76
110
 
77
- export type { DurablyEvent as D, InferInput as I, LogEntry as L, Progress as P, RunStatus as R, InferOutput as a };
111
+ export type { ClientRun as C, DurablyEvent as D, InferInput as I, LogEntry as L, Progress as P, RunStatus as R, TypedRun as T, InferOutput as a, TypedClientRun as b };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coji/durably-react",
3
- "version": "0.6.1",
3
+ "version": "0.8.0",
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.6.1"
69
+ "zod": "^4.3.5",
70
+ "@coji/durably": "0.8.0"
71
71
  },
72
72
  "scripts": {
73
73
  "build": "tsup",
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/shared/create-log-entry.ts","../src/shared/subscription-reducer.ts","../src/shared/use-subscription.ts"],"sourcesContent":["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:start' }\n | { type: 'run:complete'; output: TOutput }\n | { type: 'run:fail'; error: string }\n | { type: 'run:cancel' }\n | { type: 'run:retry' }\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:start':\n return { ...state, status: 'running' }\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:retry':\n return { ...state, status: 'pending', error: null }\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:start':\n case 'run:cancel':\n case 'run:retry':\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":[]}