@synnaxlabs/client 0.55.0 → 0.56.1

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 (197) hide show
  1. package/.turbo/turbo-build.log +10 -13
  2. package/dist/client.cjs +60 -36
  3. package/dist/client.js +6435 -4786
  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/client.d.ts.map +1 -1
  19. package/dist/src/arc/compiler/types.gen.d.ts +1 -1
  20. package/dist/src/arc/compiler/types.gen.d.ts.map +1 -1
  21. package/dist/src/arc/graph/types.gen.d.ts +29 -29
  22. package/dist/src/arc/graph/types.gen.d.ts.map +1 -1
  23. package/dist/src/arc/ir/types.gen.d.ts +123 -123
  24. package/dist/src/arc/ir/types.gen.d.ts.map +1 -1
  25. package/dist/src/arc/module/types.gen.d.ts +45 -45
  26. package/dist/src/arc/program/types.gen.d.ts +45 -45
  27. package/dist/src/arc/types/types.gen.d.ts +11 -11
  28. package/dist/src/arc/types/types.gen.d.ts.map +1 -1
  29. package/dist/src/arc/types.gen.d.ts +99 -99
  30. package/dist/src/auth/auth.d.ts +3 -3
  31. package/dist/src/auth/auth.d.ts.map +1 -1
  32. package/dist/src/channel/client.d.ts +2 -2
  33. package/dist/src/channel/client.d.ts.map +1 -1
  34. package/dist/src/channel/retriever.d.ts +5 -8
  35. package/dist/src/channel/retriever.d.ts.map +1 -1
  36. package/dist/src/channel/types.gen.d.ts +3 -3
  37. package/dist/src/channel/writer.d.ts.map +1 -1
  38. package/dist/src/connection/checker.d.ts +1 -1
  39. package/dist/src/connection/checker.d.ts.map +1 -1
  40. package/dist/src/device/client.d.ts.map +1 -1
  41. package/dist/src/device/types.gen.d.ts +6 -8
  42. package/dist/src/device/types.gen.d.ts.map +1 -1
  43. package/dist/src/errors.d.ts +2 -0
  44. package/dist/src/errors.d.ts.map +1 -1
  45. package/dist/src/framer/adapter.d.ts.map +1 -1
  46. package/dist/src/framer/client.d.ts +2 -2
  47. package/dist/src/framer/codec.d.ts +9 -1
  48. package/dist/src/framer/codec.d.ts.map +1 -1
  49. package/dist/src/framer/deleter.d.ts.map +1 -1
  50. package/dist/src/framer/frame.d.ts +1 -1
  51. package/dist/src/framer/iterator.d.ts +84 -3
  52. package/dist/src/framer/iterator.d.ts.map +1 -1
  53. package/dist/src/framer/streamProxy.d.ts.map +1 -1
  54. package/dist/src/framer/streamer.d.ts +1 -3
  55. package/dist/src/framer/streamer.d.ts.map +1 -1
  56. package/dist/src/framer/types.gen.d.ts +18 -0
  57. package/dist/src/framer/types.gen.d.ts.map +1 -1
  58. package/dist/src/framer/writer.d.ts +8 -8
  59. package/dist/src/framer/writer.d.ts.map +1 -1
  60. package/dist/src/group/client.d.ts +1 -2
  61. package/dist/src/group/client.d.ts.map +1 -1
  62. package/dist/src/group/types.gen.d.ts +2 -2
  63. package/dist/src/index.d.ts +2 -1
  64. package/dist/src/index.d.ts.map +1 -1
  65. package/dist/src/label/client.d.ts +5 -8
  66. package/dist/src/label/client.d.ts.map +1 -1
  67. package/dist/src/lineplot/client.d.ts.map +1 -1
  68. package/dist/src/lineplot/types.gen.d.ts +2 -2
  69. package/dist/src/log/client.d.ts.map +1 -1
  70. package/dist/src/log/types.gen.d.ts +2 -2
  71. package/dist/src/ontology/client.d.ts +1 -3
  72. package/dist/src/ontology/client.d.ts.map +1 -1
  73. package/dist/src/ontology/payload.d.ts +12 -16
  74. package/dist/src/ontology/payload.d.ts.map +1 -1
  75. package/dist/src/ontology/types.gen.d.ts +1 -2
  76. package/dist/src/ontology/types.gen.d.ts.map +1 -1
  77. package/dist/src/ontology/writer.d.ts +5 -10
  78. package/dist/src/ontology/writer.d.ts.map +1 -1
  79. package/dist/src/rack/client.d.ts.map +1 -1
  80. package/dist/src/rack/types.gen.d.ts +3 -3
  81. package/dist/src/ranger/alias/client.d.ts.map +1 -1
  82. package/dist/src/ranger/client.d.ts.map +1 -1
  83. package/dist/src/ranger/kv/client.d.ts.map +1 -1
  84. package/dist/src/ranger/types.gen.d.ts +6 -6
  85. package/dist/src/ranger/types.gen.d.ts.map +1 -1
  86. package/dist/src/ranger/writer.d.ts +2 -3
  87. package/dist/src/ranger/writer.d.ts.map +1 -1
  88. package/dist/src/schematic/actions.d.ts +147 -0
  89. package/dist/src/schematic/actions.d.ts.map +1 -0
  90. package/dist/src/schematic/actions.gen.d.ts +484 -0
  91. package/dist/src/schematic/actions.gen.d.ts.map +1 -0
  92. package/dist/src/schematic/actions.spec.d.ts +2 -0
  93. package/dist/src/schematic/actions.spec.d.ts.map +1 -0
  94. package/dist/src/schematic/client.d.ts +53 -2
  95. package/dist/src/schematic/client.d.ts.map +1 -1
  96. package/dist/src/schematic/external.d.ts +2 -0
  97. package/dist/src/schematic/external.d.ts.map +1 -1
  98. package/dist/src/schematic/symbol/client.d.ts.map +1 -1
  99. package/dist/src/schematic/symbol/types.gen.d.ts +48 -58
  100. package/dist/src/schematic/symbol/types.gen.d.ts.map +1 -1
  101. package/dist/src/schematic/types.gen.d.ts +131 -5
  102. package/dist/src/schematic/types.gen.d.ts.map +1 -1
  103. package/dist/src/status/client.d.ts.map +1 -1
  104. package/dist/src/status/payload.d.ts +3 -3
  105. package/dist/src/table/actions.d.ts +156 -0
  106. package/dist/src/table/actions.d.ts.map +1 -0
  107. package/dist/src/table/actions.gen.d.ts +587 -0
  108. package/dist/src/table/actions.gen.d.ts.map +1 -0
  109. package/dist/src/table/client.d.ts +28 -2
  110. package/dist/src/table/client.d.ts.map +1 -1
  111. package/dist/src/table/external.d.ts +2 -0
  112. package/dist/src/table/external.d.ts.map +1 -1
  113. package/dist/src/table/types.gen.d.ts +71 -4
  114. package/dist/src/table/types.gen.d.ts.map +1 -1
  115. package/dist/src/task/client.d.ts.map +1 -1
  116. package/dist/src/task/types.gen.d.ts +7 -7
  117. package/dist/src/task/types.gen.d.ts.map +1 -1
  118. package/dist/src/user/client.d.ts +2 -2
  119. package/dist/src/user/client.d.ts.map +1 -1
  120. package/dist/src/user/types.gen.d.ts +2 -2
  121. package/dist/src/view/client.d.ts.map +1 -1
  122. package/dist/src/view/types.gen.d.ts +2 -2
  123. package/dist/src/workspace/client.d.ts.map +1 -1
  124. package/dist/src/workspace/types.gen.d.ts +3 -3
  125. package/dist/src/workspace/types.gen.d.ts.map +1 -1
  126. package/package.json +12 -11
  127. package/src/access/policy/client.ts +4 -7
  128. package/src/access/role/client.ts +6 -26
  129. package/src/actions/actions.spec.ts +229 -0
  130. package/src/actions/actions.ts +104 -0
  131. package/src/actions/external.ts +10 -0
  132. package/src/actions/index.ts +10 -0
  133. package/src/arc/client.ts +3 -7
  134. package/src/arc/compiler/types.gen.ts +2 -1
  135. package/src/arc/ir/types.gen.ts +2 -2
  136. package/src/arc/lsp.spec.ts +3 -7
  137. package/src/arc/types/types.gen.ts +3 -3
  138. package/src/auth/auth.spec.ts +12 -13
  139. package/src/auth/auth.ts +36 -34
  140. package/src/channel/batchRetriever.spec.ts +13 -4
  141. package/src/channel/client.ts +8 -6
  142. package/src/channel/retriever.ts +7 -16
  143. package/src/channel/writer.ts +4 -20
  144. package/src/connection/checker.ts +6 -6
  145. package/src/connection/connection.spec.ts +5 -8
  146. package/src/device/client.ts +5 -8
  147. package/src/device/types.gen.ts +4 -4
  148. package/src/errors.ts +9 -9
  149. package/src/framer/adapter.ts +2 -4
  150. package/src/framer/client.ts +1 -1
  151. package/src/framer/codec.spec.ts +53 -3
  152. package/src/framer/codec.ts +58 -25
  153. package/src/framer/deleter.ts +2 -8
  154. package/src/framer/iterator.ts +43 -40
  155. package/src/framer/streamProxy.ts +13 -13
  156. package/src/framer/streamer.spec.ts +12 -3
  157. package/src/framer/streamer.ts +7 -12
  158. package/src/framer/types.gen.ts +20 -0
  159. package/src/framer/writer.spec.ts +77 -0
  160. package/src/framer/writer.ts +51 -28
  161. package/src/group/client.ts +4 -7
  162. package/src/index.ts +3 -2
  163. package/src/label/client.ts +6 -16
  164. package/src/lineplot/client.ts +6 -21
  165. package/src/log/client.ts +6 -21
  166. package/src/ontology/client.ts +3 -4
  167. package/src/ontology/types.gen.ts +0 -1
  168. package/src/ontology/writer.ts +4 -7
  169. package/src/rack/client.ts +4 -7
  170. package/src/ranger/alias/client.ts +6 -11
  171. package/src/ranger/client.ts +3 -4
  172. package/src/ranger/kv/client.ts +5 -8
  173. package/src/ranger/writer.ts +4 -17
  174. package/src/schematic/access.spec.ts +6 -6
  175. package/src/schematic/actions.gen.ts +200 -0
  176. package/src/schematic/actions.spec.ts +699 -0
  177. package/src/schematic/actions.ts +168 -0
  178. package/src/schematic/client.ts +34 -30
  179. package/src/schematic/external.ts +2 -0
  180. package/src/schematic/schematic.spec.ts +233 -69
  181. package/src/schematic/symbol/client.ts +6 -11
  182. package/src/schematic/symbol/types.gen.ts +1 -10
  183. package/src/schematic/types.gen.ts +55 -6
  184. package/src/status/client.ts +4 -10
  185. package/src/table/access.spec.ts +0 -6
  186. package/src/table/actions.gen.ts +243 -0
  187. package/src/table/actions.ts +255 -0
  188. package/src/table/client.ts +21 -25
  189. package/src/table/external.ts +2 -0
  190. package/src/table/table.spec.ts +588 -43
  191. package/src/table/types.gen.ts +58 -5
  192. package/src/task/client.ts +7 -11
  193. package/src/task/types.gen.ts +8 -6
  194. package/src/user/client.ts +6 -11
  195. package/src/view/client.ts +4 -7
  196. package/src/workspace/client.ts +6 -16
  197. package/src/workspace/types.gen.ts +2 -1
