@coji/durably-react 0.9.0 → 0.11.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 CHANGED
@@ -1,468 +1,590 @@
1
1
  import {
2
- initialSubscriptionState,
3
2
  isJobDefinition,
4
- subscriptionReducer,
5
3
  useSubscription
6
4
  } from "./chunk-XRJEZWAV.js";
7
5
 
8
- // src/context.tsx
9
- import { Suspense, createContext, use, useContext } from "react";
10
- import { jsx } from "react/jsx-runtime";
11
- var DurablyContext = createContext(null);
12
- function DurablyProviderInner({
13
- durably: durablyOrPromise,
14
- children
15
- }) {
16
- const durably = durablyOrPromise instanceof Promise ? use(durablyOrPromise) : durablyOrPromise;
17
- return /* @__PURE__ */ jsx(DurablyContext.Provider, { value: { durably }, children });
18
- }
19
- function DurablyProvider({
20
- durably,
21
- fallback,
22
- children
23
- }) {
24
- const inner = /* @__PURE__ */ jsx(DurablyProviderInner, { durably, children });
25
- if (fallback !== void 0) {
26
- return /* @__PURE__ */ jsx(Suspense, { fallback, children: inner });
27
- }
28
- return inner;
29
- }
30
- function useDurably() {
31
- const context = useContext(DurablyContext);
32
- if (!context) {
33
- throw new Error("useDurably must be used within a DurablyProvider");
34
- }
35
- return context;
6
+ // src/client/use-job.ts
7
+ import { useCallback, useEffect, useRef, useState } from "react";
8
+
9
+ // src/client/use-sse-subscription.ts
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",
47
+ runId: data.runId,
48
+ stepName: null,
49
+ level: data.level,
50
+ message: data.message,
51
+ data: data.data
52
+ });
53
+ break;
54
+ }
55
+ } catch {
56
+ }
57
+ };
58
+ eventSource.onerror = () => {
59
+ onEvent({ type: "connection_error", error: "Connection failed" });
60
+ eventSource.close();
61
+ };
62
+ return () => {
63
+ eventSource.close();
64
+ };
65
+ }
66
+ };
36
67
  }
37
68
 
38
- // src/hooks/use-job.ts
39
- import { useCallback as useCallback2, useEffect as useEffect3, useMemo, useRef as useRef2 } from "react";
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
+ }
40
77
 
