@coji/durably-react 0.6.0 → 0.7.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.
@@ -0,0 +1,142 @@
1
+ // src/types.ts
2
+ function isJobDefinition(obj) {
3
+ return typeof obj === "object" && obj !== null && "name" in obj && "run" in obj && typeof obj.run === "function";
4
+ }
5
+
6
+ // src/shared/create-log-entry.ts
7
+ function createLogEntry(params) {
8
+ return {
9
+ id: crypto.randomUUID(),
10
+ runId: params.runId,
11
+ stepName: params.stepName,
12
+ level: params.level,
13
+ message: params.message,
14
+ data: params.data,
15
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
16
+ };
17
+ }
18
+ function appendLog(logs, newLog, maxLogs) {
19
+ const newLogs = [...logs, newLog];
20
+ if (maxLogs > 0 && newLogs.length > maxLogs) {
21
+ return newLogs.slice(-maxLogs);
22
+ }
23
+ return newLogs;
24
+ }
25
+
26
+ // src/shared/subscription-reducer.ts
27
+ var initialSubscriptionState = {
28
+ status: null,
29
+ output: null,
30
+ error: null,
31
+ logs: [],
32
+ progress: null
33
+ };
34
+ function subscriptionReducer(state, action) {
35
+ switch (action.type) {
36
+ case "run:start":
37
+ return { ...state, status: "running" };
38
+ case "run:complete":
39
+ return { ...state, status: "completed", output: action.output };
40
+ case "run:fail":
41
+ return { ...state, status: "failed", error: action.error };
42
+ case "run:cancel":
43
+ return { ...state, status: "cancelled" };
44
+ case "run:retry":
45
+ return { ...state, status: "pending", error: null };
46
+ case "run:progress":
47
+ return { ...state, progress: action.progress };
48
+ case "log:write": {
49
+ const newLog = createLogEntry({
50
+ runId: action.runId,
51
+ stepName: action.stepName,
52
+ level: action.level,
53
+ message: action.message,
54
+ data: action.data
55
+ });
56
+ return { ...state, logs: appendLog(state.logs, newLog, action.maxLogs) };
57
+ }
58
+ case "reset":
59
+ return initialSubscriptionState;
60
+ case "clear_logs":
61
+ return { ...state, logs: [] };
62
+ case "connection_error":
63
+ return { ...state, error: action.error };
64
+ default:
65
+ return state;
66
+ }
67
+ }
68
+
69
+ // src/shared/use-subscription.ts
70
+ import { useCallback, useEffect, useReducer, useRef } from "react";
71
+ function useSubscription(subscriber, runId, options) {
72
+ const [state, dispatch] = useReducer(
73
+ subscriptionReducer,
74
+ initialSubscriptionState
75
+ );
76
+ const runIdRef = useRef(runId);
77
+ const prevRunIdRef = useRef(null);
78
+ const maxLogs = options?.maxLogs ?? 0;
79
+ if (prevRunIdRef.current !== runId) {
80
+ prevRunIdRef.current = runId;
81
+ if (runIdRef.current !== runId) {
82
+ dispatch({ type: "reset" });
83
+ }
84
+ }
85
+ runIdRef.current = runId;
86
+ useEffect(() => {
87
+ if (!subscriber || !runId) return;
88
+ const unsubscribe = subscriber.subscribe(runId, (event) => {
89
+ if (runIdRef.current !== runId) return;
90
+ switch (event.type) {
91
+ case "run:start":
92
+ case "run:cancel":
93
+ case "run:retry":
94
+ dispatch({ type: event.type });
95
+ break;
96
+ case "run:complete":
97
+ dispatch({ type: "run:complete", output: event.output });
98
+ break;
99
+ case "run:fail":
100
+ dispatch({ type: "run:fail", error: event.error });
101
+ break;
102
+ case "run:progress":
103
+ dispatch({ type: "run:progress", progress: event.progress });
104
+ break;
105
+ case "log:write":
106
+ dispatch({
107
+ type: "log:write",
108
+ runId: event.runId,
109
+ stepName: event.stepName,
110
+ level: event.level,
111
+ message: event.message,
112
+ data: event.data,
113
+ maxLogs
114
+ });
115
+ break;
116
+ case "connection_error":
117
+ dispatch({ type: "connection_error", error: event.error });
118
+ break;
119
+ }
120
+ });
121
+ return unsubscribe;
122
+ }, [subscriber, runId, maxLogs]);
123
+ const clearLogs = useCallback(() => {
124
+ dispatch({ type: "clear_logs" });
125
+ }, []);
126
+ const reset = useCallback(() => {
127
+ dispatch({ type: "reset" });
128
+ }, []);
129
+ return {
130
+ ...state,
131
+ clearLogs,
132
+ reset
133
+ };
134
+ }
135
+
136
+ export {
137
+ initialSubscriptionState,
138
+ subscriptionReducer,
139
+ useSubscription,
140
+ isJobDefinition
141
+ };
142
+ //# sourceMappingURL=chunk-42AVE35N.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 { 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)\nexport type DurablyEvent =\n | { type: 'run:start'; runId: string; jobName: string; payload: 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:retry'; runId: string; jobName: string }\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: 'log:write'\n runId: string\n jobName: 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 payload/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, 'payload' | 'output'> & {\n payload: TInput\n output: TOutput | null\n}\n\n/**\n * Run type for client mode (matches server response).\n * Used by client hooks (HTTP/SSE connection).\n */\nexport interface ClientRun {\n id: string\n jobName: string\n status: RunStatus\n input: unknown\n output: unknown | null\n error: string | null\n currentStepIndex: number\n stepCount: number\n progress: Progress | null\n createdAt: string\n startedAt: string | null\n completedAt: string | null\n}\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:retry' }\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:retry':\n return { ...state, status: 'pending', error: null }\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 case 'run:retry':\n dispatch({ type: event.type })\n break\n case 'run:complete':\n dispatch({ type: 'run:complete', output: event.output })\n break\n case 'run:fail':\n dispatch({ type: 'run:fail', error: event.error })\n break\n case 'run:progress':\n dispatch({ type: 'run:progress', progress: event.progress })\n break\n case 'log:write':\n dispatch({\n type: 'log:write',\n runId: event.runId,\n stepName: event.stepName,\n level: event.level,\n message: event.message,\n data: event.data,\n maxLogs,\n })\n break\n case 'connection_error':\n dispatch({ type: 'connection_error', error: event.error })\n break\n }\n })\n\n return unsubscribe\n }, [subscriber, runId, maxLogs])\n\n const clearLogs = useCallback(() => {\n dispatch({ type: 'clear_logs' })\n }, [])\n\n const reset = useCallback(() => {\n dispatch({ type: 'reset' })\n }, [])\n\n return {\n ...state,\n clearLogs,\n reset,\n }\n}\n"],"mappings":"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","names":[]}
package/dist/client.d.ts CHANGED
@@ -1,5 +1,6 @@
1
+ import { R as RunStatus, L as LogEntry, P as Progress, I as InferInput, a as InferOutput, b as TypedClientRun } from './types-DY4Y6ggJ.js';
2
+ export { C as ClientRun } from './types-DY4Y6ggJ.js';
1
3
  import { JobDefinition } from '@coji/durably';
