@coji/durably-react 0.14.0 → 0.15.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/{chunk-33VIIDHK.js → chunk-IXJA5WWJ.js} +12 -1
- package/dist/chunk-IXJA5WWJ.js.map +1 -0
- package/dist/index.d.ts +46 -25
- package/dist/index.js +73 -102
- package/dist/index.js.map +1 -1
- package/dist/spa.d.ts +24 -7
- package/dist/spa.js +29 -17
- package/dist/spa.js.map +1 -1
- package/dist/{types-DMtqQ6Wp.d.ts → types-CQltMEdB.d.ts} +10 -4
- package/package.json +2 -2
- package/dist/chunk-33VIIDHK.js.map +0 -1
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
// src/shared/use-stable-value.ts
|
|
2
|
+
import { useMemo } from "react";
|
|
3
|
+
function useStableValue(value) {
|
|
4
|
+
const key = value !== void 0 ? JSON.stringify(value) : void 0;
|
|
5
|
+
return useMemo(
|
|
6
|
+
() => key !== void 0 ? JSON.parse(key) : void 0,
|
|
7
|
+
[key]
|
|
8
|
+
);
|
|
9
|
+
}
|
|
10
|
+
|
|
1
11
|
// src/types.ts
|
|
2
12
|
function isJobDefinition(obj) {
|
|
3
13
|
return typeof obj === "object" && obj !== null && "name" in obj && "run" in obj && typeof obj.run === "function";
|
|
@@ -134,6 +144,7 @@ export {
|
|
|
134
144
|
initialSubscriptionState,
|
|
135
145
|
subscriptionReducer,
|
|
136
146
|
useSubscription,
|
|
147
|
+
useStableValue,
|
|
137
148
|
isJobDefinition
|
|
138
149
|
};
|
|
139
|
-
//# sourceMappingURL=chunk-
|
|
150
|
+
//# sourceMappingURL=chunk-IXJA5WWJ.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/shared/use-stable-value.ts","../src/types.ts","../src/shared/create-log-entry.ts","../src/shared/subscription-reducer.ts","../src/shared/use-subscription.ts"],"sourcesContent":["import { useMemo } from 'react'\n\n/**\n * Stabilize a value reference using JSON serialization.\n * Prevents re-render loops when callers pass inline arrays/objects.\n */\nexport function useStableValue<T>(value: T | undefined): T | undefined {\n const key = value !== undefined ? JSON.stringify(value) : undefined\n return useMemo(\n () => (key !== undefined ? (JSON.parse(key) as T) : undefined),\n [key],\n )\n}\n","// Shared type definitions for @coji/durably-react\n\nimport type { ClientRun, JobDefinition, Run, RunStatus } from '@coji/durably'\n\nexport type { RunStatus }\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 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:coalesced'\n runId: string\n jobName: string\n labels: Record<string, string>\n skippedInput: unknown\n skippedLabels: Record<string, string>\n }\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 = Record<string, unknown>,\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 = Record<string, unknown>,\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,7 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export { D as DurablyEvent } from './types-
|
|
3
|
-
import { JobDefinition, ClientRun } from '@coji/durably';
|
|
4
|
-
export { ClientRun } from '@coji/durably';
|
|
1
|
+
import { L as LogEntry, P as Progress, I as InferInput, a as InferOutput, T as TypedClientRun } from './types-CQltMEdB.js';
|
|
2
|
+
export { D as DurablyEvent } from './types-CQltMEdB.js';
|
|
3
|
+
import { RunStatus, JobDefinition, ClientRun } from '@coji/durably';
|
|
4
|
+
export { ClientRun, RunStatus } from '@coji/durably';
|
|
5
5
|
|
|
6
6
|
interface UseJobClientOptions {
|
|
7
7
|
/**
|
|
@@ -85,6 +85,14 @@ interface UseJobClientResult<TInput, TOutput> {
|
|
|
85
85
|
* Whether the run was cancelled
|
|
86
86
|
*/
|
|
87
87
|
isCancelled: boolean;
|
|
88
|
+
/**
|
|
89
|
+
* Whether the run reached a terminal status (completed, failed, or cancelled)
|
|
90
|
+
*/
|
|
91
|
+
isTerminal: boolean;
|
|
92
|
+
/**
|
|
93
|
+
* Whether the run is pending or leased (actively queued or executing)
|
|
94
|
+
*/
|
|
95
|
+
isActive: boolean;
|
|
88
96
|
/**
|
|
89
97
|
* Current run ID
|
|
90
98
|
*/
|
|
@@ -199,6 +207,14 @@ interface UseJobRunClientResult<TOutput = unknown> {
|
|
|
199
207
|
* Whether the run was cancelled
|
|
200
208
|
*/
|
|
201
209
|
isCancelled: boolean;
|
|
210
|
+
/**
|
|
211
|
+
* Whether the run reached a terminal status (completed, failed, or cancelled)
|
|
212
|
+
*/
|
|
213
|
+
isTerminal: boolean;
|
|
214
|
+
/**
|
|
215
|
+
* Whether the run is pending or leased (actively queued or executing)
|
|
216
|
+
*/
|
|
217
|
+
isActive: boolean;
|
|
202
218
|
}
|
|
203
219
|
/**
|
|
204
220
|
* Hook for subscribing to an existing run via server API.
|
|
@@ -226,17 +242,15 @@ interface JobHooks<TInput, TOutput> {
|
|
|
226
242
|
/**
|
|
227
243
|
* Hook for triggering and monitoring the job
|
|
228
244
|
*/
|
|
229
|
-
useJob: () => UseJobClientResult<TInput, TOutput>;
|
|
245
|
+
useJob: (options?: Omit<UseJobClientOptions, 'api' | 'jobName'>) => UseJobClientResult<TInput, TOutput>;
|
|
230
246
|
/**
|
|
231
247
|
* Hook for subscribing to an existing run by ID
|
|
232
248
|
*/
|
|
233
|
-
useRun: (runId: string | null) => UseJobRunClientResult<TOutput>;
|
|
249
|
+
useRun: (runId: string | null, options?: Omit<UseJobRunClientOptions, 'api' | 'runId'>) => UseJobRunClientResult<TOutput>;
|
|
234
250
|
/**
|
|
235
251
|
* Hook for subscribing to logs from a run
|
|
236
252
|
*/
|
|
237
|
-
useLogs: (runId: string | null, options?:
|
|
238
|
-
maxLogs?: number;
|
|
239
|
-
}) => UseJobLogsClientResult;
|
|
253
|
+
useLogs: (runId: string | null, options?: Omit<UseJobLogsClientOptions, 'api' | 'runId'>) => UseJobLogsClientResult;
|
|
240
254
|
}
|
|
241
255
|
/**
|
|
242
256
|
* Create type-safe hooks for a specific job.
|
|
@@ -301,14 +315,6 @@ interface UseRunActionsClientResult {
|
|
|
301
315
|
* Get steps for a run
|
|
302
316
|
*/
|
|
303
317
|
getSteps: (runId: string) => Promise<StepRecord[]>;
|
|
304
|
-
/**
|
|
305
|
-
* Whether an action is in progress
|
|
306
|
-
*/
|
|
307
|
-
isLoading: boolean;
|
|
308
|
-
/**
|
|
309
|
-
* Error message from last action
|
|
310
|
-
*/
|
|
311
|
-
error: string | null;
|
|
312
318
|
}
|
|
313
319
|
/**
|
|
314
320
|
* Hook for run actions via server API.
|
|
@@ -316,23 +322,38 @@ interface UseRunActionsClientResult {
|
|
|
316
322
|
* @example
|
|
317
323
|
* ```tsx
|
|
318
324
|
* function RunActions({ runId, status }: { runId: string; status: string }) {
|
|
319
|
-
* const { retrigger, cancel
|
|
325
|
+
* const { retrigger, cancel } = useRunActions({
|
|
320
326
|
* api: '/api/durably',
|
|
321
327
|
* })
|
|
328
|
+
* const [isPending, startTransition] = useTransition()
|
|
322
329
|
*
|
|
323
330
|
* return (
|
|
324
331
|
* <div>
|
|
325
332
|
* {status === 'failed' && (
|
|
326
|
-
* <button
|
|
333
|
+
* <button
|
|
334
|
+
* onClick={() =>
|
|
335
|
+
* startTransition(() =>
|
|
336
|
+
* // Handle errors in production (e.g. toast, local state)
|
|
337
|
+
* retrigger(runId).catch(console.error),
|
|
338
|
+
* )
|
|
339
|
+
* }
|
|
340
|
+
* disabled={isPending}
|
|
341
|
+
* >
|
|
327
342
|
* Run Again
|
|
328
343
|
* </button>
|
|
329
344
|
* )}
|
|
330
345
|
* {(status === 'pending' || status === 'leased') && (
|
|
331
|
-
* <button
|
|
346
|
+
* <button
|
|
347
|
+
* onClick={() =>
|
|
348
|
+
* startTransition(() =>
|
|
349
|
+
* cancel(runId).catch(console.error),
|
|
350
|
+
* )
|
|
351
|
+
* }
|
|
352
|
+
* disabled={isPending}
|
|
353
|
+
* >
|
|
332
354
|
* Cancel
|
|
333
355
|
* </button>
|
|
334
356
|
* )}
|
|
335
|
-
* {error && <span className="error">{error}</span>}
|
|
336
357
|
* </div>
|
|
337
358
|
* )
|
|
338
359
|
* }
|
|
@@ -350,9 +371,9 @@ interface UseRunsClientOptions {
|
|
|
350
371
|
*/
|
|
351
372
|
jobName?: string | string[];
|
|
352
373
|
/**
|
|
353
|
-
* Filter by status
|
|
374
|
+
* Filter by status(es). Pass one status, or an array for multiple (OR).
|
|
354
375
|
*/
|
|
355
|
-
status?: RunStatus;
|
|
376
|
+
status?: RunStatus | RunStatus[];
|
|
356
377
|
/**
|
|
357
378
|
* Filter by labels (all specified labels must match)
|
|
358
379
|
*/
|
|
@@ -368,7 +389,7 @@ interface UseRunsClientOptions {
|
|
|
368
389
|
*/
|
|
369
390
|
realtime?: boolean;
|
|
370
391
|
}
|
|
371
|
-
interface UseRunsClientResult<TInput extends Record<string, unknown> = Record<string, unknown>, TOutput extends Record<string, unknown> | undefined = Record<string, unknown
|
|
392
|
+
interface UseRunsClientResult<TInput extends Record<string, unknown> = Record<string, unknown>, TOutput extends Record<string, unknown> | undefined = Record<string, unknown>> {
|
|
372
393
|
/**
|
|
373
394
|
* List of runs for the current page
|
|
374
395
|
*/
|
|
@@ -517,4 +538,4 @@ type DurablyClient<T> = {
|
|
|
517
538
|
*/
|
|
518
539
|
declare function createDurably<T>(options: CreateDurablyOptions): DurablyClient<T>;
|
|
519
540
|
|
|
520
|
-
export { type CreateDurablyOptions, type CreateJobHooksOptions, type DurablyClient, type JobHooks, LogEntry, Progress,
|
|
541
|
+
export { type CreateDurablyOptions, type CreateJobHooksOptions, type DurablyClient, type JobHooks, LogEntry, Progress, type StepRecord, TypedClientRun, type UseJobClientOptions, type UseJobClientResult, type UseJobLogsClientOptions, type UseJobLogsClientResult, type UseJobRunClientOptions, type UseJobRunClientResult, type UseRunActionsClientOptions, type UseRunActionsClientResult, type UseRunsClientOptions, type UseRunsClientResult, createDurably, createJobHooks, useJob, useJobLogs, useJobRun, useRunActions, useRuns };
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import {
|
|
2
2
|
isJobDefinition,
|
|
3
|
+
useStableValue,
|
|
3
4
|
useSubscription
|
|
4
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-IXJA5WWJ.js";
|
|
5
6
|
|
|
6
7
|
// src/client/use-job.ts
|
|
7
8
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
@@ -138,7 +139,7 @@ function useJob(options) {
|
|
|
138
139
|
eventSource.onmessage = (event) => {
|
|
139
140
|
try {
|
|
140
141
|
const data = JSON.parse(event.data);
|
|
141
|
-
if ((data.type === "run:trigger" || data.type === "run:leased") && data.runId) {
|
|
142
|
+
if ((data.type === "run:trigger" || data.type === "run:coalesced" || data.type === "run:leased") && data.runId) {
|
|
142
143
|
setCurrentRunId(data.runId);
|
|
143
144
|
}
|
|
144
145
|
} catch {
|
|
@@ -232,6 +233,8 @@ function useJob(options) {
|
|
|
232
233
|
isCompleted: effectiveStatus === "completed",
|
|
233
234
|
isFailed: effectiveStatus === "failed",
|
|
234
235
|
isCancelled: effectiveStatus === "cancelled",
|
|
236
|
+
isTerminal: effectiveStatus === "completed" || effectiveStatus === "failed" || effectiveStatus === "cancelled",
|
|
237
|
+
isActive: effectiveStatus === "pending" || effectiveStatus === "leased",
|
|
235
238
|
currentRunId,
|
|
236
239
|
reset
|
|
237
240
|
};
|
|
@@ -293,7 +296,9 @@ function useJobRun(options) {
|
|
|
293
296
|
isPending,
|
|
294
297
|
isCompleted,
|
|
295
298
|
isFailed,
|
|
296
|
-
isCancelled
|
|
299
|
+
isCancelled,
|
|
300
|
+
isTerminal: isCompleted || isFailed || isCancelled,
|
|
301
|
+
isActive: isPending || isLeased
|
|
297
302
|
};
|
|
298
303
|
}
|
|
299
304
|
|
|
@@ -301,162 +306,128 @@ function useJobRun(options) {
|
|
|
301
306
|
function createJobHooks(options) {
|
|
302
307
|
const { api, jobName } = options;
|
|
303
308
|
return {
|
|
304
|
-
useJob: () => {
|
|
305
|
-
return useJob({
|
|
309
|
+
useJob: (jobOptions) => {
|
|
310
|
+
return useJob({
|
|
311
|
+
api,
|
|
312
|
+
jobName,
|
|
313
|
+
...jobOptions
|
|
314
|
+
});
|
|
306
315
|
},
|
|
307
|
-
useRun: (runId) => {
|
|
308
|
-
return useJobRun({ api, runId });
|
|
316
|
+
useRun: (runId, runOptions) => {
|
|
317
|
+
return useJobRun({ api, runId, ...runOptions });
|
|
309
318
|
},
|
|
310
319
|
useLogs: (runId, logsOptions) => {
|
|
311
|
-
return useJobLogs({ api, runId,
|
|
320
|
+
return useJobLogs({ api, runId, ...logsOptions });
|
|
312
321
|
}
|
|
313
322
|
};
|
|
314
323
|
}
|
|
315
324
|
|
|
316
325
|
// src/client/use-run-actions.ts
|
|
317
|
-
import { useCallback as useCallback2
|
|
326
|
+
import { useCallback as useCallback2 } from "react";
|
|
327
|
+
async function parseErrorResponse(response, actionName) {
|
|
328
|
+
let errorMessage = `Failed to ${actionName}: ${response.statusText}`;
|
|
329
|
+
try {
|
|
330
|
+
const data = await response.json();
|
|
331
|
+
if (typeof data === "object" && data !== null && "error" in data && data.error) {
|
|
332
|
+
errorMessage = String(data.error);
|
|
333
|
+
}
|
|
334
|
+
} catch {
|
|
335
|
+
}
|
|
336
|
+
return errorMessage;
|
|
337
|
+
}
|
|
318
338
|
function useRunActions(options) {
|
|
319
339
|
const { api } = options;
|
|
320
|
-
const
|
|
321
|
-
const [error, setError] = useState2(null);
|
|
322
|
-
const executeAction = useCallback2(
|
|
340
|
+
const executeJson = useCallback2(
|
|
323
341
|
async (url, actionName, init) => {
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
if (!response.ok) {
|
|
329
|
-
let errorMessage = `Failed to ${actionName}: ${response.statusText}`;
|
|
330
|
-
try {
|
|
331
|
-
const data = await response.json();
|
|
332
|
-
if (data.error) {
|
|
333
|
-
errorMessage = data.error;
|
|
334
|
-
}
|
|
335
|
-
} catch {
|
|
336
|
-
}
|
|
337
|
-
throw new Error(errorMessage);
|
|
338
|
-
}
|
|
339
|
-
return await response.json();
|
|
340
|
-
} catch (err) {
|
|
341
|
-
const message = err instanceof Error ? err.message : "Unknown error";
|
|
342
|
-
setError(message);
|
|
343
|
-
throw err;
|
|
344
|
-
} finally {
|
|
345
|
-
setIsLoading(false);
|
|
342
|
+
const response = await fetch(url, init);
|
|
343
|
+
if (!response.ok) {
|
|
344
|
+
const errorMessage = await parseErrorResponse(response, actionName);
|
|
345
|
+
throw new Error(errorMessage);
|
|
346
346
|
}
|
|
347
|
+
return await response.json();
|
|
347
348
|
},
|
|
348
349
|
[]
|
|
349
350
|
);
|
|
350
351
|
const retrigger = useCallback2(
|
|
351
352
|
async (runId) => {
|
|
352
353
|
const enc = encodeURIComponent(runId);
|
|
353
|
-
const data = await
|
|
354
|
+
const data = await executeJson(
|
|
354
355
|
`${api}/retrigger?runId=${enc}`,
|
|
355
356
|
"retrigger",
|
|
356
357
|
{ method: "POST" }
|
|
357
358
|
);
|
|
358
359
|
if (!data.runId) {
|
|
359
|
-
|
|
360
|
-
setError(message);
|
|
361
|
-
throw new Error(message);
|
|
360
|
+
throw new Error("Failed to retrigger: missing runId in response");
|
|
362
361
|
}
|
|
363
362
|
return data.runId;
|
|
364
363
|
},
|
|
365
|
-
[api,
|
|
364
|
+
[api, executeJson]
|
|
366
365
|
);
|
|
367
366
|
const cancel = useCallback2(
|
|
368
367
|
async (runId) => {
|
|
369
368
|
const enc = encodeURIComponent(runId);
|
|
370
|
-
await
|
|
369
|
+
await executeJson(`${api}/cancel?runId=${enc}`, "cancel", {
|
|
371
370
|
method: "POST"
|
|
372
371
|
});
|
|
373
372
|
},
|
|
374
|
-
[api,
|
|
373
|
+
[api, executeJson]
|
|
375
374
|
);
|
|
376
375
|
const deleteRun = useCallback2(
|
|
377
376
|
async (runId) => {
|
|
378
377
|
const enc = encodeURIComponent(runId);
|
|
379
|
-
await
|
|
378
|
+
await executeJson(`${api}/run?runId=${enc}`, "delete", {
|
|
380
379
|
method: "DELETE"
|
|
381
380
|
});
|
|
382
381
|
},
|
|
383
|
-
[api,
|
|
382
|
+
[api, executeJson]
|
|
384
383
|
);
|
|
385
384
|
const getRun = useCallback2(
|
|
386
385
|
async (runId) => {
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
if (!response.ok) {
|
|
396
|
-
let errorMessage = `Failed to get run: ${response.statusText}`;
|
|
397
|
-
try {
|
|
398
|
-
const data = await response.json();
|
|
399
|
-
if (data.error) {
|
|
400
|
-
errorMessage = data.error;
|
|
401
|
-
}
|
|
402
|
-
} catch {
|
|
403
|
-
}
|
|
404
|
-
throw new Error(errorMessage);
|
|
405
|
-
}
|
|
406
|
-
return await response.json();
|
|
407
|
-
} catch (err) {
|
|
408
|
-
const message = err instanceof Error ? err.message : "Unknown error";
|
|
409
|
-
setError(message);
|
|
410
|
-
throw err;
|
|
411
|
-
} finally {
|
|
412
|
-
setIsLoading(false);
|
|
386
|
+
const enc = encodeURIComponent(runId);
|
|
387
|
+
const response = await fetch(`${api}/run?runId=${enc}`);
|
|
388
|
+
if (response.status === 404) {
|
|
389
|
+
return null;
|
|
390
|
+
}
|
|
391
|
+
if (!response.ok) {
|
|
392
|
+
const errorMessage = await parseErrorResponse(response, "get run");
|
|
393
|
+
throw new Error(errorMessage);
|
|
413
394
|
}
|
|
395
|
+
return await response.json();
|
|
414
396
|
},
|
|
415
397
|
[api]
|
|
416
398
|
);
|
|
417
399
|
const getSteps = useCallback2(
|
|
418
400
|
async (runId) => {
|
|
419
401
|
const enc = encodeURIComponent(runId);
|
|
420
|
-
return
|
|
421
|
-
`${api}/steps?runId=${enc}`,
|
|
422
|
-
"get steps"
|
|
423
|
-
);
|
|
402
|
+
return executeJson(`${api}/steps?runId=${enc}`, "get steps");
|
|
424
403
|
},
|
|
425
|
-
[api,
|
|
404
|
+
[api, executeJson]
|
|
426
405
|
);
|
|
427
406
|
return {
|
|
428
407
|
retrigger,
|
|
429
408
|
cancel,
|
|
430
409
|
deleteRun,
|
|
431
410
|
getRun,
|
|
432
|
-
getSteps
|
|
433
|
-
isLoading,
|
|
434
|
-
error
|
|
411
|
+
getSteps
|
|
435
412
|
};
|
|
436
413
|
}
|
|
437
414
|
|
|
438
415
|
// src/client/use-runs.ts
|
|
439
|
-
import { useCallback as useCallback3, useEffect as useEffect3,
|
|
416
|
+
import { useCallback as useCallback3, useEffect as useEffect3, useRef as useRef3, useState as useState2 } from "react";
|
|
440
417
|
function useRuns(jobDefinitionOrOptions, optionsArg) {
|
|
441
418
|
const isJob = isJobDefinition(jobDefinitionOrOptions);
|
|
442
419
|
const jobName = isJob ? jobDefinitionOrOptions.name : jobDefinitionOrOptions.jobName;
|
|
443
420
|
const options = isJob ? optionsArg : jobDefinitionOrOptions;
|
|
444
421
|
const { api, status, labels, pageSize = 10, realtime = true } = options;
|
|
445
|
-
const
|
|
446
|
-
const
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
);
|
|
450
|
-
const
|
|
451
|
-
const
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
);
|
|
455
|
-
const [runs, setRuns] = useState3([]);
|
|
456
|
-
const [page, setPage] = useState3(0);
|
|
457
|
-
const [hasMore, setHasMore] = useState3(false);
|
|
458
|
-
const [isLoading, setIsLoading] = useState3(true);
|
|
459
|
-
const [error, setError] = useState3(null);
|
|
422
|
+
const stableLabels = useStableValue(labels);
|
|
423
|
+
const stableJobName = useStableValue(jobName);
|
|
424
|
+
const stableStatus = useStableValue(status);
|
|
425
|
+
const normalizedStatus = Array.isArray(stableStatus) && stableStatus.length === 0 ? void 0 : stableStatus;
|
|
426
|
+
const [runs, setRuns] = useState2([]);
|
|
427
|
+
const [page, setPage] = useState2(0);
|
|
428
|
+
const [hasMore, setHasMore] = useState2(false);
|
|
429
|
+
const [isLoading, setIsLoading] = useState2(true);
|
|
430
|
+
const [error, setError] = useState2(null);
|
|
460
431
|
const isMountedRef = useRef3(true);
|
|
461
432
|
const eventSourceRef = useRef3(null);
|
|
462
433
|
const refresh = useCallback3(async () => {
|
|
@@ -464,8 +435,8 @@ function useRuns(jobDefinitionOrOptions, optionsArg) {
|
|
|
464
435
|
setError(null);
|
|
465
436
|
try {
|
|
466
437
|
const params = new URLSearchParams();
|
|
467
|
-
|
|
468
|
-
|
|
438
|
+
appendArrayParam(params, "jobName", stableJobName);
|
|
439
|
+
appendArrayParam(params, "status", normalizedStatus);
|
|
469
440
|
appendLabelsToParams(params, stableLabels);
|
|
470
441
|
params.set("limit", String(pageSize + 1));
|
|
471
442
|
params.set("offset", String(page * pageSize));
|
|
@@ -488,7 +459,7 @@ function useRuns(jobDefinitionOrOptions, optionsArg) {
|
|
|
488
459
|
setIsLoading(false);
|
|
489
460
|
}
|
|
490
461
|
}
|
|
491
|
-
}, [api, stableJobName,
|
|
462
|
+
}, [api, stableJobName, normalizedStatus, stableLabels, pageSize, page]);
|
|
492
463
|
useEffect3(() => {
|
|
493
464
|
isMountedRef.current = true;
|
|
494
465
|
refresh();
|
|
@@ -505,7 +476,7 @@ function useRuns(jobDefinitionOrOptions, optionsArg) {
|
|
|
505
476
|
return;
|
|
506
477
|
}
|
|
507
478
|
const params = new URLSearchParams();
|
|
508
|
-
|
|
479
|
+
appendArrayParam(params, "jobName", stableJobName);
|
|
509
480
|
appendLabelsToParams(params, stableLabels);
|
|
510
481
|
const sseUrl = `${api}/runs/subscribe${params.toString() ? `?${params.toString()}` : ""}`;
|
|
511
482
|
const eventSource = new EventSource(sseUrl);
|
|
@@ -513,7 +484,7 @@ function useRuns(jobDefinitionOrOptions, optionsArg) {
|
|
|
513
484
|
eventSource.onmessage = (event) => {
|
|
514
485
|
try {
|
|
515
486
|
const data = JSON.parse(event.data);
|
|
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") {
|
|
487
|
+
if (data.type === "run:trigger" || data.type === "run:coalesced" || data.type === "run:leased" || data.type === "run:complete" || data.type === "run:fail" || data.type === "run:cancel" || data.type === "run:delete") {
|
|
517
488
|
refresh();
|
|
518
489
|
}
|
|
519
490
|
if (data.type === "run:progress") {
|
|
@@ -566,10 +537,10 @@ function useRuns(jobDefinitionOrOptions, optionsArg) {
|
|
|
566
537
|
refresh
|
|
567
538
|
};
|
|
568
539
|
}
|
|
569
|
-
function
|
|
570
|
-
if (
|
|
571
|
-
for (const
|
|
572
|
-
params.append(
|
|
540
|
+
function appendArrayParam(params, key, value) {
|
|
541
|
+
if (value === void 0) return;
|
|
542
|
+
for (const v of Array.isArray(value) ? value : [value]) {
|
|
543
|
+
params.append(key, v);
|
|
573
544
|
}
|
|
574
545
|
}
|
|
575
546
|
function appendLabelsToParams(params, labels) {
|