@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/dist/index.cjs CHANGED
@@ -168,6 +168,607 @@ function useWasmSupport() {
168
168
  const kit = useComputeKit();
169
169
  return kit.isWasmSupported();
170
170
  }
171
+ function createInitialPipelineState(stages) {
172
+ return {
173
+ status: "idle",
174
+ stages: stages.map((config) => ({
175
+ id: config.id,
176
+ name: config.name,
177
+ functionName: config.functionName,
178
+ status: "pending",
179
+ retryCount: 0,
180
+ options: config.options
181
+ })),
182
+ currentStageIndex: -1,
183
+ currentStage: null,
184
+ progress: 0,
185
+ output: null,
186
+ input: null,
187
+ error: null,
188
+ startedAt: null,
189
+ completedAt: null,
190
+ totalDuration: null,
191
+ stageResults: [],
192
+ metrics: {
193
+ totalStages: stages.length,
194
+ completedStages: 0,
195
+ failedStages: 0,
196
+ skippedStages: 0,
197
+ totalRetries: 0,
198
+ slowestStage: null,
199
+ fastestStage: null,
200
+ averageStageDuration: 0,
201
+ timeline: []
202
+ }
203
+ };
204
+ }
205
+ function formatDuration(ms) {
206
+ if (ms < 1e3) return `${ms.toFixed(0)}ms`;
207
+ if (ms < 6e4) return `${(ms / 1e3).toFixed(2)}s`;
208
+ return `${(ms / 6e4).toFixed(2)}min`;
209
+ }
210
+ function usePipeline(stageConfigs, options = {}) {
211
+ const kit = useComputeKit();
212
+ const abortControllerRef = react.useRef(null);
213
+ const pausedRef = react.useRef(false);
214
+ const resumePromiseRef = react.useRef(null);
215
+ const [state, setState] = react.useState(
216
+ () => createInitialPipelineState(stageConfigs)
217
+ );
218
+ const stages = react.useMemo(() => stageConfigs, [stageConfigs]);
219
+ const addTimelineEvent = react.useCallback(
220
+ (stageId, stageName, event, duration, error) => {
221
+ if (options.trackTimeline === false) return;
222
+ setState((prev) => ({
223
+ ...prev,
224
+ metrics: {
225
+ ...prev.metrics,
226
+ timeline: [
227
+ ...prev.metrics.timeline,
228
+ {
229
+ stageId,
230
+ stageName,
231
+ event,
232
+ timestamp: Date.now(),
233
+ duration,
234
+ error
235
+ }
236
+ ]
237
+ }
238
+ }));
239
+ },
240
+ [options.trackTimeline]
241
+ );
242
+ const updateMetrics = react.useCallback(
243
+ (_completedStage, allStages) => {
244
+ const completedStages = allStages.filter((s) => s.status === "completed");
245
+ const durations = completedStages.filter((s) => s.duration !== void 0).map((s) => ({ id: s.id, name: s.name, duration: s.duration }));
246
+ const slowest = durations.length ? durations.reduce((a, b) => a.duration > b.duration ? a : b) : null;
247
+ const fastest = durations.length ? durations.reduce((a, b) => a.duration < b.duration ? a : b) : null;
248
+ const avgDuration = durations.length ? durations.reduce((sum, d) => sum + d.duration, 0) / durations.length : 0;
249
+ return {
250
+ totalStages: allStages.length,
251
+ completedStages: completedStages.length,
252
+ failedStages: allStages.filter((s) => s.status === "failed").length,
253
+ skippedStages: allStages.filter((s) => s.status === "skipped").length,
254
+ totalRetries: allStages.reduce((sum, s) => sum + s.retryCount, 0),
255
+ slowestStage: slowest,
256
+ fastestStage: fastest,
257
+ averageStageDuration: avgDuration
258
+ };
259
+ },
260
+ []
261
+ );
262
+ const executeStage = react.useCallback(
263
+ async (stageConfig, stageIndex, input, previousResults, signal) => {
264
+ const maxRetries = stageConfig.maxRetries ?? 0;
265
+ const retryDelay = stageConfig.retryDelay ?? 1e3;
266
+ let lastError;
267
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
268
+ if (signal.aborted) {
269
+ return { success: false, error: new Error("Pipeline cancelled") };
270
+ }
271
+ if (pausedRef.current) {
272
+ await new Promise((resolve, reject) => {
273
+ resumePromiseRef.current = { resolve, reject };
274
+ });
275
+ }
276
+ if (stageConfig.shouldSkip?.(input, previousResults)) {
277
+ setState((prev) => {
278
+ const newStages = [...prev.stages];
279
+ newStages[stageIndex] = {
280
+ ...newStages[stageIndex],
281
+ status: "skipped"
282
+ };
283
+ return {
284
+ ...prev,
285
+ stages: newStages
286
+ };
287
+ });
288
+ addTimelineEvent(stageConfig.id, stageConfig.name, "skipped");
289
+ options.onStageComplete?.(state.stages[stageIndex]);
290
+ return { success: true, output: previousResults[previousResults.length - 1] };
291
+ }
292
+ const transformedInput = stageConfig.transformInput ? stageConfig.transformInput(input, previousResults) : input;
293
+ const startTime = performance.now();
294
+ setState((prev) => {
295
+ const newStages = [...prev.stages];
296
+ newStages[stageIndex] = {
297
+ ...newStages[stageIndex],
298
+ status: "running",
299
+ input: transformedInput,
300
+ startedAt: Date.now(),
301
+ retryCount: attempt
302
+ };
303
+ return {
304
+ ...prev,
305
+ stages: newStages,
306
+ currentStageIndex: stageIndex,
307
+ currentStage: newStages[stageIndex]
308
+ };
309
+ });
310
+ if (attempt === 0) {
311
+ addTimelineEvent(stageConfig.id, stageConfig.name, "started");
312
+ options.onStageStart?.(state.stages[stageIndex]);
313
+ } else {
314
+ addTimelineEvent(stageConfig.id, stageConfig.name, "retry");
315
+ options.onStageRetry?.(state.stages[stageIndex], attempt);
316
+ }
317
+ try {
318
+ const result = await kit.run(stageConfig.functionName, transformedInput, {
319
+ ...stageConfig.options,
320
+ signal,
321
+ onProgress: (progress) => {
322
+ setState((prev) => {
323
+ const newStages = [...prev.stages];
324
+ newStages[stageIndex] = {
325
+ ...newStages[stageIndex],
326
+ progress: progress.percent
327
+ };
328
+ const stageProgress = progress.percent / 100;
329
+ const overallProgress = (stageIndex + stageProgress) / stages.length * 100;
330
+ return {
331
+ ...prev,
332
+ stages: newStages,
333
+ progress: overallProgress
334
+ };
335
+ });
336
+ }
337
+ });
338
+ const duration = performance.now() - startTime;
339
+ const transformedOutput = stageConfig.transformOutput ? stageConfig.transformOutput(result) : result;
340
+ setState((prev) => {
341
+ const newStages = [...prev.stages];
342
+ newStages[stageIndex] = {
343
+ ...newStages[stageIndex],
344
+ status: "completed",
345
+ output: transformedOutput,
346
+ completedAt: Date.now(),
347
+ duration,
348
+ progress: 100
349
+ };
350
+ const newMetrics = {
351
+ ...prev.metrics,
352
+ ...updateMetrics(newStages[stageIndex], newStages)
353
+ };
354
+ return {
355
+ ...prev,
356
+ stages: newStages,
357
+ metrics: newMetrics,
358
+ progress: (stageIndex + 1) / stages.length * 100
359
+ };
360
+ });
361
+ addTimelineEvent(stageConfig.id, stageConfig.name, "completed", duration);
362
+ options.onStageComplete?.(state.stages[stageIndex]);
363
+ return { success: true, output: transformedOutput };
364
+ } catch (err) {
365
+ lastError = err instanceof Error ? err : new Error(String(err));
366
+ if (attempt < maxRetries) {
367
+ await new Promise((resolve) => setTimeout(resolve, retryDelay));
368
+ continue;
369
+ }
370
+ const duration = performance.now() - startTime;
371
+ setState((prev) => {
372
+ const newStages = [...prev.stages];
373
+ newStages[stageIndex] = {
374
+ ...newStages[stageIndex],
375
+ status: "failed",
376
+ error: lastError,
377
+ completedAt: Date.now(),
378
+ duration
379
+ };
380
+ return {
381
+ ...prev,
382
+ stages: newStages,
383
+ metrics: {
384
+ ...prev.metrics,
385
+ failedStages: prev.metrics.failedStages + 1
386
+ }
387
+ };
388
+ });
389
+ addTimelineEvent(
390
+ stageConfig.id,
391
+ stageConfig.name,
392
+ "failed",
393
+ duration,
394
+ lastError.message
395
+ );
396
+ options.onStageError?.(state.stages[stageIndex], lastError);
397
+ return { success: false, error: lastError };
398
+ }
399
+ }
400
+ return { success: false, error: lastError };
401
+ },
402
+ [kit, stages, state.stages, addTimelineEvent, updateMetrics, options]
403
+ );
404
+ const run = react.useCallback(
405
+ async (input) => {
406
+ if (abortControllerRef.current) {
407
+ abortControllerRef.current.abort();
408
+ }
409
+ const abortController = new AbortController();
410
+ abortControllerRef.current = abortController;
411
+ pausedRef.current = false;
412
+ const startTime = Date.now();
413
+ setState(() => ({
414
+ ...createInitialPipelineState(stages),
415
+ status: "running",
416
+ input,
417
+ startedAt: startTime
418
+ }));
419
+ const stageResults = [];
420
+ let currentInput = input;
421
+ let finalError = null;
422
+ for (let i = 0; i < stages.length; i++) {
423
+ if (abortController.signal.aborted) {
424
+ setState((prev) => ({
425
+ ...prev,
426
+ status: "cancelled",
427
+ completedAt: Date.now(),
428
+ totalDuration: Date.now() - startTime
429
+ }));
430
+ return;
431
+ }
432
+ const result = await executeStage(
433
+ stages[i],
434
+ i,
435
+ currentInput,
436
+ stageResults,
437
+ abortController.signal
438
+ );
439
+ if (!result.success) {
440
+ finalError = result.error ?? new Error("Stage failed");
441
+ if (options.stopOnError !== false) {
442
+ setState((prev) => ({
443
+ ...prev,
444
+ status: "failed",
445
+ error: finalError,
446
+ stageResults,
447
+ completedAt: Date.now(),
448
+ totalDuration: Date.now() - startTime
449
+ }));
450
+ return;
451
+ }
452
+ }
453
+ if (result.output !== void 0) {
454
+ stageResults.push(result.output);
455
+ currentInput = result.output;
456
+ }
457
+ }
458
+ setState((prev) => ({
459
+ ...prev,
460
+ status: finalError ? "failed" : "completed",
461
+ output: currentInput ?? null,
462
+ error: finalError,
463
+ stageResults,
464
+ completedAt: Date.now(),
465
+ totalDuration: Date.now() - startTime,
466
+ currentStageIndex: -1,
467
+ currentStage: null,
468
+ progress: 100
469
+ }));
470
+ options.onStateChange?.(state);
471
+ },
472
+ [stages, executeStage, options, state]
473
+ );
474
+ const cancel = react.useCallback(() => {
475
+ if (abortControllerRef.current) {
476
+ abortControllerRef.current.abort();
477
+ abortControllerRef.current = null;
478
+ }
479
+ if (resumePromiseRef.current) {
480
+ resumePromiseRef.current.reject(new Error("Pipeline cancelled"));
481
+ resumePromiseRef.current = null;
482
+ }
483
+ setState((prev) => ({
484
+ ...prev,
485
+ status: "cancelled",
486
+ completedAt: Date.now(),
487
+ totalDuration: prev.startedAt ? Date.now() - prev.startedAt : null
488
+ }));
489
+ }, []);
490
+ const reset = react.useCallback(() => {
491
+ cancel();
492
+ setState(createInitialPipelineState(stages));
493
+ }, [cancel, stages]);
494
+ const pause = react.useCallback(() => {
495
+ pausedRef.current = true;
496
+ setState((prev) => ({
497
+ ...prev,
498
+ status: "paused"
499
+ }));
500
+ }, []);
501
+ const resume = react.useCallback(() => {
502
+ pausedRef.current = false;
503
+ if (resumePromiseRef.current) {
504
+ resumePromiseRef.current.resolve();
505
+ resumePromiseRef.current = null;
506
+ }
507
+ setState((prev) => ({
508
+ ...prev,
509
+ status: "running"
510
+ }));
511
+ }, []);
512
+ const retry = react.useCallback(async () => {
513
+ if (state.status !== "failed" || !state.input) return;
514
+ const failedIndex = state.stages.findIndex((s) => s.status === "failed");
515
+ if (failedIndex === -1) return;
516
+ const retryInput = failedIndex === 0 ? state.input : state.stageResults[failedIndex - 1];
517
+ const abortController = new AbortController();
518
+ abortControllerRef.current = abortController;
519
+ setState((prev) => ({
520
+ ...prev,
521
+ status: "running",
522
+ error: null
523
+ }));
524
+ const stageResults = [...state.stageResults.slice(0, failedIndex)];
525
+ let currentInput = retryInput;
526
+ for (let i = failedIndex; i < stages.length; i++) {
527
+ if (abortController.signal.aborted) {
528
+ setState((prev) => ({ ...prev, status: "cancelled" }));
529
+ return;
530
+ }
531
+ const result = await executeStage(
532
+ stages[i],
533
+ i,
534
+ currentInput,
535
+ stageResults,
536
+ abortController.signal
537
+ );
538
+ if (!result.success) {
539
+ setState((prev) => ({
540
+ ...prev,
541
+ status: "failed",
542
+ error: result.error ?? new Error("Stage failed"),
543
+ stageResults
544
+ }));
545
+ return;
546
+ }
547
+ if (result.output !== void 0) {
548
+ stageResults.push(result.output);
549
+ currentInput = result.output;
550
+ }
551
+ }
552
+ setState((prev) => ({
553
+ ...prev,
554
+ status: "completed",
555
+ output: currentInput,
556
+ stageResults,
557
+ completedAt: Date.now(),
558
+ totalDuration: prev.startedAt ? Date.now() - prev.startedAt : null,
559
+ progress: 100
560
+ }));
561
+ }, [state, stages, executeStage]);
562
+ const getReport = react.useCallback(() => {
563
+ const stageDetails = state.stages.map((stage) => ({
564
+ name: stage.name,
565
+ status: stage.status,
566
+ duration: stage.duration ? formatDuration(stage.duration) : "-",
567
+ error: stage.error?.message
568
+ }));
569
+ const timeline = state.metrics.timeline.map((event) => {
570
+ const time = new Date(event.timestamp).toISOString().split("T")[1].split(".")[0];
571
+ const duration = event.duration ? ` (${formatDuration(event.duration)})` : "";
572
+ const error = event.error ? ` - ${event.error}` : "";
573
+ return `[${time}] ${event.stageName}: ${event.event}${duration}${error}`;
574
+ });
575
+ const insights = [];
576
+ if (state.metrics.slowestStage) {
577
+ insights.push(
578
+ `Slowest stage: ${state.metrics.slowestStage.name} (${formatDuration(
579
+ state.metrics.slowestStage.duration
580
+ )})`
581
+ );
582
+ }
583
+ if (state.metrics.fastestStage) {
584
+ insights.push(
585
+ `Fastest stage: ${state.metrics.fastestStage.name} (${formatDuration(
586
+ state.metrics.fastestStage.duration
587
+ )})`
588
+ );
589
+ }
590
+ if (state.metrics.totalRetries > 0) {
591
+ insights.push(`Total retries: ${state.metrics.totalRetries}`);
592
+ }
593
+ if (state.metrics.averageStageDuration > 0) {
594
+ insights.push(
595
+ `Average stage duration: ${formatDuration(state.metrics.averageStageDuration)}`
596
+ );
597
+ }
598
+ const successRate = state.metrics.totalStages > 0 ? state.metrics.completedStages / state.metrics.totalStages * 100 : 0;
599
+ const summary = [
600
+ `Pipeline Status: ${state.status.toUpperCase()}`,
601
+ `Stages: ${state.metrics.completedStages}/${state.metrics.totalStages} completed`,
602
+ `Success Rate: ${successRate.toFixed(0)}%`,
603
+ state.totalDuration ? `Total Duration: ${formatDuration(state.totalDuration)}` : "",
604
+ state.error ? `Error: ${state.error.message}` : ""
605
+ ].filter(Boolean).join("\n");
606
+ return {
607
+ summary,
608
+ stageDetails,
609
+ timeline,
610
+ insights,
611
+ metrics: state.metrics
612
+ };
613
+ }, [state]);
614
+ const isStageComplete = react.useCallback(
615
+ (stageId) => {
616
+ const stage = state.stages.find((s) => s.id === stageId);
617
+ return stage?.status === "completed";
618
+ },
619
+ [state.stages]
620
+ );
621
+ const getStage = react.useCallback(
622
+ (stageId) => {
623
+ return state.stages.find((s) => s.id === stageId);
624
+ },
625
+ [state.stages]
626
+ );
627
+ react.useEffect(() => {
628
+ if (options.autoRun && options.initialInput !== void 0) {
629
+ run(options.initialInput);
630
+ }
631
+ }, []);
632
+ react.useEffect(() => {
633
+ return () => {
634
+ cancel();
635
+ };
636
+ }, [cancel]);
637
+ return {
638
+ ...state,
639
+ run,
640
+ cancel,
641
+ reset,
642
+ pause,
643
+ resume,
644
+ retry,
645
+ getReport,
646
+ isRunning: state.status === "running",
647
+ isComplete: state.status === "completed",
648
+ isFailed: state.status === "failed",
649
+ isStageComplete,
650
+ getStage
651
+ };
652
+ }
653
+ function useParallelBatch(functionName, options = {}) {
654
+ const kit = useComputeKit();
655
+ const abortControllerRef = react.useRef(null);
656
+ const [state, setState] = react.useState({
657
+ result: null,
658
+ loading: false,
659
+ progress: 0,
660
+ completedCount: 0,
661
+ totalCount: 0
662
+ });
663
+ const run = react.useCallback(
664
+ async (items) => {
665
+ if (abortControllerRef.current) {
666
+ abortControllerRef.current.abort();
667
+ }
668
+ const abortController = new AbortController();
669
+ abortControllerRef.current = abortController;
670
+ setState({
671
+ result: null,
672
+ loading: true,
673
+ progress: 0,
674
+ completedCount: 0,
675
+ totalCount: items.length
676
+ });
677
+ const startTime = performance.now();
678
+ const results = [];
679
+ const concurrency = options.concurrency ?? items.length;
680
+ for (let i = 0; i < items.length; i += concurrency) {
681
+ if (abortController.signal.aborted) {
682
+ break;
683
+ }
684
+ const batch = items.slice(i, i + concurrency);
685
+ const batchPromises = batch.map(async (item, batchIndex) => {
686
+ const index = i + batchIndex;
687
+ const itemStart = performance.now();
688
+ try {
689
+ const data = await kit.run(functionName, item, {
690
+ ...options.computeOptions,
691
+ signal: abortController.signal
692
+ });
693
+ const itemResult = {
694
+ index,
695
+ success: true,
696
+ data,
697
+ duration: performance.now() - itemStart
698
+ };
699
+ return itemResult;
700
+ } catch (err) {
701
+ const itemResult = {
702
+ index,
703
+ success: false,
704
+ error: err instanceof Error ? err : new Error(String(err)),
705
+ duration: performance.now() - itemStart
706
+ };
707
+ return itemResult;
708
+ }
709
+ });
710
+ const batchResults = await Promise.all(batchPromises);
711
+ results.push(...batchResults);
712
+ const completed = results.length;
713
+ setState((prev) => ({
714
+ ...prev,
715
+ completedCount: completed,
716
+ progress: completed / items.length * 100
717
+ }));
718
+ }
719
+ const totalDuration = performance.now() - startTime;
720
+ const successful = results.filter((r) => r.success && r.data !== void 0).map((r) => r.data);
721
+ const failed = results.filter((r) => !r.success).map((r) => ({ index: r.index, error: r.error }));
722
+ const finalResult = {
723
+ results,
724
+ successful,
725
+ failed,
726
+ totalDuration,
727
+ successRate: successful.length / items.length
728
+ };
729
+ setState({
730
+ result: finalResult,
731
+ loading: false,
732
+ progress: 100,
733
+ completedCount: items.length,
734
+ totalCount: items.length
735
+ });
736
+ return finalResult;
737
+ },
738
+ [kit, functionName, options.concurrency, options.computeOptions]
739
+ );
740
+ const cancel = react.useCallback(() => {
741
+ if (abortControllerRef.current) {
742
+ abortControllerRef.current.abort();
743
+ abortControllerRef.current = null;
744
+ }
745
+ setState((prev) => ({
746
+ ...prev,
747
+ loading: false
748
+ }));
749
+ }, []);
750
+ const reset = react.useCallback(() => {
751
+ cancel();
752
+ setState({
753
+ result: null,
754
+ loading: false,
755
+ progress: 0,
756
+ completedCount: 0,
757
+ totalCount: 0
758
+ });
759
+ }, [cancel]);
760
+ react.useEffect(() => {
761
+ return () => {
762
+ cancel();
763
+ };
764
+ }, [cancel]);
765
+ return {
766
+ ...state,
767
+ run,
768
+ cancel,
769
+ reset
770
+ };
771
+ }
171
772
 
172
773
  Object.defineProperty(exports, "ComputeKit", {
173
774
  enumerable: true,
@@ -178,6 +779,8 @@ exports.useCompute = useCompute;
178
779
  exports.useComputeCallback = useComputeCallback;
179
780
  exports.useComputeFunction = useComputeFunction;
180
781
  exports.useComputeKit = useComputeKit;
782
+ exports.useParallelBatch = useParallelBatch;
783
+ exports.usePipeline = usePipeline;
181
784
  exports.usePoolStats = usePoolStats;
182
785
  exports.useWasmSupport = useWasmSupport;
183
786
  //# sourceMappingURL=index.cjs.map