@coji/durably-react 0.13.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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @coji/durably-react
2
2
 
3
- React bindings for [Durably](https://github.com/coji/durably) - step-oriented resumable batch execution.
3
+ React bindings for [Durably](https://github.com/coji/durably) steps that survive crashes.
4
4
 
5
5
  **[Documentation](https://coji.github.io/durably/)** | **[GitHub](https://github.com/coji/durably)**
6
6
 
@@ -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-33VIIDHK.js.map
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 { 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
- 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, isLoading, error } = useRunActions({
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 onClick={() => retrigger(runId)} disabled={isLoading}>
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 onClick={() => cancel(runId)} disabled={isLoading}>
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> | undefined> {
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, RunStatus, 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 };
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-33VIIDHK.js";
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({ api, jobName });
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, maxLogs: logsOptions?.maxLogs });
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, useState as useState2 } from "react";
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 [isLoading, setIsLoading] = useState2(false);
321
- const [error, setError] = useState2(null);
322
- const executeAction = useCallback2(
340
+ const executeJson = useCallback2(
323
341
  async (url, actionName, init) => {
324
- setIsLoading(true);
325
- setError(null);
326
- try {
327
- const response = await fetch(url, init);
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 executeAction(
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
- const message = "Failed to retrigger: missing runId in response";
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, executeAction]
364
+ [api, executeJson]
366
365
  );
367
366
  const cancel = useCallback2(
368
367
  async (runId) => {
369
368
  const enc = encodeURIComponent(runId);
370
- await executeAction(`${api}/cancel?runId=${enc}`, "cancel", {
369
+ await executeJson(`${api}/cancel?runId=${enc}`, "cancel", {
371
370
  method: "POST"
372
371
  });
373
372
  },
374
- [api, executeAction]
373
+ [api, executeJson]
375
374
  );
376
375
  const deleteRun = useCallback2(
377
376
  async (runId) => {
378
377
  const enc = encodeURIComponent(runId);
379
- await executeAction(`${api}/run?runId=${enc}`, "delete", {
378
+ await executeJson(`${api}/run?runId=${enc}`, "delete", {
380
379
  method: "DELETE"
381
380
  });
382
381
  },
383
- [api, executeAction]
382
+ [api, executeJson]
384
383
  );
385
384
  const getRun = useCallback2(
386
385
  async (runId) => {
387
- setIsLoading(true);
388
- setError(null);
389
- try {
390
- const enc = encodeURIComponent(runId);
391
- const response = await fetch(`${api}/run?runId=${enc}`);
392
- if (response.status === 404) {
393
- return null;
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 executeAction(
421
- `${api}/steps?runId=${enc}`,
422
- "get steps"
423
- );
402
+ return executeJson(`${api}/steps?runId=${enc}`, "get steps");
424
403
  },
425
- [api, executeAction]
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, useMemo as useMemo2, useRef as useRef3, useState as useState3 } from "react";
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 labelsKey = labels ? JSON.stringify(labels) : void 0;
446
- const stableLabels = useMemo2(
447
- () => labelsKey ? JSON.parse(labelsKey) : void 0,
448
- [labelsKey]
449
- );
450
- const jobNameKey = jobName ? JSON.stringify(jobName) : void 0;
451
- const stableJobName = useMemo2(
452
- () => jobNameKey ? JSON.parse(jobNameKey) : void 0,
453
- [jobNameKey]
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
- appendJobNameToParams(params, stableJobName);
468
- if (status) params.set("status", status);
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, status, stableLabels, pageSize, page]);
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
- appendJobNameToParams(params, stableJobName);
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 appendJobNameToParams(params, jobName) {
570
- if (!jobName) return;
571
- for (const name of Array.isArray(jobName) ? jobName : [jobName]) {
572
- params.append("jobName", name);
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) {