@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 +1 -1
- 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
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/client/use-job.ts","../src/client/use-sse-subscription.ts","../src/shared/sse-event-subscriber.ts","../src/client/use-job-logs.ts","../src/client/use-job-run.ts","../src/client/create-job-hooks.ts","../src/client/use-run-actions.ts","../src/client/use-runs.ts","../src/client/create-durably.ts"],"sourcesContent":["import { useCallback, useEffect, useRef, useState } from 'react'\nimport type { LogEntry, Progress, RunStatus } from '../types'\nimport { useSSESubscription } from './use-sse-subscription'\n\nexport interface UseJobClientOptions {\n /**\n * API endpoint URL (e.g., '/api/durably')\n */\n api: string\n /**\n * Job name to trigger\n */\n jobName: string\n /**\n * Initial Run ID to subscribe to (for reconnection scenarios)\n * When provided, the hook will immediately start subscribing to this run\n */\n initialRunId?: string\n /**\n * Automatically resume tracking a leased/pending job on mount\n * @default true\n */\n autoResume?: boolean\n /**\n * Automatically switch to tracking the latest triggered job\n * @default true\n */\n followLatest?: boolean\n}\n\nexport interface UseJobClientResult<TInput, TOutput> {\n /**\n * Whether the hook is ready (always true for client mode)\n */\n /**\n * Trigger the job with the given input\n */\n trigger: (input: TInput) => Promise<{ runId: string }>\n /**\n * Trigger and wait for completion\n */\n triggerAndWait: (input: TInput) => Promise<{ runId: string; output: TOutput }>\n /**\n * Current run status\n */\n status: RunStatus | null\n /**\n * Output from completed run\n */\n output: TOutput | null\n /**\n * Error message from failed run\n */\n error: string | null\n /**\n * Logs collected during execution\n */\n logs: LogEntry[]\n /**\n * Current progress\n */\n progress: Progress | null\n /**\n * Whether a run is currently running\n */\n isLeased: boolean\n /**\n * Whether a run is pending\n */\n isPending: boolean\n /**\n * Whether the run completed successfully\n */\n isCompleted: boolean\n /**\n * Whether the run failed\n */\n isFailed: boolean\n /**\n * Whether the run was cancelled\n */\n isCancelled: boolean\n /**\n * Current run ID\n */\n currentRunId: string | null\n /**\n * Reset all state\n */\n reset: () => void\n}\n\n/**\n * Hook for triggering and subscribing to jobs via server API.\n * Uses fetch for triggering and EventSource for SSE subscription.\n */\nexport function useJob<\n TInput extends Record<string, unknown> = Record<string, unknown>,\n TOutput extends Record<string, unknown> = Record<string, unknown>,\n>(options: UseJobClientOptions): UseJobClientResult<TInput, TOutput> {\n const {\n api,\n jobName,\n initialRunId,\n autoResume = true,\n followLatest = true,\n } = options\n\n const [currentRunId, setCurrentRunId] = useState<string | null>(\n initialRunId ?? null,\n )\n const [isPending, setIsPending] = useState(false)\n\n // Track if user has triggered a run (to prevent autoResume from overwriting)\n const hasUserTriggered = useRef(false)\n const waitIntervalRef = useRef<ReturnType<typeof setInterval> | null>(null)\n\n const subscription = useSSESubscription<TOutput>(api, currentRunId)\n\n // Keep a ref to the latest subscription state for use in triggerAndWait\n const subscriptionRef = useRef(subscription)\n subscriptionRef.current = subscription\n\n // Auto-resume: fetch leased/pending job on mount\n useEffect(() => {\n if (!autoResume) return\n if (initialRunId) return // Skip if initialRunId is provided\n\n const abortController = new AbortController()\n\n const findActiveRun = async () => {\n // Fetch leased and pending in parallel\n const signal = abortController.signal\n const [leasedRes, pendingRes] = await Promise.all([\n fetch(\n `${api}/runs?${new URLSearchParams({ jobName, status: 'leased', limit: '1' })}`,\n { signal },\n ),\n fetch(\n `${api}/runs?${new URLSearchParams({ jobName, status: 'pending', limit: '1' })}`,\n { signal },\n ),\n ])\n\n if (hasUserTriggered.current) return\n\n // Prefer leased over pending\n if (leasedRes.ok) {\n const runs = (await leasedRes.json()) as Array<{ id: string }>\n if (runs.length > 0) {\n setCurrentRunId(runs[0].id)\n return\n }\n }\n\n if (pendingRes.ok) {\n const runs = (await pendingRes.json()) as Array<{ id: string }>\n if (runs.length > 0) {\n setCurrentRunId(runs[0].id)\n }\n }\n }\n\n findActiveRun().catch((err) => {\n // Ignore abort errors\n if (err.name !== 'AbortError') {\n console.error('autoResume error:', err)\n }\n })\n\n return () => {\n abortController.abort()\n }\n }, [api, jobName, autoResume, initialRunId])\n\n // Follow latest: subscribe to job-level SSE for run:trigger/run:leased events\n useEffect(() => {\n if (!followLatest) return\n\n const params = new URLSearchParams({ jobName })\n const eventSource = new EventSource(`${api}/runs/subscribe?${params}`)\n\n eventSource.onmessage = (event) => {\n try {\n const data = JSON.parse(event.data) as {\n type: string\n runId?: string\n }\n if (\n (data.type === 'run:trigger' || data.type === 'run:leased') &&\n data.runId\n ) {\n setCurrentRunId(data.runId)\n }\n } catch {\n // Ignore parse errors\n }\n }\n\n eventSource.onerror = () => {\n // SSE connection error - could reconnect or log for debugging\n // No need to surface error to user as this is a background subscription\n }\n\n return () => {\n eventSource.close()\n }\n }, [api, jobName, followLatest])\n\n const trigger = useCallback(\n async (input: TInput): Promise<{ runId: string }> => {\n // Mark that user has triggered (prevents autoResume from overwriting)\n hasUserTriggered.current = true\n\n // Reset state\n subscription.reset()\n setIsPending(true)\n\n const response = await fetch(`${api}/trigger`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({ jobName, input }),\n })\n\n if (!response.ok) {\n setIsPending(false)\n const errorText = await response.text()\n throw new Error(errorText || `HTTP ${response.status}`)\n }\n\n const { runId } = (await response.json()) as { runId: string }\n setCurrentRunId(runId)\n\n return { runId }\n },\n [api, jobName, subscription.reset],\n )\n\n const triggerAndWait = useCallback(\n async (input: TInput): Promise<{ runId: string; output: TOutput }> => {\n const { runId } = await trigger(input)\n\n return new Promise((resolve, reject) => {\n // Clear any previous wait interval\n if (waitIntervalRef.current) {\n clearInterval(waitIntervalRef.current)\n }\n\n const checkInterval = setInterval(() => {\n const sub = subscriptionRef.current\n if (sub.status === 'completed' && sub.output) {\n clearInterval(checkInterval)\n waitIntervalRef.current = null\n resolve({ runId, output: sub.output })\n } else if (sub.status === 'failed') {\n clearInterval(checkInterval)\n waitIntervalRef.current = null\n reject(new Error(sub.error ?? 'Job failed'))\n } else if (sub.status === 'cancelled') {\n clearInterval(checkInterval)\n waitIntervalRef.current = null\n reject(new Error('Job cancelled'))\n }\n }, 50)\n\n waitIntervalRef.current = checkInterval\n })\n },\n [trigger],\n )\n\n // Clean up wait interval on unmount\n useEffect(() => {\n return () => {\n if (waitIntervalRef.current) {\n clearInterval(waitIntervalRef.current)\n }\n }\n }, [])\n\n const reset = useCallback(() => {\n subscription.reset()\n setCurrentRunId(null)\n setIsPending(false)\n }, [subscription.reset])\n\n // Compute effective status (pending overrides null when we've triggered but SSE hasn't started)\n const effectiveStatus = subscription.status ?? (isPending ? 'pending' : null)\n\n // Clear pending when we get a real status\n useEffect(() => {\n if (subscription.status && isPending) {\n setIsPending(false)\n }\n }, [subscription.status, isPending])\n\n return {\n trigger,\n triggerAndWait,\n status: effectiveStatus,\n output: subscription.output,\n error: subscription.error,\n logs: subscription.logs,\n progress: subscription.progress,\n isLeased: effectiveStatus === 'leased',\n isPending: effectiveStatus === 'pending',\n isCompleted: effectiveStatus === 'completed',\n isFailed: effectiveStatus === 'failed',\n isCancelled: effectiveStatus === 'cancelled',\n currentRunId,\n reset,\n }\n}\n","import { useMemo } from 'react'\nimport { createSSEEventSubscriber } from '../shared/sse-event-subscriber'\nimport {\n useSubscription,\n type UseSubscriptionOptions,\n type UseSubscriptionResult,\n} from '../shared/use-subscription'\nimport type { SubscriptionState } from '../types'\n\n/** @deprecated Use SubscriptionState from '../types' instead */\nexport type SSESubscriptionState<TOutput = unknown> = SubscriptionState<TOutput>\n\n/** @deprecated Use UseSubscriptionOptions from '../shared/use-subscription' instead */\nexport type UseSSESubscriptionOptions = UseSubscriptionOptions\n\n/** @deprecated Use UseSubscriptionResult from '../shared/use-subscription' instead */\nexport type UseSSESubscriptionResult<TOutput = unknown> =\n UseSubscriptionResult<TOutput>\n\n/**\n * Internal hook for subscribing to run events via SSE.\n * Used by client-mode hooks (useJob, useJobRun, useJobLogs).\n *\n * @deprecated Consider using useSubscription with createSSEEventSubscriber directly.\n */\nexport function useSSESubscription<TOutput = unknown>(\n api: string | null,\n runId: string | null,\n options?: UseSSESubscriptionOptions,\n): UseSSESubscriptionResult<TOutput> {\n const subscriber = useMemo(\n () => (api ? createSSEEventSubscriber(api) : null),\n [api],\n )\n\n return useSubscription<TOutput>(subscriber, runId, options)\n}\n","import type { DurablyEvent } from '../types'\nimport type { EventSubscriber, SubscriptionEvent } from './event-subscriber'\n\n/**\n * EventSubscriber implementation using Server-Sent Events (SSE).\n * Used in client environments that communicate with a Durably server via HTTP.\n */\nexport function createSSEEventSubscriber(apiBaseUrl: string): EventSubscriber {\n return {\n subscribe<TOutput = unknown>(\n runId: string,\n onEvent: (event: SubscriptionEvent<TOutput>) => void,\n ): () => void {\n const url = `${apiBaseUrl}/subscribe?runId=${encodeURIComponent(runId)}`\n const eventSource = new EventSource(url)\n\n eventSource.onmessage = (messageEvent) => {\n try {\n const data = JSON.parse(messageEvent.data) as DurablyEvent\n if (data.runId !== runId) return\n\n switch (data.type) {\n case 'run:leased':\n onEvent({ type: 'run:leased' })\n break\n case 'run:complete':\n onEvent({\n type: 'run:complete',\n output: data.output as TOutput,\n })\n break\n case 'run:fail':\n onEvent({ type: 'run:fail', error: data.error })\n break\n case 'run:cancel':\n onEvent({ type: 'run:cancel' })\n break\n case 'run:progress':\n onEvent({ type: 'run:progress', progress: data.progress })\n break\n case 'log:write':\n onEvent({\n type: 'log:write',\n runId: data.runId,\n stepName: null,\n level: data.level,\n message: data.message,\n data: data.data,\n })\n break\n }\n } catch {\n // Ignore parse errors\n }\n }\n\n // Let EventSource handle reconnection automatically.\n // Only close on permanent failures (CLOSED state).\n eventSource.onerror = () => {\n if (eventSource.readyState === EventSource.CLOSED) {\n onEvent({ type: 'connection_error', error: 'Connection failed' })\n }\n }\n\n return () => {\n eventSource.close()\n }\n },\n }\n}\n","import type { LogEntry } from '../types'\nimport { useSSESubscription } from './use-sse-subscription'\n\nexport interface UseJobLogsClientOptions {\n /**\n * API endpoint URL (e.g., '/api/durably')\n */\n api: string\n /**\n * The run ID to subscribe to logs for\n */\n runId: string | null\n /**\n * Maximum number of logs to keep (default: unlimited)\n */\n maxLogs?: number\n}\n\nexport interface UseJobLogsClientResult {\n /**\n * Whether the hook is ready (always true for client mode)\n */\n /**\n * Logs collected during execution\n */\n logs: LogEntry[]\n /**\n * Clear all logs\n */\n clearLogs: () => void\n}\n\n/**\n * Hook for subscribing to logs from a run via server API.\n * Uses EventSource for SSE subscription.\n */\nexport function useJobLogs(\n options: UseJobLogsClientOptions,\n): UseJobLogsClientResult {\n const { api, runId, maxLogs } = options\n\n const subscription = useSSESubscription(api, runId, { maxLogs })\n\n return {\n logs: subscription.logs,\n clearLogs: subscription.clearLogs,\n }\n}\n","import { useEffect, useRef } from 'react'\nimport type { LogEntry, Progress, RunStatus } from '../types'\nimport { useSSESubscription } from './use-sse-subscription'\n\nexport interface UseJobRunClientOptions {\n /**\n * API endpoint URL (e.g., '/api/durably')\n */\n api: string\n /**\n * The run ID to subscribe to\n */\n runId: string | null\n /**\n * Callback when run starts (transitions to pending/running)\n */\n onStart?: () => void\n /**\n * Callback when run completes successfully\n */\n onComplete?: () => void\n /**\n * Callback when run fails\n */\n onFail?: () => void\n}\n\nexport interface UseJobRunClientResult<TOutput = unknown> {\n /**\n * Whether the hook is ready (always true for client mode)\n */\n /**\n * Current run status\n */\n status: RunStatus | null\n /**\n * Output from completed run\n */\n output: TOutput | null\n /**\n * Error message from failed run\n */\n error: string | null\n /**\n * Logs collected during execution\n */\n logs: LogEntry[]\n /**\n * Current progress\n */\n progress: Progress | null\n /**\n * Whether a run is currently running\n */\n isLeased: boolean\n /**\n * Whether a run is pending\n */\n isPending: boolean\n /**\n * Whether the run completed successfully\n */\n isCompleted: boolean\n /**\n * Whether the run failed\n */\n isFailed: boolean\n /**\n * Whether the run was cancelled\n */\n isCancelled: boolean\n}\n\n/**\n * Hook for subscribing to an existing run via server API.\n * Uses EventSource for SSE subscription.\n */\nexport function useJobRun<TOutput = unknown>(\n options: UseJobRunClientOptions,\n): UseJobRunClientResult<TOutput> {\n const { api, runId, onStart, onComplete, onFail } = options\n\n const subscription = useSSESubscription<TOutput>(api, runId)\n\n // If we have a runId but no status yet, treat as pending\n const effectiveStatus = subscription.status ?? (runId ? 'pending' : null)\n\n const isCompleted = effectiveStatus === 'completed'\n const isFailed = effectiveStatus === 'failed'\n const isPending = effectiveStatus === 'pending'\n const isLeased = effectiveStatus === 'leased'\n const isCancelled = effectiveStatus === 'cancelled'\n\n // Track previous status to detect transitions (use effectiveStatus, not subscription.status)\n const prevStatusRef = useRef<RunStatus | null>(null)\n\n useEffect(() => {\n const prevStatus = prevStatusRef.current\n prevStatusRef.current = effectiveStatus\n\n // Only fire callbacks on status transitions\n if (prevStatus !== effectiveStatus) {\n // Fire onStart when transitioning from null to pending/running\n if (prevStatus === null && (isPending || isLeased) && onStart) {\n onStart()\n }\n if (isCompleted && onComplete) {\n onComplete()\n }\n if (isFailed && onFail) {\n onFail()\n }\n }\n }, [\n effectiveStatus,\n isPending,\n isLeased,\n isCompleted,\n isFailed,\n onStart,\n onComplete,\n onFail,\n ])\n\n return {\n status: effectiveStatus,\n output: subscription.output,\n error: subscription.error,\n logs: subscription.logs,\n progress: subscription.progress,\n isLeased,\n isPending,\n isCompleted,\n isFailed,\n isCancelled,\n }\n}\n","import type { JobDefinition } from '@coji/durably'\nimport type { InferInput, InferOutput } from '../types'\nimport { useJob, type UseJobClientResult } from './use-job'\nimport { useJobLogs, type UseJobLogsClientResult } from './use-job-logs'\nimport { useJobRun, type UseJobRunClientResult } from './use-job-run'\n\n/**\n * Options for createJobHooks\n */\nexport interface CreateJobHooksOptions {\n /**\n * API endpoint URL (e.g., '/api/durably')\n */\n api: string\n /**\n * Job name (must match the server-side job name)\n */\n jobName: string\n}\n\n/**\n * Type-safe hooks for a specific job\n */\nexport interface JobHooks<TInput, TOutput> {\n /**\n * Hook for triggering and monitoring the job\n */\n useJob: () => UseJobClientResult<TInput, TOutput>\n\n /**\n * Hook for subscribing to an existing run by ID\n */\n useRun: (runId: string | null) => UseJobRunClientResult<TOutput>\n\n /**\n * Hook for subscribing to logs from a run\n */\n useLogs: (\n runId: string | null,\n options?: { maxLogs?: number },\n ) => UseJobLogsClientResult\n}\n\n/**\n * Create type-safe hooks for a specific job.\n *\n * @example\n * ```tsx\n * // Import job type from server (type-only import is safe)\n * import type { importCsvJob } from '~/lib/durably.server'\n * import { createJobHooks } from '@coji/durably-react'\n *\n * const importCsv = createJobHooks<typeof importCsvJob>({\n * api: '/api/durably',\n * jobName: 'import-csv',\n * })\n *\n * // In your component - fully type-safe\n * function CsvImporter() {\n * const { trigger, output, progress, isLeased } = importCsv.useJob()\n *\n * return (\n * <button onClick={() => trigger({ rows: [...] })}>\n * Import\n * </button>\n * )\n * }\n * ```\n */\nexport function createJobHooks<\n // biome-ignore lint/suspicious/noExplicitAny: TJob needs to accept any JobDefinition\n TJob extends JobDefinition<string, any, any>,\n>(\n options: CreateJobHooksOptions,\n): JobHooks<InferInput<TJob>, InferOutput<TJob>> {\n const { api, jobName } = options\n\n return {\n useJob: () => {\n return useJob<InferInput<TJob>, InferOutput<TJob>>({ api, jobName })\n },\n\n useRun: (runId: string | null) => {\n return useJobRun<InferOutput<TJob>>({ api, runId })\n },\n\n useLogs: (runId: string | null, logsOptions?: { maxLogs?: number }) => {\n return useJobLogs({ api, runId, maxLogs: logsOptions?.maxLogs })\n },\n }\n}\n","import { useCallback, useState } from 'react'\nimport type { ClientRun } from '../types'\n\n/**\n * Step record returned from the server API\n */\nexport interface StepRecord {\n name: string\n status: 'completed' | 'failed' | 'cancelled'\n output: unknown\n}\n\nexport interface UseRunActionsClientOptions {\n /**\n * API endpoint URL (e.g., '/api/durably')\n */\n api: string\n}\n\nexport interface UseRunActionsClientResult {\n /**\n * Create a fresh run from a completed, failed, or cancelled run\n */\n retrigger: (runId: string) => Promise<string>\n /**\n * Cancel a pending or leased run\n */\n cancel: (runId: string) => Promise<void>\n /**\n * Delete a run (only completed, failed, or cancelled runs)\n */\n deleteRun: (runId: string) => Promise<void>\n /**\n * Get a single run by ID\n */\n getRun: (runId: string) => Promise<ClientRun | null>\n /**\n * Get steps for a run\n */\n getSteps: (runId: string) => Promise<StepRecord[]>\n /**\n * Whether an action is in progress\n */\n isLoading: boolean\n /**\n * Error message from last action\n */\n error: string | null\n}\n\n/**\n * Hook for run actions via server API.\n *\n * @example\n * ```tsx\n * function RunActions({ runId, status }: { runId: string; status: string }) {\n * const { retrigger, cancel, isLoading, error } = useRunActions({\n * api: '/api/durably',\n * })\n *\n * return (\n * <div>\n * {status === 'failed' && (\n * <button onClick={() => retrigger(runId)} disabled={isLoading}>\n * Run Again\n * </button>\n * )}\n * {(status === 'pending' || status === 'leased') && (\n * <button onClick={() => cancel(runId)} disabled={isLoading}>\n * Cancel\n * </button>\n * )}\n * {error && <span className=\"error\">{error}</span>}\n * </div>\n * )\n * }\n * ```\n */\nexport function useRunActions(\n options: UseRunActionsClientOptions,\n): UseRunActionsClientResult {\n const { api } = options\n\n const [isLoading, setIsLoading] = useState(false)\n const [error, setError] = useState<string | null>(null)\n\n const executeAction = useCallback(\n async <T>(\n url: string,\n actionName: string,\n init?: RequestInit,\n ): Promise<T> => {\n setIsLoading(true)\n setError(null)\n\n try {\n const response = await fetch(url, init)\n\n if (!response.ok) {\n let errorMessage = `Failed to ${actionName}: ${response.statusText}`\n try {\n const data = await response.json()\n if (data.error) {\n errorMessage = data.error\n }\n } catch {\n // Response is not JSON, use statusText\n }\n throw new Error(errorMessage)\n }\n\n return (await response.json()) as T\n } catch (err) {\n const message = err instanceof Error ? err.message : 'Unknown error'\n setError(message)\n throw err\n } finally {\n setIsLoading(false)\n }\n },\n [],\n )\n\n const retrigger = useCallback(\n async (runId: string) => {\n const enc = encodeURIComponent(runId)\n const data = await executeAction<{ runId?: string }>(\n `${api}/retrigger?runId=${enc}`,\n 'retrigger',\n { method: 'POST' },\n )\n if (!data.runId) {\n const message = 'Failed to retrigger: missing runId in response'\n setError(message)\n throw new Error(message)\n }\n return data.runId\n },\n [api, executeAction],\n )\n\n const cancel = useCallback(\n async (runId: string) => {\n const enc = encodeURIComponent(runId)\n await executeAction(`${api}/cancel?runId=${enc}`, 'cancel', {\n method: 'POST',\n })\n },\n [api, executeAction],\n )\n\n const deleteRun = useCallback(\n async (runId: string) => {\n const enc = encodeURIComponent(runId)\n await executeAction(`${api}/run?runId=${enc}`, 'delete', {\n method: 'DELETE',\n })\n },\n [api, executeAction],\n )\n\n const getRun = useCallback(\n async (runId: string): Promise<ClientRun | null> => {\n setIsLoading(true)\n setError(null)\n\n try {\n const enc = encodeURIComponent(runId)\n const response = await fetch(`${api}/run?runId=${enc}`)\n\n if (response.status === 404) {\n return null\n }\n\n if (!response.ok) {\n let errorMessage = `Failed to get run: ${response.statusText}`\n try {\n const data = await response.json()\n if (data.error) {\n errorMessage = data.error\n }\n } catch {\n // Response is not JSON, use statusText\n }\n throw new Error(errorMessage)\n }\n\n return (await response.json()) as ClientRun\n } catch (err) {\n const message = err instanceof Error ? err.message : 'Unknown error'\n setError(message)\n throw err\n } finally {\n setIsLoading(false)\n }\n },\n [api],\n )\n\n const getSteps = useCallback(\n async (runId: string): Promise<StepRecord[]> => {\n const enc = encodeURIComponent(runId)\n return executeAction<StepRecord[]>(\n `${api}/steps?runId=${enc}`,\n 'get steps',\n )\n },\n [api, executeAction],\n )\n\n return {\n retrigger,\n cancel,\n deleteRun,\n getRun,\n getSteps,\n isLoading,\n error,\n }\n}\n","import type { JobDefinition } from '@coji/durably'\nimport { useCallback, useEffect, useMemo, useRef, useState } from 'react'\nimport {\n type Progress,\n type RunStatus,\n type TypedClientRun,\n isJobDefinition,\n} from '../types'\n\n// Re-export types for convenience\nexport type { ClientRun, TypedClientRun } from '../types'\n\n/**\n * SSE notification event from /runs/subscribe\n */\ntype RunUpdateEvent =\n | {\n type:\n | 'run:trigger'\n | 'run:leased'\n | 'run:complete'\n | 'run:fail'\n | 'run:cancel'\n | 'run:delete'\n runId: string\n jobName: string\n }\n | { type: 'run:progress'; runId: string; jobName: string; progress: Progress }\n | {\n type: 'step:start' | 'step:complete'\n runId: string\n jobName: string\n stepName: string\n stepIndex: number\n }\n | {\n type: 'step:fail'\n runId: string\n jobName: string\n stepName: string\n stepIndex: number\n error: string\n labels: Record<string, string>\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 stepName: string | null\n level: 'info' | 'warn' | 'error'\n message: string\n data: unknown\n }\n\nexport interface UseRunsClientOptions {\n /**\n * API endpoint URL (e.g., '/api/durably')\n */\n api: string\n /**\n * Filter by job name(s). Pass a string for one, or an array for multiple.\n */\n jobName?: string | string[]\n /**\n * Filter by status\n */\n status?: RunStatus\n /**\n * Filter by labels (all specified labels must match)\n */\n labels?: Record<string, string>\n /**\n * Number of runs per page\n * @default 10\n */\n pageSize?: number\n /**\n * Subscribe to real-time updates via SSE (first page only)\n * @default true\n */\n realtime?: boolean\n}\n\nexport interface UseRunsClientResult<\n TInput extends Record<string, unknown> = Record<string, unknown>,\n TOutput extends Record<string, unknown> | undefined =\n | Record<string, unknown>\n | undefined,\n> {\n /**\n * List of runs for the current page\n */\n runs: TypedClientRun<TInput, TOutput>[]\n /**\n * Current page (0-indexed)\n */\n page: number\n /**\n * Whether there are more pages\n */\n hasMore: boolean\n /**\n * Whether data is being loaded\n */\n isLoading: boolean\n /**\n * Error message if fetch failed\n */\n error: string | null\n /**\n * Go to the next page\n */\n nextPage: () => void\n /**\n * Go to the previous page\n */\n prevPage: () => void\n /**\n * Go to a specific page\n */\n goToPage: (page: number) => void\n /**\n * Refresh the current page\n */\n refresh: () => Promise<void>\n}\n\n/**\n * Hook for listing runs via server API with pagination.\n * First page (page 0) automatically subscribes to SSE for real-time updates.\n * Other pages are static and require manual refresh.\n *\n * @example With generic type parameter (dashboard with multiple job types)\n * ```tsx\n * type DashboardRun = TypedClientRun<ImportInput, ImportOutput> | TypedClientRun<SyncInput, SyncOutput>\n *\n * function Dashboard() {\n * const { runs } = useRuns<DashboardRun>({ api: '/api/durably', pageSize: 10 })\n * // runs are typed as DashboardRun[]\n * }\n * ```\n *\n * @example With JobDefinition (single job, auto-filters by jobName)\n * ```tsx\n * const myJob = defineJob({ name: 'my-job', ... })\n *\n * function RunHistory() {\n * const { runs } = useRuns(myJob, { api: '/api/durably' })\n * // runs[0].output is typed!\n * return <div>{runs[0]?.output?.someField}</div>\n * }\n * ```\n *\n * @example With options only (untyped)\n * ```tsx\n * function RunHistory() {\n * const { runs } = useRuns({ api: '/api/durably', pageSize: 10 })\n * // runs[0].output is unknown\n * }\n * ```\n */\n// Overload 1: With generic type parameter\nexport function useRuns<\n TRun extends TypedClientRun<\n Record<string, unknown>,\n Record<string, unknown> | undefined\n >,\n>(\n options: UseRunsClientOptions,\n): UseRunsClientResult<\n TRun extends TypedClientRun<infer I, infer _O> ? I : Record<string, unknown>,\n TRun extends TypedClientRun<infer _I, infer O> ? O : Record<string, unknown>\n>\n\n// Overload 2: With JobDefinition for type inference (auto-filters by jobName)\nexport function useRuns<\n TName extends string,\n TInput extends Record<string, unknown>,\n TOutput extends Record<string, unknown> | undefined,\n>(\n jobDefinition: JobDefinition<TName, TInput, TOutput>,\n options: Omit<UseRunsClientOptions, 'jobName'>,\n): UseRunsClientResult<TInput, TOutput>\n\n// Overload 3: Without type parameter (untyped, backward compatible)\nexport function useRuns(options: UseRunsClientOptions): UseRunsClientResult\n\n// Implementation\nexport function useRuns<\n TName extends string,\n TInput extends Record<string, unknown>,\n TOutput extends Record<string, unknown> | undefined,\n>(\n jobDefinitionOrOptions:\n | JobDefinition<TName, TInput, TOutput>\n | UseRunsClientOptions,\n optionsArg?: Omit<UseRunsClientOptions, 'jobName'>,\n): UseRunsClientResult<TInput, TOutput> {\n // Determine if first argument is a JobDefinition using type guard\n const isJob = isJobDefinition(jobDefinitionOrOptions)\n\n const jobName = isJob\n ? jobDefinitionOrOptions.name\n : (jobDefinitionOrOptions as UseRunsClientOptions).jobName\n\n const options = isJob\n ? (optionsArg as Omit<UseRunsClientOptions, 'jobName'>)\n : (jobDefinitionOrOptions as UseRunsClientOptions)\n\n const { api, status, labels, pageSize = 10, realtime = true } = options\n\n // Stabilize labels reference to prevent infinite re-renders\n const labelsKey = labels ? JSON.stringify(labels) : undefined\n const stableLabels = useMemo(\n () =>\n labelsKey ? (JSON.parse(labelsKey) as Record<string, string>) : undefined,\n [labelsKey],\n )\n\n // Stabilize jobName reference to prevent infinite re-renders with array literals\n const jobNameKey = jobName ? JSON.stringify(jobName) : undefined\n const stableJobName = useMemo(\n () =>\n jobNameKey ? (JSON.parse(jobNameKey) as string | string[]) : undefined,\n [jobNameKey],\n )\n\n const [runs, setRuns] = useState<TypedClientRun<TInput, TOutput>[]>([])\n const [page, setPage] = useState(0)\n const [hasMore, setHasMore] = useState(false)\n const [isLoading, setIsLoading] = useState(true)\n const [error, setError] = useState<string | null>(null)\n\n const isMountedRef = useRef(true)\n const eventSourceRef = useRef<EventSource | null>(null)\n\n const refresh = useCallback(async () => {\n setIsLoading(true)\n setError(null)\n\n try {\n const params = new URLSearchParams()\n appendJobNameToParams(params, stableJobName)\n if (status) params.set('status', status)\n appendLabelsToParams(params, stableLabels)\n params.set('limit', String(pageSize + 1))\n params.set('offset', String(page * pageSize))\n\n const url = `${api}/runs?${params.toString()}`\n const response = await fetch(url)\n\n if (!response.ok) {\n throw new Error(`Failed to fetch runs: ${response.statusText}`)\n }\n\n const data = (await response.json()) as TypedClientRun<TInput, TOutput>[]\n\n if (isMountedRef.current) {\n setHasMore(data.length > pageSize)\n setRuns(data.slice(0, pageSize))\n }\n } catch (err) {\n if (isMountedRef.current) {\n setError(err instanceof Error ? err.message : 'Unknown error')\n }\n } finally {\n if (isMountedRef.current) {\n setIsLoading(false)\n }\n }\n }, [api, stableJobName, status, stableLabels, pageSize, page])\n\n // Initial fetch\n useEffect(() => {\n isMountedRef.current = true\n refresh()\n\n return () => {\n isMountedRef.current = false\n }\n }, [refresh])\n\n // SSE subscription for first page only (when realtime is enabled)\n useEffect(() => {\n // Only subscribe to SSE on first page with realtime enabled\n if (!realtime || page !== 0) {\n // Clean up any existing connection when navigating away from first page\n if (eventSourceRef.current) {\n eventSourceRef.current.close()\n eventSourceRef.current = null\n }\n return\n }\n\n // Build SSE URL\n const params = new URLSearchParams()\n appendJobNameToParams(params, stableJobName)\n appendLabelsToParams(params, stableLabels)\n const sseUrl = `${api}/runs/subscribe${params.toString() ? `?${params.toString()}` : ''}`\n\n const eventSource = new EventSource(sseUrl)\n eventSourceRef.current = eventSource\n\n eventSource.onmessage = (event) => {\n try {\n const data = JSON.parse(event.data) as RunUpdateEvent\n // On run lifecycle events, refresh the list\n if (\n data.type === 'run:trigger' ||\n data.type === 'run:leased' ||\n data.type === 'run:complete' ||\n data.type === 'run:fail' ||\n data.type === 'run:cancel' ||\n data.type === 'run:delete'\n ) {\n refresh()\n }\n // On progress update, update the run in place\n if (data.type === 'run:progress') {\n setRuns((prev) =>\n prev.map((run) =>\n run.id === data.runId ? { ...run, progress: data.progress } : run,\n ),\n )\n }\n // On step complete, update currentStepIndex\n if (data.type === 'step:complete') {\n setRuns((prev) =>\n prev.map((run) =>\n run.id === data.runId\n ? { ...run, currentStepIndex: data.stepIndex + 1 }\n : run,\n ),\n )\n }\n // On step start or fail, refresh to get latest state\n if (\n data.type === 'step:start' ||\n data.type === 'step:fail' ||\n data.type === 'step:cancel'\n ) {\n refresh()\n }\n // log:write is handled by useJobLogs, not useRuns\n } catch {\n // Ignore parse errors\n }\n }\n\n eventSource.onerror = () => {\n // EventSource will automatically reconnect\n }\n\n return () => {\n eventSource.close()\n eventSourceRef.current = null\n }\n }, [api, stableJobName, stableLabels, page, realtime, refresh])\n\n const nextPage = useCallback(() => {\n if (hasMore) {\n setPage((p) => p + 1)\n }\n }, [hasMore])\n\n const prevPage = useCallback(() => {\n setPage((p) => Math.max(0, p - 1))\n }, [])\n\n const goToPage = useCallback((newPage: number) => {\n setPage(Math.max(0, newPage))\n }, [])\n\n return {\n runs,\n page,\n hasMore,\n isLoading,\n error,\n nextPage,\n prevPage,\n goToPage,\n refresh,\n }\n}\n\nfunction appendJobNameToParams(\n params: URLSearchParams,\n jobName: string | string[] | undefined,\n) {\n if (!jobName) return\n for (const name of Array.isArray(jobName) ? jobName : [jobName]) {\n params.append('jobName', name)\n }\n}\n\nfunction appendLabelsToParams(\n params: URLSearchParams,\n labels: Record<string, string> | undefined,\n) {\n if (!labels) return\n for (const [key, value] of Object.entries(labels)) {\n params.set(`label.${key}`, value)\n }\n}\n","import type { InferInput, InferOutput } from '../types'\nimport { createJobHooks, type JobHooks } from './create-job-hooks'\nimport {\n useRunActions,\n type UseRunActionsClientResult,\n} from './use-run-actions'\nimport {\n useRuns,\n type UseRunsClientOptions,\n type UseRunsClientResult,\n} from './use-runs'\n\n/**\n * Options for createDurably\n */\nexport interface CreateDurablyOptions {\n /**\n * API endpoint URL (e.g., '/api/durably')\n */\n api: string\n}\n\n/**\n * Extract the jobs record from a Durably instance type.\n * Allows `createDurably<typeof serverDurably>()` to infer job types.\n */\ntype ExtractJobs<T> = T extends { readonly jobs: infer TJobs } ? TJobs : T\n\n/**\n * A type-safe Durably client with per-job hooks and cross-job utilities.\n */\nexport type DurablyClient<T> = {\n [K in keyof ExtractJobs<T>]: JobHooks<\n InferInput<ExtractJobs<T>[K]>,\n InferOutput<ExtractJobs<T>[K]>\n >\n} & {\n /**\n * List runs with pagination and real-time updates (cross-job).\n * The `api` option is pre-configured.\n */\n useRuns: <\n TInput extends Record<string, unknown> = Record<string, unknown>,\n TOutput extends Record<string, unknown> | undefined =\n | Record<string, unknown>\n | undefined,\n >(\n options?: Omit<UseRunsClientOptions, 'api'>,\n ) => UseRunsClientResult<TInput, TOutput>\n\n /**\n * Run actions: retrigger, cancel, delete, getRun, getSteps (cross-job).\n * The `api` option is pre-configured.\n */\n useRunActions: () => UseRunActionsClientResult\n}\n\n/**\n * Create a type-safe Durably client for React.\n *\n * Uses the same name as the server-side `createDurably` — the API endpoint\n * option distinguishes it from the server constructor.\n *\n * @example\n * ```tsx\n * // Server: create Durably instance\n * // app/lib/durably.server.ts\n * import { createDurably } from '@coji/durably'\n * export const durably = createDurably({\n * dialect,\n * jobs: { importCsv: importCsvJob, syncUsers: syncUsersJob },\n * })\n *\n * // Client: create typed hooks\n * // app/lib/durably.ts\n * import type { durably as serverDurably } from '~/lib/durably.server'\n * import { createDurably } from '@coji/durably-react'\n *\n * export const durably = createDurably<typeof serverDurably>({\n * api: '/api/durably',\n * })\n *\n * // In your component — fully type-safe with autocomplete\n * function CsvImporter() {\n * const { trigger, output, isLeased } = durably.importCsv.useJob()\n * return <button onClick={() => trigger({ rows: [...] })}>Import</button>\n * }\n *\n * // Cross-job hooks\n * function Dashboard() {\n * const { runs, nextPage } = durably.useRuns({ pageSize: 10 })\n * const { retrigger, cancel } = durably.useRunActions()\n * }\n * ```\n */\nexport function createDurably<T>(\n options: CreateDurablyOptions,\n): DurablyClient<T> {\n const { api } = options\n const cache = new Map<string, unknown>()\n\n // Built-in cross-job hooks. These names are reserved and cannot be used as job names.\n // If a job is registered with one of these names, the built-in hook takes precedence.\n const builtins: Record<string, unknown> = {\n useRuns: (opts?: Omit<UseRunsClientOptions, 'api'>) =>\n useRuns({ api, ...opts }),\n useRunActions: () => useRunActions({ api }),\n }\n\n // Create a proxy that generates and caches job hooks on demand\n return new Proxy({} as DurablyClient<T>, {\n get(_target, key) {\n if (typeof key !== 'string') return undefined\n\n // Return built-in hooks first\n if (key in builtins) return builtins[key]\n\n // Return cached or create new job hooks\n let hooks = cache.get(key)\n if (!hooks) {\n hooks = createJobHooks({ api, jobName: key })\n cache.set(key, hooks)\n }\n return hooks\n },\n })\n}\n"],"mappings":"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","names":["useEffect","useRef","useRef","useEffect","useCallback","useState","useCallback","useEffect","useMemo","useRef","useState","useMemo","useState","useRef","useCallback","useEffect"]}
|
|
1
|
+
{"version":3,"sources":["../src/client/use-job.ts","../src/client/use-sse-subscription.ts","../src/shared/sse-event-subscriber.ts","../src/client/use-job-logs.ts","../src/client/use-job-run.ts","../src/client/create-job-hooks.ts","../src/client/use-run-actions.ts","../src/client/use-runs.ts","../src/client/create-durably.ts"],"sourcesContent":["import { useCallback, useEffect, useRef, useState } from 'react'\nimport type { LogEntry, Progress, RunStatus } from '../types'\nimport { useSSESubscription } from './use-sse-subscription'\n\nexport interface UseJobClientOptions {\n /**\n * API endpoint URL (e.g., '/api/durably')\n */\n api: string\n /**\n * Job name to trigger\n */\n jobName: string\n /**\n * Initial Run ID to subscribe to (for reconnection scenarios)\n * When provided, the hook will immediately start subscribing to this run\n */\n initialRunId?: string\n /**\n * Automatically resume tracking a leased/pending job on mount\n * @default true\n */\n autoResume?: boolean\n /**\n * Automatically switch to tracking the latest triggered job\n * @default true\n */\n followLatest?: boolean\n}\n\nexport interface UseJobClientResult<TInput, TOutput> {\n /**\n * Whether the hook is ready (always true for client mode)\n */\n /**\n * Trigger the job with the given input\n */\n trigger: (input: TInput) => Promise<{ runId: string }>\n /**\n * Trigger and wait for completion\n */\n triggerAndWait: (input: TInput) => Promise<{ runId: string; output: TOutput }>\n /**\n * Current run status\n */\n status: RunStatus | null\n /**\n * Output from completed run\n */\n output: TOutput | null\n /**\n * Error message from failed run\n */\n error: string | null\n /**\n * Logs collected during execution\n */\n logs: LogEntry[]\n /**\n * Current progress\n */\n progress: Progress | null\n /**\n * Whether a run is currently running\n */\n isLeased: boolean\n /**\n * Whether a run is pending\n */\n isPending: boolean\n /**\n * Whether the run completed successfully\n */\n isCompleted: boolean\n /**\n * Whether the run failed\n */\n isFailed: boolean\n /**\n * Whether the run was cancelled\n */\n isCancelled: boolean\n /**\n * Whether the run reached a terminal status (completed, failed, or cancelled)\n */\n isTerminal: boolean\n /**\n * Whether the run is pending or leased (actively queued or executing)\n */\n isActive: boolean\n /**\n * Current run ID\n */\n currentRunId: string | null\n /**\n * Reset all state\n */\n reset: () => void\n}\n\n/**\n * Hook for triggering and subscribing to jobs via server API.\n * Uses fetch for triggering and EventSource for SSE subscription.\n */\nexport function useJob<\n TInput extends Record<string, unknown> = Record<string, unknown>,\n TOutput extends Record<string, unknown> = Record<string, unknown>,\n>(options: UseJobClientOptions): UseJobClientResult<TInput, TOutput> {\n const {\n api,\n jobName,\n initialRunId,\n autoResume = true,\n followLatest = true,\n } = options\n\n const [currentRunId, setCurrentRunId] = useState<string | null>(\n initialRunId ?? null,\n )\n const [isPending, setIsPending] = useState(false)\n\n // Track if user has triggered a run (to prevent autoResume from overwriting)\n const hasUserTriggered = useRef(false)\n const waitIntervalRef = useRef<ReturnType<typeof setInterval> | null>(null)\n\n const subscription = useSSESubscription<TOutput>(api, currentRunId)\n\n // Keep a ref to the latest subscription state for use in triggerAndWait\n const subscriptionRef = useRef(subscription)\n subscriptionRef.current = subscription\n\n // Auto-resume: fetch leased/pending job on mount\n useEffect(() => {\n if (!autoResume) return\n if (initialRunId) return // Skip if initialRunId is provided\n\n const abortController = new AbortController()\n\n const findActiveRun = async () => {\n // Fetch leased and pending in parallel\n const signal = abortController.signal\n const [leasedRes, pendingRes] = await Promise.all([\n fetch(\n `${api}/runs?${new URLSearchParams({ jobName, status: 'leased', limit: '1' })}`,\n { signal },\n ),\n fetch(\n `${api}/runs?${new URLSearchParams({ jobName, status: 'pending', limit: '1' })}`,\n { signal },\n ),\n ])\n\n if (hasUserTriggered.current) return\n\n // Prefer leased over pending\n if (leasedRes.ok) {\n const runs = (await leasedRes.json()) as Array<{ id: string }>\n if (runs.length > 0) {\n setCurrentRunId(runs[0].id)\n return\n }\n }\n\n if (pendingRes.ok) {\n const runs = (await pendingRes.json()) as Array<{ id: string }>\n if (runs.length > 0) {\n setCurrentRunId(runs[0].id)\n }\n }\n }\n\n findActiveRun().catch((err) => {\n // Ignore abort errors\n if (err.name !== 'AbortError') {\n console.error('autoResume error:', err)\n }\n })\n\n return () => {\n abortController.abort()\n }\n }, [api, jobName, autoResume, initialRunId])\n\n // Follow latest: subscribe to job-level SSE for run:trigger/run:leased events\n useEffect(() => {\n if (!followLatest) return\n\n const params = new URLSearchParams({ jobName })\n const eventSource = new EventSource(`${api}/runs/subscribe?${params}`)\n\n eventSource.onmessage = (event) => {\n try {\n const data = JSON.parse(event.data) as {\n type: string\n runId?: string\n }\n if (\n (data.type === 'run:trigger' ||\n data.type === 'run:coalesced' ||\n data.type === 'run:leased') &&\n data.runId\n ) {\n setCurrentRunId(data.runId)\n }\n } catch {\n // Ignore parse errors\n }\n }\n\n eventSource.onerror = () => {\n // SSE connection error - could reconnect or log for debugging\n // No need to surface error to user as this is a background subscription\n }\n\n return () => {\n eventSource.close()\n }\n }, [api, jobName, followLatest])\n\n const trigger = useCallback(\n async (input: TInput): Promise<{ runId: string }> => {\n // Mark that user has triggered (prevents autoResume from overwriting)\n hasUserTriggered.current = true\n\n // Reset state\n subscription.reset()\n setIsPending(true)\n\n const response = await fetch(`${api}/trigger`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({ jobName, input }),\n })\n\n if (!response.ok) {\n setIsPending(false)\n const errorText = await response.text()\n throw new Error(errorText || `HTTP ${response.status}`)\n }\n\n const { runId } = (await response.json()) as { runId: string }\n setCurrentRunId(runId)\n\n return { runId }\n },\n [api, jobName, subscription.reset],\n )\n\n const triggerAndWait = useCallback(\n async (input: TInput): Promise<{ runId: string; output: TOutput }> => {\n const { runId } = await trigger(input)\n\n return new Promise((resolve, reject) => {\n // Clear any previous wait interval\n if (waitIntervalRef.current) {\n clearInterval(waitIntervalRef.current)\n }\n\n const checkInterval = setInterval(() => {\n const sub = subscriptionRef.current\n if (sub.status === 'completed' && sub.output) {\n clearInterval(checkInterval)\n waitIntervalRef.current = null\n resolve({ runId, output: sub.output })\n } else if (sub.status === 'failed') {\n clearInterval(checkInterval)\n waitIntervalRef.current = null\n reject(new Error(sub.error ?? 'Job failed'))\n } else if (sub.status === 'cancelled') {\n clearInterval(checkInterval)\n waitIntervalRef.current = null\n reject(new Error('Job cancelled'))\n }\n }, 50)\n\n waitIntervalRef.current = checkInterval\n })\n },\n [trigger],\n )\n\n // Clean up wait interval on unmount\n useEffect(() => {\n return () => {\n if (waitIntervalRef.current) {\n clearInterval(waitIntervalRef.current)\n }\n }\n }, [])\n\n const reset = useCallback(() => {\n subscription.reset()\n setCurrentRunId(null)\n setIsPending(false)\n }, [subscription.reset])\n\n // Compute effective status (pending overrides null when we've triggered but SSE hasn't started)\n const effectiveStatus = subscription.status ?? (isPending ? 'pending' : null)\n\n // Clear pending when we get a real status\n useEffect(() => {\n if (subscription.status && isPending) {\n setIsPending(false)\n }\n }, [subscription.status, isPending])\n\n return {\n trigger,\n triggerAndWait,\n status: effectiveStatus,\n output: subscription.output,\n error: subscription.error,\n logs: subscription.logs,\n progress: subscription.progress,\n isLeased: effectiveStatus === 'leased',\n isPending: effectiveStatus === 'pending',\n isCompleted: effectiveStatus === 'completed',\n isFailed: effectiveStatus === 'failed',\n isCancelled: effectiveStatus === 'cancelled',\n isTerminal:\n effectiveStatus === 'completed' ||\n effectiveStatus === 'failed' ||\n effectiveStatus === 'cancelled',\n isActive: effectiveStatus === 'pending' || effectiveStatus === 'leased',\n currentRunId,\n reset,\n }\n}\n","import { useMemo } from 'react'\nimport { createSSEEventSubscriber } from '../shared/sse-event-subscriber'\nimport {\n useSubscription,\n type UseSubscriptionOptions,\n type UseSubscriptionResult,\n} from '../shared/use-subscription'\nimport type { SubscriptionState } from '../types'\n\n/** @deprecated Use SubscriptionState from '../types' instead */\nexport type SSESubscriptionState<TOutput = unknown> = SubscriptionState<TOutput>\n\n/** @deprecated Use UseSubscriptionOptions from '../shared/use-subscription' instead */\nexport type UseSSESubscriptionOptions = UseSubscriptionOptions\n\n/** @deprecated Use UseSubscriptionResult from '../shared/use-subscription' instead */\nexport type UseSSESubscriptionResult<TOutput = unknown> =\n UseSubscriptionResult<TOutput>\n\n/**\n * Internal hook for subscribing to run events via SSE.\n * Used by client-mode hooks (useJob, useJobRun, useJobLogs).\n *\n * @deprecated Consider using useSubscription with createSSEEventSubscriber directly.\n */\nexport function useSSESubscription<TOutput = unknown>(\n api: string | null,\n runId: string | null,\n options?: UseSSESubscriptionOptions,\n): UseSSESubscriptionResult<TOutput> {\n const subscriber = useMemo(\n () => (api ? createSSEEventSubscriber(api) : null),\n [api],\n )\n\n return useSubscription<TOutput>(subscriber, runId, options)\n}\n","import type { DurablyEvent } from '../types'\nimport type { EventSubscriber, SubscriptionEvent } from './event-subscriber'\n\n/**\n * EventSubscriber implementation using Server-Sent Events (SSE).\n * Used in client environments that communicate with a Durably server via HTTP.\n */\nexport function createSSEEventSubscriber(apiBaseUrl: string): EventSubscriber {\n return {\n subscribe<TOutput = unknown>(\n runId: string,\n onEvent: (event: SubscriptionEvent<TOutput>) => void,\n ): () => void {\n const url = `${apiBaseUrl}/subscribe?runId=${encodeURIComponent(runId)}`\n const eventSource = new EventSource(url)\n\n eventSource.onmessage = (messageEvent) => {\n try {\n const data = JSON.parse(messageEvent.data) as DurablyEvent\n if (data.runId !== runId) return\n\n switch (data.type) {\n case 'run:leased':\n onEvent({ type: 'run:leased' })\n break\n case 'run:complete':\n onEvent({\n type: 'run:complete',\n output: data.output as TOutput,\n })\n break\n case 'run:fail':\n onEvent({ type: 'run:fail', error: data.error })\n break\n case 'run:cancel':\n onEvent({ type: 'run:cancel' })\n break\n case 'run:progress':\n onEvent({ type: 'run:progress', progress: data.progress })\n break\n case 'log:write':\n onEvent({\n type: 'log:write',\n runId: data.runId,\n stepName: null,\n level: data.level,\n message: data.message,\n data: data.data,\n })\n break\n }\n } catch {\n // Ignore parse errors\n }\n }\n\n // Let EventSource handle reconnection automatically.\n // Only close on permanent failures (CLOSED state).\n eventSource.onerror = () => {\n if (eventSource.readyState === EventSource.CLOSED) {\n onEvent({ type: 'connection_error', error: 'Connection failed' })\n }\n }\n\n return () => {\n eventSource.close()\n }\n },\n }\n}\n","import type { LogEntry } from '../types'\nimport { useSSESubscription } from './use-sse-subscription'\n\nexport interface UseJobLogsClientOptions {\n /**\n * API endpoint URL (e.g., '/api/durably')\n */\n api: string\n /**\n * The run ID to subscribe to logs for\n */\n runId: string | null\n /**\n * Maximum number of logs to keep (default: unlimited)\n */\n maxLogs?: number\n}\n\nexport interface UseJobLogsClientResult {\n /**\n * Whether the hook is ready (always true for client mode)\n */\n /**\n * Logs collected during execution\n */\n logs: LogEntry[]\n /**\n * Clear all logs\n */\n clearLogs: () => void\n}\n\n/**\n * Hook for subscribing to logs from a run via server API.\n * Uses EventSource for SSE subscription.\n */\nexport function useJobLogs(\n options: UseJobLogsClientOptions,\n): UseJobLogsClientResult {\n const { api, runId, maxLogs } = options\n\n const subscription = useSSESubscription(api, runId, { maxLogs })\n\n return {\n logs: subscription.logs,\n clearLogs: subscription.clearLogs,\n }\n}\n","import { useEffect, useRef } from 'react'\nimport type { LogEntry, Progress, RunStatus } from '../types'\nimport { useSSESubscription } from './use-sse-subscription'\n\nexport interface UseJobRunClientOptions {\n /**\n * API endpoint URL (e.g., '/api/durably')\n */\n api: string\n /**\n * The run ID to subscribe to\n */\n runId: string | null\n /**\n * Callback when run starts (transitions to pending/running)\n */\n onStart?: () => void\n /**\n * Callback when run completes successfully\n */\n onComplete?: () => void\n /**\n * Callback when run fails\n */\n onFail?: () => void\n}\n\nexport interface UseJobRunClientResult<TOutput = unknown> {\n /**\n * Whether the hook is ready (always true for client mode)\n */\n /**\n * Current run status\n */\n status: RunStatus | null\n /**\n * Output from completed run\n */\n output: TOutput | null\n /**\n * Error message from failed run\n */\n error: string | null\n /**\n * Logs collected during execution\n */\n logs: LogEntry[]\n /**\n * Current progress\n */\n progress: Progress | null\n /**\n * Whether a run is currently running\n */\n isLeased: boolean\n /**\n * Whether a run is pending\n */\n isPending: boolean\n /**\n * Whether the run completed successfully\n */\n isCompleted: boolean\n /**\n * Whether the run failed\n */\n isFailed: boolean\n /**\n * Whether the run was cancelled\n */\n isCancelled: boolean\n /**\n * Whether the run reached a terminal status (completed, failed, or cancelled)\n */\n isTerminal: boolean\n /**\n * Whether the run is pending or leased (actively queued or executing)\n */\n isActive: boolean\n}\n\n/**\n * Hook for subscribing to an existing run via server API.\n * Uses EventSource for SSE subscription.\n */\nexport function useJobRun<TOutput = unknown>(\n options: UseJobRunClientOptions,\n): UseJobRunClientResult<TOutput> {\n const { api, runId, onStart, onComplete, onFail } = options\n\n const subscription = useSSESubscription<TOutput>(api, runId)\n\n // If we have a runId but no status yet, treat as pending\n const effectiveStatus = subscription.status ?? (runId ? 'pending' : null)\n\n const isCompleted = effectiveStatus === 'completed'\n const isFailed = effectiveStatus === 'failed'\n const isPending = effectiveStatus === 'pending'\n const isLeased = effectiveStatus === 'leased'\n const isCancelled = effectiveStatus === 'cancelled'\n\n // Track previous status to detect transitions (use effectiveStatus, not subscription.status)\n const prevStatusRef = useRef<RunStatus | null>(null)\n\n useEffect(() => {\n const prevStatus = prevStatusRef.current\n prevStatusRef.current = effectiveStatus\n\n // Only fire callbacks on status transitions\n if (prevStatus !== effectiveStatus) {\n // Fire onStart when transitioning from null to pending/running\n if (prevStatus === null && (isPending || isLeased) && onStart) {\n onStart()\n }\n if (isCompleted && onComplete) {\n onComplete()\n }\n if (isFailed && onFail) {\n onFail()\n }\n }\n }, [\n effectiveStatus,\n isPending,\n isLeased,\n isCompleted,\n isFailed,\n onStart,\n onComplete,\n onFail,\n ])\n\n return {\n status: effectiveStatus,\n output: subscription.output,\n error: subscription.error,\n logs: subscription.logs,\n progress: subscription.progress,\n isLeased,\n isPending,\n isCompleted,\n isFailed,\n isCancelled,\n isTerminal: isCompleted || isFailed || isCancelled,\n isActive: isPending || isLeased,\n }\n}\n","import type { JobDefinition } from '@coji/durably'\nimport type { InferInput, InferOutput } from '../types'\nimport {\n useJob,\n type UseJobClientOptions,\n type UseJobClientResult,\n} from './use-job'\nimport {\n useJobLogs,\n type UseJobLogsClientOptions,\n type UseJobLogsClientResult,\n} from './use-job-logs'\nimport {\n useJobRun,\n type UseJobRunClientOptions,\n type UseJobRunClientResult,\n} from './use-job-run'\n\n/**\n * Options for createJobHooks\n */\nexport interface CreateJobHooksOptions {\n /**\n * API endpoint URL (e.g., '/api/durably')\n */\n api: string\n /**\n * Job name (must match the server-side job name)\n */\n jobName: string\n}\n\n/**\n * Type-safe hooks for a specific job\n */\nexport interface JobHooks<TInput, TOutput> {\n /**\n * Hook for triggering and monitoring the job\n */\n useJob: (\n options?: Omit<UseJobClientOptions, 'api' | 'jobName'>,\n ) => UseJobClientResult<TInput, TOutput>\n\n /**\n * Hook for subscribing to an existing run by ID\n */\n useRun: (\n runId: string | null,\n options?: Omit<UseJobRunClientOptions, 'api' | 'runId'>,\n ) => UseJobRunClientResult<TOutput>\n\n /**\n * Hook for subscribing to logs from a run\n */\n useLogs: (\n runId: string | null,\n options?: Omit<UseJobLogsClientOptions, 'api' | 'runId'>,\n ) => UseJobLogsClientResult\n}\n\n/**\n * Create type-safe hooks for a specific job.\n *\n * @example\n * ```tsx\n * // Import job type from server (type-only import is safe)\n * import type { importCsvJob } from '~/lib/durably.server'\n * import { createJobHooks } from '@coji/durably-react'\n *\n * const importCsv = createJobHooks<typeof importCsvJob>({\n * api: '/api/durably',\n * jobName: 'import-csv',\n * })\n *\n * // In your component - fully type-safe\n * function CsvImporter() {\n * const { trigger, output, progress, isLeased } = importCsv.useJob()\n *\n * return (\n * <button onClick={() => trigger({ rows: [...] })}>\n * Import\n * </button>\n * )\n * }\n * ```\n */\nexport function createJobHooks<\n // biome-ignore lint/suspicious/noExplicitAny: TJob needs to accept any JobDefinition\n TJob extends JobDefinition<string, any, any>,\n>(\n options: CreateJobHooksOptions,\n): JobHooks<InferInput<TJob>, InferOutput<TJob>> {\n const { api, jobName } = options\n\n return {\n useJob: (jobOptions) => {\n return useJob<InferInput<TJob>, InferOutput<TJob>>({\n api,\n jobName,\n ...jobOptions,\n })\n },\n\n useRun: (runId, runOptions) => {\n return useJobRun<InferOutput<TJob>>({ api, runId, ...runOptions })\n },\n\n useLogs: (runId, logsOptions) => {\n return useJobLogs({ api, runId, ...logsOptions })\n },\n }\n}\n","import { useCallback } from 'react'\nimport type { ClientRun } from '../types'\n\n/**\n * Step record returned from the server API\n */\nexport interface StepRecord {\n name: string\n status: 'completed' | 'failed' | 'cancelled'\n output: unknown\n}\n\nexport interface UseRunActionsClientOptions {\n /**\n * API endpoint URL (e.g., '/api/durably')\n */\n api: string\n}\n\nexport interface UseRunActionsClientResult {\n /**\n * Create a fresh run from a completed, failed, or cancelled run\n */\n retrigger: (runId: string) => Promise<string>\n /**\n * Cancel a pending or leased run\n */\n cancel: (runId: string) => Promise<void>\n /**\n * Delete a run (only completed, failed, or cancelled runs)\n */\n deleteRun: (runId: string) => Promise<void>\n /**\n * Get a single run by ID\n */\n getRun: (runId: string) => Promise<ClientRun | null>\n /**\n * Get steps for a run\n */\n getSteps: (runId: string) => Promise<StepRecord[]>\n}\n\nasync function parseErrorResponse(\n response: Response,\n actionName: string,\n): Promise<string> {\n let errorMessage = `Failed to ${actionName}: ${response.statusText}`\n try {\n const data: unknown = await response.json()\n if (\n typeof data === 'object' &&\n data !== null &&\n 'error' in data &&\n (data as { error: unknown }).error\n ) {\n errorMessage = String((data as { error: unknown }).error)\n }\n } catch {\n // Response is not JSON, use statusText\n }\n return errorMessage\n}\n\n/**\n * Hook for run actions via server API.\n *\n * @example\n * ```tsx\n * function RunActions({ runId, status }: { runId: string; status: string }) {\n * const { retrigger, cancel } = useRunActions({\n * api: '/api/durably',\n * })\n * const [isPending, startTransition] = useTransition()\n *\n * return (\n * <div>\n * {status === 'failed' && (\n * <button\n * onClick={() =>\n * startTransition(() =>\n * // Handle errors in production (e.g. toast, local state)\n * retrigger(runId).catch(console.error),\n * )\n * }\n * disabled={isPending}\n * >\n * Run Again\n * </button>\n * )}\n * {(status === 'pending' || status === 'leased') && (\n * <button\n * onClick={() =>\n * startTransition(() =>\n * cancel(runId).catch(console.error),\n * )\n * }\n * disabled={isPending}\n * >\n * Cancel\n * </button>\n * )}\n * </div>\n * )\n * }\n * ```\n */\nexport function useRunActions(\n options: UseRunActionsClientOptions,\n): UseRunActionsClientResult {\n const { api } = options\n\n const executeJson = useCallback(\n async <T>(\n url: string,\n actionName: string,\n init?: RequestInit,\n ): Promise<T> => {\n const response = await fetch(url, init)\n\n if (!response.ok) {\n const errorMessage = await parseErrorResponse(response, actionName)\n throw new Error(errorMessage)\n }\n\n return (await response.json()) as T\n },\n [],\n )\n\n const retrigger = useCallback(\n async (runId: string) => {\n const enc = encodeURIComponent(runId)\n const data = await executeJson<{ runId?: string }>(\n `${api}/retrigger?runId=${enc}`,\n 'retrigger',\n { method: 'POST' },\n )\n if (!data.runId) {\n throw new Error('Failed to retrigger: missing runId in response')\n }\n return data.runId\n },\n [api, executeJson],\n )\n\n const cancel = useCallback(\n async (runId: string) => {\n const enc = encodeURIComponent(runId)\n await executeJson(`${api}/cancel?runId=${enc}`, 'cancel', {\n method: 'POST',\n })\n },\n [api, executeJson],\n )\n\n const deleteRun = useCallback(\n async (runId: string) => {\n const enc = encodeURIComponent(runId)\n await executeJson(`${api}/run?runId=${enc}`, 'delete', {\n method: 'DELETE',\n })\n },\n [api, executeJson],\n )\n\n const getRun = useCallback(\n async (runId: string): Promise<ClientRun | null> => {\n const enc = encodeURIComponent(runId)\n const response = await fetch(`${api}/run?runId=${enc}`)\n\n if (response.status === 404) {\n return null\n }\n\n if (!response.ok) {\n const errorMessage = await parseErrorResponse(response, 'get run')\n throw new Error(errorMessage)\n }\n\n return (await response.json()) as ClientRun\n },\n [api],\n )\n\n const getSteps = useCallback(\n async (runId: string): Promise<StepRecord[]> => {\n const enc = encodeURIComponent(runId)\n return executeJson<StepRecord[]>(`${api}/steps?runId=${enc}`, 'get steps')\n },\n [api, executeJson],\n )\n\n return {\n retrigger,\n cancel,\n deleteRun,\n getRun,\n getSteps,\n }\n}\n","import type { JobDefinition } from '@coji/durably'\nimport { useCallback, useEffect, useRef, useState } from 'react'\nimport { useStableValue } from '../shared/use-stable-value'\nimport {\n type Progress,\n type RunStatus,\n type TypedClientRun,\n isJobDefinition,\n} from '../types'\n\n// Re-export types for convenience\nexport type { ClientRun, TypedClientRun } from '../types'\n\n/**\n * SSE notification event from /runs/subscribe\n */\ntype RunUpdateEvent =\n | {\n type:\n | 'run:trigger'\n | 'run:coalesced'\n | 'run:leased'\n | 'run:complete'\n | 'run:fail'\n | 'run:cancel'\n | 'run:delete'\n runId: string\n jobName: string\n }\n | { type: 'run:progress'; runId: string; jobName: string; progress: Progress }\n | {\n type: 'step:start' | 'step:complete'\n runId: string\n jobName: string\n stepName: string\n stepIndex: number\n }\n | {\n type: 'step:fail'\n runId: string\n jobName: string\n stepName: string\n stepIndex: number\n error: string\n labels: Record<string, string>\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 stepName: string | null\n level: 'info' | 'warn' | 'error'\n message: string\n data: unknown\n }\n\nexport interface UseRunsClientOptions {\n /**\n * API endpoint URL (e.g., '/api/durably')\n */\n api: string\n /**\n * Filter by job name(s). Pass a string for one, or an array for multiple.\n */\n jobName?: string | string[]\n /**\n * Filter by status(es). Pass one status, or an array for multiple (OR).\n */\n status?: RunStatus | RunStatus[]\n /**\n * Filter by labels (all specified labels must match)\n */\n labels?: Record<string, string>\n /**\n * Number of runs per page\n * @default 10\n */\n pageSize?: number\n /**\n * Subscribe to real-time updates via SSE (first page only)\n * @default true\n */\n realtime?: boolean\n}\n\nexport interface UseRunsClientResult<\n TInput extends Record<string, unknown> = Record<string, unknown>,\n TOutput extends Record<string, unknown> | undefined = Record<string, unknown>,\n> {\n /**\n * List of runs for the current page\n */\n runs: TypedClientRun<TInput, TOutput>[]\n /**\n * Current page (0-indexed)\n */\n page: number\n /**\n * Whether there are more pages\n */\n hasMore: boolean\n /**\n * Whether data is being loaded\n */\n isLoading: boolean\n /**\n * Error message if fetch failed\n */\n error: string | null\n /**\n * Go to the next page\n */\n nextPage: () => void\n /**\n * Go to the previous page\n */\n prevPage: () => void\n /**\n * Go to a specific page\n */\n goToPage: (page: number) => void\n /**\n * Refresh the current page\n */\n refresh: () => Promise<void>\n}\n\n/**\n * Hook for listing runs via server API with pagination.\n * First page (page 0) automatically subscribes to SSE for real-time updates.\n * Other pages are static and require manual refresh.\n *\n * @example With generic type parameter (dashboard with multiple job types)\n * ```tsx\n * type DashboardRun = TypedClientRun<ImportInput, ImportOutput> | TypedClientRun<SyncInput, SyncOutput>\n *\n * function Dashboard() {\n * const { runs } = useRuns<DashboardRun>({ api: '/api/durably', pageSize: 10 })\n * // runs are typed as DashboardRun[]\n * }\n * ```\n *\n * @example With JobDefinition (single job, auto-filters by jobName)\n * ```tsx\n * const myJob = defineJob({ name: 'my-job', ... })\n *\n * function RunHistory() {\n * const { runs } = useRuns(myJob, { api: '/api/durably' })\n * // runs[0].output is typed!\n * return <div>{runs[0]?.output?.someField}</div>\n * }\n * ```\n *\n * @example With options only (untyped)\n * ```tsx\n * function RunHistory() {\n * const { runs } = useRuns({ api: '/api/durably', pageSize: 10 })\n * // runs[0].output is unknown\n * }\n * ```\n */\n// Overload 1: With generic type parameter\nexport function useRuns<\n TRun extends TypedClientRun<\n Record<string, unknown>,\n Record<string, unknown> | undefined\n >,\n>(\n options: UseRunsClientOptions,\n): UseRunsClientResult<\n TRun extends TypedClientRun<infer I, infer _O> ? I : Record<string, unknown>,\n TRun extends TypedClientRun<infer _I, infer O> ? O : Record<string, unknown>\n>\n\n// Overload 2: With JobDefinition for type inference (auto-filters by jobName)\nexport function useRuns<\n TName extends string,\n TInput extends Record<string, unknown>,\n TOutput extends Record<string, unknown> | undefined,\n>(\n jobDefinition: JobDefinition<TName, TInput, TOutput>,\n options: Omit<UseRunsClientOptions, 'jobName'>,\n): UseRunsClientResult<TInput, TOutput>\n\n// Overload 3: Without type parameter (untyped, backward compatible)\nexport function useRuns(options: UseRunsClientOptions): UseRunsClientResult\n\n// Implementation\nexport function useRuns<\n TName extends string,\n TInput extends Record<string, unknown>,\n TOutput extends Record<string, unknown> | undefined,\n>(\n jobDefinitionOrOptions:\n | JobDefinition<TName, TInput, TOutput>\n | UseRunsClientOptions,\n optionsArg?: Omit<UseRunsClientOptions, 'jobName'>,\n): UseRunsClientResult<TInput, TOutput> {\n // Determine if first argument is a JobDefinition using type guard\n const isJob = isJobDefinition(jobDefinitionOrOptions)\n\n const jobName = isJob\n ? jobDefinitionOrOptions.name\n : (jobDefinitionOrOptions as UseRunsClientOptions).jobName\n\n const options = isJob\n ? (optionsArg as Omit<UseRunsClientOptions, 'jobName'>)\n : (jobDefinitionOrOptions as UseRunsClientOptions)\n\n const { api, status, labels, pageSize = 10, realtime = true } = options\n\n const stableLabels = useStableValue(labels)\n const stableJobName = useStableValue(jobName)\n const stableStatus = useStableValue(status)\n\n // Normalize empty status array to undefined (no filter)\n const normalizedStatus =\n Array.isArray(stableStatus) && stableStatus.length === 0\n ? undefined\n : stableStatus\n\n const [runs, setRuns] = useState<TypedClientRun<TInput, TOutput>[]>([])\n const [page, setPage] = useState(0)\n const [hasMore, setHasMore] = useState(false)\n const [isLoading, setIsLoading] = useState(true)\n const [error, setError] = useState<string | null>(null)\n\n const isMountedRef = useRef(true)\n const eventSourceRef = useRef<EventSource | null>(null)\n\n const refresh = useCallback(async () => {\n setIsLoading(true)\n setError(null)\n\n try {\n const params = new URLSearchParams()\n appendArrayParam(params, 'jobName', stableJobName)\n appendArrayParam(params, 'status', normalizedStatus)\n appendLabelsToParams(params, stableLabels)\n params.set('limit', String(pageSize + 1))\n params.set('offset', String(page * pageSize))\n\n const url = `${api}/runs?${params.toString()}`\n const response = await fetch(url)\n\n if (!response.ok) {\n throw new Error(`Failed to fetch runs: ${response.statusText}`)\n }\n\n const data = (await response.json()) as TypedClientRun<TInput, TOutput>[]\n\n if (isMountedRef.current) {\n setHasMore(data.length > pageSize)\n setRuns(data.slice(0, pageSize))\n }\n } catch (err) {\n if (isMountedRef.current) {\n setError(err instanceof Error ? err.message : 'Unknown error')\n }\n } finally {\n if (isMountedRef.current) {\n setIsLoading(false)\n }\n }\n }, [api, stableJobName, normalizedStatus, stableLabels, pageSize, page])\n\n // Initial fetch\n useEffect(() => {\n isMountedRef.current = true\n refresh()\n\n return () => {\n isMountedRef.current = false\n }\n }, [refresh])\n\n // SSE subscription for first page only (when realtime is enabled)\n useEffect(() => {\n // Only subscribe to SSE on first page with realtime enabled\n if (!realtime || page !== 0) {\n // Clean up any existing connection when navigating away from first page\n if (eventSourceRef.current) {\n eventSourceRef.current.close()\n eventSourceRef.current = null\n }\n return\n }\n\n // Build SSE URL\n const params = new URLSearchParams()\n appendArrayParam(params, 'jobName', stableJobName)\n appendLabelsToParams(params, stableLabels)\n const sseUrl = `${api}/runs/subscribe${params.toString() ? `?${params.toString()}` : ''}`\n\n const eventSource = new EventSource(sseUrl)\n eventSourceRef.current = eventSource\n\n eventSource.onmessage = (event) => {\n try {\n const data = JSON.parse(event.data) as RunUpdateEvent\n // On run lifecycle events, refresh the list\n if (\n data.type === 'run:trigger' ||\n data.type === 'run:coalesced' ||\n data.type === 'run:leased' ||\n data.type === 'run:complete' ||\n data.type === 'run:fail' ||\n data.type === 'run:cancel' ||\n data.type === 'run:delete'\n ) {\n refresh()\n }\n // On progress update, update the run in place\n if (data.type === 'run:progress') {\n setRuns((prev) =>\n prev.map((run) =>\n run.id === data.runId ? { ...run, progress: data.progress } : run,\n ),\n )\n }\n // On step complete, update currentStepIndex\n if (data.type === 'step:complete') {\n setRuns((prev) =>\n prev.map((run) =>\n run.id === data.runId\n ? { ...run, currentStepIndex: data.stepIndex + 1 }\n : run,\n ),\n )\n }\n // On step start or fail, refresh to get latest state\n if (\n data.type === 'step:start' ||\n data.type === 'step:fail' ||\n data.type === 'step:cancel'\n ) {\n refresh()\n }\n // log:write is handled by useJobLogs, not useRuns\n } catch {\n // Ignore parse errors\n }\n }\n\n eventSource.onerror = () => {\n // EventSource will automatically reconnect\n }\n\n return () => {\n eventSource.close()\n eventSourceRef.current = null\n }\n }, [api, stableJobName, stableLabels, page, realtime, refresh])\n\n const nextPage = useCallback(() => {\n if (hasMore) {\n setPage((p) => p + 1)\n }\n }, [hasMore])\n\n const prevPage = useCallback(() => {\n setPage((p) => Math.max(0, p - 1))\n }, [])\n\n const goToPage = useCallback((newPage: number) => {\n setPage(Math.max(0, newPage))\n }, [])\n\n return {\n runs,\n page,\n hasMore,\n isLoading,\n error,\n nextPage,\n prevPage,\n goToPage,\n refresh,\n }\n}\n\nfunction appendArrayParam(\n params: URLSearchParams,\n key: string,\n value: string | string[] | undefined,\n) {\n if (value === undefined) return\n for (const v of Array.isArray(value) ? value : [value]) {\n params.append(key, v)\n }\n}\n\nfunction appendLabelsToParams(\n params: URLSearchParams,\n labels: Record<string, string> | undefined,\n) {\n if (!labels) return\n for (const [key, value] of Object.entries(labels)) {\n params.set(`label.${key}`, value)\n }\n}\n","import type { InferInput, InferOutput } from '../types'\nimport { createJobHooks, type JobHooks } from './create-job-hooks'\nimport {\n useRunActions,\n type UseRunActionsClientResult,\n} from './use-run-actions'\nimport {\n useRuns,\n type UseRunsClientOptions,\n type UseRunsClientResult,\n} from './use-runs'\n\n/**\n * Options for createDurably\n */\nexport interface CreateDurablyOptions {\n /**\n * API endpoint URL (e.g., '/api/durably')\n */\n api: string\n}\n\n/**\n * Extract the jobs record from a Durably instance type.\n * Allows `createDurably<typeof serverDurably>()` to infer job types.\n */\ntype ExtractJobs<T> = T extends { readonly jobs: infer TJobs } ? TJobs : T\n\n/**\n * A type-safe Durably client with per-job hooks and cross-job utilities.\n */\nexport type DurablyClient<T> = {\n [K in keyof ExtractJobs<T>]: JobHooks<\n InferInput<ExtractJobs<T>[K]>,\n InferOutput<ExtractJobs<T>[K]>\n >\n} & {\n /**\n * List runs with pagination and real-time updates (cross-job).\n * The `api` option is pre-configured.\n */\n useRuns: <\n TInput extends Record<string, unknown> = Record<string, unknown>,\n TOutput extends Record<string, unknown> | undefined =\n | Record<string, unknown>\n | undefined,\n >(\n options?: Omit<UseRunsClientOptions, 'api'>,\n ) => UseRunsClientResult<TInput, TOutput>\n\n /**\n * Run actions: retrigger, cancel, delete, getRun, getSteps (cross-job).\n * The `api` option is pre-configured.\n */\n useRunActions: () => UseRunActionsClientResult\n}\n\n/**\n * Create a type-safe Durably client for React.\n *\n * Uses the same name as the server-side `createDurably` — the API endpoint\n * option distinguishes it from the server constructor.\n *\n * @example\n * ```tsx\n * // Server: create Durably instance\n * // app/lib/durably.server.ts\n * import { createDurably } from '@coji/durably'\n * export const durably = createDurably({\n * dialect,\n * jobs: { importCsv: importCsvJob, syncUsers: syncUsersJob },\n * })\n *\n * // Client: create typed hooks\n * // app/lib/durably.ts\n * import type { durably as serverDurably } from '~/lib/durably.server'\n * import { createDurably } from '@coji/durably-react'\n *\n * export const durably = createDurably<typeof serverDurably>({\n * api: '/api/durably',\n * })\n *\n * // In your component — fully type-safe with autocomplete\n * function CsvImporter() {\n * const { trigger, output, isLeased } = durably.importCsv.useJob()\n * return <button onClick={() => trigger({ rows: [...] })}>Import</button>\n * }\n *\n * // Cross-job hooks\n * function Dashboard() {\n * const { runs, nextPage } = durably.useRuns({ pageSize: 10 })\n * const { retrigger, cancel } = durably.useRunActions()\n * }\n * ```\n */\nexport function createDurably<T>(\n options: CreateDurablyOptions,\n): DurablyClient<T> {\n const { api } = options\n const cache = new Map<string, unknown>()\n\n // Built-in cross-job hooks. These names are reserved and cannot be used as job names.\n // If a job is registered with one of these names, the built-in hook takes precedence.\n const builtins: Record<string, unknown> = {\n useRuns: (opts?: Omit<UseRunsClientOptions, 'api'>) =>\n useRuns({ api, ...opts }),\n useRunActions: () => useRunActions({ api }),\n }\n\n // Create a proxy that generates and caches job hooks on demand\n return new Proxy({} as DurablyClient<T>, {\n get(_target, key) {\n if (typeof key !== 'string') return undefined\n\n // Return built-in hooks first\n if (key in builtins) return builtins[key]\n\n // Return cached or create new job hooks\n let hooks = cache.get(key)\n if (!hooks) {\n hooks = createJobHooks({ api, jobName: key })\n cache.set(key, hooks)\n }\n return hooks\n },\n })\n}\n"],"mappings":"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","names":["useEffect","useRef","useRef","useEffect","useCallback","useCallback","useEffect","useRef","useState","useState","useRef","useCallback","useEffect"]}
|
package/dist/spa.d.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
-
import { Durably, JobDefinition } from '@coji/durably';
|
|
2
|
+
import { Durably, RunStatus, JobDefinition } from '@coji/durably';
|
|
3
|
+
export { RunStatus } from '@coji/durably';
|
|
3
4
|
import { ReactNode } from 'react';
|
|
4
|
-
import {
|
|
5
|
-
export { D as DurablyEvent } from './types-
|
|
5
|
+
import { L as LogEntry, P as Progress, b as TypedRun } from './types-CQltMEdB.js';
|
|
6
|
+
export { D as DurablyEvent } from './types-CQltMEdB.js';
|
|
6
7
|
|
|
7
8
|
type AnyDurably = Durably<any, any>;
|
|
8
9
|
interface DurablyContextValue {
|
|
@@ -113,6 +114,14 @@ interface UseJobResult<TInput, TOutput> {
|
|
|
113
114
|
* Whether the run was cancelled
|
|
114
115
|
*/
|
|
115
116
|
isCancelled: boolean;
|
|
117
|
+
/**
|
|
118
|
+
* Whether the run reached a terminal status (completed, failed, or cancelled)
|
|
119
|
+
*/
|
|
120
|
+
isTerminal: boolean;
|
|
121
|
+
/**
|
|
122
|
+
* Whether the run is pending or leased (actively queued or executing)
|
|
123
|
+
*/
|
|
124
|
+
isActive: boolean;
|
|
116
125
|
/**
|
|
117
126
|
* Current run ID
|
|
118
127
|
*/
|
|
@@ -197,6 +206,14 @@ interface UseJobRunResult<TOutput = unknown> {
|
|
|
197
206
|
* Whether the run was cancelled
|
|
198
207
|
*/
|
|
199
208
|
isCancelled: boolean;
|
|
209
|
+
/**
|
|
210
|
+
* Whether the run reached a terminal status (completed, failed, or cancelled)
|
|
211
|
+
*/
|
|
212
|
+
isTerminal: boolean;
|
|
213
|
+
/**
|
|
214
|
+
* Whether the run is pending or leased (actively queued or executing)
|
|
215
|
+
*/
|
|
216
|
+
isActive: boolean;
|
|
200
217
|
}
|
|
201
218
|
/**
|
|
202
219
|
* Hook for subscribing to an existing run by ID.
|
|
@@ -210,9 +227,9 @@ interface UseRunsOptions {
|
|
|
210
227
|
*/
|
|
211
228
|
jobName?: string | string[];
|
|
212
229
|
/**
|
|
213
|
-
* Filter by status
|
|
230
|
+
* Filter by status(es). Pass one status, or an array for multiple (OR).
|
|
214
231
|
*/
|
|
215
|
-
status?:
|
|
232
|
+
status?: RunStatus | RunStatus[];
|
|
216
233
|
/**
|
|
217
234
|
* Filter by labels (all specified labels must match)
|
|
218
235
|
*/
|
|
@@ -228,7 +245,7 @@ interface UseRunsOptions {
|
|
|
228
245
|
*/
|
|
229
246
|
realtime?: boolean;
|
|
230
247
|
}
|
|
231
|
-
interface UseRunsResult<TInput extends Record<string, unknown> = Record<string, unknown>, TOutput extends Record<string, unknown> | undefined = Record<string, unknown
|
|
248
|
+
interface UseRunsResult<TInput extends Record<string, unknown> = Record<string, unknown>, TOutput extends Record<string, unknown> | undefined = Record<string, unknown>> {
|
|
232
249
|
/**
|
|
233
250
|
* List of runs for the current page.
|
|
234
251
|
*/
|
|
@@ -298,4 +315,4 @@ declare function useRuns<TRun extends TypedRun<Record<string, unknown>, Record<s
|
|
|
298
315
|
declare function useRuns<TName extends string, TInput extends Record<string, unknown>, TOutput extends Record<string, unknown> | undefined>(jobDefinition: JobDefinition<TName, TInput, TOutput>, options?: Omit<UseRunsOptions, 'jobName'>): UseRunsResult<TInput, TOutput>;
|
|
299
316
|
declare function useRuns(options?: UseRunsOptions): UseRunsResult;
|
|
300
317
|
|
|
301
|
-
export { DurablyProvider, type DurablyProviderProps, LogEntry, Progress,
|
|
318
|
+
export { DurablyProvider, type DurablyProviderProps, LogEntry, Progress, TypedRun, type UseJobLogsOptions, type UseJobLogsResult, type UseJobOptions, type UseJobResult, type UseJobRunOptions, type UseJobRunResult, type UseRunsOptions, type UseRunsResult, useDurably, useJob, useJobLogs, useJobRun, useRuns };
|
package/dist/spa.js
CHANGED
|
@@ -2,8 +2,9 @@ import {
|
|
|
2
2
|
initialSubscriptionState,
|
|
3
3
|
isJobDefinition,
|
|
4
4
|
subscriptionReducer,
|
|
5
|
+
useStableValue,
|
|
5
6
|
useSubscription
|
|
6
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-IXJA5WWJ.js";
|
|
7
8
|
|
|
8
9
|
// src/context.tsx
|
|
9
10
|
import { Suspense, createContext, use, useContext } from "react";
|
|
@@ -81,7 +82,7 @@ function jobSubscriptionReducer(state, action) {
|
|
|
81
82
|
return {
|
|
82
83
|
...initialSubscriptionState,
|
|
83
84
|
currentRunId: action.runId,
|
|
84
|
-
status: "leased"
|
|
85
|
+
status: action.status ?? "leased"
|
|
85
86
|
};
|
|
86
87
|
case "reset":
|
|
87
88
|
return {
|
|
@@ -123,6 +124,19 @@ function useJobSubscription(durably, jobName, options) {
|
|
|
123
124
|
}
|
|
124
125
|
})
|
|
125
126
|
);
|
|
127
|
+
unsubscribes.push(
|
|
128
|
+
durably.on("run:coalesced", (event) => {
|
|
129
|
+
if (event.jobName !== jobName) return;
|
|
130
|
+
if (followLatest) {
|
|
131
|
+
dispatch({
|
|
132
|
+
type: "switch_to_run",
|
|
133
|
+
runId: event.runId,
|
|
134
|
+
status: "pending"
|
|
135
|
+
});
|
|
136
|
+
currentRunIdRef.current = event.runId;
|
|
137
|
+
}
|
|
138
|
+
})
|
|
139
|
+
);
|
|
126
140
|
unsubscribes.push(
|
|
127
141
|
durably.on("run:complete", (event) => {
|
|
128
142
|
if (event.runId !== currentRunIdRef.current) return;
|
|
@@ -281,6 +295,8 @@ function useJob(jobDefinition, options) {
|
|
|
281
295
|
isCompleted: subscription.status === "completed",
|
|
282
296
|
isFailed: subscription.status === "failed",
|
|
283
297
|
isCancelled: subscription.status === "cancelled",
|
|
298
|
+
isTerminal: subscription.status === "completed" || subscription.status === "failed" || subscription.status === "cancelled",
|
|
299
|
+
isActive: subscription.status === "pending" || subscription.status === "leased",
|
|
284
300
|
currentRunId: subscription.currentRunId,
|
|
285
301
|
reset: subscription.reset
|
|
286
302
|
};
|
|
@@ -382,12 +398,14 @@ function useJobRun(options) {
|
|
|
382
398
|
isPending: effectiveStatus === "pending",
|
|
383
399
|
isCompleted: effectiveStatus === "completed",
|
|
384
400
|
isFailed: effectiveStatus === "failed",
|
|
385
|
-
isCancelled: effectiveStatus === "cancelled"
|
|
401
|
+
isCancelled: effectiveStatus === "cancelled",
|
|
402
|
+
isTerminal: effectiveStatus === "completed" || effectiveStatus === "failed" || effectiveStatus === "cancelled",
|
|
403
|
+
isActive: effectiveStatus === "pending" || effectiveStatus === "leased"
|
|
386
404
|
};
|
|
387
405
|
}
|
|
388
406
|
|
|
389
407
|
// src/hooks/use-runs.ts
|
|
390
|
-
import { useCallback as useCallback3, useEffect as useEffect4,
|
|
408
|
+
import { useCallback as useCallback3, useEffect as useEffect4, useState } from "react";
|
|
391
409
|
function useRuns(jobDefinitionOrOptions, optionsArg) {
|
|
392
410
|
const { durably } = useDurably();
|
|
393
411
|
const isJob = isJobDefinition(jobDefinitionOrOptions);
|
|
@@ -395,17 +413,10 @@ function useRuns(jobDefinitionOrOptions, optionsArg) {
|
|
|
395
413
|
const options = isJob ? optionsArg : jobDefinitionOrOptions;
|
|
396
414
|
const pageSize = options?.pageSize ?? 10;
|
|
397
415
|
const realtime = options?.realtime ?? true;
|
|
398
|
-
const
|
|
399
|
-
const
|
|
400
|
-
const
|
|
401
|
-
|
|
402
|
-
[jobNameKey]
|
|
403
|
-
);
|
|
404
|
-
const labelsKey = options?.labels ? JSON.stringify(options.labels) : void 0;
|
|
405
|
-
const labels = useMemo3(
|
|
406
|
-
() => labelsKey ? JSON.parse(labelsKey) : void 0,
|
|
407
|
-
[labelsKey]
|
|
408
|
-
);
|
|
416
|
+
const stableJobName = useStableValue(jobName);
|
|
417
|
+
const stableStatus = useStableValue(options?.status);
|
|
418
|
+
const labels = useStableValue(options?.labels);
|
|
419
|
+
const normalizedStatus = Array.isArray(stableStatus) && stableStatus.length === 0 ? void 0 : stableStatus;
|
|
409
420
|
const [runs, setRuns] = useState([]);
|
|
410
421
|
const [page, setPage] = useState(0);
|
|
411
422
|
const [hasMore, setHasMore] = useState(false);
|
|
@@ -416,7 +427,7 @@ function useRuns(jobDefinitionOrOptions, optionsArg) {
|
|
|
416
427
|
try {
|
|
417
428
|
const data = await durably.getRuns({
|
|
418
429
|
jobName: stableJobName,
|
|
419
|
-
status,
|
|
430
|
+
status: normalizedStatus,
|
|
420
431
|
labels,
|
|
421
432
|
limit: pageSize + 1,
|
|
422
433
|
offset: page * pageSize
|
|
@@ -426,13 +437,14 @@ function useRuns(jobDefinitionOrOptions, optionsArg) {
|
|
|
426
437
|
} finally {
|
|
427
438
|
setIsLoading(false);
|
|
428
439
|
}
|
|
429
|
-
}, [durably, stableJobName,
|
|
440
|
+
}, [durably, stableJobName, normalizedStatus, labels, pageSize, page]);
|
|
430
441
|
useEffect4(() => {
|
|
431
442
|
if (!durably) return;
|
|
432
443
|
refresh();
|
|
433
444
|
if (!realtime) return;
|
|
434
445
|
const unsubscribes = [
|
|
435
446
|
durably.on("run:trigger", refresh),
|
|
447
|
+
durably.on("run:coalesced", refresh),
|
|
436
448
|
durably.on("run:leased", refresh),
|
|
437
449
|
durably.on("run:complete", refresh),
|
|
438
450
|
durably.on("run:fail", refresh),
|