@duraton/react 0.1.0-beta.7
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/LICENSE +202 -0
- package/NOTICE +5 -0
- package/dist/index.d.ts +113 -0
- package/dist/index.js +477 -0
- package/dist/index.js.map +1 -0
- package/package.json +74 -0
- package/src/hooks/use-apps.ts +62 -0
- package/src/hooks/use-control-actions.ts +11 -0
- package/src/hooks/use-deliveries.ts +56 -0
- package/src/hooks/use-events.ts +54 -0
- package/src/hooks/use-flow-state.ts +17 -0
- package/src/hooks/use-run-controls.ts +68 -0
- package/src/hooks/use-run-stats.ts +14 -0
- package/src/hooks/use-run-stream.ts +74 -0
- package/src/hooks/use-run-timeseries.ts +15 -0
- package/src/hooks/use-run.ts +27 -0
- package/src/hooks/use-runners.ts +15 -0
- package/src/hooks/use-runs-stream.ts +45 -0
- package/src/hooks/use-runs.ts +17 -0
- package/src/hooks/use-webhook-config.ts +92 -0
- package/src/hooks/use-workflows.ts +14 -0
- package/src/hooks.test.tsx +87 -0
- package/src/index.ts +111 -0
- package/src/lib/stream.ts +18 -0
- package/src/provider.tsx +22 -0
- package/src/run-controls.test.tsx +76 -0
- package/src/run-status.test.ts +23 -0
- package/src/run-status.ts +8 -0
- package/src/test-utils.tsx +105 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
export { DuratonProvider, useDuratonClient } from "./provider";
|
|
2
|
+
export { ACTIVE_STATUSES, TERMINAL_STATUSES } from "./run-status";
|
|
3
|
+
export { useRuns } from "./hooks/use-runs";
|
|
4
|
+
export { useRunStats } from "./hooks/use-run-stats";
|
|
5
|
+
export { useRunsStream } from "./hooks/use-runs-stream";
|
|
6
|
+
export { useRun, useRunSteps } from "./hooks/use-run";
|
|
7
|
+
export { useRunStream } from "./hooks/use-run-stream";
|
|
8
|
+
export { useWorkflows } from "./hooks/use-workflows";
|
|
9
|
+
export {
|
|
10
|
+
useCancelRun,
|
|
11
|
+
usePauseRun,
|
|
12
|
+
useResumeRun,
|
|
13
|
+
useReplayRun,
|
|
14
|
+
useRetryFromStep,
|
|
15
|
+
useBulkReplay,
|
|
16
|
+
} from "./hooks/use-run-controls";
|
|
17
|
+
export { useControlActions } from "./hooks/use-control-actions";
|
|
18
|
+
export { useRunTimeSeries } from "./hooks/use-run-timeseries";
|
|
19
|
+
export { useFlowState } from "./hooks/use-flow-state";
|
|
20
|
+
export { useRunners } from "./hooks/use-runners";
|
|
21
|
+
export { useApps } from "./hooks/use-apps";
|
|
22
|
+
export type { AppInfo, AppStatus, AppWorkflow } from "./hooks/use-apps";
|
|
23
|
+
export { useEvents, useTriggerEvent } from "./hooks/use-events";
|
|
24
|
+
export {
|
|
25
|
+
useDeliveries,
|
|
26
|
+
useDelivery,
|
|
27
|
+
useEndpoints,
|
|
28
|
+
useReceivers,
|
|
29
|
+
useEndpointStats,
|
|
30
|
+
} from "./hooks/use-deliveries";
|
|
31
|
+
export {
|
|
32
|
+
useCreateEndpoint,
|
|
33
|
+
useUpdateEndpoint,
|
|
34
|
+
useDeleteEndpoint,
|
|
35
|
+
useCreateReceiver,
|
|
36
|
+
useUpdateReceiver,
|
|
37
|
+
useDeleteReceiver,
|
|
38
|
+
useRedeliverDelivery,
|
|
39
|
+
} from "./hooks/use-webhook-config";
|
|
40
|
+
|
|
41
|
+
export {
|
|
42
|
+
createClient,
|
|
43
|
+
RUN_STATUSES,
|
|
44
|
+
STEP_STATUSES,
|
|
45
|
+
CONTROL_ACTION_KINDS,
|
|
46
|
+
DELIVERY_STATUSES,
|
|
47
|
+
DELIVERY_EVENT_KINDS,
|
|
48
|
+
SUBSCRIBABLE_EVENT_KINDS,
|
|
49
|
+
ATTEMPT_OUTCOMES,
|
|
50
|
+
SIGNING_SCHEMES,
|
|
51
|
+
} from "@duraton/sdk/client";
|
|
52
|
+
export type {
|
|
53
|
+
ClientOptions,
|
|
54
|
+
DuratonClient,
|
|
55
|
+
Run,
|
|
56
|
+
Step,
|
|
57
|
+
StepErrorInfo,
|
|
58
|
+
RunStatus,
|
|
59
|
+
StepStatus,
|
|
60
|
+
LogLevel,
|
|
61
|
+
TriggerKind,
|
|
62
|
+
RunStats,
|
|
63
|
+
RunStatsOptions,
|
|
64
|
+
ListRunsOptions,
|
|
65
|
+
RunsPage,
|
|
66
|
+
LogFrame,
|
|
67
|
+
TimelineFrame,
|
|
68
|
+
RunStatusFrame,
|
|
69
|
+
StepStatusFrame,
|
|
70
|
+
WorkflowDef,
|
|
71
|
+
WorkflowSchedule,
|
|
72
|
+
WorkflowTrigger,
|
|
73
|
+
Runner,
|
|
74
|
+
EventSource,
|
|
75
|
+
EventLogRecord,
|
|
76
|
+
TriggeredOutcome,
|
|
77
|
+
SendEventInput,
|
|
78
|
+
SendEventResult,
|
|
79
|
+
FlowControl,
|
|
80
|
+
RunSeries,
|
|
81
|
+
RunSeriesBucket,
|
|
82
|
+
RunTimeSeriesOptions,
|
|
83
|
+
FlowState,
|
|
84
|
+
FlowStateOptions,
|
|
85
|
+
DebounceBacklog,
|
|
86
|
+
BatchBacklog,
|
|
87
|
+
ControlAction,
|
|
88
|
+
ControlActionKind,
|
|
89
|
+
ListControlActionsOptions,
|
|
90
|
+
BulkReplayFilter,
|
|
91
|
+
BulkReplayResult,
|
|
92
|
+
DeliveryStatus,
|
|
93
|
+
DeliveryEventKind,
|
|
94
|
+
SubscribableEventKind,
|
|
95
|
+
AttemptOutcome,
|
|
96
|
+
SigningScheme,
|
|
97
|
+
WebhookDelivery,
|
|
98
|
+
WebhookDeliveryAttempt,
|
|
99
|
+
WebhookDeliveryDetail,
|
|
100
|
+
WebhookEndpoint,
|
|
101
|
+
WebhookReceiver,
|
|
102
|
+
WebhookEndpointStats,
|
|
103
|
+
WebhookEndpointWithSecret,
|
|
104
|
+
WebhookReceiverWithSecret,
|
|
105
|
+
EndpointInput,
|
|
106
|
+
EndpointPatch,
|
|
107
|
+
ReceiverInput,
|
|
108
|
+
ReceiverPatch,
|
|
109
|
+
ListWebhookDeliveriesOptions,
|
|
110
|
+
WebhookDeliveriesPage,
|
|
111
|
+
} from "@duraton/sdk/client";
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// Backoff before reconnecting a dropped (non-terminal) SSE stream.
|
|
2
|
+
export const RECONNECT_MS = 1000;
|
|
3
|
+
|
|
4
|
+
// Resolves after ms, or immediately when the signal aborts (clearing its timer
|
|
5
|
+
// either way). Used by the SSE reconnect loops.
|
|
6
|
+
export function abortableDelay(ms: number, signal: AbortSignal): Promise<void> {
|
|
7
|
+
return new Promise((resolve) => {
|
|
8
|
+
const timer = setTimeout(resolve, ms);
|
|
9
|
+
signal.addEventListener(
|
|
10
|
+
"abort",
|
|
11
|
+
() => {
|
|
12
|
+
clearTimeout(timer);
|
|
13
|
+
resolve();
|
|
14
|
+
},
|
|
15
|
+
{ once: true },
|
|
16
|
+
);
|
|
17
|
+
});
|
|
18
|
+
}
|
package/src/provider.tsx
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { createContext, useContext, type ReactNode } from "react";
|
|
2
|
+
import type { DuratonClient } from "@duraton/sdk/client";
|
|
3
|
+
|
|
4
|
+
const ClientContext = createContext<DuratonClient | null>(null);
|
|
5
|
+
|
|
6
|
+
export function DuratonProvider({
|
|
7
|
+
client,
|
|
8
|
+
children,
|
|
9
|
+
}: {
|
|
10
|
+
client: DuratonClient;
|
|
11
|
+
children: ReactNode;
|
|
12
|
+
}) {
|
|
13
|
+
return <ClientContext.Provider value={client}>{children}</ClientContext.Provider>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function useDuratonClient(): DuratonClient {
|
|
17
|
+
const client = useContext(ClientContext);
|
|
18
|
+
if (!client) {
|
|
19
|
+
throw new Error("useDuratonClient must be used within a DuratonProvider");
|
|
20
|
+
}
|
|
21
|
+
return client;
|
|
22
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import { describe, expect, it, vi } from "vitest";
|
|
3
|
+
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
4
|
+
import { renderHook, waitFor } from "@testing-library/react";
|
|
5
|
+
import type { DuratonClient, Run } from "@duraton/sdk/client";
|
|
6
|
+
import { DuratonProvider } from "./provider";
|
|
7
|
+
import { useControlActions } from "./hooks/use-control-actions";
|
|
8
|
+
import { useBulkReplay, useCancelRun, useReplayRun } from "./hooks/use-run-controls";
|
|
9
|
+
import { fakeClient, makeControlAction, makeRun } from "./test-utils";
|
|
10
|
+
|
|
11
|
+
function harness(client: DuratonClient) {
|
|
12
|
+
const qc = new QueryClient({ defaultOptions: { queries: { retry: false } } });
|
|
13
|
+
const wrapper = ({ children }: { children: ReactNode }) => (
|
|
14
|
+
<QueryClientProvider client={qc}>
|
|
15
|
+
<DuratonProvider client={client}>{children}</DuratonProvider>
|
|
16
|
+
</QueryClientProvider>
|
|
17
|
+
);
|
|
18
|
+
return { qc, wrapper };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
describe("useControlActions", () => {
|
|
22
|
+
it("returns a run's control-action history", async () => {
|
|
23
|
+
const client = fakeClient({ controlActions: [makeControlAction({ id: "a1" })] });
|
|
24
|
+
const { wrapper } = harness(client);
|
|
25
|
+
const { result } = renderHook(() => useControlActions({ runId: "run_1" }), { wrapper });
|
|
26
|
+
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
|
27
|
+
expect(result.current.data?.[0]?.id).toBe("a1");
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
describe("useCancelRun", () => {
|
|
32
|
+
it("writes the updated run into the cache and invalidates the list", async () => {
|
|
33
|
+
const cancel = vi.fn(async () => makeRun({ id: "run_1", status: "cancelled" }));
|
|
34
|
+
const { qc, wrapper } = harness(fakeClient({ cancel }));
|
|
35
|
+
qc.setQueryData(["runs"], { runs: [], nextCursor: null });
|
|
36
|
+
const { result } = renderHook(() => useCancelRun(), { wrapper });
|
|
37
|
+
|
|
38
|
+
await result.current.mutateAsync("run_1");
|
|
39
|
+
|
|
40
|
+
expect(cancel).toHaveBeenCalledWith("run_1");
|
|
41
|
+
expect((qc.getQueryData(["run", "run_1"]) as Run).status).toBe("cancelled");
|
|
42
|
+
expect(qc.getQueryState(["runs"])?.isInvalidated).toBe(true);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe("useReplayRun", () => {
|
|
47
|
+
it("forks with the optional edited input", async () => {
|
|
48
|
+
const replay = vi.fn(async () => makeRun({ id: "run_fork" }));
|
|
49
|
+
const { wrapper } = harness(fakeClient({ replay }));
|
|
50
|
+
const { result } = renderHook(() => useReplayRun(), { wrapper });
|
|
51
|
+
|
|
52
|
+
const forked = await result.current.mutateAsync({ runId: "run_1", input: { a: 1 } });
|
|
53
|
+
|
|
54
|
+
expect(replay).toHaveBeenCalledWith("run_1", { a: 1 });
|
|
55
|
+
expect(forked.id).toBe("run_fork");
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe("useBulkReplay", () => {
|
|
60
|
+
it("replays the filter and returns the outcome counts", async () => {
|
|
61
|
+
const bulkReplay = vi.fn(async () => ({
|
|
62
|
+
matched: 4,
|
|
63
|
+
replayed: 4,
|
|
64
|
+
skipped: 0,
|
|
65
|
+
failed: 0,
|
|
66
|
+
capped: false,
|
|
67
|
+
}));
|
|
68
|
+
const { wrapper } = harness(fakeClient({ bulkReplay }));
|
|
69
|
+
const { result } = renderHook(() => useBulkReplay(), { wrapper });
|
|
70
|
+
|
|
71
|
+
const res = await result.current.mutateAsync({ app: "default" });
|
|
72
|
+
|
|
73
|
+
expect(bulkReplay).toHaveBeenCalledWith({ app: "default" });
|
|
74
|
+
expect(res.replayed).toBe(4);
|
|
75
|
+
});
|
|
76
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { ACTIVE_STATUSES, TERMINAL_STATUSES } from "./run-status";
|
|
3
|
+
|
|
4
|
+
describe("run status sets", () => {
|
|
5
|
+
it("treats queued/running/waiting as active, not terminal", () => {
|
|
6
|
+
for (const s of ["queued", "running", "waiting"] as const) {
|
|
7
|
+
expect(ACTIVE_STATUSES.has(s)).toBe(true);
|
|
8
|
+
expect(TERMINAL_STATUSES.has(s)).toBe(false);
|
|
9
|
+
}
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it("treats succeeded/failed/cancelled as terminal, not active", () => {
|
|
13
|
+
for (const s of ["succeeded", "failed", "cancelled"] as const) {
|
|
14
|
+
expect(TERMINAL_STATUSES.has(s)).toBe(true);
|
|
15
|
+
expect(ACTIVE_STATUSES.has(s)).toBe(false);
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("treats paused as neither active nor terminal", () => {
|
|
20
|
+
expect(ACTIVE_STATUSES.has("paused")).toBe(false);
|
|
21
|
+
expect(TERMINAL_STATUSES.has("paused")).toBe(false);
|
|
22
|
+
});
|
|
23
|
+
});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { RunStatus } from "@duraton/sdk/client";
|
|
2
|
+
|
|
3
|
+
export const ACTIVE_STATUSES: ReadonlySet<RunStatus> = new Set(["queued", "running", "waiting"]);
|
|
4
|
+
export const TERMINAL_STATUSES: ReadonlySet<RunStatus> = new Set([
|
|
5
|
+
"succeeded",
|
|
6
|
+
"failed",
|
|
7
|
+
"cancelled",
|
|
8
|
+
]);
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
3
|
+
import type {
|
|
4
|
+
BulkReplayResult,
|
|
5
|
+
ControlAction,
|
|
6
|
+
DuratonClient,
|
|
7
|
+
Run,
|
|
8
|
+
RunStats,
|
|
9
|
+
Step,
|
|
10
|
+
TimelineFrame,
|
|
11
|
+
WorkflowDef,
|
|
12
|
+
} from "@duraton/sdk/client";
|
|
13
|
+
import { DuratonProvider } from "./provider";
|
|
14
|
+
|
|
15
|
+
export function makeRun(overrides: Partial<Run> = {}): Run {
|
|
16
|
+
return {
|
|
17
|
+
id: "run_1",
|
|
18
|
+
workflowName: "sendEmail",
|
|
19
|
+
app: "default",
|
|
20
|
+
status: "succeeded",
|
|
21
|
+
errorCount: 0,
|
|
22
|
+
attempt: 1,
|
|
23
|
+
triggerKind: "event",
|
|
24
|
+
stepCount: 0,
|
|
25
|
+
currentStepIndex: -1,
|
|
26
|
+
startedAt: "2026-07-01T00:00:00.000Z",
|
|
27
|
+
...overrides,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function makeStats(overrides: Partial<RunStats> = {}): RunStats {
|
|
32
|
+
return {
|
|
33
|
+
total: 0,
|
|
34
|
+
active: 0,
|
|
35
|
+
queued: 0,
|
|
36
|
+
running: 0,
|
|
37
|
+
succeeded: 0,
|
|
38
|
+
failed: 0,
|
|
39
|
+
successRate: 0,
|
|
40
|
+
...overrides,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function* framesOf(frames: TimelineFrame[]): AsyncGenerator<TimelineFrame> {
|
|
45
|
+
for (const f of frames) yield f;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function makeControlAction(overrides: Partial<ControlAction> = {}): ControlAction {
|
|
49
|
+
return { id: "act_1", action: "replay", createdAt: "2026-07-01T00:00:00.000Z", ...overrides };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const DEFAULT_BULK: BulkReplayResult = {
|
|
53
|
+
matched: 0,
|
|
54
|
+
replayed: 0,
|
|
55
|
+
skipped: 0,
|
|
56
|
+
failed: 0,
|
|
57
|
+
capped: false,
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export interface FakeClientOptions {
|
|
61
|
+
runs?: Run[];
|
|
62
|
+
run?: Run;
|
|
63
|
+
steps?: Step[];
|
|
64
|
+
stats?: RunStats;
|
|
65
|
+
workflows?: WorkflowDef[];
|
|
66
|
+
frames?: TimelineFrame[];
|
|
67
|
+
controlActions?: ControlAction[];
|
|
68
|
+
cancel?: (id: string) => Promise<Run>;
|
|
69
|
+
pause?: (id: string) => Promise<Run>;
|
|
70
|
+
resume?: (id: string) => Promise<Run>;
|
|
71
|
+
replay?: (id: string, input?: unknown) => Promise<Run>;
|
|
72
|
+
retryFromStep?: (id: string, step: string) => Promise<Run>;
|
|
73
|
+
bulkReplay?: (filter: unknown) => Promise<BulkReplayResult>;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function fakeClient(opts: FakeClientOptions = {}): DuratonClient {
|
|
77
|
+
const run = async () => opts.run ?? makeRun();
|
|
78
|
+
return {
|
|
79
|
+
runs: {
|
|
80
|
+
list: async () => ({ runs: opts.runs ?? [], nextCursor: null }),
|
|
81
|
+
get: async () => opts.run ?? makeRun(),
|
|
82
|
+
steps: async () => opts.steps ?? [],
|
|
83
|
+
stats: async () => opts.stats ?? makeStats(),
|
|
84
|
+
watch: () => framesOf(opts.frames ?? []),
|
|
85
|
+
watchAll: () => framesOf(opts.frames ?? []),
|
|
86
|
+
cancel: opts.cancel ?? run,
|
|
87
|
+
pause: opts.pause ?? run,
|
|
88
|
+
resume: opts.resume ?? run,
|
|
89
|
+
replay: opts.replay ?? run,
|
|
90
|
+
retryFromStep: opts.retryFromStep ?? run,
|
|
91
|
+
bulkReplay: opts.bulkReplay ?? (async () => DEFAULT_BULK),
|
|
92
|
+
controlActions: async () => opts.controlActions ?? [],
|
|
93
|
+
},
|
|
94
|
+
workflows: { list: async () => opts.workflows ?? [] },
|
|
95
|
+
} as unknown as DuratonClient;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function wrapper(client: DuratonClient) {
|
|
99
|
+
const qc = new QueryClient({ defaultOptions: { queries: { retry: false } } });
|
|
100
|
+
return ({ children }: { children: ReactNode }) => (
|
|
101
|
+
<QueryClientProvider client={qc}>
|
|
102
|
+
<DuratonProvider client={client}>{children}</DuratonProvider>
|
|
103
|
+
</QueryClientProvider>
|
|
104
|
+
);
|
|
105
|
+
}
|