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