@coji/durably-react 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,3 +1,10 @@
1
+ import {
2
+ initialSubscriptionState,
3
+ isJobDefinition,
4
+ subscriptionReducer,
5
+ useSubscription
6
+ } from "./chunk-42AVE35N.js";
7
+
1
8
  // src/context.tsx
2
9
  import { Suspense, createContext, use, useContext } from "react";
3
10
  import { jsx } from "react/jsx-runtime";
@@ -29,176 +36,222 @@ function useDurably() {
29
36
  }
30
37
 
31
38
  // src/hooks/use-job.ts
32
- import { useCallback, useEffect, useRef, useState } from "react";
33
- function useJob(jobDefinition, options) {
34
- const { durably } = useDurably();
35
- const [status, setStatus] = useState(null);
36
- const [output, setOutput] = useState(null);
37
- const [error, setError] = useState(null);
38
- const [logs, setLogs] = useState([]);
39
- const [progress, setProgress] = useState(null);
40
- const [currentRunId, setCurrentRunId] = useState(
41
- options?.initialRunId ?? null
42
- );
43
- const jobHandleRef = useRef(null);
44
- const currentRunIdRef = useRef(currentRunId);
45
- currentRunIdRef.current = currentRunId;
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;
46
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(() => {
47
112
  if (!durably) return;
48
- const d = durably.register({
49
- _job: jobDefinition
50
- });
51
- const jobHandle = d.jobs._job;
52
- jobHandleRef.current = jobHandle;
53
113
  const unsubscribes = [];
54
114
  unsubscribes.push(
55
115
  durably.on("run:start", (event) => {
56
- if (event.jobName !== jobDefinition.name) return;
57
- if (options?.followLatest === false) {
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 {
58
121
  if (event.runId !== currentRunIdRef.current) return;
59
- setStatus("running");
60
- return;
122
+ dispatch({ type: "run:start" });
61
123
  }
62
- setCurrentRunId(event.runId);
63
- currentRunIdRef.current = event.runId;
64
- setStatus("running");
65
- setOutput(null);
66
- setError(null);
67
- setLogs([]);
68
- setProgress(null);
69
124
  })
70
125
  );
71
126
  unsubscribes.push(
72
127
  durably.on("run:complete", (event) => {
73
128
  if (event.runId !== currentRunIdRef.current) return;
74
- setStatus("completed");
75
- setOutput(event.output);
129
+ dispatch({ type: "run:complete", output: event.output });
76
130
  })
77
131
  );
78
132
  unsubscribes.push(
79
133
  durably.on("run:fail", (event) => {
80
134
  if (event.runId !== currentRunIdRef.current) return;
81
- setStatus("failed");
82
- setError(event.error);
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" });
83
148
  })
84
149
  );
85
150
  unsubscribes.push(
86
151
  durably.on("run:progress", (event) => {
87
152
  if (event.runId !== currentRunIdRef.current) return;
88
- setProgress(event.progress);
153
+ dispatch({ type: "run:progress", progress: event.progress });
89
154
  })
90
155
  );
91
156
  unsubscribes.push(
92
157
  durably.on("log:write", (event) => {
93
158
  if (event.runId !== currentRunIdRef.current) return;
94
- setLogs((prev) => [
95
- ...prev,
96
- {
97
- id: crypto.randomUUID(),
98
- runId: event.runId,
99
- stepName: event.stepName,
100
- level: event.level,
101
- message: event.message,
102
- data: event.data,
103
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
104
- }
105
- ]);
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
+ });
106
168
  })
107
169
  );
108
- if (options?.initialRunId && currentRunIdRef.current) {
109
- jobHandle.getRun(currentRunIdRef.current).then((run) => {
110
- if (run) {
111
- setStatus(run.status);
112
- if (run.status === "completed" && run.output) {
113
- setOutput(run.output);
114
- }
115
- if (run.status === "failed" && run.error) {
116
- setError(run.error);
117
- }
118
- }
119
- });
120
- }
121
- if (options?.autoResume !== false && !options?.initialRunId) {
122
- ;
123
- (async () => {
124
- const runningRuns = await jobHandle.getRuns({ status: "running" });
125
- if (runningRuns.length > 0) {
126
- const run = runningRuns[0];
127
- setCurrentRunId(run.id);
128
- currentRunIdRef.current = run.id;
129
- setStatus(run.status);
130
- return;
131
- }
132
- const pendingRuns = await jobHandle.getRuns({ status: "pending" });
133
- if (pendingRuns.length > 0) {
134
- const run = pendingRuns[0];
135
- setCurrentRunId(run.id);
136
- currentRunIdRef.current = run.id;
137
- setStatus(run.status);
138
- }
139
- })();
140
- }
141
170
  return () => {
142
171
  for (const unsubscribe of unsubscribes) {
143
172
  unsubscribe();
144
173
  }
145
174
  };
146
- }, [
175
+ }, [durably, jobName, followLatest, maxLogs]);
176
+ const setCurrentRunId = useCallback((runId) => {
177
+ dispatch({ type: "set_run_id", runId });
178
+ currentRunIdRef.current = runId;
179
+ }, []);
180
+ const clearLogs = useCallback(() => {
181
+ dispatch({ type: "clear_logs" });
182
+ }, []);
183
+ const reset = useCallback(() => {
184
+ dispatch({ type: "reset" });
185
+ currentRunIdRef.current = null;
186
+ }, []);
187
+ return {
188
+ ...state,
189
+ setCurrentRunId,
190
+ clearLogs,
191
+ reset
192
+ };
193
+ }
194
+
195
+ // src/hooks/use-job.ts
196
+ function useJob(jobDefinition, options) {
197
+ const { durably } = useDurably();
198
+ const jobHandleRef = useRef2(null);
199
+ useEffect3(() => {
200
+ if (!durably) return;
201
+ const d = durably.register({
202
+ _job: jobDefinition
203
+ });
204
+ jobHandleRef.current = d.jobs._job;
205
+ }, [durably, jobDefinition]);
206
+ const subscription = useJobSubscription(
147
207
  durably,
148
- jobDefinition,
149
- options?.initialRunId,
150
- options?.autoResume,
151
- options?.followLatest
152
- ]);
153
- useEffect(() => {
154
- if (!durably || !currentRunId) return;
155
- const jobHandle = jobHandleRef.current;
156
- if (jobHandle && options?.initialRunId) {
157
- jobHandle.getRun(currentRunId).then((run) => {
158
- if (run) {
159
- setStatus(run.status);
160
- if (run.status === "completed" && run.output) {
161
- setOutput(run.output);
162
- }
163
- if (run.status === "failed" && run.error) {
164
- setError(run.error);
165
- }
166
- }
167
- });
208
+ jobDefinition.name,
209
+ {
210
+ followLatest: options?.followLatest
168
211
  }
169
- }, [durably, currentRunId, options?.initialRunId]);
170
- const trigger = useCallback(
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(
171
234
  async (input) => {
172
235
  const jobHandle = jobHandleRef.current;
173
236
  if (!jobHandle) {
174
237
  throw new Error("Job not ready");
175
238
  }
176
- setOutput(null);
177
- setError(null);
178
- setLogs([]);
179
- setProgress(null);
239
+ subscription.reset();
180
240
  const run = await jobHandle.trigger(input);
181
- setCurrentRunId(run.id);
182
- currentRunIdRef.current = run.id;
183
- setStatus("pending");
241
+ subscription.setCurrentRunId(run.id);
184
242
  return { runId: run.id };
185
243
  },
186
- []
244
+ [subscription]
187
245
  );
188
- const triggerAndWait = useCallback(
246
+ const triggerAndWait = useCallback2(
189
247
  async (input) => {
190
248
  const jobHandle = jobHandleRef.current;
191
249
  if (!jobHandle || !durably) {
192
250
  throw new Error("Job not ready");
193
251
  }
194
- setOutput(null);
195
- setError(null);
196
- setLogs([]);
197
- setProgress(null);
252
+ subscription.reset();
198
253
  const run = await jobHandle.trigger(input);
199
- setCurrentRunId(run.id);
200
- currentRunIdRef.current = run.id;
201
- setStatus("pending");
254
+ subscription.setCurrentRunId(run.id);
202
255
  return new Promise((resolve, reject) => {
203
256
  const checkCompletion = async () => {
204
257
  const updatedRun = await jobHandle.getRun(run.id);
@@ -219,135 +272,101 @@ function useJob(jobDefinition, options) {
219
272
  checkCompletion();
220
273
  });
221
274
  },
222
- [durably]
275
+ [durably, subscription]
223
276
  );
224
- const reset = useCallback(() => {
225
- setStatus(null);
226
- setOutput(null);
227
- setError(null);
228
- setLogs([]);
229
- setProgress(null);
230
- setCurrentRunId(null);
231
- }, []);
232
277
  return {
233
278
  trigger,
234
279
  triggerAndWait,
235
- status,
236
- output,
237
- error,
238
- logs,
239
- progress,
240
- isRunning: status === "running",
241
- isPending: status === "pending",
242
- isCompleted: status === "completed",
243
- isFailed: status === "failed",
244
- isCancelled: status === "cancelled",
245
- currentRunId,
246
- reset
280
+ status: subscription.status,
281
+ output: subscription.output,
282
+ error: subscription.error,
283
+ logs: subscription.logs,
284
+ progress: subscription.progress,
285
+ isRunning: subscription.status === "running",
286
+ isPending: subscription.status === "pending",
287
+ isCompleted: subscription.status === "completed",
288
+ isFailed: subscription.status === "failed",
289
+ isCancelled: subscription.status === "cancelled",
290
+ currentRunId: subscription.currentRunId,
291
+ reset: subscription.reset
247
292
  };
248
293
  }
249
294
 
250
295
  // src/hooks/use-run-subscription.ts
251
- import { useEffect as useEffect2, useRef as useRef2, useState as useState2 } from "react";
252
- function useRunSubscription(durably, runId, options) {
253
- const [status, setStatus] = useState2(null);
254
- const [output, setOutput] = useState2(null);
255
- const [error, setError] = useState2(null);
256
- const [logs, setLogs] = useState2([]);
257
- const [progress, setProgress] = useState2(null);
258
- const runIdRef = useRef2(runId);
259
- runIdRef.current = runId;
260
- const maxLogs = options?.maxLogs ?? 0;
261
- useEffect2(() => {
262
- if (!durably || !runId) return;
263
- const unsubscribes = [];
264
- unsubscribes.push(
265
- durably.on("run:start", (event) => {
266
- if (event.runId !== runIdRef.current) return;
267
- setStatus("running");
268
- })
269
- );
270
- unsubscribes.push(
271
- durably.on("run:complete", (event) => {
272
- if (event.runId !== runIdRef.current) return;
273
- setStatus("completed");
274
- setOutput(event.output);
275
- })
276
- );
277
- unsubscribes.push(
278
- durably.on("run:fail", (event) => {
279
- if (event.runId !== runIdRef.current) return;
280
- setStatus("failed");
281
- setError(event.error);
282
- })
283
- );
284
- unsubscribes.push(
285
- durably.on("run:cancel", (event) => {
286
- if (event.runId !== runIdRef.current) return;
287
- setStatus("cancelled");
288
- })
289
- );
290
- unsubscribes.push(
291
- durably.on("run:retry", (event) => {
292
- if (event.runId !== runIdRef.current) return;
293
- setStatus("pending");
294
- setError(null);
295
- })
296
- );
297
- unsubscribes.push(
298
- durably.on("run:progress", (event) => {
299
- if (event.runId !== runIdRef.current) return;
300
- setProgress(event.progress);
301
- })
302
- );
303
- unsubscribes.push(
304
- durably.on("log:write", (event) => {
305
- if (event.runId !== runIdRef.current) return;
306
- setLogs((prev) => {
307
- const newLog = {
308
- id: crypto.randomUUID(),
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",
309
344
  runId: event.runId,
310
345
  stepName: event.stepName,
311
346
  level: event.level,
312
347
  message: event.message,
313
- data: event.data,
314
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
315
- };
316
- const newLogs = [...prev, newLog];
317
- if (maxLogs > 0 && newLogs.length > maxLogs) {
318
- return newLogs.slice(-maxLogs);
319
- }
320
- return newLogs;
321
- });
322
- })
323
- );
324
- return () => {
325
- for (const unsubscribe of unsubscribes) {
326
- unsubscribe();
327
- }
328
- };
329
- }, [durably, runId, maxLogs]);
330
- const clearLogs = () => {
331
- setLogs([]);
332
- };
333
- const reset = () => {
334
- setStatus(null);
335
- setOutput(null);
336
- setError(null);
337
- setLogs([]);
338
- setProgress(null);
339
- };
340
- return {
341
- status,
342
- output,
343
- error,
344
- logs,
345
- progress,
346
- clearLogs,
347
- reset
348
+ data: event.data
349
+ });
350
+ })
351
+ );
352
+ return () => {
353
+ for (const unsubscribe of unsubscribes) {
354
+ unsubscribe();
355
+ }
356
+ };
357
+ }
348
358
  };
