@synnaxlabs/client 0.54.2 → 0.56.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.
Files changed (222) hide show
  1. package/.turbo/turbo-build.log +10 -13
  2. package/dist/client.cjs +60 -42
  3. package/dist/client.js +8037 -6265
  4. package/dist/src/access/policy/client.d.ts +70 -80
  5. package/dist/src/access/policy/client.d.ts.map +1 -1
  6. package/dist/src/access/policy/types.gen.d.ts +18 -20
  7. package/dist/src/access/policy/types.gen.d.ts.map +1 -1
  8. package/dist/src/access/role/client.d.ts.map +1 -1
  9. package/dist/src/access/role/types.gen.d.ts +2 -2
  10. package/dist/src/actions/actions.d.ts +68 -0
  11. package/dist/src/actions/actions.d.ts.map +1 -0
  12. package/dist/src/actions/actions.spec.d.ts +2 -0
  13. package/dist/src/actions/actions.spec.d.ts.map +1 -0
  14. package/dist/src/actions/external.d.ts +2 -0
  15. package/dist/src/actions/external.d.ts.map +1 -0
  16. package/dist/src/actions/index.d.ts +2 -0
  17. package/dist/src/actions/index.d.ts.map +1 -0
  18. package/dist/src/arc/arc.spec.d.ts +2 -0
  19. package/dist/src/arc/arc.spec.d.ts.map +1 -0
  20. package/dist/src/arc/client.d.ts.map +1 -1
  21. package/dist/src/arc/compiler/types.gen.d.ts +1 -1
  22. package/dist/src/arc/compiler/types.gen.d.ts.map +1 -1
  23. package/dist/src/arc/graph/types.gen.d.ts +40 -40
  24. package/dist/src/arc/graph/types.gen.d.ts.map +1 -1
  25. package/dist/src/arc/ir/types.gen.d.ts +202 -233
  26. package/dist/src/arc/ir/types.gen.d.ts.map +1 -1
  27. package/dist/src/arc/module/types.gen.d.ts +63 -82
  28. package/dist/src/arc/module/types.gen.d.ts.map +1 -1
  29. package/dist/src/arc/program/types.gen.d.ts +63 -82
  30. package/dist/src/arc/program/types.gen.d.ts.map +1 -1
  31. package/dist/src/arc/types/types.gen.d.ts +11 -11
  32. package/dist/src/arc/types/types.gen.d.ts.map +1 -1
  33. package/dist/src/arc/types.gen.d.ts +139 -158
  34. package/dist/src/arc/types.gen.d.ts.map +1 -1
  35. package/dist/src/auth/auth.d.ts +3 -3
  36. package/dist/src/auth/auth.d.ts.map +1 -1
  37. package/dist/src/channel/client.d.ts +2 -2
  38. package/dist/src/channel/client.d.ts.map +1 -1
  39. package/dist/src/channel/retriever.d.ts +5 -8
  40. package/dist/src/channel/retriever.d.ts.map +1 -1
  41. package/dist/src/channel/types.gen.d.ts +3 -3
  42. package/dist/src/channel/types.gen.d.ts.map +1 -1
  43. package/dist/src/channel/writer.d.ts.map +1 -1
  44. package/dist/src/client.d.ts +5 -0
  45. package/dist/src/client.d.ts.map +1 -1
  46. package/dist/src/connection/checker.d.ts +17 -2
  47. package/dist/src/connection/checker.d.ts.map +1 -1
  48. package/dist/src/control/state.d.ts.map +1 -1
  49. package/dist/src/device/client.d.ts.map +1 -1
  50. package/dist/src/device/types.gen.d.ts +6 -8
  51. package/dist/src/device/types.gen.d.ts.map +1 -1
  52. package/dist/src/errors.d.ts +2 -0
  53. package/dist/src/errors.d.ts.map +1 -1
  54. package/dist/src/framer/adapter.d.ts.map +1 -1
  55. package/dist/src/framer/client.d.ts +2 -2
  56. package/dist/src/framer/client.d.ts.map +1 -1
  57. package/dist/src/framer/codec.d.ts +9 -1
  58. package/dist/src/framer/codec.d.ts.map +1 -1
  59. package/dist/src/framer/deleter.d.ts.map +1 -1
  60. package/dist/src/framer/frame.d.ts +1 -1
  61. package/dist/src/framer/iterator.d.ts +84 -3
  62. package/dist/src/framer/iterator.d.ts.map +1 -1
  63. package/dist/src/framer/streamProxy.d.ts.map +1 -1
  64. package/dist/src/framer/streamer.d.ts +1 -3
  65. package/dist/src/framer/streamer.d.ts.map +1 -1
  66. package/dist/src/framer/types.gen.d.ts +18 -0
  67. package/dist/src/framer/types.gen.d.ts.map +1 -1
  68. package/dist/src/framer/writer.d.ts +8 -8
  69. package/dist/src/framer/writer.d.ts.map +1 -1
  70. package/dist/src/group/client.d.ts +1 -2
  71. package/dist/src/group/client.d.ts.map +1 -1
  72. package/dist/src/group/types.gen.d.ts +2 -2
  73. package/dist/src/index.d.ts +2 -1
  74. package/dist/src/index.d.ts.map +1 -1
  75. package/dist/src/label/client.d.ts +5 -8
  76. package/dist/src/label/client.d.ts.map +1 -1
  77. package/dist/src/lineplot/client.d.ts.map +1 -1
  78. package/dist/src/lineplot/types.gen.d.ts +2 -2
  79. package/dist/src/log/client.d.ts.map +1 -1
  80. package/dist/src/log/types.gen.d.ts +2 -2
  81. package/dist/src/ontology/client.d.ts +1 -3
  82. package/dist/src/ontology/client.d.ts.map +1 -1
  83. package/dist/src/ontology/payload.d.ts +12 -16
  84. package/dist/src/ontology/payload.d.ts.map +1 -1
  85. package/dist/src/ontology/types.gen.d.ts +1 -2
  86. package/dist/src/ontology/types.gen.d.ts.map +1 -1
  87. package/dist/src/ontology/writer.d.ts +5 -10
  88. package/dist/src/ontology/writer.d.ts.map +1 -1
  89. package/dist/src/rack/client.d.ts.map +1 -1
  90. package/dist/src/rack/types.gen.d.ts +3 -3
  91. package/dist/src/ranger/alias/client.d.ts.map +1 -1
  92. package/dist/src/ranger/client.d.ts.map +1 -1
  93. package/dist/src/ranger/kv/client.d.ts.map +1 -1
  94. package/dist/src/ranger/types.gen.d.ts +6 -6
  95. package/dist/src/ranger/types.gen.d.ts.map +1 -1
  96. package/dist/src/ranger/writer.d.ts +2 -3
  97. package/dist/src/ranger/writer.d.ts.map +1 -1
  98. package/dist/src/schematic/actions.d.ts +147 -0
  99. package/dist/src/schematic/actions.d.ts.map +1 -0
  100. package/dist/src/schematic/actions.gen.d.ts +484 -0
  101. package/dist/src/schematic/actions.gen.d.ts.map +1 -0
  102. package/dist/src/schematic/actions.spec.d.ts +2 -0
  103. package/dist/src/schematic/actions.spec.d.ts.map +1 -0
  104. package/dist/src/schematic/client.d.ts +53 -2
  105. package/dist/src/schematic/client.d.ts.map +1 -1
  106. package/dist/src/schematic/external.d.ts +2 -0
  107. package/dist/src/schematic/external.d.ts.map +1 -1
  108. package/dist/src/schematic/symbol/client.d.ts.map +1 -1
  109. package/dist/src/schematic/symbol/types.gen.d.ts +48 -58
  110. package/dist/src/schematic/symbol/types.gen.d.ts.map +1 -1
  111. package/dist/src/schematic/types.gen.d.ts +131 -5
  112. package/dist/src/schematic/types.gen.d.ts.map +1 -1
  113. package/dist/src/status/client.d.ts.map +1 -1
  114. package/dist/src/status/payload.d.ts +3 -3
  115. package/dist/src/table/actions.d.ts +156 -0
  116. package/dist/src/table/actions.d.ts.map +1 -0
  117. package/dist/src/table/actions.gen.d.ts +587 -0
  118. package/dist/src/table/actions.gen.d.ts.map +1 -0
  119. package/dist/src/table/client.d.ts +28 -2
  120. package/dist/src/table/client.d.ts.map +1 -1
  121. package/dist/src/table/external.d.ts +2 -0
  122. package/dist/src/table/external.d.ts.map +1 -1
  123. package/dist/src/table/types.gen.d.ts +71 -4
  124. package/dist/src/table/types.gen.d.ts.map +1 -1
  125. package/dist/src/task/client.d.ts.map +1 -1
  126. package/dist/src/task/types.gen.d.ts +7 -7
  127. package/dist/src/task/types.gen.d.ts.map +1 -1
  128. package/dist/src/user/client.d.ts +2 -2
  129. package/dist/src/user/client.d.ts.map +1 -1
  130. package/dist/src/user/types.gen.d.ts +2 -2
  131. package/dist/src/view/client.d.ts.map +1 -1
  132. package/dist/src/view/types.gen.d.ts +2 -2
  133. package/dist/src/workspace/client.d.ts.map +1 -1
  134. package/dist/src/workspace/types.gen.d.ts +3 -3
  135. package/dist/src/workspace/types.gen.d.ts.map +1 -1
  136. package/package.json +12 -11
  137. package/src/access/policy/client.ts +4 -7
  138. package/src/access/role/client.ts +6 -26
  139. package/src/actions/actions.spec.ts +229 -0
  140. package/src/actions/actions.ts +104 -0
  141. package/src/actions/external.ts +10 -0
  142. package/src/actions/index.ts +10 -0
  143. package/src/arc/arc.spec.ts +44 -0
  144. package/src/arc/client.ts +3 -7
  145. package/src/arc/compiler/types.gen.ts +2 -1
  146. package/src/arc/ir/types.gen.ts +102 -48
  147. package/src/arc/lsp.spec.ts +3 -7
  148. package/src/arc/types/types.gen.ts +3 -3
  149. package/src/auth/auth.spec.ts +12 -13
  150. package/src/auth/auth.ts +48 -34
  151. package/src/channel/batchRetriever.spec.ts +13 -4
  152. package/src/channel/channel.spec.ts +13 -0
  153. package/src/channel/client.ts +8 -6
  154. package/src/channel/retriever.ts +7 -16
  155. package/src/channel/types.gen.ts +1 -2
  156. package/src/channel/writer.ts +4 -20
  157. package/src/client.ts +3 -0
  158. package/src/connection/checker.ts +48 -10
  159. package/src/connection/connection.spec.ts +64 -2
  160. package/src/control/state.ts +5 -4
  161. package/src/device/client.ts +5 -8
  162. package/src/device/device.spec.ts +7 -5
  163. package/src/device/types.gen.ts +4 -4
  164. package/src/errors.ts +8 -9
  165. package/src/framer/adapter.ts +2 -4
  166. package/src/framer/client.ts +12 -0
  167. package/src/framer/codec.spec.ts +53 -3
  168. package/src/framer/codec.ts +58 -25
  169. package/src/framer/deleter.ts +2 -8
  170. package/src/framer/iterator.ts +42 -39
  171. package/src/framer/streamProxy.ts +11 -12
  172. package/src/framer/streamer.spec.ts +1 -1
  173. package/src/framer/streamer.ts +2 -7
  174. package/src/framer/types.gen.ts +20 -0
  175. package/src/framer/writer.spec.ts +221 -1
  176. package/src/framer/writer.ts +53 -28
  177. package/src/group/client.ts +4 -7
  178. package/src/index.ts +3 -2
  179. package/src/label/client.ts +6 -16
  180. package/src/label/label.spec.ts +12 -0
  181. package/src/lineplot/client.ts +6 -21
  182. package/src/log/client.ts +6 -21
  183. package/src/ontology/client.ts +2 -3
  184. package/src/ontology/ontology.spec.ts +10 -0
  185. package/src/ontology/types.gen.ts +0 -1
  186. package/src/ontology/writer.ts +4 -7
  187. package/src/rack/client.ts +4 -7
  188. package/src/rack/rack.spec.ts +12 -1
  189. package/src/ranger/alias/client.ts +6 -11
  190. package/src/ranger/client.ts +2 -3
  191. package/src/ranger/kv/client.ts +4 -7
  192. package/src/ranger/ranger.spec.ts +12 -0
  193. package/src/ranger/writer.ts +4 -17
  194. package/src/schematic/access.spec.ts +6 -6
  195. package/src/schematic/actions.gen.ts +200 -0
  196. package/src/schematic/actions.spec.ts +699 -0
  197. package/src/schematic/actions.ts +168 -0
  198. package/src/schematic/client.ts +34 -30
  199. package/src/schematic/external.ts +2 -0
  200. package/src/schematic/schematic.spec.ts +233 -69
  201. package/src/schematic/symbol/client.spec.ts +33 -9
  202. package/src/schematic/symbol/client.ts +6 -11
  203. package/src/schematic/symbol/types.gen.ts +1 -10
  204. package/src/schematic/types.gen.ts +55 -6
  205. package/src/status/client.ts +4 -10
  206. package/src/status/status.spec.ts +7 -6
  207. package/src/table/access.spec.ts +0 -6
  208. package/src/table/actions.gen.ts +243 -0
  209. package/src/table/actions.ts +255 -0
  210. package/src/table/client.ts +21 -25
  211. package/src/table/external.ts +2 -0
  212. package/src/table/table.spec.ts +588 -43
  213. package/src/table/types.gen.ts +58 -5
  214. package/src/task/client.ts +14 -20
  215. package/src/task/task.spec.ts +15 -1
  216. package/src/task/types.gen.ts +8 -6
  217. package/src/user/client.ts +6 -11
  218. package/src/view/client.ts +4 -7
  219. package/src/view/view.spec.ts +9 -5
  220. package/src/workspace/client.ts +6 -16
  221. package/src/workspace/types.gen.ts +2 -1
  222. package/src/workspace/workspace.spec.ts +14 -1
