@critical-path/react 0.13.11 → 0.14.1

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/hooks.js CHANGED
@@ -1,3 +1,4 @@
1
+ 'use client';
1
2
  import { useState, useEffect, useCallback, useMemo, useRef } from 'react';
2
3
  import { resolveStatusDefinition, isTempTaskId } from '@critical-path/core';
3
4
  import { registerWebMcpTools } from '@critical-path/mcp/web';
@@ -7,22 +8,32 @@ export function useWorkflows() {
7
8
  const [workflows, setWorkflows] = useState([]);
8
9
  const [loading, setLoading] = useState(true);
9
10
  const [error, setError] = useState(null);
10
- const fetchWorkflows = useCallback(async () => {
11
+ const fetchWorkflows = useCallback(async (signal) => {
12
+ const api = signal ? client.with({ signal }) : client;
11
13
  try {
12
14
  setLoading(true);
13
- const data = await client.getWorkflows();
15
+ const data = await api.getWorkflows();
16
+ if (signal?.aborted)
17
+ return;
14
18
  setWorkflows(data);
15
19
  setError(null);
16
20
  }
17
21
  catch (err) {
22
+ if (signal?.aborted)
23
+ return;
18
24
  setError(err instanceof Error ? err : new Error(String(err)));
19
25
  }
20
26
  finally {
21
- setLoading(false);
27
+ if (!signal?.aborted)
28
+ setLoading(false);
22
29
  }
23
30
  }, [client]);
24
31
  useEffect(() => {
25
- fetchWorkflows();
32
+ // Abort the in-flight request when inputs change or the component unmounts, so a slow
33
+ // response for old inputs can never overwrite newer data.
34
+ const controller = new AbortController();
35
+ fetchWorkflows(controller.signal);
36
+ return () => controller.abort();
26
37
  }, [fetchWorkflows]);
27
38
  const createWorkflow = async (input) => {
28
39
  try {
@@ -59,57 +70,77 @@ export function useWorkflows() {
59
70
  throw errorObj;
60
71
  }
61
72
  };
62
- return { workflows, loading, error, refresh: fetchWorkflows, createWorkflow, updateWorkflow, deleteWorkflow };
73
+ return { workflows, loading, error, refresh: () => fetchWorkflows(), createWorkflow, updateWorkflow, deleteWorkflow };
63
74
  }
64
75
  export function useTaskTransitions(taskId) {
65
76
  const client = useCriticalPathClient();
66
77
  const [allowedTransitions, setAllowedTransitions] = useState([]);
67
78
  const [loading, setLoading] = useState(false);
68
79
  const [error, setError] = useState(null);
69
- const fetchTransitions = useCallback(async () => {
80
+ const fetchTransitions = useCallback(async (signal) => {
81
+ const api = signal ? client.with({ signal }) : client;
70
82
  if (!taskId) {
71
83
  setAllowedTransitions([]);
72
84
  return;
73
85
  }
74
86
  try {
75
87
  setLoading(true);
76
- const data = await client.getAllowedTaskTransitions(taskId);
88
+ const data = await api.getAllowedTaskTransitions(taskId);
89
+ if (signal?.aborted)
90
+ return;
77
91
  setAllowedTransitions(data);
78
92
  setError(null);
79
93
  }
80
94
  catch (err) {
95
+ if (signal?.aborted)
96
+ return;
81
97
  setError(err instanceof Error ? err : new Error(String(err)));
82
98
  }
83
99
  finally {
84
- setLoading(false);
100
+ if (!signal?.aborted)
101
+ setLoading(false);
85
102
  }
86
103
  }, [client, taskId]);
87
104
  useEffect(() => {
88
- fetchTransitions();
105
+ // Abort the in-flight request when inputs change or the component unmounts, so a slow
106
+ // response for old inputs can never overwrite newer data.
107
+ const controller = new AbortController();
108
+ fetchTransitions(controller.signal);
109
+ return () => controller.abort();
89
110
  }, [fetchTransitions]);
90
- return { allowedTransitions, loading, error, refresh: fetchTransitions };
111
+ return { allowedTransitions, loading, error, refresh: () => fetchTransitions() };
91
112
  }
92
113
  export function useProjects() {
93
114
  const client = useCriticalPathClient();
94
115
  const [projects, setProjects] = useState([]);
95
116
  const [loading, setLoading] = useState(true);
96
117
  const [error, setError] = useState(null);
97
- const fetchProjects = useCallback(async () => {
118
+ const fetchProjects = useCallback(async (signal) => {
119
+ const api = signal ? client.with({ signal }) : client;
98
120
  try {
99
121
  setLoading(true);
100
- const data = await client.getProjects();
122
+ const data = await api.getProjects();
123
+ if (signal?.aborted)
124
+ return;
101
125
  setProjects(data);
102
126
  setError(null);
103
127
  }
104
128
  catch (err) {
129
+ if (signal?.aborted)
130
+ return;
105
131
  setError(err instanceof Error ? err : new Error(String(err)));
106
132
  }
107
133
  finally {
108
- setLoading(false);
134
+ if (!signal?.aborted)
135
+ setLoading(false);
109
136
  }
110
137
  }, [client]);
111
138
  useEffect(() => {
112
- fetchProjects();
139
+ // Abort the in-flight request when inputs change or the component unmounts, so a slow
140
+ // response for old inputs can never overwrite newer data.
141
+ const controller = new AbortController();
142
+ fetchProjects(controller.signal);
143
+ return () => controller.abort();
113
144
  }, [fetchProjects]);
114
145
  const createProject = async (input) => {
115
146
  const tempId = `temp_${Date.now()}_${Math.random().toString(36).substring(2, 7)}`;
@@ -133,31 +164,41 @@ export function useProjects() {
133
164
  throw errorObj;
134
165
  }
135
166
  };
136
- return { projects, loading, error, refresh: fetchProjects, createProject };
167
+ return { projects, loading, error, refresh: () => fetchProjects(), createProject };
137
168
  }
138
169
  export function useTasks(projectId) {
139
170
  const client = useCriticalPathClient();
140
171
  const [tasks, setTasks] = useState([]);
141
172
  const [loading, setLoading] = useState(true);
142
173
  const [error, setError] = useState(null);
143
- const fetchTasks = useCallback(async () => {
174
+ const fetchTasks = useCallback(async (signal) => {
175
+ const api = signal ? client.with({ signal }) : client;
144
176
  try {
145
177
  setLoading(true);
146
- const data = await client.getTasks(projectId);
178
+ const data = await api.getTasks(projectId);
179
+ if (signal?.aborted)
180
+ return;
147
181
  setTasks(data);
148
182
  setError(null);
149
183
  }
150
184
  catch (err) {
185
+ if (signal?.aborted)
186
+ return;
151
187
  setError(err instanceof Error ? err : new Error(String(err)));
152
188
  }
153
189
  finally {
154
- setLoading(false);
190
+ if (!signal?.aborted)
191
+ setLoading(false);
155
192
  }
156
193
  }, [client, projectId]);
157
194
  const pendingCreationsRef = useRef(new Map());
158
195
  const tempToRealIdMapRef = useRef(new Map());
159
196
  useEffect(() => {
160
- fetchTasks();
197
+ // Abort the in-flight request when inputs change or the component unmounts, so a slow
198
+ // response for old inputs can never overwrite newer data.
199
+ const controller = new AbortController();
200
+ fetchTasks(controller.signal);
201
+ return () => controller.abort();
161
202
  }, [fetchTasks]);
162
203
  const createTask = async (input) => {
163
204
  const tempId = `temp_${Date.now()}_${Math.random().toString(36).substring(2, 7)}`;
@@ -298,7 +339,7 @@ export function useTasks(projectId) {
298
339
  throw errorObj;
299
340
  }
300
341
  };
301
- return { tasks, loading, error, refresh: fetchTasks, createTask, updateTask, updateTaskStatus, deleteTask };
342
+ return { tasks, loading, error, refresh: () => fetchTasks(), createTask, updateTask, updateTaskStatus, deleteTask };
302
343
  }
303
344
  export function useKanban(projectId, options) {
304
345
  const { tasks, loading, error, refresh, updateTaskStatus, createTask } = useTasks(projectId);
@@ -353,7 +394,8 @@ export function useComments(taskId) {
353
394
  const [comments, setComments] = useState([]);
354
395
  const [loading, setLoading] = useState(true);
355
396
  const [error, setError] = useState(null);
356
- const fetchComments = useCallback(async () => {
397
+ const fetchComments = useCallback(async (signal) => {
398
+ const api = signal ? client.with({ signal }) : client;
357
399
  if (!taskId) {
358
400
  setComments([]);
359
401
  setLoading(false);
@@ -361,19 +403,28 @@ export function useComments(taskId) {
361
403
  }
362
404
  try {
363
405
  setLoading(true);
364
- const data = await client.getComments(taskId);
406
+ const data = await api.getComments(taskId);
407
+ if (signal?.aborted)
408
+ return;
365
409
  setComments(data);
366
410
  setError(null);
367
411
  }
368
412
  catch (err) {
413
+ if (signal?.aborted)
414
+ return;
369
415
  setError(err instanceof Error ? err : new Error(String(err)));
370
416
  }
371
417
  finally {
372
- setLoading(false);
418
+ if (!signal?.aborted)
419
+ setLoading(false);
373
420
  }
374
421
  }, [client, taskId]);
375
422
  useEffect(() => {
376
- fetchComments();
423
+ // Abort the in-flight request when inputs change or the component unmounts, so a slow
424
+ // response for old inputs can never overwrite newer data.
425
+ const controller = new AbortController();
426
+ fetchComments(controller.signal);
427
+ return () => controller.abort();
377
428
  }, [fetchComments]);
378
429
  const addComment = async (input) => {
379
430
  try {
@@ -461,7 +512,7 @@ export function useComments(taskId) {
461
512
  threads,
462
513
  loading,
463
514
  error,
464
- refresh: fetchComments,
515
+ refresh: () => fetchComments(),
465
516
  addComment,
466
517
  updateComment,
467
518
  deleteComment,
@@ -475,22 +526,32 @@ export function useAttachments(filter) {
475
526
  const [loading, setLoading] = useState(true);
476
527
  const [error, setError] = useState(null);
477
528
  const filterKey = JSON.stringify(filter || {});
478
- const fetchAttachments = useCallback(async () => {
529
+ const fetchAttachments = useCallback(async (signal) => {
530
+ const api = signal ? client.with({ signal }) : client;
479
531
  try {
480
532
  setLoading(true);
481
- const data = await client.getAttachments(filter);
533
+ const data = await api.getAttachments(filter);
534
+ if (signal?.aborted)
535
+ return;
482
536
  setAttachments(data);
483
537
  setError(null);
484
538
  }
485
539
  catch (err) {
540
+ if (signal?.aborted)
541
+ return;
486
542
  setError(err instanceof Error ? err : new Error(String(err)));
487
543
  }
488
544
  finally {
489
- setLoading(false);
545
+ if (!signal?.aborted)
546
+ setLoading(false);
490
547
  }
491
548
  }, [client, filterKey]);
492
549
  useEffect(() => {
493
- fetchAttachments();
550
+ // Abort the in-flight request when inputs change or the component unmounts, so a slow
551
+ // response for old inputs can never overwrite newer data.
552
+ const controller = new AbortController();
553
+ fetchAttachments(controller.signal);
554
+ return () => controller.abort();
494
555
  }, [fetchAttachments]);
495
556
  const createAttachment = async (input) => {
496
557
  try {
@@ -519,7 +580,7 @@ export function useAttachments(filter) {
519
580
  attachments,
520
581
  loading,
521
582
  error,
522
- refresh: fetchAttachments,
583
+ refresh: () => fetchAttachments(),
523
584
  createAttachment,
524
585
  deleteAttachment
525
586
  };
@@ -529,7 +590,8 @@ export function useDeliverables(projectId) {
529
590
  const [deliverables, setDeliverables] = useState([]);
530
591
  const [loading, setLoading] = useState(true);
531
592
  const [error, setError] = useState(null);
532
- const fetchDeliverables = useCallback(async () => {
593
+ const fetchDeliverables = useCallback(async (signal) => {
594
+ const api = signal ? client.with({ signal }) : client;
533
595
  if (!projectId) {
534
596
  setDeliverables([]);
535
597
  setLoading(false);
@@ -537,19 +599,28 @@ export function useDeliverables(projectId) {
537
599
  }
538
600
  try {
539
601
  setLoading(true);
540
- const data = await client.getDeliverables(projectId);
602
+ const data = await api.getDeliverables(projectId);
603
+ if (signal?.aborted)
604
+ return;
541
605
  setDeliverables(data);
542
606
  setError(null);
543
607
  }
544
608
  catch (err) {
609
+ if (signal?.aborted)
610
+ return;
545
611
  setError(err instanceof Error ? err : new Error(String(err)));
546
612
  }
547
613
  finally {
548
- setLoading(false);
614
+ if (!signal?.aborted)
615
+ setLoading(false);
549
616
  }
550
617
  }, [client, projectId]);
551
618
  useEffect(() => {
552
- fetchDeliverables();
619
+ // Abort the in-flight request when inputs change or the component unmounts, so a slow
620
+ // response for old inputs can never overwrite newer data.
621
+ const controller = new AbortController();
622
+ fetchDeliverables(controller.signal);
623
+ return () => controller.abort();
553
624
  }, [fetchDeliverables]);
554
625
  const createDeliverable = async (input) => {
555
626
  try {
@@ -590,7 +661,7 @@ export function useDeliverables(projectId) {
590
661
  deliverables,
591
662
  loading,
592
663
  error,
593
- refresh: fetchDeliverables,
664
+ refresh: () => fetchDeliverables(),
594
665
  createDeliverable,
595
666
  updateDeliverable,
596
667
  deleteDeliverable
@@ -601,7 +672,8 @@ export function useDeliverableSummary(deliverableId) {
601
672
  const [summary, setSummary] = useState(null);
602
673
  const [loading, setLoading] = useState(true);
603
674
  const [error, setError] = useState(null);
604
- const fetchSummary = useCallback(async () => {
675
+ const fetchSummary = useCallback(async (signal) => {
676
+ const api = signal ? client.with({ signal }) : client;
605
677
  if (!deliverableId) {
606
678
  setSummary(null);
607
679
  setLoading(false);
@@ -609,25 +681,34 @@ export function useDeliverableSummary(deliverableId) {
609
681
  }
610
682
  try {
611
683
  setLoading(true);
612
- const data = await client.getDeliverableSummary(deliverableId);
684
+ const data = await api.getDeliverableSummary(deliverableId);
685
+ if (signal?.aborted)
686
+ return;
613
687
  setSummary(data);
614
688
  setError(null);
615
689
  }
616
690
  catch (err) {
691
+ if (signal?.aborted)
692
+ return;
617
693
  setError(err instanceof Error ? err : new Error(String(err)));
618
694
  }
619
695
  finally {
620
- setLoading(false);
696
+ if (!signal?.aborted)
697
+ setLoading(false);
621
698
  }
622
699
  }, [client, deliverableId]);
623
700
  useEffect(() => {
624
- fetchSummary();
701
+ // Abort the in-flight request when inputs change or the component unmounts, so a slow
702
+ // response for old inputs can never overwrite newer data.
703
+ const controller = new AbortController();
704
+ fetchSummary(controller.signal);
705
+ return () => controller.abort();
625
706
  }, [fetchSummary]);
626
707
  return {
627
708
  summary,
628
709
  loading,
629
710
  error,
630
- refresh: fetchSummary
711
+ refresh: () => fetchSummary()
631
712
  };
632
713
  }
633
714
  export function useWebMCP(options = {}) {
@@ -675,7 +756,8 @@ export function useTaskActivity(taskId) {
675
756
  const [attachments, setAttachments] = useState([]);
676
757
  const [loading, setLoading] = useState(false);
677
758
  const [error, setError] = useState(null);
678
- const fetchActivity = useCallback(async () => {
759
+ const fetchActivity = useCallback(async (signal) => {
760
+ const api = signal ? client.with({ signal }) : client;
679
761
  if (!taskId) {
680
762
  setComments([]);
681
763
  setAttachments([]);
@@ -685,22 +767,31 @@ export function useTaskActivity(taskId) {
685
767
  try {
686
768
  setLoading(true);
687
769
  const [fetchedComments, fetchedAttachments] = await Promise.all([
688
- client.getComments(taskId),
689
- client.getAttachments({ taskId })
770
+ api.getComments(taskId),
771
+ api.getAttachments({ taskId })
690
772
  ]);
773
+ if (signal?.aborted)
774
+ return;
691
775
  setComments(fetchedComments);
692
776
  setAttachments(fetchedAttachments);
693
777
  setError(null);
694
778
  }
695
779
  catch (err) {
780
+ if (signal?.aborted)
781
+ return;
696
782
  setError(err instanceof Error ? err : new Error(String(err)));
697
783
  }
698
784
  finally {
699
- setLoading(false);
785
+ if (!signal?.aborted)
786
+ setLoading(false);
700
787
  }
701
788
  }, [client, taskId]);
702
789
  useEffect(() => {
703
- fetchActivity();
790
+ // Abort the in-flight request when inputs change or the component unmounts, so a slow
791
+ // response for old inputs can never overwrite newer data.
792
+ const controller = new AbortController();
793
+ fetchActivity(controller.signal);
794
+ return () => controller.abort();
704
795
  }, [fetchActivity]);
705
796
  const threads = useMemo(() => {
706
797
  const map = new Map();
@@ -841,7 +932,7 @@ export function useTaskActivity(taskId) {
841
932
  standaloneAttachments,
842
933
  loading,
843
934
  error,
844
- refresh: fetchActivity,
935
+ refresh: () => fetchActivity(),
845
936
  addComment,
846
937
  addAttachment,
847
938
  deleteComment,
@@ -855,7 +946,8 @@ export function useCriticalPath(projectId) {
855
946
  const [analysis, setAnalysis] = useState(null);
856
947
  const [loading, setLoading] = useState(true);
857
948
  const [error, setError] = useState(null);
858
- const fetchCriticalPath = useCallback(async () => {
949
+ const fetchCriticalPath = useCallback(async (signal) => {
950
+ const api = signal ? client.with({ signal }) : client;
859
951
  if (!projectId) {
860
952
  setAnalysis(null);
861
953
  setLoading(false);
@@ -863,25 +955,34 @@ export function useCriticalPath(projectId) {
863
955
  }
864
956
  try {
865
957
  setLoading(true);
866
- const data = await client.calculateCriticalPath(projectId);
958
+ const data = await api.calculateCriticalPath(projectId);
959
+ if (signal?.aborted)
960
+ return;
867
961
  setAnalysis(data);
868
962
  setError(null);
869
963
  }
870
964
  catch (err) {
965
+ if (signal?.aborted)
966
+ return;
871
967
  setError(err instanceof Error ? err : new Error(String(err)));
872
968
  }
873
969
  finally {
874
- setLoading(false);
970
+ if (!signal?.aborted)
971
+ setLoading(false);
875
972
  }
876
973
  }, [client, projectId]);
877
974
  useEffect(() => {
878
- fetchCriticalPath();
975
+ // Abort the in-flight request when inputs change or the component unmounts, so a slow
976
+ // response for old inputs can never overwrite newer data.
977
+ const controller = new AbortController();
978
+ fetchCriticalPath(controller.signal);
979
+ return () => controller.abort();
879
980
  }, [fetchCriticalPath]);
880
981
  return {
881
982
  analysis,
882
983
  loading,
883
984
  error,
884
- refresh: fetchCriticalPath
985
+ refresh: () => fetchCriticalPath()
885
986
  };
886
987
  }
887
988
  export function useTimelineLadder(projectId, initialOptions = {}) {
@@ -890,7 +991,8 @@ export function useTimelineLadder(projectId, initialOptions = {}) {
890
991
  const [ladder, setLadder] = useState(null);
891
992
  const [loading, setLoading] = useState(true);
892
993
  const [error, setError] = useState(null);
893
- const fetchLadder = useCallback(async () => {
994
+ const fetchLadder = useCallback(async (signal) => {
995
+ const api = signal ? client.with({ signal }) : client;
894
996
  if (!projectId) {
895
997
  setLadder(null);
896
998
  setLoading(false);
@@ -898,22 +1000,31 @@ export function useTimelineLadder(projectId, initialOptions = {}) {
898
1000
  }
899
1001
  try {
900
1002
  setLoading(true);
901
- const data = await client.getTimelineLadder(projectId, {
1003
+ const data = await api.getTimelineLadder(projectId, {
902
1004
  ...initialOptions,
903
1005
  level
904
1006
  });
1007
+ if (signal?.aborted)
1008
+ return;
905
1009
  setLadder(data);
906
1010
  setError(null);
907
1011
  }
908
1012
  catch (err) {
1013
+ if (signal?.aborted)
1014
+ return;
909
1015
  setError(err instanceof Error ? err : new Error(String(err)));
910
1016
  }
911
1017
  finally {
912
- setLoading(false);
1018
+ if (!signal?.aborted)
1019
+ setLoading(false);
913
1020
  }
914
1021
  }, [client, projectId, level, initialOptions.containerId, initialOptions.iterationId]);
915
1022
  useEffect(() => {
916
- fetchLadder();
1023
+ // Abort the in-flight request when inputs change or the component unmounts, so a slow
1024
+ // response for old inputs can never overwrite newer data.
1025
+ const controller = new AbortController();
1026
+ fetchLadder(controller.signal);
1027
+ return () => controller.abort();
917
1028
  }, [fetchLadder]);
918
1029
  return {
919
1030
  ladder,
@@ -924,7 +1035,7 @@ export function useTimelineLadder(projectId, initialOptions = {}) {
924
1035
  concrete: ladder?.concrete ?? null,
925
1036
  loading,
926
1037
  error,
927
- refresh: fetchLadder
1038
+ refresh: () => fetchLadder()
928
1039
  };
929
1040
  }
930
1041
  export function useTaskLadder(taskId) {
@@ -932,7 +1043,8 @@ export function useTaskLadder(taskId) {
932
1043
  const [taskLadder, setTaskLadder] = useState(null);
933
1044
  const [loading, setLoading] = useState(true);
934
1045
  const [error, setError] = useState(null);
935
- const fetchTaskLadder = useCallback(async () => {
1046
+ const fetchTaskLadder = useCallback(async (signal) => {
1047
+ const api = signal ? client.with({ signal }) : client;
936
1048
  if (!taskId) {
937
1049
  setTaskLadder(null);
938
1050
  setLoading(false);
@@ -940,19 +1052,28 @@ export function useTaskLadder(taskId) {
940
1052
  }
941
1053
  try {
942
1054
  setLoading(true);
943
- const data = await client.getTaskLadder(taskId);
1055
+ const data = await api.getTaskLadder(taskId);
1056
+ if (signal?.aborted)
1057
+ return;
944
1058
  setTaskLadder(data);
945
1059
  setError(null);
946
1060
  }
947
1061
  catch (err) {
1062
+ if (signal?.aborted)
1063
+ return;
948
1064
  setError(err instanceof Error ? err : new Error(String(err)));
949
1065
  }
950
1066
  finally {
951
- setLoading(false);
1067
+ if (!signal?.aborted)
1068
+ setLoading(false);
952
1069
  }
953
1070
  }, [client, taskId]);
954
1071
  useEffect(() => {
955
- fetchTaskLadder();
1072
+ // Abort the in-flight request when inputs change or the component unmounts, so a slow
1073
+ // response for old inputs can never overwrite newer data.
1074
+ const controller = new AbortController();
1075
+ fetchTaskLadder(controller.signal);
1076
+ return () => controller.abort();
956
1077
  }, [fetchTaskLadder]);
957
1078
  return {
958
1079
  taskLadder,
@@ -962,7 +1083,7 @@ export function useTaskLadder(taskId) {
962
1083
  metrics: taskLadder?.metrics ?? null,
963
1084
  loading,
964
1085
  error,
965
- refresh: fetchTaskLadder
1086
+ refresh: () => fetchTaskLadder()
966
1087
  };
967
1088
  }
968
1089
  export function useTaskMetrics(taskId) {
@@ -971,7 +1092,8 @@ export function useTaskMetrics(taskId) {
971
1092
  const [history, setHistory] = useState(null);
972
1093
  const [loading, setLoading] = useState(true);
973
1094
  const [error, setError] = useState(null);
974
- const fetchMetrics = useCallback(async () => {
1095
+ const fetchMetrics = useCallback(async (signal) => {
1096
+ const api = signal ? client.with({ signal }) : client;
975
1097
  if (!taskId) {
976
1098
  setMetrics(null);
977
1099
  setHistory(null);
@@ -981,22 +1103,31 @@ export function useTaskMetrics(taskId) {
981
1103
  try {
982
1104
  setLoading(true);
983
1105
  const [m, h] = await Promise.all([
984
- client.getTaskMetrics(taskId),
985
- client.getTaskProgressHistory(taskId)
1106
+ api.getTaskMetrics(taskId),
1107
+ api.getTaskProgressHistory(taskId)
986
1108
  ]);
1109
+ if (signal?.aborted)
1110
+ return;
987
1111
  setMetrics(m);
988
1112
  setHistory(h);
989
1113
  setError(null);
990
1114
  }
991
1115
  catch (err) {
1116
+ if (signal?.aborted)
1117
+ return;
992
1118
  setError(err instanceof Error ? err : new Error(String(err)));
993
1119
  }
994
1120
  finally {
995
- setLoading(false);
1121
+ if (!signal?.aborted)
1122
+ setLoading(false);
996
1123
  }
997
1124
  }, [client, taskId]);
998
1125
  useEffect(() => {
999
- fetchMetrics();
1126
+ // Abort the in-flight request when inputs change or the component unmounts, so a slow
1127
+ // response for old inputs can never overwrite newer data.
1128
+ const controller = new AbortController();
1129
+ fetchMetrics(controller.signal);
1130
+ return () => controller.abort();
1000
1131
  }, [fetchMetrics]);
1001
1132
  return {
1002
1133
  metrics,
@@ -1008,7 +1139,7 @@ export function useTaskMetrics(taskId) {
1008
1139
  curveProfile: history?.curveProfile ?? null,
1009
1140
  loading,
1010
1141
  error,
1011
- refresh: fetchMetrics
1142
+ refresh: () => fetchMetrics()
1012
1143
  };
1013
1144
  }
1014
1145
  export function useWorkloadDistribution(projectId, initialOptions = {}) {
@@ -1019,23 +1150,29 @@ export function useWorkloadDistribution(projectId, initialOptions = {}) {
1019
1150
  const [workload, setWorkload] = useState(null);
1020
1151
  const [loading, setLoading] = useState(true);
1021
1152
  const [error, setError] = useState(null);
1022
- const fetchWorkload = useCallback(async () => {
1153
+ const fetchWorkload = useCallback(async (signal) => {
1154
+ const api = signal ? client.with({ signal }) : client;
1023
1155
  try {
1024
1156
  setLoading(true);
1025
- const data = await client.getWorkloadDistribution(projectId, {
1157
+ const data = await api.getWorkloadDistribution(projectId, {
1026
1158
  ...initialOptions,
1027
1159
  interval,
1028
1160
  groupBy,
1029
1161
  metric
1030
1162
  });
1163
+ if (signal?.aborted)
1164
+ return;
1031
1165
  setWorkload(data);
1032
1166
  setError(null);
1033
1167
  }
1034
1168
  catch (err) {
1169
+ if (signal?.aborted)
1170
+ return;
1035
1171
  setError(err instanceof Error ? err : new Error(String(err)));
1036
1172
  }
1037
1173
  finally {
1038
- setLoading(false);
1174
+ if (!signal?.aborted)
1175
+ setLoading(false);
1039
1176
  }
1040
1177
  }, [
1041
1178
  client,
@@ -1048,7 +1185,11 @@ export function useWorkloadDistribution(projectId, initialOptions = {}) {
1048
1185
  initialOptions.defaultWeeklyCapacityHours
1049
1186
  ]);
1050
1187
  useEffect(() => {
1051
- fetchWorkload();
1188
+ // Abort the in-flight request when inputs change or the component unmounts, so a slow
1189
+ // response for old inputs can never overwrite newer data.
1190
+ const controller = new AbortController();
1191
+ fetchWorkload(controller.signal);
1192
+ return () => controller.abort();
1052
1193
  }, [fetchWorkload]);
1053
1194
  return {
1054
1195
  workload,
@@ -1066,7 +1207,7 @@ export function useWorkloadDistribution(projectId, initialOptions = {}) {
1066
1207
  averageUtilization: workload?.averageUtilization,
1067
1208
  loading,
1068
1209
  error,
1069
- refresh: fetchWorkload
1210
+ refresh: () => fetchWorkload()
1070
1211
  };
1071
1212
  }
1072
1213
  //# sourceMappingURL=hooks.js.map