349
359
  }
350
360
 
361
+ // src/hooks/use-run-subscription.ts
362
+ function useRunSubscription(durably, runId, options) {
363
+ const subscriber = useMemo2(
364
+ () => durably ? createDurablyEventSubscriber(durably) : null,
365
+ [durably]
366
+ );
367
+ return useSubscription(subscriber, runId, options);
368
+ }
369
+
351
370
  // src/hooks/use-job-logs.ts
352
371
  function useJobLogs(options) {
353
372
  const { durably } = useDurably();
@@ -360,13 +379,13 @@ function useJobLogs(options) {
360
379
  }
361
380
 
362
381
  // src/hooks/use-job-run.ts
363
- import { useEffect as useEffect3, useRef as useRef3 } from "react";
382
+ import { useEffect as useEffect4, useRef as useRef3 } from "react";
364
383
  function useJobRun(options) {
365
384
  const { durably } = useDurably();
366
385
  const { runId } = options;
367
386
  const subscription = useRunSubscription(durably, runId);
368
387
  const fetchedRef = useRef3(/* @__PURE__ */ new Set());
369
- useEffect3(() => {
388
+ useEffect4(() => {
370
389
  if (!durably || !runId || fetchedRef.current.has(runId)) return;
371
390
  fetchedRef.current.add(runId);
372
391
  }, [durably, runId]);
@@ -385,22 +404,26 @@ function useJobRun(options) {
385
404
  }
386
405
 
387
406
  // src/hooks/use-runs.ts
388
- import { useCallback as useCallback2, useEffect as useEffect4, useState as useState3 } from "react";
389
- function useRuns(options) {
407
+ import { useCallback as useCallback3, useEffect as useEffect5, useState } from "react";
408
+ function useRuns(jobDefinitionOrOptions, optionsArg) {
390
409
  const { durably } = useDurably();
410
+ const isJob = isJobDefinition(jobDefinitionOrOptions);
411
+ const jobName = isJob ? jobDefinitionOrOptions.name : jobDefinitionOrOptions?.jobName;
412
+ const options = isJob ? optionsArg : jobDefinitionOrOptions;
391
413
  const pageSize = options?.pageSize ?? 10;
392
414
  const realtime = options?.realtime ?? true;
393
- const [runs, setRuns] = useState3([]);
394
- const [page, setPage] = useState3(0);
395
- const [hasMore, setHasMore] = useState3(false);
396
- const [isLoading, setIsLoading] = useState3(false);
397
- const refresh = useCallback2(async () => {
415
+ const status = options?.status;
416
+ const [runs, setRuns] = useState([]);
417
+ const [page, setPage] = useState(0);
418
+ const [hasMore, setHasMore] = useState(false);
419
+ const [isLoading, setIsLoading] = useState(false);
420
+ const refresh = useCallback3(async () => {
398
421
  if (!durably) return;
399
422
  setIsLoading(true);
400
423
  try {
401
424
  const data = await durably.getRuns({
402
- jobName: options?.jobName,
403
- status: options?.status,
425
+ jobName,
426
+ status,
404
427
  limit: pageSize + 1,
405
428
  offset: page * pageSize
406
429
  });
@@ -409,8 +432,8 @@ function useRuns(options) {
409
432
  } finally {
410
433
  setIsLoading(false);
411
434
  }
412
- }, [durably, options?.jobName, options?.status, pageSize, page]);
413
- useEffect4(() => {
435
+ }, [durably, jobName, status, pageSize, page]);
436
+ useEffect5(() => {
414
437
  if (!durably) return;
415
438
  refresh();
416
439
  if (!realtime) return;
@@ -431,15 +454,15 @@ function useRuns(options) {
431
454
  }
432
455
  };
433
456
  }, [durably, refresh, realtime]);
434
- const nextPage = useCallback2(() => {
457
+ const nextPage = useCallback3(() => {
435
458
  if (hasMore) {
436
459
  setPage((p) => p + 1);
437
460
  }
438
461
  }, [hasMore]);
439
- const prevPage = useCallback2(() => {
462
+ const prevPage = useCallback3(() => {
440
463
  setPage((p) => Math.max(0, p - 1));
441
464
  }, []);
442
- const goToPage = useCallback2((newPage) => {
465
+ const goToPage = useCallback3((newPage) => {
443
466
  setPage(Math.max(0, newPage));
444
467
  }, []);
445
468
  return {