@computekit/react 0.1.2 → 0.2.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/src/index.tsx CHANGED
@@ -20,8 +20,168 @@ import {
20
20
  type ComputeOptions,
21
21
  type ComputeProgress,
22
22
  type PoolStats,
23
+ type ComputeFunctionRegistry,
24
+ type RegisteredFunctionName,
25
+ type FunctionInput,
26
+ type FunctionOutput,
27
+ type ComputeFn,
23
28
  } from '@computekit/core';
24
29
 
30
+ // ============================================================================
31
+ // Pipeline Types (defined here for React, also exported from @computekit/core)
32
+ // ============================================================================
33
+
34
+ /** Status of a pipeline stage */
35
+ export type StageStatus = 'pending' | 'running' | 'completed' | 'failed' | 'skipped';
36
+
37
+ /** Detailed information about a single pipeline stage */
38
+ export interface StageInfo<TInput = unknown, TOutput = unknown> {
39
+ /** Unique identifier for the stage */
40
+ id: string;
41
+ /** Display name for the stage */
42
+ name: string;
43
+ /** Name of the registered compute function to execute */
44
+ functionName: string;
45
+ /** Current status of this stage */
46
+ status: StageStatus;
47
+ /** Input data for this stage (set when stage starts) */
48
+ input?: TInput;
49
+ /** Output data from this stage (set when stage completes) */
50
+ output?: TOutput;
51
+ /** Error if stage failed */
52
+ error?: Error;
53
+ /** Start timestamp (ms since epoch) */
54
+ startedAt?: number;
55
+ /** End timestamp (ms since epoch) */
56
+ completedAt?: number;
57
+ /** Duration in milliseconds */
58
+ duration?: number;
59
+ /** Progress within this stage (0-100) */
60
+ progress?: number;
61
+ /** Number of retry attempts */
62
+ retryCount: number;
63
+ /** Compute options specific to this stage */
64
+ options?: ComputeOptions;
65
+ }
66
+
67
+ /** Configuration for a pipeline stage */
68
+ export interface StageConfig<TInput = unknown, TOutput = unknown> {
69
+ /** Unique identifier for the stage */
70
+ id: string;
71
+ /** Display name for the stage */
72
+ name: string;
73
+ /** Name of the registered compute function */
74
+ functionName: string;
75
+ /** Transform input before passing to compute function */
76
+ transformInput?: (input: TInput, previousResults: unknown[]) => unknown;
77
+ /** Transform output after compute function returns */
78
+ transformOutput?: (output: unknown) => TOutput;
79
+ /** Whether to skip this stage based on previous results */
80
+ shouldSkip?: (input: TInput, previousResults: unknown[]) => boolean;
81
+ /** Maximum retry attempts on failure (default: 0) */
82
+ maxRetries?: number;
83
+ /** Delay between retries in ms (default: 1000) */
84
+ retryDelay?: number;
85
+ /** Compute options for this stage */
86
+ options?: ComputeOptions;
87
+ }
88
+
89
+ /** Overall pipeline status */
90
+ export type PipelineStatus =
91
+ | 'idle' // Not started
92
+ | 'running' // Currently executing
93
+ | 'paused' // Paused mid-execution
94
+ | 'completed' // All stages completed successfully
95
+ | 'failed' // A stage failed (and wasn't recovered)
96
+ | 'cancelled'; // User cancelled
97
+
98
+ /** Metrics for pipeline debugging and reporting */
99
+ export interface PipelineMetrics {
100
+ /** Total stages in pipeline */
101
+ totalStages: number;
102
+ /** Number of completed stages */
103
+ completedStages: number;
104
+ /** Number of failed stages */
105
+ failedStages: number;
106
+ /** Number of skipped stages */
107
+ skippedStages: number;
108
+ /** Total retry attempts across all stages */
109
+ totalRetries: number;
110
+ /** Slowest stage info */
111
+ slowestStage: { id: string; name: string; duration: number } | null;
112
+ /** Fastest stage info */
113
+ fastestStage: { id: string; name: string; duration: number } | null;
114
+ /** Average stage duration */
115
+ averageStageDuration: number;
116
+ /** Timestamp of each stage transition for timeline view */
117
+ timeline: Array<{
118
+ stageId: string;
119
+ stageName: string;
120
+ event: 'started' | 'completed' | 'failed' | 'skipped' | 'retry';
121
+ timestamp: number;
122
+ duration?: number;
123
+ error?: string;
124
+ }>;
125
+ }
126
+
127
+ /** Comprehensive pipeline state for debugging */
128
+ export interface PipelineState<TInput = unknown, TOutput = unknown> {
129
+ /** Overall pipeline status */
130
+ status: PipelineStatus;
131
+ /** All stage information */
132
+ stages: StageInfo[];
133
+ /** Index of currently executing stage (-1 if not running) */
134
+ currentStageIndex: number;
135
+ /** Current stage info (convenience) */
136
+ currentStage: StageInfo | null;
137
+ /** Overall progress percentage (0-100) */
138
+ progress: number;
139
+ /** Final output from the last stage */
140
+ output: TOutput | null;
141
+ /** Initial input that started the pipeline */
142
+ input: TInput | null;
143
+ /** Error that caused pipeline failure */
144
+ error: Error | null;
145
+ /** Pipeline start timestamp */
146
+ startedAt: number | null;
147
+ /** Pipeline completion timestamp */
148
+ completedAt: number | null;
149
+ /** Total duration in milliseconds */
150
+ totalDuration: number | null;
151
+ /** Results from each completed stage */
152
+ stageResults: unknown[];
153
+ /** Execution metrics for debugging */
154
+ metrics: PipelineMetrics;
155
+ }
156
+
157
+ /** Result of a single item in parallel batch */
158
+ export interface BatchItemResult<TOutput = unknown> {
159
+ /** Index of the item in original array */
160
+ index: number;
161
+ /** Whether this item succeeded */
162
+ success: boolean;
163
+ /** Result if successful */
164
+ data?: TOutput;
165
+ /** Error if failed */
166
+ error?: Error;
167
+ /** Duration in ms */
168
+ duration: number;
169
+ }
170
+
171
+ /** Aggregate result of parallel batch processing */
172
+ export interface ParallelBatchResult<TOutput = unknown> {
173
+ /** All individual results */
174
+ results: BatchItemResult<TOutput>[];
175
+ /** Successfully processed items */
176
+ successful: TOutput[];
177
+ /** Failed items with their errors */
178
+ failed: Array<{ index: number; error: Error }>;
179
+ /** Total duration */
180
+ totalDuration: number;
181
+ /** Success rate (0-1) */
182
+ successRate: number;
183
+ }
184
+
25
185
  // ============================================================================
26
186
  // Context
27
187
  // ============================================================================
