@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/chunk-M3YA2EA4.js +136 -0
- package/dist/chunk-M3YA2EA4.js.map +1 -0
- package/dist/client.d.ts +2 -24
- package/dist/client.js +96 -129
- package/dist/client.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +269 -245
- package/dist/index.js.map +1 -1
- package/dist/{types-BDUvsa8u.d.ts → types-C7h7nZv3.d.ts} +11 -1
- package/package.json +2 -2
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
// src/shared/create-log-entry.ts
|
|
2
|
+
function createLogEntry(params) {
|
|
3
|
+
return {
|
|
4
|
+
id: crypto.randomUUID(),
|
|
5
|
+
runId: params.runId,
|
|
6
|
+
stepName: params.stepName,
|
|
7
|
+
level: params.level,
|
|
8
|
+
message: params.message,
|
|
9
|
+
data: params.data,
|
|
10
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
function appendLog(logs, newLog, maxLogs) {
|
|
14
|
+
const newLogs = [...logs, newLog];
|
|
15
|
+
if (maxLogs > 0 && newLogs.length > maxLogs) {
|
|
16
|
+
return newLogs.slice(-maxLogs);
|
|
17
|
+
}
|
|
18
|
+
return newLogs;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// src/shared/subscription-reducer.ts
|
|
22
|
+
var initialSubscriptionState = {
|
|
23
|
+
status: null,
|
|
24
|
+
output: null,
|
|
25
|
+
error: null,
|
|
26
|
+
logs: [],
|
|
27
|
+
progress: null
|
|
28
|
+
};
|
|
29
|
+
function subscriptionReducer(state, action) {
|
|
30
|
+
switch (action.type) {
|
|
31
|
+
case "run:start":
|
|
32
|
+
return { ...state, status: "running" };
|
|
33
|
+
case "run:complete":
|
|
34
|
+
return { ...state, status: "completed", output: action.output };
|
|
35
|
+
case "run:fail":
|
|
36
|
+
return { ...state, status: "failed", error: action.error };
|
|
37
|
+
case "run:cancel":
|
|
38
|
+
return { ...state, status: "cancelled" };
|
|
39
|
+
case "run:retry":
|
|
40
|
+
return { ...state, status: "pending", error: null };
|
|
41
|
+
case "run:progress":
|
|
42
|
+
return { ...state, progress: action.progress };
|
|
43
|
+
case "log:write": {
|
|
44
|
+
const newLog = createLogEntry({
|
|
45
|
+
runId: action.runId,
|
|
46
|
+
stepName: action.stepName,
|
|
47
|
+
level: action.level,
|
|
48
|
+
message: action.message,
|
|
49
|
+
data: action.data
|
|
50
|
+
});
|
|
51
|
+
return { ...state, logs: appendLog(state.logs, newLog, action.maxLogs) };
|
|
52
|
+
}
|
|
53
|
+
case "reset":
|
|
54
|
+
return initialSubscriptionState;
|
|
55
|
+
case "clear_logs":
|
|
56
|
+
return { ...state, logs: [] };
|
|
57
|
+
case "connection_error":
|
|
58
|
+
return { ...state, error: action.error };
|
|
59
|
+
default:
|
|
60
|
+
return state;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// src/shared/use-subscription.ts
|
|
65
|
+
import { useCallback, useEffect, useReducer, useRef } from "react";
|
|
66
|
+
function useSubscription(subscriber, runId, options) {
|
|
67
|
+
const [state, dispatch] = useReducer(
|
|
68
|
+
subscriptionReducer,
|
|
69
|
+
initialSubscriptionState
|
|
70
|
+
);
|
|
71
|
+
const runIdRef = useRef(runId);
|
|
72
|
+
const prevRunIdRef = useRef(null);
|
|
73
|
+
const maxLogs = options?.maxLogs ?? 0;
|
|
74
|
+
if (prevRunIdRef.current !== runId) {
|
|
75
|
+
prevRunIdRef.current = runId;
|
|
76
|
+
if (runIdRef.current !== runId) {
|
|
77
|
+
dispatch({ type: "reset" });
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
runIdRef.current = runId;
|
|
81
|
+
useEffect(() => {
|
|
82
|
+
if (!subscriber || !runId) return;
|
|
83
|
+
const unsubscribe = subscriber.subscribe(runId, (event) => {
|
|
84
|
+
if (runIdRef.current !== runId) return;
|
|
85
|
+
switch (event.type) {
|
|
86
|
+
case "run:start":
|
|
87
|
+
case "run:cancel":
|
|
88
|
+
case "run:retry":
|
|
89
|
+
dispatch({ type: event.type });
|
|
90
|
+
break;
|
|
91
|
+
case "run:complete":
|
|
92
|
+
dispatch({ type: "run:complete", output: event.output });
|
|
93
|
+
break;
|
|
94
|
+
case "run:fail":
|
|
95
|
+
dispatch({ type: "run:fail", error: event.error });
|
|
96
|
+
break;
|
|
97
|
+
case "run:progress":
|
|
98
|
+
dispatch({ type: "run:progress", progress: event.progress });
|
|
99
|
+
break;
|
|
100
|
+
case "log:write":
|
|
101
|
+
dispatch({
|
|
102
|
+
type: "log:write",
|
|
103
|
+
runId: event.runId,
|
|
104
|
+
stepName: event.stepName,
|
|
105
|
+
level: event.level,
|
|
106
|
+
message: event.message,
|
|
107
|
+
data: event.data,
|
|
108
|
+
maxLogs
|
|
109
|
+
});
|
|
110
|
+
break;
|
|
111
|
+
case "connection_error":
|
|
112
|
+
dispatch({ type: "connection_error", error: event.error });
|
|
113
|
+
break;
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
return unsubscribe;
|
|
117
|
+
}, [subscriber, runId, maxLogs]);
|
|
118
|
+
const clearLogs = useCallback(() => {
|
|
119
|
+
dispatch({ type: "clear_logs" });
|
|
120
|
+
}, []);
|
|
121
|
+
const reset = useCallback(() => {
|
|
122
|
+
dispatch({ type: "reset" });
|
|
123
|
+
}, []);
|
|
124
|
+
return {
|
|
125
|
+
...state,
|
|
126
|
+
clearLogs,
|
|
127
|
+
reset
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export {
|
|
132
|
+
initialSubscriptionState,
|
|
133
|
+
subscriptionReducer,
|
|
134
|
+
useSubscription
|
|
135
|
+
};
|
|
136
|
+
//# sourceMappingURL=chunk-M3YA2EA4.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/shared/create-log-entry.ts","../src/shared/subscription-reducer.ts","../src/shared/use-subscription.ts"],"sourcesContent":["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":[]}
|
package/dist/client.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
+
import { R as RunStatus, L as LogEntry, P as Progress, I as InferInput, a as InferOutput } from './types-C7h7nZv3.js';
|
|
1
2
|
import { JobDefinition } from '@coji/durably';
|
|
2
|
-
import { R as RunStatus, L as LogEntry, P as Progress } from './types-BDUvsa8u.js';
|
|
3
3
|
|
|
4
4
|
interface UseJobClientOptions {
|
|
5
5
|
/**
|
|
@@ -190,20 +190,6 @@ interface UseJobRunClientResult<TOutput = unknown> {
|
|
|
190
190
|
*/
|
|
191
191
|
declare function useJobRun<TOutput = unknown>(options: UseJobRunClientOptions): UseJobRunClientResult<TOutput>;
|
|
192
192
|
|
|
193
|
-
/**
|
|
194
|
-
* Extract input type from a JobDefinition or JobHandle
|
|
195
|
-
*/
|
|
196
|
-
type InferInput$1<T> = T extends JobDefinition<string, infer TInput, unknown> ? TInput extends Record<string, unknown> ? TInput : Record<string, unknown> : T extends {
|
|
197
|
-
trigger: (input: infer TInput) => unknown;
|
|
198
|
-
} ? TInput extends Record<string, unknown> ? TInput : Record<string, unknown> : Record<string, unknown>;
|
|
199
|
-
/**
|
|
200
|
-
* Extract output type from a JobDefinition or JobHandle
|
|
201
|
-
*/
|
|
202
|
-
type InferOutput$1<T> = T extends JobDefinition<string, unknown, infer TOutput> ? TOutput extends Record<string, unknown> ? TOutput : Record<string, unknown> : T extends {
|
|
203
|
-
trigger: (input: unknown) => Promise<{
|
|
204
|
-
output?: infer TOutput;
|
|
205
|
-
}>;
|
|
206
|
-
} ? TOutput extends Record<string, unknown> ? TOutput : Record<string, unknown> : Record<string, unknown>;
|
|
207
193
|
/**
|
|
208
194
|
* Type-safe hooks for a specific job
|
|
209
195
|
*/
|
|
@@ -236,7 +222,7 @@ interface CreateDurablyClientOptions {
|
|
|
236
222
|
* A type-safe client with hooks for each registered job
|
|
237
223
|
*/
|
|
238
224
|
type DurablyClient<TJobs extends Record<string, unknown>> = {
|
|
239
|
-
[K in keyof TJobs]: JobClient<InferInput
|
|
225
|
+
[K in keyof TJobs]: JobClient<InferInput<TJobs[K]>, InferOutput<TJobs[K]>>;
|
|
240
226
|
};
|
|
241
227
|
/**
|
|
242
228
|
* Create a type-safe Durably client with hooks for all registered jobs.
|
|
@@ -273,14 +259,6 @@ type DurablyClient<TJobs extends Record<string, unknown>> = {
|
|
|
273
259
|
*/
|
|
274
260
|
declare function createDurablyClient<TJobs extends Record<string, unknown>>(options: CreateDurablyClientOptions): DurablyClient<TJobs>;
|
|
275
261
|
|
|
276
|
-
/**
|
|
277
|
-
* Extract input type from a JobDefinition
|
|
278
|
-
*/
|
|
279
|
-
type InferInput<T> = T extends JobDefinition<string, infer TInput, unknown> ? TInput extends Record<string, unknown> ? TInput : Record<string, unknown> : Record<string, unknown>;
|
|
280
|
-
/**
|
|
281
|
-
* Extract output type from a JobDefinition
|
|
282
|
-
*/
|
|
283
|
-
type InferOutput<T> = T extends JobDefinition<string, unknown, infer TOutput> ? TOutput extends Record<string, unknown> ? TOutput : Record<string, unknown> : Record<string, unknown>;
|
|
284
262
|
/**
|
|
285
263
|
* Options for createJobHooks
|
|
286
264
|
*/
|
package/dist/client.js
CHANGED
|
@@ -1,121 +1,88 @@
|
|
|
1
|
+
import {
|
|
2
|
+
useSubscription
|
|
3
|
+
} from "./chunk-M3YA2EA4.js";
|
|
4
|
+
|
|
1
5
|
// src/client/use-job.ts
|
|
2
|
-
import { useCallback
|
|
6
|
+
import { useCallback, useEffect, useState } from "react";
|
|
3
7
|
|
|
4
8
|
// src/client/use-sse-subscription.ts
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
setOutput(data.output);
|
|
43
|
-
break;
|
|
44
|
-
case "run:fail":
|
|
45
|
-
setStatus("failed");
|
|
46
|
-
setError(data.error);
|
|
47
|
-
break;
|
|
48
|
-
case "run:cancel":
|
|
49
|
-
setStatus("cancelled");
|
|
50
|
-
break;
|
|
51
|
-
case "run:retry":
|
|
52
|
-
setStatus("pending");
|
|
53
|
-
setError(null);
|
|
54
|
-
break;
|
|
55
|
-
case "run:progress":
|
|
56
|
-
setProgress(data.progress);
|
|
57
|
-
break;
|
|
58
|
-
case "log:write":
|
|
59
|
-
setLogs((prev) => {
|
|
60
|
-
const newLog = {
|
|
61
|
-
id: crypto.randomUUID(),
|
|
9
|
+
import { useMemo } from "react";
|
|
10
|
+
|
|
11
|
+
// src/shared/sse-event-subscriber.ts
|
|
12
|
+
function createSSEEventSubscriber(apiBaseUrl) {
|
|
13
|
+
return {
|
|
14
|
+
subscribe(runId, onEvent) {
|
|
15
|
+
const url = `${apiBaseUrl}/subscribe?runId=${encodeURIComponent(runId)}`;
|
|
16
|
+
const eventSource = new EventSource(url);
|
|
17
|
+
eventSource.onmessage = (messageEvent) => {
|
|
18
|
+
try {
|
|
19
|
+
const data = JSON.parse(messageEvent.data);
|
|
20
|
+
if (data.runId !== runId) return;
|
|
21
|
+
switch (data.type) {
|
|
22
|
+
case "run:start":
|
|
23
|
+
onEvent({ type: "run:start" });
|
|
24
|
+
break;
|
|
25
|
+
case "run:complete":
|
|
26
|
+
onEvent({
|
|
27
|
+
type: "run:complete",
|
|
28
|
+
output: data.output
|
|
29
|
+
});
|
|
30
|
+
break;
|
|
31
|
+
case "run:fail":
|
|
32
|
+
onEvent({ type: "run:fail", error: data.error });
|
|
33
|
+
break;
|
|
34
|
+
case "run:cancel":
|
|
35
|
+
onEvent({ type: "run:cancel" });
|
|
36
|
+
break;
|
|
37
|
+
case "run:retry":
|
|
38
|
+
onEvent({ type: "run:retry" });
|
|
39
|
+
break;
|
|
40
|
+
case "run:progress":
|
|
41
|
+
onEvent({ type: "run:progress", progress: data.progress });
|
|
42
|
+
break;
|
|
43
|
+
case "log:write":
|
|
44
|
+
onEvent({
|
|
45
|
+
type: "log:write",
|
|
62
46
|
runId: data.runId,
|
|
63
47
|
stepName: null,
|
|
64
48
|
level: data.level,
|
|
65
49
|
message: data.message,
|
|
66
|
-
data: data.data
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
return newLogs.slice(-maxLogs);
|
|
72
|
-
}
|
|
73
|
-
return newLogs;
|
|
74
|
-
});
|
|
75
|
-
break;
|
|
50
|
+
data: data.data
|
|
51
|
+
});
|
|
52
|
+
break;
|
|
53
|
+
}
|
|
54
|
+
} catch {
|
|
76
55
|
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
eventSourceRef.current = null;
|
|
87
|
-
};
|
|
88
|
-
}, [api, runId, maxLogs]);
|
|
89
|
-
const clearLogs = useCallback(() => {
|
|
90
|
-
setLogs([]);
|
|
91
|
-
}, []);
|
|
92
|
-
const reset = useCallback(() => {
|
|
93
|
-
setStatus(null);
|
|
94
|
-
setOutput(null);
|
|
95
|
-
setError(null);
|
|
96
|
-
setLogs([]);
|
|
97
|
-
setProgress(null);
|
|
98
|
-
}, []);
|
|
99
|
-
return {
|
|
100
|
-
status,
|
|
101
|
-
output,
|
|
102
|
-
error,
|
|
103
|
-
logs,
|
|
104
|
-
progress,
|
|
105
|
-
clearLogs,
|
|
106
|
-
reset
|
|
56
|
+
};
|
|
57
|
+
eventSource.onerror = () => {
|
|
58
|
+
onEvent({ type: "connection_error", error: "Connection failed" });
|
|
59
|
+
eventSource.close();
|
|
60
|
+
};
|
|
61
|
+
return () => {
|
|
62
|
+
eventSource.close();
|
|
63
|
+
};
|
|
64
|
+
}
|
|
107
65
|
};
|
|
108
66
|
}
|
|
109
67
|
|
|
68
|
+
// src/client/use-sse-subscription.ts
|
|
69
|
+
function useSSESubscription(api, runId, options) {
|
|
70
|
+
const subscriber = useMemo(
|
|
71
|
+
() => api ? createSSEEventSubscriber(api) : null,
|
|
72
|
+
[api]
|
|
73
|
+
);
|
|
74
|
+
return useSubscription(subscriber, runId, options);
|
|
75
|
+
}
|
|
76
|
+
|
|
110
77
|
// src/client/use-job.ts
|
|
111
78
|
function useJob(options) {
|
|
112
79
|
const { api, jobName, initialRunId } = options;
|
|
113
|
-
const [currentRunId, setCurrentRunId] =
|
|
80
|
+
const [currentRunId, setCurrentRunId] = useState(
|
|
114
81
|
initialRunId ?? null
|
|
115
82
|
);
|
|
116
|
-
const [isPending, setIsPending] =
|
|
83
|
+
const [isPending, setIsPending] = useState(false);
|
|
117
84
|
const subscription = useSSESubscription(api, currentRunId);
|
|
118
|
-
const trigger =
|
|
85
|
+
const trigger = useCallback(
|
|
119
86
|
async (input) => {
|
|
120
87
|
subscription.reset();
|
|
121
88
|
setIsPending(true);
|
|
@@ -137,7 +104,7 @@ function useJob(options) {
|
|
|
137
104
|
},
|
|
138
105
|
[api, jobName, subscription.reset]
|
|
139
106
|
);
|
|
140
|
-
const triggerAndWait =
|
|
107
|
+
const triggerAndWait = useCallback(
|
|
141
108
|
async (input) => {
|
|
142
109
|
const { runId } = await trigger(input);
|
|
143
110
|
return new Promise((resolve, reject) => {
|
|
@@ -157,13 +124,13 @@ function useJob(options) {
|
|
|
157
124
|
},
|
|
158
125
|
[trigger, subscription.status, subscription.output, subscription.error]
|
|
159
126
|
);
|
|
160
|
-
const reset =
|
|
127
|
+
const reset = useCallback(() => {
|
|
161
128
|
subscription.reset();
|
|
162
129
|
setCurrentRunId(null);
|
|
163
130
|
setIsPending(false);
|
|
164
131
|
}, [subscription.reset]);
|
|
165
132
|
const effectiveStatus = subscription.status ?? (isPending ? "pending" : null);
|
|
166
|
-
|
|
133
|
+
useEffect(() => {
|
|
167
134
|
if (subscription.status && isPending) {
|
|
168
135
|
setIsPending(false);
|
|
169
136
|
}
|
|
@@ -196,7 +163,7 @@ function useJobLogs(options) {
|
|
|
196
163
|
}
|
|
197
164
|
|
|
198
165
|
// src/client/use-job-run.ts
|
|
199
|
-
import { useEffect as
|
|
166
|
+
import { useEffect as useEffect2, useRef } from "react";
|
|
200
167
|
function useJobRun(options) {
|
|
201
168
|
const { api, runId, onStart, onComplete, onFail } = options;
|
|
202
169
|
const subscription = useSSESubscription(api, runId);
|
|
@@ -206,8 +173,8 @@ function useJobRun(options) {
|
|
|
206
173
|
const isPending = effectiveStatus === "pending";
|
|
207
174
|
const isRunning = effectiveStatus === "running";
|
|
208
175
|
const isCancelled = effectiveStatus === "cancelled";
|
|
209
|
-
const prevStatusRef =
|
|
210
|
-
|
|
176
|
+
const prevStatusRef = useRef(null);
|
|
177
|
+
useEffect2(() => {
|
|
211
178
|
const prevStatus = prevStatusRef.current;
|
|
212
179
|
prevStatusRef.current = effectiveStatus;
|
|
213
180
|
if (prevStatus !== effectiveStatus) {
|
|
@@ -282,17 +249,17 @@ function createJobHooks(options) {
|
|
|
282
249
|
}
|
|
283
250
|
|
|
284
251
|
// src/client/use-runs.ts
|
|
285
|
-
import { useCallback as
|
|
252
|
+
import { useCallback as useCallback2, useEffect as useEffect3, useRef as useRef2, useState as useState2 } from "react";
|
|
286
253
|
function useRuns(options) {
|
|
287
254
|
const { api, jobName, status, pageSize = 10 } = options;
|
|
288
|
-
const [runs, setRuns] =
|
|
289
|
-
const [page, setPage] =
|
|
290
|
-
const [hasMore, setHasMore] =
|
|
291
|
-
const [isLoading, setIsLoading] =
|
|
292
|
-
const [error, setError] =
|
|
293
|
-
const isMountedRef =
|
|
294
|
-
const eventSourceRef =
|
|
295
|
-
const refresh =
|
|
255
|
+
const [runs, setRuns] = useState2([]);
|
|
256
|
+
const [page, setPage] = useState2(0);
|
|
257
|
+
const [hasMore, setHasMore] = useState2(false);
|
|
258
|
+
const [isLoading, setIsLoading] = useState2(false);
|
|
259
|
+
const [error, setError] = useState2(null);
|
|
260
|
+
const isMountedRef = useRef2(true);
|
|
261
|
+
const eventSourceRef = useRef2(null);
|
|
262
|
+
const refresh = useCallback2(async () => {
|
|
296
263
|
setIsLoading(true);
|
|
297
264
|
setError(null);
|
|
298
265
|
try {
|
|
@@ -321,14 +288,14 @@ function useRuns(options) {
|
|
|
321
288
|
}
|
|
322
289
|
}
|
|
323
290
|
}, [api, jobName, status, pageSize, page]);
|
|
324
|
-
|
|
291
|
+
useEffect3(() => {
|
|
325
292
|
isMountedRef.current = true;
|
|
326
293
|
refresh();
|
|
327
294
|
return () => {
|
|
328
295
|
isMountedRef.current = false;
|
|
329
296
|
};
|
|
330
297
|
}, [refresh]);
|
|
331
|
-
|
|
298
|
+
useEffect3(() => {
|
|
332
299
|
if (page !== 0) {
|
|
333
300
|
if (eventSourceRef.current) {
|
|
334
301
|
eventSourceRef.current.close();
|
|
@@ -374,15 +341,15 @@ function useRuns(options) {
|
|
|
374
341
|
eventSourceRef.current = null;
|
|
375
342
|
};
|
|
376
343
|
}, [api, jobName, page, refresh]);
|
|
377
|
-
const nextPage =
|
|
344
|
+
const nextPage = useCallback2(() => {
|
|
378
345
|
if (hasMore) {
|
|
379
346
|
setPage((p) => p + 1);
|
|
380
347
|
}
|
|
381
348
|
}, [hasMore]);
|
|
382
|
-
const prevPage =
|
|
349
|
+
const prevPage = useCallback2(() => {
|
|
383
350
|
setPage((p) => Math.max(0, p - 1));
|
|
384
351
|
}, []);
|
|
385
|
-
const goToPage =
|
|
352
|
+
const goToPage = useCallback2((newPage) => {
|
|
386
353
|
setPage(Math.max(0, newPage));
|
|
387
354
|
}, []);
|
|
388
355
|
return {
|
|
@@ -399,12 +366,12 @@ function useRuns(options) {
|
|
|
399
366
|
}
|
|
400
367
|
|
|
401
368
|
// src/client/use-run-actions.ts
|
|
402
|
-
import { useCallback as
|
|
369
|
+
import { useCallback as useCallback3, useState as useState3 } from "react";
|
|
403
370
|
function useRunActions(options) {
|
|
404
371
|
const { api } = options;
|
|
405
|
-
const [isLoading, setIsLoading] =
|
|
406
|
-
const [error, setError] =
|
|
407
|
-
const retry =
|
|
372
|
+
const [isLoading, setIsLoading] = useState3(false);
|
|
373
|
+
const [error, setError] = useState3(null);
|
|
374
|
+
const retry = useCallback3(
|
|
408
375
|
async (runId) => {
|
|
409
376
|
setIsLoading(true);
|
|
410
377
|
setError(null);
|
|
@@ -432,7 +399,7 @@ function useRunActions(options) {
|
|
|
432
399
|
},
|
|
433
400
|
[api]
|
|
434
401
|
);
|
|
435
|
-
const cancel =
|
|
402
|
+
const cancel = useCallback3(
|
|
436
403
|
async (runId) => {
|
|
437
404
|
setIsLoading(true);
|
|
438
405
|
setError(null);
|
|
@@ -460,7 +427,7 @@ function useRunActions(options) {
|
|
|
460
427
|
},
|
|
461
428
|
[api]
|
|
462
429
|
);
|
|
463
|
-
const deleteRun =
|
|
430
|
+
const deleteRun = useCallback3(
|
|
464
431
|
async (runId) => {
|
|
465
432
|
setIsLoading(true);
|
|
466
433
|
setError(null);
|
|
@@ -488,7 +455,7 @@ function useRunActions(options) {
|
|
|
488
455
|
},
|
|
489
456
|
[api]
|
|
490
457
|
);
|
|
491
|
-
const getRun =
|
|
458
|
+
const getRun = useCallback3(
|
|
492
459
|
async (runId) => {
|
|
493
460
|
setIsLoading(true);
|
|
494
461
|
setError(null);
|
|
@@ -520,7 +487,7 @@ function useRunActions(options) {
|
|
|
520
487
|
},
|
|
521
488
|
[api]
|
|
522
489
|
);
|
|
523
|
-
const getSteps =
|
|
490
|
+
const getSteps = useCallback3(
|
|
524
491
|
async (runId) => {
|
|
525
492
|
setIsLoading(true);
|
|
526
493
|
setError(null);
|