@coji/durably-react 0.6.0 → 0.6.1

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