Skip to content

Commit 0fc04a3

Browse files
committed
addressed pull request comments
1 parent f66740c commit 0fc04a3

13 files changed

Lines changed: 231 additions & 208 deletions

File tree

client/ts/src/schematic/actions.gen.ts

Lines changed: 57 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,6 @@ import { caseconv, control, record, spatial, zod } from "@synnaxlabs/x";
1212
import { produce } from "immer";
1313
import { z } from "zod";
1414

15-
import {
16-
handleAddNode,
17-
handleRemoveEdge,
18-
handleRemoveNode,
19-
handleSetAuthority,
20-
handleSetConfig,
21-
handleSetEdge,
22-
handleSetLegend,
23-
handleSetNodePosition,
24-
} from "@/schematic/actions";
2515
import { edgeZ, legendZ, nodeZ, type Schematic } from "@/schematic/types.gen";
2616

2717
/** SetNodePosition moves a node to a new position. */
@@ -33,15 +23,16 @@ export const setNodePositionPayloadZ = z.object({
3323
export type SetNodePositionPayload = z.infer<typeof setNodePositionPayloadZ>;
3424

3525
/**
36-
* AddNode appends a node to the schematic. If config is non-empty it is stored
37-
* under the node's key in the schematic configs map.
26+
* SetNode inserts the node if no node with the same key exists, otherwise
27+
* replaces the existing node in place. If config is non-empty it is
28+
* stored under the node's key in the schematic configs map.
3829
*/
39-
export const addNodePayloadZ = z.object({
30+
export const setNodePayloadZ = z.object({
4031
node: nodeZ,
4132
config: caseconv.preserveCase(zod.nullToUndefined(record.unknownZ())),
4233
});
4334

44-
export type AddNodePayload = z.infer<typeof addNodePayloadZ>;
35+
export type SetNodePayload = z.infer<typeof setNodePayloadZ>;
4536

4637
/** RemoveNode removes a node and any config stored under its key. */
4738
export const removeNodePayloadZ = z.object({
@@ -91,7 +82,7 @@ export type SetLegendPayload = z.infer<typeof setLegendPayloadZ>;
9182

9283
export const ACTION_TYPES = {
9384
set_node_position: "set_node_position",
94-
add_node: "add_node",
85+
set_node: "set_node",
9586
remove_node: "remove_node",
9687
set_edge: "set_edge",
9788
remove_edge: "remove_edge",
@@ -105,7 +96,7 @@ export const actionZ = z.discriminatedUnion("type", [
10596
type: z.literal("set_node_position"),
10697
setNodePosition: setNodePositionPayloadZ,
10798
}),
108-
z.object({ type: z.literal("add_node"), addNode: addNodePayloadZ }),
99+
z.object({ type: z.literal("set_node"), setNode: setNodePayloadZ }),
109100
z.object({ type: z.literal("remove_node"), removeNode: removeNodePayloadZ }),
110101
z.object({ type: z.literal("set_edge"), setEdge: setEdgePayloadZ }),
111102
z.object({ type: z.literal("remove_edge"), removeEdge: removeEdgePayloadZ }),
@@ -121,9 +112,9 @@ export const setNodePosition = (payload: SetNodePositionPayload): Action => ({
121112
setNodePosition: payload,
122113
});
123114

124-
export const addNode = (payload: AddNodePayload): Action => ({
125-
type: "add_node",
126-
addNode: payload,
115+
export const setNode = (payload: SetNodePayload): Action => ({
116+
type: "set_node",
117+
setNode: payload,
127118
});
128119

129120
export const removeNode = (payload: RemoveNodePayload): Action => ({
@@ -156,35 +147,51 @@ export const setLegend = (payload: SetLegendPayload): Action => ({
156147
setLegend: payload,
157148
});
158149

159-
export const reduce = (state: Schematic, action: Action): Schematic => {
160-
switch (action.type) {
161-
case "set_node_position":
162-
handleSetNodePosition(state, action.setNodePosition);
163-
break;
164-
case "add_node":
165-
handleAddNode(state, action.addNode);
166-
break;
167-
case "remove_node":
168-
handleRemoveNode(state, action.removeNode);
169-
break;
170-
case "set_edge":
171-
handleSetEdge(state, action.setEdge);
172-
break;
173-
case "remove_edge":
174-
handleRemoveEdge(state, action.removeEdge);
175-
break;
176-
case "set_config":
177-
handleSetConfig(state, action.setConfig);
178-
break;
179-
case "set_authority":
180-
handleSetAuthority(state, action.setAuthority);
181-
break;
182-
case "set_legend":
183-
handleSetLegend(state, action.setLegend);
184-
break;
185-
}
186-
return state;
150+
export interface Handlers {
151+
setNodePosition: (state: Schematic, payload: SetNodePositionPayload) => void;
152+
setNode: (state: Schematic, payload: SetNodePayload) => void;
153+
removeNode: (state: Schematic, payload: RemoveNodePayload) => void;
154+
setEdge: (state: Schematic, payload: SetEdgePayload) => void;
155+
removeEdge: (state: Schematic, payload: RemoveEdgePayload) => void;
156+
setConfig: (state: Schematic, payload: SetConfigPayload) => void;
157+
setAuthority: (state: Schematic, payload: SetAuthorityPayload) => void;
158+
setLegend: (state: Schematic, payload: SetLegendPayload) => void;
159+
}
160+
161+
export const createReducer =
162+
(handlers: Handlers) =>
163+
(state: Schematic, action: Action): Schematic => {
164+
switch (action.type) {
165+
case "set_node_position":
166+
handlers.setNodePosition(state, action.setNodePosition);
167+
break;
168+
case "set_node":
169+
handlers.setNode(state, action.setNode);
170+
break;
171+
case "remove_node":
172+
handlers.removeNode(state, action.removeNode);
173+
break;
174+
case "set_edge":
175+
handlers.setEdge(state, action.setEdge);
176+
break;
177+
case "remove_edge":
178+
handlers.removeEdge(state, action.removeEdge);
179+
break;
180+
case "set_config":
181+
handlers.setConfig(state, action.setConfig);
182+
break;
183+
case "set_authority":
184+
handlers.setAuthority(state, action.setAuthority);
185+
break;
186+
case "set_legend":
187+
handlers.setLegend(state, action.setLegend);
188+
break;
189+
}
190+
return state;
191+
};
192+
193+
export const createReduceAll = (handlers: Handlers) => {
194+
const reduce = createReducer(handlers);
195+
return (state: Schematic, actions: Action[]): Schematic =>
196+
produce(state, (draft) => actions.forEach((action) => reduce(draft, action)));
187197
};
188-
189-
export const reduceAll = (state: Schematic, actions: Action[]): Schematic =>
190-
produce(state, (draft) => actions.forEach((action) => reduce(draft, action)));

client/ts/src/schematic/actions.spec.ts

Lines changed: 21 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,11 @@ import { color } from "@synnaxlabs/x";
1111
import { describe, expect, it } from "vitest";
1212
import { z } from "zod";
1313

14+
import { reduce, reduceAll } from "@/schematic/actions";
1415
import {
1516
type Action,
1617
actionZ,
17-
addNode,
18-
reduce,
19-
reduceAll,
18+
setNode,
2019
removeEdge,
2120
removeNode,
2221
setAuthority,
@@ -83,28 +82,31 @@ describe("schematic reducer", () => {
8382
});
8483
});
8584

86-
describe("addNode", () => {
87-
it("should append the node to the end of the slice", () => {
85+
describe("setNode", () => {
86+
it("should append the node to the end of the slice when no node has the same key", () => {
8887
const state = empty({ nodes: [node("n1", 0, 0)] });
89-
const out = reduceAll(state, [addNode({ node: node("n2", 1, 2) })]);
88+
const out = reduceAll(state, [setNode({ node: node("n2", 1, 2) })]);
9089
expect(out.nodes).toEqual([node("n1", 0, 0), node("n2", 1, 2)]);
9190
});
9291
it("should write config under the node's key when config is non-undefined", () => {
9392
const out = reduceAll(empty(), [
94-
addNode({ node: node("n1", 0, 0), config: { label: "Pump", color: "#f00" } }),
93+
setNode({ node: node("n1", 0, 0), config: { label: "Pump", color: "#f00" } }),
9594
]);
9695
expect(out.configs).toEqual({ n1: { label: "Pump", color: "#f00" } });
9796
});
9897
it("should leave configs untouched when the action's config is undefined", () => {
99-
const out = reduceAll(empty(), [addNode({ node: node("n1", 0, 0) })]);
98+
const out = reduceAll(empty(), [setNode({ node: node("n1", 0, 0) })]);
10099
expect(out.configs).toEqual({});
101100
});
102-
it("should append a duplicate-key node, locking current behavior", () => {
103-
const state = empty({ nodes: [node("n1", 0, 0)] });
104-
const out = reduceAll(state, [addNode({ node: node("n1", 9, 9) })]);
105-
expect(out.nodes).toHaveLength(2);
101+
it("should replace an existing node in place when the key already exists, preserving slice index", () => {
102+
const state = empty({
103+
nodes: [node("n1", 0, 0), node("n2", 1, 1), node("n3", 2, 2)],
104+
});
105+
const out = reduceAll(state, [setNode({ node: node("n2", 9, 9) })]);
106+
expect(out.nodes).toHaveLength(3);
106107
expect(out.nodes[0]).toEqual(node("n1", 0, 0));
107-
expect(out.nodes[1]).toEqual(node("n1", 9, 9));
108+
expect(out.nodes[1]).toEqual(node("n2", 9, 9));
109+
expect(out.nodes[2]).toEqual(node("n3", 2, 2));
108110
});
109111
});
110112

@@ -253,9 +255,9 @@ describe("schematic reducer", () => {
253255

254256
it("should build a complete graph from an empty schematic", () => {
255257
const out = reduceAll(empty(), [
256-
addNode({ node: node("pump", 0, 0) }),
257-
addNode({ node: node("valve", 100, 0) }),
258-
addNode({ node: node("tank", 200, 0) }),
258+
setNode({ node: node("pump", 0, 0) }),
259+
setNode({ node: node("valve", 100, 0) }),
260+
setNode({ node: node("tank", 200, 0) }),
259261
setEdge({ edge: edge("e1", "pump", "out", "valve", "in") }),
260262
setEdge({ edge: edge("e2", "valve", "out", "tank", "in") }),
261263
setConfig({ key: "pump", config: { label: "Main Pump" } }),
@@ -277,7 +279,7 @@ describe("schematic reducer", () => {
277279
});
278280
const out = reduceAll(state, [
279281
removeNode({ key: "n1" }),
280-
addNode({ node: node("n1", 50, 50) }),
282+
setNode({ node: node("n1", 50, 50) }),
281283
]);
282284
expect(out.nodes).toHaveLength(2);
283285
expect(out.nodes[1]).toEqual(node("n1", 50, 50));
@@ -296,7 +298,7 @@ describe("schematic reducer", () => {
296298
const state = empty();
297299
const actions: Action[] = [];
298300
for (let i = 0; i < 5; i++)
299-
actions.push(addNode({ node: node(`n${i}`, i * 100, 0) }));
301+
actions.push(setNode({ node: node(`n${i}`, i * 100, 0) }));
300302
for (let i = 0; i < 5; i++) {
301303
actions.push(
302304
setNodePosition({ key: `n${i}`, position: { x: i * 100, y: 50 } }),
@@ -338,7 +340,7 @@ describe("schematic reducer", () => {
338340

339341
describe("single-action reduce", () => {
340342
it("should apply a single action without wrapping it in an array", () => {
341-
const out = reduce(empty(), addNode({ node: node("n1", 1, 2) }));
343+
const out = reduce(empty(), setNode({ node: node("n1", 1, 2) }));
342344
expect(out.nodes).toEqual([node("n1", 1, 2)]);
343345
});
344346
});

client/ts/src/schematic/actions.ts

Lines changed: 40 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -7,65 +7,43 @@
77
// License, use of this software will be governed by the Apache License, Version 2.0,
88
// included in the file licenses/APL.txt.
99

10-
import {
11-
type AddNodePayload,
12-
type RemoveEdgePayload,
13-
type RemoveNodePayload,
14-
type SetAuthorityPayload,
15-
type SetConfigPayload,
16-
type SetEdgePayload,
17-
type SetLegendPayload,
18-
type SetNodePositionPayload,
19-
} from "@/schematic/actions.gen";
20-
import { type Schematic } from "@/schematic/types.gen";
21-
22-
export const handleSetNodePosition = (
23-
state: Schematic,
24-
payload: SetNodePositionPayload,
25-
): void => {
26-
const node = state.nodes.find((n) => n.key === payload.key);
27-
if (node != null) node.position = payload.position;
28-
};
29-
30-
export const handleAddNode = (state: Schematic, payload: AddNodePayload): void => {
31-
state.nodes.push(payload.node);
32-
if (payload.config != null) state.configs[payload.node.key] = payload.config;
33-
};
34-
35-
export const handleRemoveNode = (
36-
state: Schematic,
37-
payload: RemoveNodePayload,
38-
): void => {
39-
const idx = state.nodes.findIndex((n) => n.key === payload.key);
40-
if (idx !== -1) state.nodes.splice(idx, 1);
41-
delete state.configs[payload.key];
42-
};
43-
44-
export const handleSetEdge = (state: Schematic, payload: SetEdgePayload): void => {
45-
const idx = state.edges.findIndex((e) => e.key === payload.edge.key);
46-
if (idx !== -1) state.edges[idx] = payload.edge;
47-
else state.edges.push(payload.edge);
48-
};
49-
50-
export const handleRemoveEdge = (
51-
state: Schematic,
52-
payload: RemoveEdgePayload,
53-
): void => {
54-
const idx = state.edges.findIndex((e) => e.key === payload.key);
55-
if (idx !== -1) state.edges.splice(idx, 1);
56-
};
57-
58-
export const handleSetConfig = (state: Schematic, payload: SetConfigPayload): void => {
59-
state.configs[payload.key] = payload.config;
60-
};
61-
62-
export const handleSetAuthority = (
63-
state: Schematic,
64-
payload: SetAuthorityPayload,
65-
): void => {
66-
state.authority = payload.value;
67-
};
68-
69-
export const handleSetLegend = (state: Schematic, payload: SetLegendPayload): void => {
70-
state.legend = payload.legend;
71-
};
10+
import { createReducer, createReduceAll, type Handlers } from "@/schematic/actions.gen";
11+
12+
const handlers: Handlers = {
13+
setNodePosition: (state, payload) => {
14+
const node = state.nodes.find((n) => n.key === payload.key);
15+
if (node != null) node.position = payload.position;
16+
},
17+
setNode: (state, payload) => {
18+
const idx = state.nodes.findIndex((n) => n.key === payload.node.key);
19+
if (idx !== -1) state.nodes[idx] = payload.node;
20+
else state.nodes.push(payload.node);
21+
if (payload.config != null) state.configs[payload.node.key] = payload.config;
22+
},
23+
removeNode: (state, payload) => {
24+
const idx = state.nodes.findIndex((n) => n.key === payload.key);
25+
if (idx !== -1) state.nodes.splice(idx, 1);
26+
delete state.configs[payload.key];
27+
},
28+
setEdge: (state, payload) => {
29+
const idx = state.edges.findIndex((e) => e.key === payload.edge.key);
30+
if (idx !== -1) state.edges[idx] = payload.edge;
31+
else state.edges.push(payload.edge);
32+
},
33+
removeEdge: (state, payload) => {
34+
const idx = state.edges.findIndex((e) => e.key === payload.key);
35+
if (idx !== -1) state.edges.splice(idx, 1);
36+
},
37+
setConfig: (state, payload) => {
38+
state.configs[payload.key] = payload.config;
39+
},
40+
setAuthority: (state, payload) => {
41+
state.authority = payload.value;
42+
},
43+
setLegend: (state, payload) => {
44+
state.legend = payload.legend;
45+
},
46+
};
47+
48+
export const reduce = createReducer(handlers);
49+
export const reduceAll = createReduceAll(handlers);

client/ts/src/schematic/schematic.spec.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -194,10 +194,10 @@ describe("Schematic", () => {
194194
expect(res.nodes[0].position).toEqual({ x: 100, y: 200 });
195195
});
196196

197-
test("addNode appends a node and writes its config", async () => {
197+
test("setNode inserts a node and writes its config", async () => {
198198
const { schem } = await newWorkspaceSchematic(client);
199199
await client.schematics.dispatch(schem.key, "sess-1", [
200-
schematic.addNode({
200+
schematic.setNode({
201201
node: { key: "n1", position: { x: 1, y: 2 } },
202202
config: { label: "Pump" },
203203
}),
@@ -311,8 +311,8 @@ describe("Schematic", () => {
311311
test("applies a multi-action sequence atomically", async () => {
312312
const { schem } = await newWorkspaceSchematic(client);
313313
await client.schematics.dispatch(schem.key, "sess-1", [
314-
schematic.addNode({ node: { key: "pump", position: { x: 0, y: 0 } } }),
315-
schematic.addNode({ node: { key: "valve", position: { x: 100, y: 0 } } }),
314+
schematic.setNode({ node: { key: "pump", position: { x: 0, y: 0 } } }),
315+
schematic.setNode({ node: { key: "valve", position: { x: 100, y: 0 } } }),
316316
schematic.setEdge({
317317
edge: {
318318
key: "e1",

core/pkg/api/schematic/schematic_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,10 +84,10 @@ var _ = Describe("api.Service.Dispatch", func() {
8484
Key: s.Key,
8585
SessionKey: "sess-1",
8686
Actions: []schematic.Action{
87-
schematic.NewAddNodeAction(schematic.AddNode{
87+
schematic.NewSetNodeAction(schematic.SetNode{
8888
Node: schematic.Node{Key: "n1", Position: spatial.XY{X: 1, Y: 2}},
8989
}),
90-
schematic.NewAddNodeAction(schematic.AddNode{
90+
schematic.NewSetNodeAction(schematic.SetNode{
9191
Node: schematic.Node{Key: "n2", Position: spatial.XY{X: 3, Y: 4}},
9292
}),
9393
schematic.NewSetEdgeAction(schematic.SetEdge{Edge: schematic.Edge{

0 commit comments

Comments
 (0)