@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
@@ -11,6 +11,7 @@ import { uuid } from "@synnaxlabs/x";
11
11
  import { describe, expect, test } from "vitest";
12
12
 
13
13
  import { NotFoundError } from "@/errors";
14
+ import { table } from "@/table";
14
15
  import { createTestClient } from "@/testutil/client";
15
16
 
16
17
  const client = createTestClient();
@@ -19,88 +20,632 @@ describe("Table", () => {
19
20
  describe("create", () => {
20
21
  test("create one", async () => {
21
22
  const ws = await client.workspaces.create({ name: "Table", layout: { one: 1 } });
22
- const table = await client.tables.create(ws.key, {
23
+ const t = await client.tables.create(ws.key, {
23
24
  name: "Table",
24
- data: { one: 1 },
25
25
  });
26
- expect(table.name).toEqual("Table");
27
- expect(table.key).not.toEqual(uuid.ZERO);
28
- expect(table.data.one).toEqual(1);
26
+ expect(t.name).toEqual("Table");
27
+ expect(t.key).not.toEqual(uuid.ZERO);
28
+ const retrieved = await client.tables.retrieve({ key: t.key });
29
+ expect(retrieved.key).toEqual(t.key);
29
30
  });
30
31
  });
32
+
31
33
  describe("rename", () => {
32
34
  test("rename one", async () => {
33
35
  const ws = await client.workspaces.create({ name: "Table", layout: { one: 1 } });
34
- const table = await client.tables.create(ws.key, {
36
+ const t = await client.tables.create(ws.key, {
35
37
  name: "Table",
36
- data: { one: 1 },
37
38
  });
38
- await client.tables.rename(table.key, "Table2");
39
- const res = await client.tables.retrieve({ key: table.key });
39
+ await client.tables.rename(t.key, "Table2");
40
+ const res = await client.tables.retrieve({ key: t.key });
40
41
  expect(res.name).toEqual("Table2");
41
42
  });
42
43
  });
44
+
43
45
  describe("setData", () => {
44
- test("set data", async () => {
46
+ test("set data replaces body fields while preserving key and name", async () => {
45
47
  const ws = await client.workspaces.create({ name: "Table", layout: { one: 1 } });
46
- const table = await client.tables.create(ws.key, {
48
+ const t = await client.tables.create(ws.key, {
47
49
  name: "Table",
48
- data: { one: 1 },
49
50
  });
50
- await client.tables.setData(table.key, { two: 2 });
51
- const res = await client.tables.retrieve({ key: table.key });
52
- expect(res.data.two).toEqual(2);
51
+ await client.tables.setData(t.key, {
52
+ rows: [{ size: 40, cells: ["a", "b"] }],
53
+ columns: [{ size: 80 }, { size: 100 }],
54
+ cells: {
55
+ a: { key: "a", variant: "text", props: { value: "hello" } },
56
+ b: { key: "b", variant: "value", props: { units: "psi" } },
57
+ },
58
+ });
59
+ const res = await client.tables.retrieve({ key: t.key });
60
+ expect(res.name).toEqual("Table");
61
+ expect(res.rows).toHaveLength(1);
62
+ expect(res.rows[0].cells).toEqual(["a", "b"]);
63
+ expect(res.columns).toHaveLength(2);
64
+ expect(res.cells.a.variant).toEqual("text");
65
+ expect((res.cells.a.props as Record<string, unknown>).value).toEqual("hello");
66
+ expect(res.cells.b.variant).toEqual("value");
53
67
  });
54
68
  });
69
+
55
70
  describe("delete", () => {
56
71
  test("delete one", async () => {
57
72
  const ws = await client.workspaces.create({ name: "Table", layout: { one: 1 } });
58
- const table = await client.tables.create(ws.key, {
73
+ const t = await client.tables.create(ws.key, {
59
74
  name: "Table",
60
- data: { one: 1 },
61
75
  });
62
- await client.tables.delete(table.key);
63
- await expect(client.tables.retrieve({ key: table.key })).rejects.toThrow(
76
+ await client.tables.delete(t.key);
77
+ await expect(client.tables.retrieve({ key: t.key })).rejects.toThrow(
64
78
  NotFoundError,
65
79
  );
66
80
  });
67
81
  });
68
- describe("case preservation", () => {
69
- test("should preserve key casing in data field on create/retrieve cycle", async () => {
82
+
83
+ describe("cell props case preservation", () => {
84
+ test("preserves arbitrary key casing within cell props values", async () => {
70
85
  const ws = await client.workspaces.create({ name: "CaseTest", layout: {} });
71
- const table = await client.tables.create(ws.key, {
86
+ const t = await client.tables.create(ws.key, {
72
87
  name: "CaseTest",
73
- data: {
74
- camelCaseKey: "value1",
75
- PascalCaseKey: "value2",
76
- snake_case_key: "value3",
77
- nested: {
78
- innerCamelCase: 123,
79
- InnerPascalCase: { deepKey: true },
88
+ cells: {
89
+ a: {
90
+ key: "a",
91
+ variant: "value",
92
+ props: {
93
+ camelCaseKey: "value1",
94
+ PascalCaseKey: "value2",
95
+ snake_case_key: "value3",
96
+ nested: {
97
+ innerCamelCase: 123,
98
+ InnerPascalCase: { deepKey: true },
99
+ },
100
+ },
80
101
  },
81
102
  },
82
103
  });
83
-
84
- const retrieved = await client.tables.retrieve({ key: table.key });
85
-
86
- const data = retrieved.data as Record<string, unknown>;
87
- expect(data.camelCaseKey).toEqual("value1");
88
- expect(data.PascalCaseKey).toEqual("value2");
89
- expect(data.snake_case_key).toEqual("value3");
90
- expect((data.nested as Record<string, unknown>).innerCamelCase).toEqual(123);
104
+ const retrieved = await client.tables.retrieve({ key: t.key });
105
+ const props = retrieved.cells.a.props as Record<string, unknown>;
106
+ expect(props.camelCaseKey).toEqual("value1");
107
+ expect(props.PascalCaseKey).toEqual("value2");
108
+ expect(props.snake_case_key).toEqual("value3");
109
+ expect((props.nested as Record<string, unknown>).innerCamelCase).toEqual(123);
91
110
  expect(
92
111
  (
93
- (data.nested as Record<string, unknown>).InnerPascalCase as Record<
112
+ (props.nested as Record<string, unknown>).InnerPascalCase as Record<
94
113
  string,
95
114
  unknown
96
115
  >
97
116
  ).deepKey,
98
117
  ).toEqual(true);
99
- expect(Object.keys(data)).toContain("camelCaseKey");
100
- expect(Object.keys(data)).toContain("PascalCaseKey");
101
- expect(Object.keys(data)).toContain("snake_case_key");
102
- expect(Object.keys(data)).not.toContain("camel_case_key");
103
- expect(Object.keys(data)).not.toContain("pascal_case_key");
118
+ expect(Object.keys(props)).toContain("camelCaseKey");
119
+ expect(Object.keys(props)).toContain("PascalCaseKey");
120
+ expect(Object.keys(props)).toContain("snake_case_key");
121
+ });
122
+ });
123
+
124
+ describe("dispatch", () => {
125
+ const seed = async () => {
126
+ const ws = await client.workspaces.create({ name: "Dispatch", layout: {} });
127
+ return client.tables.create(ws.key, {
128
+ name: "Dispatch",
129
+ rows: [{ size: 30, cells: ["a", "b"] }],
130
+ columns: [{ size: 80 }, { size: 100 }],
131
+ cells: {
132
+ a: { key: "a", variant: "text", props: { value: "A" } },
133
+ b: { key: "b", variant: "text", props: { value: "B" } },
134
+ },
135
+ });
136
+ };
137
+
138
+ test("rename applies via dispatch", async () => {
139
+ const t = await seed();
140
+ await client.tables.dispatch(t.key, "dk-1", [table.rename({ name: "renamed" })]);
141
+ const res = await client.tables.retrieve({ key: t.key });
142
+ expect(res.name).toEqual("renamed");
143
+ });
144
+
145
+ test("addRow appends a row and its cells", async () => {
146
+ const t = await seed();
147
+ await client.tables.dispatch(t.key, "dk-1", [
148
+ table.addRow({
149
+ index: 1,
150
+ size: 40,
151
+ cells: [
152
+ { key: "c", variant: "text", props: { value: "C" } },
153
+ { key: "d", variant: "text", props: { value: "D" } },
154
+ ],
155
+ }),
156
+ ]);
157
+ const res = await client.tables.retrieve({ key: t.key });
158
+ expect(res.rows).toHaveLength(2);
159
+ expect(res.rows[1].cells).toEqual(["c", "d"]);
160
+ expect(res.cells.c.props.value).toEqual("C");
161
+ });
162
+
163
+ test("removeRow drops the row and its cells", async () => {
164
+ const t = await seed();
165
+ await client.tables.dispatch(t.key, "dk-1", [table.removeRow({ index: 0 })]);
166
+ const res = await client.tables.retrieve({ key: t.key });
167
+ expect(res.rows).toEqual([]);
168
+ expect(Object.keys(res.cells)).toEqual([]);
169
+ });
170
+
171
+ test("addCol inserts a column at the given index across every row", async () => {
172
+ const t = await seed();
173
+ await client.tables.dispatch(t.key, "dk-1", [
174
+ table.addCol({
175
+ index: 1,
176
+ size: 90,
177
+ cells: [{ key: "mid-1", variant: "text", props: { value: "M1" } }],
178
+ }),
179
+ ]);
180
+ const res = await client.tables.retrieve({ key: t.key });
181
+ expect(res.columns).toHaveLength(3);
182
+ expect(res.rows[0].cells).toEqual(["a", "mid-1", "b"]);
183
+ });
184
+
185
+ test("removeCol drops every cell in that column", async () => {
186
+ const t = await seed();
187
+ await client.tables.dispatch(t.key, "dk-1", [table.removeCol({ index: 0 })]);
188
+ const res = await client.tables.retrieve({ key: t.key });
189
+ expect(res.columns).toHaveLength(1);
190
+ expect(res.rows[0].cells).toEqual(["b"]);
191
+ expect(res.cells.a).toBeUndefined();
192
+ });
193
+
194
+ test("resize actions update size in place", async () => {
195
+ const t = await seed();
196
+ await client.tables.dispatch(t.key, "dk-1", [
197
+ table.resizeRow({ index: 0, size: 55 }),
198
+ table.resizeCol({ index: 1, size: 200 }),
199
+ ]);
200
+ const res = await client.tables.retrieve({ key: t.key });
201
+ expect(res.rows[0].size).toEqual(55);
202
+ expect(res.columns[1].size).toEqual(200);
203
+ });
204
+
205
+ test("setCell replaces an existing cell", async () => {
206
+ const t = await seed();
207
+ await client.tables.dispatch(t.key, "dk-1", [
208
+ table.setCell({
209
+ cell: { key: "a", variant: "value", props: { units: "psi" } },
210
+ }),
211
+ ]);
212
+ const res = await client.tables.retrieve({ key: t.key });
213
+ expect(res.cells.a.variant).toEqual("value");
214
+ expect(res.cells.a.props.units).toEqual("psi");
215
+ });
216
+
217
+ test("setCell is a no-op for an unknown key", async () => {
218
+ const t = await seed();
219
+ await client.tables.dispatch(t.key, "dk-1", [
220
+ table.setCell({ cell: { key: "ghost", variant: "text", props: {} } }),
221
+ ]);
222
+ const res = await client.tables.retrieve({ key: t.key });
223
+ expect(res.cells.ghost).toBeUndefined();
224
+ expect(Object.keys(res.cells).sort()).toEqual(["a", "b"]);
225
+ });
226
+
227
+ test("multi-action sequence applies atomically", async () => {
228
+ const t = await seed();
229
+ await client.tables.dispatch(t.key, "dk-1", [
230
+ table.rename({ name: "multi" }),
231
+ table.addRow({
232
+ index: 1,
233
+ size: 40,
234
+ cells: [
235
+ { key: "c", variant: "text", props: {} },
236
+ { key: "d", variant: "text", props: {} },
237
+ ],
238
+ }),
239
+ table.setCell({
240
+ cell: { key: "c", variant: "value", props: { telem: "ch1" } },
241
+ }),
242
+ ]);
243
+ const res = await client.tables.retrieve({ key: t.key });
244
+ expect(res.name).toEqual("multi");
245
+ expect(res.rows).toHaveLength(2);
246
+ expect(res.cells.c.variant).toEqual("value");
247
+ expect(res.cells.c.props.telem).toEqual("ch1");
248
+ });
249
+ });
250
+
251
+ describe("reduceAll", () => {
252
+ const KEY = "00000000-0000-0000-0000-000000000001";
253
+ const TEMPLATE_KEY = "11111111-2222-4333-8444-555555555555";
254
+ const DERIVED_KEY_0 = "11111111-2222-4333-8444-555555550000";
255
+ const DERIVED_KEY_1 = "11111111-2222-4333-8444-555555550001";
256
+ const textTemplate: table.CellTemplate = { variant: "text", props: {} };
257
+
258
+ const emptyState = (name = "t"): table.Table => ({
259
+ key: KEY,
260
+ name,
261
+ rows: [],
262
+ columns: [],
263
+ cells: {},
264
+ });
265
+
266
+ const seed2x2 = (): table.Table => ({
267
+ key: KEY,
268
+ name: "before",
269
+ rows: [
270
+ { size: 36, cells: ["a", "b"] },
271
+ { size: 36, cells: ["c", "d"] },
272
+ ],
273
+ columns: [{ size: 80 }, { size: 100 }],
274
+ cells: {
275
+ a: { key: "a", variant: "text", props: { value: "A" } },
276
+ b: { key: "b", variant: "text", props: { value: "B" } },
277
+ c: { key: "c", variant: "text", props: { value: "C" } },
278
+ d: { key: "d", variant: "text", props: { value: "D" } },
279
+ },
280
+ });
281
+
282
+ const seed3x3 = (): table.Table => {
283
+ const keys = ["a", "b", "c", "d", "e", "f", "g", "h", "i"];
284
+ return {
285
+ key: KEY,
286
+ name: "t",
287
+ rows: [
288
+ { size: 36, cells: ["a", "b", "c"] },
289
+ { size: 36, cells: ["d", "e", "f"] },
290
+ { size: 36, cells: ["g", "h", "i"] },
291
+ ],
292
+ columns: [{ size: 80 }, { size: 80 }, { size: 80 }],
293
+ cells: Object.fromEntries(
294
+ keys.map((k) => [k, { key: k, variant: "value", props: { units: "psi" } }]),
295
+ ),
296
+ };
297
+ };
298
+
299
+ describe("inverses", () => {
300
+ test("rename produces a rename inverse with the old name", () => {
301
+ const { next, inverse } = table.reduceAll(emptyState("before"), [
302
+ table.rename({ name: "after" }),
303
+ ]);
304
+ expect(next.name).toEqual("after");
305
+ expect(inverse).toHaveLength(1);
306
+ expect(inverse[0].type).toEqual("rename");
307
+ expect(inverse[0].type === "rename" ? inverse[0].rename.name : null).toEqual(
308
+ "before",
309
+ );
310
+ });
311
+
312
+ test("addRow inverse is a removeRow at the inserted index", () => {
313
+ const { inverse } = table.reduceAll(emptyState(), [
314
+ table.addRow({
315
+ index: 0,
316
+ size: 30,
317
+ cells: [{ key: "x", variant: "text", props: {} }],
318
+ }),
319
+ ]);
320
+ expect(inverse).toHaveLength(1);
321
+ expect(inverse[0].type).toEqual("remove_row");
322
+ });
323
+
324
+ test("setCell inverse restores the previous cell value", () => {
325
+ const { next, inverse } = table.reduceAll(
326
+ {
327
+ ...emptyState(),
328
+ rows: [{ size: 30, cells: ["a"] }],
329
+ columns: [{ size: 80 }],
330
+ cells: { a: { key: "a", variant: "text", props: { v: 1 } } },
331
+ },
332
+ [table.setCell({ cell: { key: "a", variant: "value", props: { v: 2 } } })],
333
+ );
334
+ expect(next.cells.a.variant).toEqual("value");
335
+ expect(inverse).toHaveLength(1);
336
+ expect(inverse[0].type).toEqual("set_cell");
337
+ if (inverse[0].type === "set_cell") {
338
+ expect(inverse[0].setCell.cell.variant).toEqual("text");
339
+ expect(inverse[0].setCell.cell.props).toEqual({ v: 1 });
340
+ }
341
+ });
342
+
343
+ test("setCell on an unknown key returns no inverse", () => {
344
+ const { inverse } = table.reduceAll(emptyState(), [
345
+ table.setCell({ cell: { key: "ghost", variant: "text", props: {} } }),
346
+ ]);
347
+ expect(inverse).toEqual([]);
348
+ });
349
+ });
350
+
351
+ describe("bootstrap", () => {
352
+ test("addRow on an empty table bootstraps one default column per cell", () => {
353
+ const { next } = table.reduceAll(emptyState(), [
354
+ table.addRow({
355
+ index: 0,
356
+ size: 36,
357
+ cells: [
358
+ { key: "a", variant: "text", props: {} },
359
+ { key: "b", variant: "text", props: {} },
360
+ ],
361
+ }),
362
+ ]);
363
+ expect(next.rows).toHaveLength(1);
364
+ expect(next.columns).toHaveLength(2);
365
+ expect(next.columns[0].size).toEqual(72);
366
+ expect(next.rows[0].cells).toEqual(["a", "b"]);
367
+ });
368
+
369
+ test("addCol on an empty table bootstraps one default row per cell", () => {
370
+ const { next } = table.reduceAll(emptyState(), [
371
+ table.addCol({
372
+ index: 0,
373
+ size: 72,
374
+ cells: [
375
+ { key: "a", variant: "text", props: {} },
376
+ { key: "b", variant: "text", props: {} },
377
+ ],
378
+ }),
379
+ ]);
380
+ expect(next.columns).toHaveLength(1);
381
+ expect(next.rows).toHaveLength(2);
382
+ expect(next.rows[0].size).toEqual(36);
383
+ expect(next.rows[0].cells).toEqual(["a"]);
384
+ expect(next.rows[1].cells).toEqual(["b"]);
385
+ });
386
+ });
387
+
388
+ describe("cellTemplate", () => {
389
+ const template = { key: TEMPLATE_KEY, variant: "text", props: { value: "t" } };
390
+
391
+ test("addRow replicates the template across existing columns", () => {
392
+ const { next } = table.reduceAll(
393
+ {
394
+ ...emptyState(),
395
+ rows: [{ size: 36, cells: ["a", "b"] }],
396
+ columns: [{ size: 80 }, { size: 80 }],
397
+ cells: {
398
+ a: { key: "a", variant: "text", props: {} },
399
+ b: { key: "b", variant: "text", props: {} },
400
+ },
401
+ },
402
+ [table.addRow({ index: 1, size: 36, cells: [], cellTemplate: template })],
403
+ );
404
+ expect(next.rows[1].cells).toEqual([DERIVED_KEY_0, DERIVED_KEY_1]);
405
+ expect(next.cells[DERIVED_KEY_0].variant).toEqual("text");
406
+ expect(next.cells[DERIVED_KEY_0].props).toEqual({ value: "t" });
407
+ });
408
+
409
+ test("addCol replicates the template across existing rows", () => {
410
+ const { next } = table.reduceAll(
411
+ {
412
+ ...emptyState(),
413
+ rows: [
414
+ { size: 36, cells: ["a"] },
415
+ { size: 36, cells: ["b"] },
416
+ ],
417
+ columns: [{ size: 80 }],
418
+ cells: {
419
+ a: { key: "a", variant: "text", props: {} },
420
+ b: { key: "b", variant: "text", props: {} },
421
+ },
422
+ },
423
+ [table.addCol({ index: 1, size: 80, cells: [], cellTemplate: template })],
424
+ );
425
+ expect(next.rows[0].cells).toEqual(["a", DERIVED_KEY_0]);
426
+ expect(next.rows[1].cells).toEqual(["b", DERIVED_KEY_1]);
427
+ });
428
+
429
+ test("addCol with template adds an empty column when rows are empty", () => {
430
+ const { next } = table.reduceAll(
431
+ { ...emptyState(), columns: [{ size: 80 }, { size: 80 }] },
432
+ [table.addCol({ index: 2, size: 72, cells: [], cellTemplate: template })],
433
+ );
434
+ expect(next.rows).toHaveLength(0);
435
+ expect(next.columns).toHaveLength(3);
436
+ expect(next.cells).toEqual({});
437
+ });
438
+
439
+ test("addRow with template adds an empty row when columns are empty", () => {
440
+ const { next } = table.reduceAll(
441
+ { ...emptyState(), rows: [{ size: 36, cells: [] }] },
442
+ [table.addRow({ index: 1, size: 36, cells: [], cellTemplate: template })],
443
+ );
444
+ expect(next.rows).toHaveLength(2);
445
+ expect(next.rows[1].cells).toEqual([]);
446
+ expect(next.cells).toEqual({});
447
+ });
448
+
449
+ test("addRow with template on an empty table seeds a 1x1 grid", () => {
450
+ const { next } = table.reduceAll(emptyState(), [
451
+ table.addRow({ index: 0, size: 36, cells: [], cellTemplate: template }),
452
+ ]);
453
+ expect(next.columns).toHaveLength(1);
454
+ expect(next.rows[0].cells).toEqual([DERIVED_KEY_0]);
455
+ });
456
+ });
457
+
458
+ describe("size clamping", () => {
459
+ const MIN = 32;
460
+ const baseWithOneCol = {
461
+ ...emptyState(),
462
+ columns: [{ size: 80 }],
463
+ };
464
+ const baseWithOneCell: table.Table = {
465
+ ...emptyState(),
466
+ rows: [{ size: 60, cells: ["a"] }],
467
+ columns: [{ size: 80 }],
468
+ cells: { a: { key: "a", variant: "text", props: {} } },
469
+ };
470
+ test.for<{
471
+ name: string;
472
+ state: table.Table;
473
+ action: table.Action;
474
+ get: (t: table.Table) => number;
475
+ }>([
476
+ {
477
+ name: "addRow size below minimum",
478
+ state: baseWithOneCol,
479
+ action: table.addRow({
480
+ index: 0,
481
+ size: 5,
482
+ cells: [{ key: "a", variant: "text", props: {} }],
483
+ }),
484
+ get: (t) => t.rows[0].size,
485
+ },
486
+ {
487
+ name: "addCol size below minimum",
488
+ state: baseWithOneCell,
489
+ action: table.addCol({
490
+ index: 1,
491
+ size: 10,
492
+ cells: [{ key: "b", variant: "text", props: {} }],
493
+ }),
494
+ get: (t) => t.columns[1].size,
495
+ },
496
+ {
497
+ name: "resizeRow size below minimum",
498
+ state: baseWithOneCell,
499
+ action: table.resizeRow({ index: 0, size: 5 }),
500
+ get: (t) => t.rows[0].size,
501
+ },
502
+ {
503
+ name: "resizeCol size below minimum",
504
+ state: baseWithOneCell,
505
+ action: table.resizeCol({ index: 0, size: 0 }),
506
+ get: (t) => t.columns[0].size,
507
+ },
508
+ ])("clamps $name to the floor", ({ state, action, get }) => {
509
+ const { next } = table.reduceAll(state, [action]);
510
+ expect(get(next)).toEqual(MIN);
511
+ });
512
+ });
513
+
514
+ describe("eraseCells", () => {
515
+ test.for<{
516
+ name: string;
517
+ cells: string[];
518
+ rows: number;
519
+ columns: number;
520
+ row0?: string[];
521
+ }>([
522
+ {
523
+ name: "partial selection leaves axes intact",
524
+ cells: ["b", "e"],
525
+ rows: 3,
526
+ columns: 3,
527
+ row0: ["a", "b", "c"],
528
+ },
529
+ {
530
+ name: "fully-selected row is removed",
531
+ cells: ["d", "e", "f"],
532
+ rows: 2,
533
+ columns: 3,
534
+ row0: ["a", "b", "c"],
535
+ },
536
+ {
537
+ name: "fully-selected column is removed",
538
+ cells: ["b", "e", "h"],
539
+ rows: 3,
540
+ columns: 2,
541
+ row0: ["a", "c"],
542
+ },
543
+ {
544
+ name: "full row + full column in one call",
545
+ cells: ["a", "b", "c", "f", "i"],
546
+ rows: 2,
547
+ columns: 2,
548
+ row0: ["d", "e"],
549
+ },
550
+ ])("$name", ({ cells, rows, columns, row0 }) => {
551
+ const { next } = table.reduceAll(seed3x3(), [
552
+ table.eraseCells({ cells, template: textTemplate }),
553
+ ]);
554
+ expect(next.rows).toHaveLength(rows);
555
+ expect(next.columns).toHaveLength(columns);
556
+ if (row0 != null) expect(next.rows[0].cells).toEqual(row0);
557
+ });
558
+
559
+ test("resets surviving cells to the template variant", () => {
560
+ const { next } = table.reduceAll(seed3x3(), [
561
+ table.eraseCells({ cells: ["b", "e"], template: textTemplate }),
562
+ ]);
563
+ expect(next.cells.b.variant).toEqual("text");
564
+ expect(next.cells.e.variant).toEqual("text");
565
+ expect(next.cells.a.variant).toEqual("value");
566
+ });
567
+
568
+ test("drops cells from the map when their row is fully removed", () => {
569
+ const { next } = table.reduceAll(seed3x3(), [
570
+ table.eraseCells({ cells: ["d", "e", "f"], template: textTemplate }),
571
+ ]);
572
+ for (const k of ["d", "e", "f"]) expect(next.cells[k]).toBeUndefined();
573
+ });
574
+
575
+ test("inverse restores the prior state on undo", () => {
576
+ const seed = seed3x3();
577
+ const { next, inverse } = table.reduceAll(seed, [
578
+ table.eraseCells({
579
+ cells: ["a", "b", "c", "e"],
580
+ template: textTemplate,
581
+ }),
582
+ ]);
583
+ expect(next.rows).toHaveLength(2);
584
+ const { next: restored } = table.reduceAll(next, inverse);
585
+ expect(restored).toEqual(seed);
586
+ });
587
+
588
+ test("empty selection returns no inverse", () => {
589
+ const { inverse } = table.reduceAll(seed3x3(), [
590
+ table.eraseCells({ cells: [], template: textTemplate }),
591
+ ]);
592
+ expect(inverse).toEqual([]);
593
+ });
594
+
595
+ test("inverse round-trips when payload.cells contains duplicates", () => {
596
+ const seed = seed3x3();
597
+ const { next, inverse } = table.reduceAll(seed, [
598
+ table.eraseCells({ cells: ["b", "b", "b"], template: textTemplate }),
599
+ ]);
600
+ expect(next.cells.b.variant).toEqual("text");
601
+ const { next: restored } = table.reduceAll(next, inverse);
602
+ expect(restored).toEqual(seed);
603
+ });
604
+ });
605
+
606
+ describe("round-trip", () => {
607
+ const roundTrip = (forward: table.Action) => {
608
+ const original = seed2x2();
609
+ const { next, inverse } = table.reduceAll(original, [forward]);
610
+ const { next: restored } = table.reduceAll(next, inverse);
611
+ expect(restored).toEqual(original);
612
+ };
613
+
614
+ test.for<{ name: string; action: table.Action }>([
615
+ { name: "rename", action: table.rename({ name: "after" }) },
616
+ {
617
+ name: "addRow",
618
+ action: table.addRow({
619
+ index: 1,
620
+ size: 40,
621
+ cells: [
622
+ { key: "e", variant: "text", props: { value: "E" } },
623
+ { key: "f", variant: "text", props: { value: "F" } },
624
+ ],
625
+ }),
626
+ },
627
+ { name: "removeRow", action: table.removeRow({ index: 0 }) },
628
+ {
629
+ name: "addCol",
630
+ action: table.addCol({
631
+ index: 1,
632
+ size: 60,
633
+ cells: [
634
+ { key: "m1", variant: "text", props: { value: "M1" } },
635
+ { key: "m2", variant: "value", props: { units: "psi" } },
636
+ ],
637
+ }),
638
+ },
639
+ { name: "removeCol", action: table.removeCol({ index: 0 }) },
640
+ { name: "resizeRow", action: table.resizeRow({ index: 0, size: 55 }) },
641
+ { name: "resizeCol", action: table.resizeCol({ index: 1, size: 200 }) },
642
+ {
643
+ name: "setCell",
644
+ action: table.setCell({
645
+ cell: { key: "a", variant: "value", props: { units: "psi" } },
646
+ }),
647
+ },
648
+ ])("$name round-trips", ({ action }) => roundTrip(action));
104
649
  });
105
650
  });
106
651
  });