@coji/durably-react 0.11.0 → 0.13.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 +4 -4
- package/dist/{chunk-XRJEZWAV.js → chunk-33VIIDHK.js} +4 -7
- package/dist/chunk-33VIIDHK.js.map +1 -0
- package/dist/index.d.ts +17 -17
- package/dist/index.js +58 -100
- package/dist/index.js.map +1 -1
- package/dist/spa.d.ts +8 -8
- package/dist/spa.js +20 -38
- package/dist/spa.js.map +1 -1
- package/dist/{types-JIBwGTm6.d.ts → types-DMtqQ6Wp.d.ts} +2 -6
- package/package.json +20 -20
- package/LICENSE +0 -21
- package/dist/chunk-XRJEZWAV.js.map +0 -1
package/README.md
CHANGED
|
@@ -28,10 +28,10 @@ export const durably = createDurably<typeof durably>({
|
|
|
28
28
|
})
|
|
29
29
|
|
|
30
30
|
function MyComponent() {
|
|
31
|
-
const { trigger,
|
|
31
|
+
const { trigger, isLeased, isCompleted, output } = durably.myJob.useJob()
|
|
32
32
|
|
|
33
33
|
return (
|
|
34
|
-
<button onClick={() => trigger({ id: '123' })} disabled={
|
|
34
|
+
<button onClick={() => trigger({ id: '123' })} disabled={isLeased}>
|
|
35
35
|
Run
|
|
36
36
|
</button>
|
|
37
37
|
)
|
|
@@ -79,9 +79,9 @@ function App() {
|
|
|
79
79
|
}
|
|
80
80
|
|
|
81
81
|
function MyComponent() {
|
|
82
|
-
const { trigger,
|
|
82
|
+
const { trigger, isLeased, isCompleted } = useJob(myJob)
|
|
83
83
|
return (
|
|
84
|
-
<button onClick={() => trigger({ id: '123' })} disabled={
|
|
84
|
+
<button onClick={() => trigger({ id: '123' })} disabled={isLeased}>
|
|
85
85
|
Run
|
|
86
86
|
</button>
|
|
87
87
|
)
|
|
@@ -33,16 +33,14 @@ var initialSubscriptionState = {
|
|
|
33
33
|
};
|
|
34
34
|
function subscriptionReducer(state, action) {
|
|
35
35
|
switch (action.type) {
|
|
36
|
-
case "run:
|
|
37
|
-
return { ...state, status: "
|
|
36
|
+
case "run:leased":
|
|
37
|
+
return { ...state, status: "leased" };
|
|
38
38
|
case "run:complete":
|
|
39
39
|
return { ...state, status: "completed", output: action.output };
|
|
40
40
|
case "run:fail":
|
|
41
41
|
return { ...state, status: "failed", error: action.error };
|
|
42
42
|
case "run:cancel":
|
|
43
43
|
return { ...state, status: "cancelled" };
|
|
44
|
-
case "run:retry":
|
|
45
|
-
return { ...state, status: "pending", error: null };
|
|
46
44
|
case "run:progress":
|
|
47
45
|
return { ...state, progress: action.progress };
|
|
48
46
|
case "log:write": {
|
|
@@ -88,9 +86,8 @@ function useSubscription(subscriber, runId, options) {
|
|
|
88
86
|
const unsubscribe = subscriber.subscribe(runId, (event) => {
|
|
89
87
|
if (runIdRef.current !== runId) return;
|
|
90
88
|
switch (event.type) {
|
|
91
|
-
case "run:
|
|
89
|
+
case "run:leased":
|
|
92
90
|
case "run:cancel":
|
|
93
|
-
case "run:retry":
|
|
94
91
|
dispatch({ type: event.type });
|
|
95
92
|
break;
|
|
96
93
|
case "run:complete":
|
|
@@ -139,4 +136,4 @@ export {
|
|
|
139
136
|
useSubscription,
|
|
140
137
|
isJobDefinition
|
|
141
138
|
};
|
|
142
|
-
//# sourceMappingURL=chunk-
|
|
139
|
+
//# sourceMappingURL=chunk-33VIIDHK.js.map
|
|
@@ -0,0 +1 @@
|
|
|
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 | 'leased'\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:leased'; 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 | {\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:leased' }\n | { type: 'run:complete'; output: TOutput }\n | { type: 'run:fail'; error: string }\n | { type: 'run:cancel' }\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:leased':\n return { ...state, status: 'leased' }\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: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:leased':\n case 'run:cancel':\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/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { R as RunStatus, L as LogEntry, P as Progress, I as InferInput, a as InferOutput, T as TypedClientRun } from './types-
|
|
2
|
-
export { D as DurablyEvent } from './types-
|
|
1
|
+
import { R as RunStatus, L as LogEntry, P as Progress, I as InferInput, a as InferOutput, T as TypedClientRun } from './types-DMtqQ6Wp.js';
|
|
2
|
+
export { D as DurablyEvent } from './types-DMtqQ6Wp.js';
|
|
3
3
|
import { JobDefinition, ClientRun } from '@coji/durably';
|
|
4
4
|
export { ClientRun } from '@coji/durably';
|
|
5
5
|
|
|
@@ -18,7 +18,7 @@ interface UseJobClientOptions {
|
|
|
18
18
|
*/
|
|
19
19
|
initialRunId?: string;
|
|
20
20
|
/**
|
|
21
|
-
* Automatically resume tracking a
|
|
21
|
+
* Automatically resume tracking a leased/pending job on mount
|
|
22
22
|
* @default true
|
|
23
23
|
*/
|
|
24
24
|
autoResume?: boolean;
|
|
@@ -68,7 +68,7 @@ interface UseJobClientResult<TInput, TOutput> {
|
|
|
68
68
|
/**
|
|
69
69
|
* Whether a run is currently running
|
|
70
70
|
*/
|
|
71
|
-
|
|
71
|
+
isLeased: boolean;
|
|
72
72
|
/**
|
|
73
73
|
* Whether a run is pending
|
|
74
74
|
*/
|
|
@@ -182,7 +182,7 @@ interface UseJobRunClientResult<TOutput = unknown> {
|
|
|
182
182
|
/**
|
|
183
183
|
* Whether a run is currently running
|
|
184
184
|
*/
|
|
185
|
-
|
|
185
|
+
isLeased: boolean;
|
|
186
186
|
/**
|
|
187
187
|
* Whether a run is pending
|
|
188
188
|
*/
|
|
@@ -254,7 +254,7 @@ interface JobHooks<TInput, TOutput> {
|
|
|
254
254
|
*
|
|
255
255
|
* // In your component - fully type-safe
|
|
256
256
|
* function CsvImporter() {
|
|
257
|
-
* const { trigger, output, progress,
|
|
257
|
+
* const { trigger, output, progress, isLeased } = importCsv.useJob()
|
|
258
258
|
*
|
|
259
259
|
* return (
|
|
260
260
|
* <button onClick={() => trigger({ rows: [...] })}>
|
|
@@ -282,11 +282,11 @@ interface UseRunActionsClientOptions {
|
|
|
282
282
|
}
|
|
283
283
|
interface UseRunActionsClientResult {
|
|
284
284
|
/**
|
|
285
|
-
*
|
|
285
|
+
* Create a fresh run from a completed, failed, or cancelled run
|
|
286
286
|
*/
|
|
287
|
-
|
|
287
|
+
retrigger: (runId: string) => Promise<string>;
|
|
288
288
|
/**
|
|
289
|
-
* Cancel a pending or
|
|
289
|
+
* Cancel a pending or leased run
|
|
290
290
|
*/
|
|
291
291
|
cancel: (runId: string) => Promise<void>;
|
|
292
292
|
/**
|
|
@@ -311,23 +311,23 @@ interface UseRunActionsClientResult {
|
|
|
311
311
|
error: string | null;
|
|
312
312
|
}
|
|
313
313
|
/**
|
|
314
|
-
* Hook for run actions
|
|
314
|
+
* Hook for run actions via server API.
|
|
315
315
|
*
|
|
316
316
|
* @example
|
|
317
317
|
* ```tsx
|
|
318
318
|
* function RunActions({ runId, status }: { runId: string; status: string }) {
|
|
319
|
-
* const {
|
|
319
|
+
* const { retrigger, cancel, isLoading, error } = useRunActions({
|
|
320
320
|
* api: '/api/durably',
|
|
321
321
|
* })
|
|
322
322
|
*
|
|
323
323
|
* return (
|
|
324
324
|
* <div>
|
|
325
325
|
* {status === 'failed' && (
|
|
326
|
-
* <button onClick={() =>
|
|
327
|
-
*
|
|
326
|
+
* <button onClick={() => retrigger(runId)} disabled={isLoading}>
|
|
327
|
+
* Run Again
|
|
328
328
|
* </button>
|
|
329
329
|
* )}
|
|
330
|
-
* {(status === 'pending' || status === '
|
|
330
|
+
* {(status === 'pending' || status === 'leased') && (
|
|
331
331
|
* <button onClick={() => cancel(runId)} disabled={isLoading}>
|
|
332
332
|
* Cancel
|
|
333
333
|
* </button>
|
|
@@ -472,7 +472,7 @@ type DurablyClient<T> = {
|
|
|
472
472
|
*/
|
|
473
473
|
useRuns: <TInput extends Record<string, unknown> = Record<string, unknown>, TOutput extends Record<string, unknown> | undefined = Record<string, unknown> | undefined>(options?: Omit<UseRunsClientOptions, 'api'>) => UseRunsClientResult<TInput, TOutput>;
|
|
474
474
|
/**
|
|
475
|
-
* Run actions:
|
|
475
|
+
* Run actions: retrigger, cancel, delete, getRun, getSteps (cross-job).
|
|
476
476
|
* The `api` option is pre-configured.
|
|
477
477
|
*/
|
|
478
478
|
useRunActions: () => UseRunActionsClientResult;
|
|
@@ -504,14 +504,14 @@ type DurablyClient<T> = {
|
|
|
504
504
|
*
|
|
505
505
|
* // In your component — fully type-safe with autocomplete
|
|
506
506
|
* function CsvImporter() {
|
|
507
|
-
* const { trigger, output,
|
|
507
|
+
* const { trigger, output, isLeased } = durably.importCsv.useJob()
|
|
508
508
|
* return <button onClick={() => trigger({ rows: [...] })}>Import</button>
|
|
509
509
|
* }
|
|
510
510
|
*
|
|
511
511
|
* // Cross-job hooks
|
|
512
512
|
* function Dashboard() {
|
|
513
513
|
* const { runs, nextPage } = durably.useRuns({ pageSize: 10 })
|
|
514
|
-
* const {
|
|
514
|
+
* const { retrigger, cancel } = durably.useRunActions()
|
|
515
515
|
* }
|
|
516
516
|
* ```
|
|
517
517
|
*/
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
isJobDefinition,
|
|
3
3
|
useSubscription
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-33VIIDHK.js";
|
|
5
5
|
|
|
6
6
|
// src/client/use-job.ts
|
|
7
7
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
@@ -20,8 +20,8 @@ function createSSEEventSubscriber(apiBaseUrl) {
|
|
|
20
20
|
const data = JSON.parse(messageEvent.data);
|
|
21
21
|
if (data.runId !== runId) return;
|
|
22
22
|
switch (data.type) {
|
|
23
|
-
case "run:
|
|
24
|
-
onEvent({ type: "run:
|
|
23
|
+
case "run:leased":
|
|
24
|
+
onEvent({ type: "run:leased" });
|
|
25
25
|
break;
|
|
26
26
|
case "run:complete":
|
|
27
27
|
onEvent({
|
|
@@ -35,9 +35,6 @@ function createSSEEventSubscriber(apiBaseUrl) {
|
|
|
35
35
|
case "run:cancel":
|
|
36
36
|
onEvent({ type: "run:cancel" });
|
|
37
37
|
break;
|
|
38
|
-
case "run:retry":
|
|
39
|
-
onEvent({ type: "run:retry" });
|
|
40
|
-
break;
|
|
41
38
|
case "run:progress":
|
|
42
39
|
onEvent({ type: "run:progress", progress: data.progress });
|
|
43
40
|
break;
|
|
@@ -56,8 +53,9 @@ function createSSEEventSubscriber(apiBaseUrl) {
|
|
|
56
53
|
}
|
|
57
54
|
};
|
|
58
55
|
eventSource.onerror = () => {
|
|
59
|
-
|
|
60
|
-
|
|
56
|
+
if (eventSource.readyState === EventSource.CLOSED) {
|
|
57
|
+
onEvent({ type: "connection_error", error: "Connection failed" });
|
|
58
|
+
}
|
|
61
59
|
};
|
|
62
60
|
return () => {
|
|
63
61
|
eventSource.close();
|
|
@@ -99,9 +97,9 @@ function useJob(options) {
|
|
|
99
97
|
const abortController = new AbortController();
|
|
100
98
|
const findActiveRun = async () => {
|
|
101
99
|
const signal = abortController.signal;
|
|
102
|
-
const [
|
|
100
|
+
const [leasedRes, pendingRes] = await Promise.all([
|
|
103
101
|
fetch(
|
|
104
|
-
`${api}/runs?${new URLSearchParams({ jobName, status: "
|
|
102
|
+
`${api}/runs?${new URLSearchParams({ jobName, status: "leased", limit: "1" })}`,
|
|
105
103
|
{ signal }
|
|
106
104
|
),
|
|
107
105
|
fetch(
|
|
@@ -110,8 +108,8 @@ function useJob(options) {
|
|
|
110
108
|
)
|
|
111
109
|
]);
|
|
112
110
|
if (hasUserTriggered.current) return;
|
|
113
|
-
if (
|
|
114
|
-
const runs = await
|
|
111
|
+
if (leasedRes.ok) {
|
|
112
|
+
const runs = await leasedRes.json();
|
|
115
113
|
if (runs.length > 0) {
|
|
116
114
|
setCurrentRunId(runs[0].id);
|
|
117
115
|
return;
|
|
@@ -140,7 +138,7 @@ function useJob(options) {
|
|
|
140
138
|
eventSource.onmessage = (event) => {
|
|
141
139
|
try {
|
|
142
140
|
const data = JSON.parse(event.data);
|
|
143
|
-
if ((data.type === "run:trigger" || data.type === "run:
|
|
141
|
+
if ((data.type === "run:trigger" || data.type === "run:leased") && data.runId) {
|
|
144
142
|
setCurrentRunId(data.runId);
|
|
145
143
|
}
|
|
146
144
|
} catch {
|
|
@@ -229,7 +227,7 @@ function useJob(options) {
|
|
|
229
227
|
error: subscription.error,
|
|
230
228
|
logs: subscription.logs,
|
|
231
229
|
progress: subscription.progress,
|
|
232
|
-
|
|
230
|
+
isLeased: effectiveStatus === "leased",
|
|
233
231
|
isPending: effectiveStatus === "pending",
|
|
234
232
|
isCompleted: effectiveStatus === "completed",
|
|
235
233
|
isFailed: effectiveStatus === "failed",
|
|
@@ -258,14 +256,14 @@ function useJobRun(options) {
|
|
|
258
256
|
const isCompleted = effectiveStatus === "completed";
|
|
259
257
|
const isFailed = effectiveStatus === "failed";
|
|
260
258
|
const isPending = effectiveStatus === "pending";
|
|
261
|
-
const
|
|
259
|
+
const isLeased = effectiveStatus === "leased";
|
|
262
260
|
const isCancelled = effectiveStatus === "cancelled";
|
|
263
261
|
const prevStatusRef = useRef2(null);
|
|
264
262
|
useEffect2(() => {
|
|
265
263
|
const prevStatus = prevStatusRef.current;
|
|
266
264
|
prevStatusRef.current = effectiveStatus;
|
|
267
265
|
if (prevStatus !== effectiveStatus) {
|
|
268
|
-
if (prevStatus === null && (isPending ||
|
|
266
|
+
if (prevStatus === null && (isPending || isLeased) && onStart) {
|
|
269
267
|
onStart();
|
|
270
268
|
}
|
|
271
269
|
if (isCompleted && onComplete) {
|
|
@@ -278,7 +276,7 @@ function useJobRun(options) {
|
|
|
278
276
|
}, [
|
|
279
277
|
effectiveStatus,
|
|
280
278
|
isPending,
|
|
281
|
-
|
|
279
|
+
isLeased,
|
|
282
280
|
isCompleted,
|
|
283
281
|
isFailed,
|
|
284
282
|
onStart,
|
|
@@ -291,7 +289,7 @@ function useJobRun(options) {
|
|
|
291
289
|
error: subscription.error,
|
|
292
290
|
logs: subscription.logs,
|
|
293
291
|
progress: subscription.progress,
|
|
294
|
-
|
|
292
|
+
isLeased,
|
|
295
293
|
isPending,
|
|
296
294
|
isCompleted,
|
|
297
295
|
isFailed,
|
|
@@ -321,15 +319,14 @@ function useRunActions(options) {
|
|
|
321
319
|
const { api } = options;
|
|
322
320
|
const [isLoading, setIsLoading] = useState2(false);
|
|
323
321
|
const [error, setError] = useState2(null);
|
|
324
|
-
const
|
|
325
|
-
async (
|
|
322
|
+
const executeAction = useCallback2(
|
|
323
|
+
async (url, actionName, init) => {
|
|
326
324
|
setIsLoading(true);
|
|
327
325
|
setError(null);
|
|
328
326
|
try {
|
|
329
|
-
const
|
|
330
|
-
const response = await fetch(url, { method: "POST" });
|
|
327
|
+
const response = await fetch(url, init);
|
|
331
328
|
if (!response.ok) {
|
|
332
|
-
let errorMessage = `Failed to
|
|
329
|
+
let errorMessage = `Failed to ${actionName}: ${response.statusText}`;
|
|
333
330
|
try {
|
|
334
331
|
const data = await response.json();
|
|
335
332
|
if (data.error) {
|
|
@@ -339,6 +336,7 @@ function useRunActions(options) {
|
|
|
339
336
|
}
|
|
340
337
|
throw new Error(errorMessage);
|
|
341
338
|
}
|
|
339
|
+
return await response.json();
|
|
342
340
|
} catch (err) {
|
|
343
341
|
const message = err instanceof Error ? err.message : "Unknown error";
|
|
344
342
|
setError(message);
|
|
@@ -347,71 +345,50 @@ function useRunActions(options) {
|
|
|
347
345
|
setIsLoading(false);
|
|
348
346
|
}
|
|
349
347
|
},
|
|
350
|
-
[
|
|
348
|
+
[]
|
|
351
349
|
);
|
|
352
|
-
const
|
|
350
|
+
const retrigger = useCallback2(
|
|
353
351
|
async (runId) => {
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
const data = await response.json();
|
|
363
|
-
if (data.error) {
|
|
364
|
-
errorMessage = data.error;
|
|
365
|
-
}
|
|
366
|
-
} catch {
|
|
367
|
-
}
|
|
368
|
-
throw new Error(errorMessage);
|
|
369
|
-
}
|
|
370
|
-
} catch (err) {
|
|
371
|
-
const message = err instanceof Error ? err.message : "Unknown error";
|
|
352
|
+
const enc = encodeURIComponent(runId);
|
|
353
|
+
const data = await executeAction(
|
|
354
|
+
`${api}/retrigger?runId=${enc}`,
|
|
355
|
+
"retrigger",
|
|
356
|
+
{ method: "POST" }
|
|
357
|
+
);
|
|
358
|
+
if (!data.runId) {
|
|
359
|
+
const message = "Failed to retrigger: missing runId in response";
|
|
372
360
|
setError(message);
|
|
373
|
-
throw
|
|
374
|
-
} finally {
|
|
375
|
-
setIsLoading(false);
|
|
361
|
+
throw new Error(message);
|
|
376
362
|
}
|
|
363
|
+
return data.runId;
|
|
377
364
|
},
|
|
378
|
-
[api]
|
|
365
|
+
[api, executeAction]
|
|
366
|
+
);
|
|
367
|
+
const cancel = useCallback2(
|
|
368
|
+
async (runId) => {
|
|
369
|
+
const enc = encodeURIComponent(runId);
|
|
370
|
+
await executeAction(`${api}/cancel?runId=${enc}`, "cancel", {
|
|
371
|
+
method: "POST"
|
|
372
|
+
});
|
|
373
|
+
},
|
|
374
|
+
[api, executeAction]
|
|
379
375
|
);
|
|
380
376
|
const deleteRun = useCallback2(
|
|
381
377
|
async (runId) => {
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
const response = await fetch(url, { method: "DELETE" });
|
|
387
|
-
if (!response.ok) {
|
|
388
|
-
let errorMessage = `Failed to delete: ${response.statusText}`;
|
|
389
|
-
try {
|
|
390
|
-
const data = await response.json();
|
|
391
|
-
if (data.error) {
|
|
392
|
-
errorMessage = data.error;
|
|
393
|
-
}
|
|
394
|
-
} catch {
|
|
395
|
-
}
|
|
396
|
-
throw new Error(errorMessage);
|
|
397
|
-
}
|
|
398
|
-
} catch (err) {
|
|
399
|
-
const message = err instanceof Error ? err.message : "Unknown error";
|
|
400
|
-
setError(message);
|
|
401
|
-
throw err;
|
|
402
|
-
} finally {
|
|
403
|
-
setIsLoading(false);
|
|
404
|
-
}
|
|
378
|
+
const enc = encodeURIComponent(runId);
|
|
379
|
+
await executeAction(`${api}/run?runId=${enc}`, "delete", {
|
|
380
|
+
method: "DELETE"
|
|
381
|
+
});
|
|
405
382
|
},
|
|
406
|
-
[api]
|
|
383
|
+
[api, executeAction]
|
|
407
384
|
);
|
|
408
385
|
const getRun = useCallback2(
|
|
409
386
|
async (runId) => {
|
|
410
387
|
setIsLoading(true);
|
|
411
388
|
setError(null);
|
|
412
389
|
try {
|
|
413
|
-
const
|
|
414
|
-
const response = await fetch(
|
|
390
|
+
const enc = encodeURIComponent(runId);
|
|
391
|
+
const response = await fetch(`${api}/run?runId=${enc}`);
|
|
415
392
|
if (response.status === 404) {
|
|
416
393
|
return null;
|
|
417
394
|
}
|
|
@@ -439,35 +416,16 @@ function useRunActions(options) {
|
|
|
439
416
|
);
|
|
440
417
|
const getSteps = useCallback2(
|
|
441
418
|
async (runId) => {
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
if (!response.ok) {
|
|
448
|
-
let errorMessage = `Failed to get steps: ${response.statusText}`;
|
|
449
|
-
try {
|
|
450
|
-
const data = await response.json();
|
|
451
|
-
if (data.error) {
|
|
452
|
-
errorMessage = data.error;
|
|
453
|
-
}
|
|
454
|
-
} catch {
|
|
455
|
-
}
|
|
456
|
-
throw new Error(errorMessage);
|
|
457
|
-
}
|
|
458
|
-
return await response.json();
|
|
459
|
-
} catch (err) {
|
|
460
|
-
const message = err instanceof Error ? err.message : "Unknown error";
|
|
461
|
-
setError(message);
|
|
462
|
-
throw err;
|
|
463
|
-
} finally {
|
|
464
|
-
setIsLoading(false);
|
|
465
|
-
}
|
|
419
|
+
const enc = encodeURIComponent(runId);
|
|
420
|
+
return executeAction(
|
|
421
|
+
`${api}/steps?runId=${enc}`,
|
|
422
|
+
"get steps"
|
|
423
|
+
);
|
|
466
424
|
},
|
|
467
|
-
[api]
|
|
425
|
+
[api, executeAction]
|
|
468
426
|
);
|
|
469
427
|
return {
|
|
470
|
-
|
|
428
|
+
retrigger,
|
|
471
429
|
cancel,
|
|
472
430
|
deleteRun,
|
|
473
431
|
getRun,
|
|
@@ -555,7 +513,7 @@ function useRuns(jobDefinitionOrOptions, optionsArg) {
|
|
|
555
513
|
eventSource.onmessage = (event) => {
|
|
556
514
|
try {
|
|
557
515
|
const data = JSON.parse(event.data);
|
|
558
|
-
if (data.type === "run:trigger" || data.type === "run:
|
|
516
|
+
if (data.type === "run:trigger" || data.type === "run:leased" || data.type === "run:complete" || data.type === "run:fail" || data.type === "run:cancel" || data.type === "run:delete") {
|
|
559
517
|
refresh();
|
|
560
518
|
}
|
|
561
519
|
if (data.type === "run:progress") {
|