@@ -0,0 +1,168 @@
1
+ // Copyright 2026 Synnax Labs, Inc.
2
+ //
3
+ // Use of this software is governed by the Business Source License included in the file
4
+ // licenses/BSL.txt.
5
+ //
6
+ // As of the Change Date specified in that file, in accordance with the Business Source
7
+ // License, use of this software will be governed by the Apache License, Version 2.0,
8
+ // included in the file licenses/APL.txt.
9
+
10
+ import { color, type record } from "@synnaxlabs/x";
11
+ import { current, type Draft, isDraft } from "immer";
12
+
13
+ import {
14
+ type Action,
15
+ addEdge,
16
+ createReduceAll,
17
+ type HandlerResult,
18
+ type Handlers,
19
+ removeEdge,
20
+ removeNode,
21
+ rename,
22
+ setConfig,
23
+ setNode,
24
+ setNodePosition,
25
+ } from "@/schematic/actions.gen";
26
+
27
+ const NO_OP: HandlerResult = { inverse: [], targets: [] };
28
+
29
+ // Snapshots a value out of an Immer draft so the result is safe to embed in an
30
+ // action stored on the undo stack. When reduceAll applies multiple actions in
31
+ // one produce(), an earlier action's wholesale assignment (e.g. state.configs[k]
32
+ // = {...existing, ...payload.config}) leaves the slot as a plain object — the
33
+ // next action would crash if it called current() unconditionally.
34
+ const snapshot = <T>(v: T): T => (isDraft(v) ? current(v as Draft<T>) : v);
35
+
36
+ const handlers: Handlers = {
37
+ rename: (state, payload) => {
38
+ const oldName = state.name;
39
+ state.name = payload.name;
40
+ return {
41
+ inverse: [rename({ name: oldName })],
42
+ targets: [state.key],
43
+ };
44
+ },
45
+ setNodePosition: (state, payload) => {
46
+ const node = state.nodes.find((n) => n.key === payload.key);
47
+ if (node == null) return NO_OP;
48
+ const oldPosition = { x: node.position.x, y: node.position.y };
49
+ node.position = payload.position;
50
+ return {
51
+ inverse: [setNodePosition({ key: payload.key, position: oldPosition })],
52
+ targets: [payload.key],
53
+ };
54
+ },
55
+
56
+ // set_node_measured carries layout-derived dimensions, not user intent.
57
+ // Both the inverse and the target list are empty so it neither contributes
58
+ // to the undo stack nor invalidates undoables targeting the same node when
59
+ // a remote session emits it.
60
+ setNodeMeasured: (state, payload) => {
61
+ const node = state.nodes.find((n) => n.key === payload.key);
62
+ if (node == null) return NO_OP;
63
+ node.measured = payload.measured;
64
+ return { inverse: [], targets: [] };
65
+ },
66
+ setNode: (state, payload) => {
67
+ const idx = state.nodes.findIndex((n) => n.key === payload.node.key);
68
+ if (idx === -1) {
69
+ state.nodes.push(payload.node);
70
+ if (payload.config != null) state.configs[payload.node.key] = payload.config;
71
+ return {
72
+ inverse: [removeNode({ key: payload.node.key })],
73
+ targets: [payload.node.key],
74
+ };
75
+ }
76
+ const oldNode = snapshot(state.nodes[idx]);
77
+ const oldConfigRaw = state.configs[payload.node.key];
78
+ const oldConfig = oldConfigRaw != null ? snapshot(oldConfigRaw) : undefined;
79
+ state.nodes[idx] = payload.node;
80
+ if (payload.config != null) state.configs[payload.node.key] = payload.config;
81
+ return {
82
+ inverse: [
83
+ setNode(
84
+ oldConfig != null
85
+ ? { node: oldNode, config: oldConfig }
86
+ : { node: oldNode, config: undefined },
87
+ ),
88
+ ],
89
+ targets: [payload.node.key],
90
+ };
91
+ },
92
+ removeNode: (state, payload) => {
93
+ const idx = state.nodes.findIndex((n) => n.key === payload.key);
94
+ if (idx === -1) return NO_OP;
95
+ const oldNode = snapshot(state.nodes[idx]);
96
+ const oldConfigRaw = state.configs[payload.key];
97
+ const oldConfig = oldConfigRaw != null ? snapshot(oldConfigRaw) : undefined;
98
+ state.nodes.splice(idx, 1);
99
+ delete state.configs[payload.key];
100
+ return {
101
+ inverse: [
102
+ setNode(
103
+ oldConfig != null
104
+ ? { node: oldNode, config: oldConfig }
105
+ : { node: oldNode, config: undefined },
106
+ ),
107
+ ],
108
+ targets: [payload.key],
109
+ };
110
+ },
111
+ addEdge: (state, payload) => {
112
+ if (state.edges.some((e) => e.key === payload.edge.key)) return NO_OP;
113
+ state.edges.push(payload.edge);
114
+ return {
115
+ inverse: [removeEdge({ key: payload.edge.key })],
116
+ targets: [payload.edge.key],
117
+ };
118
+ },
119
+
120
+ removeEdge: (state, payload) => {
121
+ const idx = state.edges.findIndex((e) => e.key === payload.key);
122
+ if (idx === -1) return NO_OP;
123
+ const oldEdge = snapshot(state.edges[idx]);
124
+ state.edges.splice(idx, 1);
125
+ return {
126
+ inverse: [addEdge({ edge: oldEdge })],
127
+ targets: [payload.key],
128
+ };
129
+ },
130
+ // The inverse of SetConfig is imperfect for keys the action newly
131
+ // introduces: SetConfig only merges, so it cannot remove keys that did not
132
+ // previously exist. The inverse here restores values for keys that DID
133
+ // exist before the merge; keys added by the action remain on undo as
134
+ // phantom fields. A future ReplaceConfig action can close the gap by
135
+ // enabling wholesale replacement.
136
+ setConfig: (state, payload) => {
137
+ const existingRaw = state.configs[payload.key];
138
+ if (existingRaw != null) {
139
+ const existing = snapshot(existingRaw);
140
+ const restoreFields: record.Unknown = {};
141
+ for (const k of Object.keys(payload.config))
142
+ if (existing[k] !== undefined) restoreFields[k] = existing[k];
143
+ state.configs[payload.key] = { ...existing, ...payload.config };
144
+ if (Object.keys(restoreFields).length === 0)
145
+ return { inverse: [], targets: [payload.key] };
146
+ return {
147
+ inverse: [setConfig({ key: payload.key, config: restoreFields })],
148
+ targets: [payload.key],
149
+ };
150
+ }
151
+ let cfg = payload.config;
152
+ const edge = state.edges.find((e) => e.key === payload.key);
153
+ if (edge != null) {
154
+ const srcCfg = state.configs[edge.source.node] as
155
+ | { color?: color.Crude }
156
+ | undefined;
157
+ if (srcCfg?.color != null && !color.isZero(srcCfg.color))
158
+ cfg = { ...cfg, color: srcCfg.color };
159
+ }
160
+ state.configs[payload.key] = cfg;
161
+ return { inverse: [], targets: [payload.key] };
162
+ },
163
+ };
164
+
165
+ export const reduceAll = createReduceAll(handlers);
166
+
167
+ export const isUndoable = (action: Action): boolean =>
168
+ action.type !== "set_node_measured";
@@ -7,10 +7,15 @@
7
7
  // License, use of this software will be governed by the Apache License, Version 2.0,
