@coji/durably-react 0.6.0 → 0.7.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/dist/chunk-42AVE35N.js +142 -0
- package/dist/chunk-42AVE35N.js.map +1 -0
- package/dist/client.d.ts +31 -60
- package/dist/client.js +102 -131
- package/dist/client.js.map +1 -1
- package/dist/index.d.ts +31 -19
- package/dist/index.js +272 -249
- package/dist/index.js.map +1 -1
- package/dist/types-DY4Y6ggJ.d.ts +111 -0
- package/package.json +2 -2
- package/dist/types-BDUvsa8u.d.ts +0 -67
package/dist/client.js
CHANGED
|
@@ -1,121 +1,89 @@
|
|
|
1
|
+
import {
|
|
2
|
+
isJobDefinition,
|
|
3
|
+
useSubscription
|
|
4
|
+
} from "./chunk-42AVE35N.js";
|
|
5
|
+
|
|
1
6
|
// src/client/use-job.ts
|
|
2
|
-
import { useCallback
|
|
7
|
+
import { useCallback, useEffect, useState } from "react";
|
|
3
8
|
|
|
4
9
|
// src/client/use-sse-subscription.ts
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
setOutput(data.output);
|
|
43
|
-
break;
|
|
44
|
-
case "run:fail":
|
|
45
|
-
setStatus("failed");
|
|
46
|
-
setError(data.error);
|
|
47
|
-
break;
|
|
48
|
-
case "run:cancel":
|
|
49
|
-
setStatus("cancelled");
|
|
50
|
-
break;
|
|
51
|
-
case "run:retry":
|
|
52
|
-
setStatus("pending");
|
|
53
|
-
setError(null);
|
|
54
|
-
break;
|
|
55
|
-
case "run:progress":
|
|
56
|
-
setProgress(data.progress);
|
|
57
|
-
break;
|
|
58
|
-
case "log:write":
|
|
59
|
-
setLogs((prev) => {
|
|
60
|
-
const newLog = {
|
|
61
|
-
id: crypto.randomUUID(),
|
|
10
|
+
import { useMemo } from "react";
|
|
11
|
+
|
|
12
|
+
// src/shared/sse-event-subscriber.ts
|
|
13
|
+
function createSSEEventSubscriber(apiBaseUrl) {
|
|
14
|
+
return {
|
|
15
|
+
subscribe(runId, onEvent) {
|
|
16
|
+
const url = `${apiBaseUrl}/subscribe?runId=${encodeURIComponent(runId)}`;
|
|
17
|
+
const eventSource = new EventSource(url);
|
|
18
|
+
eventSource.onmessage = (messageEvent) => {
|
|
19
|
+
try {
|
|
20
|
+
const data = JSON.parse(messageEvent.data);
|
|
21
|
+
if (data.runId !== runId) return;
|
|
22
|
+
switch (data.type) {
|
|
23
|
+
case "run:start":
|
|
24
|
+
onEvent({ type: "run:start" });
|
|
25
|
+
break;
|
|
26
|
+
case "run:complete":
|
|
27
|
+
onEvent({
|
|
28
|
+
type: "run:complete",
|
|
29
|
+
output: data.output
|
|
30
|
+
});
|
|
31
|
+
break;
|
|
32
|
+
case "run:fail":
|
|
33
|
+
onEvent({ type: "run:fail", error: data.error });
|
|
34
|
+
break;
|
|
35
|
+
case "run:cancel":
|
|
36
|
+
onEvent({ type: "run:cancel" });
|
|
37
|
+
break;
|
|
38
|
+
case "run:retry":
|
|
39
|
+
onEvent({ type: "run:retry" });
|
|
40
|
+
break;
|
|
41
|
+
case "run:progress":
|
|
42
|
+
onEvent({ type: "run:progress", progress: data.progress });
|
|
43
|
+
break;
|
|
44
|
+
case "log:write":
|
|
45
|
+
onEvent({
|
|
46
|
+
type: "log:write",
|
|
62
47
|
runId: data.runId,
|
|
63
48
|
stepName: null,
|
|
64
49
|
level: data.level,
|
|
65
50
|
message: data.message,
|
|
66
|
-
data: data.data
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
return newLogs.slice(-maxLogs);
|
|
72
|
-
}
|
|
73
|
-
return newLogs;
|
|
74
|
-
});
|
|
75
|
-
break;
|
|
51
|
+
data: data.data
|
|
52
|
+
});
|
|
53
|
+
break;
|
|
54
|
+
}
|
|
55
|
+
} catch {
|
|
76
56
|
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
eventSourceRef.current = null;
|
|
87
|
-
};
|
|
88
|
-
}, [api, runId, maxLogs]);
|
|
89
|
-
const clearLogs = useCallback(() => {
|
|
90
|
-
setLogs([]);
|
|
91
|
-
}, []);
|
|
92
|
-
const reset = useCallback(() => {
|
|
93
|
-
setStatus(null);
|
|
94
|
-
setOutput(null);
|
|
95
|
-
setError(null);
|
|
96
|
-
setLogs([]);
|
|
97
|
-
setProgress(null);
|
|
98
|
-
}, []);
|
|
99
|
-
return {
|
|
100
|
-
status,
|
|
101
|
-
output,
|
|
102
|
-
error,
|
|
103
|
-
logs,
|
|
104
|
-
progress,
|
|
105
|
-
clearLogs,
|
|
106
|
-
reset
|
|
57
|
+
};
|
|
58
|
+
eventSource.onerror = () => {
|
|
59
|
+
onEvent({ type: "connection_error", error: "Connection failed" });
|
|
60
|
+
eventSource.close();
|
|
61
|
+
};
|
|
62
|
+
return () => {
|
|
63
|
+
eventSource.close();
|
|
64
|
+
};
|
|
65
|
+
}
|
|
107
66
|
};
|
|
108
67
|
}
|
|
109
68
|
|
|
69
|
+
// src/client/use-sse-subscription.ts
|
|
70
|
+
function useSSESubscription(api, runId, options) {
|
|
71
|
+
const subscriber = useMemo(
|
|
72
|
+
() => api ? createSSEEventSubscriber(api) : null,
|
|
73
|
+
[api]
|
|
74
|
+
);
|
|
75
|
+
return useSubscription(subscriber, runId, options);
|
|
76
|
+
}
|
|
77
|
+
|
|
110
78
|
// src/client/use-job.ts
|
|
111
79
|
function useJob(options) {
|
|
112
80
|
const { api, jobName, initialRunId } = options;
|
|
113
|
-
const [currentRunId, setCurrentRunId] =
|
|
81
|
+
const [currentRunId, setCurrentRunId] = useState(
|
|
114
82
|
initialRunId ?? null
|
|
115
83
|
);
|
|
116
|
-
const [isPending, setIsPending] =
|
|
84
|
+
const [isPending, setIsPending] = useState(false);
|
|
117
85
|
const subscription = useSSESubscription(api, currentRunId);
|
|
118
|
-
const trigger =
|
|
86
|
+
const trigger = useCallback(
|
|
119
87
|
async (input) => {
|
|
120
88
|
subscription.reset();
|
|
121
89
|
setIsPending(true);
|
|
@@ -137,7 +105,7 @@ function useJob(options) {
|
|
|
137
105
|
},
|
|
138
106
|
[api, jobName, subscription.reset]
|
|
139
107
|
);
|
|
140
|
-
const triggerAndWait =
|
|
108
|
+
const triggerAndWait = useCallback(
|
|
141
109
|
async (input) => {
|
|
142
110
|
const { runId } = await trigger(input);
|
|
143
111
|
return new Promise((resolve, reject) => {
|
|
@@ -157,13 +125,13 @@ function useJob(options) {
|
|
|
157
125
|
},
|
|
158
126
|
[trigger, subscription.status, subscription.output, subscription.error]
|
|
159
127
|
);
|
|
160
|
-
const reset =
|
|
128
|
+
const reset = useCallback(() => {
|
|
161
129
|
subscription.reset();
|
|
162
130
|
setCurrentRunId(null);
|
|
163
131
|
setIsPending(false);
|
|
164
132
|
}, [subscription.reset]);
|
|
165
133
|
const effectiveStatus = subscription.status ?? (isPending ? "pending" : null);
|
|
166
|
-
|
|
134
|
+
useEffect(() => {
|
|
167
135
|
if (subscription.status && isPending) {
|
|
168
136
|
setIsPending(false);
|
|
169
137
|
}
|
|
@@ -196,7 +164,7 @@ function useJobLogs(options) {
|
|
|
196
164
|
}
|
|
197
165
|
|
|
198
166
|
// src/client/use-job-run.ts
|
|
199
|
-
import { useEffect as
|
|
167
|
+
import { useEffect as useEffect2, useRef } from "react";
|
|
200
168
|
function useJobRun(options) {
|
|
201
169
|
const { api, runId, onStart, onComplete, onFail } = options;
|
|
202
170
|
const subscription = useSSESubscription(api, runId);
|
|
@@ -206,8 +174,8 @@ function useJobRun(options) {
|
|
|
206
174
|
const isPending = effectiveStatus === "pending";
|
|
207
175
|
const isRunning = effectiveStatus === "running";
|
|
208
176
|
const isCancelled = effectiveStatus === "cancelled";
|
|
209
|
-
const prevStatusRef =
|
|
210
|
-
|
|
177
|
+
const prevStatusRef = useRef(null);
|
|
178
|
+
useEffect2(() => {
|
|
211
179
|
const prevStatus = prevStatusRef.current;
|
|
212
180
|
prevStatusRef.current = effectiveStatus;
|
|
213
181
|
if (prevStatus !== effectiveStatus) {
|
|
@@ -282,17 +250,20 @@ function createJobHooks(options) {
|
|
|
282
250
|
}
|
|
283
251
|
|
|
284
252
|
// src/client/use-runs.ts
|
|
285
|
-
import { useCallback as
|
|
286
|
-
function useRuns(
|
|
287
|
-
const
|
|
288
|
-
const
|
|
289
|
-
const
|
|
290
|
-
const
|
|
291
|
-
const [
|
|
292
|
-
const [
|
|
293
|
-
const
|
|
294
|
-
const
|
|
295
|
-
const
|
|
253
|
+
import { useCallback as useCallback2, useEffect as useEffect3, useRef as useRef2, useState as useState2 } from "react";
|
|
254
|
+
function useRuns(jobDefinitionOrOptions, optionsArg) {
|
|
255
|
+
const isJob = isJobDefinition(jobDefinitionOrOptions);
|
|
256
|
+
const jobName = isJob ? jobDefinitionOrOptions.name : jobDefinitionOrOptions.jobName;
|
|
257
|
+
const options = isJob ? optionsArg : jobDefinitionOrOptions;
|
|
258
|
+
const { api, status, pageSize = 10 } = options;
|
|
259
|
+
const [runs, setRuns] = useState2([]);
|
|
260
|
+
const [page, setPage] = useState2(0);
|
|
261
|
+
const [hasMore, setHasMore] = useState2(false);
|
|
262
|
+
const [isLoading, setIsLoading] = useState2(false);
|
|
263
|
+
const [error, setError] = useState2(null);
|
|
264
|
+
const isMountedRef = useRef2(true);
|
|
265
|
+
const eventSourceRef = useRef2(null);
|
|
266
|
+
const refresh = useCallback2(async () => {
|
|
296
267
|
setIsLoading(true);
|
|
297
268
|
setError(null);
|
|
298
269
|
try {
|
|
@@ -321,14 +292,14 @@ function useRuns(options) {
|
|
|
321
292
|
}
|
|
322
293
|
}
|
|
323
294
|
}, [api, jobName, status, pageSize, page]);
|
|
324
|
-
|
|
295
|
+
useEffect3(() => {
|
|
325
296
|
isMountedRef.current = true;
|
|
326
297
|
refresh();
|
|
327
298
|
return () => {
|
|
328
299
|
isMountedRef.current = false;
|
|
329
300
|
};
|
|
330
301
|
}, [refresh]);
|
|
331
|
-
|
|
302
|
+
useEffect3(() => {
|
|
332
303
|
if (page !== 0) {
|
|
333
304
|
if (eventSourceRef.current) {
|
|
334
305
|
eventSourceRef.current.close();
|
|
@@ -374,15 +345,15 @@ function useRuns(options) {
|
|
|
374
345
|
eventSourceRef.current = null;
|
|
375
346
|
};
|
|
376
347
|
}, [api, jobName, page, refresh]);
|
|
377
|
-
const nextPage =
|
|
348
|
+
const nextPage = useCallback2(() => {
|
|
378
349
|
if (hasMore) {
|
|
379
350
|
setPage((p) => p + 1);
|
|
380
351
|
}
|
|
381
352
|
}, [hasMore]);
|
|
382
|
-
const prevPage =
|
|
353
|
+
const prevPage = useCallback2(() => {
|
|
383
354
|
setPage((p) => Math.max(0, p - 1));
|
|
384
355
|
}, []);
|
|
385
|
-
const goToPage =
|
|
356
|
+
const goToPage = useCallback2((newPage) => {
|
|
386
357
|
setPage(Math.max(0, newPage));
|
|
387
358
|
}, []);
|
|
388
359
|
return {
|
|
@@ -399,12 +370,12 @@ function useRuns(options) {
|
|
|
399
370
|
}
|
|
400
371
|
|
|
401
372
|
// src/client/use-run-actions.ts
|
|
402
|
-
import { useCallback as
|
|
373
|
+
import { useCallback as useCallback3, useState as useState3 } from "react";
|
|
403
374
|
function useRunActions(options) {
|
|
404
375
|
const { api } = options;
|
|
405
|
-
const [isLoading, setIsLoading] =
|
|
406
|
-
const [error, setError] =
|
|
407
|
-
const retry =
|
|
376
|
+
const [isLoading, setIsLoading] = useState3(false);
|
|
377
|
+
const [error, setError] = useState3(null);
|
|
378
|
+
const retry = useCallback3(
|
|
408
379
|
async (runId) => {
|
|
409
380
|
setIsLoading(true);
|
|
410
381
|
setError(null);
|
|
@@ -432,7 +403,7 @@ function useRunActions(options) {
|
|
|
432
403
|
},
|
|
433
404
|
[api]
|
|
434
405
|
);
|
|
435
|
-
const cancel =
|
|
406
|
+
const cancel = useCallback3(
|
|
436
407
|
async (runId) => {
|
|
437
408
|
setIsLoading(true);
|
|
438
409
|
setError(null);
|
|
@@ -460,7 +431,7 @@ function useRunActions(options) {
|
|
|
460
431
|
},
|
|
461
432
|
[api]
|
|
462
433
|
);
|
|
463
|
-
const deleteRun =
|
|
434
|
+
const deleteRun = useCallback3(
|
|
464
435
|
async (runId) => {
|
|
465
436
|
setIsLoading(true);
|
|
466
437
|
setError(null);
|
|
@@ -488,7 +459,7 @@ function useRunActions(options) {
|
|
|
488
459
|
},
|
|
489
460
|
[api]
|
|
490
461
|
);
|
|
491
|
-
const getRun =
|
|
462
|
+
const getRun = useCallback3(
|
|
492
463
|
async (runId) => {
|
|
493
464
|
setIsLoading(true);
|
|
494
465
|
setError(null);
|
|
@@ -520,7 +491,7 @@ function useRunActions(options) {
|
|
|
520
491
|
},
|
|
521
492
|
[api]
|
|
522
493
|
);
|
|
523
|
-
const getSteps =
|
|
494
|
+
const getSteps = useCallback3(
|
|
524
495
|
async (runId) => {
|
|
525
496
|
setIsLoading(true);
|
|
526
497
|
setError(null);
|
package/dist/client.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/client/use-job.ts","../src/client/use-sse-subscription.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 { useCallback, useEffect, useRef, useState } from 'react'\nimport type { DurablyEvent, LogEntry, Progress, RunStatus } from '../types'\n\nexport interface SSESubscriptionState<TOutput = unknown> {\n status: RunStatus | null\n output: TOutput | null\n error: string | null\n logs: LogEntry[]\n progress: Progress | null\n}\n\nexport interface UseSSESubscriptionOptions {\n /**\n * Maximum number of logs to keep (0 = unlimited)\n */\n maxLogs?: number\n}\n\nexport interface UseSSESubscriptionResult<\n TOutput = unknown,\n> extends SSESubscriptionState<TOutput> {\n /**\n * Clear all logs\n */\n clearLogs: () => void\n /**\n * Reset all state\n */\n reset: () => void\n}\n\n/**\n * Internal hook for subscribing to run events via SSE.\n * Used by client-mode hooks (useJob, useJobRun, useJobLogs).\n */\nexport function useSSESubscription<TOutput = unknown>(\n api: string | null,\n runId: string | null,\n options?: UseSSESubscriptionOptions,\n): UseSSESubscriptionResult<TOutput> {\n const [status, setStatus] = useState<RunStatus | null>(null)\n const [output, setOutput] = useState<TOutput | null>(null)\n const [error, setError] = useState<string | null>(null)\n const [logs, setLogs] = useState<LogEntry[]>([])\n const [progress, setProgress] = useState<Progress | null>(null)\n\n const eventSourceRef = useRef<EventSource | null>(null)\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 // Only reset if this isn't the initial render (runIdRef already set)\n if (runIdRef.current !== runId) {\n setStatus(null)\n setOutput(null)\n setError(null)\n setLogs([])\n setProgress(null)\n }\n }\n runIdRef.current = runId\n\n // Subscribe to SSE events\n useEffect(() => {\n if (!api || !runId) return\n\n const url = `${api}/subscribe?runId=${encodeURIComponent(runId)}`\n const eventSource = new EventSource(url)\n eventSourceRef.current = eventSource\n\n eventSource.onmessage = (event) => {\n try {\n const data = JSON.parse(event.data) as DurablyEvent\n if (data.runId !== runIdRef.current) return\n\n switch (data.type) {\n case 'run:start':\n setStatus('running')\n break\n case 'run:complete':\n setStatus('completed')\n setOutput(data.output as TOutput)\n break\n case 'run:fail':\n setStatus('failed')\n setError(data.error)\n break\n case 'run:cancel':\n setStatus('cancelled')\n break\n case 'run:retry':\n setStatus('pending')\n setError(null)\n break\n case 'run:progress':\n setProgress(data.progress)\n break\n case 'log:write':\n setLogs((prev) => {\n const newLog: LogEntry = {\n id: crypto.randomUUID(),\n runId: data.runId,\n stepName: null,\n level: data.level,\n message: data.message,\n data: data.data,\n timestamp: new Date().toISOString(),\n }\n const newLogs = [...prev, newLog]\n if (maxLogs > 0 && newLogs.length > maxLogs) {\n return newLogs.slice(-maxLogs)\n }\n return newLogs\n })\n break\n }\n } catch {\n // Ignore parse errors\n }\n }\n\n eventSource.onerror = () => {\n setError('Connection failed')\n eventSource.close()\n }\n\n return () => {\n eventSource.close()\n eventSourceRef.current = null\n }\n }, [api, runId, maxLogs])\n\n const clearLogs = useCallback(() => {\n setLogs([])\n }, [])\n\n const reset = useCallback(() => {\n setStatus(null)\n setOutput(null)\n setError(null)\n setLogs([])\n setProgress(null)\n }, [])\n\n return {\n status,\n output,\n error,\n logs,\n progress,\n clearLogs,\n reset,\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 { JobDefinition } from '@coji/durably'\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 * Extract input type from a JobDefinition or JobHandle\n */\ntype 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\n/**\n * Extract output type from a JobDefinition or JobHandle\n */\ntype 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\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 { 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 * Extract input type from a JobDefinition\n */\ntype InferInput<T> =\n T extends JobDefinition<string, infer TInput, unknown>\n ? TInput extends Record<string, unknown>\n ? TInput\n : Record<string, unknown>\n : Record<string, unknown>\n\n/**\n * Extract output type from a JobDefinition\n */\ntype InferOutput<T> =\n T extends JobDefinition<string, unknown, infer TOutput>\n ? TOutput extends Record<string, unknown>\n ? TOutput\n : Record<string, unknown>\n : Record<string, unknown>\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":"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","names":["useCallback","useEffect","useState","useState","useCallback","useEffect","useEffect","useRef","useRef","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, useState } from 'react'\nimport type { LogEntry, Progress, RunStatus } from '../types'\nimport { useSSESubscription } from './use-sse-subscription'\n\nexport interface UseJobClientOptions {\n /**\n * API endpoint URL (e.g., '/api/durably')\n */\n api: string\n /**\n * Job name to trigger\n */\n jobName: string\n /**\n * Initial Run ID to subscribe to (for reconnection scenarios)\n * When provided, the hook will immediately start subscribing to this run\n */\n initialRunId?: string\n}\n\nexport interface UseJobClientResult<TInput, TOutput> {\n /**\n * Whether the hook is ready (always true for client mode)\n */\n /**\n * Trigger the job with the given input\n */\n trigger: (input: TInput) => Promise<{ runId: string }>\n /**\n * Trigger and wait for completion\n */\n triggerAndWait: (input: TInput) => Promise<{ runId: string; output: TOutput }>\n /**\n * Current run status\n */\n status: RunStatus | null\n /**\n * Output from completed run\n */\n output: TOutput | null\n /**\n * Error message from failed run\n */\n error: string | null\n /**\n * Logs collected during execution\n */\n logs: LogEntry[]\n /**\n * Current progress\n */\n progress: Progress | null\n /**\n * Whether a run is currently running\n */\n isRunning: boolean\n /**\n * Whether a run is pending\n */\n isPending: boolean\n /**\n * Whether the run completed successfully\n */\n isCompleted: boolean\n /**\n * Whether the run failed\n */\n isFailed: boolean\n /**\n * Current run ID\n */\n currentRunId: string | null\n /**\n * Reset all state\n */\n reset: () => void\n}\n\n/**\n * Hook for triggering and subscribing to jobs via server API.\n * Uses fetch for triggering and EventSource for SSE subscription.\n */\nexport function useJob<\n TInput extends Record<string, unknown> = Record<string, unknown>,\n TOutput extends Record<string, unknown> = Record<string, unknown>,\n>(options: UseJobClientOptions): UseJobClientResult<TInput, TOutput> {\n const { api, jobName, initialRunId } = options\n\n const [currentRunId, setCurrentRunId] = useState<string | null>(\n initialRunId ?? null,\n )\n const [isPending, setIsPending] = useState(false)\n\n const subscription = useSSESubscription<TOutput>(api, currentRunId)\n\n const trigger = useCallback(\n async (input: TInput): Promise<{ runId: string }> => {\n // Reset state\n subscription.reset()\n setIsPending(true)\n\n const response = await fetch(`${api}/trigger`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({ jobName, input }),\n })\n\n if (!response.ok) {\n setIsPending(false)\n const errorText = await response.text()\n throw new Error(errorText || `HTTP ${response.status}`)\n }\n\n const { runId } = (await response.json()) as { runId: string }\n setCurrentRunId(runId)\n\n return { runId }\n },\n [api, jobName, subscription.reset],\n )\n\n const triggerAndWait = useCallback(\n async (input: TInput): Promise<{ runId: string; output: TOutput }> => {\n const { runId } = await trigger(input)\n\n return new Promise((resolve, reject) => {\n const checkInterval = setInterval(() => {\n if (subscription.status === 'completed' && subscription.output) {\n clearInterval(checkInterval)\n resolve({ runId, output: subscription.output })\n } else if (subscription.status === 'failed') {\n clearInterval(checkInterval)\n reject(new Error(subscription.error ?? 'Job failed'))\n } else if (subscription.status === 'cancelled') {\n clearInterval(checkInterval)\n reject(new Error('Job cancelled'))\n }\n }, 50)\n })\n },\n [trigger, subscription.status, subscription.output, subscription.error],\n )\n\n const reset = useCallback(() => {\n subscription.reset()\n setCurrentRunId(null)\n setIsPending(false)\n }, [subscription.reset])\n\n // Compute effective status (pending overrides null when we've triggered but SSE hasn't started)\n const effectiveStatus = subscription.status ?? (isPending ? 'pending' : null)\n\n // Clear pending when we get a real status\n useEffect(() => {\n if (subscription.status && isPending) {\n setIsPending(false)\n }\n }, [subscription.status, isPending])\n\n return {\n trigger,\n triggerAndWait,\n status: effectiveStatus,\n output: subscription.output,\n error: subscription.error,\n logs: subscription.logs,\n progress: subscription.progress,\n isRunning: effectiveStatus === 'running',\n isPending: effectiveStatus === 'pending',\n isCompleted: effectiveStatus === 'completed',\n isFailed: effectiveStatus === 'failed',\n currentRunId,\n reset,\n }\n}\n","import { useMemo } from 'react'\nimport { createSSEEventSubscriber } from '../shared/sse-event-subscriber'\nimport {\n useSubscription,\n type UseSubscriptionOptions,\n type UseSubscriptionResult,\n} from '../shared/use-subscription'\nimport type { SubscriptionState } from '../types'\n\n/** @deprecated Use SubscriptionState from '../types' instead */\nexport type SSESubscriptionState<TOutput = unknown> = SubscriptionState<TOutput>\n\n/** @deprecated Use UseSubscriptionOptions from '../shared/use-subscription' instead */\nexport type UseSSESubscriptionOptions = UseSubscriptionOptions\n\n/** @deprecated Use UseSubscriptionResult from '../shared/use-subscription' instead */\nexport type UseSSESubscriptionResult<TOutput = unknown> =\n UseSubscriptionResult<TOutput>\n\n/**\n * Internal hook for subscribing to run events via SSE.\n * Used by client-mode hooks (useJob, useJobRun, useJobLogs).\n *\n * @deprecated Consider using useSubscription with createSSEEventSubscriber directly.\n */\nexport function useSSESubscription<TOutput = unknown>(\n api: string | null,\n runId: string | null,\n options?: UseSSESubscriptionOptions,\n): UseSSESubscriptionResult<TOutput> {\n const subscriber = useMemo(\n () => (api ? createSSEEventSubscriber(api) : null),\n [api],\n )\n\n return useSubscription<TOutput>(subscriber, runId, options)\n}\n","import type { DurablyEvent } from '../types'\nimport type { EventSubscriber, SubscriptionEvent } from './event-subscriber'\n\n/**\n * EventSubscriber implementation using Server-Sent Events (SSE).\n * Used in client environments that communicate with a Durably server via HTTP.\n */\nexport function createSSEEventSubscriber(apiBaseUrl: string): EventSubscriber {\n return {\n subscribe<TOutput = unknown>(\n runId: string,\n onEvent: (event: SubscriptionEvent<TOutput>) => void,\n ): () => void {\n const url = `${apiBaseUrl}/subscribe?runId=${encodeURIComponent(runId)}`\n const eventSource = new EventSource(url)\n\n eventSource.onmessage = (messageEvent) => {\n try {\n const data = JSON.parse(messageEvent.data) as DurablyEvent\n if (data.runId !== runId) return\n\n switch (data.type) {\n case 'run:start':\n onEvent({ type: 'run:start' })\n break\n case 'run:complete':\n onEvent({\n type: 'run:complete',\n output: data.output as TOutput,\n })\n break\n case 'run:fail':\n onEvent({ type: 'run:fail', error: data.error })\n break\n case 'run:cancel':\n onEvent({ type: 'run:cancel' })\n break\n case 'run:retry':\n onEvent({ type: 'run:retry' })\n break\n case 'run:progress':\n onEvent({ type: 'run:progress', progress: data.progress })\n break\n case 'log:write':\n onEvent({\n type: 'log:write',\n runId: data.runId,\n stepName: null,\n level: data.level,\n message: data.message,\n data: data.data,\n })\n break\n }\n } catch {\n // Ignore parse errors\n }\n }\n\n eventSource.onerror = () => {\n onEvent({ type: 'connection_error', error: 'Connection failed' })\n eventSource.close()\n }\n\n return () => {\n eventSource.close()\n }\n },\n }\n}\n","import type { LogEntry } from '../types'\nimport { useSSESubscription } from './use-sse-subscription'\n\nexport interface UseJobLogsClientOptions {\n /**\n * API endpoint URL (e.g., '/api/durably')\n */\n api: string\n /**\n * The run ID to subscribe to logs for\n */\n runId: string | null\n /**\n * Maximum number of logs to keep (default: unlimited)\n */\n maxLogs?: number\n}\n\nexport interface UseJobLogsClientResult {\n /**\n * Whether the hook is ready (always true for client mode)\n */\n /**\n * Logs collected during execution\n */\n logs: LogEntry[]\n /**\n * Clear all logs\n */\n clearLogs: () => void\n}\n\n/**\n * Hook for subscribing to logs from a run via server API.\n * Uses EventSource for SSE subscription.\n */\nexport function useJobLogs(\n options: UseJobLogsClientOptions,\n): UseJobLogsClientResult {\n const { api, runId, maxLogs } = options\n\n const subscription = useSSESubscription(api, runId, { maxLogs })\n\n return {\n logs: subscription.logs,\n clearLogs: subscription.clearLogs,\n }\n}\n","import { useEffect, useRef } from 'react'\nimport type { LogEntry, Progress, RunStatus } from '../types'\nimport { useSSESubscription } from './use-sse-subscription'\n\nexport interface UseJobRunClientOptions {\n /**\n * API endpoint URL (e.g., '/api/durably')\n */\n api: string\n /**\n * The run ID to subscribe to\n */\n runId: string | null\n /**\n * Callback when run starts (transitions to pending/running)\n */\n onStart?: () => void\n /**\n * Callback when run completes successfully\n */\n onComplete?: () => void\n /**\n * Callback when run fails\n */\n onFail?: () => void\n}\n\nexport interface UseJobRunClientResult<TOutput = unknown> {\n /**\n * Whether the hook is ready (always true for client mode)\n */\n /**\n * Current run status\n */\n status: RunStatus | null\n /**\n * Output from completed run\n */\n output: TOutput | null\n /**\n * Error message from failed run\n */\n error: string | null\n /**\n * Logs collected during execution\n */\n logs: LogEntry[]\n /**\n * Current progress\n */\n progress: Progress | null\n /**\n * Whether a run is currently running\n */\n isRunning: boolean\n /**\n * Whether a run is pending\n */\n isPending: boolean\n /**\n * Whether the run completed successfully\n */\n isCompleted: boolean\n /**\n * Whether the run failed\n */\n isFailed: boolean\n /**\n * Whether the run was cancelled\n */\n isCancelled: boolean\n}\n\n/**\n * Hook for subscribing to an existing run via server API.\n * Uses EventSource for SSE subscription.\n */\nexport function useJobRun<TOutput = unknown>(\n options: UseJobRunClientOptions,\n): UseJobRunClientResult<TOutput> {\n const { api, runId, onStart, onComplete, onFail } = options\n\n const subscription = useSSESubscription<TOutput>(api, runId)\n\n // If we have a runId but no status yet, treat as pending\n const effectiveStatus = subscription.status ?? (runId ? 'pending' : null)\n\n const isCompleted = effectiveStatus === 'completed'\n const isFailed = effectiveStatus === 'failed'\n const isPending = effectiveStatus === 'pending'\n const isRunning = effectiveStatus === 'running'\n const isCancelled = effectiveStatus === 'cancelled'\n\n // Track previous status to detect transitions (use effectiveStatus, not subscription.status)\n const prevStatusRef = useRef<RunStatus | null>(null)\n\n useEffect(() => {\n const prevStatus = prevStatusRef.current\n prevStatusRef.current = effectiveStatus\n\n // Only fire callbacks on status transitions\n if (prevStatus !== effectiveStatus) {\n // Fire onStart when transitioning from null to pending/running\n if (prevStatus === null && (isPending || isRunning) && onStart) {\n onStart()\n }\n if (isCompleted && onComplete) {\n onComplete()\n }\n if (isFailed && onFail) {\n onFail()\n }\n }\n }, [\n effectiveStatus,\n isPending,\n isRunning,\n isCompleted,\n isFailed,\n onStart,\n onComplete,\n onFail,\n ])\n\n return {\n status: effectiveStatus,\n output: subscription.output,\n error: subscription.error,\n logs: subscription.logs,\n progress: subscription.progress,\n isRunning,\n isPending,\n isCompleted,\n isFailed,\n isCancelled,\n }\n}\n","import type { InferInput, InferOutput } from '../types'\nimport { useJob, type UseJobClientResult } from './use-job'\nimport { useJobLogs, type UseJobLogsClientResult } from './use-job-logs'\nimport { useJobRun, type UseJobRunClientResult } from './use-job-run'\n\n/**\n * Type-safe hooks for a specific job\n */\nexport interface JobClient<TInput, TOutput> {\n /**\n * Hook for triggering and monitoring the job\n */\n useJob: () => UseJobClientResult<TInput, TOutput>\n\n /**\n * Hook for subscribing to an existing run by ID\n */\n useRun: (runId: string | null) => UseJobRunClientResult<TOutput>\n\n /**\n * Hook for subscribing to logs from a run\n */\n useLogs: (\n runId: string | null,\n options?: { maxLogs?: number },\n ) => UseJobLogsClientResult\n}\n\n/**\n * Options for createDurablyClient\n */\nexport interface CreateDurablyClientOptions {\n /**\n * API endpoint URL (e.g., '/api/durably')\n */\n api: string\n}\n\n/**\n * A type-safe client with hooks for each registered job\n */\nexport type DurablyClient<TJobs extends Record<string, unknown>> = {\n [K in keyof TJobs]: JobClient<InferInput<TJobs[K]>, InferOutput<TJobs[K]>>\n}\n\n/**\n * Create a type-safe Durably client with hooks for all registered jobs.\n *\n * @example\n * ```tsx\n * // Server: register jobs\n * // app/lib/durably.server.ts\n * export const jobs = durably.register({\n * importCsv: importCsvJob,\n * syncUsers: syncUsersJob,\n * })\n *\n * // Client: create typed client\n * // app/lib/durably.client.ts\n * import type { jobs } from '~/lib/durably.server'\n * import { createDurablyClient } from '@coji/durably-react/client'\n *\n * export const durably = createDurablyClient<typeof jobs>({\n * api: '/api/durably',\n * })\n *\n * // In your component - fully type-safe with autocomplete\n * function CsvImporter() {\n * const { trigger, output, isRunning } = durably.importCsv.useJob()\n *\n * return (\n * <button onClick={() => trigger({ rows: [...] })}>\n * Import\n * </button>\n * )\n * }\n * ```\n */\nexport function createDurablyClient<TJobs extends Record<string, unknown>>(\n options: CreateDurablyClientOptions,\n): DurablyClient<TJobs> {\n const { api } = options\n\n // Create a proxy that generates job clients on demand\n return new Proxy({} as DurablyClient<TJobs>, {\n get(_target, jobKey: string) {\n return {\n useJob: () => {\n return useJob({ api, jobName: jobKey })\n },\n\n useRun: (runId: string | null) => {\n return useJobRun({ api, runId })\n },\n\n useLogs: (runId: string | null, logsOptions?: { maxLogs?: number }) => {\n return useJobLogs({ api, runId, maxLogs: logsOptions?.maxLogs })\n },\n }\n },\n })\n}\n","import type { JobDefinition } from '@coji/durably'\nimport type { InferInput, InferOutput } from '../types'\nimport { useJob, type UseJobClientResult } from './use-job'\nimport { useJobLogs, type UseJobLogsClientResult } from './use-job-logs'\nimport { useJobRun, type UseJobRunClientResult } from './use-job-run'\n\n/**\n * Options for createJobHooks\n */\nexport interface CreateJobHooksOptions {\n /**\n * API endpoint URL (e.g., '/api/durably')\n */\n api: string\n /**\n * Job name (must match the server-side job name)\n */\n jobName: string\n}\n\n/**\n * Type-safe hooks for a specific job\n */\nexport interface JobHooks<TInput, TOutput> {\n /**\n * Hook for triggering and monitoring the job\n */\n useJob: () => UseJobClientResult<TInput, TOutput>\n\n /**\n * Hook for subscribing to an existing run by ID\n */\n useRun: (runId: string | null) => UseJobRunClientResult<TOutput>\n\n /**\n * Hook for subscribing to logs from a run\n */\n useLogs: (\n runId: string | null,\n options?: { maxLogs?: number },\n ) => UseJobLogsClientResult\n}\n\n/**\n * Create type-safe hooks for a specific job.\n *\n * @example\n * ```tsx\n * // Import job type from server (type-only import is safe)\n * import type { importCsvJob } from '~/lib/durably.server'\n * import { createJobHooks } from '@coji/durably-react/client'\n *\n * const importCsv = createJobHooks<typeof importCsvJob>({\n * api: '/api/durably',\n * jobName: 'import-csv',\n * })\n *\n * // In your component - fully type-safe\n * function CsvImporter() {\n * const { trigger, output, progress, isRunning } = importCsv.useJob()\n *\n * return (\n * <button onClick={() => trigger({ rows: [...] })}>\n * Import\n * </button>\n * )\n * }\n * ```\n */\nexport function createJobHooks<\n // biome-ignore lint/suspicious/noExplicitAny: TJob needs to accept any JobDefinition\n TJob extends JobDefinition<string, any, any>,\n>(\n options: CreateJobHooksOptions,\n): JobHooks<InferInput<TJob>, InferOutput<TJob>> {\n const { api, jobName } = options\n\n return {\n useJob: () => {\n return useJob<InferInput<TJob>, InferOutput<TJob>>({ api, jobName })\n },\n\n useRun: (runId: string | null) => {\n return useJobRun<InferOutput<TJob>>({ api, runId })\n },\n\n useLogs: (runId: string | null, logsOptions?: { maxLogs?: number }) => {\n return useJobLogs({ api, runId, maxLogs: logsOptions?.maxLogs })\n },\n }\n}\n","import type { JobDefinition } from '@coji/durably'\nimport { useCallback, useEffect, useRef, useState } from 'react'\nimport {\n type Progress,\n type RunStatus,\n type TypedClientRun,\n isJobDefinition,\n} from '../types'\n\n// Re-export types for convenience\nexport type { ClientRun, TypedClientRun } from '../types'\n\n/**\n * SSE notification event from /runs/subscribe\n */\ntype RunUpdateEvent =\n | {\n type:\n | 'run:trigger'\n | 'run:start'\n | 'run:complete'\n | 'run:fail'\n | 'run:cancel'\n | 'run:retry'\n runId: string\n jobName: string\n }\n | { type: 'run:progress'; runId: string; jobName: string; progress: Progress }\n | {\n type: 'step:start' | 'step:complete'\n runId: string\n jobName: string\n stepName: string\n stepIndex: number\n }\n | {\n type: 'step:fail'\n runId: string\n jobName: string\n stepName: string\n stepIndex: number\n error: string\n }\n | {\n type: 'log:write'\n runId: string\n stepName: string | null\n level: 'info' | 'warn' | 'error'\n message: string\n data: unknown\n }\n\nexport interface UseRunsClientOptions {\n /**\n * API endpoint URL (e.g., '/api/durably')\n */\n api: string\n /**\n * Filter by job name\n */\n jobName?: string\n /**\n * Filter by status\n */\n status?: RunStatus\n /**\n * Number of runs per page\n * @default 10\n */\n pageSize?: number\n}\n\nexport interface UseRunsClientResult<\n TInput extends Record<string, unknown> = Record<string, unknown>,\n TOutput extends Record<string, unknown> | undefined =\n | Record<string, unknown>\n | undefined,\n> {\n /**\n * List of runs for the current page\n */\n runs: TypedClientRun<TInput, TOutput>[]\n /**\n * Current page (0-indexed)\n */\n page: number\n /**\n * Whether there are more pages\n */\n hasMore: boolean\n /**\n * Whether data is being loaded\n */\n isLoading: boolean\n /**\n * Error message if fetch failed\n */\n error: string | null\n /**\n * Go to the next page\n */\n nextPage: () => void\n /**\n * Go to the previous page\n */\n prevPage: () => void\n /**\n * Go to a specific page\n */\n goToPage: (page: number) => void\n /**\n * Refresh the current page\n */\n refresh: () => Promise<void>\n}\n\n/**\n * Hook for listing runs via server API with pagination.\n * First page (page 0) automatically subscribes to SSE for real-time updates.\n * Other pages are static and require manual refresh.\n *\n * @example With generic type parameter (dashboard with multiple job types)\n * ```tsx\n * type DashboardRun = TypedClientRun<ImportInput, ImportOutput> | TypedClientRun<SyncInput, SyncOutput>\n *\n * function Dashboard() {\n * const { runs } = useRuns<DashboardRun>({ api: '/api/durably', pageSize: 10 })\n * // runs are typed as DashboardRun[]\n * }\n * ```\n *\n * @example With JobDefinition (single job, auto-filters by jobName)\n * ```tsx\n * const myJob = defineJob({ name: 'my-job', ... })\n *\n * function RunHistory() {\n * const { runs } = useRuns(myJob, { api: '/api/durably' })\n * // runs[0].output is typed!\n * return <div>{runs[0]?.output?.someField}</div>\n * }\n * ```\n *\n * @example With options only (untyped)\n * ```tsx\n * function RunHistory() {\n * const { runs } = useRuns({ api: '/api/durably', pageSize: 10 })\n * // runs[0].output is unknown\n * }\n * ```\n */\n// Overload 1: With generic type parameter\nexport function useRuns<\n TRun extends TypedClientRun<\n Record<string, unknown>,\n Record<string, unknown> | undefined\n >,\n>(\n options: UseRunsClientOptions,\n): UseRunsClientResult<\n TRun extends TypedClientRun<infer I, infer _O> ? I : Record<string, unknown>,\n TRun extends TypedClientRun<infer _I, infer O> ? O : Record<string, unknown>\n>\n\n// Overload 2: With JobDefinition for type inference (auto-filters by jobName)\nexport function useRuns<\n TName extends string,\n TInput extends Record<string, unknown>,\n TOutput extends Record<string, unknown> | undefined,\n>(\n jobDefinition: JobDefinition<TName, TInput, TOutput>,\n options: Omit<UseRunsClientOptions, 'jobName'>,\n): UseRunsClientResult<TInput, TOutput>\n\n// Overload 3: Without type parameter (untyped, backward compatible)\nexport function useRuns(options: UseRunsClientOptions): UseRunsClientResult\n\n// Implementation\nexport function useRuns<\n TName extends string,\n TInput extends Record<string, unknown>,\n TOutput extends Record<string, unknown> | undefined,\n>(\n jobDefinitionOrOptions:\n | JobDefinition<TName, TInput, TOutput>\n | UseRunsClientOptions,\n optionsArg?: Omit<UseRunsClientOptions, 'jobName'>,\n): UseRunsClientResult<TInput, TOutput> {\n // Determine if first argument is a JobDefinition using type guard\n const isJob = isJobDefinition(jobDefinitionOrOptions)\n\n const jobName = isJob\n ? jobDefinitionOrOptions.name\n : (jobDefinitionOrOptions as UseRunsClientOptions).jobName\n\n const options = isJob\n ? (optionsArg as Omit<UseRunsClientOptions, 'jobName'>)\n : (jobDefinitionOrOptions as UseRunsClientOptions)\n\n const { api, status, pageSize = 10 } = options\n\n const [runs, setRuns] = useState<TypedClientRun<TInput, TOutput>[]>([])\n const [page, setPage] = useState(0)\n const [hasMore, setHasMore] = useState(false)\n const [isLoading, setIsLoading] = useState(false)\n const [error, setError] = useState<string | null>(null)\n\n const isMountedRef = useRef(true)\n const eventSourceRef = useRef<EventSource | null>(null)\n\n const refresh = useCallback(async () => {\n setIsLoading(true)\n setError(null)\n\n try {\n const params = new URLSearchParams()\n if (jobName) params.set('jobName', jobName)\n if (status) params.set('status', status)\n params.set('limit', String(pageSize + 1))\n params.set('offset', String(page * pageSize))\n\n const url = `${api}/runs?${params.toString()}`\n const response = await fetch(url)\n\n if (!response.ok) {\n throw new Error(`Failed to fetch runs: ${response.statusText}`)\n }\n\n const data = (await response.json()) as TypedClientRun<TInput, TOutput>[]\n\n if (isMountedRef.current) {\n setHasMore(data.length > pageSize)\n setRuns(data.slice(0, pageSize))\n }\n } catch (err) {\n if (isMountedRef.current) {\n setError(err instanceof Error ? err.message : 'Unknown error')\n }\n } finally {\n if (isMountedRef.current) {\n setIsLoading(false)\n }\n }\n }, [api, jobName, status, pageSize, page])\n\n // Initial fetch\n useEffect(() => {\n isMountedRef.current = true\n refresh()\n\n return () => {\n isMountedRef.current = false\n }\n }, [refresh])\n\n // SSE subscription for first page only\n useEffect(() => {\n // Only subscribe to SSE on first page\n if (page !== 0) {\n // Clean up any existing connection when navigating away from first page\n if (eventSourceRef.current) {\n eventSourceRef.current.close()\n eventSourceRef.current = null\n }\n return\n }\n\n // Build SSE URL\n const params = new URLSearchParams()\n if (jobName) params.set('jobName', jobName)\n const sseUrl = `${api}/runs/subscribe${params.toString() ? `?${params.toString()}` : ''}`\n\n const eventSource = new EventSource(sseUrl)\n eventSourceRef.current = eventSource\n\n eventSource.onmessage = (event) => {\n try {\n const data = JSON.parse(event.data) as RunUpdateEvent\n // On run lifecycle events, refresh the list\n if (\n data.type === 'run:trigger' ||\n data.type === 'run:start' ||\n data.type === 'run:complete' ||\n data.type === 'run:fail' ||\n data.type === 'run:cancel' ||\n data.type === 'run:retry'\n ) {\n refresh()\n }\n // On progress update, update the run in place\n if (data.type === 'run:progress') {\n setRuns((prev) =>\n prev.map((run) =>\n run.id === data.runId ? { ...run, progress: data.progress } : run,\n ),\n )\n }\n // On step complete, update currentStepIndex\n if (data.type === 'step:complete') {\n setRuns((prev) =>\n prev.map((run) =>\n run.id === data.runId\n ? { ...run, currentStepIndex: data.stepIndex + 1 }\n : run,\n ),\n )\n }\n // On step start or fail, refresh to get latest state\n if (data.type === 'step:start' || data.type === 'step:fail') {\n refresh()\n }\n // log:write is handled by useJobLogs, not useRuns\n } catch {\n // Ignore parse errors\n }\n }\n\n eventSource.onerror = () => {\n // EventSource will automatically reconnect\n }\n\n return () => {\n eventSource.close()\n eventSourceRef.current = null\n }\n }, [api, jobName, page, refresh])\n\n const nextPage = useCallback(() => {\n if (hasMore) {\n setPage((p) => p + 1)\n }\n }, [hasMore])\n\n const prevPage = useCallback(() => {\n setPage((p) => Math.max(0, p - 1))\n }, [])\n\n const goToPage = useCallback((newPage: number) => {\n setPage(Math.max(0, newPage))\n }, [])\n\n return {\n runs,\n page,\n hasMore,\n isLoading,\n error,\n nextPage,\n prevPage,\n goToPage,\n refresh,\n }\n}\n","import { useCallback, useState } from 'react'\n\n/**\n * Run record returned from the server API\n */\nexport interface RunRecord {\n id: string\n jobName: string\n status: 'pending' | 'running' | 'completed' | 'failed' | 'cancelled'\n payload: unknown\n output: unknown | null\n error: string | null\n progress: { current: number; total?: number; message?: string } | null\n currentStepIndex: number\n stepCount: number\n createdAt: string\n startedAt: string | null\n completedAt: string | null\n}\n\n/**\n * Step record returned from the server API\n */\nexport interface StepRecord {\n name: string\n status: 'completed' | 'failed'\n output: unknown\n}\n\nexport interface UseRunActionsClientOptions {\n /**\n * API endpoint URL (e.g., '/api/durably')\n */\n api: string\n}\n\nexport interface UseRunActionsClientResult {\n /**\n * Retry a failed or cancelled run\n */\n retry: (runId: string) => Promise<void>\n /**\n * Cancel a pending or running run\n */\n cancel: (runId: string) => Promise<void>\n /**\n * Delete a run (only completed, failed, or cancelled runs)\n */\n deleteRun: (runId: string) => Promise<void>\n /**\n * Get a single run by ID\n */\n getRun: (runId: string) => Promise<RunRecord | null>\n /**\n * Get steps for a run\n */\n getSteps: (runId: string) => Promise<StepRecord[]>\n /**\n * Whether an action is in progress\n */\n isLoading: boolean\n /**\n * Error message from last action\n */\n error: string | null\n}\n\n/**\n * Hook for run actions (retry, cancel) via server API.\n *\n * @example\n * ```tsx\n * function RunActions({ runId, status }: { runId: string; status: string }) {\n * const { retry, cancel, isLoading, error } = useRunActions({\n * api: '/api/durably',\n * })\n *\n * return (\n * <div>\n * {status === 'failed' && (\n * <button onClick={() => retry(runId)} disabled={isLoading}>\n * Retry\n * </button>\n * )}\n * {(status === 'pending' || status === 'running') && (\n * <button onClick={() => cancel(runId)} disabled={isLoading}>\n * Cancel\n * </button>\n * )}\n * {error && <span className=\"error\">{error}</span>}\n * </div>\n * )\n * }\n * ```\n */\nexport function useRunActions(\n options: UseRunActionsClientOptions,\n): UseRunActionsClientResult {\n const { api } = options\n\n const [isLoading, setIsLoading] = useState(false)\n const [error, setError] = useState<string | null>(null)\n\n const retry = useCallback(\n async (runId: string) => {\n setIsLoading(true)\n setError(null)\n\n try {\n const url = `${api}/retry?runId=${encodeURIComponent(runId)}`\n const response = await fetch(url, { method: 'POST' })\n\n if (!response.ok) {\n let errorMessage = `Failed to retry: ${response.statusText}`\n try {\n const data = await response.json()\n if (data.error) {\n errorMessage = data.error\n }\n } catch {\n // Response is not JSON, use statusText\n }\n throw new Error(errorMessage)\n }\n } catch (err) {\n const message = err instanceof Error ? err.message : 'Unknown error'\n setError(message)\n throw err\n } finally {\n setIsLoading(false)\n }\n },\n [api],\n )\n\n const cancel = useCallback(\n async (runId: string) => {\n setIsLoading(true)\n setError(null)\n\n try {\n const url = `${api}/cancel?runId=${encodeURIComponent(runId)}`\n const response = await fetch(url, { method: 'POST' })\n\n if (!response.ok) {\n let errorMessage = `Failed to cancel: ${response.statusText}`\n try {\n const data = await response.json()\n if (data.error) {\n errorMessage = data.error\n }\n } catch {\n // Response is not JSON, use statusText\n }\n throw new Error(errorMessage)\n }\n } catch (err) {\n const message = err instanceof Error ? err.message : 'Unknown error'\n setError(message)\n throw err\n } finally {\n setIsLoading(false)\n }\n },\n [api],\n )\n\n const deleteRun = useCallback(\n async (runId: string) => {\n setIsLoading(true)\n setError(null)\n\n try {\n const url = `${api}/run?runId=${encodeURIComponent(runId)}`\n const response = await fetch(url, { method: 'DELETE' })\n\n if (!response.ok) {\n let errorMessage = `Failed to delete: ${response.statusText}`\n try {\n const data = await response.json()\n if (data.error) {\n errorMessage = data.error\n }\n } catch {\n // Response is not JSON, use statusText\n }\n throw new Error(errorMessage)\n }\n } catch (err) {\n const message = err instanceof Error ? err.message : 'Unknown error'\n setError(message)\n throw err\n } finally {\n setIsLoading(false)\n }\n },\n [api],\n )\n\n const getRun = useCallback(\n async (runId: string): Promise<RunRecord | null> => {\n setIsLoading(true)\n setError(null)\n\n try {\n const url = `${api}/run?runId=${encodeURIComponent(runId)}`\n const response = await fetch(url)\n\n if (response.status === 404) {\n return null\n }\n\n if (!response.ok) {\n let errorMessage = `Failed to get run: ${response.statusText}`\n try {\n const data = await response.json()\n if (data.error) {\n errorMessage = data.error\n }\n } catch {\n // Response is not JSON, use statusText\n }\n throw new Error(errorMessage)\n }\n\n return (await response.json()) as RunRecord\n } catch (err) {\n const message = err instanceof Error ? err.message : 'Unknown error'\n setError(message)\n throw err\n } finally {\n setIsLoading(false)\n }\n },\n [api],\n )\n\n const getSteps = useCallback(\n async (runId: string): Promise<StepRecord[]> => {\n setIsLoading(true)\n setError(null)\n\n try {\n const url = `${api}/steps?runId=${encodeURIComponent(runId)}`\n const response = await fetch(url)\n\n if (!response.ok) {\n let errorMessage = `Failed to get steps: ${response.statusText}`\n try {\n const data = await response.json()\n if (data.error) {\n errorMessage = data.error\n }\n } catch {\n // Response is not JSON, use statusText\n }\n throw new Error(errorMessage)\n }\n\n return (await response.json()) as StepRecord[]\n } catch (err) {\n const message = err instanceof Error ? err.message : 'Unknown error'\n setError(message)\n throw err\n } finally {\n setIsLoading(false)\n }\n },\n [api],\n )\n\n return {\n retry,\n cancel,\n deleteRun,\n getRun,\n getSteps,\n isLoading,\n error,\n }\n}\n"],"mappings":"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","names":["useEffect","useEffect","useCallback","useEffect","useRef","useState","useState","useRef","useCallback","useEffect","useCallback","useState"]}
|