@cosmicdrift/kumiko-renderer 0.165.0 → 2.0.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.
|
|
3
|
+
"version": "2.0.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.
|
|
19
|
-
"@cosmicdrift/kumiko-headless": "0.
|
|
18
|
+
"@cosmicdrift/kumiko-framework": "2.0.0",
|
|
19
|
+
"@cosmicdrift/kumiko-headless": "2.0.0",
|
|
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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
79
|
+
settleAborted(ctrl);
|
|
77
80
|
return;
|
|
78
81
|
}
|
|
79
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
99
|
+
settleAborted(ctrl);
|
|
98
100
|
return;
|
|
99
101
|
}
|
|
100
102
|
setError(mapped);
|
|
101
103
|
setStatus("error");
|
|
102
104
|
}
|
|
103
105
|
},
|
|
104
|
-
[dispatcher, type,
|
|
106
|
+
[dispatcher, type, settleAborted],
|
|
105
107
|
);
|
|
106
108
|
|
|
107
109
|
useEffect(() => {
|