@cosmicdrift/kumiko-renderer 0.160.0 → 0.162.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmicdrift/kumiko-renderer",
3
- "version": "0.160.0",
3
+ "version": "0.162.0",
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.160.0",
19
- "@cosmicdrift/kumiko-headless": "0.160.0",
18
+ "@cosmicdrift/kumiko-framework": "0.162.0",
19
+ "@cosmicdrift/kumiko-headless": "0.162.0",
20
20
  "react": "^19.2.6",
21
21
  "temporal-polyfill": "^0.3.2"
22
22
  },
@@ -70,7 +70,7 @@ describe("useStreamHandler", () => {
70
70
  expect(result.current.error?.code).toBe("access_denied");
71
71
  });
72
72
 
73
- test("abort during stream leaves status without late done", async () => {
73
+ test("abort during stream resets status to idle (no late done)", async () => {
74
74
  let release!: () => void;
75
75
  const gate = new Promise<void>((r) => {
76
76
  release = r;
@@ -102,6 +102,6 @@ describe("useStreamHandler", () => {
102
102
  release();
103
103
  await started;
104
104
  expect(result.current.chunks).toEqual([{ i: 0 }]);
105
- expect(result.current.status).not.toBe("done");
105
+ expect(result.current.status).toBe("idle");
106
106
  });
107
107
  });
@@ -45,6 +45,8 @@ export function useStreamHandler<TChunk = unknown>(
45
45
  const abort = useCallback((): void => {
46
46
  activeCtrl.current?.abort();
47
47
  activeCtrl.current = null;
48
+ // User cancel — exit streaming so UI can re-enable the start button.
49
+ setStatus((s) => (s === "streaming" ? "idle" : s));
48
50
  }, []);
49
51
 
50
52
  const reset = useCallback((): void => {
@@ -70,19 +72,31 @@ export function useStreamHandler<TChunk = unknown>(
70
72
  const accumulated: TChunk[] = [];
71
73
  for await (const chunk of dispatcher.stream<TChunk>(type, body, { signal: ctrl.signal })) {
72
74
  // skip: a newer start() already superseded this run
73
- if (ctrl.signal.aborted) return;
75
+ if (ctrl.signal.aborted) {
76
+ if (activeCtrl.current === null || activeCtrl.current === ctrl) setStatus("idle");
77
+ return;
78
+ }
74
79
  accumulated.push(chunk);
75
80
  setChunks([...accumulated]);
76
81
  }
77
82
  // skip: aborted after last chunk, don't mark done
78
- if (ctrl.signal.aborted) return;
83
+ if (ctrl.signal.aborted) {
84
+ if (activeCtrl.current === null || activeCtrl.current === ctrl) setStatus("idle");
85
+ return;
86
+ }
79
87
  setStatus("done");
80
88
  } catch (e) {
81
89
  // skip: abort is not an error toast
82
- if (ctrl.signal.aborted) return;
90
+ if (ctrl.signal.aborted) {
91
+ if (activeCtrl.current === null || activeCtrl.current === ctrl) setStatus("idle");
92
+ return;
93
+ }
83
94
  const mapped = asDispatcherError(e);
84
95
  // skip: dispatcher-mapped abort (fetch cancelled)
85
- if (mapped.code === "aborted") return;
96
+ if (mapped.code === "aborted") {
97
+ if (activeCtrl.current === null || activeCtrl.current === ctrl) setStatus("idle");
98
+ return;
99
+ }
86
100
  setError(mapped);
87
101
  setStatus("error");
88
102
  }