2
- import { R as RunStatus, L as LogEntry, P as Progress } from './types-BDUvsa8u.js';
3
4
 
4
5
  interface UseJobClientOptions {
5
6
  /**
@@ -190,20 +191,6 @@ interface UseJobRunClientResult<TOutput = unknown> {
190
191
  */
191
192
  declare function useJobRun<TOutput = unknown>(options: UseJobRunClientOptions): UseJobRunClientResult<TOutput>;
192
193
 
193
- /**
194
- * Extract input type from a JobDefinition or JobHandle
195
- */
196
- type InferInput$1<T> = T extends JobDefinition<string, infer TInput, unknown> ? TInput extends Record<string, unknown> ? TInput : Record<string, unknown> : T extends {
197
- trigger: (input: infer TInput) => unknown;
198
- } ? TInput extends Record<string, unknown> ? TInput : Record<string, unknown> : Record<string, unknown>;
199
- /**
200
- * Extract output type from a JobDefinition or JobHandle
201
- */
202
- type InferOutput$1<T> = T extends JobDefinition<string, unknown, infer TOutput> ? TOutput extends Record<string, unknown> ? TOutput : Record<string, unknown> : T extends {
203
- trigger: (input: unknown) => Promise<{
204
- output?: infer TOutput;
205
- }>;
206
- } ? TOutput extends Record<string, unknown> ? TOutput : Record<string, unknown> : Record<string, unknown>;
207
194
  /**
208
195
  * Type-safe hooks for a specific job
209
196
  */
@@ -236,7 +223,7 @@ interface CreateDurablyClientOptions {
236
223
  * A type-safe client with hooks for each registered job
237
224
  */
238
225
  type DurablyClient<TJobs extends Record<string, unknown>> = {
239
- [K in keyof TJobs]: JobClient<InferInput$1<TJobs[K]>, InferOutput$1<TJobs[K]>>;
226
+ [K in keyof TJobs]: JobClient<InferInput<TJobs[K]>, InferOutput<TJobs[K]>>;
240
227
  };
241
228
  /**
242
229
  * Create a type-safe Durably client with hooks for all registered jobs.
@@ -273,14 +260,6 @@ type DurablyClient<TJobs extends Record<string, unknown>> = {
273
260
  */
274
261
  declare function createDurablyClient<TJobs extends Record<string, unknown>>(options: CreateDurablyClientOptions): DurablyClient<TJobs>;
275
262
 
276
- /**
277
- * Extract input type from a JobDefinition
278
- */
279
- type InferInput<T> = T extends JobDefinition<string, infer TInput, unknown> ? TInput extends Record<string, unknown> ? TInput : Record<string, unknown> : Record<string, unknown>;
280
- /**
281
- * Extract output type from a JobDefinition
282
- */
283
- type InferOutput<T> = T extends JobDefinition<string, unknown, infer TOutput> ? TOutput extends Record<string, unknown> ? TOutput : Record<string, unknown> : Record<string, unknown>;
284
263
  /**
285
264
  * Options for createJobHooks
286
265
  */
@@ -341,23 +320,6 @@ interface JobHooks<TInput, TOutput> {
341
320
  */
342
321
  declare function createJobHooks<TJob extends JobDefinition<string, any, any>>(options: CreateJobHooksOptions): JobHooks<InferInput<TJob>, InferOutput<TJob>>;
343
322
 
344
- /**
345
- * Run type for client mode (matches server response)
346
- */
347
- interface ClientRun {
348
- id: string;
349
- jobName: string;
350
- status: RunStatus;
351
- input: unknown;
352
- output: unknown | null;
353
- error: string | null;
354
- currentStepIndex: number;
355
- stepCount: number;
356
- progress: Progress | null;
357
- createdAt: string;
358
- startedAt: string | null;
359
- completedAt: string | null;
360
- }
361
323
  interface UseRunsClientOptions {
362
324
  /**
363
325
  * API endpoint URL (e.g., '/api/durably')
@@ -377,11 +339,11 @@ interface UseRunsClientOptions {
377
339
  */
378
340
  pageSize?: number;
379
341
  }
380
- interface UseRunsClientResult {
342
+ interface UseRunsClientResult<TInput extends Record<string, unknown> = Record<string, unknown>, TOutput extends Record<string, unknown> | undefined = Record<string, unknown> | undefined> {
381
343
  /**
382
344
  * List of runs for the current page
383
345
  */
384
- runs: ClientRun[];
346
+ runs: TypedClientRun<TInput, TOutput>[];
385
347
  /**
386
348
  * Current page (0-indexed)
387
349
  */
@@ -420,28 +382,37 @@ interface UseRunsClientResult {
420
382
  * First page (page 0) automatically subscribes to SSE for real-time updates.
421
383
  * Other pages are static and require manual refresh.
422
384
  *
423
- * @example
385
+ * @example With generic type parameter (dashboard with multiple job types)
386
+ * ```tsx
387
+ * type DashboardRun = TypedClientRun<ImportInput, ImportOutput> | TypedClientRun<SyncInput, SyncOutput>
388
+ *
389
+ * function Dashboard() {
390
+ * const { runs } = useRuns<DashboardRun>({ api: '/api/durably', pageSize: 10 })
391
+ * // runs are typed as DashboardRun[]
392
+ * }
393
+ * ```
394
+ *
395
+ * @example With JobDefinition (single job, auto-filters by jobName)
424
396
  * ```tsx
397
+ * const myJob = defineJob({ name: 'my-job', ... })
398
+ *
425
399
  * function RunHistory() {
426
- * const { runs, page, hasMore, nextPage, prevPage, refresh } = useRuns({
427
- * api: '/api/durably',
428
- * jobName: 'import-csv',
429
- * pageSize: 10,
430
- * })
400
+ * const { runs } = useRuns(myJob, { api: '/api/durably' })
401
+ * // runs[0].output is typed!
402
+ * return <div>{runs[0]?.output?.someField}</div>
403
+ * }
404
+ * ```
431
405
  *
432
- * return (
433
- * <div>
434
- * {runs.map(run => (
435
- * <div key={run.id}>{run.jobName}: {run.status}</div>
436
- * ))}
437
- * <button onClick={prevPage} disabled={page === 0}>Prev</button>
438
- * <button onClick={nextPage} disabled={!hasMore}>Next</button>
439
- * <button onClick={refresh}>Refresh</button>
440
- * </div>
441
- * )
406
+ * @example With options only (untyped)
407
+ * ```tsx
408
+ * function RunHistory() {
409
+ * const { runs } = useRuns({ api: '/api/durably', pageSize: 10 })
410
+ * // runs[0].output is unknown
442
411
  * }
443
412
  * ```
444
413
  */
414
+ declare function useRuns<TRun extends TypedClientRun<Record<string, unknown>, Record<string, unknown> | undefined>>(options: UseRunsClientOptions): UseRunsClientResult<TRun extends TypedClientRun<infer I, infer _O> ? I : Record<string, unknown>, TRun extends TypedClientRun<infer _I, infer O> ? O : Record<string, unknown>>;
415
+ declare function useRuns<TName extends string, TInput extends Record<string, unknown>, TOutput extends Record<string, unknown> | undefined>(jobDefinition: JobDefinition<TName, TInput, TOutput>, options: Omit<UseRunsClientOptions, 'jobName'>): UseRunsClientResult<TInput, TOutput>;
445
416
  declare function useRuns(options: UseRunsClientOptions): UseRunsClientResult;
446
417
 
447
418
  /**
@@ -539,4 +510,4 @@ interface UseRunActionsClientResult {
539
510
  */
540
511
  declare function useRunActions(options: UseRunActionsClientOptions): UseRunActionsClientResult;
541
512
 
542
- export { type ClientRun, type CreateDurablyClientOptions, type CreateJobHooksOptions, type DurablyClient, type JobClient, type JobHooks, LogEntry, Progress, type RunRecord, RunStatus, type StepRecord, type UseJobClientOptions, type UseJobClientResult, type UseJobLogsClientOptions, type UseJobLogsClientResult, type UseJobRunClientOptions, type UseJobRunClientResult, type UseRunActionsClientOptions, type UseRunActionsClientResult, type UseRunsClientOptions, type UseRunsClientResult, createDurablyClient, createJobHooks, useJob, useJobLogs, useJobRun, useRunActions, useRuns };
513
+ export { type CreateDurablyClientOptions, type CreateJobHooksOptions, type DurablyClient, type JobClient, type JobHooks, LogEntry, Progress, type RunRecord, RunStatus, type StepRecord, TypedClientRun, type UseJobClientOptions, type UseJobClientResult, type UseJobLogsClientOptions, type UseJobLogsClientResult, type UseJobRunClientOptions, type UseJobRunClientResult, type UseRunActionsClientOptions, type UseRunActionsClientResult, type UseRunsClientOptions, type UseRunsClientResult, createDurablyClient, createJobHooks, useJob, useJobLogs, useJobRun, useRunActions, useRuns };