@coji/durably-react 0.10.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/README.md +37 -47
- package/dist/index.d.ts +296 -76
- package/dist/index.js +555 -397
- package/dist/index.js.map +1 -1
- package/dist/spa.d.ts +301 -0
- package/dist/spa.js +501 -0
- package/dist/spa.js.map +1 -0
- package/dist/{types-xrRs7jov.d.ts → types-JIBwGTm6.d.ts} +2 -2
- package/package.json +6 -6
- package/dist/client.d.ts +0 -515
- package/dist/client.js +0 -629
- package/dist/client.js.map +0 -1
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/
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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/
|
|
39
|
-
|
|
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/
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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 (!
|
|
49
|
-
if (
|
|
50
|
-
|
|
51
|
-
let cancelled = false;
|
|
97
|
+
if (!autoResume) return;
|
|
98
|
+
if (initialRunId) return;
|
|
99
|
+
const abortController = new AbortController();
|
|
52
100
|
const findActiveRun = async () => {
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
133
|
+
abortController.abort();
|
|
70
134
|
};
|
|
71
|
-
}, [
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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
|
-
|
|
236
|
-
if (!jobHandle) {
|
|
237
|
-
throw new Error("Job not ready");
|
|
238
|
-
}
|
|
157
|
+
hasUserTriggered.current = true;
|
|
239
158
|
subscription.reset();
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
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 =
|
|
178
|
+
const triggerAndWait = useCallback(
|
|
247
179
|
async (input) => {
|
|
248
|
-
const
|
|
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
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
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
|
-
|
|
200
|
+
}, 50);
|
|
201
|
+
waitIntervalRef.current = checkInterval;
|
|
273
202
|
});
|
|
274
203
|
},
|
|
275
|
-
[
|
|
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:
|
|
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:
|
|
286
|
-
isPending:
|
|
287
|
-
isCompleted:
|
|
288
|
-
isFailed:
|
|
289
|
-
isCancelled:
|
|
290
|
-
currentRunId
|
|
291
|
-
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/
|
|
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 {
|
|
373
|
-
const
|
|
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/
|
|
382
|
-
import { useEffect as
|
|
252
|
+
// src/client/use-job-run.ts
|
|
253
|
+
import { useEffect as useEffect2, useRef as useRef2 } from "react";
|
|
383
254
|
function useJobRun(options) {
|
|
384
|
-
const {
|
|
385
|
-
const
|
|
386
|
-
const
|
|
387
|
-
const
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
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:
|
|
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
|
|
399
|
-
isPending
|
|
400
|
-
isCompleted
|
|
401
|
-
isFailed
|
|
402
|
-
isCancelled
|
|
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/
|
|
407
|
-
import { useCallback as useCallback3, useEffect as
|
|
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
|
|
484
|
+
const jobName = isJob ? jobDefinitionOrOptions.name : jobDefinitionOrOptions.jobName;
|
|
412
485
|
const options = isJob ? optionsArg : jobDefinitionOrOptions;
|
|
413
|
-
const pageSize =
|
|
414
|
-
const
|
|
415
|
-
const
|
|
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
|
|
422
|
-
const
|
|
423
|
-
|
|
424
|
-
|
|
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
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
})
|
|
436
|
-
|
|
437
|
-
|
|
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
|
-
|
|
529
|
+
if (isMountedRef.current) {
|
|
530
|
+
setIsLoading(false);
|
|
531
|
+
}
|
|
440
532
|
}
|
|
441
|
-
}, [
|
|
442
|
-
|
|
443
|
-
|
|
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
|
-
|
|
462
|
-
|
|
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
|
-
|
|
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
|
-
|
|
490
|
-
|
|
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
|