@@ -0,0 +1,229 @@
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 { type Draft, produce } from "immer";
11
+ import { describe, expect, it } from "vitest";
12
+ import { z } from "zod";
13
+
14
+ import { actions } from "@/actions";
15
+
16
+ interface DemoState {
17
+ name: string;
18
+ items: { key: string; value: number }[];
19
+ }
20
+
21
+ type DemoAction =
22
+ | { type: "rename"; payload: { name: string } }
23
+ | { type: "set_item"; payload: { key: string; value: number } }
24
+ | { type: "remove_item"; payload: { key: string } };
25
+
26
+ const renameAction = (name: string): DemoAction => ({
27
+ type: "rename",
28
+ payload: { name },
29
+ });
30
+ const setItemAction = (key: string, value: number): DemoAction => ({
31
+ type: "set_item",
32
+ payload: { key, value },
33
+ });
34
+ const removeItemAction = (key: string): DemoAction => ({
35
+ type: "remove_item",
36
+ payload: { key },
37
+ });
38
+
39
+ const reduceOne = (
40
+ draft: Draft<DemoState>,
41
+ action: DemoAction,
42
+ ): actions.HandlerResult<DemoAction> => {
43
+ switch (action.type) {
44
+ case "rename": {
45
+ const prev = draft.name;
46
+ draft.name = action.payload.name;
47
+ return { inverse: [renameAction(prev)], targets: ["name"] };
48
+ }
49
+ case "set_item": {
50
+ const idx = draft.items.findIndex((i) => i.key === action.payload.key);
51
+ if (idx === -1) {
52
+ draft.items.push(action.payload);
53
+ return {
54
+ inverse: [removeItemAction(action.payload.key)],
55
+ targets: [action.payload.key],
56
+ };
57
+ }
58
+ const prevValue = draft.items[idx].value;
59
+ draft.items[idx].value = action.payload.value;
60
+ return {
61
+ inverse: [setItemAction(action.payload.key, prevValue)],
62
+ targets: [action.payload.key],
63
+ };
64
+ }
65
+ case "remove_item": {
66
+ const idx = draft.items.findIndex((i) => i.key === action.payload.key);
67
+ if (idx === -1) return actions.NO_OP_RESULT;
68
+ const removed = draft.items[idx];
69
+ draft.items.splice(idx, 1);
70
+ return {
71
+ inverse: [setItemAction(removed.key, removed.value)],
72
+ targets: [removed.key],
73
+ };
74
+ }
75
+ }
76
+ };
77
+
78
+ const reduceAll = actions.createReduceAll(reduceOne);
79
+
80
+ describe("snapshotDraft", () => {
81
+ it("Should return plain values unchanged", () => {
82
+ const v = { a: 1, b: [2, 3] };
83
+ expect(actions.snapshotDraft(v)).toBe(v);
84
+ });
85
+
86
+ it("Should freeze a draft into a plain object that survives the produce closure", () => {
87
+ const original = { items: [{ key: "a", value: 1 }] };
88
+ let captured: typeof original | undefined;
89
+ const next = produce(original, (draft) => {
90
+ captured = actions.snapshotDraft(draft.items[0]) as unknown as typeof original;
91
+ draft.items[0].value = 99;
92
+ });
93
+ expect(next.items[0].value).toBe(99);
94
+ expect(captured).toEqual({ key: "a", value: 1 });
95
+ });
96
+
97
+ it("Should preserve primitive values without wrapping", () => {
98
+ expect(actions.snapshotDraft(42)).toBe(42);
99
+ expect(actions.snapshotDraft("hello")).toBe("hello");
100
+ expect(actions.snapshotDraft(null)).toBe(null);
101
+ });
102
+ });
103
+
104
+ describe("NO_OP_RESULT", () => {
105
+ it("Should expose empty inverse and targets", () => {
106
+ expect(actions.NO_OP_RESULT.inverse).toHaveLength(0);
107
+ expect(actions.NO_OP_RESULT.targets).toHaveLength(0);
108
+ });
109
+ });
110
+
111
+ describe("createReduceAll", () => {
112
+ const initial: DemoState = { name: "before", items: [{ key: "a", value: 1 }] };
113
+
114
+ it("Should apply a single action and return the new state", () => {
115
+ const { next } = reduceAll(initial, [renameAction("after")]);
116
+ expect(next.name).toBe("after");
117
+ expect(initial.name).toBe("before");
118
+ });
119
+
120
+ it("Should accumulate inverses in reverse application order", () => {
121
+ const { inverse } = reduceAll(initial, [
122
+ renameAction("step-1"),
123
+ setItemAction("a", 2),
124
+ setItemAction("b", 7),
125
+ ]);
126
+ expect(inverse).toEqual([
127
+ removeItemAction("b"),
128
+ setItemAction("a", 1),
129
+ renameAction("before"),
130
+ ]);
131
+ });
132
+
133
+ it("Should round-trip state through forward then inverse application", () => {
134
+ const forward = [
135
+ renameAction("after"),
136
+ setItemAction("a", 2),
137
+ setItemAction("b", 7),
138
+ ];
139
+ const { next, inverse } = reduceAll(initial, forward);
140
+ const { next: roundtripped } = reduceAll(next, inverse);
141
+ expect(roundtripped).toEqual(initial);
142
+ });
143
+
144
+ it("Should de-duplicate targets across actions", () => {
145
+ const { targets } = reduceAll(initial, [
146
+ setItemAction("a", 5),
147
+ setItemAction("a", 6),
148
+ setItemAction("b", 7),
149
+ ]);
150
+ expect(targets).toHaveLength(2);
151
+ expect(targets).toEqual(expect.arrayContaining(["a", "b"]));
152
+ });
153
+
154
+ it("Should drop NO_OP_RESULT entries from the inverse list", () => {
155
+ const { inverse } = reduceAll(initial, [removeItemAction("missing")]);
156
+ expect(inverse).toHaveLength(0);
157
+ });
158
+
159
+ it("Should leave the source state untouched after produce", () => {
160
+ const before = JSON.parse(JSON.stringify(initial));
161
+ reduceAll(initial, [
162
+ renameAction("x"),
163
+ setItemAction("a", 99),
164
+ setItemAction("c", 3),
165
+ ]);
166
+ expect(initial).toEqual(before);
167
+ });
168
+ });
169
+
170
+ describe("scopedZ", () => {
171
+ const schema = actions.scopedZ(z.string(), z.object({ kind: z.string() }));
172
+
173
+ it("Should parse a fully-populated envelope", () => {
174
+ const parsed = schema.parse({
175
+ key: "abc",
176
+ dispatchKey: "dk",
177
+ seq: 42,
178
+ actions: [{ kind: "rename" }],
179
+ });
180
+ expect(parsed.key).toBe("abc");
181
+ expect(parsed.seq).toBe(42);
182
+ expect(parsed.actions).toEqual([{ kind: "rename" }]);
183
+ });
184
+
185
+ it("Should default seq to 0 when missing", () => {
186
+ const parsed = schema.parse({ key: "abc", dispatchKey: "dk", actions: [] });
187
+ expect(parsed.seq).toBe(0);
188
+ });
189
+
190
+ it("Should reject negative seq", () => {
191
+ expect(() =>
192
+ schema.parse({ key: "abc", dispatchKey: "dk", seq: -1, actions: [] }),
193
+ ).toThrow();
194
+ });
195
+
196
+ it("Should reject non-integer seq", () => {
197
+ expect(() =>
198
+ schema.parse({ key: "abc", dispatchKey: "dk", seq: 1.5, actions: [] }),
199
+ ).toThrow();
200
+ });
201
+
202
+ it("Should reject envelopes whose actions fail the inner schema", () => {
203
+ expect(() =>
204
+ schema.parse({
205
+ key: "abc",
206
+ dispatchKey: "dk",
207
+ actions: [{ kind: 7 }],
208
+ }),
209
+ ).toThrow();
210
+ });
211
+ });
212
+
213
+ describe("dispatchReqZ", () => {
214
+ const schema = actions.dispatchReqZ(z.string(), z.object({ kind: z.string() }));
215
+
216
+ it("Should parse a camelCase dispatchKey body", () => {
217
+ const parsed = schema.parse({
218
+ key: "abc",
219
+ dispatchKey: "dk",
220
+ actions: [{ kind: "x" }],
221
+ });
222
+ expect(parsed.dispatchKey).toBe("dk");
223
+ expect(parsed.actions).toEqual([{ kind: "x" }]);
224
+ });
225
+
226
+ it("Should reject a body missing dispatchKey", () => {
227
+ expect(() => schema.parse({ key: "abc", actions: [] })).toThrow();
228
+ });
229
+ });
@@ -0,0 +1,104 @@
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 { current, type Draft, isDraft, produce } from "immer";
11
+ import { z } from "zod";
12
+
13
+ /**
14
+ * Result returned by a single per-variant action handler. `inverse` is the
15
+ * action sequence the undo stack should replay to revert the handler's effect.
16
+ * `targets` is the set of document keys the handler touched, used by consumers
17
+ * to invalidate undoables targeting overlapping resources when a remote
18
+ * session emits a competing action.
19
+ */
20
+ export interface HandlerResult<A> {
21
+ inverse: A[];
22
+ targets: readonly string[];
23
+ }
24
+
25
+ /**
26
+ * NO_OP_RESULT is the zero-value HandlerResult. Return it from a handler that
27
+ * declined to mutate state (e.g. lookup miss) so it neither contributes to the
28
+ * undo stack nor invalidates undoables targeting the same resource.
29
+ */
30
+ export const NO_OP_RESULT: HandlerResult<never> = { inverse: [], targets: [] };
31
+
32
+ /**
33
+ * Result returned by a reduceAll application: the new state, the inverse
34
+ * sequence that reverts the whole batch, and the union of all targets touched.
35
+ */
36
+ export interface ReduceAllResult<S, A> {
37
+ next: S;
38
+ inverse: A[];
39
+ targets: readonly string[];
40
+ }
41
+
42
+ /**
43
+ * Returns a plain-object snapshot of an Immer draft so the result is safe to
44
+ * embed in an action stored on the undo stack. Returns non-draft inputs
45
+ * unchanged. The non-draft passthrough exists because when a reducer applies
46
+ * multiple actions in one produce(), an earlier action's wholesale
47
+ * assignment leaves the slot as a plain object, so the next action would
48
+ * crash if it called `current` unconditionally.
49
+ */
50
+ export const snapshotDraft = <T>(v: T): T => (isDraft(v) ? current(v as Draft<T>) : v);
51
+
52
+ /**
53
+ * createReduceAll lifts a per-variant `reduceOne` switch into a batched
54
+ * reducer. The returned function applies each action against an Immer draft of
55
+ * `state` in order, accumulates the inverses in reverse application order so a
56
+ * single undo replays them correctly, and unions the touched targets.
57
+ */
58
+ export const createReduceAll =
59
+ <S, A>(reduceOne: (draft: Draft<S>, action: A) => HandlerResult<A>) =>
60
+ (state: S, actions: A[]): ReduceAllResult<S, A> => {
61
+ const inverse: A[] = [];
62
+ const targetsSet = new Set<string>();
63
+ const next = produce(state, (draft) => {
64
+ for (const action of actions) {
65
+ const result = reduceOne(draft, action);
66
+ if (result.inverse.length > 0) inverse.unshift(...result.inverse);
67
+ for (const t of result.targets) targetsSet.add(t);
68
+ }
69
+ });
70
+ return { next, inverse, targets: Array.from(targetsSet) };
71
+ };
72
+
73
+ /**
74
+ * scopedZ builds the schema for envelopes broadcast on the sy_<service>_set
75
+ * cluster signals channel. The framer's JSON codec runs snakeToCamel before
76
+ * handing the value to the schema, so `dispatchKey` and `seq` are camelCase
77
+ * here even though the server emits them as snake_case. `seq` defaults to 0 so
78
+ * frames from servers that predate the field stay parseable.
79
+ */
80
+ export const scopedZ = <K extends z.ZodType, A extends z.ZodType>(
81
+ keyZ: K,
82
+ actionZ: A,
83
+ ) =>
84
+ z.object({
85
+ key: keyZ,
86
+ dispatchKey: z.string(),
87
+ seq: z.number().int().nonnegative().default(0),
88
+ actions: actionZ.array(),
89
+ });
90
+
91
+ /**
92
+ * dispatchReqZ builds the request body schema for the per-service dispatch
93
+ * endpoint. Stays camelCase; the JSON codec runs camelToSnake on encode so the
94
+ * wire ends up snake_case without per-field conversion here.
95
+ */
96
+ export const dispatchReqZ = <K extends z.ZodType, A extends z.ZodType>(
97
+ keyZ: K,
98
+ actionZ: A,
99
+ ) =>
100
+ z.object({
101
+ key: keyZ,
102
+ dispatchKey: z.string(),
103
+ actions: actionZ.array(),
104
+ });
@@ -0,0 +1,10 @@
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
+ export * from "@/actions/actions";
@@ -0,0 +1,10 @@
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
+ export * as actions from "@/actions/external";
package/src/arc/client.ts CHANGED
@@ -8,7 +8,6 @@
8
8
  // included in the file licenses/APL.txt.