41
- // src/hooks/use-auto-resume.ts
42
- import { useEffect } from "react";
43
- function useAutoResume(jobHandle, options, callbacks) {
44
- const enabled = options.enabled !== false;
45
- const skipIfInitialRunId = options.skipIfInitialRunId !== false;
46
- const initialRunId = options.initialRunId;
78
+ // src/client/use-job.ts
79
+ function useJob(options) {
80
+ const {
81
+ api,
82
+ jobName,
83
+ initialRunId,
84
+ autoResume = true,
85
+ followLatest = true
86
+ } = options;
87
+ const [currentRunId, setCurrentRunId] = useState(
88
+ initialRunId ?? null
89
+ );
90
+ const [isPending, setIsPending] = useState(false);
91
+ const hasUserTriggered = useRef(false);
92
+ const waitIntervalRef = useRef(null);
93
+ const subscription = useSSESubscription(api, currentRunId);
94
+ const subscriptionRef = useRef(subscription);
95
+ subscriptionRef.current = subscription;
47
96
  useEffect(() => {
48
- if (!jobHandle) return;
49
- if (!enabled) return;
50
- if (skipIfInitialRunId && initialRunId) return;
51
- let cancelled = false;
97
+ if (!autoResume) return;
98
+ if (initialRunId) return;
99
+ const abortController = new AbortController();
52
100
  const findActiveRun = async () => {
53
- const runningRuns = await jobHandle.getRuns({ status: "running" });
54
- if (cancelled) return;
55
- if (runningRuns.length > 0) {
56
- const run = runningRuns[0];
57
- callbacks.onRunFound(run.id, run.status);
58
- return;
101
+ const signal = abortController.signal;
102
+ const [runningRes, pendingRes] = await Promise.all([
103
+ fetch(
104
+ `${api}/runs?${new URLSearchParams({ jobName, status: "running", limit: "1" })}`,
105
+ { signal }
106
+ ),
107
+ fetch(
108
+ `${api}/runs?${new URLSearchParams({ jobName, status: "pending", limit: "1" })}`,
109
+ { signal }
110
+ )
111
+ ]);
112
+ if (hasUserTriggered.current) return;
113
+ if (runningRes.ok) {
114
+ const runs = await runningRes.json();
115
+ if (runs.length > 0) {
116
+ setCurrentRunId(runs[0].id);
117
+ return;
118
+ }
59
119
  }
60
- const pendingRuns = await jobHandle.getRuns({ status: "pending" });
61
- if (cancelled) return;
62
- if (pendingRuns.length > 0) {
63
- const run = pendingRuns[0];
64
- callbacks.onRunFound(run.id, run.status);
120
+ if (pendingRes.ok) {
121
+ const runs = await pendingRes.json();
122
+ if (runs.length > 0) {
123
+ setCurrentRunId(runs[0].id);
124
+ }
65
125
  }
66
126
  };
67
- findActiveRun();
127
+ findActiveRun().catch((err) => {
128
+ if (err.name !== "AbortError") {
129
+ console.error("autoResume error:", err);
130
+ }
131
+ });
68
132
  return () => {
69
- cancelled = true;
133
+ abortController.abort();
70
134
  };
71
- }, [jobHandle, enabled, skipIfInitialRunId, initialRunId, callbacks]);
72
- }
73
-
74
- // src/hooks/use-job-subscription.ts
75
- import { useCallback, useEffect as useEffect2, useReducer, useRef } from "react";
76
- function jobSubscriptionReducer(state, action) {
77
- switch (action.type) {
78
- case "set_run_id":
79
- return { ...state, currentRunId: action.runId };
80
- case "switch_to_run":
81
- return {
82
- ...initialSubscriptionState,
83
- currentRunId: action.runId,
84
- status: "running"
85
- };
86
- case "reset":
87
- return {
88
- ...initialSubscriptionState,
89
- currentRunId: null
90
- };
91
- default:
92
- return {
93
- ...subscriptionReducer(state, action),
94
- currentRunId: state.currentRunId
95
- };
96
- }
97
- }
98
- function useJobSubscription(durably, jobName, options) {
99
- const initialState = {
100
- ...initialSubscriptionState,
101
- currentRunId: null
102
- };
103
- const [state, dispatch] = useReducer(
104
- jobSubscriptionReducer,
105
- initialState
106
- );
107
- const currentRunIdRef = useRef(null);
108
- currentRunIdRef.current = state.currentRunId;
109
- const followLatest = options?.followLatest !== false;
110
- const maxLogs = options?.maxLogs ?? 0;
111
- useEffect2(() => {
112
- if (!durably) return;
113
- const unsubscribes = [];
114
- unsubscribes.push(
115
- durably.on("run:start", (event) => {
116
- if (event.jobName !== jobName) return;
117
- if (followLatest) {
118
- dispatch({ type: "switch_to_run", runId: event.runId });
119
- currentRunIdRef.current = event.runId;
120
- } else {
121
- if (event.runId !== currentRunIdRef.current) return;
122
- dispatch({ type: "run:start" });
135
+ }, [api, jobName, autoResume, initialRunId]);
136
+ useEffect(() => {
137
+ if (!followLatest) return;
138
+ const params = new URLSearchParams({ jobName });
139
+ const eventSource = new EventSource(`${api}/runs/subscribe?${params}`);
140
+ eventSource.onmessage = (event) => {
141
+ try {
142
+ const data = JSON.parse(event.data);
143
+ if ((data.type === "run:trigger" || data.type === "run:start") && data.runId) {
144
+ setCurrentRunId(data.runId);
123
145
  }
124
- })
125
- );
126
- unsubscribes.push(
127
- durably.on("run:complete", (event) => {
128
- if (event.runId !== currentRunIdRef.current) return;
129
- dispatch({ type: "run:complete", output: event.output });
130
- })
131
- );
132
- unsubscribes.push(
133
- durably.on("run:fail", (event) => {
134
- if (event.runId !== currentRunIdRef.current) return;
135
- dispatch({ type: "run:fail", error: event.error });
136
- })
137
- );
138
- unsubscribes.push(
139
- durably.on("run:cancel", (event) => {
140
- if (event.runId !== currentRunIdRef.current) return;
141
- dispatch({ type: "run:cancel" });
142
- })
143
- );
144
- unsubscribes.push(
145
- durably.on("run:retry", (event) => {
146
- if (event.runId !== currentRunIdRef.current) return;
147
- dispatch({ type: "run:retry" });
148
- })
149
- );
150
- unsubscribes.push(
151
- durably.on("run:progress", (event) => {
152
- if (event.runId !== currentRunIdRef.current) return;
153
- dispatch({ type: "run:progress", progress: event.progress });
154
- })
155
- );
156
- unsubscribes.push(
157
- durably.on("log:write", (event) => {
158
- if (event.runId !== currentRunIdRef.current) return;
159
- dispatch({
160
- type: "log:write",
161
- runId: event.runId,
162
- stepName: event.stepName,
163
- level: event.level,
164
- message: event.message,
165
- data: event.data,
166
- maxLogs
167
- });
168
- })
169
- );
170
- return () => {
171
- for (const unsubscribe of unsubscribes) {
172
- unsubscribe();
146
+ } catch {
173
147
  }
174
148
  };
175
- }, [durably, jobName, followLatest, maxLogs]);
176
- const setCurrentRunId = useCallback((runId) => {
177
- dispatch({ type: "set_run_id", runId });
178
- currentRunIdRef.current = runId;
179
- }, []);
180
- const clearLogs = useCallback(() => {
181
- dispatch({ type: "clear_logs" });
182
- }, []);
183
- const reset = useCallback(() => {
184
- dispatch({ type: "reset" });
185
- currentRunIdRef.current = null;
186
- }, []);
187
- return {
188
- ...state,
189
- setCurrentRunId,
190
- clearLogs,
191
- reset
192
- };
193
- }
194
-
195
- // src/hooks/use-job.ts
196
- function useJob(jobDefinition, options) {
197
- const { durably } = useDurably();
198
- const jobHandleRef = useRef2(null);
199
- useEffect3(() => {
200
- if (!durably) return;
201
- const d = durably.register({
202
- _job: jobDefinition
203
- });
204
- jobHandleRef.current = d.jobs._job;
205
- }, [durably, jobDefinition]);
206
- const subscription = useJobSubscription(
207
- durably,
208
- jobDefinition.name,
209
- {
210
- followLatest: options?.followLatest
211
- }
212
- );
213
- const autoResumeCallbacks = useMemo(
214
- () => ({
215
- onRunFound: (runId, _status) => {
216
- subscription.setCurrentRunId(runId);
217
- }
218
- }),
219
- [subscription.setCurrentRunId]
220
- );
221
- useAutoResume(
222
- jobHandleRef.current,
223
- {
224
- enabled: options?.autoResume,
225
- initialRunId: options?.initialRunId
226
- },
227
- autoResumeCallbacks
228
- );
229
- useEffect3(() => {
230
- if (!durably || !options?.initialRunId) return;
231
- subscription.setCurrentRunId(options.initialRunId);
232
- }, [durably, options?.initialRunId, subscription.setCurrentRunId]);
233
- const trigger = useCallback2(
149
+ eventSource.onerror = () => {
150
+ };
151
+ return () => {
152
+ eventSource.close();
153
+ };
154
+ }, [api, jobName, followLatest]);
155
+ const trigger = useCallback(
234
156
  async (input) => {
235
- const jobHandle = jobHandleRef.current;
236
- if (!jobHandle) {
237
- throw new Error("Job not ready");
238
- }
157
+ hasUserTriggered.current = true;
239
158
  subscription.reset();
240
- const run = await jobHandle.trigger(input);
241
- subscription.setCurrentRunId(run.id);
242
- return { runId: run.id };
159
+ setIsPending(true);
160
+ const response = await fetch(`${api}/trigger`, {
161
+ method: "POST",
162
+ headers: {
163
+ "Content-Type": "application/json"
164
+ },
165
+ body: JSON.stringify({ jobName, input })
166
+ });
167
+ if (!response.ok) {
168
+ setIsPending(false);
169
+ const errorText = await response.text();
170
+ throw new Error(errorText || `HTTP ${response.status}`);
171
+ }
172
+ const { runId } = await response.json();
173
+ setCurrentRunId(runId);
174
+ return { runId };
243
175
  },
244
- [subscription]
176
+ [api, jobName, subscription.reset]
245
177
  );
246
- const triggerAndWait = useCallback2(
178
+ const triggerAndWait = useCallback(
247
179
  async (input) => {
248
- const jobHandle = jobHandleRef.current;
249
- if (!jobHandle || !durably) {
250
- throw new Error("Job not ready");
251
- }
252
- subscription.reset();
253
- const run = await jobHandle.trigger(input);
254
- subscription.setCurrentRunId(run.id);
180
+ const { runId } = await trigger(input);
255
181
  return new Promise((resolve, reject) => {
256
- const checkCompletion = async () => {
257
- const updatedRun = await jobHandle.getRun(run.id);
258
- if (!updatedRun) {
259
- reject(new Error("Run not found"));
260
- return;
261
- }
262
- if (updatedRun.status === "completed") {
263
- resolve({ runId: run.id, output: updatedRun.output });
264
- } else if (updatedRun.status === "failed") {
265
- reject(new Error(updatedRun.error ?? "Job failed"));
266
- } else if (updatedRun.status === "cancelled") {
182
+ if (waitIntervalRef.current) {
183
+ clearInterval(waitIntervalRef.current);
184
+ }
185
+ const checkInterval = setInterval(() => {
186
+ const sub = subscriptionRef.current;
187
+ if (sub.status === "completed" && sub.output) {
188
+ clearInterval(checkInterval);
189
+ waitIntervalRef.current = null;
190
+ resolve({ runId, output: sub.output });
191
+ } else if (sub.status === "failed") {
192
+ clearInterval(checkInterval);
193
+ waitIntervalRef.current = null;
194
+ reject(new Error(sub.error ?? "Job failed"));
195
+ } else if (sub.status === "cancelled") {
196
+ clearInterval(checkInterval);
197
+ waitIntervalRef.current = null;
267
198
  reject(new Error("Job cancelled"));
268
- } else {
269
- setTimeout(checkCompletion, 50);
270
199
  }
271
- };
272
- checkCompletion();
200
+ }, 50);
201
+ waitIntervalRef.current = checkInterval;
273
202
  });
274
203
  },
275
- [durably, subscription]
204
+ [trigger]
276
205
  );
206
+ useEffect(() => {
207
+ return () => {
208
+ if (waitIntervalRef.current) {
209
+ clearInterval(waitIntervalRef.current);
210
+ }
211
+ };
212
+ }, []);
213
+ const reset = useCallback(() => {
214
+ subscription.reset();
215
+ setCurrentRunId(null);
216
+ setIsPending(false);
217
+ }, [subscription.reset]);
218
+ const effectiveStatus = subscription.status ?? (isPending ? "pending" : null);
219
+ useEffect(() => {
220
+ if (subscription.status && isPending) {
221
+ setIsPending(false);
222
+ }
223
+ }, [subscription.status, isPending]);
277
224
  return {
278
225
  trigger,
279
226
  triggerAndWait,
280
- status: subscription.status,
227
+ status: effectiveStatus,
281
228
  output: subscription.output,
282
229
  error: subscription.error,
283
230
  logs: subscription.logs,
284
231
  progress: subscription.progress,
285
- isRunning: subscription.status === "running",
286
- isPending: subscription.status === "pending",
287
- isCompleted: subscription.status === "completed",
288
- isFailed: subscription.status === "failed",
289
- isCancelled: subscription.status === "cancelled",
290
- currentRunId: subscription.currentRunId,
291
- reset: subscription.reset
292
- };
293
- }
294
-
295
- // src/hooks/use-run-subscription.ts
296
- import { useMemo as useMemo2 } from "react";
297
-
298
- // src/shared/durably-event-subscriber.ts
299
- function createDurablyEventSubscriber(durably) {
300
- return {
301
- subscribe(runId, onEvent) {
302
- const unsubscribes = [];
303
- unsubscribes.push(
304
- durably.on("run:start", (event) => {
305
- if (event.runId !== runId) return;
306
- onEvent({ type: "run:start" });
307
- })
308
- );
309
- unsubscribes.push(
310
- durably.on("run:complete", (event) => {
311
- if (event.runId !== runId) return;
312
- onEvent({ type: "run:complete", output: event.output });
313
- })
314
- );
315
- unsubscribes.push(
316
- durably.on("run:fail", (event) => {
317
- if (event.runId !== runId) return;
318
- onEvent({ type: "run:fail", error: event.error });
319
- })
320
- );
321
- unsubscribes.push(
322
- durably.on("run:cancel", (event) => {
323
- if (event.runId !== runId) return;
324
- onEvent({ type: "run:cancel" });
325
- })
326
- );
327
- unsubscribes.push(
328
- durably.on("run:retry", (event) => {
329
- if (event.runId !== runId) return;
330
- onEvent({ type: "run:retry" });
331
- })
332
- );
333
- unsubscribes.push(
334
- durably.on("run:progress", (event) => {
335
- if (event.runId !== runId) return;
336
- onEvent({ type: "run:progress", progress: event.progress });
337
- })
338
- );
339
- unsubscribes.push(
340
- durably.on("log:write", (event) => {
341
- if (event.runId !== runId) return;
342
- onEvent({
343
- type: "log:write",
344
- runId: event.runId,
345
- stepName: event.stepName,
346
- level: event.level,
347
- message: event.message,
348
- data: event.data
349
- });
350
- })
351
- );
352
- return () => {
353
- for (const unsubscribe of unsubscribes) {
354
- unsubscribe();
355
- }
356
- };
357
- }
232
+ isRunning: effectiveStatus === "running",
233
+ isPending: effectiveStatus === "pending",
234
+ isCompleted: effectiveStatus === "completed",
235
+ isFailed: effectiveStatus === "failed",
236
+ isCancelled: effectiveStatus === "cancelled",
237
+ currentRunId,
238
+ reset
358
239
  };
359
240
  }
360
241
 
361
- // src/hooks/use-run-subscription.ts
362
- function useRunSubscription(durably, runId, options) {
363
- const subscriber = useMemo2(
364
- () => durably ? createDurablyEventSubscriber(durably) : null,
365
- [durably]
366
- );
367
- return useSubscription(subscriber, runId, options);
368
- }
369
-
370
- // src/hooks/use-job-logs.ts
242
+ // src/client/use-job-logs.ts
371
243
  function useJobLogs(options) {
372
- const { durably } = useDurably();
373
- const { runId, maxLogs } = options;
374
- const subscription = useRunSubscription(durably, runId, { maxLogs });
244
+ const { api, runId, maxLogs } = options;
245
+ const subscription = useSSESubscription(api, runId, { maxLogs });
375
246
  return {
376
247
  logs: subscription.logs,
377
248
  clearLogs: subscription.clearLogs
378
249
  };
379
250
  }
380
251
 
381
- // src/hooks/use-job-run.ts
382
- import { useEffect as useEffect4, useRef as useRef3 } from "react";
252
+ // src/client/use-job-run.ts
253
+ import { useEffect as useEffect2, useRef as useRef2 } from "react";
383
254
  function useJobRun(options) {
384
- const { durably } = useDurably();
385
- const { runId } = options;
386
- const subscription = useRunSubscription(durably, runId);
387
- const fetchedRef = useRef3(/* @__PURE__ */ new Set());
388
- useEffect4(() => {
389
- if (!durably || !runId || fetchedRef.current.has(runId)) return;
390
- fetchedRef.current.add(runId);
391
- }, [durably, runId]);
255
+ const { api, runId, onStart, onComplete, onFail } = options;
256
+ const subscription = useSSESubscription(api, runId);
257
+ const effectiveStatus = subscription.status ?? (runId ? "pending" : null);
258
+ const isCompleted = effectiveStatus === "completed";
259
+ const isFailed = effectiveStatus === "failed";
260
+ const isPending = effectiveStatus === "pending";
261
+ const isRunning = effectiveStatus === "running";
262
+ const isCancelled = effectiveStatus === "cancelled";
263
+ const prevStatusRef = useRef2(null);
264
+ useEffect2(() => {
265
+ const prevStatus = prevStatusRef.current;
266
+ prevStatusRef.current = effectiveStatus;
267
+ if (prevStatus !== effectiveStatus) {
268
+ if (prevStatus === null && (isPending || isRunning) && onStart) {
269
+ onStart();
270
+ }
271
+ if (isCompleted && onComplete) {
272
+ onComplete();
273
+ }
274
+ if (isFailed && onFail) {
275
+ onFail();
276
+ }
277
+ }
278
+ }, [
279
+ effectiveStatus,
280
+ isPending,
281
+ isRunning,
282
+ isCompleted,
283
+ isFailed,
284
+ onStart,
285
+ onComplete,
286
+ onFail
287
+ ]);
392
288
  return {
393
- status: subscription.status,
289
+ status: effectiveStatus,
394
290
  output: subscription.output,
395
291
  error: subscription.error,
396
292
  logs: subscription.logs,
397
293
  progress: subscription.progress,
398
- isRunning: subscription.status === "running",
399
- isPending: subscription.status === "pending",
400
- isCompleted: subscription.status === "completed",
401
- isFailed: subscription.status === "failed",
402
- isCancelled: subscription.status === "cancelled"
294
+ isRunning,
295
+ isPending,
296
+ isCompleted,
297
+ isFailed,
298
+ isCancelled
299
+ };
300
+ }
301
+
302
+ // src/client/create-job-hooks.ts
303
+ function createJobHooks(options) {
304
+ const { api, jobName } = options;
305
+ return {
306
+ useJob: () => {
307
+ return useJob({ api, jobName });
308
+ },
309
+ useRun: (runId) => {
310
+ return useJobRun({ api, runId });
311
+ },
312
+ useLogs: (runId, logsOptions) => {
313
+ return useJobLogs({ api, runId, maxLogs: logsOptions?.maxLogs });
314
+ }
315
+ };
316
+ }
317
+
318
+ // src/client/use-run-actions.ts
319
+ import { useCallback as useCallback2, useState as useState2 } from "react";
320
+ function useRunActions(options) {
321
+ const { api } = options;
322
+ const [isLoading, setIsLoading] = useState2(false);
323
+ const [error, setError] = useState2(null);
324
+ const retry = useCallback2(
325
+ async (runId) => {
326
+ setIsLoading(true);
327
+ setError(null);
328
+ try {
329
+ const url = `${api}/retry?runId=${encodeURIComponent(runId)}`;
330
+ const response = await fetch(url, { method: "POST" });
331
+ if (!response.ok) {
332
+ let errorMessage = `Failed to retry: ${response.statusText}`;
333
+ try {
334
+ const data = await response.json();
335
+ if (data.error) {
336
+ errorMessage = data.error;
337
+ }
338
+ } catch {
339
+ }
340
+ throw new Error(errorMessage);
341
+ }
342
+ } catch (err) {
343
+ const message = err instanceof Error ? err.message : "Unknown error";
344
+ setError(message);
345
+ throw err;
346
+ } finally {
347
+ setIsLoading(false);
348
+ }
349
+ },
350
+ [api]
351
+ );
352
+ const cancel = useCallback2(
353
+ async (runId) => {
354
+ setIsLoading(true);
355
+ setError(null);
356
+ try {
357
+ const url = `${api}/cancel?runId=${encodeURIComponent(runId)}`;
358
+ const response = await fetch(url, { method: "POST" });
359
+ if (!response.ok) {
360
+ let errorMessage = `Failed to cancel: ${response.statusText}`;
361
+ try {
362
+ const data = await response.json();
363
+ if (data.error) {
364
+ errorMessage = data.error;
365
+ }
366
+ } catch {
367
+ }
368
+ throw new Error(errorMessage);
369
+ }
370
+ } catch (err) {
371
+ const message = err instanceof Error ? err.message : "Unknown error";
372
+ setError(message);
373
+ throw err;
374
+ } finally {
375
+ setIsLoading(false);
376
+ }
377
+ },
378
+ [api]
379
+ );
380
+ const deleteRun = useCallback2(
381
+ async (runId) => {
382
+ setIsLoading(true);
383
+ setError(null);
384
+ try {
385
+ const url = `${api}/run?runId=${encodeURIComponent(runId)}`;
386
+ const response = await fetch(url, { method: "DELETE" });
387
+ if (!response.ok) {
388
+ let errorMessage = `Failed to delete: ${response.statusText}`;
389
+ try {
390
+ const data = await response.json();
391
+ if (data.error) {
392
+ errorMessage = data.error;
393
+ }
394
+ } catch {
395
+ }
396
+ throw new Error(errorMessage);
397
+ }
398
+ } catch (err) {
399
+ const message = err instanceof Error ? err.message : "Unknown error";
400
+ setError(message);
401
+ throw err;
402
+ } finally {
403
+ setIsLoading(false);
404
+ }
405
+ },
406
+ [api]
407
+ );
408
+ const getRun = useCallback2(
409
+ async (runId) => {
410
+ setIsLoading(true);
411
+ setError(null);
412
+ try {
413
+ const url = `${api}/run?runId=${encodeURIComponent(runId)}`;
414
+ const response = await fetch(url);
415
+ if (response.status === 404) {
416
+ return null;
417
+ }
418
+ if (!response.ok) {
419
+ let errorMessage = `Failed to get run: ${response.statusText}`;
420
+ try {
421
+ const data = await response.json();
422
+ if (data.error) {
423
+ errorMessage = data.error;
424
+ }
425
+ } catch {
426
+ }
427
+ throw new Error(errorMessage);
428
+ }
429
+ return await response.json();
430
+ } catch (err) {
431
+ const message = err instanceof Error ? err.message : "Unknown error";
432
+ setError(message);
433
+ throw err;
434
+ } finally {
435
+ setIsLoading(false);
436
+ }
437
+ },
438
+ [api]
439
+ );
440
+ const getSteps = useCallback2(
441
+ async (runId) => {
442
+ setIsLoading(true);
443
+ setError(null);
444
+ try {
445
+ const url = `${api}/steps?runId=${encodeURIComponent(runId)}`;
446
+ const response = await fetch(url);
447
+ if (!response.ok) {
448
+ let errorMessage = `Failed to get steps: ${response.statusText}`;
449
+ try {
450
+ const data = await response.json();
451
+ if (data.error) {
452
+ errorMessage = data.error;
453
+ }
454
+ } catch {
455
+ }
456
+ throw new Error(errorMessage);
457
+ }
458
+ return await response.json();
459
+ } catch (err) {
460
+ const message = err instanceof Error ? err.message : "Unknown error";
461
+ setError(message);
462
+ throw err;
463
+ } finally {
464
+ setIsLoading(false);
465
+ }
466
+ },
467
+ [api]
468
+ );
469
+ return {
470
+ retry,
471
+ cancel,
472
+ deleteRun,
473
+ getRun,
474
+ getSteps,
475
+ isLoading,
476
+ error
403
477
  };
404
478
  }
405
479
 
406
- // src/hooks/use-runs.ts
407
- import { useCallback as useCallback3, useEffect as useEffect5, useMemo as useMemo3, useState } from "react";
480
+ // src/client/use-runs.ts
481
+ import { useCallback as useCallback3, useEffect as useEffect3, useMemo as useMemo2, useRef as useRef3, useState as useState3 } from "react";
408
482
  function useRuns(jobDefinitionOrOptions, optionsArg) {
409
- const { durably } = useDurably();
410
483
  const isJob = isJobDefinition(jobDefinitionOrOptions);
411
- const jobName = isJob ? jobDefinitionOrOptions.name : jobDefinitionOrOptions?.jobName;
484
+ const jobName = isJob ? jobDefinitionOrOptions.name : jobDefinitionOrOptions.jobName;
412
485
  const options = isJob ? optionsArg : jobDefinitionOrOptions;
413
- const pageSize = options?.pageSize ?? 10;
414
- const realtime = options?.realtime ?? true;
415
- const status = options?.status;
416
- const labelsKey = options?.labels ? JSON.stringify(options.labels) : void 0;
417
- const labels = useMemo3(
486
+ const { api, status, labels, pageSize = 10, realtime = true } = options;
487
+ const labelsKey = labels ? JSON.stringify(labels) : void 0;
488
+ const stableLabels = useMemo2(
418
489
  () => labelsKey ? JSON.parse(labelsKey) : void 0,
419
490
  [labelsKey]
420
491
  );
421
- const [runs, setRuns] = useState([]);
422
- const [page, setPage] = useState(0);
423
- const [hasMore, setHasMore] = useState(false);
424
- const [isLoading, setIsLoading] = useState(true);
492
+ const jobNameKey = jobName ? JSON.stringify(jobName) : void 0;
493
+ const stableJobName = useMemo2(
494
+ () => jobNameKey ? JSON.parse(jobNameKey) : void 0,
495
+ [jobNameKey]
496
+ );
497
+ const [runs, setRuns] = useState3([]);
498
+ const [page, setPage] = useState3(0);
499
+ const [hasMore, setHasMore] = useState3(false);
500
+ const [isLoading, setIsLoading] = useState3(true);
501
+ const [error, setError] = useState3(null);
502
+ const isMountedRef = useRef3(true);
503
+ const eventSourceRef = useRef3(null);
425
504
  const refresh = useCallback3(async () => {
426
- if (!durably) return;
427
505
  setIsLoading(true);
506
+ setError(null);
428
507
  try {
429
- const data = await durably.getRuns({
430
- jobName,
431
- status,
432
- labels,
433
- limit: pageSize + 1,
434
- offset: page * pageSize
435
- });
436
- setHasMore(data.length > pageSize);
437
- setRuns(data.slice(0, pageSize));
508
+ const params = new URLSearchParams();
509
+ appendJobNameToParams(params, stableJobName);
510
+ if (status) params.set("status", status);
511
+ appendLabelsToParams(params, stableLabels);
512
+ params.set("limit", String(pageSize + 1));
513
+ params.set("offset", String(page * pageSize));
514
+ const url = `${api}/runs?${params.toString()}`;
515
+ const response = await fetch(url);
516
+ if (!response.ok) {
517
+ throw new Error(`Failed to fetch runs: ${response.statusText}`);
518
+ }
519
+ const data = await response.json();
520
+ if (isMountedRef.current) {
521
+ setHasMore(data.length > pageSize);
522
+ setRuns(data.slice(0, pageSize));
523
+ }
524
+ } catch (err) {
525
+ if (isMountedRef.current) {
526
+ setError(err instanceof Error ? err.message : "Unknown error");
527
+ }
438
528
  } finally {
439
- setIsLoading(false);
529
+ if (isMountedRef.current) {
530
+ setIsLoading(false);
531
+ }
440
532
  }
441
- }, [durably, jobName, status, labels, pageSize, page]);
442
- useEffect5(() => {
443
- if (!durably) return;
533
+ }, [api, stableJobName, status, stableLabels, pageSize, page]);
534
+ useEffect3(() => {
535
+ isMountedRef.current = true;
444
536
  refresh();
445
- if (!realtime) return;
446
- const unsubscribes = [
447
- durably.on("run:trigger", refresh),
448
- durably.on("run:start", refresh),
449
- durably.on("run:complete", refresh),
450
- durably.on("run:fail", refresh),
451
- durably.on("run:cancel", refresh),
452
- durably.on("run:delete", refresh),
453
- durably.on("run:retry", refresh),
454
- durably.on("run:progress", refresh),
455
- durably.on("step:start", refresh),
456
- durably.on("step:complete", refresh),
457
- durably.on("step:fail", refresh),
458
- durably.on("step:cancel", refresh)
459
- ];
460
537
  return () => {
461
- for (const unsubscribe of unsubscribes) {
462
- unsubscribe();
538
+ isMountedRef.current = false;
539
+ };
540
+ }, [refresh]);
541
+ useEffect3(() => {
542
+ if (!realtime || page !== 0) {
543
+ if (eventSourceRef.current) {
544
+ eventSourceRef.current.close();
545
+ eventSourceRef.current = null;
546
+ }
547
+ return;
548
+ }
549
+ const params = new URLSearchParams();
550
+ appendJobNameToParams(params, stableJobName);
551
+ appendLabelsToParams(params, stableLabels);
552
+ const sseUrl = `${api}/runs/subscribe${params.toString() ? `?${params.toString()}` : ""}`;
553
+ const eventSource = new EventSource(sseUrl);
554
+ eventSourceRef.current = eventSource;
555
+ eventSource.onmessage = (event) => {
556
+ try {
557
+ const data = JSON.parse(event.data);
558
+ if (data.type === "run:trigger" || data.type === "run:start" || data.type === "run:complete" || data.type === "run:fail" || data.type === "run:cancel" || data.type === "run:delete" || data.type === "run:retry") {
559
+ refresh();
560
+ }
561
+ if (data.type === "run:progress") {
562
+ setRuns(
563
+ (prev) => prev.map(
564
+ (run) => run.id === data.runId ? { ...run, progress: data.progress } : run
565
+ )
566
+ );
567
+ }
568
+ if (data.type === "step:complete") {
569
+ setRuns(
570
+ (prev) => prev.map(
571
+ (run) => run.id === data.runId ? { ...run, currentStepIndex: data.stepIndex + 1 } : run
572
+ )
573
+ );
574
+ }
575
+ if (data.type === "step:start" || data.type === "step:fail" || data.type === "step:cancel") {
576
+ refresh();
577
+ }
578
+ } catch {
463
579
  }
464
580
  };
465
- }, [durably, refresh, realtime]);
581
+ eventSource.onerror = () => {
582
+ };
583
+ return () => {
584
+ eventSource.close();
585
+ eventSourceRef.current = null;
586
+ };
587
+ }, [api, stableJobName, stableLabels, page, realtime, refresh]);
466
588
  const nextPage = useCallback3(() => {
467
589
  if (hasMore) {
468
590
  setPage((p) => p + 1);
@@ -479,18 +601,54 @@ function useRuns(jobDefinitionOrOptions, optionsArg) {
479
601
  page,
480
602
  hasMore,
481
603
  isLoading,
604
+ error,
482
605
  nextPage,
483
606
  prevPage,
484
607
  goToPage,
485
608
  refresh
486
609
  };
487
610
  }
611
+ function appendJobNameToParams(params, jobName) {
612
+ if (!jobName) return;
613
+ for (const name of Array.isArray(jobName) ? jobName : [jobName]) {
614
+ params.append("jobName", name);
615
+ }
616
+ }
617
+ function appendLabelsToParams(params, labels) {
618
+ if (!labels) return;
619
+ for (const [key, value] of Object.entries(labels)) {
620
+ params.set(`label.${key}`, value);
621
+ }
622
+ }
623
+
624
+ // src/client/create-durably.ts
625
+ function createDurably(options) {
626
+ const { api } = options;
627
+ const cache = /* @__PURE__ */ new Map();
628
+ const builtins = {
629
+ useRuns: (opts) => useRuns({ api, ...opts }),
630
+ useRunActions: () => useRunActions({ api })
631
+ };
632
+ return new Proxy({}, {
633
+ get(_target, key) {
634
+ if (typeof key !== "string") return void 0;
635
+ if (key in builtins) return builtins[key];
636
+ let hooks = cache.get(key);
637
+ if (!hooks) {
638
+ hooks = createJobHooks({ api, jobName: key });
639
+ cache.set(key, hooks);
640
+ }
641
+ return hooks;
642
+ }
643
+ });
644
+ }
488
645
  export {
489
- DurablyProvider,
490
- useDurably,
646
+ createDurably,
647
+ createJobHooks,
491
648
  useJob,
492
649
  useJobLogs,
493
650
  useJobRun,
651
+ useRunActions,
494
652
  useRuns
495
653
  };
496
654
  //# sourceMappingURL=index.js.map