@coji/durably-react 0.6.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/LICENSE ADDED
@@ -0,0 +1,21 @@
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.
package/README.md ADDED
@@ -0,0 +1,102 @@
1
+ # @coji/durably-react
2
+
3
+ React bindings for [Durably](https://github.com/coji/durably) - step-oriented resumable batch execution.
4
+
5
+ **[Documentation](https://coji.github.io/durably/)** | **[GitHub](https://github.com/coji/durably)**
6
+
7
+ > **Note:** This package is ESM-only. CommonJS is not supported.
8
+
9
+ ## Installation
10
+
11
+ ```bash
12
+ # Browser mode (with SQLocal)
13
+ npm install @coji/durably-react @coji/durably kysely zod sqlocal
14
+
15
+ # Server-connected mode (client only)
16
+ npm install @coji/durably-react
17
+ ```
18
+
19
+ ## Quick Start
20
+
21
+ ```tsx
22
+ import { Suspense } from 'react'
23
+ import { createDurably, defineJob } from '@coji/durably'
24
+ import { DurablyProvider, useJob } from '@coji/durably-react'
25
+ import { SQLocalKysely } from 'sqlocal/kysely'
26
+ import { z } from 'zod'
27
+
28
+ const myJob = defineJob({
29
+ name: 'my-job',
30
+ input: z.object({ id: z.string() }),
31
+ run: async (step, payload) => {
32
+ await step.run('step-1', async () => {
33
+ /* ... */
34
+ })
35
+ },
36
+ })
37
+
38
+ // Initialize Durably
39
+ async function initDurably() {
40
+ const sqlocal = new SQLocalKysely('app.sqlite3')
41
+ const durably = createDurably({ dialect: sqlocal.dialect }).register({
42
+ myJob,
43
+ })
44
+ await durably.init() // migrate + start
45
+ return durably
46
+ }
47
+ const durablyPromise = initDurably()
48
+
49
+ function App() {
50
+ return (
51
+ <Suspense fallback={<div>Loading...</div>}>
52
+ <DurablyProvider durably={durablyPromise}>
53
+ <MyComponent />
54
+ </DurablyProvider>
55
+ </Suspense>
56
+ )
57
+ }
58
+
59
+ function MyComponent() {
60
+ const { trigger, isRunning, isCompleted } = useJob(myJob)
61
+ return (
62
+ <button onClick={() => trigger({ id: '123' })} disabled={isRunning}>
63
+ Run
64
+ </button>
65
+ )
66
+ }
67
+ ```
68
+
69
+ ## Server-Connected Mode
70
+
71
+ For full-stack apps, use hooks from `@coji/durably-react/client`:
72
+
73
+ ```tsx
74
+ import { useJob } from '@coji/durably-react/client'
75
+
76
+ function MyComponent() {
77
+ const { trigger, status, output, isRunning } = useJob<
78
+ { id: string },
79
+ { result: number }
80
+ >({
81
+ api: '/api/durably',
82
+ jobName: 'my-job',
83
+ })
84
+
85
+ return (
86
+ <button onClick={() => trigger({ id: '123' })} disabled={isRunning}>
87
+ Run
88
+ </button>
89
+ )
90
+ }
91
+ ```
92
+
93
+ ## Documentation
94
+
95
+ For full documentation, visit [coji.github.io/durably](https://coji.github.io/durably/).
96
+
97
+ - [React Guide](https://coji.github.io/durably/guide/react) - Browser mode with hooks
98
+ - [Full-Stack Guide](https://coji.github.io/durably/guide/full-stack) - Server-connected mode
99
+
100
+ ## License
101
+
102
+ MIT
@@ -0,0 +1,542 @@
1
+ import { JobDefinition } from '@coji/durably';
2
+ import { R as RunStatus, L as LogEntry, P as Progress } from './types-BDUvsa8u.js';
3
+
4
+ interface UseJobClientOptions {
5
+ /**
6
+ * API endpoint URL (e.g., '/api/durably')
7
+ */
8
+ api: string;
9
+ /**
10
+ * Job name to trigger
11
+ */
12
+ jobName: string;
13
+ /**
14
+ * Initial Run ID to subscribe to (for reconnection scenarios)
15
+ * When provided, the hook will immediately start subscribing to this run
16
+ */
17
+ initialRunId?: string;
18
+ }
19
+ interface UseJobClientResult<TInput, TOutput> {
20
+ /**
21
+ * Whether the hook is ready (always true for client mode)
22
+ */
23
+ /**
24
+ * Trigger the job with the given input
25
+ */
26
+ trigger: (input: TInput) => Promise<{
27
+ runId: string;
28
+ }>;
29
+ /**
30
+ * Trigger and wait for completion
31
+ */
32
+ triggerAndWait: (input: TInput) => Promise<{
33
+ runId: string;
34
+ output: TOutput;
35
+ }>;
36
+ /**
37
+ * Current run status
38
+ */
39
+ status: RunStatus | null;
40
+ /**
41
+ * Output from completed run
42
+ */
43
+ output: TOutput | null;
44
+ /**
45
+ * Error message from failed run
46
+ */
47
+ error: string | null;
48
+ /**
49
+ * Logs collected during execution
50
+ */
51
+ logs: LogEntry[];
52
+ /**
53
+ * Current progress
54
+ */
55
+ progress: Progress | null;
56
+ /**
57
+ * Whether a run is currently running
58
+ */
59
+ isRunning: boolean;
60
+ /**
61
+ * Whether a run is pending
62
+ */
63
+ isPending: boolean;
64
+ /**
65
+ * Whether the run completed successfully
66
+ */
67
+ isCompleted: boolean;
68
+ /**
69
+ * Whether the run failed
70
+ */
71
+ isFailed: boolean;
72
+ /**
73
+ * Current run ID
74
+ */
75
+ currentRunId: string | null;
76
+ /**
77
+ * Reset all state
78
+ */
79
+ reset: () => void;
80
+ }
81
+ /**
82
+ * Hook for triggering and subscribing to jobs via server API.
83
+ * Uses fetch for triggering and EventSource for SSE subscription.
84
+ */
85
+ declare function useJob<TInput extends Record<string, unknown> = Record<string, unknown>, TOutput extends Record<string, unknown> = Record<string, unknown>>(options: UseJobClientOptions): UseJobClientResult<TInput, TOutput>;
86
+
87
+ interface UseJobLogsClientOptions {
88
+ /**
89
+ * API endpoint URL (e.g., '/api/durably')
90
+ */
91
+ api: string;
92
+ /**
93
+ * The run ID to subscribe to logs for
94
+ */
95
+ runId: string | null;
96
+ /**
97
+ * Maximum number of logs to keep (default: unlimited)
98
+ */
99
+ maxLogs?: number;
100
+ }
101
+ interface UseJobLogsClientResult {
102
+ /**
103
+ * Whether the hook is ready (always true for client mode)
104
+ */
105
+ /**
106
+ * Logs collected during execution
107
+ */
108
+ logs: LogEntry[];
109
+ /**
110
+ * Clear all logs
111
+ */
112
+ clearLogs: () => void;
113
+ }
114
+ /**
115
+ * Hook for subscribing to logs from a run via server API.
116
+ * Uses EventSource for SSE subscription.
117
+ */
118
+ declare function useJobLogs(options: UseJobLogsClientOptions): UseJobLogsClientResult;
119
+
120
+ interface UseJobRunClientOptions {
121
+ /**
122
+ * API endpoint URL (e.g., '/api/durably')
123
+ */
124
+ api: string;
125
+ /**
126
+ * The run ID to subscribe to
127
+ */
128
+ runId: string | null;
129
+ /**
130
+ * Callback when run starts (transitions to pending/running)
131
+ */
132
+ onStart?: () => void;
133
+ /**
134
+ * Callback when run completes successfully
135
+ */
136
+ onComplete?: () => void;
137
+ /**
138
+ * Callback when run fails
139
+ */
140
+ onFail?: () => void;
141
+ }
142
+ interface UseJobRunClientResult<TOutput = unknown> {
143
+ /**
144
+ * Whether the hook is ready (always true for client mode)
145
+ */
146
+ /**
147
+ * Current run status
148
+ */
149
+ status: RunStatus | null;
150
+ /**
151
+ * Output from completed run
152
+ */
153
+ output: TOutput | null;
154
+ /**
155
+ * Error message from failed run
156
+ */
157
+ error: string | null;
158
+ /**
159
+ * Logs collected during execution
160
+ */
161
+ logs: LogEntry[];
162
+ /**
163
+ * Current progress
164
+ */
165
+ progress: Progress | null;
166
+ /**
167
+ * Whether a run is currently running
168
+ */
169
+ isRunning: boolean;
170
+ /**
171
+ * Whether a run is pending
172
+ */
173
+ isPending: boolean;
174
+ /**
175
+ * Whether the run completed successfully
176
+ */
177
+ isCompleted: boolean;
178
+ /**
179
+ * Whether the run failed
180
+ */
181
+ isFailed: boolean;
182
+ /**
183
+ * Whether the run was cancelled
184
+ */
185
+ isCancelled: boolean;
186
+ }
187
+ /**
188
+ * Hook for subscribing to an existing run via server API.
189
+ * Uses EventSource for SSE subscription.
190
+ */
191
+ declare function useJobRun<TOutput = unknown>(options: UseJobRunClientOptions): UseJobRunClientResult<TOutput>;
192
+
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
+ /**
208
+ * Type-safe hooks for a specific job
209
+ */
210
+ interface JobClient<TInput, TOutput> {
211
+ /**
212
+ * Hook for triggering and monitoring the job
213
+ */
214
+ useJob: () => UseJobClientResult<TInput, TOutput>;
215
+ /**
216
+ * Hook for subscribing to an existing run by ID
217
+ */
218
+ useRun: (runId: string | null) => UseJobRunClientResult<TOutput>;
219
+ /**
220
+ * Hook for subscribing to logs from a run
221
+ */
222
+ useLogs: (runId: string | null, options?: {
223
+ maxLogs?: number;
224
+ }) => UseJobLogsClientResult;
225
+ }
226
+ /**
227
+ * Options for createDurablyClient
228
+ */
229
+ interface CreateDurablyClientOptions {
230
+ /**
231
+ * API endpoint URL (e.g., '/api/durably')
232
+ */
233
+ api: string;
234
+ }
235
+ /**
236
+ * A type-safe client with hooks for each registered job
237
+ */
238
+ type DurablyClient<TJobs extends Record<string, unknown>> = {
239
+ [K in keyof TJobs]: JobClient<InferInput$1<TJobs[K]>, InferOutput$1<TJobs[K]>>;
240
+ };
241
+ /**
242
+ * Create a type-safe Durably client with hooks for all registered jobs.
243
+ *
244
+ * @example
245
+ * ```tsx
246
+ * // Server: register jobs
247
+ * // app/lib/durably.server.ts
248
+ * export const jobs = durably.register({
249
+ * importCsv: importCsvJob,
250
+ * syncUsers: syncUsersJob,
251
+ * })
252
+ *
253
+ * // Client: create typed client
254
+ * // app/lib/durably.client.ts
255
+ * import type { jobs } from '~/lib/durably.server'
256
+ * import { createDurablyClient } from '@coji/durably-react/client'
257
+ *
258
+ * export const durably = createDurablyClient<typeof jobs>({
259
+ * api: '/api/durably',
260
+ * })
261
+ *
262
+ * // In your component - fully type-safe with autocomplete
263
+ * function CsvImporter() {
264
+ * const { trigger, output, isRunning } = durably.importCsv.useJob()
265
+ *
266
+ * return (
267
+ * <button onClick={() => trigger({ rows: [...] })}>
268
+ * Import
269
+ * </button>
270
+ * )
271
+ * }
272
+ * ```
273
+ */
274
+ declare function createDurablyClient<TJobs extends Record<string, unknown>>(options: CreateDurablyClientOptions): DurablyClient<TJobs>;
275
+
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
+ /**
285
+ * Options for createJobHooks
286
+ */
287
+ interface CreateJobHooksOptions {
288
+ /**
289
+ * API endpoint URL (e.g., '/api/durably')
290
+ */
291
+ api: string;
292
+ /**
293
+ * Job name (must match the server-side job name)
294
+ */
295
+ jobName: string;
296
+ }
297
+ /**
298
+ * Type-safe hooks for a specific job
299
+ */
300
+ interface JobHooks<TInput, TOutput> {
301
+ /**
302
+ * Hook for triggering and monitoring the job
303
+ */
304
+ useJob: () => UseJobClientResult<TInput, TOutput>;
305
+ /**
306
+ * Hook for subscribing to an existing run by ID
307
+ */
308
+ useRun: (runId: string | null) => UseJobRunClientResult<TOutput>;
309
+ /**
310
+ * Hook for subscribing to logs from a run
311
+ */
312
+ useLogs: (runId: string | null, options?: {
313
+ maxLogs?: number;
314
+ }) => UseJobLogsClientResult;
315
+ }
316
+ /**
317
+ * Create type-safe hooks for a specific job.
318
+ *
319
+ * @example
320
+ * ```tsx
321
+ * // Import job type from server (type-only import is safe)
322
+ * import type { importCsvJob } from '~/lib/durably.server'
323
+ * import { createJobHooks } from '@coji/durably-react/client'
324
+ *
325
+ * const importCsv = createJobHooks<typeof importCsvJob>({
326
+ * api: '/api/durably',
327
+ * jobName: 'import-csv',
328
+ * })
329
+ *
330
+ * // In your component - fully type-safe
331
+ * function CsvImporter() {
332
+ * const { trigger, output, progress, isRunning } = importCsv.useJob()
333
+ *
334
+ * return (
335
+ * <button onClick={() => trigger({ rows: [...] })}>
336
+ * Import
337
+ * </button>
338
+ * )
339
+ * }
340
+ * ```
341
+ */
342
+ declare function createJobHooks<TJob extends JobDefinition<string, any, any>>(options: CreateJobHooksOptions): JobHooks<InferInput<TJob>, InferOutput<TJob>>;
343
+
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
+ interface UseRunsClientOptions {
362
+ /**
363
+ * API endpoint URL (e.g., '/api/durably')
364
+ */
365
+ api: string;
366
+ /**
367
+ * Filter by job name
368
+ */
369
+ jobName?: string;
370
+ /**
371
+ * Filter by status
372
+ */
373
+ status?: RunStatus;
374
+ /**
375
+ * Number of runs per page
376
+ * @default 10
377
+ */
378
+ pageSize?: number;
379
+ }
380
+ interface UseRunsClientResult {
381
+ /**
382
+ * List of runs for the current page
383
+ */
384
+ runs: ClientRun[];
385
+ /**
386
+ * Current page (0-indexed)
387
+ */
388
+ page: number;
389
+ /**
390
+ * Whether there are more pages
391
+ */
392
+ hasMore: boolean;
393
+ /**
394
+ * Whether data is being loaded
395
+ */
396
+ isLoading: boolean;
397
+ /**
398
+ * Error message if fetch failed
399
+ */
400
+ error: string | null;
401
+ /**
402
+ * Go to the next page
403
+ */
404
+ nextPage: () => void;
405
+ /**
406
+ * Go to the previous page
407
+ */
408
+ prevPage: () => void;
409
+ /**
410
+ * Go to a specific page
411
+ */
412
+ goToPage: (page: number) => void;
413
+ /**
414
+ * Refresh the current page
415
+ */
416
+ refresh: () => Promise<void>;
417
+ }
418
+ /**
419
+ * Hook for listing runs via server API with pagination.
420
+ * First page (page 0) automatically subscribes to SSE for real-time updates.
421
+ * Other pages are static and require manual refresh.
422
+ *
423
+ * @example
424
+ * ```tsx
425
+ * function RunHistory() {
426
+ * const { runs, page, hasMore, nextPage, prevPage, refresh } = useRuns({
427
+ * api: '/api/durably',
428
+ * jobName: 'import-csv',
429
+ * pageSize: 10,
430
+ * })
431
+ *
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
+ * )
442
+ * }
443
+ * ```
444
+ */
445
+ declare function useRuns(options: UseRunsClientOptions): UseRunsClientResult;
446
+
447
+ /**
448
+ * Run record returned from the server API
449
+ */
450
+ interface RunRecord {
451
+ id: string;
452
+ jobName: string;
453
+ status: 'pending' | 'running' | 'completed' | 'failed' | 'cancelled';
454
+ payload: unknown;
455
+ output: unknown | null;
456
+ error: string | null;
457
+ progress: {
458
+ current: number;
459
+ total?: number;
460
+ message?: string;
461
+ } | null;
462
+ currentStepIndex: number;
463
+ stepCount: number;
464
+ createdAt: string;
465
+ startedAt: string | null;
466
+ completedAt: string | null;
467
+ }
468
+ /**
469
+ * Step record returned from the server API
470
+ */
471
+ interface StepRecord {
472
+ name: string;
473
+ status: 'completed' | 'failed';
474
+ output: unknown;
475
+ }
476
+ interface UseRunActionsClientOptions {
477
+ /**
478
+ * API endpoint URL (e.g., '/api/durably')
479
+ */
480
+ api: string;
481
+ }
482
+ interface UseRunActionsClientResult {
483
+ /**
484
+ * Retry a failed or cancelled run
485
+ */
486
+ retry: (runId: string) => Promise<void>;
487
+ /**
488
+ * Cancel a pending or running run
489
+ */
490
+ cancel: (runId: string) => Promise<void>;
491
+ /**
492
+ * Delete a run (only completed, failed, or cancelled runs)
493
+ */
494
+ deleteRun: (runId: string) => Promise<void>;
495
+ /**
496
+ * Get a single run by ID
497
+ */
498
+ getRun: (runId: string) => Promise<RunRecord | null>;
499
+ /**
500
+ * Get steps for a run
501
+ */
502
+ getSteps: (runId: string) => Promise<StepRecord[]>;
503
+ /**
504
+ * Whether an action is in progress
505
+ */
506
+ isLoading: boolean;
507
+ /**
508
+ * Error message from last action
509
+ */
510
+ error: string | null;
511
+ }
512
+ /**
513
+ * Hook for run actions (retry, cancel) via server API.
514
+ *
515
+ * @example
516
+ * ```tsx
517
+ * function RunActions({ runId, status }: { runId: string; status: string }) {
518
+ * const { retry, cancel, isLoading, error } = useRunActions({
519
+ * api: '/api/durably',
520
+ * })
521
+ *
522
+ * return (
523
+ * <div>
524
+ * {status === 'failed' && (
525
+ * <button onClick={() => retry(runId)} disabled={isLoading}>
526
+ * Retry
527
+ * </button>
528
+ * )}
529
+ * {(status === 'pending' || status === 'running') && (
530
+ * <button onClick={() => cancel(runId)} disabled={isLoading}>
531
+ * Cancel
532
+ * </button>
533
+ * )}
534
+ * {error && <span className="error">{error}</span>}
535
+ * </div>
536
+ * )
537
+ * }
538
+ * ```
539
+ */
540
+ declare function useRunActions(options: UseRunActionsClientOptions): UseRunActionsClientResult;
541
+
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 };