@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/spa.js
ADDED
|
@@ -0,0 +1,483 @@
|
|
|
1
|
+
import {
|
|
2
|
+
initialSubscriptionState,
|
|
3
|
+
isJobDefinition,
|
|
4
|
+
subscriptionReducer,
|
|
5
|
+
useSubscription
|
|
6
|
+
} from "./chunk-TGMPMPMX.js";
|
|
7
|
+
|
|
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;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// src/hooks/use-job.ts
|
|
39
|
+
import { useCallback as useCallback2, useEffect as useEffect3, useMemo, useRef as useRef2 } from "react";
|
|
40
|
+
|
|
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;
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
if (!jobHandle) return;
|
|
49
|
+
if (!enabled) return;
|
|
50
|
+
if (skipIfInitialRunId && initialRunId) return;
|
|
51
|
+
let cancelled = false;
|
|
52
|
+
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;
|
|
59
|
+
}
|
|
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);
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
findActiveRun();
|
|
68
|
+
return () => {
|
|
69
|
+
cancelled = true;
|
|
70
|
+
};
|
|
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" });
|
|
123
|
+
}
|
|
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:progress", (event) => {
|
|
146
|
+
if (event.runId !== currentRunIdRef.current) return;
|
|
147
|
+
dispatch({ type: "run:progress", progress: event.progress });
|
|
148
|
+
})
|
|
149
|
+
);
|
|
150
|
+
unsubscribes.push(
|
|
151
|
+
durably.on("log:write", (event) => {
|
|
152
|
+
if (event.runId !== currentRunIdRef.current) return;
|
|
153
|
+
dispatch({
|
|
154
|
+
type: "log:write",
|
|
155
|
+
runId: event.runId,
|
|
156
|
+
stepName: event.stepName,
|
|
157
|
+
level: event.level,
|
|
158
|
+
message: event.message,
|
|
159
|
+
data: event.data,
|
|
160
|
+
maxLogs
|
|
161
|
+
});
|
|
162
|
+
})
|
|
163
|
+
);
|
|
164
|
+
return () => {
|
|
165
|
+
for (const unsubscribe of unsubscribes) {
|
|
166
|
+
unsubscribe();
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
}, [durably, jobName, followLatest, maxLogs]);
|
|
170
|
+
const setCurrentRunId = useCallback((runId) => {
|
|
171
|
+
dispatch({ type: "set_run_id", runId });
|
|
172
|
+
currentRunIdRef.current = runId;
|
|
173
|
+
}, []);
|
|
174
|
+
const clearLogs = useCallback(() => {
|
|
175
|
+
dispatch({ type: "clear_logs" });
|
|
176
|
+
}, []);
|
|
177
|
+
const reset = useCallback(() => {
|
|
178
|
+
dispatch({ type: "reset" });
|
|
179
|
+
currentRunIdRef.current = null;
|
|
180
|
+
}, []);
|
|
181
|
+
return {
|
|
182
|
+
...state,
|
|
183
|
+
setCurrentRunId,
|
|
184
|
+
clearLogs,
|
|
185
|
+
reset
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// src/hooks/use-job.ts
|
|
190
|
+
function useJob(jobDefinition, options) {
|
|
191
|
+
const { durably } = useDurably();
|
|
192
|
+
const jobHandleRef = useRef2(null);
|
|
193
|
+
useEffect3(() => {
|
|
194
|
+
if (!durably) return;
|
|
195
|
+
const d = durably.register({
|
|
196
|
+
_job: jobDefinition
|
|
197
|
+
});
|
|
198
|
+
jobHandleRef.current = d.jobs._job;
|
|
199
|
+
}, [durably, jobDefinition]);
|
|
200
|
+
const subscription = useJobSubscription(
|
|
201
|
+
durably,
|
|
202
|
+
jobDefinition.name,
|
|
203
|
+
{
|
|
204
|
+
followLatest: options?.followLatest
|
|
205
|
+
}
|
|
206
|
+
);
|
|
207
|
+
const autoResumeCallbacks = useMemo(
|
|
208
|
+
() => ({
|
|
209
|
+
onRunFound: (runId, _status) => {
|
|
210
|
+
subscription.setCurrentRunId(runId);
|
|
211
|
+
}
|
|
212
|
+
}),
|
|
213
|
+
[subscription.setCurrentRunId]
|
|
214
|
+
);
|
|
215
|
+
useAutoResume(
|
|
216
|
+
jobHandleRef.current,
|
|
217
|
+
{
|
|
218
|
+
enabled: options?.autoResume,
|
|
219
|
+
initialRunId: options?.initialRunId
|
|
220
|
+
},
|
|
221
|
+
autoResumeCallbacks
|
|
222
|
+
);
|
|
223
|
+
useEffect3(() => {
|
|
224
|
+
if (!durably || !options?.initialRunId) return;
|
|
225
|
+
subscription.setCurrentRunId(options.initialRunId);
|
|
226
|
+
}, [durably, options?.initialRunId, subscription.setCurrentRunId]);
|
|
227
|
+
const trigger = useCallback2(
|
|
228
|
+
async (input) => {
|
|
229
|
+
const jobHandle = jobHandleRef.current;
|
|
230
|
+
if (!jobHandle) {
|
|
231
|
+
throw new Error("Job not ready");
|
|
232
|
+
}
|
|
233
|
+
subscription.reset();
|
|
234
|
+
const run = await jobHandle.trigger(input);
|
|
235
|
+
subscription.setCurrentRunId(run.id);
|
|
236
|
+
return { runId: run.id };
|
|
237
|
+
},
|
|
238
|
+
[subscription]
|
|
239
|
+
);
|
|
240
|
+
const triggerAndWait = useCallback2(
|
|
241
|
+
async (input) => {
|
|
242
|
+
const jobHandle = jobHandleRef.current;
|
|
243
|
+
if (!jobHandle || !durably) {
|
|
244
|
+
throw new Error("Job not ready");
|
|
245
|
+
}
|
|
246
|
+
subscription.reset();
|
|
247
|
+
const run = await jobHandle.trigger(input);
|
|
248
|
+
subscription.setCurrentRunId(run.id);
|
|
249
|
+
return new Promise((resolve, reject) => {
|
|
250
|
+
const checkCompletion = async () => {
|
|
251
|
+
const updatedRun = await jobHandle.getRun(run.id);
|
|
252
|
+
if (!updatedRun) {
|
|
253
|
+
reject(new Error("Run not found"));
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
if (updatedRun.status === "completed") {
|
|
257
|
+
resolve({ runId: run.id, output: updatedRun.output });
|
|
258
|
+
} else if (updatedRun.status === "failed") {
|
|
259
|
+
reject(new Error(updatedRun.error ?? "Job failed"));
|
|
260
|
+
} else if (updatedRun.status === "cancelled") {
|
|
261
|
+
reject(new Error("Job cancelled"));
|
|
262
|
+
} else {
|
|
263
|
+
setTimeout(checkCompletion, 50);
|
|
264
|
+
}
|
|
265
|
+
};
|
|
266
|
+
checkCompletion();
|
|
267
|
+
});
|
|
268
|
+
},
|
|
269
|
+
[durably, subscription]
|
|
270
|
+
);
|
|
271
|
+
return {
|
|
272
|
+
trigger,
|
|
273
|
+
triggerAndWait,
|
|
274
|
+
status: subscription.status,
|
|
275
|
+
output: subscription.output,
|
|
276
|
+
error: subscription.error,
|
|
277
|
+
logs: subscription.logs,
|
|
278
|
+
progress: subscription.progress,
|
|
279
|
+
isRunning: subscription.status === "running",
|
|
280
|
+
isPending: subscription.status === "pending",
|
|
281
|
+
isCompleted: subscription.status === "completed",
|
|
282
|
+
isFailed: subscription.status === "failed",
|
|
283
|
+
isCancelled: subscription.status === "cancelled",
|
|
284
|
+
currentRunId: subscription.currentRunId,
|
|
285
|
+
reset: subscription.reset
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// src/hooks/use-run-subscription.ts
|
|
290
|
+
import { useMemo as useMemo2 } from "react";
|
|
291
|
+
|
|
292
|
+
// src/shared/durably-event-subscriber.ts
|
|
293
|
+
function createDurablyEventSubscriber(durably) {
|
|
294
|
+
return {
|
|
295
|
+
subscribe(runId, onEvent) {
|
|
296
|
+
const unsubscribes = [];
|
|
297
|
+
unsubscribes.push(
|
|
298
|
+
durably.on("run:start", (event) => {
|
|
299
|
+
if (event.runId !== runId) return;
|
|
300
|
+
onEvent({ type: "run:start" });
|
|
301
|
+
})
|
|
302
|
+
);
|
|
303
|
+
unsubscribes.push(
|
|
304
|
+
durably.on("run:complete", (event) => {
|
|
305
|
+
if (event.runId !== runId) return;
|
|
306
|
+
onEvent({ type: "run:complete", output: event.output });
|
|
307
|
+
})
|
|
308
|
+
);
|
|
309
|
+
unsubscribes.push(
|
|
310
|
+
durably.on("run:fail", (event) => {
|
|
311
|
+
if (event.runId !== runId) return;
|
|
312
|
+
onEvent({ type: "run:fail", error: event.error });
|
|
313
|
+
})
|
|
314
|
+
);
|
|
315
|
+
unsubscribes.push(
|
|
316
|
+
durably.on("run:cancel", (event) => {
|
|
317
|
+
if (event.runId !== runId) return;
|
|
318
|
+
onEvent({ type: "run:cancel" });
|
|
319
|
+
})
|
|
320
|
+
);
|
|
321
|
+
unsubscribes.push(
|
|
322
|
+
durably.on("run:progress", (event) => {
|
|
323
|
+
if (event.runId !== runId) return;
|
|
324
|
+
onEvent({ type: "run:progress", progress: event.progress });
|
|
325
|
+
})
|
|
326
|
+
);
|
|
327
|
+
unsubscribes.push(
|
|
328
|
+
durably.on("log:write", (event) => {
|
|
329
|
+
if (event.runId !== runId) return;
|
|
330
|
+
onEvent({
|
|
331
|
+
type: "log:write",
|
|
332
|
+
runId: event.runId,
|
|
333
|
+
stepName: event.stepName,
|
|
334
|
+
level: event.level,
|
|
335
|
+
message: event.message,
|
|
336
|
+
data: event.data
|
|
337
|
+
});
|
|
338
|
+
})
|
|
339
|
+
);
|
|
340
|
+
return () => {
|
|
341
|
+
for (const unsubscribe of unsubscribes) {
|
|
342
|
+
unsubscribe();
|
|
343
|
+
}
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// src/hooks/use-run-subscription.ts
|
|
350
|
+
function useRunSubscription(durably, runId, options) {
|
|
351
|
+
const subscriber = useMemo2(
|
|
352
|
+
() => durably ? createDurablyEventSubscriber(durably) : null,
|
|
353
|
+
[durably]
|
|
354
|
+
);
|
|
355
|
+
return useSubscription(subscriber, runId, options);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// src/hooks/use-job-logs.ts
|
|
359
|
+
function useJobLogs(options) {
|
|
360
|
+
const { durably } = useDurably();
|
|
361
|
+
const { runId, maxLogs } = options;
|
|
362
|
+
const subscription = useRunSubscription(durably, runId, { maxLogs });
|
|
363
|
+
return {
|
|
364
|
+
logs: subscription.logs,
|
|
365
|
+
clearLogs: subscription.clearLogs
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// src/hooks/use-job-run.ts
|
|
370
|
+
function useJobRun(options) {
|
|
371
|
+
const { durably } = useDurably();
|
|
372
|
+
const { runId } = options;
|
|
373
|
+
const subscription = useRunSubscription(durably, runId);
|
|
374
|
+
const effectiveStatus = subscription.status ?? (runId ? "pending" : null);
|
|
375
|
+
return {
|
|
376
|
+
status: effectiveStatus,
|
|
377
|
+
output: subscription.output,
|
|
378
|
+
error: subscription.error,
|
|
379
|
+
logs: subscription.logs,
|
|
380
|
+
progress: subscription.progress,
|
|
381
|
+
isRunning: effectiveStatus === "running",
|
|
382
|
+
isPending: effectiveStatus === "pending",
|
|
383
|
+
isCompleted: effectiveStatus === "completed",
|
|
384
|
+
isFailed: effectiveStatus === "failed",
|
|
385
|
+
isCancelled: effectiveStatus === "cancelled"
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// src/hooks/use-runs.ts
|
|
390
|
+
import { useCallback as useCallback3, useEffect as useEffect4, useMemo as useMemo3, useState } from "react";
|
|
391
|
+
function useRuns(jobDefinitionOrOptions, optionsArg) {
|
|
392
|
+
const { durably } = useDurably();
|
|
393
|
+
const isJob = isJobDefinition(jobDefinitionOrOptions);
|
|
394
|
+
const jobName = isJob ? jobDefinitionOrOptions.name : jobDefinitionOrOptions?.jobName;
|
|
395
|
+
const options = isJob ? optionsArg : jobDefinitionOrOptions;
|
|
396
|
+
const pageSize = options?.pageSize ?? 10;
|
|
397
|
+
const realtime = options?.realtime ?? true;
|
|
398
|
+
const status = options?.status;
|
|
399
|
+
const jobNameKey = jobName ? JSON.stringify(jobName) : void 0;
|
|
400
|
+
const stableJobName = useMemo3(
|
|
401
|
+
() => jobNameKey ? JSON.parse(jobNameKey) : void 0,
|
|
402
|
+
[jobNameKey]
|
|
403
|
+
);
|
|
404
|
+
const labelsKey = options?.labels ? JSON.stringify(options.labels) : void 0;
|
|
405
|
+
const labels = useMemo3(
|
|
406
|
+
() => labelsKey ? JSON.parse(labelsKey) : void 0,
|
|
407
|
+
[labelsKey]
|
|
408
|
+
);
|
|
409
|
+
const [runs, setRuns] = useState([]);
|
|
410
|
+
const [page, setPage] = useState(0);
|
|
411
|
+
const [hasMore, setHasMore] = useState(false);
|
|
412
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
413
|
+
const refresh = useCallback3(async () => {
|
|
414
|
+
if (!durably) return;
|
|
415
|
+
setIsLoading(true);
|
|
416
|
+
try {
|
|
417
|
+
const data = await durably.getRuns({
|
|
418
|
+
jobName: stableJobName,
|
|
419
|
+
status,
|
|
420
|
+
labels,
|
|
421
|
+
limit: pageSize + 1,
|
|
422
|
+
offset: page * pageSize
|
|
423
|
+
});
|
|
424
|
+
setHasMore(data.length > pageSize);
|
|
425
|
+
setRuns(data.slice(0, pageSize));
|
|
426
|
+
} finally {
|
|
427
|
+
setIsLoading(false);
|
|
428
|
+
}
|
|
429
|
+
}, [durably, stableJobName, status, labels, pageSize, page]);
|
|
430
|
+
useEffect4(() => {
|
|
431
|
+
if (!durably) return;
|
|
432
|
+
refresh();
|
|
433
|
+
if (!realtime) return;
|
|
434
|
+
const unsubscribes = [
|
|
435
|
+
durably.on("run:trigger", refresh),
|
|
436
|
+
durably.on("run:start", refresh),
|
|
437
|
+
durably.on("run:complete", refresh),
|
|
438
|
+
durably.on("run:fail", refresh),
|
|
439
|
+
durably.on("run:cancel", refresh),
|
|
440
|
+
durably.on("run:delete", refresh),
|
|
441
|
+
durably.on("run:progress", refresh),
|
|
442
|
+
durably.on("step:start", refresh),
|
|
443
|
+
durably.on("step:complete", refresh),
|
|
444
|
+
durably.on("step:fail", refresh),
|
|
445
|
+
durably.on("step:cancel", refresh)
|
|
446
|
+
];
|
|
447
|
+
return () => {
|
|
448
|
+
for (const unsubscribe of unsubscribes) {
|
|
449
|
+
unsubscribe();
|
|
450
|
+
}
|
|
451
|
+
};
|
|
452
|
+
}, [durably, refresh, realtime]);
|
|
453
|
+
const nextPage = useCallback3(() => {
|
|
454
|
+
if (hasMore) {
|
|
455
|
+
setPage((p) => p + 1);
|
|
456
|
+
}
|
|
457
|
+
}, [hasMore]);
|
|
458
|
+
const prevPage = useCallback3(() => {
|
|
459
|
+
setPage((p) => Math.max(0, p - 1));
|
|
460
|
+
}, []);
|
|
461
|
+
const goToPage = useCallback3((newPage) => {
|
|
462
|
+
setPage(Math.max(0, newPage));
|
|
463
|
+
}, []);
|
|
464
|
+
return {
|
|
465
|
+
runs,
|
|
466
|
+
page,
|
|
467
|
+
hasMore,
|
|
468
|
+
isLoading,
|
|
469
|
+
nextPage,
|
|
470
|
+
prevPage,
|
|
471
|
+
goToPage,
|
|
472
|
+
refresh
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
export {
|
|
476
|
+
DurablyProvider,
|
|
477
|
+
useDurably,
|
|
478
|
+
useJob,
|
|
479
|
+
useJobLogs,
|
|
480
|
+
useJobRun,
|
|
481
|
+
useRuns
|
|
482
|
+
};
|
|
483
|
+
//# sourceMappingURL=spa.js.map
|
package/dist/spa.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/context.tsx","../src/hooks/use-job.ts","../src/hooks/use-auto-resume.ts","../src/hooks/use-job-subscription.ts","../src/hooks/use-run-subscription.ts","../src/shared/durably-event-subscriber.ts","../src/hooks/use-job-logs.ts","../src/hooks/use-job-run.ts","../src/hooks/use-runs.ts"],"sourcesContent":["import type { Durably } from '@coji/durably'\nimport { Suspense, createContext, use, useContext, type ReactNode } from 'react'\n\n// biome-ignore lint/suspicious/noExplicitAny: Durably context accepts any job/label configuration\ntype AnyDurably = Durably<any, any>\n\ninterface DurablyContextValue {\n durably: AnyDurably\n}\n\nconst DurablyContext = createContext<DurablyContextValue | null>(null)\n\nexport interface DurablyProviderProps {\n /**\n * Durably instance or Promise that resolves to one.\n * The instance should already be initialized via `await durably.init()`.\n *\n * When passing a Promise, wrap the provider with Suspense or use the fallback prop.\n *\n * @example\n * // With Suspense (recommended)\n * <Suspense fallback={<Loading />}>\n * <DurablyProvider durably={durablyPromise}>\n * <App />\n * </DurablyProvider>\n * </Suspense>\n *\n * @example\n * // With fallback prop\n * <DurablyProvider durably={durablyPromise} fallback={<Loading />}>\n * <App />\n * </DurablyProvider>\n */\n durably: AnyDurably | Promise<AnyDurably>\n /**\n * Fallback to show while waiting for the Durably Promise to resolve.\n * This wraps the provider content in a Suspense boundary automatically.\n */\n fallback?: ReactNode\n children: ReactNode\n}\n\n/**\n * Internal component that uses the `use()` hook to resolve the Promise\n */\nfunction DurablyProviderInner({\n durably: durablyOrPromise,\n children,\n}: Omit<DurablyProviderProps, 'fallback'>) {\n const durably =\n durablyOrPromise instanceof Promise\n ? use(durablyOrPromise)\n : durablyOrPromise\n\n return (\n <DurablyContext.Provider value={{ durably }}>\n {children}\n </DurablyContext.Provider>\n )\n}\n\nexport function DurablyProvider({\n durably,\n fallback,\n children,\n}: DurablyProviderProps) {\n const inner = (\n <DurablyProviderInner durably={durably}>{children}</DurablyProviderInner>\n )\n\n if (fallback !== undefined) {\n return <Suspense fallback={fallback}>{inner}</Suspense>\n }\n\n return inner\n}\n\nexport function useDurably(): DurablyContextValue {\n const context = useContext(DurablyContext)\n if (!context) {\n throw new Error('useDurably must be used within a DurablyProvider')\n }\n return context\n}\n","import type { JobDefinition, JobHandle } from '@coji/durably'\nimport { useCallback, useEffect, useMemo, useRef } from 'react'\nimport { useDurably } from '../context'\nimport type { LogEntry, Progress, RunStatus } from '../types'\nimport { useAutoResume } from './use-auto-resume'\nimport { useJobSubscription } from './use-job-subscription'\n\nexport interface UseJobOptions {\n /**\n * Initial Run ID to subscribe to (for reconnection scenarios)\n */\n initialRunId?: string\n /**\n * Automatically resume tracking any pending or running job on initialization.\n * If a pending or running run exists for this job, the hook will subscribe to it.\n * @default true\n */\n autoResume?: boolean\n /**\n * Automatically switch to tracking the latest running job when a new run starts.\n * When true, the hook will update to track any new run for this job as soon as it starts running.\n * When false, the hook will only track the run that was triggered or explicitly set.\n * @default true\n */\n followLatest?: boolean\n}\n\nexport interface UseJobResult<TInput, TOutput> {\n /**\n * Trigger the job with the given input\n */\n trigger: (input: TInput) => Promise<{ runId: string }>\n /**\n * Trigger and wait for completion\n */\n triggerAndWait: (input: TInput) => Promise<{ runId: string; output: TOutput }>\n /**\n * Current run status\n */\n status: RunStatus | null\n /**\n * Output from completed run\n */\n output: TOutput | null\n /**\n * Error message from failed run\n */\n error: string | null\n /**\n * Logs collected during execution\n */\n logs: LogEntry[]\n /**\n * Current progress\n */\n progress: Progress | null\n /**\n * Whether a run is currently running\n */\n isRunning: boolean\n /**\n * Whether a run is pending\n */\n isPending: boolean\n /**\n * Whether the run completed successfully\n */\n isCompleted: boolean\n /**\n * Whether the run failed\n */\n isFailed: boolean\n /**\n * Whether the run was cancelled\n */\n isCancelled: boolean\n /**\n * Current run ID\n */\n currentRunId: string | null\n /**\n * Reset all state\n */\n reset: () => void\n}\n\nexport function useJob<\n TName extends string,\n TInput extends Record<string, unknown>,\n // biome-ignore lint/suspicious/noConfusingVoidType: TOutput can be void for jobs without return value\n TOutput extends Record<string, unknown> | void,\n>(\n jobDefinition: JobDefinition<TName, TInput, TOutput>,\n options?: UseJobOptions,\n): UseJobResult<TInput, TOutput> {\n const { durably } = useDurably()\n\n const jobHandleRef = useRef<JobHandle<TName, TInput, TOutput> | null>(null)\n\n // Register job\n useEffect(() => {\n if (!durably) return\n\n const d = durably.register({\n _job: jobDefinition,\n })\n jobHandleRef.current = d.jobs._job\n }, [durably, jobDefinition])\n\n // Use the extracted job subscription hook\n const subscription = useJobSubscription<TOutput>(\n durably,\n jobDefinition.name,\n {\n followLatest: options?.followLatest,\n },\n )\n\n // Auto-resume callbacks - stable reference\n const autoResumeCallbacks = useMemo(\n () => ({\n onRunFound: (runId: string, _status: RunStatus) => {\n subscription.setCurrentRunId(runId)\n },\n }),\n [subscription.setCurrentRunId],\n )\n\n // Use the extracted auto-resume hook\n useAutoResume(\n jobHandleRef.current,\n {\n enabled: options?.autoResume,\n initialRunId: options?.initialRunId,\n },\n autoResumeCallbacks,\n )\n\n // Handle initialRunId - set it to start tracking\n useEffect(() => {\n if (!durably || !options?.initialRunId) return\n\n subscription.setCurrentRunId(options.initialRunId)\n }, [durably, options?.initialRunId, subscription.setCurrentRunId])\n\n const trigger = useCallback(\n async (input: TInput): Promise<{ runId: string }> => {\n const jobHandle = jobHandleRef.current\n if (!jobHandle) {\n throw new Error('Job not ready')\n }\n\n // Reset state before triggering\n subscription.reset()\n\n const run = await jobHandle.trigger(input)\n subscription.setCurrentRunId(run.id)\n\n return { runId: run.id }\n },\n [subscription],\n )\n\n const triggerAndWait = useCallback(\n async (input: TInput): Promise<{ runId: string; output: TOutput }> => {\n const jobHandle = jobHandleRef.current\n if (!jobHandle || !durably) {\n throw new Error('Job not ready')\n }\n\n // Reset state before triggering\n subscription.reset()\n\n const run = await jobHandle.trigger(input)\n subscription.setCurrentRunId(run.id)\n\n // Wait for completion by polling\n return new Promise((resolve, reject) => {\n const checkCompletion = async () => {\n const updatedRun = await jobHandle.getRun(run.id)\n if (!updatedRun) {\n reject(new Error('Run not found'))\n return\n }\n\n if (updatedRun.status === 'completed') {\n resolve({ runId: run.id, output: updatedRun.output as TOutput })\n } else if (updatedRun.status === 'failed') {\n reject(new Error(updatedRun.error ?? 'Job failed'))\n } else if (updatedRun.status === 'cancelled') {\n reject(new Error('Job cancelled'))\n } else {\n // Still running, check again\n setTimeout(checkCompletion, 50)\n }\n }\n checkCompletion()\n })\n },\n [durably, subscription],\n )\n\n return {\n trigger,\n triggerAndWait,\n status: subscription.status,\n output: subscription.output,\n error: subscription.error,\n logs: subscription.logs,\n progress: subscription.progress,\n isRunning: subscription.status === 'running',\n isPending: subscription.status === 'pending',\n isCompleted: subscription.status === 'completed',\n isFailed: subscription.status === 'failed',\n isCancelled: subscription.status === 'cancelled',\n currentRunId: subscription.currentRunId,\n reset: subscription.reset,\n }\n}\n","import type { JobHandle } from '@coji/durably'\nimport { useEffect } from 'react'\nimport type { RunStatus } from '../types'\n\nexport interface UseAutoResumeOptions {\n /**\n * Whether to automatically resume tracking pending/running runs\n * @default true\n */\n enabled?: boolean\n /**\n * Skip auto-resume if an initial run ID is provided\n */\n skipIfInitialRunId?: boolean\n /**\n * Initial run ID (if provided, auto-resume is skipped)\n */\n initialRunId?: string\n}\n\nexport interface UseAutoResumeCallbacks {\n /**\n * Called when a run is found to resume\n */\n onRunFound: (runId: string, status: RunStatus) => void\n}\n\n/**\n * Hook that automatically finds and resumes tracking of pending/running runs.\n * Extracted from useJob to separate the auto-resume concern.\n */\nexport function useAutoResume<\n TName extends string,\n TInput extends Record<string, unknown>,\n TOutput,\n>(\n jobHandle: JobHandle<TName, TInput, TOutput> | null,\n options: UseAutoResumeOptions,\n callbacks: UseAutoResumeCallbacks,\n): void {\n const enabled = options.enabled !== false\n const skipIfInitialRunId = options.skipIfInitialRunId !== false\n const initialRunId = options.initialRunId\n\n useEffect(() => {\n if (!jobHandle) return\n if (!enabled) return\n if (skipIfInitialRunId && initialRunId) return\n\n let cancelled = false\n\n const findActiveRun = async () => {\n // First check for running runs\n const runningRuns = await jobHandle.getRuns({ status: 'running' })\n if (cancelled) return\n\n if (runningRuns.length > 0) {\n const run = runningRuns[0]\n callbacks.onRunFound(run.id, run.status as RunStatus)\n return\n }\n\n // Then check for pending runs\n const pendingRuns = await jobHandle.getRuns({ status: 'pending' })\n if (cancelled) return\n\n if (pendingRuns.length > 0) {\n const run = pendingRuns[0]\n callbacks.onRunFound(run.id, run.status as RunStatus)\n }\n }\n\n findActiveRun()\n\n return () => {\n cancelled = true\n }\n }, [jobHandle, enabled, skipIfInitialRunId, initialRunId, callbacks])\n}\n","import type { Durably } from '@coji/durably'\nimport { useCallback, useEffect, useReducer, useRef } from 'react'\nimport {\n initialSubscriptionState,\n subscriptionReducer,\n type SubscriptionAction,\n} from '../shared/subscription-reducer'\nimport type { SubscriptionState } from '../types'\n\nexport interface UseJobSubscriptionOptions {\n /**\n * Automatically switch to tracking the latest running job when a new run starts.\n * @default true\n */\n followLatest?: boolean\n /**\n * Maximum number of logs to keep (0 = unlimited)\n */\n maxLogs?: number\n}\n\nexport interface UseJobSubscriptionResult<\n TOutput = unknown,\n> extends SubscriptionState<TOutput> {\n /**\n * Current run ID being tracked\n */\n currentRunId: string | null\n /**\n * Set the current run ID to track\n */\n setCurrentRunId: (runId: string | null) => void\n /**\n * Clear all logs\n */\n clearLogs: () => void\n /**\n * Reset all state including currentRunId\n */\n reset: () => void\n}\n\n// Extended state for job subscription (includes currentRunId)\ninterface JobSubscriptionState<\n TOutput = unknown,\n> extends SubscriptionState<TOutput> {\n currentRunId: string | null\n}\n\n// Extended actions for job subscription\ntype JobSubscriptionAction<TOutput = unknown> =\n | SubscriptionAction<TOutput>\n | { type: 'set_run_id'; runId: string | null }\n | {\n type: 'switch_to_run'\n runId: string\n }\n\nfunction jobSubscriptionReducer<TOutput = unknown>(\n state: JobSubscriptionState<TOutput>,\n action: JobSubscriptionAction<TOutput>,\n): JobSubscriptionState<TOutput> {\n switch (action.type) {\n case 'set_run_id':\n return { ...state, currentRunId: action.runId }\n\n case 'switch_to_run':\n // Switch to a new run, resetting state\n return {\n ...initialSubscriptionState,\n currentRunId: action.runId,\n status: 'running',\n } as JobSubscriptionState<TOutput>\n\n case 'reset':\n return {\n ...(initialSubscriptionState as SubscriptionState<TOutput>),\n currentRunId: null,\n }\n\n default:\n // Delegate to base subscription reducer\n return {\n ...subscriptionReducer(state, action as SubscriptionAction<TOutput>),\n currentRunId: state.currentRunId,\n }\n }\n}\n\n/**\n * Hook for subscribing to job events with followLatest support.\n * This is a specialized version of useSubscription for job-level tracking.\n */\nexport function useJobSubscription<TOutput = unknown>(\n durably: Durably | null,\n jobName: string,\n options?: UseJobSubscriptionOptions,\n): UseJobSubscriptionResult<TOutput> {\n const initialState: JobSubscriptionState<TOutput> = {\n ...(initialSubscriptionState as SubscriptionState<TOutput>),\n currentRunId: null,\n }\n\n const [state, dispatch] = useReducer(\n jobSubscriptionReducer<TOutput>,\n initialState,\n )\n\n const currentRunIdRef = useRef<string | null>(null)\n currentRunIdRef.current = state.currentRunId\n\n const followLatest = options?.followLatest !== false\n const maxLogs = options?.maxLogs ?? 0\n\n useEffect(() => {\n if (!durably) return\n\n const unsubscribes: (() => void)[] = []\n\n unsubscribes.push(\n durably.on('run:start', (event) => {\n if (event.jobName !== jobName) return\n\n if (followLatest) {\n // Switch to tracking the new run\n dispatch({ type: 'switch_to_run', runId: event.runId })\n currentRunIdRef.current = event.runId\n } else {\n // Only update if this is our current run\n if (event.runId !== currentRunIdRef.current) return\n dispatch({ type: 'run:start' })\n }\n }),\n )\n\n unsubscribes.push(\n durably.on('run:complete', (event) => {\n if (event.runId !== currentRunIdRef.current) return\n dispatch({ type: 'run:complete', output: event.output as TOutput })\n }),\n )\n\n unsubscribes.push(\n durably.on('run:fail', (event) => {\n if (event.runId !== currentRunIdRef.current) return\n dispatch({ type: 'run:fail', error: event.error })\n }),\n )\n\n unsubscribes.push(\n durably.on('run:cancel', (event) => {\n if (event.runId !== currentRunIdRef.current) return\n dispatch({ type: 'run:cancel' })\n }),\n )\n\n unsubscribes.push(\n durably.on('run:progress', (event) => {\n if (event.runId !== currentRunIdRef.current) return\n dispatch({ type: 'run:progress', progress: event.progress })\n }),\n )\n\n unsubscribes.push(\n durably.on('log:write', (event) => {\n if (event.runId !== currentRunIdRef.current) return\n dispatch({\n type: 'log:write',\n runId: event.runId,\n stepName: event.stepName,\n level: event.level,\n message: event.message,\n data: event.data,\n maxLogs,\n })\n }),\n )\n\n return () => {\n for (const unsubscribe of unsubscribes) {\n unsubscribe()\n }\n }\n }, [durably, jobName, followLatest, maxLogs])\n\n const setCurrentRunId = useCallback((runId: string | null) => {\n dispatch({ type: 'set_run_id', runId })\n currentRunIdRef.current = runId\n }, [])\n\n const clearLogs = useCallback(() => {\n dispatch({ type: 'clear_logs' })\n }, [])\n\n const reset = useCallback(() => {\n dispatch({ type: 'reset' })\n currentRunIdRef.current = null\n }, [])\n\n return {\n ...state,\n setCurrentRunId,\n clearLogs,\n reset,\n }\n}\n","import type { Durably } from '@coji/durably'\nimport { useMemo } from 'react'\nimport { createDurablyEventSubscriber } from '../shared/durably-event-subscriber'\nimport {\n useSubscription,\n type UseSubscriptionOptions,\n type UseSubscriptionResult,\n} from '../shared/use-subscription'\nimport type { SubscriptionState } from '../types'\n\n/** @deprecated Use SubscriptionState from '../types' instead */\nexport type RunSubscriptionState<TOutput = unknown> = SubscriptionState<TOutput>\n\n/** @deprecated Use UseSubscriptionOptions from '../shared/use-subscription' instead */\nexport type UseRunSubscriptionOptions = UseSubscriptionOptions\n\n/** @deprecated Use UseSubscriptionResult from '../shared/use-subscription' instead */\nexport type UseRunSubscriptionResult<TOutput = unknown> =\n UseSubscriptionResult<TOutput>\n\n/**\n * Internal hook for subscribing to run events via Durably.on().\n * Shared by useJob, useJobRun, and useJobLogs.\n *\n * @deprecated Consider using useSubscription with createDurablyEventSubscriber directly.\n */\nexport function useRunSubscription<TOutput = unknown>(\n durably: Durably | null,\n runId: string | null,\n options?: UseRunSubscriptionOptions,\n): UseRunSubscriptionResult<TOutput> {\n const subscriber = useMemo(\n () => (durably ? createDurablyEventSubscriber(durably) : null),\n [durably],\n )\n\n return useSubscription<TOutput>(subscriber, runId, options)\n}\n","import type { Durably } from '@coji/durably'\nimport type { EventSubscriber, SubscriptionEvent } from './event-subscriber'\n\n/**\n * EventSubscriber implementation using Durably.on() for direct subscriptions.\n * Used in browser environments where Durably instance is available.\n */\nexport function createDurablyEventSubscriber(\n durably: Durably,\n): EventSubscriber {\n return {\n subscribe<TOutput = unknown>(\n runId: string,\n onEvent: (event: SubscriptionEvent<TOutput>) => void,\n ): () => void {\n const unsubscribes: (() => void)[] = []\n\n unsubscribes.push(\n durably.on('run:start', (event) => {\n if (event.runId !== runId) return\n onEvent({ type: 'run:start' })\n }),\n )\n\n unsubscribes.push(\n durably.on('run:complete', (event) => {\n if (event.runId !== runId) return\n onEvent({ type: 'run:complete', output: event.output as TOutput })\n }),\n )\n\n unsubscribes.push(\n durably.on('run:fail', (event) => {\n if (event.runId !== runId) return\n onEvent({ type: 'run:fail', error: event.error })\n }),\n )\n\n unsubscribes.push(\n durably.on('run:cancel', (event) => {\n if (event.runId !== runId) return\n onEvent({ type: 'run:cancel' })\n }),\n )\n\n unsubscribes.push(\n durably.on('run:progress', (event) => {\n if (event.runId !== runId) return\n onEvent({ type: 'run:progress', progress: event.progress })\n }),\n )\n\n unsubscribes.push(\n durably.on('log:write', (event) => {\n if (event.runId !== runId) return\n onEvent({\n type: 'log:write',\n runId: event.runId,\n stepName: event.stepName,\n level: event.level,\n message: event.message,\n data: event.data,\n })\n }),\n )\n\n return () => {\n for (const unsubscribe of unsubscribes) {\n unsubscribe()\n }\n }\n },\n }\n}\n","import { useDurably } from '../context'\nimport type { LogEntry } from '../types'\nimport { useRunSubscription } from './use-run-subscription'\n\nexport interface UseJobLogsOptions {\n /**\n * The run ID to subscribe to logs for\n */\n runId: string | null\n /**\n * Maximum number of logs to keep (default: unlimited)\n */\n maxLogs?: number\n}\n\nexport interface UseJobLogsResult {\n /**\n * Logs collected during execution\n */\n logs: LogEntry[]\n /**\n * Clear all logs\n */\n clearLogs: () => void\n}\n\n/**\n * Hook for subscribing to logs from a run.\n * Use this when you only need logs, not full run status.\n */\nexport function useJobLogs(options: UseJobLogsOptions): UseJobLogsResult {\n const { durably } = useDurably()\n const { runId, maxLogs } = options\n\n const subscription = useRunSubscription(durably, runId, { maxLogs })\n\n return {\n logs: subscription.logs,\n clearLogs: subscription.clearLogs,\n }\n}\n","import { useDurably } from '../context'\nimport type { LogEntry, Progress, RunStatus } from '../types'\nimport { useRunSubscription } from './use-run-subscription'\n\n// Note: Unlike UseJobRunClientOptions (client mode), this interface intentionally\n// omits onStart/onComplete/onFail callbacks. In browser mode, use durably.on()\n// directly for event callbacks.\nexport interface UseJobRunOptions {\n /**\n * The run ID to subscribe to\n */\n runId: string | null\n}\n\nexport interface UseJobRunResult<TOutput = unknown> {\n /**\n * Current run status\n */\n status: RunStatus | null\n /**\n * Output from completed run\n */\n output: TOutput | null\n /**\n * Error message from failed run\n */\n error: string | null\n /**\n * Logs collected during execution\n */\n logs: LogEntry[]\n /**\n * Current progress\n */\n progress: Progress | null\n /**\n * Whether a run is currently running\n */\n isRunning: boolean\n /**\n * Whether a run is pending\n */\n isPending: boolean\n /**\n * Whether the run completed successfully\n */\n isCompleted: boolean\n /**\n * Whether the run failed\n */\n isFailed: boolean\n /**\n * Whether the run was cancelled\n */\n isCancelled: boolean\n}\n\n/**\n * Hook for subscribing to an existing run by ID.\n * Use this when you have a runId and want to track its status.\n */\nexport function useJobRun<TOutput = unknown>(\n options: UseJobRunOptions,\n): UseJobRunResult<TOutput> {\n const { durably } = useDurably()\n const { runId } = options\n\n const subscription = useRunSubscription<TOutput>(durably, runId)\n\n // If we have a runId but no status yet, treat as pending\n const effectiveStatus = subscription.status ?? (runId ? 'pending' : null)\n\n return {\n status: effectiveStatus,\n output: subscription.output,\n error: subscription.error,\n logs: subscription.logs,\n progress: subscription.progress,\n isRunning: effectiveStatus === 'running',\n isPending: effectiveStatus === 'pending',\n isCompleted: effectiveStatus === 'completed',\n isFailed: effectiveStatus === 'failed',\n isCancelled: effectiveStatus === 'cancelled',\n }\n}\n","import type { JobDefinition } from '@coji/durably'\nimport { useCallback, useEffect, useMemo, useState } from 'react'\nimport { useDurably } from '../context'\nimport { type TypedRun, isJobDefinition } from '../types'\n\n// Re-export TypedRun for convenience\nexport type { TypedRun } from '../types'\n\nexport interface UseRunsOptions {\n /**\n * Filter by job name(s). Pass a string for one, or an array for multiple.\n */\n jobName?: string | string[]\n /**\n * Filter by status\n */\n status?: 'pending' | 'running' | 'completed' | 'failed' | 'cancelled'\n /**\n * Filter by labels (all specified labels must match)\n */\n labels?: Record<string, string>\n /**\n * Number of runs per page\n * @default 10\n */\n pageSize?: number\n /**\n * Subscribe to real-time updates\n * @default true\n */\n realtime?: boolean\n}\n\n// Note: Unlike UseRunsClientResult (client mode), this interface intentionally\n// omits `error` because browser mode operates on a local SQLite database\n// where network errors don't occur.\nexport interface UseRunsResult<\n TInput extends Record<string, unknown> = Record<string, unknown>,\n TOutput extends Record<string, unknown> | undefined =\n | Record<string, unknown>\n | undefined,\n> {\n /**\n * List of runs for the current page\n */\n runs: TypedRun<TInput, TOutput>[]\n /**\n * Current page (0-indexed)\n */\n page: number\n /**\n * Whether there are more pages\n */\n hasMore: boolean\n /**\n * Whether data is being loaded\n */\n isLoading: boolean\n /**\n * Go to the next page\n */\n nextPage: () => void\n /**\n * Go to the previous page\n */\n prevPage: () => void\n /**\n * Go to a specific page\n */\n goToPage: (page: number) => void\n /**\n * Refresh the current page\n */\n refresh: () => Promise<void>\n}\n\n/**\n * Hook for listing runs with pagination and real-time updates.\n *\n * @example With generic type parameter (dashboard with multiple job types)\n * ```tsx\n * type DashboardRun = TypedRun<ImportInput, ImportOutput> | TypedRun<SyncInput, SyncOutput>\n *\n * function Dashboard() {\n * const { runs } = useRuns<DashboardRun>({ pageSize: 10 })\n * // runs are typed as DashboardRun[]\n * }\n * ```\n *\n * @example With JobDefinition (single job, auto-filters by jobName)\n * ```tsx\n * const myJob = defineJob({ name: 'my-job', ... })\n *\n * function Dashboard() {\n * const { runs } = useRuns(myJob)\n * // runs[0].output is typed!\n * return <div>{runs[0]?.output?.someField}</div>\n * }\n * ```\n *\n * @example With options only (untyped)\n * ```tsx\n * function Dashboard() {\n * const { runs } = useRuns({ pageSize: 20 })\n * // runs[0].output is unknown\n * }\n * ```\n */\n// Overload 1: With generic type parameter\nexport function useRuns<\n TRun extends TypedRun<\n Record<string, unknown>,\n Record<string, unknown> | undefined\n >,\n>(\n options?: UseRunsOptions,\n): UseRunsResult<\n TRun extends TypedRun<infer I, infer _O> ? I : Record<string, unknown>,\n TRun extends TypedRun<infer _I, infer O> ? O : Record<string, unknown>\n>\n\n// Overload 2: With JobDefinition for type inference (auto-filters by jobName)\nexport function useRuns<\n TName extends string,\n TInput extends Record<string, unknown>,\n TOutput extends Record<string, unknown> | undefined,\n>(\n jobDefinition: JobDefinition<TName, TInput, TOutput>,\n options?: Omit<UseRunsOptions, 'jobName'>,\n): UseRunsResult<TInput, TOutput>\n\n// Overload 3: Without type parameter (untyped, backward compatible)\nexport function useRuns(options?: UseRunsOptions): UseRunsResult\n\n// Implementation\nexport function useRuns<\n TName extends string,\n TInput extends Record<string, unknown>,\n TOutput extends Record<string, unknown> | undefined,\n>(\n jobDefinitionOrOptions?:\n | JobDefinition<TName, TInput, TOutput>\n | UseRunsOptions,\n optionsArg?: Omit<UseRunsOptions, 'jobName'>,\n): UseRunsResult<TInput, TOutput> {\n const { durably } = useDurably()\n\n // Determine if first argument is a JobDefinition using type guard\n const isJob = isJobDefinition(jobDefinitionOrOptions)\n\n const jobName = isJob\n ? jobDefinitionOrOptions.name\n : (jobDefinitionOrOptions as UseRunsOptions | undefined)?.jobName\n\n const options = isJob\n ? optionsArg\n : (jobDefinitionOrOptions as UseRunsOptions | undefined)\n\n const pageSize = options?.pageSize ?? 10\n const realtime = options?.realtime ?? true\n const status = options?.status\n\n // Stabilize jobName reference to prevent re-fetch loops with array literals\n const jobNameKey = jobName ? JSON.stringify(jobName) : undefined\n const stableJobName = useMemo(\n () =>\n jobNameKey ? (JSON.parse(jobNameKey) as string | string[]) : undefined,\n [jobNameKey],\n )\n\n // Stabilize labels reference to prevent infinite re-renders\n const labelsKey = options?.labels ? JSON.stringify(options.labels) : undefined\n const labels = useMemo(\n () =>\n labelsKey ? (JSON.parse(labelsKey) as Record<string, string>) : undefined,\n [labelsKey],\n )\n\n const [runs, setRuns] = useState<TypedRun<TInput, TOutput>[]>([])\n const [page, setPage] = useState(0)\n const [hasMore, setHasMore] = useState(false)\n const [isLoading, setIsLoading] = useState(true)\n\n const refresh = useCallback(async () => {\n if (!durably) return\n\n setIsLoading(true)\n try {\n const data = await durably.getRuns({\n jobName: stableJobName,\n status,\n labels,\n limit: pageSize + 1,\n offset: page * pageSize,\n })\n setHasMore(data.length > pageSize)\n setRuns(data.slice(0, pageSize) as TypedRun<TInput, TOutput>[])\n } finally {\n setIsLoading(false)\n }\n }, [durably, stableJobName, status, labels, pageSize, page])\n\n // Initial fetch and subscribe to events\n useEffect(() => {\n if (!durably) return\n\n refresh()\n\n if (!realtime) return\n\n const unsubscribes = [\n durably.on('run:trigger', refresh),\n durably.on('run:start', refresh),\n durably.on('run:complete', refresh),\n durably.on('run:fail', refresh),\n durably.on('run:cancel', refresh),\n durably.on('run:delete', refresh),\n durably.on('run:progress', refresh),\n durably.on('step:start', refresh),\n durably.on('step:complete', refresh),\n durably.on('step:fail', refresh),\n durably.on('step:cancel', refresh),\n ]\n\n return () => {\n for (const unsubscribe of unsubscribes) {\n unsubscribe()\n }\n }\n }, [durably, refresh, realtime])\n\n const nextPage = useCallback(() => {\n if (hasMore) {\n setPage((p) => p + 1)\n }\n }, [hasMore])\n\n const prevPage = useCallback(() => {\n setPage((p) => Math.max(0, p - 1))\n }, [])\n\n const goToPage = useCallback((newPage: number) => {\n setPage(Math.max(0, newPage))\n }, [])\n\n return {\n runs,\n page,\n hasMore,\n isLoading,\n nextPage,\n prevPage,\n goToPage,\n refresh,\n }\n}\n"],"mappings":"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","names":["useCallback","useEffect","useRef","useEffect","useEffect","useRef","useEffect","useCallback","useMemo","useMemo","useCallback","useEffect","useMemo","useMemo","useCallback","useEffect"]}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { JobDefinition, ClientRun, Run } from '@coji/durably';
|
|
2
2
|
|
|
3
3
|
type InferInput<T> = T extends JobDefinition<string, infer TInput, unknown> ? TInput extends Record<string, unknown> ? TInput : Record<string, unknown> : T extends {
|
|
4
4
|
trigger: (input: infer TInput) => unknown;
|
|
@@ -52,10 +52,6 @@ type DurablyEvent = {
|
|
|
52
52
|
runId: string;
|
|
53
53
|
jobName: string;
|
|
54
54
|
input: unknown;
|
|
55
|
-
} | {
|
|
56
|
-
type: 'run:retry';
|
|
57
|
-
runId: string;
|
|
58
|
-
jobName: string;
|
|
59
55
|
} | {
|
|
60
56
|
type: 'run:progress';
|
|
61
57
|
runId: string;
|
|
@@ -109,4 +105,4 @@ type TypedClientRun<TInput extends Record<string, unknown> = Record<string, unkn
|
|
|
109
105
|
output: TOutput | null;
|
|
110
106
|
};
|
|
111
107
|
|
|
112
|
-
export type { DurablyEvent as D, InferInput as I, LogEntry as L, Progress as P, RunStatus as R,
|
|
108
|
+
export type { DurablyEvent as D, InferInput as I, LogEntry as L, Progress as P, RunStatus as R, TypedClientRun as T, InferOutput as a, TypedRun as b };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@coji/durably-react",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"description": "React bindings for Durably - step-oriented resumable batch execution",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -10,9 +10,9 @@
|
|
|
10
10
|
"types": "./dist/index.d.ts",
|
|
11
11
|
"import": "./dist/index.js"
|
|
12
12
|
},
|
|
13
|
-
"./
|
|
14
|
-
"types": "./dist/
|
|
15
|
-
"import": "./dist/
|
|
13
|
+
"./spa": {
|
|
14
|
+
"types": "./dist/spa.d.ts",
|
|
15
|
+
"import": "./dist/spa.js"
|
|
16
16
|
}
|
|
17
17
|
},
|
|
18
18
|
"files": [
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
}
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
|
-
"@biomejs/biome": "^2.4.
|
|
52
|
+
"@biomejs/biome": "^2.4.6",
|
|
53
53
|
"@testing-library/react": "^16.3.2",
|
|
54
54
|
"@types/react": "^19.2.14",
|
|
55
55
|
"@types/react-dom": "^19.2.3",
|
|
@@ -67,7 +67,7 @@
|
|
|
67
67
|
"typescript": "^5.9.3",
|
|
68
68
|
"vitest": "^4.0.18",
|
|
69
69
|
"zod": "^4.3.6",
|
|
70
|
-
"@coji/durably": "0.
|
|
70
|
+
"@coji/durably": "0.12.0"
|
|
71
71
|
},
|
|
72
72
|
"scripts": {
|
|
73
73
|
"build": "tsup",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/types.ts","../src/shared/create-log-entry.ts","../src/shared/subscription-reducer.ts","../src/shared/use-subscription.ts"],"sourcesContent":["// Shared type definitions for @coji/durably-react\n\nimport type { ClientRun, JobDefinition, Run } from '@coji/durably'\n\n// Type inference utilities for extracting Input/Output types from JobDefinition\nexport type InferInput<T> =\n T extends JobDefinition<string, infer TInput, unknown>\n ? TInput extends Record<string, unknown>\n ? TInput\n : Record<string, unknown>\n : T extends { trigger: (input: infer TInput) => unknown }\n ? TInput extends Record<string, unknown>\n ? TInput\n : Record<string, unknown>\n : Record<string, unknown>\n\nexport type InferOutput<T> =\n T extends JobDefinition<string, unknown, infer TOutput>\n ? TOutput extends Record<string, unknown>\n ? TOutput\n : Record<string, unknown>\n : T extends {\n trigger: (input: unknown) => Promise<{ output?: infer TOutput }>\n }\n ? TOutput extends Record<string, unknown>\n ? TOutput\n : Record<string, unknown>\n : Record<string, unknown>\n\nexport type RunStatus =\n | 'pending'\n | 'running'\n | 'completed'\n | 'failed'\n | 'cancelled'\n\nexport interface Progress {\n current: number\n total?: number\n message?: string\n}\n\nexport interface LogEntry {\n id: string\n runId: string\n stepName: string | null\n level: 'info' | 'warn' | 'error'\n message: string\n data: unknown\n timestamp: string\n}\n\n// Shared subscription state (used by both direct and SSE subscriptions)\nexport interface SubscriptionState<TOutput = unknown> {\n status: RunStatus | null\n output: TOutput | null\n error: string | null\n logs: LogEntry[]\n progress: Progress | null\n}\n\n// SSE event types (sent from server).\n// Note: Unlike core DurablyEvent, these omit timestamp/sequence because\n// the SSE handler in server.ts sends only the fields needed by the UI.\nexport type DurablyEvent =\n | { type: 'run:start'; runId: string; jobName: string; input: unknown }\n | {\n type: 'run:complete'\n runId: string\n jobName: string\n output: unknown\n duration: number\n }\n | { type: 'run:fail'; runId: string; jobName: string; error: string }\n | { type: 'run:cancel'; runId: string; jobName: string }\n | { type: 'run:delete'; runId: string; jobName: string }\n | { type: 'run:trigger'; runId: string; jobName: string; input: unknown }\n | { type: 'run:retry'; runId: string; jobName: string }\n | {\n type: 'run:progress'\n runId: string\n jobName: string\n progress: Progress\n }\n | {\n type: 'step:start'\n runId: string\n jobName: string\n stepName: string\n stepIndex: number\n }\n | {\n type: 'step:complete'\n runId: string\n jobName: string\n stepName: string\n stepIndex: number\n output: unknown\n }\n | {\n type: 'step:cancel'\n runId: string\n jobName: string\n stepName: string\n stepIndex: number\n labels: Record<string, string>\n }\n | {\n type: 'log:write'\n runId: string\n jobName: string\n stepName: string | null\n labels: Record<string, string>\n level: 'info' | 'warn' | 'error'\n message: string\n data: unknown\n }\n\n// =============================================================================\n// Typed Run types for useRuns hooks\n// =============================================================================\n\n/**\n * A typed version of Run with generic input/output types.\n * Used by browser hooks (direct durably access).\n */\nexport type TypedRun<\n TInput extends Record<string, unknown> = Record<string, unknown>,\n TOutput extends Record<string, unknown> | undefined =\n | Record<string, unknown>\n | undefined,\n> = Omit<Run, 'input' | 'output'> & {\n input: TInput\n output: TOutput | null\n}\n\n// ClientRun is imported from '@coji/durably' and re-exported for consumers.\nexport type { ClientRun } from '@coji/durably'\n\n/**\n * A typed version of ClientRun with generic input/output types.\n * Used by client hooks (HTTP/SSE connection).\n */\nexport type TypedClientRun<\n TInput extends Record<string, unknown> = Record<string, unknown>,\n TOutput extends Record<string, unknown> | undefined =\n | Record<string, unknown>\n | undefined,\n> = Omit<ClientRun, 'input' | 'output'> & {\n input: TInput\n output: TOutput | null\n}\n\n/**\n * Type guard to check if an object is a JobDefinition.\n * Used to distinguish between JobDefinition and options objects in overloaded functions.\n */\nexport function isJobDefinition<\n TName extends string = string,\n TInput extends Record<string, unknown> = Record<string, unknown>,\n TOutput extends Record<string, unknown> | undefined = undefined,\n>(obj: unknown): obj is JobDefinition<TName, TInput, TOutput> {\n return (\n typeof obj === 'object' &&\n obj !== null &&\n 'name' in obj &&\n 'run' in obj &&\n typeof (obj as { run: unknown }).run === 'function'\n )\n}\n","import type { LogEntry } from '../types'\n\nexport interface CreateLogEntryParams {\n runId: string\n stepName: string | null\n level: 'info' | 'warn' | 'error'\n message: string\n data: unknown\n}\n\n/**\n * Creates a LogEntry with auto-generated id and timestamp.\n * Extracted to eliminate duplication between subscription hooks.\n */\nexport function createLogEntry(params: CreateLogEntryParams): LogEntry {\n return {\n id: crypto.randomUUID(),\n runId: params.runId,\n stepName: params.stepName,\n level: params.level,\n message: params.message,\n data: params.data,\n timestamp: new Date().toISOString(),\n }\n}\n\n/**\n * Appends a log entry to the array, respecting maxLogs limit.\n */\nexport function appendLog(\n logs: LogEntry[],\n newLog: LogEntry,\n maxLogs: number,\n): LogEntry[] {\n const newLogs = [...logs, newLog]\n if (maxLogs > 0 && newLogs.length > maxLogs) {\n return newLogs.slice(-maxLogs)\n }\n return newLogs\n}\n","import type { Progress, SubscriptionState } from '../types'\nimport { appendLog, createLogEntry } from './create-log-entry'\n\n// Action types for subscription state transitions\nexport type SubscriptionAction<TOutput = unknown> =\n | { type: 'run:start' }\n | { type: 'run:complete'; output: TOutput }\n | { type: 'run:fail'; error: string }\n | { type: 'run:cancel' }\n | { type: 'run:retry' }\n | { type: 'run:progress'; progress: Progress }\n | {\n type: 'log:write'\n runId: string\n stepName: string | null\n level: 'info' | 'warn' | 'error'\n message: string\n data: unknown\n maxLogs: number\n }\n | { type: 'reset' }\n | { type: 'clear_logs' }\n | { type: 'connection_error'; error: string }\n\nexport const initialSubscriptionState: SubscriptionState<unknown> = {\n status: null,\n output: null,\n error: null,\n logs: [],\n progress: null,\n}\n\n/**\n * Pure reducer for subscription state transitions.\n * Extracted to eliminate duplication between useRunSubscription and useSSESubscription.\n */\nexport function subscriptionReducer<TOutput = unknown>(\n state: SubscriptionState<TOutput>,\n action: SubscriptionAction<TOutput>,\n): SubscriptionState<TOutput> {\n switch (action.type) {\n case 'run:start':\n return { ...state, status: 'running' }\n\n case 'run:complete':\n return { ...state, status: 'completed', output: action.output }\n\n case 'run:fail':\n return { ...state, status: 'failed', error: action.error }\n\n case 'run:cancel':\n return { ...state, status: 'cancelled' }\n\n case 'run:retry':\n return { ...state, status: 'pending', error: null }\n\n case 'run:progress':\n return { ...state, progress: action.progress }\n\n case 'log:write': {\n const newLog = createLogEntry({\n runId: action.runId,\n stepName: action.stepName,\n level: action.level,\n message: action.message,\n data: action.data,\n })\n return { ...state, logs: appendLog(state.logs, newLog, action.maxLogs) }\n }\n\n case 'reset':\n return initialSubscriptionState as SubscriptionState<TOutput>\n\n case 'clear_logs':\n return { ...state, logs: [] }\n\n case 'connection_error':\n return { ...state, error: action.error }\n\n default:\n return state\n }\n}\n","import { useCallback, useEffect, useReducer, useRef } from 'react'\nimport type { SubscriptionState } from '../types'\nimport type { EventSubscriber } from './event-subscriber'\nimport {\n initialSubscriptionState,\n subscriptionReducer,\n} from './subscription-reducer'\n\nexport interface UseSubscriptionOptions {\n /**\n * Maximum number of logs to keep (0 = unlimited)\n */\n maxLogs?: number\n}\n\nexport interface UseSubscriptionResult<\n TOutput = unknown,\n> extends SubscriptionState<TOutput> {\n /**\n * Clear all logs\n */\n clearLogs: () => void\n /**\n * Reset all state\n */\n reset: () => void\n}\n\n/**\n * Core subscription hook that works with any EventSubscriber implementation.\n * This unifies the subscription logic between Durably.on and SSE.\n */\nexport function useSubscription<TOutput = unknown>(\n subscriber: EventSubscriber | null,\n runId: string | null,\n options?: UseSubscriptionOptions,\n): UseSubscriptionResult<TOutput> {\n const [state, dispatch] = useReducer(\n subscriptionReducer<TOutput>,\n initialSubscriptionState as SubscriptionState<TOutput>,\n )\n\n const runIdRef = useRef<string | null>(runId)\n const prevRunIdRef = useRef<string | null>(null)\n\n const maxLogs = options?.maxLogs ?? 0\n\n // Reset state when runId changes\n if (prevRunIdRef.current !== runId) {\n prevRunIdRef.current = runId\n if (runIdRef.current !== runId) {\n dispatch({ type: 'reset' })\n }\n }\n runIdRef.current = runId\n\n useEffect(() => {\n if (!subscriber || !runId) return\n\n const unsubscribe = subscriber.subscribe<TOutput>(runId, (event) => {\n // Verify runId hasn't changed during async operation\n if (runIdRef.current !== runId) return\n\n switch (event.type) {\n case 'run:start':\n case 'run:cancel':\n case 'run:retry':\n dispatch({ type: event.type })\n break\n case 'run:complete':\n dispatch({ type: 'run:complete', output: event.output })\n break\n case 'run:fail':\n dispatch({ type: 'run:fail', error: event.error })\n break\n case 'run:progress':\n dispatch({ type: 'run:progress', progress: event.progress })\n break\n case 'log:write':\n dispatch({\n type: 'log:write',\n runId: event.runId,\n stepName: event.stepName,\n level: event.level,\n message: event.message,\n data: event.data,\n maxLogs,\n })\n break\n case 'connection_error':\n dispatch({ type: 'connection_error', error: event.error })\n break\n }\n })\n\n return unsubscribe\n }, [subscriber, runId, maxLogs])\n\n const clearLogs = useCallback(() => {\n dispatch({ type: 'clear_logs' })\n }, [])\n\n const reset = useCallback(() => {\n dispatch({ type: 'reset' })\n }, [])\n\n return {\n ...state,\n clearLogs,\n reset,\n }\n}\n"],"mappings":"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","names":[]}
|