@@ -148,6 +308,7 @@ export interface UseComputeOptions extends ComputeOptions {
148
308
  *
149
309
  * @example
150
310
  * ```tsx
311
+ * // Basic usage with explicit types
151
312
  * function FibonacciCalculator() {
152
313
  * const { data, loading, error, run } = useCompute<number, number>('fibonacci');
153
314
  *
@@ -162,17 +323,43 @@ export interface UseComputeOptions extends ComputeOptions {
162
323
  * </div>
163
324
  * );
164
325
  * }
326
+ *
327
+ * // With typed registry (extend ComputeFunctionRegistry for autocomplete)
328
+ * // declare module '@computekit/core' {
329
+ * // interface ComputeFunctionRegistry {
330
+ * // fibonacci: { input: number; output: number };
331
+ * // }
332
+ * // }
333
+ * // const { data, run } = useCompute('fibonacci'); // Types are inferred!
165
334
  * ```
166
335
  */
167
- export function useCompute<TInput = unknown, TOutput = unknown>(
168
- functionName: string,
336
+ export function useCompute<
337
+ TName extends RegisteredFunctionName,
338
+ TInput = FunctionInput<TName extends string ? TName : never>,
339
+ TOutput = FunctionOutput<TName extends string ? TName : never>,
340
+ >(
341
+ functionName: TName,
169
342
  options: UseComputeOptions = {}
170
- ): UseComputeReturn<TInput, TOutput> {
343
+ ): UseComputeReturn<
344
+ TName extends keyof ComputeFunctionRegistry
345
+ ? ComputeFunctionRegistry[TName]['input']
346
+ : TInput,
347
+ TName extends keyof ComputeFunctionRegistry
348
+ ? ComputeFunctionRegistry[TName]['output']
349
+ : TOutput
350
+ > {
351
+ type ActualInput = TName extends keyof ComputeFunctionRegistry
352
+ ? ComputeFunctionRegistry[TName]['input']
353
+ : TInput;
354
+ type ActualOutput = TName extends keyof ComputeFunctionRegistry
355
+ ? ComputeFunctionRegistry[TName]['output']
356
+ : TOutput;
357
+
171
358
  const kit = useComputeKit();
172
359
  const abortControllerRef = useRef<AbortController | null>(null);
173
360
  const cancelledRef = useRef(false);
174
361
 
175
- const [state, setState] = useState<UseComputeState<TOutput>>({
362
+ const [state, setState] = useState<UseComputeState<ActualOutput>>({
176
363
  data: null,
177
364
  loading: false,
178
365
  error: null,
@@ -205,7 +392,7 @@ export function useCompute<TInput = unknown, TOutput = unknown>(
205
392
  }, []);
206
393
 
207
394
  const run = useCallback(
208
- async (input: TInput, runOptions?: ComputeOptions) => {
395
+ async (input: ActualInput, runOptions?: ComputeOptions) => {
209
396
  // Cancel any ongoing computation
210
397
  cancel();
211
398
  cancelledRef.current = false;
@@ -228,7 +415,7 @@ export function useCompute<TInput = unknown, TOutput = unknown>(
228
415
  }
229
416
 
230
417
  try {
231
- const result = await kit.run<TInput, TOutput>(functionName, input, {
418
+ const result = (await kit.run(functionName, input, {
232
419
  ...options,
233
420
  ...runOptions,
234
421
  signal: runOptions?.signal ?? abortController.signal,
@@ -237,7 +424,7 @@ export function useCompute<TInput = unknown, TOutput = unknown>(
237
424
  options.onProgress?.(progress);
238
425
  runOptions?.onProgress?.(progress);
239
426
  },
240
- });
427
+ })) as ActualOutput;
241
428
 
242
429
  if (!abortController.signal.aborted) {
243
430
  setState({
@@ -266,7 +453,7 @@ export function useCompute<TInput = unknown, TOutput = unknown>(
266
453
  // Auto-run on mount if configured
267
454
  useEffect(() => {
268
455
  if (options.autoRun && options.initialInput !== undefined) {
269
- run(options.initialInput as TInput);
456
+ run(options.initialInput as ActualInput);
270
457
  }
271
458
  // Only run on mount
272
459
  // eslint-disable-next-line react-hooks/exhaustive-deps
@@ -284,7 +471,7 @@ export function useCompute<TInput = unknown, TOutput = unknown>(
284
471
  run,
285
472
  reset,
286
473
  cancel,
287
- };
474
+ } as UseComputeReturn<ActualInput, ActualOutput>;
288
475
  }
289
476
 
290
477
  // ============================================================================
@@ -296,6 +483,7 @@ export function useCompute<TInput = unknown, TOutput = unknown>(
296
483
  *
297
484
  * @example
298
485
  * ```tsx
486
+ * // Basic usage with explicit types
299
487
  * function Calculator() {
300
488
  * const calculate = useComputeCallback<number[], number>('sum');
301
489
  *
@@ -306,20 +494,43 @@ export function useCompute<TInput = unknown, TOutput = unknown>(
306
494
  *
307
495
  * return <button onClick={handleClick}>Calculate Sum</button>;
308
496
  * }
497
+ *
498
+ * // With typed registry - types are inferred!
499
+ * // const calculate = useComputeCallback('sum');
309
500
  * ```
310
501
  */
311
- export function useComputeCallback<TInput = unknown, TOutput = unknown>(
312
- functionName: string,
502
+ export function useComputeCallback<
503
+ TName extends RegisteredFunctionName,
504
+ TInput = FunctionInput<TName extends string ? TName : never>,
505
+ TOutput = FunctionOutput<TName extends string ? TName : never>,
506
+ >(
507
+ functionName: TName,
313
508
  options?: ComputeOptions
314
- ): (input: TInput, runOptions?: ComputeOptions) => Promise<TOutput> {
509
+ ): (
510
+ input: TName extends keyof ComputeFunctionRegistry
511
+ ? ComputeFunctionRegistry[TName]['input']
512
+ : TInput,
513
+ runOptions?: ComputeOptions
514
+ ) => Promise<
515
+ TName extends keyof ComputeFunctionRegistry
516
+ ? ComputeFunctionRegistry[TName]['output']
517
+ : TOutput
518
+ > {
519
+ type ActualInput = TName extends keyof ComputeFunctionRegistry
520
+ ? ComputeFunctionRegistry[TName]['input']
521
+ : TInput;
522
+ type ActualOutput = TName extends keyof ComputeFunctionRegistry
523
+ ? ComputeFunctionRegistry[TName]['output']
524
+ : TOutput;
525
+
315
526
  const kit = useComputeKit();
316
527
 
317
528
  return useCallback(
318
- (input: TInput, runOptions?: ComputeOptions) => {
319
- return kit.run<TInput, TOutput>(functionName, input, {
529
+ (input: ActualInput, runOptions?: ComputeOptions): Promise<ActualOutput> => {
530
+ return kit.run(functionName, input, {
320
531
  ...options,
321
532
  ...runOptions,
322
- });
533
+ }) as Promise<ActualOutput>;
323
534
  },
324
535
  [kit, functionName, options]
325
536
  );
@@ -334,6 +545,7 @@ export function useComputeCallback<TInput = unknown, TOutput = unknown>(
334
545
  *
335
546
  * @example
336
547
  * ```tsx
548
+ * // Basic usage
337
549
  * function MyComponent() {
338
550
  * const { run, loading, data } = useComputeFunction(
339
551
  * 'myFunction',
@@ -346,13 +558,36 @@ export function useComputeCallback<TInput = unknown, TOutput = unknown>(
346
558
  * </button>
347
559
  * );
348
560
  * }
561
+ *
562
+ * // With typed registry - provides autocomplete and type safety
563
+ * // declare module '@computekit/core' {
564
+ * // interface ComputeFunctionRegistry {
565
+ * // myFunction: { input: number; output: number };
566
+ * // }
567
+ * // }
349
568
  * ```
350
569
  */
351
- export function useComputeFunction<TInput = unknown, TOutput = unknown>(
352
- name: string,
353
- fn: (input: TInput) => TOutput | Promise<TOutput>,
570
+ export function useComputeFunction<
571
+ TName extends RegisteredFunctionName,
572
+ TInput = FunctionInput<TName extends string ? TName : never>,
573
+ TOutput = FunctionOutput<TName extends string ? TName : never>,
574
+ >(
575
+ name: TName,
576
+ fn: TName extends keyof ComputeFunctionRegistry
577
+ ? ComputeFn<
578
+ ComputeFunctionRegistry[TName]['input'],
579
+ ComputeFunctionRegistry[TName]['output']
580
+ >
581
+ : ComputeFn<TInput, TOutput>,
354
582
  options?: UseComputeOptions
355
- ): UseComputeReturn<TInput, TOutput> {
583
+ ): UseComputeReturn<
584
+ TName extends keyof ComputeFunctionRegistry
585
+ ? ComputeFunctionRegistry[TName]['input']
586
+ : TInput,
587
+ TName extends keyof ComputeFunctionRegistry
588
+ ? ComputeFunctionRegistry[TName]['output']
589
+ : TOutput
590
+ > {
356
591
  const kit = useComputeKit();
357
592
 
358
593
  // Register function on mount
@@ -360,7 +595,7 @@ export function useComputeFunction<TInput = unknown, TOutput = unknown>(
360
595
  kit.register(name, fn);
361
596
  }, [kit, name, fn]);
362
597
 
363
- return useCompute<TInput, TOutput>(name, options);
598
+ return useCompute(name, options);
364
599
  }
365
600
 
366
601
  // ============================================================================
@@ -417,6 +652,1046 @@ export function useWasmSupport(): boolean {
417
652
  return kit.isWasmSupported();
418
653
  }
419
654
 
655
+ // ============================================================================
656
+ // usePipeline Hook - Multi-stage Processing
657
+ // ============================================================================
658
+
659
+ /**
660
+ * Options for usePipeline hook
661
+ */
662
+ export interface UsePipelineOptions {
663
+ /** Stop pipeline on first stage failure (default: true) */
664
+ stopOnError?: boolean;
665
+ /** Global timeout for entire pipeline in ms */
666
+ timeout?: number;
667
+ /** Enable detailed timeline tracking (default: true) */
668
+ trackTimeline?: boolean;
669
+ /** Called when pipeline state changes */
670
+ onStateChange?: (state: PipelineState) => void;
671
+ /** Called when a stage starts */
672
+ onStageStart?: (stage: StageInfo) => void;
673
+ /** Called when a stage completes */
674
+ onStageComplete?: (stage: StageInfo) => void;
675
+ /** Called when a stage fails */
676
+ onStageError?: (stage: StageInfo, error: Error) => void;
677
+ /** Called when a stage is retried */
678
+ onStageRetry?: (stage: StageInfo, attempt: number) => void;
679
+ /** Automatically run pipeline on mount */
680
+ autoRun?: boolean;
681
+ /** Initial input for autoRun */
682
+ initialInput?: unknown;
683
+ }
684
+
685
+ /**
686
+ * Actions returned by usePipeline
687
+ */
688
+ export interface UsePipelineActions<TInput> {
689
+ /** Start the pipeline with input */
690
+ run: (input: TInput) => Promise<void>;
691
+ /** Cancel the running pipeline */
692
+ cancel: () => void;
693
+ /** Reset pipeline to initial state */
694
+ reset: () => void;
695
+ /** Pause the pipeline (if supported) */
696
+ pause: () => void;
697
+ /** Resume a paused pipeline */
698
+ resume: () => void;
699
+ /** Retry failed stages */
700
+ retry: () => Promise<void>;
701
+ /** Get a formatted report of the pipeline execution */
702
+ getReport: () => PipelineReport;
703
+ }
704
+
705
+ /**
706
+ * Formatted report for debugging
707
+ */
708
+ export interface PipelineReport {
709
+ /** Human-readable summary */
710
+ summary: string;
711
+ /** Detailed stage-by-stage breakdown */
712
+ stageDetails: Array<{
713
+ name: string;
714
+ status: StageStatus;
715
+ duration: string;
716
+ error?: string;
717
+ }>;
718
+ /** Timeline of events */
719
+ timeline: string[];
720
+ /** Performance insights */
721
+ insights: string[];
722
+ /** Raw metrics */
723
+ metrics: PipelineMetrics;
724
+ }
725
+
726
+ /**
727
+ * Return type for usePipeline
728
+ */
729
+ export type UsePipelineReturn<TInput, TOutput> = PipelineState<TInput, TOutput> &
730
+ UsePipelineActions<TInput> & {
731
+ /** Whether pipeline is currently running */
732
+ isRunning: boolean;
733
+ /** Whether pipeline completed successfully */
734
+ isComplete: boolean;
735
+ /** Whether pipeline has failed */
736
+ isFailed: boolean;
737
+ /** Quick access to check if a specific stage is done */
738
+ isStageComplete: (stageId: string) => boolean;
739
+ /** Get a specific stage by ID */
740
+ getStage: (stageId: string) => StageInfo | undefined;
741
+ };
742
+
743
+ /**
744
+ * Create initial pipeline state
745
+ */
746
+ function createInitialPipelineState<TInput, TOutput>(
747
+ stages: StageConfig[]
748
+ ): PipelineState<TInput, TOutput> {
749
+ return {
750
+ status: 'idle',
751
+ stages: stages.map((config) => ({
752
+ id: config.id,
753
+ name: config.name,
754
+ functionName: config.functionName,
755
+ status: 'pending' as StageStatus,
756
+ retryCount: 0,
757
+ options: config.options,
758
+ })),
759
+ currentStageIndex: -1,
760
+ currentStage: null,
761
+ progress: 0,
762
+ output: null,
763
+ input: null,
764
+ error: null,
765
+ startedAt: null,
766
+ completedAt: null,
767
+ totalDuration: null,
768
+ stageResults: [],
769
+ metrics: {
770
+ totalStages: stages.length,
771
+ completedStages: 0,
772
+ failedStages: 0,
773
+ skippedStages: 0,
774
+ totalRetries: 0,
775
+ slowestStage: null,
776
+ fastestStage: null,
777
+ averageStageDuration: 0,
778
+ timeline: [],
779
+ },
780
+ };
781
+ }
782
+
783
+ /**
784
+ * Format duration in human-readable format
785
+ */
786
+ function formatDuration(ms: number): string {
787
+ if (ms < 1000) return `${ms.toFixed(0)}ms`;
788
+ if (ms < 60000) return `${(ms / 1000).toFixed(2)}s`;
789
+ return `${(ms / 60000).toFixed(2)}min`;
790
+ }
791
+
792
+ /**
793
+ * Hook for multi-stage pipeline processing
794
+ *
795
+ * Provides comprehensive debugging, progress tracking, and error handling
796
+ * for complex multi-stage compute workflows.
797
+ *
798
+ * @example
799
+ * ```tsx
800
+ * function FileProcessor() {
801
+ * const pipeline = usePipeline<string[], ProcessedFiles>([
802
+ * { id: 'download', name: 'Download Files', functionName: 'downloadFiles' },
803
+ * { id: 'process', name: 'Process Files', functionName: 'processFiles' },
804
+ * { id: 'compress', name: 'Compress Output', functionName: 'compressFiles' },
805
+ * ]);
806
+ *
807
+ * return (
808
+ * <div>
809
+ * <button onClick={() => pipeline.run(urls)} disabled={pipeline.isRunning}>
810
+ * Start Processing
811
+ * </button>
812
+ *
813
+ * <div>Status: {pipeline.status}</div>
814
+ * <div>Progress: {pipeline.progress.toFixed(0)}%</div>
815
+ *
816
+ * {pipeline.currentStage && (
817
+ * <div>Current: {pipeline.currentStage.name}</div>
818
+ * )}
819
+ *
820
+ * {pipeline.stages.map(stage => (
821
+ * <div key={stage.id}>
822
+ * {stage.name}: {stage.status}
823
+ * {stage.duration && ` (${stage.duration}ms)`}
824
+ * </div>
825
+ * ))}
826
+ *
827
+ * {pipeline.isFailed && (
828
+ * <button onClick={pipeline.retry}>Retry Failed</button>
829
+ * )}
830
+ *
831
+ * {pipeline.isComplete && (
832
+ * <pre>{JSON.stringify(pipeline.getReport(), null, 2)}</pre>
833
+ * )}
834
+ * </div>
835
+ * );
836
+ * }
837
+ * ```
838
+ */
839
+ export function usePipeline<TInput = unknown, TOutput = unknown>(
840
+ stageConfigs: StageConfig[],
841
+ options: UsePipelineOptions = {}
842
+ ): UsePipelineReturn<TInput, TOutput> {
843
+ const kit = useComputeKit();
844
+ const abortControllerRef = useRef<AbortController | null>(null);
845
+ const pausedRef = useRef(false);
846
+ const resumePromiseRef = useRef<{
847
+ resolve: () => void;
848
+ reject: (err: Error) => void;
849
+ } | null>(null);
850
+
851
+ const [state, setState] = useState<PipelineState<TInput, TOutput>>(() =>
852
+ createInitialPipelineState<TInput, TOutput>(stageConfigs)
853
+ );
854
+
855
+ // Memoize stage configs to prevent unnecessary re-renders
856
+ const stages = useMemo(() => stageConfigs, [stageConfigs]);
857
+
858
+ /**
859
+ * Add event to timeline
860
+ */
861
+ const addTimelineEvent = useCallback(
862
+ (
863
+ stageId: string,
864
+ stageName: string,
865
+ event: 'started' | 'completed' | 'failed' | 'skipped' | 'retry',
866
+ duration?: number,
867
+ error?: string
868
+ ) => {
869
+ if (options.trackTimeline === false) return;
870
+
871
+ setState((prev) => ({
872
+ ...prev,
873
+ metrics: {
874
+ ...prev.metrics,
875
+ timeline: [
876
+ ...prev.metrics.timeline,
877
+ {
878
+ stageId,
879
+ stageName,
880
+ event,
881
+ timestamp: Date.now(),
882
+ duration,
883
+ error,
884
+ },
885
+ ],
886
+ },
887
+ }));
888
+ },
889
+ [options.trackTimeline]
890
+ );
891
+
892
+ /**
893
+ * Update metrics after stage completion
894
+ */
895
+ const updateMetrics = useCallback(
896
+ (_completedStage: StageInfo, allStages: StageInfo[]) => {
897
+ const completedStages = allStages.filter((s) => s.status === 'completed');
898
+ const durations = completedStages
899
+ .filter((s) => s.duration !== undefined)
900
+ .map((s) => ({ id: s.id, name: s.name, duration: s.duration! }));
901
+
902
+ const slowest = durations.length
903
+ ? durations.reduce((a, b) => (a.duration > b.duration ? a : b))
904
+ : null;
905
+ const fastest = durations.length
906
+ ? durations.reduce((a, b) => (a.duration < b.duration ? a : b))
907
+ : null;
908
+ const avgDuration = durations.length
909
+ ? durations.reduce((sum, d) => sum + d.duration, 0) / durations.length
910
+ : 0;
911
+
912
+ return {
913
+ totalStages: allStages.length,
914
+ completedStages: completedStages.length,
915
+ failedStages: allStages.filter((s) => s.status === 'failed').length,
916
+ skippedStages: allStages.filter((s) => s.status === 'skipped').length,
917
+ totalRetries: allStages.reduce((sum, s) => sum + s.retryCount, 0),
918
+ slowestStage: slowest,
919
+ fastestStage: fastest,
920
+ averageStageDuration: avgDuration,
921
+ };
922
+ },
923
+ []
924
+ );
925
+
926
+ /**
927
+ * Execute a single stage with retries
928
+ */
929
+ const executeStage = useCallback(
930
+ async (
931
+ stageConfig: StageConfig,
932
+ stageIndex: number,
933
+ input: unknown,
934
+ previousResults: unknown[],
935
+ signal: AbortSignal
936
+ ): Promise<{ success: boolean; output?: unknown; error?: Error }> => {
937
+ const maxRetries = stageConfig.maxRetries ?? 0;
938
+ const retryDelay = stageConfig.retryDelay ?? 1000;
939
+ let lastError: Error | undefined;
940
+
941
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
942
+ // Check for abort
943
+ if (signal.aborted) {
944
+ return { success: false, error: new Error('Pipeline cancelled') };
945
+ }
946
+
947
+ // Check for pause
948
+ if (pausedRef.current) {
949
+ await new Promise<void>((resolve, reject) => {
950
+ resumePromiseRef.current = { resolve, reject };
951
+ });
952
+ }
953
+
954
+ // Check if should skip
955
+ if (stageConfig.shouldSkip?.(input as never, previousResults)) {
956
+ setState((prev) => {
957
+ const newStages = [...prev.stages];
958
+ newStages[stageIndex] = {
959
+ ...newStages[stageIndex],
960
+ status: 'skipped',
961
+ };
962
+ return {
963
+ ...prev,
964
+ stages: newStages,
965
+ };
966
+ });
967
+ addTimelineEvent(stageConfig.id, stageConfig.name, 'skipped');
968
+ options.onStageComplete?.(state.stages[stageIndex]);
969
+ return { success: true, output: previousResults[previousResults.length - 1] };
970
+ }
971
+
972
+ // Transform input if needed
973
+ const transformedInput = stageConfig.transformInput
974
+ ? stageConfig.transformInput(input as never, previousResults)
975
+ : input;
976
+
977
+ const startTime = performance.now();
978
+
979
+ // Update stage to running
980
+ setState((prev) => {
981
+ const newStages = [...prev.stages];
982
+ newStages[stageIndex] = {
983
+ ...newStages[stageIndex],
984
+ status: 'running',
985
+ input: transformedInput,
986
+ startedAt: Date.now(),
987
+ retryCount: attempt,
988
+ };
989
+ return {
990
+ ...prev,
991
+ stages: newStages,
992
+ currentStageIndex: stageIndex,
993
+ currentStage: newStages[stageIndex],
994
+ };
995
+ });
996
+
997
+ if (attempt === 0) {
998
+ addTimelineEvent(stageConfig.id, stageConfig.name, 'started');
999
+ options.onStageStart?.(state.stages[stageIndex]);
1000
+ } else {
1001
+ addTimelineEvent(stageConfig.id, stageConfig.name, 'retry');
1002
+ options.onStageRetry?.(state.stages[stageIndex], attempt);
1003
+ }
1004
+
1005
+ try {
1006
+ const result = await kit.run(stageConfig.functionName, transformedInput, {
1007
+ ...stageConfig.options,
1008
+ signal,
1009
+ onProgress: (progress) => {
1010
+ setState((prev) => {
1011
+ const newStages = [...prev.stages];
1012
+ newStages[stageIndex] = {
1013
+ ...newStages[stageIndex],
1014
+ progress: progress.percent,
1015
+ };
1016
+ // Calculate overall progress
1017
+ const stageProgress = progress.percent / 100;
1018
+ const overallProgress =
1019
+ ((stageIndex + stageProgress) / stages.length) * 100;
1020
+ return {
1021
+ ...prev,
1022
+ stages: newStages,
1023
+ progress: overallProgress,
1024
+ };
1025
+ });
1026
+ },
1027
+ });
1028
+
1029
+ const duration = performance.now() - startTime;
1030
+
1031
+ // Transform output if needed
1032
+ const transformedOutput = stageConfig.transformOutput
1033
+ ? stageConfig.transformOutput(result)
1034
+ : result;
1035
+
1036
+ // Update stage to completed
1037
+ setState((prev) => {
1038
+ const newStages = [...prev.stages];
1039
+ newStages[stageIndex] = {
1040
+ ...newStages[stageIndex],
1041
+ status: 'completed',
1042
+ output: transformedOutput,
1043
+ completedAt: Date.now(),
1044
+ duration,
1045
+ progress: 100,
1046
+ };
1047
+
1048
+ const newMetrics = {
1049
+ ...prev.metrics,
1050
+ ...updateMetrics(newStages[stageIndex], newStages),
1051
+ };
1052
+
1053
+ return {
1054
+ ...prev,
1055
+ stages: newStages,
1056
+ metrics: newMetrics,
1057
+ progress: ((stageIndex + 1) / stages.length) * 100,
1058
+ };
1059
+ });
1060
+
1061
+ addTimelineEvent(stageConfig.id, stageConfig.name, 'completed', duration);
1062
+ options.onStageComplete?.(state.stages[stageIndex]);
1063
+
1064
+ return { success: true, output: transformedOutput };
1065
+ } catch (err) {
1066
+ lastError = err instanceof Error ? err : new Error(String(err));
1067
+
1068
+ if (attempt < maxRetries) {
1069
+ // Wait before retry
1070
+ await new Promise((resolve) => setTimeout(resolve, retryDelay));
1071
+ continue;
1072
+ }
1073
+
1074
+ // Final failure
1075
+ const duration = performance.now() - startTime;
1076
+
1077
+ setState((prev) => {
1078
+ const newStages = [...prev.stages];
1079
+ newStages[stageIndex] = {
1080
+ ...newStages[stageIndex],
1081
+ status: 'failed',
1082
+ error: lastError,
1083
+ completedAt: Date.now(),
1084
+ duration,
1085
+ };
1086
+ return {
1087
+ ...prev,
1088
+ stages: newStages,
1089
+ metrics: {
1090
+ ...prev.metrics,
1091
+ failedStages: prev.metrics.failedStages + 1,
1092
+ },
1093
+ };
1094
+ });
1095
+
1096
+ addTimelineEvent(
1097
+ stageConfig.id,
1098
+ stageConfig.name,
1099
+ 'failed',
1100
+ duration,
1101
+ lastError.message
1102
+ );
1103
+ options.onStageError?.(state.stages[stageIndex], lastError);
1104
+
1105
+ return { success: false, error: lastError };
1106
+ }
1107
+ }
1108
+
1109
+ return { success: false, error: lastError };
1110
+ },
1111
+ [kit, stages, state.stages, addTimelineEvent, updateMetrics, options]
1112
+ );
1113
+
1114
+ /**
1115
+ * Run the pipeline
1116
+ */
1117
+ const run = useCallback(
1118
+ async (input: TInput): Promise<void> => {
1119
+ // Cancel any existing run
1120
+ if (abortControllerRef.current) {
1121
+ abortControllerRef.current.abort();
1122
+ }
1123
+
1124
+ const abortController = new AbortController();
1125
+ abortControllerRef.current = abortController;
1126
+ pausedRef.current = false;
1127
+
1128
+ const startTime = Date.now();
1129
+
1130
+ // Reset state
1131
+ setState(() => ({
1132
+ ...createInitialPipelineState<TInput, TOutput>(stages),
1133
+ status: 'running',
1134
+ input,
1135
+ startedAt: startTime,
1136
+ }));
1137
+
1138
+ const stageResults: unknown[] = [];
1139
+ let currentInput: unknown = input;
1140
+ let finalError: Error | null = null;
1141
+
1142
+ for (let i = 0; i < stages.length; i++) {
1143
+ if (abortController.signal.aborted) {
1144
+ setState((prev) => ({
1145
+ ...prev,
1146
+ status: 'cancelled',
1147
+ completedAt: Date.now(),
1148
+ totalDuration: Date.now() - startTime,
1149
+ }));
1150
+ return;
1151
+ }
1152
+
1153
+ const result = await executeStage(
1154
+ stages[i],
1155
+ i,
1156
+ currentInput,
1157
+ stageResults,
1158
+ abortController.signal
1159
+ );
1160
+
1161
+ if (!result.success) {
1162
+ finalError = result.error ?? new Error('Stage failed');
1163
+
1164
+ if (options.stopOnError !== false) {
1165
+ setState((prev) => ({
1166
+ ...prev,
1167
+ status: 'failed',
1168
+ error: finalError,
1169
+ stageResults,
1170
+ completedAt: Date.now(),
1171
+ totalDuration: Date.now() - startTime,
1172
+ }));
1173
+ return;
1174
+ }
1175
+ }
1176
+
1177
+ if (result.output !== undefined) {
1178
+ stageResults.push(result.output);
1179
+ currentInput = result.output;
1180
+ }
1181
+ }
1182
+
1183
+ // Pipeline completed
1184
+ setState((prev) => ({
1185
+ ...prev,
1186
+ status: finalError ? 'failed' : 'completed',
1187
+ output: (currentInput as TOutput) ?? null,
1188
+ error: finalError,
1189
+ stageResults,
1190
+ completedAt: Date.now(),
1191
+ totalDuration: Date.now() - startTime,
1192
+ currentStageIndex: -1,
1193
+ currentStage: null,
1194
+ progress: 100,
1195
+ }));
1196
+
1197
+ options.onStateChange?.(state);
1198
+ },
1199
+ [stages, executeStage, options, state]
1200
+ );
1201
+
1202
+ /**
1203
+ * Cancel the pipeline
1204
+ */
1205
+ const cancel = useCallback(() => {
1206
+ if (abortControllerRef.current) {
1207
+ abortControllerRef.current.abort();
1208
+ abortControllerRef.current = null;
1209
+ }
1210
+ if (resumePromiseRef.current) {
1211
+ resumePromiseRef.current.reject(new Error('Pipeline cancelled'));
1212
+ resumePromiseRef.current = null;
1213
+ }
1214
+ setState((prev) => ({
1215
+ ...prev,
1216
+ status: 'cancelled',
1217
+ completedAt: Date.now(),
1218
+ totalDuration: prev.startedAt ? Date.now() - prev.startedAt : null,
1219
+ }));
1220
+ }, []);
1221
+
1222
+ /**
1223
+ * Reset the pipeline
1224
+ */
1225
+ const reset = useCallback(() => {
1226
+ cancel();
1227
+ setState(createInitialPipelineState<TInput, TOutput>(stages));
1228
+ }, [cancel, stages]);
1229
+
1230
+ /**
1231
+ * Pause the pipeline
1232
+ */
1233
+ const pause = useCallback(() => {
1234
+ pausedRef.current = true;
1235
+ setState((prev) => ({
1236
+ ...prev,
1237
+ status: 'paused',
1238
+ }));
1239
+ }, []);
1240
+
1241
+ /**
1242
+ * Resume the pipeline
1243
+ */
1244
+ const resume = useCallback(() => {
1245
+ pausedRef.current = false;
1246
+ if (resumePromiseRef.current) {
1247
+ resumePromiseRef.current.resolve();
1248
+ resumePromiseRef.current = null;
1249
+ }
1250
+ setState((prev) => ({
1251
+ ...prev,
1252
+ status: 'running',
1253
+ }));
1254
+ }, []);
1255
+
1256
+ /**
1257
+ * Retry failed stages
1258
+ */
1259
+ const retry = useCallback(async (): Promise<void> => {
1260
+ if (state.status !== 'failed' || !state.input) return;
1261
+
1262
+ // Find first failed stage
1263
+ const failedIndex = state.stages.findIndex((s) => s.status === 'failed');
1264
+ if (failedIndex === -1) return;
1265
+
1266
+ // Get input for failed stage (output of previous stage or original input)
1267
+ const retryInput =
1268
+ failedIndex === 0 ? state.input : state.stageResults[failedIndex - 1];
1269
+
1270
+ // Create new abort controller
1271
+ const abortController = new AbortController();
1272
+ abortControllerRef.current = abortController;
1273
+
1274
+ setState((prev) => ({
1275
+ ...prev,
1276
+ status: 'running',
1277
+ error: null,
1278
+ }));
1279
+
1280
+ const stageResults = [...state.stageResults.slice(0, failedIndex)];
1281
+ let currentInput = retryInput;
1282
+
1283
+ for (let i = failedIndex; i < stages.length; i++) {
1284
+ if (abortController.signal.aborted) {
1285
+ setState((prev) => ({ ...prev, status: 'cancelled' }));
1286
+ return;
1287
+ }
1288
+
1289
+ const result = await executeStage(
1290
+ stages[i],
1291
+ i,
1292
+ currentInput,
1293
+ stageResults,
1294
+ abortController.signal
1295
+ );
1296
+
1297
+ if (!result.success) {
1298
+ setState((prev) => ({
1299
+ ...prev,
1300
+ status: 'failed',
1301
+ error: result.error ?? new Error('Stage failed'),
1302
+ stageResults,
1303
+ }));
1304
+ return;
1305
+ }
1306
+
1307
+ if (result.output !== undefined) {
1308
+ stageResults.push(result.output);
1309
+ currentInput = result.output;
1310
+ }
1311
+ }
1312
+
1313
+ setState((prev) => ({
1314
+ ...prev,
1315
+ status: 'completed',
1316
+ output: currentInput as TOutput,
1317
+ stageResults,
1318
+ completedAt: Date.now(),
1319
+ totalDuration: prev.startedAt ? Date.now() - prev.startedAt : null,
1320
+ progress: 100,
1321
+ }));
1322
+ }, [state, stages, executeStage]);
1323
+
1324
+ /**
1325
+ * Generate execution report
1326
+ */
1327
+ const getReport = useCallback((): PipelineReport => {
1328
+ const stageDetails = state.stages.map((stage) => ({
1329
+ name: stage.name,
1330
+ status: stage.status,
1331
+ duration: stage.duration ? formatDuration(stage.duration) : '-',
1332
+ error: stage.error?.message,
1333
+ }));
1334
+
1335
+ const timeline = state.metrics.timeline.map((event) => {
1336
+ const time = new Date(event.timestamp).toISOString().split('T')[1].split('.')[0];
1337
+ const duration = event.duration ? ` (${formatDuration(event.duration)})` : '';
1338
+ const error = event.error ? ` - ${event.error}` : '';
1339
+ return `[${time}] ${event.stageName}: ${event.event}${duration}${error}`;
1340
+ });
1341
+
1342
+ const insights: string[] = [];
1343
+
1344
+ if (state.metrics.slowestStage) {
1345
+ insights.push(
1346
+ `Slowest stage: ${state.metrics.slowestStage.name} (${formatDuration(
1347
+ state.metrics.slowestStage.duration
1348
+ )})`
1349
+ );
1350
+ }
1351
+
1352
+ if (state.metrics.fastestStage) {
1353
+ insights.push(
1354
+ `Fastest stage: ${state.metrics.fastestStage.name} (${formatDuration(
1355
+ state.metrics.fastestStage.duration
1356
+ )})`
1357
+ );
1358
+ }
1359
+
1360
+ if (state.metrics.totalRetries > 0) {
1361
+ insights.push(`Total retries: ${state.metrics.totalRetries}`);
1362
+ }
1363
+
1364
+ if (state.metrics.averageStageDuration > 0) {
1365
+ insights.push(
1366
+ `Average stage duration: ${formatDuration(state.metrics.averageStageDuration)}`
1367
+ );
1368
+ }
1369
+
1370
+ const successRate =
1371
+ state.metrics.totalStages > 0
1372
+ ? (state.metrics.completedStages / state.metrics.totalStages) * 100
1373
+ : 0;
1374
+
1375
+ const summary = [
1376
+ `Pipeline Status: ${state.status.toUpperCase()}`,
1377
+ `Stages: ${state.metrics.completedStages}/${state.metrics.totalStages} completed`,
1378
+ `Success Rate: ${successRate.toFixed(0)}%`,
1379
+ state.totalDuration ? `Total Duration: ${formatDuration(state.totalDuration)}` : '',
1380
+ state.error ? `Error: ${state.error.message}` : '',
1381
+ ]
1382
+ .filter(Boolean)
1383
+ .join('\n');
1384
+
1385
+ return {
1386
+ summary,
1387
+ stageDetails,
1388
+ timeline,
1389
+ insights,
1390
+ metrics: state.metrics,
1391
+ };
1392
+ }, [state]);
1393
+
1394
+ /**
1395
+ * Check if a stage is complete
1396
+ */
1397
+ const isStageComplete = useCallback(
1398
+ (stageId: string): boolean => {
1399
+ const stage = state.stages.find((s) => s.id === stageId);
1400
+ return stage?.status === 'completed';
1401
+ },
1402
+ [state.stages]
1403
+ );
1404
+
1405
+ /**
1406
+ * Get a stage by ID
1407
+ */
1408
+ const getStage = useCallback(
1409
+ (stageId: string): StageInfo | undefined => {
1410
+ return state.stages.find((s) => s.id === stageId);
1411
+ },
1412
+ [state.stages]
1413
+ );
1414
+
1415
+ // Auto-run on mount if configured
1416
+ useEffect(() => {
1417
+ if (options.autoRun && options.initialInput !== undefined) {
1418
+ run(options.initialInput as TInput);
1419
+ }
1420
+ // eslint-disable-next-line react-hooks/exhaustive-deps
1421
+ }, []);
1422
+
1423
+ // Cleanup on unmount
1424
+ useEffect(() => {
1425
+ return () => {
1426
+ cancel();
1427
+ };
1428
+ }, [cancel]);
1429
+
1430
+ return {
1431
+ ...state,
1432
+ run,
1433
+ cancel,
1434
+ reset,
1435
+ pause,
1436
+ resume,
1437
+ retry,
1438
+ getReport,
1439
+ isRunning: state.status === 'running',
1440
+ isComplete: state.status === 'completed',
1441
+ isFailed: state.status === 'failed',
1442
+ isStageComplete,
1443
+ getStage,
1444
+ };
1445
+ }
1446
+
1447
+ // ============================================================================
1448
+ // useParallelBatch Hook - Parallel Processing Within Stages
1449
+ // ============================================================================
1450
+
1451
+ /**
1452
+ * Result type for useParallelBatch
1453
+ */
1454
+ export interface UseParallelBatchReturn<TItem, TOutput> {
1455
+ /** Execute batch processing */
1456
+ run: (items: TItem[]) => Promise<ParallelBatchResult<TOutput>>;
1457
+ /** Current batch result */
1458
+ result: ParallelBatchResult<TOutput> | null;
1459
+ /** Loading state */
1460
+ loading: boolean;
1461
+ /** Current progress (0-100) */
1462
+ progress: number;
1463
+ /** Number of completed items */
1464
+ completedCount: number;
1465
+ /** Total items in current batch */
1466
+ totalCount: number;
1467
+ /** Cancel batch processing */
1468
+ cancel: () => void;
1469
+ /** Reset state */
1470
+ reset: () => void;
1471
+ }
1472
+
1473
+ /**
1474
+ * Hook for parallel batch processing
1475
+ *
1476
+ * Useful for processing multiple items in parallel within a pipeline stage.
1477
+ *
1478
+ * @example
1479
+ * ```tsx
1480
+ * // Basic usage with explicit types
1481
+ * function BatchProcessor() {
1482
+ * const batch = useParallelBatch<string, ProcessedFile>('processFile', {
1483
+ * concurrency: 4
1484
+ * });
1485
+ *
1486
+ * return (
1487
+ * <div>
1488
+ * <button
1489
+ * onClick={() => batch.run(fileUrls)}
1490
+ * disabled={batch.loading}
1491
+ * >
1492
+ * Process {fileUrls.length} Files
1493
+ * </button>
1494
+ *
1495
+ * {batch.loading && (
1496
+ * <div>
1497
+ * Processing: {batch.completedCount}/{batch.totalCount}
1498
+ * ({batch.progress.toFixed(0)}%)
1499
+ * </div>
1500
+ * )}
1501
+ *
1502
+ * {batch.result && (
1503
+ * <div>
1504
+ * Success: {batch.result.successful.length}
1505
+ * Failed: {batch.result.failed.length}
1506
+ * </div>
1507
+ * )}
1508
+ * </div>
1509
+ * );
1510
+ * }
1511
+ *
1512
+ * // With typed registry - types are inferred!
1513
+ * // const batch = useParallelBatch('processFile');
1514
+ * ```
1515
+ */
1516
+ export function useParallelBatch<
1517
+ TName extends RegisteredFunctionName,
1518
+ TItem = FunctionInput<TName extends string ? TName : never>,
1519
+ TOutput = FunctionOutput<TName extends string ? TName : never>,
1520
+ >(
1521
+ functionName: TName,
1522
+ options: {
1523
+ concurrency?: number;
1524
+ computeOptions?: ComputeOptions;
1525
+ } = {}
1526
+ ): UseParallelBatchReturn<
1527
+ TName extends keyof ComputeFunctionRegistry
1528
+ ? ComputeFunctionRegistry[TName]['input']
1529
+ : TItem,
1530
+ TName extends keyof ComputeFunctionRegistry
1531
+ ? ComputeFunctionRegistry[TName]['output']
1532
+ : TOutput
1533
+ > {
1534
+ type ActualItem = TName extends keyof ComputeFunctionRegistry
1535
+ ? ComputeFunctionRegistry[TName]['input']
1536
+ : TItem;
1537
+ type ActualOutput = TName extends keyof ComputeFunctionRegistry
1538
+ ? ComputeFunctionRegistry[TName]['output']
1539
+ : TOutput;
1540
+
1541
+ const kit = useComputeKit();
1542
+ const abortControllerRef = useRef<AbortController | null>(null);
1543
+
1544
+ const [state, setState] = useState<{
1545
+ result: ParallelBatchResult<ActualOutput> | null;
1546
+ loading: boolean;
1547
+ progress: number;
1548
+ completedCount: number;
1549
+ totalCount: number;
1550
+ }>({
1551
+ result: null,
1552
+ loading: false,
1553
+ progress: 0,
1554
+ completedCount: 0,
1555
+ totalCount: 0,
1556
+ });
1557
+
1558
+ const run = useCallback(
1559
+ async (items: ActualItem[]): Promise<ParallelBatchResult<ActualOutput>> => {
1560
+ // Cancel any existing batch
1561
+ if (abortControllerRef.current) {
1562
+ abortControllerRef.current.abort();
1563
+ }
1564
+
1565
+ const abortController = new AbortController();
1566
+ abortControllerRef.current = abortController;
1567
+
1568
+ setState({
1569
+ result: null,
1570
+ loading: true,
1571
+ progress: 0,
1572
+ completedCount: 0,
1573
+ totalCount: items.length,
1574
+ });
1575
+
1576
+ const startTime = performance.now();
1577
+ const results: BatchItemResult<ActualOutput>[] = [];
1578
+ const concurrency = options.concurrency ?? items.length;
1579
+
1580
+ // Process in batches based on concurrency
1581
+ for (let i = 0; i < items.length; i += concurrency) {
1582
+ if (abortController.signal.aborted) {
1583
+ break;
1584
+ }
1585
+
1586
+ const batch = items.slice(i, i + concurrency);
1587
+ const batchPromises = batch.map(async (item, batchIndex) => {
1588
+ const index = i + batchIndex;
1589
+ const itemStart = performance.now();
1590
+
1591
+ try {
1592
+ const data = (await kit.run(functionName, item, {
1593
+ ...options.computeOptions,
1594
+ signal: abortController.signal,
1595
+ })) as ActualOutput;
1596
+
1597
+ const itemResult: BatchItemResult<ActualOutput> = {
1598
+ index,
1599
+ success: true,
1600
+ data,
1601
+ duration: performance.now() - itemStart,
1602
+ };
1603
+
1604
+ return itemResult;
1605
+ } catch (err) {
1606
+ const itemResult: BatchItemResult<ActualOutput> = {
1607
+ index,
1608
+ success: false,
1609
+ error: err instanceof Error ? err : new Error(String(err)),
1610
+ duration: performance.now() - itemStart,
1611
+ };
1612
+
1613
+ return itemResult;
1614
+ }
1615
+ });
1616
+
1617
+ const batchResults = await Promise.all(batchPromises);
1618
+ results.push(...batchResults);
1619
+
1620
+ // Update progress
1621
+ const completed = results.length;
1622
+ setState((prev) => ({
1623
+ ...prev,
1624
+ completedCount: completed,
1625
+ progress: (completed / items.length) * 100,
1626
+ }));
1627
+ }
1628
+
1629
+ const totalDuration = performance.now() - startTime;
1630
+ const successful = results
1631
+ .filter((r) => r.success && r.data !== undefined)
1632
+ .map((r) => r.data as ActualOutput);
1633
+ const failed = results
1634
+ .filter((r) => !r.success)
1635
+ .map((r) => ({ index: r.index, error: r.error! }));
1636
+
1637
+ const finalResult: ParallelBatchResult<ActualOutput> = {
1638
+ results,
1639
+ successful,
1640
+ failed,
1641
+ totalDuration,
1642
+ successRate: successful.length / items.length,
1643
+ };
1644
+
1645
+ setState({
1646
+ result: finalResult,
1647
+ loading: false,
1648
+ progress: 100,
1649
+ completedCount: items.length,
1650
+ totalCount: items.length,
1651
+ });
1652
+
1653
+ return finalResult;
1654
+ },
1655
+ [kit, functionName, options.concurrency, options.computeOptions]
1656
+ );
1657
+
1658
+ const cancel = useCallback(() => {
1659
+ if (abortControllerRef.current) {
1660
+ abortControllerRef.current.abort();
1661
+ abortControllerRef.current = null;
1662
+ }
1663
+ setState((prev) => ({
1664
+ ...prev,
1665
+ loading: false,
1666
+ }));
1667
+ }, []);
1668
+
1669
+ const reset = useCallback(() => {
1670
+ cancel();
1671
+ setState({
1672
+ result: null,
1673
+ loading: false,
1674
+ progress: 0,
1675
+ completedCount: 0,
1676
+ totalCount: 0,
1677
+ });
1678
+ }, [cancel]);
1679
+
1680
+ // Cleanup on unmount
1681
+ useEffect(() => {
1682
+ return () => {
1683
+ cancel();
1684
+ };
1685
+ }, [cancel]);
1686
+
1687
+ return {
1688
+ ...state,
1689
+ run,
1690
+ cancel,
1691
+ reset,
1692
+ } as UseParallelBatchReturn<ActualItem, ActualOutput>;
1693
+ }
1694
+
420
1695
  // ============================================================================
421
1696
  // Exports
422
1697
  // ============================================================================
@@ -426,6 +1701,17 @@ export type {
426
1701
  ComputeOptions,
427
1702
  ComputeProgress,
428
1703
  PoolStats,
1704
+ // Typed registry exports
1705
+ ComputeFunctionRegistry,
1706
+ RegisteredFunctionName,
1707
+ FunctionInput,
1708
+ FunctionOutput,
1709
+ ComputeFn,
1710
+ InferComputeFn,
1711
+ DefineFunction,
1712
+ HasRegisteredFunctions,
429
1713
  } from '@computekit/core';
430
1714
 
431
1715
  export { ComputeKit } from '@computekit/core';
1716
+
1717
+ // Pipeline types are exported from interface declarations above