8
8
  // included in the file licenses/APL.txt.
9
9
 
10
- import { sendRequired, type UnaryClient } from "@synnaxlabs/freighter";
11
- import { array, caseconv, record } from "@synnaxlabs/x";
10
+ import { type UnaryClient } from "@synnaxlabs/freighter";
11
+ import { array } from "@synnaxlabs/x";
12
12
  import { z } from "zod";
13
13
 
14
+ import {
15
+ type Action,
16
+ dispatchReqZ,
17
+ rename as renameAction,
18
+ } from "@/schematic/actions.gen";
14
19
  import { symbol } from "@/schematic/symbol";
15
20
  import {
16
21
  type Key,
@@ -23,12 +28,11 @@ import {
23
28
  import { checkForMultipleOrNoResults } from "@/util/retrieve";
24
29
  import { workspace } from "@/workspace";
25
30
 
26
- const renameReqZ = z.object({ key: keyZ, name: z.string() });
31
+ export const SET_CHANNEL_NAME = "sy_schematic_set";
27
32
 
28
- const setDataReqZ = z.object({
29
- key: keyZ,
30
- data: caseconv.preserveCase(record.unknownZ()),
31
- });
33
+ const setDataBodyZ = schematicZ.omit({ key: true, name: true, snapshot: true });
34
+ export type SetDataBody = z.input<typeof setDataBodyZ>;
35
+ const setDataReqZ = z.object({ key: keyZ, data: setDataBodyZ });
32
36
  const deleteReqZ = z.object({ keys: keyZ.array() });
33
37
 
34
38
  const copyReqZ = z.object({
@@ -75,8 +79,7 @@ export class Client {
75
79
  schematics: New | New[],
76
80
  ): Promise<Schematic | Schematic[]> {
77
81
  const isMany = Array.isArray(schematics);
78
- const res = await sendRequired(
79
- this.client,
82
+ const res = await this.client.send(
80
83
  "/schematic/create",
81
84
  { workspace, schematics: array.toArray(schematics) },
82
85
  createReqZ,
@@ -86,18 +89,11 @@ export class Client {
86
89
  }
87
90
 
88
91
  async rename(key: Key, name: string): Promise<void> {
89
- await sendRequired(
90
- this.client,
91
- "/schematic/rename",
92
- { key, name },
93
- renameReqZ,
94
- emptyResZ,
95
- );
92
+ await this.dispatch(key, "", [renameAction({ name })]);
96
93
  }
97
94
 
98
- async setData(key: Key, data: record.Unknown): Promise<void> {
99
- await sendRequired(
100
- this.client,
95
+ async setData(key: Key, data: SetDataBody): Promise<void> {
96
+ await this.client.send(
101
97
  "/schematic/set-data",
102
98
  { key, data },
103
99
  setDataReqZ,
@@ -105,14 +101,22 @@ export class Client {
105
101
  );
106
102
  }
107
103
 
104
+ async dispatch(key: Key, dispatchKey: string, actions: Action[]): Promise<void> {
105
+ await this.client.send(
106
+ "/schematic/dispatch",
107
+ { key, dispatchKey, actions },
108
+ dispatchReqZ,
109
+ emptyResZ,
110
+ );
111
+ }
112
+
108
113
  async retrieve(args: RetrieveSingleParams): Promise<Schematic>;
109
114
  async retrieve(args: RetrieveMultipleParams): Promise<Schematic[]>;
110
115
  async retrieve(
111
116
  args: RetrieveSingleParams | RetrieveMultipleParams,
112
117
  ): Promise<Schematic | Schematic[]> {
113
118
  const isSingle = singleRetrieveArgsZ.safeParse(args).success;
114
- const res = await sendRequired(
115
- this.client,
119
+ const res = await this.client.send(
116
120
  "/schematic/retrieve",
117
121
  args,
118
122
  retrieveArgsZ,
@@ -123,8 +127,7 @@ export class Client {
123
127
  }
124
128
 
125
129
  async delete(keys: Key | Key[]): Promise<void> {
126
- await sendRequired(
127
- this.client,
130
+ await this.client.send(
128
131
  "/schematic/delete",
129
132
  { keys: array.toArray(keys) },
130
133
  deleteReqZ,
@@ -133,13 +136,14 @@ export class Client {
133
136
  }
134
137
 
135
138
  async copy(args: CopyArgs): Promise<Schematic> {
136
- const res = await sendRequired(
137
- this.client,
138
- "/schematic/copy",
139
- args,
140
- copyReqZ,
141
- copyResZ,
142
- );
139
+ const res = await this.client.send("/schematic/copy", args, copyReqZ, copyResZ);
143
140
  return res.schematic;
144
141
  }
145
142
  }
143
+
144
+ export const ZERO_NEW: New = {
145
+ name: "",
146
+ nodes: [],
147
+ edges: [],
148
+ configs: {},
149
+ };
@@ -7,6 +7,8 @@
7
7
  // License, use of this software will be governed by the Apache License, Version 2.0,
8
8
  // included in the file licenses/APL.txt.
9
9
 
10
+ export * from "@/schematic/actions";
11
+ export * from "@/schematic/actions.gen";
10
12
  export * from "@/schematic/client";
11
13
  export * from "@/schematic/symbol";
12
14
  export * from "@/schematic/types.gen";
@@ -11,8 +11,18 @@ import { uuid } from "@synnaxlabs/x";
11
11
  import { describe, expect, it, test } from "vitest";
12
12
 
13
13
  import { NotFoundError, ValidationError } from "@/errors";
14
+ import { schematic } from "@/schematic";
14
15
  import { createTestClient } from "@/testutil/client";
15
16
 
17
+ const newWorkspaceSchematic = async (client: ReturnType<typeof createTestClient>) => {
18
+ const ws = await client.workspaces.create({ name: "dispatch", layout: {} });
19
+ const schem = await client.schematics.create(ws.key, {
20
+ ...schematic.ZERO_NEW,
21
+ name: "dispatch",
22
+ });
23
+ return { ws, schem };
24
+ };
25
+
16
26
  const client = createTestClient();
17
27
 
18
28
  describe("Schematic", () => {
@@ -22,17 +32,14 @@ describe("Schematic", () => {
22
32
  name: "Schematic",
23
33
  layout: { one: 1 },
24
34
  });
25
- const schematic = await client.schematics.create(ws.key, {
35
+ const schem = await client.schematics.create(ws.key, {
36
+ ...schematic.ZERO_NEW,
26
37
  name: "Schematic",
27
- data: { One: 1 },
28
- });
29
- expect(schematic.name).toEqual("Schematic");
30
- expect(schematic.key).not.toEqual(uuid.ZERO);
31
- expect(schematic.data.One).toEqual(1);
32
- const retrieved = await client.schematics.retrieve({
33
- key: schematic.key,
34
38
  });
35
- expect(retrieved.data.One).toEqual(1);
39
+ expect(schem.name).toEqual("Schematic");
40
+ expect(schem.key).not.toEqual(uuid.ZERO);
41
+ const retrieved = await client.schematics.retrieve({ key: schem.key });
42
+ expect(retrieved.key).toEqual(schem.key);
36
43
  });
37
44
  });
38
45
 
@@ -42,33 +49,36 @@ describe("Schematic", () => {
42
49
  name: "Schematic",
43
50
  layout: { one: 1 },
44
51
  });
45
- const schematic = await client.schematics.create(ws.key, {
52
+ const schem = await client.schematics.create(ws.key, {
53
+ ...schematic.ZERO_NEW,
46
54
  name: "Schematic",
47
- data: { one: 1 },
48
- });
49
- await client.schematics.rename(schematic.key, "Schematic2");
50
- const res = await client.schematics.retrieve({
51
- key: schematic.key,
52
55
  });
56
+ await client.schematics.rename(schem.key, "Schematic2");
57
+ const res = await client.schematics.retrieve({ key: schem.key });
53
58
  expect(res.name).toEqual("Schematic2");
54
59
  });
55
60
  });
56
61
 
57
62
  describe("setData", () => {
58
- test("set data", async () => {
63
+ test("set data replaces body fields while preserving key and name", async () => {
59
64
  const ws = await client.workspaces.create({
60
65
  name: "Schematic",
61
66
  layout: { one: 1 },
62
67
  });
63
- const schematic = await client.schematics.create(ws.key, {
68
+ const schem = await client.schematics.create(ws.key, {
69
+ ...schematic.ZERO_NEW,
64
70
  name: "Schematic",
65
- data: { one: 1 },
66
71
  });
67
- await client.schematics.setData(schematic.key, { two: 2 });
68
- const res = await client.schematics.retrieve({
69
- key: schematic.key,
72
+ await client.schematics.setData(schem.key, {
73
+ ...schematic.ZERO_NEW,
74
+ nodes: [{ key: "n1", position: { x: 10, y: 20 }, zIndex: 0 }],
75
+ configs: { n1: { variant: "valve" } },
70
76
  });
71
- expect(res.data.two).toEqual(2);
77
+ const res = await client.schematics.retrieve({ key: schem.key });
78
+ expect(res.name).toEqual("Schematic");
79
+ expect(res.nodes).toHaveLength(1);
80
+ expect(res.nodes[0].key).toEqual("n1");
81
+ expect((res.configs.n1 as Record<string, unknown>).variant).toEqual("valve");
72
82
  });
73
83
  });
74
84
 
@@ -78,57 +88,48 @@ describe("Schematic", () => {
78
88
  name: "Schematic",
79
89
  layout: { one: 1 },
80
90
  });
81
- const schematic = await client.schematics.create(ws.key, {
91
+ const schem = await client.schematics.create(ws.key, {
92
+ ...schematic.ZERO_NEW,
82
93
  name: "Schematic",
83
- data: { one: 1 },
84
94
  });
85
- await client.schematics.delete(schematic.key);
86
- await expect(client.schematics.retrieve({ key: schematic.key })).rejects.toThrow(
95
+ await client.schematics.delete(schem.key);
96
+ await expect(client.schematics.retrieve({ key: schem.key })).rejects.toThrow(
87
97
  NotFoundError,
88
98
  );
89
99
  });
90
100
  });
91
- describe("case preservation", () => {
92
- test("should preserve key casing in data field on create/retrieve cycle", async () => {
93
- const ws = await client.workspaces.create({
94
- name: "CaseTest",
95
- layout: {},
96
- });
97
- const schematic = await client.schematics.create(ws.key, {
98
- name: "CaseTest",
99
- data: {
100
- camelCaseKey: "value1",
101
- PascalCaseKey: "value2",
102
- snake_case_key: "value3",
103
- nested: {
104
- innerCamelCase: 123,
105
- InnerPascalCase: { deepKey: true },
101
+
102
+ describe("config case preservation", () => {
103
+ test("preserves arbitrary key casing within config values", async () => {
104
+ const ws = await client.workspaces.create({ name: "CaseTest", layout: {} });
105
+ const schem = await client.schematics.create(ws.key, {
106
+ ...schematic.ZERO_NEW,
107
+ configs: {
108
+ n1: {
109
+ camelCaseKey: "value1",
110
+ PascalCaseKey: "value2",
111
+ snake_case_key: "value3",
112
+ nested: {
113
+ innerCamelCase: 123,
114
+ InnerPascalCase: { deepKey: true },
115
+ },
106
116
  },
107
117
  },
108
118
  });
109
-
110
- const retrieved = await client.schematics.retrieve({
111
- key: schematic.key,
112
- });
113
-
114
- const data = retrieved.data as Record<string, unknown>;
115
- expect(data.camelCaseKey).toEqual("value1");
116
- expect(data.PascalCaseKey).toEqual("value2");
117
- expect(data.snake_case_key).toEqual("value3");
118
- expect((data.nested as Record<string, unknown>).innerCamelCase).toEqual(123);
119
+ const retrieved = await client.schematics.retrieve({ key: schem.key });
120
+ const config = retrieved.configs.n1 as Record<string, unknown>;
121
+ expect(config.camelCaseKey).toEqual("value1");
122
+ expect(config.PascalCaseKey).toEqual("value2");
123
+ expect(config.snake_case_key).toEqual("value3");
124
+ expect((config.nested as Record<string, unknown>).innerCamelCase).toEqual(123);
119
125
  expect(
120
126
  (
121
- (data.nested as Record<string, unknown>).InnerPascalCase as Record<
127
+ (config.nested as Record<string, unknown>).InnerPascalCase as Record<
122
128
  string,
123
129
  unknown
124
130
  >
125
131
  ).deepKey,
126
132
  ).toEqual(true);
127
- expect(Object.keys(data)).toContain("camelCaseKey");
128
- expect(Object.keys(data)).toContain("PascalCaseKey");
129
- expect(Object.keys(data)).toContain("snake_case_key");
130
- expect(Object.keys(data)).not.toContain("camel_case_key");
131
- expect(Object.keys(data)).not.toContain("pascal_case_key");
132
133
  });
133
134
  });
134
135
 
@@ -138,18 +139,17 @@ describe("Schematic", () => {
138
139
  name: "Schematic",
139
140
  layout: { one: 1 },
140
141
  });
141
- const schematic = await client.schematics.create(ws.key, {
142
+ const schem = await client.schematics.create(ws.key, {
143
+ ...schematic.ZERO_NEW,
142
144
  name: "Schematic",
143
- data: { one: 1 },
144
145
  });
145
- const schematic2 = await client.schematics.copy({
146
- key: schematic.key,
146
+ const schem2 = await client.schematics.copy({
147
+ key: schem.key,
147
148
  name: "Schematic2",
148
149
  snapshot: false,
149
150
  });
150
- expect(schematic2.name).toEqual("Schematic2");
151
- expect(schematic2.key).not.toEqual(uuid.ZERO);
152
- expect(schematic2.data.one).toEqual(1);
151
+ expect(schem2.name).toEqual("Schematic2");
152
+ expect(schem2.key).not.toEqual(uuid.ZERO);
153
153
  });
154
154
 
155
155
  describe("snapshot", () => {
@@ -158,19 +158,183 @@ describe("Schematic", () => {
158
158
  name: "Schematic",
159
159
  layout: { one: 1 },
160
160
  });
161
- const schematic = await client.schematics.create(ws.key, {
161
+ const schem = await client.schematics.create(ws.key, {
162
+ ...schematic.ZERO_NEW,
162
163
  name: "Schematic",
163
- data: { one: 1 },
164
164
  });
165
- const schematic2 = await client.schematics.copy({
166
- key: schematic.key,
165
+ const schem2 = await client.schematics.copy({
166
+ key: schem.key,
167
167
  name: "Schematic2",
168
168
  snapshot: true,
169
169
  });
170
170
  await expect(
171
- client.schematics.setData(schematic2.key, { two: 2 }),
171
+ client.schematics.setData(schem2.key, { ...schematic.ZERO_NEW }),
172
172
  ).rejects.toThrow(ValidationError);
173
173
  });
174
174
  });
175
175
  });
176
+
177
+ describe("dispatch", () => {
178
+ test("setNodePosition moves the matching node", async () => {
179
+ const { schem } = await newWorkspaceSchematic(client);
180
+ await client.schematics.setData(schem.key, {
181
+ ...schematic.ZERO_NEW,
182
+ nodes: [{ key: "n1", position: { x: 0, y: 0 } }],
183
+ });
184
+ await client.schematics.dispatch(schem.key, "sess-1", [
185
+ schematic.setNodePosition({ key: "n1", position: { x: 100, y: 200 } }),
186
+ ]);
187
+ const res = await client.schematics.retrieve({ key: schem.key });
188
+ expect(res.nodes).toHaveLength(1);
189
+ expect(res.nodes[0].position).toEqual({ x: 100, y: 200 });
190
+ });
191
+
192
+ test("setNode inserts a node and writes its config", async () => {
193
+ const { schem } = await newWorkspaceSchematic(client);
194
+ await client.schematics.dispatch(schem.key, "sess-1", [
195
+ schematic.setNode({
196
+ node: { key: "n1", position: { x: 1, y: 2 } },
197
+ config: { label: "Pump" },
198
+ }),
199
+ ]);
200
+ const res = await client.schematics.retrieve({ key: schem.key });
201
+ expect(res.nodes).toHaveLength(1);
202
+ expect(res.nodes[0]).toMatchObject({ key: "n1", position: { x: 1, y: 2 } });
203
+ expect(res.configs.n1.label).toBe("Pump");
204
+ });
205
+
206
+ test("removeNode removes the node and drops its config", async () => {
207
+ const { schem } = await newWorkspaceSchematic(client);
208
+ await client.schematics.setData(schem.key, {
209
+ ...schematic.ZERO_NEW,
210
+ nodes: [
211
+ { key: "n1", position: { x: 0, y: 0 } },
212
+ { key: "n2", position: { x: 1, y: 1 } },
213
+ ],
214
+ configs: { n1: { label: "Pump" }, n2: { label: "Tank" } },
215
+ });
216
+ await client.schematics.dispatch(schem.key, "sess-1", [
217
+ schematic.removeNode({ key: "n1" }),
218
+ ]);
219
+ const res = await client.schematics.retrieve({ key: schem.key });
220
+ expect(res.nodes).toHaveLength(1);
221
+ expect(res.nodes[0]).toMatchObject({ key: "n2", position: { x: 1, y: 1 } });
222
+ expect(res.configs).toEqual({ n2: { label: "Tank" } });
223
+ });
224
+
225
+ test("addEdge appends new edges and is a no-op on duplicate keys", async () => {
226
+ const { schem } = await newWorkspaceSchematic(client);
227
+ const e = (
228
+ key: string,
229
+ srcNode: string,
230
+ srcParam: string,
231
+ tgtNode: string,
232
+ tgtParam: string,
233
+ ) => ({
234
+ key,
235
+ source: { node: srcNode, param: srcParam },
236
+ target: { node: tgtNode, param: tgtParam },
237
+ });
238
+ await client.schematics.setData(schem.key, {
239
+ ...schematic.ZERO_NEW,
240
+ edges: [e("e1", "a", "o", "b", "i"), e("e2", "b", "o", "c", "i")],
241
+ });
242
+ await client.schematics.dispatch(schem.key, "sess-1", [
243
+ schematic.addEdge({ edge: e("e2", "x", "y", "z", "w") }),
244
+ schematic.addEdge({ edge: e("e3", "c", "o", "d", "i") }),
245
+ ]);
246
+ const res = await client.schematics.retrieve({ key: schem.key });
247
+ expect(res.edges).toEqual([
248
+ e("e1", "a", "o", "b", "i"),
249
+ e("e2", "b", "o", "c", "i"),
250
+ e("e3", "c", "o", "d", "i"),
251
+ ]);
252
+ });
253
+
254
+ test("removeEdge removes the matching edge", async () => {
255
+ const { schem } = await newWorkspaceSchematic(client);
256
+ await client.schematics.setData(schem.key, {
257
+ ...schematic.ZERO_NEW,
258
+ edges: [
259
+ {
260
+ key: "e1",
261
+ source: { node: "a", param: "o" },
262
+ target: { node: "b", param: "i" },
263
+ },
264
+ ],
265
+ });
266
+ await client.schematics.dispatch(schem.key, "sess-1", [
267
+ schematic.removeEdge({ key: "e1" }),
268
+ ]);
269
+ const res = await client.schematics.retrieve({ key: schem.key });
270
+ expect(res.edges).toEqual([]);
271
+ });
272
+
273
+ test("setConfig upserts config under the given key", async () => {
274
+ const { schem } = await newWorkspaceSchematic(client);
275
+ await client.schematics.dispatch(schem.key, "sess-1", [
276
+ schematic.setConfig({ key: "n1", config: { label: "Original" } }),
277
+ schematic.setConfig({ key: "n1", config: { label: "Replaced" } }),
278
+ ]);
279
+ const res = await client.schematics.retrieve({ key: schem.key });
280
+ expect(res.configs.n1.label).toBe("Replaced");
281
+ });
282
+
283
+ test("applies a multi-action sequence atomically", async () => {
284
+ const { schem } = await newWorkspaceSchematic(client);
285
+ await client.schematics.dispatch(schem.key, "sess-1", [
286
+ schematic.setNode({ node: { key: "pump", position: { x: 0, y: 0 } } }),
287
+ schematic.setNode({ node: { key: "valve", position: { x: 100, y: 0 } } }),
288
+ schematic.addEdge({
289
+ edge: {
290
+ key: "e1",
291
+ source: { node: "pump", param: "out" },
292
+ target: { node: "valve", param: "in" },
293
+ },
294
+ }),
295
+ schematic.setConfig({ key: "pump", config: { label: "Main Pump" } }),
296
+ ]);
297
+ const res = await client.schematics.retrieve({ key: schem.key });
298
+ expect(res.nodes).toHaveLength(2);
299
+ expect(res.edges).toHaveLength(1);
300
+ expect(res.configs.pump.label).toBe("Main Pump");
301
+ });
302
+
303
+ test("converges to the final position after a 30-action drag storm", async () => {
304
+ const { schem } = await newWorkspaceSchematic(client);
305
+ await client.schematics.setData(schem.key, {
306
+ ...schematic.ZERO_NEW,
307
+ nodes: [{ key: "pump", position: { x: 0, y: 0 } }],
308
+ });
309
+ const actions = Array.from({ length: 30 }, (_, i) =>
310
+ schematic.setNodePosition({ key: "pump", position: { x: i, y: i * 2 } }),
311
+ );
312
+ await client.schematics.dispatch(schem.key, "sess-1", actions);
313
+ const res = await client.schematics.retrieve({ key: schem.key });
314
+ expect(res.nodes[0].position).toEqual({ x: 29, y: 58 });
315
+ });
316
+
317
+ test("preserves arbitrary key casing within config values through dispatch", async () => {
318
+ const { schem } = await newWorkspaceSchematic(client);
319
+ await client.schematics.dispatch(schem.key, "sess-1", [
320
+ schematic.setConfig({
321
+ key: "n1",
322
+ config: {
323
+ camelCaseKey: "v1",
324
+ PascalCaseKey: "v2",
325
+ snake_case_key: "v3",
326
+ nested: { innerCamelCase: 1, InnerPascalCase: { deepKey: true } },
327
+ },
328
+ }),
329
+ ]);
330
+ const res = await client.schematics.retrieve({ key: schem.key });
331
+ const config = res.configs.n1;
332
+ expect(config.camelCaseKey).toBe("v1");
333
+ expect(config.PascalCaseKey).toBe("v2");
334
+ expect(config.snake_case_key).toBe("v3");
335
+ const nested = config.nested as Record<string, unknown>;
336
+ expect(nested.innerCamelCase).toBe(1);
337
+ expect((nested.InnerPascalCase as Record<string, unknown>).deepKey).toBe(true);
338
+ });
339
+ });
176
340
  });