@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/README.md +37 -47
- package/dist/{chunk-XRJEZWAV.js → chunk-TGMPMPMX.js} +1 -4
- package/dist/chunk-TGMPMPMX.js.map +1 -0
- package/dist/index.d.ts +296 -76
- package/dist/index.js +513 -398
- package/dist/index.js.map +1 -1
- package/dist/spa.d.ts +301 -0
- package/dist/spa.js +483 -0
- package/dist/spa.js.map +1 -0
- package/dist/{types-xrRs7jov.d.ts → types-D17R7ZUn.d.ts} +2 -6
- package/package.json +6 -6
- package/dist/chunk-XRJEZWAV.js.map +0 -1
- 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,547 @@
|
|
|
1
1
|
import {
|
|
2
|
-
initialSubscriptionState,
|
|
3
2
|
isJobDefinition,
|
|
4
|
-
subscriptionReducer,
|
|
5
3
|
useSubscription
|
|
6
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-TGMPMPMX.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: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/
|
|
39
|
-
|
|
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/
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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 (!
|
|
49
|
-
if (
|
|
50
|
-
|
|
51
|
-
let cancelled = false;
|
|
94
|
+
if (!autoResume) return;
|
|
95
|
+
if (initialRunId) return;
|
|
96
|
+
const abortController = new AbortController();
|
|
52
97
|
const findActiveRun = async () => {
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
130
|
+
abortController.abort();
|
|
70
131
|
};
|
|
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" });
|
|
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
|
-
|
|
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(
|
|
146
|
+
eventSource.onerror = () => {
|
|
147
|
+
};
|
|
148
|
+
return () => {
|
|
149
|
+
eventSource.close();
|
|
150
|
+
};
|
|
151
|
+
}, [api, jobName, followLatest]);
|
|
152
|
+
const trigger = useCallback(
|
|
234
153
|
async (input) => {
|
|
235
|
-
|
|
236
|
-
if (!jobHandle) {
|
|
237
|
-
throw new Error("Job not ready");
|
|
238
|
-
}
|
|
154
|
+
hasUserTriggered.current = true;
|
|
239
155
|
subscription.reset();
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
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 =
|
|
175
|
+
const triggerAndWait = useCallback(
|
|
247
176
|
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);
|
|
177
|
+
const { runId } = await trigger(input);
|
|
255
178
|
return new Promise((resolve, reject) => {
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
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
|
-
|
|
197
|
+
}, 50);
|
|
198
|
+
waitIntervalRef.current = checkInterval;
|
|
273
199
|
});
|
|
274
200
|
},
|
|
275
|
-
[
|
|
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:
|
|
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:
|
|
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
|
-
}
|
|
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/
|
|
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 {
|
|
373
|
-
const
|
|
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/
|
|
382
|
-
import { useEffect as
|
|
249
|
+
// src/client/use-job-run.ts
|
|
250
|
+
import { useEffect as useEffect2, useRef as useRef2 } from "react";
|
|
383
251
|
function useJobRun(options) {
|
|
384
|
-
const {
|
|
385
|
-
const
|
|
386
|
-
const
|
|
387
|
-
const
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
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:
|
|
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
|
|
399
|
-
isPending
|
|
400
|
-
isCompleted
|
|
401
|
-
isFailed
|
|
402
|
-
isCancelled
|
|
291
|
+
isRunning,
|
|
292
|
+
isPending,
|
|
293
|
+
isCompleted,
|
|
294
|
+
isFailed,
|
|
295
|
+
isCancelled
|
|
403
296
|
};
|
|
404
297
|
}
|
|
405
298
|
|
|
406
|
-
// src/
|
|
407
|
-
|
|
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
|
|
441
|
+
const jobName = isJob ? jobDefinitionOrOptions.name : jobDefinitionOrOptions.jobName;
|
|
412
442
|
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(
|
|
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
|
|
422
|
-
const
|
|
423
|
-
|
|
424
|
-
|
|
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
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
})
|
|
436
|
-
|
|
437
|
-
|
|
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
|
-
|
|
486
|
+
if (isMountedRef.current) {
|
|
487
|
+
setIsLoading(false);
|
|
488
|
+
}
|
|
440
489
|
}
|
|
441
|
-
}, [
|
|
442
|
-
|
|
443
|
-
|
|
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
|
-
|
|
462
|
-
|
|
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
|
-
|
|
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
|
-
|
|
490
|
-
|
|
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
|