9
9
 
10
10
  import {
11
- sendRequired,
12
11
  type Stream,
13
12
  type StreamClient,
14
13
  type UnaryClient,
@@ -77,8 +76,7 @@ export class Client {
77
76
  async create(arcs: New[]): Promise<Arc[]>;
78
77
  async create(arcs: New | New[]): Promise<Arc | Arc[]> {
79
78
  const isMany = Array.isArray(arcs);
80
- const res = await sendRequired(
81
- this.client,
79
+ const res = await this.client.send(
82
80
  "/arc/create",
83
81
  { arcs: array.toArray(arcs) },
84
82
  createReqZ,
@@ -91,8 +89,7 @@ export class Client {
91
89
  async retrieve(args: RetrieveArgs): Promise<Arc[]>;
92
90
  async retrieve(args: RetrieveArgs): Promise<Arc | Arc[]> {
93
91
  const isSingle = "key" in args || "name" in args;
94
- const res = await sendRequired(
95
- this.client,
92
+ const res = await this.client.send(
96
93
  "/arc/retrieve",
97
94
  args,
98
95
  retrieveArgsZ,
@@ -103,8 +100,7 @@ export class Client {
103
100
  }
104
101
 
105
102
  async delete(keys: Key | Key[]): Promise<void> {
106
- await sendRequired(
107
- this.client,
103
+ await this.client.send(
108
104
  "/arc/delete",
109
105
  { keys: array.toArray(keys) },
110
106
  deleteReqZ,
@@ -9,6 +9,7 @@
9
9
 
10
10
  // Code generated by Oracle. DO NOT EDIT.
11
11
 
12
+ import { record } from "@synnaxlabs/x";
12
13
  import { z } from "zod";
13
14
 
14
15
  /**
@@ -22,6 +23,6 @@ export const outputZ = z.object({
22
23
  * outputMemoryBases contains memory base addresses for multi-output functions, mapping
23
24
  * function keys to their base addresses.
24
25
  */
25
- outputMemoryBases: z.record(z.string(), z.uint32()),
26
+ outputMemoryBases: record.nullishToEmpty(z.string(), z.uint32()),
26
27
  });
27
28
  export interface Output extends z.infer<typeof outputZ> {}
@@ -9,7 +9,7 @@
9
9
 
10
10
  // Code generated by Oracle. DO NOT EDIT.
11
11
 
12
- import { array, zod } from "@synnaxlabs/x";
12
+ import { array, record, zod } from "@synnaxlabs/x";
13
13
  import { z } from "zod";
14
14
 
15
15
  import { types } from "@/arc/types";
@@ -76,7 +76,7 @@ export const authoritiesZ = z.object({
76
76
  /** default is the default authority for all write channels not explicitly listed. */
77
77
  default: zod.uint8.optional(),
78
78
  /** channels maps channel keys to their specific authority values. */
79
- channels: z.record(z.uint32(), zod.uint8),
79
+ channels: record.nullishToEmpty(z.uint32(), zod.uint8),
80
80
  });
81
81
  export interface Authorities extends z.infer<typeof authoritiesZ> {}
82
82
 
@@ -27,7 +27,7 @@ type JSONRPCResponse =
27
27
  };
28
28
 
29
29
  type LSPReceiver = {
30
- receive: () => Promise<[{ content: string }, null] | [null, Error]>;
30
+ receive: () => Promise<{ content: string }>;
31
31
  };
32
32
 
33
33
  const MAX_DRAIN = 50;
@@ -38,9 +38,7 @@ const receiveResponse = async (
38
38
  expectedId: number,
39
39
  ): Promise<JSONRPCResponse> => {
40
40
  for (let i = 0; i < MAX_DRAIN; i++) {
41
- const [res, err] = await stream.receive();
42
- if (err != null) throw err;
43
- if (res == null) throw new Error("Expected response");
41
+ const res = await stream.receive();
44
42
  const msg = JSON.parse(res.content);
45
43
  if (!("method" in msg) && "id" in msg && msg.id === expectedId)
46
44
  return msg as JSONRPCResponse;
@@ -56,9 +54,7 @@ const receiveNotification = async (
56
54
  expectedMethod: string,
57
55
  ): Promise<JSONRPCRequest> => {
58
56
  for (let i = 0; i < MAX_DRAIN; i++) {
59
- const [res, err] = await stream.receive();
60
- if (err != null) throw err;
61
- if (res == null) throw new Error("Expected message");
57
+ const res = await stream.receive();
62
58
  const msg = JSON.parse(res.content);
63
59
  if ("method" in msg && msg.method === expectedMethod) return msg as JSONRPCRequest;
64
60
  }
@@ -9,7 +9,7 @@
9
9
 
10
10
  // Code generated by Oracle. DO NOT EDIT.
11
11
 
12
- import { array, zod } from "@synnaxlabs/x";
12
+ import { array, record, zod } from "@synnaxlabs/x";
13
13
  import { z } from "zod";
14
14
 
15
15
  export enum Kind {
@@ -48,9 +48,9 @@ export const chanDirectionZ = z.enum(ChanDirection);
48
48
  /** Channels contains channel declarations for reading from and writing to Synnax channels. */
49
49
  export const channelsZ = z.object({
50
50
  /** read contains readable channel indices mapped to parameter names. */
51
- read: z.record(z.uint32(), z.string()),
51
+ read: record.nullishToEmpty(z.uint32(), z.string()),
52
52
  /** write contains writable channel indices mapped to parameter names. */
53
- write: z.record(z.uint32(), z.string()),
53
+ write: record.nullishToEmpty(z.uint32(), z.string()),
54
54
  });
55
55
  export interface Channels extends z.infer<typeof channelsZ> {}
56
56
 
@@ -33,8 +33,8 @@ describe("auth", () => {
33
33
  );
34
34
  const client = new auth.Client(transport.unary, TEST_CLIENT_PARAMS);
35
35
  const mw = client.middleware();
36
- const res = await mw(DUMMY_CTX, async () => [DUMMY_CTX, null]);
37
- expect(res).toEqual([DUMMY_CTX, null]);
36
+ const res = await mw(DUMMY_CTX, async () => DUMMY_CTX);
37
+ expect(res).toEqual(DUMMY_CTX);
38
38
  });
39
39
 
40
40
  test("invalid credentials", async () => {
@@ -49,8 +49,7 @@ describe("auth", () => {
49
49
  password: "wrong",
50
50
  });
51
51
  const mw = client.middleware();
52
- const [, err] = await mw(DUMMY_CTX, async () => [DUMMY_CTX, null]);
53
- expect(err).toBeInstanceOf(AuthError);
52
+ await expect(mw(DUMMY_CTX, async () => DUMMY_CTX)).rejects.toThrow(AuthError);
54
53
  });
55
54
 
56
55
  describe("token retry", () => {
@@ -68,16 +67,16 @@ describe("auth", () => {
68
67
  let isFirst = true;
69
68
  let tkOne: string | undefined;
70
69
  let tkTwo: string | undefined;
71
- const [, err] = await mw(DUMMY_CTX, async () => {
70
+ const res = await mw(DUMMY_CTX, async () => {
72
71
  if (isFirst) {
73
72
  isFirst = false;
74
73
  tkOne = client.token;
75
- return [DUMMY_CTX, new ErrorType()];
74
+ throw new ErrorType();
76
75
  }
77
76
  tkTwo = client.token;
78
- return [DUMMY_CTX, null];
77
+ return DUMMY_CTX;
79
78
  });
80
- expect(err).toBeNull();
79
+ expect(res).toEqual(DUMMY_CTX);
81
80
  expect(tkOne).toBeDefined();
82
81
  expect(tkTwo).toBeDefined();
83
82
  });
@@ -92,11 +91,11 @@ describe("auth", () => {
92
91
  );
93
92
  const client = new auth.Client(transport.unary, TEST_CLIENT_PARAMS);
94
93
  const mw = client.middleware();
95
- const [, err] = await mw(DUMMY_CTX, async () => [
96
- DUMMY_CTX,
97
- new InvalidTokenError(),
98
- ]);
99
- expect(err).toBeInstanceOf(InvalidTokenError);
94
+ await expect(
95
+ mw(DUMMY_CTX, async () => {
96
+ throw new InvalidTokenError();
97
+ }),
98
+ ).rejects.toThrow(InvalidTokenError);
100
99
  });
101
100
  });
102
101
  });
package/src/auth/auth.ts CHANGED
@@ -7,15 +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 { type Middleware, sendRequired, type UnaryClient } from "@synnaxlabs/freighter";
11
- import { TimeStamp } from "@synnaxlabs/x";
10
+ import { type Middleware, type UnaryClient } from "@synnaxlabs/freighter";
11
+ import { errors, TimeStamp } from "@synnaxlabs/x";
12
12
  import { z } from "zod";
13
13
 
14
14
  import { ExpiredTokenError, InvalidTokenError } from "@/errors";
15
15
  import { user } from "@/user";
16
16
 
17
- const insecureCredentialsZ = z.object({ username: z.string(), password: z.string() });
18
- interface InsecureCredentials extends z.infer<typeof insecureCredentialsZ> {}
17
+ const credentialsZ = z.object({ username: z.string(), password: z.string() });
18
+ interface Credentials extends z.infer<typeof credentialsZ> {}
19
19
 
20
20
  const clusterInfoZ = z.object({
21
21
  clusterKey: z.string(),
@@ -49,12 +49,12 @@ type AuthState =
49
49
 
50
50
  export class Client {
51
51
  private readonly client: UnaryClient;
52
- private readonly credentials: InsecureCredentials;
52
+ private readonly credentials: Credentials;
53
53
  private authState: AuthState = { authenticated: false };
54
54
  authenticating: Promise<Error | null> | undefined;
55
55
  private retryCount: number;
56
56
 
57
- constructor(client: UnaryClient, credentials: InsecureCredentials) {
57
+ constructor(client: UnaryClient, credentials: Credentials) {
58
58
  this.client = client;
59
59
  this.credentials = credentials;
60
60
  this.retryCount = 0;
@@ -82,8 +82,7 @@ export class Client {
82
82
 
83
83
  async changePassword(newPassword: string): Promise<void> {
84
84
  if (!this.authenticated) throw new Error("Not authenticated");
85
- await sendRequired<typeof changePasswordReqZ, typeof changePasswordResZ>(
86
- this.client,
85
+ await this.client.send(
87
86
  "/auth/change-password",
88
87
  {
89
88
  username: this.credentials.username,
@@ -99,39 +98,42 @@ export class Client {
99
98
  middleware(): Middleware {
100
99
  const mw: Middleware = async (reqCtx, next) => {
101
100
  if (!this.authenticated && !reqCtx.target.endsWith(LOGIN_ENDPOINT)) {
102
- this.authenticating ??= new Promise((resolve, reject) => {
103
- this.client
104
- .send(
101
+ this.authenticating ??= (async (): Promise<Error | null> => {
102
+ try {
103
+ const res = await this.client.send(
105
104
  LOGIN_ENDPOINT,
106
105
  this.credentials,
107
- insecureCredentialsZ,
106
+ credentialsZ,
108
107
  tokenResponseZ,
109
- )
110
- .then(([res, err]) => {
111
- if (err != null) return resolve(err);
112
- if (res == null) return resolve(new Error("No response from login"));
113
- this.authState = {
114
- authenticated: true,
115
- user: res.user,
116
- token: res.token,
117
- };
118
- resolve(null);
119
- })
120
- .catch(reject);
121
- });
108
+ );
109
+ this.authState = {
110
+ authenticated: true,
111
+ user: res.user,
112
+ token: res.token,
113
+ };
114
+ return null;
115
+ } catch (err) {
116
+ return errors.fromUnknown(err);
117
+ }
118
+ })();
122
119
  const err = await this.authenticating;
123
- if (err != null) return [reqCtx, err];
120
+ if (err != null) throw err;
124
121
  }
125
122
  reqCtx.params.Authorization = `Bearer ${this.token}`;
126
- const [resCtx, err] = await next(reqCtx);
127
- if (RETRY_ON.some((e) => e.matches(err)) && this.retryCount < MAX_RETRIES) {
128
- this.authState = { authenticated: false };
129
- this.authenticating = undefined;
130
- this.retryCount += 1;
131
- return mw(reqCtx, next);
123
+ try {
124
+ const resCtx = await next(reqCtx);
125
+ this.retryCount = 0;
126
+ return resCtx;
127
+ } catch (err) {
128
+ if (RETRY_ON.some((e) => e.matches(err)) && this.retryCount < MAX_RETRIES) {
129
+ this.authState = { authenticated: false };
130
+ this.authenticating = undefined;
131
+ this.retryCount += 1;
132
+ return await mw(reqCtx, next);
133
+ }
134
+ this.retryCount = 0;
135
+ throw errors.fromUnknown(err);
132
136
  }
133
- this.retryCount = 0;
134
- return [resCtx, err];
135
137
  };
136
138
  return mw;
137
139
  }