@coji/durably-react 0.12.0 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -4
- package/dist/{chunk-TGMPMPMX.js → chunk-33VIIDHK.js} +4 -4
- package/dist/chunk-33VIIDHK.js.map +1 -0
- package/dist/index.d.ts +9 -9
- package/dist/index.js +17 -16
- package/dist/index.js.map +1 -1
- package/dist/spa.d.ts +8 -8
- package/dist/spa.js +12 -12
- package/dist/spa.js.map +1 -1
- package/dist/{types-D17R7ZUn.d.ts → types-DMtqQ6Wp.d.ts} +2 -2
- package/package.json +20 -20
- package/LICENSE +0 -21
- package/dist/chunk-TGMPMPMX.js.map +0 -1
package/README.md
CHANGED
|
@@ -28,10 +28,10 @@ export const durably = createDurably<typeof durably>({
|
|
|
28
28
|
})
|
|
29
29
|
|
|
30
30
|
function MyComponent() {
|
|
31
|
-
const { trigger,
|
|
31
|
+
const { trigger, isLeased, isCompleted, output } = durably.myJob.useJob()
|
|
32
32
|
|
|
33
33
|
return (
|
|
34
|
-
<button onClick={() => trigger({ id: '123' })} disabled={
|
|
34
|
+
<button onClick={() => trigger({ id: '123' })} disabled={isLeased}>
|
|
35
35
|
Run
|
|
36
36
|
</button>
|
|
37
37
|
)
|
|
@@ -79,9 +79,9 @@ function App() {
|
|
|
79
79
|
}
|
|
80
80
|
|
|
81
81
|
function MyComponent() {
|
|
82
|
-
const { trigger,
|
|
82
|
+
const { trigger, isLeased, isCompleted } = useJob(myJob)
|
|
83
83
|
return (
|
|
84
|
-
<button onClick={() => trigger({ id: '123' })} disabled={
|
|
84
|
+
<button onClick={() => trigger({ id: '123' })} disabled={isLeased}>
|
|
85
85
|
Run
|
|
86
86
|
</button>
|
|
87
87
|
)
|
|
@@ -33,8 +33,8 @@ var initialSubscriptionState = {
|
|
|
33
33
|
};
|
|
34
34
|
function subscriptionReducer(state, action) {
|
|
35
35
|
switch (action.type) {
|
|
36
|
-
case "run:
|
|
37
|
-
return { ...state, status: "
|
|
36
|
+
case "run:leased":
|
|
37
|
+
return { ...state, status: "leased" };
|
|
38
38
|
case "run:complete":
|
|
39
39
|
return { ...state, status: "completed", output: action.output };
|
|
40
40
|
case "run:fail":
|
|
@@ -86,7 +86,7 @@ function useSubscription(subscriber, runId, options) {
|
|
|
86
86
|
const unsubscribe = subscriber.subscribe(runId, (event) => {
|
|
87
87
|
if (runIdRef.current !== runId) return;
|
|
88
88
|
switch (event.type) {
|
|
89
|
-
case "run:
|
|
89
|
+
case "run:leased":
|
|
90
90
|
case "run:cancel":
|
|
91
91
|
dispatch({ type: event.type });
|
|
92
92
|
break;
|
|
@@ -136,4 +136,4 @@ export {
|
|
|
136
136
|
useSubscription,
|
|
137
137
|
isJobDefinition
|
|
138
138
|
};
|
|
139
|
-
//# sourceMappingURL=chunk-
|
|
139
|
+
//# sourceMappingURL=chunk-33VIIDHK.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/types.ts","../src/shared/create-log-entry.ts","../src/shared/subscription-reducer.ts","../src/shared/use-subscription.ts"],"sourcesContent":["// Shared type definitions for @coji/durably-react\n\nimport type { ClientRun, JobDefinition, Run } from '@coji/durably'\n\n// Type inference utilities for extracting Input/Output types from JobDefinition\nexport type InferInput<T> =\n T extends JobDefinition<string, infer TInput, unknown>\n ? TInput extends Record<string, unknown>\n ? TInput\n : Record<string, unknown>\n : T extends { trigger: (input: infer TInput) => unknown }\n ? TInput extends Record<string, unknown>\n ? TInput\n : Record<string, unknown>\n : Record<string, unknown>\n\nexport type InferOutput<T> =\n T extends JobDefinition<string, unknown, infer TOutput>\n ? TOutput extends Record<string, unknown>\n ? TOutput\n : Record<string, unknown>\n : T extends {\n trigger: (input: unknown) => Promise<{ output?: infer TOutput }>\n }\n ? TOutput extends Record<string, unknown>\n ? TOutput\n : Record<string, unknown>\n : Record<string, unknown>\n\nexport type RunStatus =\n | 'pending'\n | 'leased'\n | 'completed'\n | 'failed'\n | 'cancelled'\n\nexport interface Progress {\n current: number\n total?: number\n message?: string\n}\n\nexport interface LogEntry {\n id: string\n runId: string\n stepName: string | null\n level: 'info' | 'warn' | 'error'\n message: string\n data: unknown\n timestamp: string\n}\n\n// Shared subscription state (used by both direct and SSE subscriptions)\nexport interface SubscriptionState<TOutput = unknown> {\n status: RunStatus | null\n output: TOutput | null\n error: string | null\n logs: LogEntry[]\n progress: Progress | null\n}\n\n// SSE event types (sent from server).\n// Note: Unlike core DurablyEvent, these omit timestamp/sequence because\n// the SSE handler in server.ts sends only the fields needed by the UI.\nexport type DurablyEvent =\n | { type: 'run:leased'; runId: string; jobName: string; input: unknown }\n | {\n type: 'run:complete'\n runId: string\n jobName: string\n output: unknown\n duration: number\n }\n | { type: 'run:fail'; runId: string; jobName: string; error: string }\n | { type: 'run:cancel'; runId: string; jobName: string }\n | { type: 'run:delete'; runId: string; jobName: string }\n | { type: 'run:trigger'; runId: string; jobName: string; input: unknown }\n | {\n type: 'run:progress'\n runId: string\n jobName: string\n progress: Progress\n }\n | {\n type: 'step:start'\n runId: string\n jobName: string\n stepName: string\n stepIndex: number\n }\n | {\n type: 'step:complete'\n runId: string\n jobName: string\n stepName: string\n stepIndex: number\n output: unknown\n }\n | {\n type: 'step:cancel'\n runId: string\n jobName: string\n stepName: string\n stepIndex: number\n labels: Record<string, string>\n }\n | {\n type: 'log:write'\n runId: string\n jobName: string\n stepName: string | null\n labels: Record<string, string>\n level: 'info' | 'warn' | 'error'\n message: string\n data: unknown\n }\n\n// =============================================================================\n// Typed Run types for useRuns hooks\n// =============================================================================\n\n/**\n * A typed version of Run with generic input/output types.\n * Used by browser hooks (direct durably access).\n */\nexport type TypedRun<\n TInput extends Record<string, unknown> = Record<string, unknown>,\n TOutput extends Record<string, unknown> | undefined =\n | Record<string, unknown>\n | undefined,\n> = Omit<Run, 'input' | 'output'> & {\n input: TInput\n output: TOutput | null\n}\n\n// ClientRun is imported from '@coji/durably' and re-exported for consumers.\nexport type { ClientRun } from '@coji/durably'\n\n/**\n * A typed version of ClientRun with generic input/output types.\n * Used by client hooks (HTTP/SSE connection).\n */\nexport type TypedClientRun<\n TInput extends Record<string, unknown> = Record<string, unknown>,\n TOutput extends Record<string, unknown> | undefined =\n | Record<string, unknown>\n | undefined,\n> = Omit<ClientRun, 'input' | 'output'> & {\n input: TInput\n output: TOutput | null\n}\n\n/**\n * Type guard to check if an object is a JobDefinition.\n * Used to distinguish between JobDefinition and options objects in overloaded functions.\n */\nexport function isJobDefinition<\n TName extends string = string,\n TInput extends Record<string, unknown> = Record<string, unknown>,\n TOutput extends Record<string, unknown> | undefined = undefined,\n>(obj: unknown): obj is JobDefinition<TName, TInput, TOutput> {\n return (\n typeof obj === 'object' &&\n obj !== null &&\n 'name' in obj &&\n 'run' in obj &&\n typeof (obj as { run: unknown }).run === 'function'\n )\n}\n","import type { LogEntry } from '../types'\n\nexport interface CreateLogEntryParams {\n runId: string\n stepName: string | null\n level: 'info' | 'warn' | 'error'\n message: string\n data: unknown\n}\n\n/**\n * Creates a LogEntry with auto-generated id and timestamp.\n * Extracted to eliminate duplication between subscription hooks.\n */\nexport function createLogEntry(params: CreateLogEntryParams): LogEntry {\n return {\n id: crypto.randomUUID(),\n runId: params.runId,\n stepName: params.stepName,\n level: params.level,\n message: params.message,\n data: params.data,\n timestamp: new Date().toISOString(),\n }\n}\n\n/**\n * Appends a log entry to the array, respecting maxLogs limit.\n */\nexport function appendLog(\n logs: LogEntry[],\n newLog: LogEntry,\n maxLogs: number,\n): LogEntry[] {\n const newLogs = [...logs, newLog]\n if (maxLogs > 0 && newLogs.length > maxLogs) {\n return newLogs.slice(-maxLogs)\n }\n return newLogs\n}\n","import type { Progress, SubscriptionState } from '../types'\nimport { appendLog, createLogEntry } from './create-log-entry'\n\n// Action types for subscription state transitions\nexport type SubscriptionAction<TOutput = unknown> =\n | { type: 'run:leased' }\n | { type: 'run:complete'; output: TOutput }\n | { type: 'run:fail'; error: string }\n | { type: 'run:cancel' }\n | { type: 'run:progress'; progress: Progress }\n | {\n type: 'log:write'\n runId: string\n stepName: string | null\n level: 'info' | 'warn' | 'error'\n message: string\n data: unknown\n maxLogs: number\n }\n | { type: 'reset' }\n | { type: 'clear_logs' }\n | { type: 'connection_error'; error: string }\n\nexport const initialSubscriptionState: SubscriptionState<unknown> = {\n status: null,\n output: null,\n error: null,\n logs: [],\n progress: null,\n}\n\n/**\n * Pure reducer for subscription state transitions.\n * Extracted to eliminate duplication between useRunSubscription and useSSESubscription.\n */\nexport function subscriptionReducer<TOutput = unknown>(\n state: SubscriptionState<TOutput>,\n action: SubscriptionAction<TOutput>,\n): SubscriptionState<TOutput> {\n switch (action.type) {\n case 'run:leased':\n return { ...state, status: 'leased' }\n\n case 'run:complete':\n return { ...state, status: 'completed', output: action.output }\n\n case 'run:fail':\n return { ...state, status: 'failed', error: action.error }\n\n case 'run:cancel':\n return { ...state, status: 'cancelled' }\n\n case 'run:progress':\n return { ...state, progress: action.progress }\n\n case 'log:write': {\n const newLog = createLogEntry({\n runId: action.runId,\n stepName: action.stepName,\n level: action.level,\n message: action.message,\n data: action.data,\n })\n return { ...state, logs: appendLog(state.logs, newLog, action.maxLogs) }\n }\n\n case 'reset':\n return initialSubscriptionState as SubscriptionState<TOutput>\n\n case 'clear_logs':\n return { ...state, logs: [] }\n\n case 'connection_error':\n return { ...state, error: action.error }\n\n default:\n return state\n }\n}\n","import { useCallback, useEffect, useReducer, useRef } from 'react'\nimport type { SubscriptionState } from '../types'\nimport type { EventSubscriber } from './event-subscriber'\nimport {\n initialSubscriptionState,\n subscriptionReducer,\n} from './subscription-reducer'\n\nexport interface UseSubscriptionOptions {\n /**\n * Maximum number of logs to keep (0 = unlimited)\n */\n maxLogs?: number\n}\n\nexport interface UseSubscriptionResult<\n TOutput = unknown,\n> extends SubscriptionState<TOutput> {\n /**\n * Clear all logs\n */\n clearLogs: () => void\n /**\n * Reset all state\n */\n reset: () => void\n}\n\n/**\n * Core subscription hook that works with any EventSubscriber implementation.\n * This unifies the subscription logic between Durably.on and SSE.\n */\nexport function useSubscription<TOutput = unknown>(\n subscriber: EventSubscriber | null,\n runId: string | null,\n options?: UseSubscriptionOptions,\n): UseSubscriptionResult<TOutput> {\n const [state, dispatch] = useReducer(\n subscriptionReducer<TOutput>,\n initialSubscriptionState as SubscriptionState<TOutput>,\n )\n\n const runIdRef = useRef<string | null>(runId)\n const prevRunIdRef = useRef<string | null>(null)\n\n const maxLogs = options?.maxLogs ?? 0\n\n // Reset state when runId changes\n if (prevRunIdRef.current !== runId) {\n prevRunIdRef.current = runId\n if (runIdRef.current !== runId) {\n dispatch({ type: 'reset' })\n }\n }\n runIdRef.current = runId\n\n useEffect(() => {\n if (!subscriber || !runId) return\n\n const unsubscribe = subscriber.subscribe<TOutput>(runId, (event) => {\n // Verify runId hasn't changed during async operation\n if (runIdRef.current !== runId) return\n\n switch (event.type) {\n case 'run:leased':\n case 'run:cancel':\n dispatch({ type: event.type })\n break\n case 'run:complete':\n dispatch({ type: 'run:complete', output: event.output })\n break\n case 'run:fail':\n dispatch({ type: 'run:fail', error: event.error })\n break\n case 'run:progress':\n dispatch({ type: 'run:progress', progress: event.progress })\n break\n case 'log:write':\n dispatch({\n type: 'log:write',\n runId: event.runId,\n stepName: event.stepName,\n level: event.level,\n message: event.message,\n data: event.data,\n maxLogs,\n })\n break\n case 'connection_error':\n dispatch({ type: 'connection_error', error: event.error })\n break\n }\n })\n\n return unsubscribe\n }, [subscriber, runId, maxLogs])\n\n const clearLogs = useCallback(() => {\n dispatch({ type: 'clear_logs' })\n }, [])\n\n const reset = useCallback(() => {\n dispatch({ type: 'reset' })\n }, [])\n\n return {\n ...state,\n clearLogs,\n reset,\n }\n}\n"],"mappings":";AA4JO,SAAS,gBAId,KAA4D;AAC5D,SACE,OAAO,QAAQ,YACf,QAAQ,QACR,UAAU,OACV,SAAS,OACT,OAAQ,IAAyB,QAAQ;AAE7C;;;AC1JO,SAAS,eAAe,QAAwC;AACrE,SAAO;AAAA,IACL,IAAI,OAAO,WAAW;AAAA,IACtB,OAAO,OAAO;AAAA,IACd,UAAU,OAAO;AAAA,IACjB,OAAO,OAAO;AAAA,IACd,SAAS,OAAO;AAAA,IAChB,MAAM,OAAO;AAAA,IACb,YAAW,oBAAI,KAAK,GAAE,YAAY;AAAA,EACpC;AACF;AAKO,SAAS,UACd,MACA,QACA,SACY;AACZ,QAAM,UAAU,CAAC,GAAG,MAAM,MAAM;AAChC,MAAI,UAAU,KAAK,QAAQ,SAAS,SAAS;AAC3C,WAAO,QAAQ,MAAM,CAAC,OAAO;AAAA,EAC/B;AACA,SAAO;AACT;;;AChBO,IAAM,2BAAuD;AAAA,EAClE,QAAQ;AAAA,EACR,QAAQ;AAAA,EACR,OAAO;AAAA,EACP,MAAM,CAAC;AAAA,EACP,UAAU;AACZ;AAMO,SAAS,oBACd,OACA,QAC4B;AAC5B,UAAQ,OAAO,MAAM;AAAA,IACnB,KAAK;AACH,aAAO,EAAE,GAAG,OAAO,QAAQ,SAAS;AAAA,IAEtC,KAAK;AACH,aAAO,EAAE,GAAG,OAAO,QAAQ,aAAa,QAAQ,OAAO,OAAO;AAAA,IAEhE,KAAK;AACH,aAAO,EAAE,GAAG,OAAO,QAAQ,UAAU,OAAO,OAAO,MAAM;AAAA,IAE3D,KAAK;AACH,aAAO,EAAE,GAAG,OAAO,QAAQ,YAAY;AAAA,IAEzC,KAAK;AACH,aAAO,EAAE,GAAG,OAAO,UAAU,OAAO,SAAS;AAAA,IAE/C,KAAK,aAAa;AAChB,YAAM,SAAS,eAAe;AAAA,QAC5B,OAAO,OAAO;AAAA,QACd,UAAU,OAAO;AAAA,QACjB,OAAO,OAAO;AAAA,QACd,SAAS,OAAO;AAAA,QAChB,MAAM,OAAO;AAAA,MACf,CAAC;AACD,aAAO,EAAE,GAAG,OAAO,MAAM,UAAU,MAAM,MAAM,QAAQ,OAAO,OAAO,EAAE;AAAA,IACzE;AAAA,IAEA,KAAK;AACH,aAAO;AAAA,IAET,KAAK;AACH,aAAO,EAAE,GAAG,OAAO,MAAM,CAAC,EAAE;AAAA,IAE9B,KAAK;AACH,aAAO,EAAE,GAAG,OAAO,OAAO,OAAO,MAAM;AAAA,IAEzC;AACE,aAAO;AAAA,EACX;AACF;;;AC9EA,SAAS,aAAa,WAAW,YAAY,cAAc;AAgCpD,SAAS,gBACd,YACA,OACA,SACgC;AAChC,QAAM,CAAC,OAAO,QAAQ,IAAI;AAAA,IACxB;AAAA,IACA;AAAA,EACF;AAEA,QAAM,WAAW,OAAsB,KAAK;AAC5C,QAAM,eAAe,OAAsB,IAAI;AAE/C,QAAM,UAAU,SAAS,WAAW;AAGpC,MAAI,aAAa,YAAY,OAAO;AAClC,iBAAa,UAAU;AACvB,QAAI,SAAS,YAAY,OAAO;AAC9B,eAAS,EAAE,MAAM,QAAQ,CAAC;AAAA,IAC5B;AAAA,EACF;AACA,WAAS,UAAU;AAEnB,YAAU,MAAM;AACd,QAAI,CAAC,cAAc,CAAC,MAAO;AAE3B,UAAM,cAAc,WAAW,UAAmB,OAAO,CAAC,UAAU;AAElE,UAAI,SAAS,YAAY,MAAO;AAEhC,cAAQ,MAAM,MAAM;AAAA,QAClB,KAAK;AAAA,QACL,KAAK;AACH,mBAAS,EAAE,MAAM,MAAM,KAAK,CAAC;AAC7B;AAAA,QACF,KAAK;AACH,mBAAS,EAAE,MAAM,gBAAgB,QAAQ,MAAM,OAAO,CAAC;AACvD;AAAA,QACF,KAAK;AACH,mBAAS,EAAE,MAAM,YAAY,OAAO,MAAM,MAAM,CAAC;AACjD;AAAA,QACF,KAAK;AACH,mBAAS,EAAE,MAAM,gBAAgB,UAAU,MAAM,SAAS,CAAC;AAC3D;AAAA,QACF,KAAK;AACH,mBAAS;AAAA,YACP,MAAM;AAAA,YACN,OAAO,MAAM;AAAA,YACb,UAAU,MAAM;AAAA,YAChB,OAAO,MAAM;AAAA,YACb,SAAS,MAAM;AAAA,YACf,MAAM,MAAM;AAAA,YACZ;AAAA,UACF,CAAC;AACD;AAAA,QACF,KAAK;AACH,mBAAS,EAAE,MAAM,oBAAoB,OAAO,MAAM,MAAM,CAAC;AACzD;AAAA,MACJ;AAAA,IACF,CAAC;AAED,WAAO;AAAA,EACT,GAAG,CAAC,YAAY,OAAO,OAAO,CAAC;AAE/B,QAAM,YAAY,YAAY,MAAM;AAClC,aAAS,EAAE,MAAM,aAAa,CAAC;AAAA,EACjC,GAAG,CAAC,CAAC;AAEL,QAAM,QAAQ,YAAY,MAAM;AAC9B,aAAS,EAAE,MAAM,QAAQ,CAAC;AAAA,EAC5B,GAAG,CAAC,CAAC;AAEL,SAAO;AAAA,IACL,GAAG;AAAA,IACH;AAAA,IACA;AAAA,EACF;AACF;","names":[]}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { R as RunStatus, L as LogEntry, P as Progress, I as InferInput, a as InferOutput, T as TypedClientRun } from './types-
|
|
2
|
-
export { D as DurablyEvent } from './types-
|
|
1
|
+
import { R as RunStatus, L as LogEntry, P as Progress, I as InferInput, a as InferOutput, T as TypedClientRun } from './types-DMtqQ6Wp.js';
|
|
2
|
+
export { D as DurablyEvent } from './types-DMtqQ6Wp.js';
|
|
3
3
|
import { JobDefinition, ClientRun } from '@coji/durably';
|
|
4
4
|
export { ClientRun } from '@coji/durably';
|
|
5
5
|
|
|
@@ -18,7 +18,7 @@ interface UseJobClientOptions {
|
|
|
18
18
|
*/
|
|
19
19
|
initialRunId?: string;
|
|
20
20
|
/**
|
|
21
|
-
* Automatically resume tracking a
|
|
21
|
+
* Automatically resume tracking a leased/pending job on mount
|
|
22
22
|
* @default true
|
|
23
23
|
*/
|
|
24
24
|
autoResume?: boolean;
|
|
@@ -68,7 +68,7 @@ interface UseJobClientResult<TInput, TOutput> {
|
|
|
68
68
|
/**
|
|
69
69
|
* Whether a run is currently running
|
|
70
70
|
*/
|
|
71
|
-
|
|
71
|
+
isLeased: boolean;
|
|
72
72
|
/**
|
|
73
73
|
* Whether a run is pending
|
|
74
74
|
*/
|
|
@@ -182,7 +182,7 @@ interface UseJobRunClientResult<TOutput = unknown> {
|
|
|
182
182
|
/**
|
|
183
183
|
* Whether a run is currently running
|
|
184
184
|
*/
|
|
185
|
-
|
|
185
|
+
isLeased: boolean;
|
|
186
186
|
/**
|
|
187
187
|
* Whether a run is pending
|
|
188
188
|
*/
|
|
@@ -254,7 +254,7 @@ interface JobHooks<TInput, TOutput> {
|
|
|
254
254
|
*
|
|
255
255
|
* // In your component - fully type-safe
|
|
256
256
|
* function CsvImporter() {
|
|
257
|
-
* const { trigger, output, progress,
|
|
257
|
+
* const { trigger, output, progress, isLeased } = importCsv.useJob()
|
|
258
258
|
*
|
|
259
259
|
* return (
|
|
260
260
|
* <button onClick={() => trigger({ rows: [...] })}>
|
|
@@ -286,7 +286,7 @@ interface UseRunActionsClientResult {
|
|
|
286
286
|
*/
|
|
287
287
|
retrigger: (runId: string) => Promise<string>;
|
|
288
288
|
/**
|
|
289
|
-
* Cancel a pending or
|
|
289
|
+
* Cancel a pending or leased run
|
|
290
290
|
*/
|
|
291
291
|
cancel: (runId: string) => Promise<void>;
|
|
292
292
|
/**
|
|
@@ -327,7 +327,7 @@ interface UseRunActionsClientResult {
|
|
|
327
327
|
* Run Again
|
|
328
328
|
* </button>
|
|
329
329
|
* )}
|
|
330
|
-
* {(status === 'pending' || status === '
|
|
330
|
+
* {(status === 'pending' || status === 'leased') && (
|
|
331
331
|
* <button onClick={() => cancel(runId)} disabled={isLoading}>
|
|
332
332
|
* Cancel
|
|
333
333
|
* </button>
|
|
@@ -504,7 +504,7 @@ type DurablyClient<T> = {
|
|
|
504
504
|
*
|
|
505
505
|
* // In your component — fully type-safe with autocomplete
|
|
506
506
|
* function CsvImporter() {
|
|
507
|
-
* const { trigger, output,
|
|
507
|
+
* const { trigger, output, isLeased } = durably.importCsv.useJob()
|
|
508
508
|
* return <button onClick={() => trigger({ rows: [...] })}>Import</button>
|
|
509
509
|
* }
|
|
510
510
|
*
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
isJobDefinition,
|
|
3
3
|
useSubscription
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-33VIIDHK.js";
|
|
5
5
|
|
|
6
6
|
// src/client/use-job.ts
|
|
7
7
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
@@ -20,8 +20,8 @@ function createSSEEventSubscriber(apiBaseUrl) {
|
|
|
20
20
|
const data = JSON.parse(messageEvent.data);
|
|
21
21
|
if (data.runId !== runId) return;
|
|
22
22
|
switch (data.type) {
|
|
23
|
-
case "run:
|
|
24
|
-
onEvent({ type: "run:
|
|
23
|
+
case "run:leased":
|
|
24
|
+
onEvent({ type: "run:leased" });
|
|
25
25
|
break;
|
|
26
26
|
case "run:complete":
|
|
27
27
|
onEvent({
|
|
@@ -53,8 +53,9 @@ function createSSEEventSubscriber(apiBaseUrl) {
|
|
|
53
53
|
}
|
|
54
54
|
};
|
|
55
55
|
eventSource.onerror = () => {
|
|
56
|
-
|
|
57
|
-
|
|
56
|
+
if (eventSource.readyState === EventSource.CLOSED) {
|
|
57
|
+
onEvent({ type: "connection_error", error: "Connection failed" });
|
|
58
|
+
}
|
|
58
59
|
};
|
|
59
60
|
return () => {
|
|
60
61
|
eventSource.close();
|
|
@@ -96,9 +97,9 @@ function useJob(options) {
|
|
|
96
97
|
const abortController = new AbortController();
|
|
97
98
|
const findActiveRun = async () => {
|
|
98
99
|
const signal = abortController.signal;
|
|
99
|
-
const [
|
|
100
|
+
const [leasedRes, pendingRes] = await Promise.all([
|
|
100
101
|
fetch(
|
|
101
|
-
`${api}/runs?${new URLSearchParams({ jobName, status: "
|
|
102
|
+
`${api}/runs?${new URLSearchParams({ jobName, status: "leased", limit: "1" })}`,
|
|
102
103
|
{ signal }
|
|
103
104
|
),
|
|
104
105
|
fetch(
|
|
@@ -107,8 +108,8 @@ function useJob(options) {
|
|
|
107
108
|
)
|
|
108
109
|
]);
|
|
109
110
|
if (hasUserTriggered.current) return;
|
|
110
|
-
if (
|
|
111
|
-
const runs = await
|
|
111
|
+
if (leasedRes.ok) {
|
|
112
|
+
const runs = await leasedRes.json();
|
|
112
113
|
if (runs.length > 0) {
|
|
113
114
|
setCurrentRunId(runs[0].id);
|
|
114
115
|
return;
|
|
@@ -137,7 +138,7 @@ function useJob(options) {
|
|
|
137
138
|
eventSource.onmessage = (event) => {
|
|
138
139
|
try {
|
|
139
140
|
const data = JSON.parse(event.data);
|
|
140
|
-
if ((data.type === "run:trigger" || data.type === "run:
|
|
141
|
+
if ((data.type === "run:trigger" || data.type === "run:leased") && data.runId) {
|
|
141
142
|
setCurrentRunId(data.runId);
|
|
142
143
|
}
|
|
143
144
|
} catch {
|
|
@@ -226,7 +227,7 @@ function useJob(options) {
|
|
|
226
227
|
error: subscription.error,
|
|
227
228
|
logs: subscription.logs,
|
|
228
229
|
progress: subscription.progress,
|
|
229
|
-
|
|
230
|
+
isLeased: effectiveStatus === "leased",
|
|
230
231
|
isPending: effectiveStatus === "pending",
|
|
231
232
|
isCompleted: effectiveStatus === "completed",
|
|
232
233
|
isFailed: effectiveStatus === "failed",
|
|
@@ -255,14 +256,14 @@ function useJobRun(options) {
|
|
|
255
256
|
const isCompleted = effectiveStatus === "completed";
|
|
256
257
|
const isFailed = effectiveStatus === "failed";
|
|
257
258
|
const isPending = effectiveStatus === "pending";
|
|
258
|
-
const
|
|
259
|
+
const isLeased = effectiveStatus === "leased";
|
|
259
260
|
const isCancelled = effectiveStatus === "cancelled";
|
|
260
261
|
const prevStatusRef = useRef2(null);
|
|
261
262
|
useEffect2(() => {
|
|
262
263
|
const prevStatus = prevStatusRef.current;
|
|
263
264
|
prevStatusRef.current = effectiveStatus;
|
|
264
265
|
if (prevStatus !== effectiveStatus) {
|
|
265
|
-
if (prevStatus === null && (isPending ||
|
|
266
|
+
if (prevStatus === null && (isPending || isLeased) && onStart) {
|
|
266
267
|
onStart();
|
|
267
268
|
}
|
|
268
269
|
if (isCompleted && onComplete) {
|
|
@@ -275,7 +276,7 @@ function useJobRun(options) {
|
|
|
275
276
|
}, [
|
|
276
277
|
effectiveStatus,
|
|
277
278
|
isPending,
|
|
278
|
-
|
|
279
|
+
isLeased,
|
|
279
280
|
isCompleted,
|
|
280
281
|
isFailed,
|
|
281
282
|
onStart,
|
|
@@ -288,7 +289,7 @@ function useJobRun(options) {
|
|
|
288
289
|
error: subscription.error,
|
|
289
290
|
logs: subscription.logs,
|
|
290
291
|
progress: subscription.progress,
|
|
291
|
-
|
|
292
|
+
isLeased,
|
|
292
293
|
isPending,
|
|
293
294
|
isCompleted,
|
|
294
295
|
isFailed,
|
|
@@ -512,7 +513,7 @@ function useRuns(jobDefinitionOrOptions, optionsArg) {
|
|
|
512
513
|
eventSource.onmessage = (event) => {
|
|
513
514
|
try {
|
|
514
515
|
const data = JSON.parse(event.data);
|
|
515
|
-
if (data.type === "run:trigger" || data.type === "run:
|
|
516
|
+
if (data.type === "run:trigger" || data.type === "run:leased" || data.type === "run:complete" || data.type === "run:fail" || data.type === "run:cancel" || data.type === "run:delete") {
|
|
516
517
|
refresh();
|
|
517
518
|
}
|
|
518
519
|
if (data.type === "run:progress") {
|
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 running/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 isRunning: 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 running/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 running and pending in parallel\n const signal = abortController.signal\n const [runningRes, pendingRes] = await Promise.all([\n fetch(\n `${api}/runs?${new URLSearchParams({ jobName, status: 'running', 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 running over pending\n if (runningRes.ok) {\n const runs = (await runningRes.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:start 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:start') &&\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 isRunning: effectiveStatus === 'running',\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:start':\n onEvent({ type: 'run:start' })\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 eventSource.onerror = () => {\n onEvent({ type: 'connection_error', error: 'Connection failed' })\n eventSource.close()\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 isRunning: 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 isRunning = effectiveStatus === 'running'\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 || isRunning) && 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 isRunning,\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 isRunning,\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, isRunning } = 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 running 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 === 'running') && (\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:start'\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:start' ||\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, isRunning } = 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 * 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"]}
|
package/dist/spa.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
2
|
import { Durably, JobDefinition } from '@coji/durably';
|
|
3
3
|
import { ReactNode } from 'react';
|
|
4
|
-
import { R as RunStatus, L as LogEntry, P as Progress, b as TypedRun } from './types-
|
|
5
|
-
export { D as DurablyEvent } from './types-
|
|
4
|
+
import { R as RunStatus, L as LogEntry, P as Progress, b as TypedRun } from './types-DMtqQ6Wp.js';
|
|
5
|
+
export { D as DurablyEvent } from './types-DMtqQ6Wp.js';
|
|
6
6
|
|
|
7
7
|
type AnyDurably = Durably<any, any>;
|
|
8
8
|
interface DurablyContextValue {
|
|
@@ -94,9 +94,9 @@ interface UseJobResult<TInput, TOutput> {
|
|
|
94
94
|
*/
|
|
95
95
|
progress: Progress | null;
|
|
96
96
|
/**
|
|
97
|
-
* Whether a run is currently
|
|
97
|
+
* Whether a run is currently leased (being executed by a worker)
|
|
98
98
|
*/
|
|
99
|
-
|
|
99
|
+
isLeased: boolean;
|
|
100
100
|
/**
|
|
101
101
|
* Whether a run is pending
|
|
102
102
|
*/
|
|
@@ -178,9 +178,9 @@ interface UseJobRunResult<TOutput = unknown> {
|
|
|
178
178
|
*/
|
|
179
179
|
progress: Progress | null;
|
|
180
180
|
/**
|
|
181
|
-
* Whether a run is currently
|
|
181
|
+
* Whether a run is currently leased (being executed by a worker)
|
|
182
182
|
*/
|
|
183
|
-
|
|
183
|
+
isLeased: boolean;
|
|
184
184
|
/**
|
|
185
185
|
* Whether a run is pending
|
|
186
186
|
*/
|
|
@@ -212,7 +212,7 @@ interface UseRunsOptions {
|
|
|
212
212
|
/**
|
|
213
213
|
* Filter by status
|
|
214
214
|
*/
|
|
215
|
-
status?: 'pending' | '
|
|
215
|
+
status?: 'pending' | 'leased' | 'completed' | 'failed' | 'cancelled';
|
|
216
216
|
/**
|
|
217
217
|
* Filter by labels (all specified labels must match)
|
|
218
218
|
*/
|
|
@@ -230,7 +230,7 @@ interface UseRunsOptions {
|
|
|
230
230
|
}
|
|
231
231
|
interface UseRunsResult<TInput extends Record<string, unknown> = Record<string, unknown>, TOutput extends Record<string, unknown> | undefined = Record<string, unknown> | undefined> {
|
|
232
232
|
/**
|
|
233
|
-
* List of runs for the current page
|
|
233
|
+
* List of runs for the current page.
|
|
234
234
|
*/
|
|
235
235
|
runs: TypedRun<TInput, TOutput>[];
|
|
236
236
|
/**
|
package/dist/spa.js
CHANGED
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
isJobDefinition,
|
|
4
4
|
subscriptionReducer,
|
|
5
5
|
useSubscription
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-33VIIDHK.js";
|
|
7
7
|
|
|
8
8
|
// src/context.tsx
|
|
9
9
|
import { Suspense, createContext, use, useContext } from "react";
|
|
@@ -50,10 +50,10 @@ function useAutoResume(jobHandle, options, callbacks) {
|
|
|
50
50
|
if (skipIfInitialRunId && initialRunId) return;
|
|
51
51
|
let cancelled = false;
|
|
52
52
|
const findActiveRun = async () => {
|
|
53
|
-
const
|
|
53
|
+
const leasedRuns = await jobHandle.getRuns({ status: "leased" });
|
|
54
54
|
if (cancelled) return;
|
|
55
|
-
if (
|
|
56
|
-
const run =
|
|
55
|
+
if (leasedRuns.length > 0) {
|
|
56
|
+
const run = leasedRuns[0];
|
|
57
57
|
callbacks.onRunFound(run.id, run.status);
|
|
58
58
|
return;
|
|
59
59
|
}
|
|
@@ -81,7 +81,7 @@ function jobSubscriptionReducer(state, action) {
|
|
|
81
81
|
return {
|
|
82
82
|
...initialSubscriptionState,
|
|
83
83
|
currentRunId: action.runId,
|
|
84
|
-
status: "
|
|
84
|
+
status: "leased"
|
|
85
85
|
};
|
|
86
86
|
case "reset":
|
|
87
87
|
return {
|
|
@@ -112,14 +112,14 @@ function useJobSubscription(durably, jobName, options) {
|
|
|
112
112
|
if (!durably) return;
|
|
113
113
|
const unsubscribes = [];
|
|
114
114
|
unsubscribes.push(
|
|
115
|
-
durably.on("run:
|
|
115
|
+
durably.on("run:leased", (event) => {
|
|
116
116
|
if (event.jobName !== jobName) return;
|
|
117
117
|
if (followLatest) {
|
|
118
118
|
dispatch({ type: "switch_to_run", runId: event.runId });
|
|
119
119
|
currentRunIdRef.current = event.runId;
|
|
120
120
|
} else {
|
|
121
121
|
if (event.runId !== currentRunIdRef.current) return;
|
|
122
|
-
dispatch({ type: "run:
|
|
122
|
+
dispatch({ type: "run:leased" });
|
|
123
123
|
}
|
|
124
124
|
})
|
|
125
125
|
);
|
|
@@ -276,7 +276,7 @@ function useJob(jobDefinition, options) {
|
|
|
276
276
|
error: subscription.error,
|
|
277
277
|
logs: subscription.logs,
|
|
278
278
|
progress: subscription.progress,
|
|
279
|
-
|
|
279
|
+
isLeased: subscription.status === "leased",
|
|
280
280
|
isPending: subscription.status === "pending",
|
|
281
281
|
isCompleted: subscription.status === "completed",
|
|
282
282
|
isFailed: subscription.status === "failed",
|
|
@@ -295,9 +295,9 @@ function createDurablyEventSubscriber(durably) {
|
|
|
295
295
|
subscribe(runId, onEvent) {
|
|
296
296
|
const unsubscribes = [];
|
|
297
297
|
unsubscribes.push(
|
|
298
|
-
durably.on("run:
|
|
298
|
+
durably.on("run:leased", (event) => {
|
|
299
299
|
if (event.runId !== runId) return;
|
|
300
|
-
onEvent({ type: "run:
|
|
300
|
+
onEvent({ type: "run:leased" });
|
|
301
301
|
})
|
|
302
302
|
);
|
|
303
303
|
unsubscribes.push(
|
|
@@ -378,7 +378,7 @@ function useJobRun(options) {
|
|
|
378
378
|
error: subscription.error,
|
|
379
379
|
logs: subscription.logs,
|
|
380
380
|
progress: subscription.progress,
|
|
381
|
-
|
|
381
|
+
isLeased: effectiveStatus === "leased",
|
|
382
382
|
isPending: effectiveStatus === "pending",
|
|
383
383
|
isCompleted: effectiveStatus === "completed",
|
|
384
384
|
isFailed: effectiveStatus === "failed",
|
|
@@ -433,7 +433,7 @@ function useRuns(jobDefinitionOrOptions, optionsArg) {
|
|
|
433
433
|
if (!realtime) return;
|
|
434
434
|
const unsubscribes = [
|
|
435
435
|
durably.on("run:trigger", refresh),
|
|
436
|
-
durably.on("run:
|
|
436
|
+
durably.on("run:leased", refresh),
|
|
437
437
|
durably.on("run:complete", refresh),
|
|
438
438
|
durably.on("run:fail", refresh),
|
|
439
439
|
durably.on("run:cancel", refresh),
|
package/dist/spa.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/context.tsx","../src/hooks/use-job.ts","../src/hooks/use-auto-resume.ts","../src/hooks/use-job-subscription.ts","../src/hooks/use-run-subscription.ts","../src/shared/durably-event-subscriber.ts","../src/hooks/use-job-logs.ts","../src/hooks/use-job-run.ts","../src/hooks/use-runs.ts"],"sourcesContent":["import type { Durably } from '@coji/durably'\nimport { Suspense, createContext, use, useContext, type ReactNode } from 'react'\n\n// biome-ignore lint/suspicious/noExplicitAny: Durably context accepts any job/label configuration\ntype AnyDurably = Durably<any, any>\n\ninterface DurablyContextValue {\n durably: AnyDurably\n}\n\nconst DurablyContext = createContext<DurablyContextValue | null>(null)\n\nexport interface DurablyProviderProps {\n /**\n * Durably instance or Promise that resolves to one.\n * The instance should already be initialized via `await durably.init()`.\n *\n * When passing a Promise, wrap the provider with Suspense or use the fallback prop.\n *\n * @example\n * // With Suspense (recommended)\n * <Suspense fallback={<Loading />}>\n * <DurablyProvider durably={durablyPromise}>\n * <App />\n * </DurablyProvider>\n * </Suspense>\n *\n * @example\n * // With fallback prop\n * <DurablyProvider durably={durablyPromise} fallback={<Loading />}>\n * <App />\n * </DurablyProvider>\n */\n durably: AnyDurably | Promise<AnyDurably>\n /**\n * Fallback to show while waiting for the Durably Promise to resolve.\n * This wraps the provider content in a Suspense boundary automatically.\n */\n fallback?: ReactNode\n children: ReactNode\n}\n\n/**\n * Internal component that uses the `use()` hook to resolve the Promise\n */\nfunction DurablyProviderInner({\n durably: durablyOrPromise,\n children,\n}: Omit<DurablyProviderProps, 'fallback'>) {\n const durably =\n durablyOrPromise instanceof Promise\n ? use(durablyOrPromise)\n : durablyOrPromise\n\n return (\n <DurablyContext.Provider value={{ durably }}>\n {children}\n </DurablyContext.Provider>\n )\n}\n\nexport function DurablyProvider({\n durably,\n fallback,\n children,\n}: DurablyProviderProps) {\n const inner = (\n <DurablyProviderInner durably={durably}>{children}</DurablyProviderInner>\n )\n\n if (fallback !== undefined) {\n return <Suspense fallback={fallback}>{inner}</Suspense>\n }\n\n return inner\n}\n\nexport function useDurably(): DurablyContextValue {\n const context = useContext(DurablyContext)\n if (!context) {\n throw new Error('useDurably must be used within a DurablyProvider')\n }\n return context\n}\n","import type { JobDefinition, JobHandle } from '@coji/durably'\nimport { useCallback, useEffect, useMemo, useRef } from 'react'\nimport { useDurably } from '../context'\nimport type { LogEntry, Progress, RunStatus } from '../types'\nimport { useAutoResume } from './use-auto-resume'\nimport { useJobSubscription } from './use-job-subscription'\n\nexport interface UseJobOptions {\n /**\n * Initial Run ID to subscribe to (for reconnection scenarios)\n */\n initialRunId?: string\n /**\n * Automatically resume tracking any pending or running job on initialization.\n * If a pending or running run exists for this job, the hook will subscribe to it.\n * @default true\n */\n autoResume?: boolean\n /**\n * Automatically switch to tracking the latest running job when a new run starts.\n * When true, the hook will update to track any new run for this job as soon as it starts running.\n * When false, the hook will only track the run that was triggered or explicitly set.\n * @default true\n */\n followLatest?: boolean\n}\n\nexport interface UseJobResult<TInput, TOutput> {\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 isRunning: 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\nexport function useJob<\n TName extends string,\n TInput extends Record<string, unknown>,\n // biome-ignore lint/suspicious/noConfusingVoidType: TOutput can be void for jobs without return value\n TOutput extends Record<string, unknown> | void,\n>(\n jobDefinition: JobDefinition<TName, TInput, TOutput>,\n options?: UseJobOptions,\n): UseJobResult<TInput, TOutput> {\n const { durably } = useDurably()\n\n const jobHandleRef = useRef<JobHandle<TName, TInput, TOutput> | null>(null)\n\n // Register job\n useEffect(() => {\n if (!durably) return\n\n const d = durably.register({\n _job: jobDefinition,\n })\n jobHandleRef.current = d.jobs._job\n }, [durably, jobDefinition])\n\n // Use the extracted job subscription hook\n const subscription = useJobSubscription<TOutput>(\n durably,\n jobDefinition.name,\n {\n followLatest: options?.followLatest,\n },\n )\n\n // Auto-resume callbacks - stable reference\n const autoResumeCallbacks = useMemo(\n () => ({\n onRunFound: (runId: string, _status: RunStatus) => {\n subscription.setCurrentRunId(runId)\n },\n }),\n [subscription.setCurrentRunId],\n )\n\n // Use the extracted auto-resume hook\n useAutoResume(\n jobHandleRef.current,\n {\n enabled: options?.autoResume,\n initialRunId: options?.initialRunId,\n },\n autoResumeCallbacks,\n )\n\n // Handle initialRunId - set it to start tracking\n useEffect(() => {\n if (!durably || !options?.initialRunId) return\n\n subscription.setCurrentRunId(options.initialRunId)\n }, [durably, options?.initialRunId, subscription.setCurrentRunId])\n\n const trigger = useCallback(\n async (input: TInput): Promise<{ runId: string }> => {\n const jobHandle = jobHandleRef.current\n if (!jobHandle) {\n throw new Error('Job not ready')\n }\n\n // Reset state before triggering\n subscription.reset()\n\n const run = await jobHandle.trigger(input)\n subscription.setCurrentRunId(run.id)\n\n return { runId: run.id }\n },\n [subscription],\n )\n\n const triggerAndWait = useCallback(\n async (input: TInput): Promise<{ runId: string; output: TOutput }> => {\n const jobHandle = jobHandleRef.current\n if (!jobHandle || !durably) {\n throw new Error('Job not ready')\n }\n\n // Reset state before triggering\n subscription.reset()\n\n const run = await jobHandle.trigger(input)\n subscription.setCurrentRunId(run.id)\n\n // Wait for completion by polling\n return new Promise((resolve, reject) => {\n const checkCompletion = async () => {\n const updatedRun = await jobHandle.getRun(run.id)\n if (!updatedRun) {\n reject(new Error('Run not found'))\n return\n }\n\n if (updatedRun.status === 'completed') {\n resolve({ runId: run.id, output: updatedRun.output as TOutput })\n } else if (updatedRun.status === 'failed') {\n reject(new Error(updatedRun.error ?? 'Job failed'))\n } else if (updatedRun.status === 'cancelled') {\n reject(new Error('Job cancelled'))\n } else {\n // Still running, check again\n setTimeout(checkCompletion, 50)\n }\n }\n checkCompletion()\n })\n },\n [durably, subscription],\n )\n\n return {\n trigger,\n triggerAndWait,\n status: subscription.status,\n output: subscription.output,\n error: subscription.error,\n logs: subscription.logs,\n progress: subscription.progress,\n isRunning: subscription.status === 'running',\n isPending: subscription.status === 'pending',\n isCompleted: subscription.status === 'completed',\n isFailed: subscription.status === 'failed',\n isCancelled: subscription.status === 'cancelled',\n currentRunId: subscription.currentRunId,\n reset: subscription.reset,\n }\n}\n","import type { JobHandle } from '@coji/durably'\nimport { useEffect } from 'react'\nimport type { RunStatus } from '../types'\n\nexport interface UseAutoResumeOptions {\n /**\n * Whether to automatically resume tracking pending/running runs\n * @default true\n */\n enabled?: boolean\n /**\n * Skip auto-resume if an initial run ID is provided\n */\n skipIfInitialRunId?: boolean\n /**\n * Initial run ID (if provided, auto-resume is skipped)\n */\n initialRunId?: string\n}\n\nexport interface UseAutoResumeCallbacks {\n /**\n * Called when a run is found to resume\n */\n onRunFound: (runId: string, status: RunStatus) => void\n}\n\n/**\n * Hook that automatically finds and resumes tracking of pending/running runs.\n * Extracted from useJob to separate the auto-resume concern.\n */\nexport function useAutoResume<\n TName extends string,\n TInput extends Record<string, unknown>,\n TOutput,\n>(\n jobHandle: JobHandle<TName, TInput, TOutput> | null,\n options: UseAutoResumeOptions,\n callbacks: UseAutoResumeCallbacks,\n): void {\n const enabled = options.enabled !== false\n const skipIfInitialRunId = options.skipIfInitialRunId !== false\n const initialRunId = options.initialRunId\n\n useEffect(() => {\n if (!jobHandle) return\n if (!enabled) return\n if (skipIfInitialRunId && initialRunId) return\n\n let cancelled = false\n\n const findActiveRun = async () => {\n // First check for running runs\n const runningRuns = await jobHandle.getRuns({ status: 'running' })\n if (cancelled) return\n\n if (runningRuns.length > 0) {\n const run = runningRuns[0]\n callbacks.onRunFound(run.id, run.status as RunStatus)\n return\n }\n\n // Then check for pending runs\n const pendingRuns = await jobHandle.getRuns({ status: 'pending' })\n if (cancelled) return\n\n if (pendingRuns.length > 0) {\n const run = pendingRuns[0]\n callbacks.onRunFound(run.id, run.status as RunStatus)\n }\n }\n\n findActiveRun()\n\n return () => {\n cancelled = true\n }\n }, [jobHandle, enabled, skipIfInitialRunId, initialRunId, callbacks])\n}\n","import type { Durably } from '@coji/durably'\nimport { useCallback, useEffect, useReducer, useRef } from 'react'\nimport {\n initialSubscriptionState,\n subscriptionReducer,\n type SubscriptionAction,\n} from '../shared/subscription-reducer'\nimport type { SubscriptionState } from '../types'\n\nexport interface UseJobSubscriptionOptions {\n /**\n * Automatically switch to tracking the latest running job when a new run starts.\n * @default true\n */\n followLatest?: boolean\n /**\n * Maximum number of logs to keep (0 = unlimited)\n */\n maxLogs?: number\n}\n\nexport interface UseJobSubscriptionResult<\n TOutput = unknown,\n> extends SubscriptionState<TOutput> {\n /**\n * Current run ID being tracked\n */\n currentRunId: string | null\n /**\n * Set the current run ID to track\n */\n setCurrentRunId: (runId: string | null) => void\n /**\n * Clear all logs\n */\n clearLogs: () => void\n /**\n * Reset all state including currentRunId\n */\n reset: () => void\n}\n\n// Extended state for job subscription (includes currentRunId)\ninterface JobSubscriptionState<\n TOutput = unknown,\n> extends SubscriptionState<TOutput> {\n currentRunId: string | null\n}\n\n// Extended actions for job subscription\ntype JobSubscriptionAction<TOutput = unknown> =\n | SubscriptionAction<TOutput>\n | { type: 'set_run_id'; runId: string | null }\n | {\n type: 'switch_to_run'\n runId: string\n }\n\nfunction jobSubscriptionReducer<TOutput = unknown>(\n state: JobSubscriptionState<TOutput>,\n action: JobSubscriptionAction<TOutput>,\n): JobSubscriptionState<TOutput> {\n switch (action.type) {\n case 'set_run_id':\n return { ...state, currentRunId: action.runId }\n\n case 'switch_to_run':\n // Switch to a new run, resetting state\n return {\n ...initialSubscriptionState,\n currentRunId: action.runId,\n status: 'running',\n } as JobSubscriptionState<TOutput>\n\n case 'reset':\n return {\n ...(initialSubscriptionState as SubscriptionState<TOutput>),\n currentRunId: null,\n }\n\n default:\n // Delegate to base subscription reducer\n return {\n ...subscriptionReducer(state, action as SubscriptionAction<TOutput>),\n currentRunId: state.currentRunId,\n }\n }\n}\n\n/**\n * Hook for subscribing to job events with followLatest support.\n * This is a specialized version of useSubscription for job-level tracking.\n */\nexport function useJobSubscription<TOutput = unknown>(\n durably: Durably | null,\n jobName: string,\n options?: UseJobSubscriptionOptions,\n): UseJobSubscriptionResult<TOutput> {\n const initialState: JobSubscriptionState<TOutput> = {\n ...(initialSubscriptionState as SubscriptionState<TOutput>),\n currentRunId: null,\n }\n\n const [state, dispatch] = useReducer(\n jobSubscriptionReducer<TOutput>,\n initialState,\n )\n\n const currentRunIdRef = useRef<string | null>(null)\n currentRunIdRef.current = state.currentRunId\n\n const followLatest = options?.followLatest !== false\n const maxLogs = options?.maxLogs ?? 0\n\n useEffect(() => {\n if (!durably) return\n\n const unsubscribes: (() => void)[] = []\n\n unsubscribes.push(\n durably.on('run:start', (event) => {\n if (event.jobName !== jobName) return\n\n if (followLatest) {\n // Switch to tracking the new run\n dispatch({ type: 'switch_to_run', runId: event.runId })\n currentRunIdRef.current = event.runId\n } else {\n // Only update if this is our current run\n if (event.runId !== currentRunIdRef.current) return\n dispatch({ type: 'run:start' })\n }\n }),\n )\n\n unsubscribes.push(\n durably.on('run:complete', (event) => {\n if (event.runId !== currentRunIdRef.current) return\n dispatch({ type: 'run:complete', output: event.output as TOutput })\n }),\n )\n\n unsubscribes.push(\n durably.on('run:fail', (event) => {\n if (event.runId !== currentRunIdRef.current) return\n dispatch({ type: 'run:fail', error: event.error })\n }),\n )\n\n unsubscribes.push(\n durably.on('run:cancel', (event) => {\n if (event.runId !== currentRunIdRef.current) return\n dispatch({ type: 'run:cancel' })\n }),\n )\n\n unsubscribes.push(\n durably.on('run:progress', (event) => {\n if (event.runId !== currentRunIdRef.current) return\n dispatch({ type: 'run:progress', progress: event.progress })\n }),\n )\n\n unsubscribes.push(\n durably.on('log:write', (event) => {\n if (event.runId !== currentRunIdRef.current) return\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 }),\n )\n\n return () => {\n for (const unsubscribe of unsubscribes) {\n unsubscribe()\n }\n }\n }, [durably, jobName, followLatest, maxLogs])\n\n const setCurrentRunId = useCallback((runId: string | null) => {\n dispatch({ type: 'set_run_id', runId })\n currentRunIdRef.current = runId\n }, [])\n\n const clearLogs = useCallback(() => {\n dispatch({ type: 'clear_logs' })\n }, [])\n\n const reset = useCallback(() => {\n dispatch({ type: 'reset' })\n currentRunIdRef.current = null\n }, [])\n\n return {\n ...state,\n setCurrentRunId,\n clearLogs,\n reset,\n }\n}\n","import type { Durably } from '@coji/durably'\nimport { useMemo } from 'react'\nimport { createDurablyEventSubscriber } from '../shared/durably-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 RunSubscriptionState<TOutput = unknown> = SubscriptionState<TOutput>\n\n/** @deprecated Use UseSubscriptionOptions from '../shared/use-subscription' instead */\nexport type UseRunSubscriptionOptions = UseSubscriptionOptions\n\n/** @deprecated Use UseSubscriptionResult from '../shared/use-subscription' instead */\nexport type UseRunSubscriptionResult<TOutput = unknown> =\n UseSubscriptionResult<TOutput>\n\n/**\n * Internal hook for subscribing to run events via Durably.on().\n * Shared by useJob, useJobRun, and useJobLogs.\n *\n * @deprecated Consider using useSubscription with createDurablyEventSubscriber directly.\n */\nexport function useRunSubscription<TOutput = unknown>(\n durably: Durably | null,\n runId: string | null,\n options?: UseRunSubscriptionOptions,\n): UseRunSubscriptionResult<TOutput> {\n const subscriber = useMemo(\n () => (durably ? createDurablyEventSubscriber(durably) : null),\n [durably],\n )\n\n return useSubscription<TOutput>(subscriber, runId, options)\n}\n","import type { Durably } from '@coji/durably'\nimport type { EventSubscriber, SubscriptionEvent } from './event-subscriber'\n\n/**\n * EventSubscriber implementation using Durably.on() for direct subscriptions.\n * Used in browser environments where Durably instance is available.\n */\nexport function createDurablyEventSubscriber(\n durably: Durably,\n): EventSubscriber {\n return {\n subscribe<TOutput = unknown>(\n runId: string,\n onEvent: (event: SubscriptionEvent<TOutput>) => void,\n ): () => void {\n const unsubscribes: (() => void)[] = []\n\n unsubscribes.push(\n durably.on('run:start', (event) => {\n if (event.runId !== runId) return\n onEvent({ type: 'run:start' })\n }),\n )\n\n unsubscribes.push(\n durably.on('run:complete', (event) => {\n if (event.runId !== runId) return\n onEvent({ type: 'run:complete', output: event.output as TOutput })\n }),\n )\n\n unsubscribes.push(\n durably.on('run:fail', (event) => {\n if (event.runId !== runId) return\n onEvent({ type: 'run:fail', error: event.error })\n }),\n )\n\n unsubscribes.push(\n durably.on('run:cancel', (event) => {\n if (event.runId !== runId) return\n onEvent({ type: 'run:cancel' })\n }),\n )\n\n unsubscribes.push(\n durably.on('run:progress', (event) => {\n if (event.runId !== runId) return\n onEvent({ type: 'run:progress', progress: event.progress })\n }),\n )\n\n unsubscribes.push(\n durably.on('log:write', (event) => {\n if (event.runId !== runId) return\n onEvent({\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 })\n }),\n )\n\n return () => {\n for (const unsubscribe of unsubscribes) {\n unsubscribe()\n }\n }\n },\n }\n}\n","import { useDurably } from '../context'\nimport type { LogEntry } from '../types'\nimport { useRunSubscription } from './use-run-subscription'\n\nexport interface UseJobLogsOptions {\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 UseJobLogsResult {\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.\n * Use this when you only need logs, not full run status.\n */\nexport function useJobLogs(options: UseJobLogsOptions): UseJobLogsResult {\n const { durably } = useDurably()\n const { runId, maxLogs } = options\n\n const subscription = useRunSubscription(durably, runId, { maxLogs })\n\n return {\n logs: subscription.logs,\n clearLogs: subscription.clearLogs,\n }\n}\n","import { useDurably } from '../context'\nimport type { LogEntry, Progress, RunStatus } from '../types'\nimport { useRunSubscription } from './use-run-subscription'\n\n// Note: Unlike UseJobRunClientOptions (client mode), this interface intentionally\n// omits onStart/onComplete/onFail callbacks. In browser mode, use durably.on()\n// directly for event callbacks.\nexport interface UseJobRunOptions {\n /**\n * The run ID to subscribe to\n */\n runId: string | null\n}\n\nexport interface UseJobRunResult<TOutput = unknown> {\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 isRunning: 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 by ID.\n * Use this when you have a runId and want to track its status.\n */\nexport function useJobRun<TOutput = unknown>(\n options: UseJobRunOptions,\n): UseJobRunResult<TOutput> {\n const { durably } = useDurably()\n const { runId } = options\n\n const subscription = useRunSubscription<TOutput>(durably, 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 return {\n status: effectiveStatus,\n output: subscription.output,\n error: subscription.error,\n logs: subscription.logs,\n progress: subscription.progress,\n isRunning: effectiveStatus === 'running',\n isPending: effectiveStatus === 'pending',\n isCompleted: effectiveStatus === 'completed',\n isFailed: effectiveStatus === 'failed',\n isCancelled: effectiveStatus === 'cancelled',\n }\n}\n","import type { JobDefinition } from '@coji/durably'\nimport { useCallback, useEffect, useMemo, useState } from 'react'\nimport { useDurably } from '../context'\nimport { type TypedRun, isJobDefinition } from '../types'\n\n// Re-export TypedRun for convenience\nexport type { TypedRun } from '../types'\n\nexport interface UseRunsOptions {\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?: 'pending' | 'running' | 'completed' | 'failed' | 'cancelled'\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\n * @default true\n */\n realtime?: boolean\n}\n\n// Note: Unlike UseRunsClientResult (client mode), this interface intentionally\n// omits `error` because browser mode operates on a local SQLite database\n// where network errors don't occur.\nexport interface UseRunsResult<\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: TypedRun<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 * 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 with pagination and real-time updates.\n *\n * @example With generic type parameter (dashboard with multiple job types)\n * ```tsx\n * type DashboardRun = TypedRun<ImportInput, ImportOutput> | TypedRun<SyncInput, SyncOutput>\n *\n * function Dashboard() {\n * const { runs } = useRuns<DashboardRun>({ 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 Dashboard() {\n * const { runs } = useRuns(myJob)\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 Dashboard() {\n * const { runs } = useRuns({ pageSize: 20 })\n * // runs[0].output is unknown\n * }\n * ```\n */\n// Overload 1: With generic type parameter\nexport function useRuns<\n TRun extends TypedRun<\n Record<string, unknown>,\n Record<string, unknown> | undefined\n >,\n>(\n options?: UseRunsOptions,\n): UseRunsResult<\n TRun extends TypedRun<infer I, infer _O> ? I : Record<string, unknown>,\n TRun extends TypedRun<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<UseRunsOptions, 'jobName'>,\n): UseRunsResult<TInput, TOutput>\n\n// Overload 3: Without type parameter (untyped, backward compatible)\nexport function useRuns(options?: UseRunsOptions): UseRunsResult\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 | UseRunsOptions,\n optionsArg?: Omit<UseRunsOptions, 'jobName'>,\n): UseRunsResult<TInput, TOutput> {\n const { durably } = useDurably()\n\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 UseRunsOptions | undefined)?.jobName\n\n const options = isJob\n ? optionsArg\n : (jobDefinitionOrOptions as UseRunsOptions | undefined)\n\n const pageSize = options?.pageSize ?? 10\n const realtime = options?.realtime ?? true\n const status = options?.status\n\n // Stabilize jobName reference to prevent re-fetch loops 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 // Stabilize labels reference to prevent infinite re-renders\n const labelsKey = options?.labels ? JSON.stringify(options.labels) : undefined\n const labels = useMemo(\n () =>\n labelsKey ? (JSON.parse(labelsKey) as Record<string, string>) : undefined,\n [labelsKey],\n )\n\n const [runs, setRuns] = useState<TypedRun<TInput, TOutput>[]>([])\n const [page, setPage] = useState(0)\n const [hasMore, setHasMore] = useState(false)\n const [isLoading, setIsLoading] = useState(true)\n\n const refresh = useCallback(async () => {\n if (!durably) return\n\n setIsLoading(true)\n try {\n const data = await durably.getRuns({\n jobName: stableJobName,\n status,\n labels,\n limit: pageSize + 1,\n offset: page * pageSize,\n })\n setHasMore(data.length > pageSize)\n setRuns(data.slice(0, pageSize) as TypedRun<TInput, TOutput>[])\n } finally {\n setIsLoading(false)\n }\n }, [durably, stableJobName, status, labels, pageSize, page])\n\n // Initial fetch and subscribe to events\n useEffect(() => {\n if (!durably) return\n\n refresh()\n\n if (!realtime) return\n\n const unsubscribes = [\n durably.on('run:trigger', refresh),\n durably.on('run:start', refresh),\n durably.on('run:complete', refresh),\n durably.on('run:fail', refresh),\n durably.on('run:cancel', refresh),\n durably.on('run:delete', refresh),\n durably.on('run:progress', refresh),\n durably.on('step:start', refresh),\n durably.on('step:complete', refresh),\n durably.on('step:fail', refresh),\n durably.on('step:cancel', refresh),\n ]\n\n return () => {\n for (const unsubscribe of unsubscribes) {\n unsubscribe()\n }\n }\n }, [durably, refresh, realtime])\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 nextPage,\n prevPage,\n goToPage,\n refresh,\n }\n}\n"],"mappings":"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","names":["useCallback","useEffect","useRef","useEffect","useEffect","useRef","useEffect","useCallback","useMemo","useMemo","useCallback","useEffect","useMemo","useMemo","useCallback","useEffect"]}
|
|
1
|
+
{"version":3,"sources":["../src/context.tsx","../src/hooks/use-job.ts","../src/hooks/use-auto-resume.ts","../src/hooks/use-job-subscription.ts","../src/hooks/use-run-subscription.ts","../src/shared/durably-event-subscriber.ts","../src/hooks/use-job-logs.ts","../src/hooks/use-job-run.ts","../src/hooks/use-runs.ts"],"sourcesContent":["import type { Durably } from '@coji/durably'\nimport { Suspense, createContext, use, useContext, type ReactNode } from 'react'\n\n// biome-ignore lint/suspicious/noExplicitAny: Durably context accepts any job/label configuration\ntype AnyDurably = Durably<any, any>\n\ninterface DurablyContextValue {\n durably: AnyDurably\n}\n\nconst DurablyContext = createContext<DurablyContextValue | null>(null)\n\nexport interface DurablyProviderProps {\n /**\n * Durably instance or Promise that resolves to one.\n * The instance should already be initialized via `await durably.init()`.\n *\n * When passing a Promise, wrap the provider with Suspense or use the fallback prop.\n *\n * @example\n * // With Suspense (recommended)\n * <Suspense fallback={<Loading />}>\n * <DurablyProvider durably={durablyPromise}>\n * <App />\n * </DurablyProvider>\n * </Suspense>\n *\n * @example\n * // With fallback prop\n * <DurablyProvider durably={durablyPromise} fallback={<Loading />}>\n * <App />\n * </DurablyProvider>\n */\n durably: AnyDurably | Promise<AnyDurably>\n /**\n * Fallback to show while waiting for the Durably Promise to resolve.\n * This wraps the provider content in a Suspense boundary automatically.\n */\n fallback?: ReactNode\n children: ReactNode\n}\n\n/**\n * Internal component that uses the `use()` hook to resolve the Promise\n */\nfunction DurablyProviderInner({\n durably: durablyOrPromise,\n children,\n}: Omit<DurablyProviderProps, 'fallback'>) {\n const durably =\n durablyOrPromise instanceof Promise\n ? use(durablyOrPromise)\n : durablyOrPromise\n\n return (\n <DurablyContext.Provider value={{ durably }}>\n {children}\n </DurablyContext.Provider>\n )\n}\n\nexport function DurablyProvider({\n durably,\n fallback,\n children,\n}: DurablyProviderProps) {\n const inner = (\n <DurablyProviderInner durably={durably}>{children}</DurablyProviderInner>\n )\n\n if (fallback !== undefined) {\n return <Suspense fallback={fallback}>{inner}</Suspense>\n }\n\n return inner\n}\n\nexport function useDurably(): DurablyContextValue {\n const context = useContext(DurablyContext)\n if (!context) {\n throw new Error('useDurably must be used within a DurablyProvider')\n }\n return context\n}\n","import type { JobDefinition, JobHandle } from '@coji/durably'\nimport { useCallback, useEffect, useMemo, useRef } from 'react'\nimport { useDurably } from '../context'\nimport type { LogEntry, Progress, RunStatus } from '../types'\nimport { useAutoResume } from './use-auto-resume'\nimport { useJobSubscription } from './use-job-subscription'\n\nexport interface UseJobOptions {\n /**\n * Initial Run ID to subscribe to (for reconnection scenarios)\n */\n initialRunId?: string\n /**\n * Automatically resume tracking any pending or running job on initialization.\n * If a pending or running run exists for this job, the hook will subscribe to it.\n * @default true\n */\n autoResume?: boolean\n /**\n * Automatically switch to tracking the latest running job when a new run starts.\n * When true, the hook will update to track any new run for this job as soon as it starts running.\n * When false, the hook will only track the run that was triggered or explicitly set.\n * @default true\n */\n followLatest?: boolean\n}\n\nexport interface UseJobResult<TInput, TOutput> {\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 leased (being executed by a worker)\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\nexport function useJob<\n TName extends string,\n TInput extends Record<string, unknown>,\n // biome-ignore lint/suspicious/noConfusingVoidType: TOutput can be void for jobs without return value\n TOutput extends Record<string, unknown> | void,\n>(\n jobDefinition: JobDefinition<TName, TInput, TOutput>,\n options?: UseJobOptions,\n): UseJobResult<TInput, TOutput> {\n const { durably } = useDurably()\n\n const jobHandleRef = useRef<JobHandle<TName, TInput, TOutput> | null>(null)\n\n // Register job\n useEffect(() => {\n if (!durably) return\n\n const d = durably.register({\n _job: jobDefinition,\n })\n jobHandleRef.current = d.jobs._job\n }, [durably, jobDefinition])\n\n // Use the extracted job subscription hook\n const subscription = useJobSubscription<TOutput>(\n durably,\n jobDefinition.name,\n {\n followLatest: options?.followLatest,\n },\n )\n\n // Auto-resume callbacks - stable reference\n const autoResumeCallbacks = useMemo(\n () => ({\n onRunFound: (runId: string, _status: RunStatus) => {\n subscription.setCurrentRunId(runId)\n },\n }),\n [subscription.setCurrentRunId],\n )\n\n // Use the extracted auto-resume hook\n useAutoResume(\n jobHandleRef.current,\n {\n enabled: options?.autoResume,\n initialRunId: options?.initialRunId,\n },\n autoResumeCallbacks,\n )\n\n // Handle initialRunId - set it to start tracking\n useEffect(() => {\n if (!durably || !options?.initialRunId) return\n\n subscription.setCurrentRunId(options.initialRunId)\n }, [durably, options?.initialRunId, subscription.setCurrentRunId])\n\n const trigger = useCallback(\n async (input: TInput): Promise<{ runId: string }> => {\n const jobHandle = jobHandleRef.current\n if (!jobHandle) {\n throw new Error('Job not ready')\n }\n\n // Reset state before triggering\n subscription.reset()\n\n const run = await jobHandle.trigger(input)\n subscription.setCurrentRunId(run.id)\n\n return { runId: run.id }\n },\n [subscription],\n )\n\n const triggerAndWait = useCallback(\n async (input: TInput): Promise<{ runId: string; output: TOutput }> => {\n const jobHandle = jobHandleRef.current\n if (!jobHandle || !durably) {\n throw new Error('Job not ready')\n }\n\n // Reset state before triggering\n subscription.reset()\n\n const run = await jobHandle.trigger(input)\n subscription.setCurrentRunId(run.id)\n\n // Wait for completion by polling\n return new Promise((resolve, reject) => {\n const checkCompletion = async () => {\n const updatedRun = await jobHandle.getRun(run.id)\n if (!updatedRun) {\n reject(new Error('Run not found'))\n return\n }\n\n if (updatedRun.status === 'completed') {\n resolve({ runId: run.id, output: updatedRun.output as TOutput })\n } else if (updatedRun.status === 'failed') {\n reject(new Error(updatedRun.error ?? 'Job failed'))\n } else if (updatedRun.status === 'cancelled') {\n reject(new Error('Job cancelled'))\n } else {\n // Still running, check again\n setTimeout(checkCompletion, 50)\n }\n }\n checkCompletion()\n })\n },\n [durably, subscription],\n )\n\n return {\n trigger,\n triggerAndWait,\n status: subscription.status,\n output: subscription.output,\n error: subscription.error,\n logs: subscription.logs,\n progress: subscription.progress,\n isLeased: subscription.status === 'leased',\n isPending: subscription.status === 'pending',\n isCompleted: subscription.status === 'completed',\n isFailed: subscription.status === 'failed',\n isCancelled: subscription.status === 'cancelled',\n currentRunId: subscription.currentRunId,\n reset: subscription.reset,\n }\n}\n","import type { JobHandle } from '@coji/durably'\nimport { useEffect } from 'react'\nimport type { RunStatus } from '../types'\n\nexport interface UseAutoResumeOptions {\n /**\n * Whether to automatically resume tracking pending/running runs\n * @default true\n */\n enabled?: boolean\n /**\n * Skip auto-resume if an initial run ID is provided\n */\n skipIfInitialRunId?: boolean\n /**\n * Initial run ID (if provided, auto-resume is skipped)\n */\n initialRunId?: string\n}\n\nexport interface UseAutoResumeCallbacks {\n /**\n * Called when a run is found to resume\n */\n onRunFound: (runId: string, status: RunStatus) => void\n}\n\n/**\n * Hook that automatically finds and resumes tracking of pending/running runs.\n * Extracted from useJob to separate the auto-resume concern.\n */\nexport function useAutoResume<\n TName extends string,\n TInput extends Record<string, unknown>,\n TOutput,\n>(\n jobHandle: JobHandle<TName, TInput, TOutput> | null,\n options: UseAutoResumeOptions,\n callbacks: UseAutoResumeCallbacks,\n): void {\n const enabled = options.enabled !== false\n const skipIfInitialRunId = options.skipIfInitialRunId !== false\n const initialRunId = options.initialRunId\n\n useEffect(() => {\n if (!jobHandle) return\n if (!enabled) return\n if (skipIfInitialRunId && initialRunId) return\n\n let cancelled = false\n\n const findActiveRun = async () => {\n // First check for leased runs\n const leasedRuns = await jobHandle.getRuns({ status: 'leased' })\n if (cancelled) return\n\n if (leasedRuns.length > 0) {\n const run = leasedRuns[0]\n callbacks.onRunFound(run.id, run.status as RunStatus)\n return\n }\n\n // Then check for pending runs\n const pendingRuns = await jobHandle.getRuns({ status: 'pending' })\n if (cancelled) return\n\n if (pendingRuns.length > 0) {\n const run = pendingRuns[0]\n callbacks.onRunFound(run.id, run.status as RunStatus)\n }\n }\n\n findActiveRun()\n\n return () => {\n cancelled = true\n }\n }, [jobHandle, enabled, skipIfInitialRunId, initialRunId, callbacks])\n}\n","import type { Durably } from '@coji/durably'\nimport { useCallback, useEffect, useReducer, useRef } from 'react'\nimport {\n initialSubscriptionState,\n subscriptionReducer,\n type SubscriptionAction,\n} from '../shared/subscription-reducer'\nimport type { SubscriptionState } from '../types'\n\nexport interface UseJobSubscriptionOptions {\n /**\n * Automatically switch to tracking the latest running job when a new run starts.\n * @default true\n */\n followLatest?: boolean\n /**\n * Maximum number of logs to keep (0 = unlimited)\n */\n maxLogs?: number\n}\n\nexport interface UseJobSubscriptionResult<\n TOutput = unknown,\n> extends SubscriptionState<TOutput> {\n /**\n * Current run ID being tracked\n */\n currentRunId: string | null\n /**\n * Set the current run ID to track\n */\n setCurrentRunId: (runId: string | null) => void\n /**\n * Clear all logs\n */\n clearLogs: () => void\n /**\n * Reset all state including currentRunId\n */\n reset: () => void\n}\n\n// Extended state for job subscription (includes currentRunId)\ninterface JobSubscriptionState<\n TOutput = unknown,\n> extends SubscriptionState<TOutput> {\n currentRunId: string | null\n}\n\n// Extended actions for job subscription\ntype JobSubscriptionAction<TOutput = unknown> =\n | SubscriptionAction<TOutput>\n | { type: 'set_run_id'; runId: string | null }\n | {\n type: 'switch_to_run'\n runId: string\n }\n\nfunction jobSubscriptionReducer<TOutput = unknown>(\n state: JobSubscriptionState<TOutput>,\n action: JobSubscriptionAction<TOutput>,\n): JobSubscriptionState<TOutput> {\n switch (action.type) {\n case 'set_run_id':\n return { ...state, currentRunId: action.runId }\n\n case 'switch_to_run':\n // Switch to a new run, resetting state\n return {\n ...initialSubscriptionState,\n currentRunId: action.runId,\n status: 'leased',\n } as JobSubscriptionState<TOutput>\n\n case 'reset':\n return {\n ...(initialSubscriptionState as SubscriptionState<TOutput>),\n currentRunId: null,\n }\n\n default:\n // Delegate to base subscription reducer\n return {\n ...subscriptionReducer(state, action as SubscriptionAction<TOutput>),\n currentRunId: state.currentRunId,\n }\n }\n}\n\n/**\n * Hook for subscribing to job events with followLatest support.\n * This is a specialized version of useSubscription for job-level tracking.\n */\nexport function useJobSubscription<TOutput = unknown>(\n durably: Durably | null,\n jobName: string,\n options?: UseJobSubscriptionOptions,\n): UseJobSubscriptionResult<TOutput> {\n const initialState: JobSubscriptionState<TOutput> = {\n ...(initialSubscriptionState as SubscriptionState<TOutput>),\n currentRunId: null,\n }\n\n const [state, dispatch] = useReducer(\n jobSubscriptionReducer<TOutput>,\n initialState,\n )\n\n const currentRunIdRef = useRef<string | null>(null)\n currentRunIdRef.current = state.currentRunId\n\n const followLatest = options?.followLatest !== false\n const maxLogs = options?.maxLogs ?? 0\n\n useEffect(() => {\n if (!durably) return\n\n const unsubscribes: (() => void)[] = []\n\n unsubscribes.push(\n durably.on('run:leased', (event) => {\n if (event.jobName !== jobName) return\n\n if (followLatest) {\n // Switch to tracking the new run\n dispatch({ type: 'switch_to_run', runId: event.runId })\n currentRunIdRef.current = event.runId\n } else {\n // Only update if this is our current run\n if (event.runId !== currentRunIdRef.current) return\n dispatch({ type: 'run:leased' })\n }\n }),\n )\n\n unsubscribes.push(\n durably.on('run:complete', (event) => {\n if (event.runId !== currentRunIdRef.current) return\n dispatch({ type: 'run:complete', output: event.output as TOutput })\n }),\n )\n\n unsubscribes.push(\n durably.on('run:fail', (event) => {\n if (event.runId !== currentRunIdRef.current) return\n dispatch({ type: 'run:fail', error: event.error })\n }),\n )\n\n unsubscribes.push(\n durably.on('run:cancel', (event) => {\n if (event.runId !== currentRunIdRef.current) return\n dispatch({ type: 'run:cancel' })\n }),\n )\n\n unsubscribes.push(\n durably.on('run:progress', (event) => {\n if (event.runId !== currentRunIdRef.current) return\n dispatch({ type: 'run:progress', progress: event.progress })\n }),\n )\n\n unsubscribes.push(\n durably.on('log:write', (event) => {\n if (event.runId !== currentRunIdRef.current) return\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 }),\n )\n\n return () => {\n for (const unsubscribe of unsubscribes) {\n unsubscribe()\n }\n }\n }, [durably, jobName, followLatest, maxLogs])\n\n const setCurrentRunId = useCallback((runId: string | null) => {\n dispatch({ type: 'set_run_id', runId })\n currentRunIdRef.current = runId\n }, [])\n\n const clearLogs = useCallback(() => {\n dispatch({ type: 'clear_logs' })\n }, [])\n\n const reset = useCallback(() => {\n dispatch({ type: 'reset' })\n currentRunIdRef.current = null\n }, [])\n\n return {\n ...state,\n setCurrentRunId,\n clearLogs,\n reset,\n }\n}\n","import type { Durably } from '@coji/durably'\nimport { useMemo } from 'react'\nimport { createDurablyEventSubscriber } from '../shared/durably-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 RunSubscriptionState<TOutput = unknown> = SubscriptionState<TOutput>\n\n/** @deprecated Use UseSubscriptionOptions from '../shared/use-subscription' instead */\nexport type UseRunSubscriptionOptions = UseSubscriptionOptions\n\n/** @deprecated Use UseSubscriptionResult from '../shared/use-subscription' instead */\nexport type UseRunSubscriptionResult<TOutput = unknown> =\n UseSubscriptionResult<TOutput>\n\n/**\n * Internal hook for subscribing to run events via Durably.on().\n * Shared by useJob, useJobRun, and useJobLogs.\n *\n * @deprecated Consider using useSubscription with createDurablyEventSubscriber directly.\n */\nexport function useRunSubscription<TOutput = unknown>(\n durably: Durably | null,\n runId: string | null,\n options?: UseRunSubscriptionOptions,\n): UseRunSubscriptionResult<TOutput> {\n const subscriber = useMemo(\n () => (durably ? createDurablyEventSubscriber(durably) : null),\n [durably],\n )\n\n return useSubscription<TOutput>(subscriber, runId, options)\n}\n","import type { Durably } from '@coji/durably'\nimport type { EventSubscriber, SubscriptionEvent } from './event-subscriber'\n\n/**\n * EventSubscriber implementation using Durably.on() for direct subscriptions.\n * Used in browser environments where Durably instance is available.\n */\nexport function createDurablyEventSubscriber(\n durably: Durably,\n): EventSubscriber {\n return {\n subscribe<TOutput = unknown>(\n runId: string,\n onEvent: (event: SubscriptionEvent<TOutput>) => void,\n ): () => void {\n const unsubscribes: (() => void)[] = []\n\n unsubscribes.push(\n durably.on('run:leased', (event) => {\n if (event.runId !== runId) return\n onEvent({ type: 'run:leased' })\n }),\n )\n\n unsubscribes.push(\n durably.on('run:complete', (event) => {\n if (event.runId !== runId) return\n onEvent({ type: 'run:complete', output: event.output as TOutput })\n }),\n )\n\n unsubscribes.push(\n durably.on('run:fail', (event) => {\n if (event.runId !== runId) return\n onEvent({ type: 'run:fail', error: event.error })\n }),\n )\n\n unsubscribes.push(\n durably.on('run:cancel', (event) => {\n if (event.runId !== runId) return\n onEvent({ type: 'run:cancel' })\n }),\n )\n\n unsubscribes.push(\n durably.on('run:progress', (event) => {\n if (event.runId !== runId) return\n onEvent({ type: 'run:progress', progress: event.progress })\n }),\n )\n\n unsubscribes.push(\n durably.on('log:write', (event) => {\n if (event.runId !== runId) return\n onEvent({\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 })\n }),\n )\n\n return () => {\n for (const unsubscribe of unsubscribes) {\n unsubscribe()\n }\n }\n },\n }\n}\n","import { useDurably } from '../context'\nimport type { LogEntry } from '../types'\nimport { useRunSubscription } from './use-run-subscription'\n\nexport interface UseJobLogsOptions {\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 UseJobLogsResult {\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.\n * Use this when you only need logs, not full run status.\n */\nexport function useJobLogs(options: UseJobLogsOptions): UseJobLogsResult {\n const { durably } = useDurably()\n const { runId, maxLogs } = options\n\n const subscription = useRunSubscription(durably, runId, { maxLogs })\n\n return {\n logs: subscription.logs,\n clearLogs: subscription.clearLogs,\n }\n}\n","import { useDurably } from '../context'\nimport type { LogEntry, Progress, RunStatus } from '../types'\nimport { useRunSubscription } from './use-run-subscription'\n\n// Note: Unlike UseJobRunClientOptions (client mode), this interface intentionally\n// omits onStart/onComplete/onFail callbacks. In browser mode, use durably.on()\n// directly for event callbacks.\nexport interface UseJobRunOptions {\n /**\n * The run ID to subscribe to\n */\n runId: string | null\n}\n\nexport interface UseJobRunResult<TOutput = unknown> {\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 leased (being executed by a worker)\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 by ID.\n * Use this when you have a runId and want to track its status.\n */\nexport function useJobRun<TOutput = unknown>(\n options: UseJobRunOptions,\n): UseJobRunResult<TOutput> {\n const { durably } = useDurably()\n const { runId } = options\n\n const subscription = useRunSubscription<TOutput>(durably, 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 return {\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 }\n}\n","import type { JobDefinition } from '@coji/durably'\nimport { useCallback, useEffect, useMemo, useState } from 'react'\nimport { useDurably } from '../context'\nimport { type TypedRun, isJobDefinition } from '../types'\n\n// Re-export TypedRun for convenience\nexport type { TypedRun } from '../types'\n\nexport interface UseRunsOptions {\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?: 'pending' | 'leased' | 'completed' | 'failed' | 'cancelled'\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\n * @default true\n */\n realtime?: boolean\n}\n\n// Note: Unlike UseRunsClientResult (client mode), this interface intentionally\n// omits `error` because browser mode operates on a local SQLite database\n// where network errors don't occur.\nexport interface UseRunsResult<\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: TypedRun<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 * 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 with pagination and real-time updates.\n *\n * @example With generic type parameter (dashboard with multiple job types)\n * ```tsx\n * type DashboardRun = TypedRun<ImportInput, ImportOutput> | TypedRun<SyncInput, SyncOutput>\n *\n * function Dashboard() {\n * const { runs } = useRuns<DashboardRun>({ 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 Dashboard() {\n * const { runs } = useRuns(myJob)\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 Dashboard() {\n * const { runs } = useRuns({ pageSize: 20 })\n * // runs[0].output is unknown\n * }\n * ```\n */\n// Overload 1: With generic type parameter\nexport function useRuns<\n TRun extends TypedRun<\n Record<string, unknown>,\n Record<string, unknown> | undefined\n >,\n>(\n options?: UseRunsOptions,\n): UseRunsResult<\n TRun extends TypedRun<infer I, infer _O> ? I : Record<string, unknown>,\n TRun extends TypedRun<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<UseRunsOptions, 'jobName'>,\n): UseRunsResult<TInput, TOutput>\n\n// Overload 3: Without type parameter (untyped, backward compatible)\nexport function useRuns(options?: UseRunsOptions): UseRunsResult\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 | UseRunsOptions,\n optionsArg?: Omit<UseRunsOptions, 'jobName'>,\n): UseRunsResult<TInput, TOutput> {\n const { durably } = useDurably()\n\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 UseRunsOptions | undefined)?.jobName\n\n const options = isJob\n ? optionsArg\n : (jobDefinitionOrOptions as UseRunsOptions | undefined)\n\n const pageSize = options?.pageSize ?? 10\n const realtime = options?.realtime ?? true\n const status = options?.status\n\n // Stabilize jobName reference to prevent re-fetch loops 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 // Stabilize labels reference to prevent infinite re-renders\n const labelsKey = options?.labels ? JSON.stringify(options.labels) : undefined\n const labels = useMemo(\n () =>\n labelsKey ? (JSON.parse(labelsKey) as Record<string, string>) : undefined,\n [labelsKey],\n )\n\n const [runs, setRuns] = useState<TypedRun<TInput, TOutput>[]>([])\n const [page, setPage] = useState(0)\n const [hasMore, setHasMore] = useState(false)\n const [isLoading, setIsLoading] = useState(true)\n\n const refresh = useCallback(async () => {\n if (!durably) return\n\n setIsLoading(true)\n try {\n const data = await durably.getRuns({\n jobName: stableJobName,\n status,\n labels,\n limit: pageSize + 1,\n offset: page * pageSize,\n })\n setHasMore(data.length > pageSize)\n setRuns(data.slice(0, pageSize) as TypedRun<TInput, TOutput>[])\n } finally {\n setIsLoading(false)\n }\n }, [durably, stableJobName, status, labels, pageSize, page])\n\n // Initial fetch and subscribe to events\n useEffect(() => {\n if (!durably) return\n\n refresh()\n\n if (!realtime) return\n\n const unsubscribes = [\n durably.on('run:trigger', refresh),\n durably.on('run:leased', refresh),\n durably.on('run:complete', refresh),\n durably.on('run:fail', refresh),\n durably.on('run:cancel', refresh),\n durably.on('run:delete', refresh),\n durably.on('run:progress', refresh),\n durably.on('step:start', refresh),\n durably.on('step:complete', refresh),\n durably.on('step:fail', refresh),\n durably.on('step:cancel', refresh),\n ]\n\n return () => {\n for (const unsubscribe of unsubscribes) {\n unsubscribe()\n }\n }\n }, [durably, refresh, realtime])\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 nextPage,\n prevPage,\n goToPage,\n refresh,\n }\n}\n"],"mappings":"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","names":["useCallback","useEffect","useRef","useEffect","useEffect","useRef","useEffect","useCallback","useMemo","useMemo","useCallback","useEffect","useMemo","useMemo","useCallback","useEffect"]}
|
|
@@ -8,7 +8,7 @@ type InferOutput<T> = T extends JobDefinition<string, unknown, infer TOutput> ?
|
|
|
8
8
|
output?: infer TOutput;
|
|
9
9
|
}>;
|
|
10
10
|
} ? TOutput extends Record<string, unknown> ? TOutput : Record<string, unknown> : Record<string, unknown>;
|
|
11
|
-
type RunStatus = 'pending' | '
|
|
11
|
+
type RunStatus = 'pending' | 'leased' | 'completed' | 'failed' | 'cancelled';
|
|
12
12
|
interface Progress {
|
|
13
13
|
current: number;
|
|
14
14
|
total?: number;
|
|
@@ -24,7 +24,7 @@ interface LogEntry {
|
|
|
24
24
|
timestamp: string;
|
|
25
25
|
}
|
|
26
26
|
type DurablyEvent = {
|
|
27
|
-
type: 'run:
|
|
27
|
+
type: 'run:leased';
|
|
28
28
|
runId: string;
|
|
29
29
|
jobName: string;
|
|
30
30
|
input: unknown;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@coji/durably-react",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
4
4
|
"description": "React bindings for Durably - step-oriented resumable batch execution",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -19,6 +19,16 @@
|
|
|
19
19
|
"dist",
|
|
20
20
|
"README.md"
|
|
21
21
|
],
|
|
22
|
+
"scripts": {
|
|
23
|
+
"build": "tsup",
|
|
24
|
+
"test": "pnpm test:react",
|
|
25
|
+
"test:react": "vitest run --config vitest.config.ts",
|
|
26
|
+
"typecheck": "tsc --noEmit",
|
|
27
|
+
"lint": "biome lint .",
|
|
28
|
+
"lint:fix": "biome lint --write .",
|
|
29
|
+
"format": "prettier --experimental-cli --check .",
|
|
30
|
+
"format:fix": "prettier --experimental-cli --write ."
|
|
31
|
+
},
|
|
22
32
|
"keywords": [
|
|
23
33
|
"react",
|
|
24
34
|
"hooks",
|
|
@@ -49,14 +59,15 @@
|
|
|
49
59
|
}
|
|
50
60
|
},
|
|
51
61
|
"devDependencies": {
|
|
52
|
-
"@biomejs/biome": "^2.4.
|
|
62
|
+
"@biomejs/biome": "^2.4.7",
|
|
63
|
+
"@coji/durably": "workspace:*",
|
|
53
64
|
"@testing-library/react": "^16.3.2",
|
|
54
65
|
"@types/react": "^19.2.14",
|
|
55
66
|
"@types/react-dom": "^19.2.3",
|
|
56
|
-
"@vitejs/plugin-react": "^
|
|
57
|
-
"@vitest/browser": "^4.0
|
|
58
|
-
"@vitest/browser-playwright": "4.0
|
|
59
|
-
"kysely": "^0.28.
|
|
67
|
+
"@vitejs/plugin-react": "^6.0.1",
|
|
68
|
+
"@vitest/browser": "^4.1.0",
|
|
69
|
+
"@vitest/browser-playwright": "4.1.0",
|
|
70
|
+
"kysely": "^0.28.12",
|
|
60
71
|
"playwright": "^1.58.2",
|
|
61
72
|
"prettier": "^3.8.1",
|
|
62
73
|
"prettier-plugin-organize-imports": "^4.3.0",
|
|
@@ -65,18 +76,7 @@
|
|
|
65
76
|
"sqlocal": "^0.17.0",
|
|
66
77
|
"tsup": "^8.5.1",
|
|
67
78
|
"typescript": "^5.9.3",
|
|
68
|
-
"vitest": "^4.0
|
|
69
|
-
"zod": "^4.3.6"
|
|
70
|
-
"@coji/durably": "0.12.0"
|
|
71
|
-
},
|
|
72
|
-
"scripts": {
|
|
73
|
-
"build": "tsup",
|
|
74
|
-
"test": "pnpm test:react",
|
|
75
|
-
"test:react": "vitest run --config vitest.config.ts",
|
|
76
|
-
"typecheck": "tsc --noEmit",
|
|
77
|
-
"lint": "biome lint .",
|
|
78
|
-
"lint:fix": "biome lint --write .",
|
|
79
|
-
"format": "prettier --experimental-cli --check .",
|
|
80
|
-
"format:fix": "prettier --experimental-cli --write ."
|
|
79
|
+
"vitest": "^4.1.0",
|
|
80
|
+
"zod": "^4.3.6"
|
|
81
81
|
}
|
|
82
|
-
}
|
|
82
|
+
}
|
package/LICENSE
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2025 coji
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/types.ts","../src/shared/create-log-entry.ts","../src/shared/subscription-reducer.ts","../src/shared/use-subscription.ts"],"sourcesContent":["// Shared type definitions for @coji/durably-react\n\nimport type { ClientRun, JobDefinition, Run } from '@coji/durably'\n\n// Type inference utilities for extracting Input/Output types from JobDefinition\nexport type InferInput<T> =\n T extends JobDefinition<string, infer TInput, unknown>\n ? TInput extends Record<string, unknown>\n ? TInput\n : Record<string, unknown>\n : T extends { trigger: (input: infer TInput) => unknown }\n ? TInput extends Record<string, unknown>\n ? TInput\n : Record<string, unknown>\n : Record<string, unknown>\n\nexport type InferOutput<T> =\n T extends JobDefinition<string, unknown, infer TOutput>\n ? TOutput extends Record<string, unknown>\n ? TOutput\n : Record<string, unknown>\n : T extends {\n trigger: (input: unknown) => Promise<{ output?: infer TOutput }>\n }\n ? TOutput extends Record<string, unknown>\n ? TOutput\n : Record<string, unknown>\n : Record<string, unknown>\n\nexport type RunStatus =\n | 'pending'\n | 'running'\n | 'completed'\n | 'failed'\n | 'cancelled'\n\nexport interface Progress {\n current: number\n total?: number\n message?: string\n}\n\nexport interface LogEntry {\n id: string\n runId: string\n stepName: string | null\n level: 'info' | 'warn' | 'error'\n message: string\n data: unknown\n timestamp: string\n}\n\n// Shared subscription state (used by both direct and SSE subscriptions)\nexport interface SubscriptionState<TOutput = unknown> {\n status: RunStatus | null\n output: TOutput | null\n error: string | null\n logs: LogEntry[]\n progress: Progress | null\n}\n\n// SSE event types (sent from server).\n// Note: Unlike core DurablyEvent, these omit timestamp/sequence because\n// the SSE handler in server.ts sends only the fields needed by the UI.\nexport type DurablyEvent =\n | { type: 'run:start'; runId: string; jobName: string; input: unknown }\n | {\n type: 'run:complete'\n runId: string\n jobName: string\n output: unknown\n duration: number\n }\n | { type: 'run:fail'; runId: string; jobName: string; error: string }\n | { type: 'run:cancel'; runId: string; jobName: string }\n | { type: 'run:delete'; runId: string; jobName: string }\n | { type: 'run:trigger'; runId: string; jobName: string; input: unknown }\n | {\n type: 'run:progress'\n runId: string\n jobName: string\n progress: Progress\n }\n | {\n type: 'step:start'\n runId: string\n jobName: string\n stepName: string\n stepIndex: number\n }\n | {\n type: 'step:complete'\n runId: string\n jobName: string\n stepName: string\n stepIndex: number\n output: unknown\n }\n | {\n type: 'step:cancel'\n runId: string\n jobName: string\n stepName: string\n stepIndex: number\n labels: Record<string, string>\n }\n | {\n type: 'log:write'\n runId: string\n jobName: string\n stepName: string | null\n labels: Record<string, string>\n level: 'info' | 'warn' | 'error'\n message: string\n data: unknown\n }\n\n// =============================================================================\n// Typed Run types for useRuns hooks\n// =============================================================================\n\n/**\n * A typed version of Run with generic input/output types.\n * Used by browser hooks (direct durably access).\n */\nexport type TypedRun<\n TInput extends Record<string, unknown> = Record<string, unknown>,\n TOutput extends Record<string, unknown> | undefined =\n | Record<string, unknown>\n | undefined,\n> = Omit<Run, 'input' | 'output'> & {\n input: TInput\n output: TOutput | null\n}\n\n// ClientRun is imported from '@coji/durably' and re-exported for consumers.\nexport type { ClientRun } from '@coji/durably'\n\n/**\n * A typed version of ClientRun with generic input/output types.\n * Used by client hooks (HTTP/SSE connection).\n */\nexport type TypedClientRun<\n TInput extends Record<string, unknown> = Record<string, unknown>,\n TOutput extends Record<string, unknown> | undefined =\n | Record<string, unknown>\n | undefined,\n> = Omit<ClientRun, 'input' | 'output'> & {\n input: TInput\n output: TOutput | null\n}\n\n/**\n * Type guard to check if an object is a JobDefinition.\n * Used to distinguish between JobDefinition and options objects in overloaded functions.\n */\nexport function isJobDefinition<\n TName extends string = string,\n TInput extends Record<string, unknown> = Record<string, unknown>,\n TOutput extends Record<string, unknown> | undefined = undefined,\n>(obj: unknown): obj is JobDefinition<TName, TInput, TOutput> {\n return (\n typeof obj === 'object' &&\n obj !== null &&\n 'name' in obj &&\n 'run' in obj &&\n typeof (obj as { run: unknown }).run === 'function'\n )\n}\n","import type { LogEntry } from '../types'\n\nexport interface CreateLogEntryParams {\n runId: string\n stepName: string | null\n level: 'info' | 'warn' | 'error'\n message: string\n data: unknown\n}\n\n/**\n * Creates a LogEntry with auto-generated id and timestamp.\n * Extracted to eliminate duplication between subscription hooks.\n */\nexport function createLogEntry(params: CreateLogEntryParams): LogEntry {\n return {\n id: crypto.randomUUID(),\n runId: params.runId,\n stepName: params.stepName,\n level: params.level,\n message: params.message,\n data: params.data,\n timestamp: new Date().toISOString(),\n }\n}\n\n/**\n * Appends a log entry to the array, respecting maxLogs limit.\n */\nexport function appendLog(\n logs: LogEntry[],\n newLog: LogEntry,\n maxLogs: number,\n): LogEntry[] {\n const newLogs = [...logs, newLog]\n if (maxLogs > 0 && newLogs.length > maxLogs) {\n return newLogs.slice(-maxLogs)\n }\n return newLogs\n}\n","import type { Progress, SubscriptionState } from '../types'\nimport { appendLog, createLogEntry } from './create-log-entry'\n\n// Action types for subscription state transitions\nexport type SubscriptionAction<TOutput = unknown> =\n | { type: 'run:start' }\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:start':\n return { ...state, status: 'running' }\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:start':\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":[]}
|