@coji/durably-react 0.6.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/index.js ADDED
@@ -0,0 +1,464 @@
1
+ // src/context.tsx
2
+ import { Suspense, createContext, use, useContext } from "react";
3
+ import { jsx } from "react/jsx-runtime";
4
+ var DurablyContext = createContext(null);
5
+ function DurablyProviderInner({
6
+ durably: durablyOrPromise,
7
+ children
8
+ }) {
9
+ const durably = durablyOrPromise instanceof Promise ? use(durablyOrPromise) : durablyOrPromise;
10
+ return /* @__PURE__ */ jsx(DurablyContext.Provider, { value: { durably }, children });
11
+ }
12
+ function DurablyProvider({
13
+ durably,
14
+ fallback,
15
+ children
16
+ }) {
17
+ const inner = /* @__PURE__ */ jsx(DurablyProviderInner, { durably, children });
18
+ if (fallback !== void 0) {
19
+ return /* @__PURE__ */ jsx(Suspense, { fallback, children: inner });
20
+ }
21
+ return inner;
22
+ }
23
+ function useDurably() {
24
+ const context = useContext(DurablyContext);
25
+ if (!context) {
26
+ throw new Error("useDurably must be used within a DurablyProvider");
27
+ }
28
+ return context;
29
+ }
30
+
31
+ // src/hooks/use-job.ts
32
+ import { useCallback, useEffect, useRef, useState } from "react";
33
+ function useJob(jobDefinition, options) {
34
+ const { durably } = useDurably();
35
+ const [status, setStatus] = useState(null);
36
+ const [output, setOutput] = useState(null);
37
+ const [error, setError] = useState(null);
38
+ const [logs, setLogs] = useState([]);
39
+ const [progress, setProgress] = useState(null);
40
+ const [currentRunId, setCurrentRunId] = useState(
41
+ options?.initialRunId ?? null
42
+ );
43
+ const jobHandleRef = useRef(null);
44
+ const currentRunIdRef = useRef(currentRunId);
45
+ currentRunIdRef.current = currentRunId;
46
+ useEffect(() => {
47
+ if (!durably) return;
48
+ const d = durably.register({
49
+ _job: jobDefinition
50
+ });
51
+ const jobHandle = d.jobs._job;
52
+ jobHandleRef.current = jobHandle;
53
+ const unsubscribes = [];
54
+ unsubscribes.push(
55
+ durably.on("run:start", (event) => {
56
+ if (event.jobName !== jobDefinition.name) return;
57
+ if (options?.followLatest === false) {
58
+ if (event.runId !== currentRunIdRef.current) return;
59
+ setStatus("running");
60
+ return;
61
+ }
62
+ setCurrentRunId(event.runId);
63
+ currentRunIdRef.current = event.runId;
64
+ setStatus("running");
65
+ setOutput(null);
66
+ setError(null);
67
+ setLogs([]);
68
+ setProgress(null);
69
+ })
70
+ );
71
+ unsubscribes.push(
72
+ durably.on("run:complete", (event) => {
73
+ if (event.runId !== currentRunIdRef.current) return;
74
+ setStatus("completed");
75
+ setOutput(event.output);
76
+ })
77
+ );
78
+ unsubscribes.push(
79
+ durably.on("run:fail", (event) => {
80
+ if (event.runId !== currentRunIdRef.current) return;
81
+ setStatus("failed");
82
+ setError(event.error);
83
+ })
84
+ );
85
+ unsubscribes.push(
86
+ durably.on("run:progress", (event) => {
87
+ if (event.runId !== currentRunIdRef.current) return;
88
+ setProgress(event.progress);
89
+ })
90
+ );
91
+ unsubscribes.push(
92
+ durably.on("log:write", (event) => {
93
+ if (event.runId !== currentRunIdRef.current) return;
94
+ setLogs((prev) => [
95
+ ...prev,
96
+ {
97
+ id: crypto.randomUUID(),
98
+ runId: event.runId,
99
+ stepName: event.stepName,
100
+ level: event.level,
101
+ message: event.message,
102
+ data: event.data,
103
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
104
+ }
105
+ ]);
106
+ })
107
+ );
108
+ if (options?.initialRunId && currentRunIdRef.current) {
109
+ jobHandle.getRun(currentRunIdRef.current).then((run) => {
110
+ if (run) {
111
+ setStatus(run.status);
112
+ if (run.status === "completed" && run.output) {
113
+ setOutput(run.output);
114
+ }
115
+ if (run.status === "failed" && run.error) {
116
+ setError(run.error);
117
+ }
118
+ }
119
+ });
120
+ }
121
+ if (options?.autoResume !== false && !options?.initialRunId) {
122
+ ;
123
+ (async () => {
124
+ const runningRuns = await jobHandle.getRuns({ status: "running" });
125
+ if (runningRuns.length > 0) {
126
+ const run = runningRuns[0];
127
+ setCurrentRunId(run.id);
128
+ currentRunIdRef.current = run.id;
129
+ setStatus(run.status);
130
+ return;
131
+ }
132
+ const pendingRuns = await jobHandle.getRuns({ status: "pending" });
133
+ if (pendingRuns.length > 0) {
134
+ const run = pendingRuns[0];
135
+ setCurrentRunId(run.id);
136
+ currentRunIdRef.current = run.id;
137
+ setStatus(run.status);
138
+ }
139
+ })();
140
+ }
141
+ return () => {
142
+ for (const unsubscribe of unsubscribes) {
143
+ unsubscribe();
144
+ }
145
+ };
146
+ }, [
147
+ durably,
148
+ jobDefinition,
149
+ options?.initialRunId,
150
+ options?.autoResume,
151
+ options?.followLatest
152
+ ]);
153
+ useEffect(() => {
154
+ if (!durably || !currentRunId) return;
155
+ const jobHandle = jobHandleRef.current;
156
+ if (jobHandle && options?.initialRunId) {
157
+ jobHandle.getRun(currentRunId).then((run) => {
158
+ if (run) {
159
+ setStatus(run.status);
160
+ if (run.status === "completed" && run.output) {
161
+ setOutput(run.output);
162
+ }
163
+ if (run.status === "failed" && run.error) {
164
+ setError(run.error);
165
+ }
166
+ }
167
+ });
168
+ }
169
+ }, [durably, currentRunId, options?.initialRunId]);
170
+ const trigger = useCallback(
171
+ async (input) => {
172
+ const jobHandle = jobHandleRef.current;
173
+ if (!jobHandle) {
174
+ throw new Error("Job not ready");
175
+ }
176
+ setOutput(null);
177
+ setError(null);
178
+ setLogs([]);
179
+ setProgress(null);
180
+ const run = await jobHandle.trigger(input);
181
+ setCurrentRunId(run.id);
182
+ currentRunIdRef.current = run.id;
183
+ setStatus("pending");
184
+ return { runId: run.id };
185
+ },
186
+ []
187
+ );
188
+ const triggerAndWait = useCallback(
189
+ async (input) => {
190
+ const jobHandle = jobHandleRef.current;
191
+ if (!jobHandle || !durably) {
192
+ throw new Error("Job not ready");
193
+ }
194
+ setOutput(null);
195
+ setError(null);
196
+ setLogs([]);
197
+ setProgress(null);
198
+ const run = await jobHandle.trigger(input);
199
+ setCurrentRunId(run.id);
200
+ currentRunIdRef.current = run.id;
201
+ setStatus("pending");
202
+ return new Promise((resolve, reject) => {
203
+ const checkCompletion = async () => {
204
+ const updatedRun = await jobHandle.getRun(run.id);
205
+ if (!updatedRun) {
206
+ reject(new Error("Run not found"));
207
+ return;
208
+ }
209
+ if (updatedRun.status === "completed") {
210
+ resolve({ runId: run.id, output: updatedRun.output });
211
+ } else if (updatedRun.status === "failed") {
212
+ reject(new Error(updatedRun.error ?? "Job failed"));
213
+ } else if (updatedRun.status === "cancelled") {
214
+ reject(new Error("Job cancelled"));
215
+ } else {
216
+ setTimeout(checkCompletion, 50);
217
+ }
218
+ };
219
+ checkCompletion();
220
+ });
221
+ },
222
+ [durably]
223
+ );
224
+ const reset = useCallback(() => {
225
+ setStatus(null);
226
+ setOutput(null);
227
+ setError(null);
228
+ setLogs([]);
229
+ setProgress(null);
230
+ setCurrentRunId(null);
231
+ }, []);
232
+ return {
233
+ trigger,
234
+ triggerAndWait,
235
+ status,
236
+ output,
237
+ error,
238
+ logs,
239
+ progress,
240
+ isRunning: status === "running",
241
+ isPending: status === "pending",
242
+ isCompleted: status === "completed",
243
+ isFailed: status === "failed",
244
+ isCancelled: status === "cancelled",
245
+ currentRunId,
246
+ reset
247
+ };
248
+ }
249
+
250
+ // src/hooks/use-run-subscription.ts
251
+ import { useEffect as useEffect2, useRef as useRef2, useState as useState2 } from "react";
252
+ function useRunSubscription(durably, runId, options) {
253
+ const [status, setStatus] = useState2(null);
254
+ const [output, setOutput] = useState2(null);
255
+ const [error, setError] = useState2(null);
256
+ const [logs, setLogs] = useState2([]);
257
+ const [progress, setProgress] = useState2(null);
258
+ const runIdRef = useRef2(runId);
259
+ runIdRef.current = runId;
260
+ const maxLogs = options?.maxLogs ?? 0;
261
+ useEffect2(() => {
262
+ if (!durably || !runId) return;
263
+ const unsubscribes = [];
264
+ unsubscribes.push(
265
+ durably.on("run:start", (event) => {
266
+ if (event.runId !== runIdRef.current) return;
267
+ setStatus("running");
268
+ })
269
+ );
270
+ unsubscribes.push(
271
+ durably.on("run:complete", (event) => {
272
+ if (event.runId !== runIdRef.current) return;
273
+ setStatus("completed");
274
+ setOutput(event.output);
275
+ })
276
+ );
277
+ unsubscribes.push(
278
+ durably.on("run:fail", (event) => {
279
+ if (event.runId !== runIdRef.current) return;
280
+ setStatus("failed");
281
+ setError(event.error);
282
+ })
283
+ );
284
+ unsubscribes.push(
285
+ durably.on("run:cancel", (event) => {
286
+ if (event.runId !== runIdRef.current) return;
287
+ setStatus("cancelled");
288
+ })
289
+ );
290
+ unsubscribes.push(
291
+ durably.on("run:retry", (event) => {
292
+ if (event.runId !== runIdRef.current) return;
293
+ setStatus("pending");
294
+ setError(null);
295
+ })
296
+ );
297
+ unsubscribes.push(
298
+ durably.on("run:progress", (event) => {
299
+ if (event.runId !== runIdRef.current) return;
300
+ setProgress(event.progress);
301
+ })
302
+ );
303
+ unsubscribes.push(
304
+ durably.on("log:write", (event) => {
305
+ if (event.runId !== runIdRef.current) return;
306
+ setLogs((prev) => {
307
+ const newLog = {
308
+ id: crypto.randomUUID(),
309
+ runId: event.runId,
310
+ stepName: event.stepName,
311
+ level: event.level,
312
+ message: event.message,
313
+ data: event.data,
314
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
315
+ };
316
+ const newLogs = [...prev, newLog];
317
+ if (maxLogs > 0 && newLogs.length > maxLogs) {
318
+ return newLogs.slice(-maxLogs);
319
+ }
320
+ return newLogs;
321
+ });
322
+ })
323
+ );
324
+ return () => {
325
+ for (const unsubscribe of unsubscribes) {
326
+ unsubscribe();
327
+ }
328
+ };
329
+ }, [durably, runId, maxLogs]);
330
+ const clearLogs = () => {
331
+ setLogs([]);
332
+ };
333
+ const reset = () => {
334
+ setStatus(null);
335
+ setOutput(null);
336
+ setError(null);
337
+ setLogs([]);
338
+ setProgress(null);
339
+ };
340
+ return {
341
+ status,
342
+ output,
343
+ error,
344
+ logs,
345
+ progress,
346
+ clearLogs,
347
+ reset
348
+ };
349
+ }
350
+
351
+ // src/hooks/use-job-logs.ts
352
+ function useJobLogs(options) {
353
+ const { durably } = useDurably();
354
+ const { runId, maxLogs } = options;
355
+ const subscription = useRunSubscription(durably, runId, { maxLogs });
356
+ return {
357
+ logs: subscription.logs,
358
+ clearLogs: subscription.clearLogs
359
+ };
360
+ }
361
+
362
+ // src/hooks/use-job-run.ts
363
+ import { useEffect as useEffect3, useRef as useRef3 } from "react";
364
+ function useJobRun(options) {
365
+ const { durably } = useDurably();
366
+ const { runId } = options;
367
+ const subscription = useRunSubscription(durably, runId);
368
+ const fetchedRef = useRef3(/* @__PURE__ */ new Set());
369
+ useEffect3(() => {
370
+ if (!durably || !runId || fetchedRef.current.has(runId)) return;
371
+ fetchedRef.current.add(runId);
372
+ }, [durably, runId]);
373
+ return {
374
+ status: subscription.status,
375
+ output: subscription.output,
376
+ error: subscription.error,
377
+ logs: subscription.logs,
378
+ progress: subscription.progress,
379
+ isRunning: subscription.status === "running",
380
+ isPending: subscription.status === "pending",
381
+ isCompleted: subscription.status === "completed",
382
+ isFailed: subscription.status === "failed",
383
+ isCancelled: subscription.status === "cancelled"
384
+ };
385
+ }
386
+
387
+ // src/hooks/use-runs.ts
388
+ import { useCallback as useCallback2, useEffect as useEffect4, useState as useState3 } from "react";
389
+ function useRuns(options) {
390
+ const { durably } = useDurably();
391
+ const pageSize = options?.pageSize ?? 10;
392
+ const realtime = options?.realtime ?? true;
393
+ const [runs, setRuns] = useState3([]);
394
+ const [page, setPage] = useState3(0);
395
+ const [hasMore, setHasMore] = useState3(false);
396
+ const [isLoading, setIsLoading] = useState3(false);
397
+ const refresh = useCallback2(async () => {
398
+ if (!durably) return;
399
+ setIsLoading(true);
400
+ try {
401
+ const data = await durably.getRuns({
402
+ jobName: options?.jobName,
403
+ status: options?.status,
404
+ limit: pageSize + 1,
405
+ offset: page * pageSize
406
+ });
407
+ setHasMore(data.length > pageSize);
408
+ setRuns(data.slice(0, pageSize));
409
+ } finally {
410
+ setIsLoading(false);
411
+ }
412
+ }, [durably, options?.jobName, options?.status, pageSize, page]);
413
+ useEffect4(() => {
414
+ if (!durably) return;
415
+ refresh();
416
+ if (!realtime) return;
417
+ const unsubscribes = [
418
+ durably.on("run:trigger", refresh),
419
+ durably.on("run:start", refresh),
420
+ durably.on("run:complete", refresh),
421
+ durably.on("run:fail", refresh),
422
+ durably.on("run:cancel", refresh),
423
+ durably.on("run:retry", refresh),
424
+ durably.on("run:progress", refresh),
425
+ durably.on("step:start", refresh),
426
+ durably.on("step:complete", refresh)
427
+ ];
428
+ return () => {
429
+ for (const unsubscribe of unsubscribes) {
430
+ unsubscribe();
431
+ }
432
+ };
433
+ }, [durably, refresh, realtime]);
434
+ const nextPage = useCallback2(() => {
435
+ if (hasMore) {
436
+ setPage((p) => p + 1);
437
+ }
438
+ }, [hasMore]);
439
+ const prevPage = useCallback2(() => {
440
+ setPage((p) => Math.max(0, p - 1));
441
+ }, []);
442
+ const goToPage = useCallback2((newPage) => {
443
+ setPage(Math.max(0, newPage));
444
+ }, []);
445
+ return {
446
+ runs,
447
+ page,
448
+ hasMore,
449
+ isLoading,
450
+ nextPage,
451
+ prevPage,
452
+ goToPage,
453
+ refresh
454
+ };
455
+ }
456
+ export {
457
+ DurablyProvider,
458
+ useDurably,
459
+ useJob,
460
+ useJobLogs,
461
+ useJobRun,
462
+ useRuns
463
+ };
464
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/context.tsx","../src/hooks/use-job.ts","../src/hooks/use-run-subscription.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, useRef, useState } from 'react'\nimport { useDurably } from '../context'\nimport type { LogEntry, Progress, RunStatus } from '../types'\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 [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 const [currentRunId, setCurrentRunId] = useState<string | null>(\n options?.initialRunId ?? null,\n )\n\n const jobHandleRef = useRef<JobHandle<TName, TInput, TOutput> | null>(null)\n // Use ref to track the latest runId for event filtering\n const currentRunIdRef = useRef<string | null>(currentRunId)\n currentRunIdRef.current = currentRunId\n\n // Register job and set up event listeners\n useEffect(() => {\n if (!durably) return\n\n // Register the job (use fixed key for simpler type handling)\n const d = durably.register({\n _job: jobDefinition,\n })\n const jobHandle = d.jobs._job\n jobHandleRef.current = jobHandle\n\n // Subscribe to each event type separately\n const unsubscribes: (() => void)[] = []\n\n unsubscribes.push(\n durably.on('run:start', (event) => {\n // Check if this is a run for our job\n if (event.jobName !== jobDefinition.name) return\n\n // If followLatest is disabled, only update if this is our current run\n if (options?.followLatest === false) {\n if (event.runId !== currentRunIdRef.current) return\n setStatus('running')\n return\n }\n\n // Switch to tracking the running job (followLatest: true, default)\n setCurrentRunId(event.runId)\n currentRunIdRef.current = event.runId\n setStatus('running')\n // Reset output/error when switching to a new run\n setOutput(null)\n setError(null)\n setLogs([])\n setProgress(null)\n }),\n )\n\n unsubscribes.push(\n durably.on('run:complete', (event) => {\n if (event.runId !== currentRunIdRef.current) return\n setStatus('completed')\n setOutput(event.output as TOutput)\n }),\n )\n\n unsubscribes.push(\n durably.on('run:fail', (event) => {\n if (event.runId !== currentRunIdRef.current) return\n setStatus('failed')\n setError(event.error)\n }),\n )\n\n unsubscribes.push(\n durably.on('run:progress', (event) => {\n if (event.runId !== currentRunIdRef.current) return\n setProgress(event.progress)\n }),\n )\n\n unsubscribes.push(\n durably.on('log:write', (event) => {\n if (event.runId !== currentRunIdRef.current) return\n setLogs((prev) => [\n ...prev,\n {\n id: crypto.randomUUID(),\n runId: event.runId,\n stepName: event.stepName,\n level: event.level,\n message: event.message,\n data: event.data,\n timestamp: new Date().toISOString(),\n },\n ])\n }),\n )\n\n // If we have an initialRunId, fetch its current state\n if (options?.initialRunId && currentRunIdRef.current) {\n jobHandle.getRun(currentRunIdRef.current).then((run) => {\n if (run) {\n setStatus(run.status as RunStatus)\n if (run.status === 'completed' && run.output) {\n setOutput(run.output as TOutput)\n }\n if (run.status === 'failed' && run.error) {\n setError(run.error)\n }\n }\n })\n }\n\n // Auto-resume: find any pending or running runs for this job (default: true)\n if (options?.autoResume !== false && !options?.initialRunId) {\n ;(async () => {\n // First check for running runs\n const runningRuns = await jobHandle.getRuns({ status: 'running' })\n if (runningRuns.length > 0) {\n const run = runningRuns[0]\n setCurrentRunId(run.id)\n currentRunIdRef.current = run.id\n setStatus(run.status as RunStatus)\n return\n }\n\n // Then check for pending runs\n const pendingRuns = await jobHandle.getRuns({ status: 'pending' })\n if (pendingRuns.length > 0) {\n const run = pendingRuns[0]\n setCurrentRunId(run.id)\n currentRunIdRef.current = run.id\n setStatus(run.status as RunStatus)\n }\n })()\n }\n\n return () => {\n for (const unsubscribe of unsubscribes) {\n unsubscribe()\n }\n }\n }, [\n durably,\n jobDefinition,\n options?.initialRunId,\n options?.autoResume,\n options?.followLatest,\n ])\n\n // Update state when currentRunId changes (for initialRunId scenario)\n useEffect(() => {\n if (!durably || !currentRunId) return\n\n const jobHandle = jobHandleRef.current\n if (jobHandle && options?.initialRunId) {\n jobHandle.getRun(currentRunId).then((run) => {\n if (run) {\n setStatus(run.status as RunStatus)\n if (run.status === 'completed' && run.output) {\n setOutput(run.output as TOutput)\n }\n if (run.status === 'failed' && run.error) {\n setError(run.error)\n }\n }\n })\n }\n }, [durably, currentRunId, options?.initialRunId])\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\n setOutput(null)\n setError(null)\n setLogs([])\n setProgress(null)\n\n const run = await jobHandle.trigger(input)\n setCurrentRunId(run.id)\n currentRunIdRef.current = run.id\n setStatus('pending')\n\n return { runId: run.id }\n },\n [],\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\n setOutput(null)\n setError(null)\n setLogs([])\n setProgress(null)\n\n const run = await jobHandle.trigger(input)\n setCurrentRunId(run.id)\n currentRunIdRef.current = run.id\n setStatus('pending')\n\n // Wait for completion\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],\n )\n\n const reset = useCallback(() => {\n setStatus(null)\n setOutput(null)\n setError(null)\n setLogs([])\n setProgress(null)\n setCurrentRunId(null)\n }, [])\n\n return {\n trigger,\n triggerAndWait,\n status,\n output,\n error,\n logs,\n progress,\n isRunning: status === 'running',\n isPending: status === 'pending',\n isCompleted: status === 'completed',\n isFailed: status === 'failed',\n isCancelled: status === 'cancelled',\n currentRunId,\n reset,\n }\n}\n","import type { Durably } from '@coji/durably'\nimport { useEffect, useRef, useState } from 'react'\nimport type { LogEntry, Progress, RunStatus } from '../types'\n\nexport interface RunSubscriptionState<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 UseRunSubscriptionOptions {\n /**\n * Maximum number of logs to keep (0 = unlimited)\n */\n maxLogs?: number\n}\n\nexport interface UseRunSubscriptionResult<\n TOutput = unknown,\n> extends RunSubscriptionState<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.\n * Shared by useJob, useJobRun, and useJobLogs.\n */\nexport function useRunSubscription<TOutput = unknown>(\n durably: Durably | null,\n runId: string | null,\n options?: UseRunSubscriptionOptions,\n): UseRunSubscriptionResult<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 // Use ref to track the latest runId for event filtering\n const runIdRef = useRef<string | null>(runId)\n runIdRef.current = runId\n\n const maxLogs = options?.maxLogs ?? 0\n\n // Subscribe to events\n useEffect(() => {\n if (!durably || !runId) return\n\n const unsubscribes: (() => void)[] = []\n\n unsubscribes.push(\n durably.on('run:start', (event) => {\n if (event.runId !== runIdRef.current) return\n setStatus('running')\n }),\n )\n\n unsubscribes.push(\n durably.on('run:complete', (event) => {\n if (event.runId !== runIdRef.current) return\n setStatus('completed')\n setOutput(event.output as TOutput)\n }),\n )\n\n unsubscribes.push(\n durably.on('run:fail', (event) => {\n if (event.runId !== runIdRef.current) return\n setStatus('failed')\n setError(event.error)\n }),\n )\n\n unsubscribes.push(\n durably.on('run:cancel', (event) => {\n if (event.runId !== runIdRef.current) return\n setStatus('cancelled')\n }),\n )\n\n unsubscribes.push(\n durably.on('run:retry', (event) => {\n if (event.runId !== runIdRef.current) return\n setStatus('pending')\n setError(null)\n }),\n )\n\n unsubscribes.push(\n durably.on('run:progress', (event) => {\n if (event.runId !== runIdRef.current) return\n setProgress(event.progress)\n }),\n )\n\n unsubscribes.push(\n durably.on('log:write', (event) => {\n if (event.runId !== runIdRef.current) return\n setLogs((prev) => {\n const newLog: LogEntry = {\n id: crypto.randomUUID(),\n runId: event.runId,\n stepName: event.stepName,\n level: event.level,\n message: event.message,\n data: event.data,\n timestamp: new Date().toISOString(),\n }\n const newLogs = [...prev, newLog]\n // Apply maxLogs limit if set\n if (maxLogs > 0 && newLogs.length > maxLogs) {\n return newLogs.slice(-maxLogs)\n }\n return newLogs\n })\n }),\n )\n\n return () => {\n for (const unsubscribe of unsubscribes) {\n unsubscribe()\n }\n }\n }, [durably, runId, maxLogs])\n\n const clearLogs = () => {\n setLogs([])\n }\n\n const reset = () => {\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 { 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":";AACA,SAAS,UAAU,eAAe,KAAK,kBAAkC;AAmDrE;AA7CJ,IAAM,iBAAiB,cAA0C,IAAI;AAmCrE,SAAS,qBAAqB;AAAA,EAC5B,SAAS;AAAA,EACT;AACF,GAA2C;AACzC,QAAM,UACJ,4BAA4B,UACxB,IAAI,gBAAgB,IACpB;AAEN,SACE,oBAAC,eAAe,UAAf,EAAwB,OAAO,EAAE,QAAQ,GACvC,UACH;AAEJ;AAEO,SAAS,gBAAgB;AAAA,EAC9B;AAAA,EACA;AAAA,EACA;AACF,GAAyB;AACvB,QAAM,QACJ,oBAAC,wBAAqB,SAAmB,UAAS;AAGpD,MAAI,aAAa,QAAW;AAC1B,WAAO,oBAAC,YAAS,UAAqB,iBAAM;AAAA,EAC9C;AAEA,SAAO;AACT;AAEO,SAAS,aAAkC;AAChD,QAAM,UAAU,WAAW,cAAc;AACzC,MAAI,CAAC,SAAS;AACZ,UAAM,IAAI,MAAM,kDAAkD;AAAA,EACpE;AACA,SAAO;AACT;;;AC/EA,SAAS,aAAa,WAAW,QAAQ,gBAAgB;AAmFlD,SAAS,OAMd,eACA,SAC+B;AAC/B,QAAM,EAAE,QAAQ,IAAI,WAAW;AAE/B,QAAM,CAAC,QAAQ,SAAS,IAAI,SAA2B,IAAI;AAC3D,QAAM,CAAC,QAAQ,SAAS,IAAI,SAAyB,IAAI;AACzD,QAAM,CAAC,OAAO,QAAQ,IAAI,SAAwB,IAAI;AACtD,QAAM,CAAC,MAAM,OAAO,IAAI,SAAqB,CAAC,CAAC;AAC/C,QAAM,CAAC,UAAU,WAAW,IAAI,SAA0B,IAAI;AAC9D,QAAM,CAAC,cAAc,eAAe,IAAI;AAAA,IACtC,SAAS,gBAAgB;AAAA,EAC3B;AAEA,QAAM,eAAe,OAAiD,IAAI;AAE1E,QAAM,kBAAkB,OAAsB,YAAY;AAC1D,kBAAgB,UAAU;AAG1B,YAAU,MAAM;AACd,QAAI,CAAC,QAAS;AAGd,UAAM,IAAI,QAAQ,SAAS;AAAA,MACzB,MAAM;AAAA,IACR,CAAC;AACD,UAAM,YAAY,EAAE,KAAK;AACzB,iBAAa,UAAU;AAGvB,UAAM,eAA+B,CAAC;AAEtC,iBAAa;AAAA,MACX,QAAQ,GAAG,aAAa,CAAC,UAAU;AAEjC,YAAI,MAAM,YAAY,cAAc,KAAM;AAG1C,YAAI,SAAS,iBAAiB,OAAO;AACnC,cAAI,MAAM,UAAU,gBAAgB,QAAS;AAC7C,oBAAU,SAAS;AACnB;AAAA,QACF;AAGA,wBAAgB,MAAM,KAAK;AAC3B,wBAAgB,UAAU,MAAM;AAChC,kBAAU,SAAS;AAEnB,kBAAU,IAAI;AACd,iBAAS,IAAI;AACb,gBAAQ,CAAC,CAAC;AACV,oBAAY,IAAI;AAAA,MAClB,CAAC;AAAA,IACH;AAEA,iBAAa;AAAA,MACX,QAAQ,GAAG,gBAAgB,CAAC,UAAU;AACpC,YAAI,MAAM,UAAU,gBAAgB,QAAS;AAC7C,kBAAU,WAAW;AACrB,kBAAU,MAAM,MAAiB;AAAA,MACnC,CAAC;AAAA,IACH;AAEA,iBAAa;AAAA,MACX,QAAQ,GAAG,YAAY,CAAC,UAAU;AAChC,YAAI,MAAM,UAAU,gBAAgB,QAAS;AAC7C,kBAAU,QAAQ;AAClB,iBAAS,MAAM,KAAK;AAAA,MACtB,CAAC;AAAA,IACH;AAEA,iBAAa;AAAA,MACX,QAAQ,GAAG,gBAAgB,CAAC,UAAU;AACpC,YAAI,MAAM,UAAU,gBAAgB,QAAS;AAC7C,oBAAY,MAAM,QAAQ;AAAA,MAC5B,CAAC;AAAA,IACH;AAEA,iBAAa;AAAA,MACX,QAAQ,GAAG,aAAa,CAAC,UAAU;AACjC,YAAI,MAAM,UAAU,gBAAgB,QAAS;AAC7C,gBAAQ,CAAC,SAAS;AAAA,UAChB,GAAG;AAAA,UACH;AAAA,YACE,IAAI,OAAO,WAAW;AAAA,YACtB,OAAO,MAAM;AAAA,YACb,UAAU,MAAM;AAAA,YAChB,OAAO,MAAM;AAAA,YACb,SAAS,MAAM;AAAA,YACf,MAAM,MAAM;AAAA,YACZ,YAAW,oBAAI,KAAK,GAAE,YAAY;AAAA,UACpC;AAAA,QACF,CAAC;AAAA,MACH,CAAC;AAAA,IACH;AAGA,QAAI,SAAS,gBAAgB,gBAAgB,SAAS;AACpD,gBAAU,OAAO,gBAAgB,OAAO,EAAE,KAAK,CAAC,QAAQ;AACtD,YAAI,KAAK;AACP,oBAAU,IAAI,MAAmB;AACjC,cAAI,IAAI,WAAW,eAAe,IAAI,QAAQ;AAC5C,sBAAU,IAAI,MAAiB;AAAA,UACjC;AACA,cAAI,IAAI,WAAW,YAAY,IAAI,OAAO;AACxC,qBAAS,IAAI,KAAK;AAAA,UACpB;AAAA,QACF;AAAA,MACF,CAAC;AAAA,IACH;AAGA,QAAI,SAAS,eAAe,SAAS,CAAC,SAAS,cAAc;AAC3D;AAAC,OAAC,YAAY;AAEZ,cAAM,cAAc,MAAM,UAAU,QAAQ,EAAE,QAAQ,UAAU,CAAC;AACjE,YAAI,YAAY,SAAS,GAAG;AAC1B,gBAAM,MAAM,YAAY,CAAC;AACzB,0BAAgB,IAAI,EAAE;AACtB,0BAAgB,UAAU,IAAI;AAC9B,oBAAU,IAAI,MAAmB;AACjC;AAAA,QACF;AAGA,cAAM,cAAc,MAAM,UAAU,QAAQ,EAAE,QAAQ,UAAU,CAAC;AACjE,YAAI,YAAY,SAAS,GAAG;AAC1B,gBAAM,MAAM,YAAY,CAAC;AACzB,0BAAgB,IAAI,EAAE;AACtB,0BAAgB,UAAU,IAAI;AAC9B,oBAAU,IAAI,MAAmB;AAAA,QACnC;AAAA,MACF,GAAG;AAAA,IACL;AAEA,WAAO,MAAM;AACX,iBAAW,eAAe,cAAc;AACtC,oBAAY;AAAA,MACd;AAAA,IACF;AAAA,EACF,GAAG;AAAA,IACD;AAAA,IACA;AAAA,IACA,SAAS;AAAA,IACT,SAAS;AAAA,IACT,SAAS;AAAA,EACX,CAAC;AAGD,YAAU,MAAM;AACd,QAAI,CAAC,WAAW,CAAC,aAAc;AAE/B,UAAM,YAAY,aAAa;AAC/B,QAAI,aAAa,SAAS,cAAc;AACtC,gBAAU,OAAO,YAAY,EAAE,KAAK,CAAC,QAAQ;AAC3C,YAAI,KAAK;AACP,oBAAU,IAAI,MAAmB;AACjC,cAAI,IAAI,WAAW,eAAe,IAAI,QAAQ;AAC5C,sBAAU,IAAI,MAAiB;AAAA,UACjC;AACA,cAAI,IAAI,WAAW,YAAY,IAAI,OAAO;AACxC,qBAAS,IAAI,KAAK;AAAA,UACpB;AAAA,QACF;AAAA,MACF,CAAC;AAAA,IACH;AAAA,EACF,GAAG,CAAC,SAAS,cAAc,SAAS,YAAY,CAAC;AAEjD,QAAM,UAAU;AAAA,IACd,OAAO,UAA8C;AACnD,YAAM,YAAY,aAAa;AAC/B,UAAI,CAAC,WAAW;AACd,cAAM,IAAI,MAAM,eAAe;AAAA,MACjC;AAGA,gBAAU,IAAI;AACd,eAAS,IAAI;AACb,cAAQ,CAAC,CAAC;AACV,kBAAY,IAAI;AAEhB,YAAM,MAAM,MAAM,UAAU,QAAQ,KAAK;AACzC,sBAAgB,IAAI,EAAE;AACtB,sBAAgB,UAAU,IAAI;AAC9B,gBAAU,SAAS;AAEnB,aAAO,EAAE,OAAO,IAAI,GAAG;AAAA,IACzB;AAAA,IACA,CAAC;AAAA,EACH;AAEA,QAAM,iBAAiB;AAAA,IACrB,OAAO,UAA+D;AACpE,YAAM,YAAY,aAAa;AAC/B,UAAI,CAAC,aAAa,CAAC,SAAS;AAC1B,cAAM,IAAI,MAAM,eAAe;AAAA,MACjC;AAGA,gBAAU,IAAI;AACd,eAAS,IAAI;AACb,cAAQ,CAAC,CAAC;AACV,kBAAY,IAAI;AAEhB,YAAM,MAAM,MAAM,UAAU,QAAQ,KAAK;AACzC,sBAAgB,IAAI,EAAE;AACtB,sBAAgB,UAAU,IAAI;AAC9B,gBAAU,SAAS;AAGnB,aAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACtC,cAAM,kBAAkB,YAAY;AAClC,gBAAM,aAAa,MAAM,UAAU,OAAO,IAAI,EAAE;AAChD,cAAI,CAAC,YAAY;AACf,mBAAO,IAAI,MAAM,eAAe,CAAC;AACjC;AAAA,UACF;AAEA,cAAI,WAAW,WAAW,aAAa;AACrC,oBAAQ,EAAE,OAAO,IAAI,IAAI,QAAQ,WAAW,OAAkB,CAAC;AAAA,UACjE,WAAW,WAAW,WAAW,UAAU;AACzC,mBAAO,IAAI,MAAM,WAAW,SAAS,YAAY,CAAC;AAAA,UACpD,WAAW,WAAW,WAAW,aAAa;AAC5C,mBAAO,IAAI,MAAM,eAAe,CAAC;AAAA,UACnC,OAAO;AAEL,uBAAW,iBAAiB,EAAE;AAAA,UAChC;AAAA,QACF;AACA,wBAAgB;AAAA,MAClB,CAAC;AAAA,IACH;AAAA,IACA,CAAC,OAAO;AAAA,EACV;AAEA,QAAM,QAAQ,YAAY,MAAM;AAC9B,cAAU,IAAI;AACd,cAAU,IAAI;AACd,aAAS,IAAI;AACb,YAAQ,CAAC,CAAC;AACV,gBAAY,IAAI;AAChB,oBAAgB,IAAI;AAAA,EACtB,GAAG,CAAC,CAAC;AAEL,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,WAAW,WAAW;AAAA,IACtB,WAAW,WAAW;AAAA,IACtB,aAAa,WAAW;AAAA,IACxB,UAAU,WAAW;AAAA,IACrB,aAAa,WAAW;AAAA,IACxB;AAAA,IACA;AAAA,EACF;AACF;;;AC/VA,SAAS,aAAAA,YAAW,UAAAC,SAAQ,YAAAC,iBAAgB;AAmCrC,SAAS,mBACd,SACA,OACA,SACmC;AACnC,QAAM,CAAC,QAAQ,SAAS,IAAIA,UAA2B,IAAI;AAC3D,QAAM,CAAC,QAAQ,SAAS,IAAIA,UAAyB,IAAI;AACzD,QAAM,CAAC,OAAO,QAAQ,IAAIA,UAAwB,IAAI;AACtD,QAAM,CAAC,MAAM,OAAO,IAAIA,UAAqB,CAAC,CAAC;AAC/C,QAAM,CAAC,UAAU,WAAW,IAAIA,UAA0B,IAAI;AAG9D,QAAM,WAAWD,QAAsB,KAAK;AAC5C,WAAS,UAAU;AAEnB,QAAM,UAAU,SAAS,WAAW;AAGpC,EAAAD,WAAU,MAAM;AACd,QAAI,CAAC,WAAW,CAAC,MAAO;AAExB,UAAM,eAA+B,CAAC;AAEtC,iBAAa;AAAA,MACX,QAAQ,GAAG,aAAa,CAAC,UAAU;AACjC,YAAI,MAAM,UAAU,SAAS,QAAS;AACtC,kBAAU,SAAS;AAAA,MACrB,CAAC;AAAA,IACH;AAEA,iBAAa;AAAA,MACX,QAAQ,GAAG,gBAAgB,CAAC,UAAU;AACpC,YAAI,MAAM,UAAU,SAAS,QAAS;AACtC,kBAAU,WAAW;AACrB,kBAAU,MAAM,MAAiB;AAAA,MACnC,CAAC;AAAA,IACH;AAEA,iBAAa;AAAA,MACX,QAAQ,GAAG,YAAY,CAAC,UAAU;AAChC,YAAI,MAAM,UAAU,SAAS,QAAS;AACtC,kBAAU,QAAQ;AAClB,iBAAS,MAAM,KAAK;AAAA,MACtB,CAAC;AAAA,IACH;AAEA,iBAAa;AAAA,MACX,QAAQ,GAAG,cAAc,CAAC,UAAU;AAClC,YAAI,MAAM,UAAU,SAAS,QAAS;AACtC,kBAAU,WAAW;AAAA,MACvB,CAAC;AAAA,IACH;AAEA,iBAAa;AAAA,MACX,QAAQ,GAAG,aAAa,CAAC,UAAU;AACjC,YAAI,MAAM,UAAU,SAAS,QAAS;AACtC,kBAAU,SAAS;AACnB,iBAAS,IAAI;AAAA,MACf,CAAC;AAAA,IACH;AAEA,iBAAa;AAAA,MACX,QAAQ,GAAG,gBAAgB,CAAC,UAAU;AACpC,YAAI,MAAM,UAAU,SAAS,QAAS;AACtC,oBAAY,MAAM,QAAQ;AAAA,MAC5B,CAAC;AAAA,IACH;AAEA,iBAAa;AAAA,MACX,QAAQ,GAAG,aAAa,CAAC,UAAU;AACjC,YAAI,MAAM,UAAU,SAAS,QAAS;AACtC,gBAAQ,CAAC,SAAS;AAChB,gBAAM,SAAmB;AAAA,YACvB,IAAI,OAAO,WAAW;AAAA,YACtB,OAAO,MAAM;AAAA,YACb,UAAU,MAAM;AAAA,YAChB,OAAO,MAAM;AAAA,YACb,SAAS,MAAM;AAAA,YACf,MAAM,MAAM;AAAA,YACZ,YAAW,oBAAI,KAAK,GAAE,YAAY;AAAA,UACpC;AACA,gBAAM,UAAU,CAAC,GAAG,MAAM,MAAM;AAEhC,cAAI,UAAU,KAAK,QAAQ,SAAS,SAAS;AAC3C,mBAAO,QAAQ,MAAM,CAAC,OAAO;AAAA,UAC/B;AACA,iBAAO;AAAA,QACT,CAAC;AAAA,MACH,CAAC;AAAA,IACH;AAEA,WAAO,MAAM;AACX,iBAAW,eAAe,cAAc;AACtC,oBAAY;AAAA,MACd;AAAA,IACF;AAAA,EACF,GAAG,CAAC,SAAS,OAAO,OAAO,CAAC;AAE5B,QAAM,YAAY,MAAM;AACtB,YAAQ,CAAC,CAAC;AAAA,EACZ;AAEA,QAAM,QAAQ,MAAM;AAClB,cAAU,IAAI;AACd,cAAU,IAAI;AACd,aAAS,IAAI;AACb,YAAQ,CAAC,CAAC;AACV,gBAAY,IAAI;AAAA,EAClB;AAEA,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;;;AC7HO,SAAS,WAAW,SAA8C;AACvE,QAAM,EAAE,QAAQ,IAAI,WAAW;AAC/B,QAAM,EAAE,OAAO,QAAQ,IAAI;AAE3B,QAAM,eAAe,mBAAmB,SAAS,OAAO,EAAE,QAAQ,CAAC;AAEnE,SAAO;AAAA,IACL,MAAM,aAAa;AAAA,IACnB,WAAW,aAAa;AAAA,EAC1B;AACF;;;ACxCA,SAAS,aAAAG,YAAW,UAAAC,eAAc;AA2D3B,SAAS,UACd,SAC0B;AAC1B,QAAM,EAAE,QAAQ,IAAI,WAAW;AAC/B,QAAM,EAAE,MAAM,IAAI;AAElB,QAAM,eAAe,mBAA4B,SAAS,KAAK;AAG/D,QAAM,aAAaC,QAAoB,oBAAI,IAAI,CAAC;AAEhD,EAAAC,WAAU,MAAM;AACd,QAAI,CAAC,WAAW,CAAC,SAAS,WAAW,QAAQ,IAAI,KAAK,EAAG;AAGzD,eAAW,QAAQ,IAAI,KAAK;AAAA,EAK9B,GAAG,CAAC,SAAS,KAAK,CAAC;AAEnB,SAAO;AAAA,IACL,QAAQ,aAAa;AAAA,IACrB,QAAQ,aAAa;AAAA,IACrB,OAAO,aAAa;AAAA,IACpB,MAAM,aAAa;AAAA,IACnB,UAAU,aAAa;AAAA,IACvB,WAAW,aAAa,WAAW;AAAA,IACnC,WAAW,aAAa,WAAW;AAAA,IACnC,aAAa,aAAa,WAAW;AAAA,IACrC,UAAU,aAAa,WAAW;AAAA,IAClC,aAAa,aAAa,WAAW;AAAA,EACvC;AACF;;;AC5FA,SAAS,eAAAC,cAAa,aAAAC,YAAW,YAAAC,iBAAgB;AAiF1C,SAAS,QAAQ,SAAyC;AAC/D,QAAM,EAAE,QAAQ,IAAI,WAAW;AAC/B,QAAM,WAAW,SAAS,YAAY;AACtC,QAAM,WAAW,SAAS,YAAY;AAEtC,QAAM,CAAC,MAAM,OAAO,IAAIC,UAAgB,CAAC,CAAC;AAC1C,QAAM,CAAC,MAAM,OAAO,IAAIA,UAAS,CAAC;AAClC,QAAM,CAAC,SAAS,UAAU,IAAIA,UAAS,KAAK;AAC5C,QAAM,CAAC,WAAW,YAAY,IAAIA,UAAS,KAAK;AAEhD,QAAM,UAAUC,aAAY,YAAY;AACtC,QAAI,CAAC,QAAS;AAEd,iBAAa,IAAI;AACjB,QAAI;AACF,YAAM,OAAO,MAAM,QAAQ,QAAQ;AAAA,QACjC,SAAS,SAAS;AAAA,QAClB,QAAQ,SAAS;AAAA,QACjB,OAAO,WAAW;AAAA,QAClB,QAAQ,OAAO;AAAA,MACjB,CAAC;AACD,iBAAW,KAAK,SAAS,QAAQ;AACjC,cAAQ,KAAK,MAAM,GAAG,QAAQ,CAAC;AAAA,IACjC,UAAE;AACA,mBAAa,KAAK;AAAA,IACpB;AAAA,EACF,GAAG,CAAC,SAAS,SAAS,SAAS,SAAS,QAAQ,UAAU,IAAI,CAAC;AAG/D,EAAAC,WAAU,MAAM;AACd,QAAI,CAAC,QAAS;AAEd,YAAQ;AAER,QAAI,CAAC,SAAU;AAEf,UAAM,eAAe;AAAA,MACnB,QAAQ,GAAG,eAAe,OAAO;AAAA,MACjC,QAAQ,GAAG,aAAa,OAAO;AAAA,MAC/B,QAAQ,GAAG,gBAAgB,OAAO;AAAA,MAClC,QAAQ,GAAG,YAAY,OAAO;AAAA,MAC9B,QAAQ,GAAG,cAAc,OAAO;AAAA,MAChC,QAAQ,GAAG,aAAa,OAAO;AAAA,MAC/B,QAAQ,GAAG,gBAAgB,OAAO;AAAA,MAClC,QAAQ,GAAG,cAAc,OAAO;AAAA,MAChC,QAAQ,GAAG,iBAAiB,OAAO;AAAA,IACrC;AAEA,WAAO,MAAM;AACX,iBAAW,eAAe,cAAc;AACtC,oBAAY;AAAA,MACd;AAAA,IACF;AAAA,EACF,GAAG,CAAC,SAAS,SAAS,QAAQ,CAAC;AAE/B,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,EACF;AACF;","names":["useEffect","useRef","useState","useEffect","useRef","useRef","useEffect","useCallback","useEffect","useState","useState","useCallback","useEffect"]}
@@ -0,0 +1,67 @@
1
+ type RunStatus = 'pending' | 'running' | 'completed' | 'failed' | 'cancelled';
2
+ interface Progress {
3
+ current: number;
4
+ total?: number;
5
+ message?: string;
6
+ }
7
+ interface LogEntry {
8
+ id: string;
9
+ runId: string;
10
+ stepName: string | null;
11
+ level: 'info' | 'warn' | 'error';
12
+ message: string;
13
+ data: unknown;
14
+ timestamp: string;
15
+ }
16
+ type DurablyEvent = {
17
+ type: 'run:start';
18
+ runId: string;
19
+ jobName: string;
20
+ payload: unknown;
21
+ } | {
22
+ type: 'run:complete';
23
+ runId: string;
24
+ jobName: string;
25
+ output: unknown;
26
+ duration: number;
27
+ } | {
28
+ type: 'run:fail';
29
+ runId: string;
30
+ jobName: string;
31
+ error: string;
32
+ } | {
33
+ type: 'run:cancel';
34
+ runId: string;
35
+ jobName: string;
36
+ } | {
37
+ type: 'run:retry';
38
+ runId: string;
39
+ jobName: string;
40
+ } | {
41
+ type: 'run:progress';
42
+ runId: string;
43
+ jobName: string;
44
+ progress: Progress;
45
+ } | {
46
+ type: 'step:start';
47
+ runId: string;
48
+ jobName: string;
49
+ stepName: string;
50
+ stepIndex: number;
51
+ } | {
52
+ type: 'step:complete';
53
+ runId: string;
54
+ jobName: string;
55
+ stepName: string;
56
+ stepIndex: number;
57
+ output: unknown;
58
+ } | {
59
+ type: 'log:write';
60
+ runId: string;
61
+ jobName: string;
62
+ level: 'info' | 'warn' | 'error';
63
+ message: string;
64
+ data: unknown;
65
+ };
66
+
67
+ export type { DurablyEvent as D, LogEntry as L, Progress as P, RunStatus as R };
package/package.json ADDED
@@ -0,0 +1,82 @@
1
+ {
2
+ "name": "@coji/durably-react",
3
+ "version": "0.6.0",
4
+ "description": "React bindings for Durably - step-oriented resumable batch execution",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ },
13
+ "./client": {
14
+ "types": "./dist/client.d.ts",
15
+ "import": "./dist/client.js"
16
+ }
17
+ },
18
+ "files": [
19
+ "dist",
20
+ "README.md"
21
+ ],
22
+ "keywords": [
23
+ "react",
24
+ "hooks",
25
+ "durably",
26
+ "batch",
27
+ "job",
28
+ "queue",
29
+ "workflow",
30
+ "durable"
31
+ ],
32
+ "author": "coji",
33
+ "license": "MIT",
34
+ "repository": {
35
+ "type": "git",
36
+ "url": "git+https://github.com/coji/durably.git"
37
+ },
38
+ "bugs": {
39
+ "url": "https://github.com/coji/durably/issues"
40
+ },
41
+ "homepage": "https://github.com/coji/durably#readme",
42
+ "peerDependencies": {
43
+ "react": ">=19.0.0",
44
+ "react-dom": ">=19.0.0"
45
+ },
46
+ "peerDependenciesMeta": {
47
+ "@coji/durably": {
48
+ "optional": true
49
+ }
50
+ },
51
+ "devDependencies": {
52
+ "@biomejs/biome": "^2.3.10",
53
+ "@testing-library/react": "^16.3.1",
54
+ "@types/react": "^19.2.7",
55
+ "@types/react-dom": "^19.2.3",
56
+ "@vitejs/plugin-react": "^5.1.2",
57
+ "@vitest/browser": "^4.0.16",
58
+ "@vitest/browser-playwright": "4.0.16",
59
+ "kysely": "^0.28.9",
60
+ "playwright": "^1.57.0",
61
+ "prettier": "^3.7.4",
62
+ "prettier-plugin-organize-imports": "^4.3.0",
63
+ "react": "^19.2.3",
64
+ "react-dom": "^19.2.3",
65
+ "sqlocal": "^0.16.0",
66
+ "tsup": "^8.5.1",
67
+ "typescript": "^5.9.3",
68
+ "vitest": "^4.0.16",
69
+ "zod": "^4.3.4",
70
+ "@coji/durably": "0.6.0"
71
+ },
72
+ "scripts": {
73
+ "build": "tsup",
74
+ "test": "pnpm test:react",
75
+ "test:react": "vitest run --config vitest.config.ts",
76
+ "typecheck": "tsc --noEmit",
77
+ "lint": "biome lint .",
78
+ "lint:fix": "biome lint --write .",
79
+ "format": "prettier --experimental-cli --check .",
80
+ "format:fix": "prettier --experimental-cli --write ."
81
+ }
82
+ }