@cosmicdrift/kumiko-renderer 0.165.0 → 0.165.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmicdrift/kumiko-renderer",
3
- "version": "0.165.0",
3
+ "version": "0.165.2",
4
4
  "description": "Platform-agnostic React renderer for Kumiko screens. Contains the shared logic — primitives-contract, hooks, KumikoScreen, navigation & SSE abstractions — that any platform-specific renderer (web, native) composes. No DOM, no EventSource, no react-dom.",
5
5
  "license": "BUSL-1.1",
6
6
  "author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
@@ -15,8 +15,8 @@
15
15
  }
16
16
  },
17
17
  "dependencies": {
18
- "@cosmicdrift/kumiko-framework": "0.165.0",
19
- "@cosmicdrift/kumiko-headless": "0.165.0",
18
+ "@cosmicdrift/kumiko-framework": "0.165.2",
19
+ "@cosmicdrift/kumiko-headless": "0.165.2",
20
20
  "react": "^19.2.6",
21
21
  "temporal-polyfill": "^0.3.2"
22
22
  },
@@ -3,11 +3,13 @@ import { sortByAccessor } from "../sort-by-accessor";
3
3
 
4
4
  type Row = { readonly name: string; readonly count: number };
5
5
 
6
- const rows: readonly Row[] = [
7
- { name: "b", count: 2 },
8
- { name: "a", count: 3 },
9
- { name: "c", count: 1 },
10
- ];
6
+ function makeRows(): readonly Row[] {
7
+ return [
8
+ { name: "b", count: 2 },
9
+ { name: "a", count: 3 },
10
+ { name: "c", count: 1 },
11
+ ];
12
+ }
11
13
 
12
14
  const accessors = {
13
15
  name: (r: Row) => r.name,
@@ -16,24 +18,29 @@ const accessors = {
16
18
 
17
19
  describe("sortByAccessor", () => {
18
20
  test("sort === null returns rows unchanged (same reference)", () => {
21
+ const rows = makeRows();
19
22
  expect(sortByAccessor(rows, null, accessors)).toBe(rows);
20
23
  });
21
24
 
22
25
  test("an unknown field returns rows unchanged (same reference)", () => {
26
+ const rows = makeRows();
23
27
  expect(sortByAccessor(rows, { field: "nope", dir: "asc" }, accessors)).toBe(rows);
24
28
  });
25
29
 
26
30
  test("sorts ascending by the given accessor", () => {
31
+ const rows = makeRows();
27
32
  const sorted = sortByAccessor(rows, { field: "name", dir: "asc" }, accessors);
28
33
  expect(sorted.map((r) => r.name)).toEqual(["a", "b", "c"]);
29
34
  });
30
35
 
31
36
  test("sorts descending by the given accessor", () => {
37
+ const rows = makeRows();
32
38
  const sorted = sortByAccessor(rows, { field: "count", dir: "desc" }, accessors);
33
39
  expect(sorted.map((r) => r.count)).toEqual([3, 2, 1]);
34
40
  });
35
41
 
36
42
  test("does not mutate the input array", () => {
43
+ const rows = makeRows();
37
44
  const original = [...rows];
38
45
  sortByAccessor(rows, { field: "name", dir: "asc" }, accessors);
39
46
  expect(rows).toEqual(original);
@@ -38,7 +38,6 @@ export function useStreamHandler<TChunk = unknown>(
38
38
  const [error, setError] = useState<DispatcherError | null>(null);
39
39
 
40
40
  const activeCtrl = useRef<AbortController | null>(null);
41
- const payloadKey = JSON.stringify(payload);
42
41
  const payloadRef = useRef(payload);
43
42
  payloadRef.current = payload;
44
43
 
@@ -56,7 +55,12 @@ export function useStreamHandler<TChunk = unknown>(
56
55
  setError(null);
57
56
  }, [abort]);
58
57
 
59
- // biome-ignore lint/correctness/useExhaustiveDependencies: payload via payloadKey / payloadRef
58
+ // Shared by every abort-detection branch below: only touch status if this
59
+ // run is still the active one (or activeCtrl was already cleared by abort()).
60
+ const settleAborted = useCallback((ctrl: AbortController): void => {
61
+ if (activeCtrl.current === null || activeCtrl.current === ctrl) setStatus("idle");
62
+ }, []);
63
+
60
64
  const start = useCallback(
61
65
  async (overridePayload?: unknown): Promise<void> => {
62
66
  activeCtrl.current?.abort();
@@ -69,39 +73,37 @@ export function useStreamHandler<TChunk = unknown>(
69
73
 
70
74
  const body = overridePayload !== undefined ? overridePayload : payloadRef.current;
71
75
  try {
72
- const accumulated: TChunk[] = [];
73
76
  for await (const chunk of dispatcher.stream<TChunk>(type, body, { signal: ctrl.signal })) {
74
77
  // skip: a newer start() already superseded this run
75
78
  if (ctrl.signal.aborted) {
76
- if (activeCtrl.current === null || activeCtrl.current === ctrl) setStatus("idle");
79
+ settleAborted(ctrl);
77
80
  return;
78
81
  }
79
- accumulated.push(chunk);
80
- setChunks([...accumulated]);
82
+ setChunks((prev) => [...prev, chunk]);
81
83
  }
82
84
  // skip: aborted after last chunk, don't mark done
83
85
  if (ctrl.signal.aborted) {
84
- if (activeCtrl.current === null || activeCtrl.current === ctrl) setStatus("idle");
86
+ settleAborted(ctrl);
85
87
  return;
86
88
  }
87
89
  setStatus("done");
88
90
  } catch (e) {
89
91
  // skip: abort is not an error toast
90
92
  if (ctrl.signal.aborted) {
91
- if (activeCtrl.current === null || activeCtrl.current === ctrl) setStatus("idle");
93
+ settleAborted(ctrl);
92
94
  return;
93
95
  }
94
96
  const mapped = asDispatcherError(e);
95
97
  // skip: dispatcher-mapped abort (fetch cancelled)
96
98
  if (mapped.code === "aborted") {
97
- if (activeCtrl.current === null || activeCtrl.current === ctrl) setStatus("idle");
99
+ settleAborted(ctrl);
98
100
  return;
99
101
  }
100
102
  setError(mapped);
101
103
  setStatus("error");
102
104
  }
103
105
  },
104
- [dispatcher, type, payloadKey],
106
+ [dispatcher, type, settleAborted],
105
107
  );
106
108
 
107
109
  useEffect(() => {