@synnaxlabs/client 0.55.0 → 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.
- package/.turbo/turbo-build.log +10 -13
- package/dist/client.cjs +60 -36
- package/dist/client.js +6362 -4738
- package/dist/src/access/policy/client.d.ts +70 -80
- package/dist/src/access/policy/client.d.ts.map +1 -1
- package/dist/src/access/policy/types.gen.d.ts +18 -20
- package/dist/src/access/policy/types.gen.d.ts.map +1 -1
- package/dist/src/access/role/client.d.ts.map +1 -1
- package/dist/src/access/role/types.gen.d.ts +2 -2
- package/dist/src/actions/actions.d.ts +68 -0
- package/dist/src/actions/actions.d.ts.map +1 -0
- package/dist/src/actions/actions.spec.d.ts +2 -0
- package/dist/src/actions/actions.spec.d.ts.map +1 -0
- package/dist/src/actions/external.d.ts +2 -0
- package/dist/src/actions/external.d.ts.map +1 -0
- package/dist/src/actions/index.d.ts +2 -0
- package/dist/src/actions/index.d.ts.map +1 -0
- package/dist/src/arc/client.d.ts.map +1 -1
- package/dist/src/arc/compiler/types.gen.d.ts +1 -1
- package/dist/src/arc/compiler/types.gen.d.ts.map +1 -1
- package/dist/src/arc/graph/types.gen.d.ts +29 -29
- package/dist/src/arc/graph/types.gen.d.ts.map +1 -1
- package/dist/src/arc/ir/types.gen.d.ts +123 -123
- package/dist/src/arc/ir/types.gen.d.ts.map +1 -1
- package/dist/src/arc/module/types.gen.d.ts +45 -45
- package/dist/src/arc/program/types.gen.d.ts +45 -45
- package/dist/src/arc/types/types.gen.d.ts +11 -11
- package/dist/src/arc/types/types.gen.d.ts.map +1 -1
- package/dist/src/arc/types.gen.d.ts +99 -99
- package/dist/src/auth/auth.d.ts +3 -3
- package/dist/src/auth/auth.d.ts.map +1 -1
- package/dist/src/channel/client.d.ts +2 -2
- package/dist/src/channel/client.d.ts.map +1 -1
- package/dist/src/channel/retriever.d.ts +5 -8
- package/dist/src/channel/retriever.d.ts.map +1 -1
- package/dist/src/channel/types.gen.d.ts +3 -3
- package/dist/src/channel/writer.d.ts.map +1 -1
- package/dist/src/connection/checker.d.ts +1 -1
- package/dist/src/connection/checker.d.ts.map +1 -1
- package/dist/src/device/client.d.ts.map +1 -1
- package/dist/src/device/types.gen.d.ts +6 -8
- package/dist/src/device/types.gen.d.ts.map +1 -1
- package/dist/src/errors.d.ts +2 -0
- package/dist/src/errors.d.ts.map +1 -1
- package/dist/src/framer/adapter.d.ts.map +1 -1
- package/dist/src/framer/client.d.ts +2 -2
- package/dist/src/framer/codec.d.ts +9 -1
- package/dist/src/framer/codec.d.ts.map +1 -1
- package/dist/src/framer/deleter.d.ts.map +1 -1
- package/dist/src/framer/frame.d.ts +1 -1
- package/dist/src/framer/iterator.d.ts +84 -3
- package/dist/src/framer/iterator.d.ts.map +1 -1
- package/dist/src/framer/streamProxy.d.ts.map +1 -1
- package/dist/src/framer/streamer.d.ts +1 -3
- package/dist/src/framer/streamer.d.ts.map +1 -1
- package/dist/src/framer/types.gen.d.ts +18 -0
- package/dist/src/framer/types.gen.d.ts.map +1 -1
- package/dist/src/framer/writer.d.ts +8 -8
- package/dist/src/framer/writer.d.ts.map +1 -1
- package/dist/src/group/client.d.ts +1 -2
- package/dist/src/group/client.d.ts.map +1 -1
- package/dist/src/group/types.gen.d.ts +2 -2
- package/dist/src/index.d.ts +2 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/label/client.d.ts +5 -8
- package/dist/src/label/client.d.ts.map +1 -1
- package/dist/src/lineplot/client.d.ts.map +1 -1
- package/dist/src/lineplot/types.gen.d.ts +2 -2
- package/dist/src/log/client.d.ts.map +1 -1
- package/dist/src/log/types.gen.d.ts +2 -2
- package/dist/src/ontology/client.d.ts +1 -3
- package/dist/src/ontology/client.d.ts.map +1 -1
- package/dist/src/ontology/payload.d.ts +12 -16
- package/dist/src/ontology/payload.d.ts.map +1 -1
- package/dist/src/ontology/types.gen.d.ts +1 -2
- package/dist/src/ontology/types.gen.d.ts.map +1 -1
- package/dist/src/ontology/writer.d.ts +5 -10
- package/dist/src/ontology/writer.d.ts.map +1 -1
- package/dist/src/rack/client.d.ts.map +1 -1
- package/dist/src/rack/types.gen.d.ts +3 -3
- package/dist/src/ranger/alias/client.d.ts.map +1 -1
- package/dist/src/ranger/client.d.ts.map +1 -1
- package/dist/src/ranger/kv/client.d.ts.map +1 -1
- package/dist/src/ranger/types.gen.d.ts +6 -6
- package/dist/src/ranger/types.gen.d.ts.map +1 -1
- package/dist/src/ranger/writer.d.ts +2 -3
- package/dist/src/ranger/writer.d.ts.map +1 -1
- package/dist/src/schematic/actions.d.ts +147 -0
- package/dist/src/schematic/actions.d.ts.map +1 -0
- package/dist/src/schematic/actions.gen.d.ts +484 -0
- package/dist/src/schematic/actions.gen.d.ts.map +1 -0
- package/dist/src/schematic/actions.spec.d.ts +2 -0
- package/dist/src/schematic/actions.spec.d.ts.map +1 -0
- package/dist/src/schematic/client.d.ts +53 -2
- package/dist/src/schematic/client.d.ts.map +1 -1
- package/dist/src/schematic/external.d.ts +2 -0
- package/dist/src/schematic/external.d.ts.map +1 -1
- package/dist/src/schematic/symbol/client.d.ts.map +1 -1
- package/dist/src/schematic/symbol/types.gen.d.ts +48 -58
- package/dist/src/schematic/symbol/types.gen.d.ts.map +1 -1
- package/dist/src/schematic/types.gen.d.ts +131 -5
- package/dist/src/schematic/types.gen.d.ts.map +1 -1
- package/dist/src/status/client.d.ts.map +1 -1
- package/dist/src/status/payload.d.ts +3 -3
- package/dist/src/table/actions.d.ts +156 -0
- package/dist/src/table/actions.d.ts.map +1 -0
- package/dist/src/table/actions.gen.d.ts +587 -0
- package/dist/src/table/actions.gen.d.ts.map +1 -0
- package/dist/src/table/client.d.ts +28 -2
- package/dist/src/table/client.d.ts.map +1 -1
- package/dist/src/table/external.d.ts +2 -0
- package/dist/src/table/external.d.ts.map +1 -1
- package/dist/src/table/types.gen.d.ts +71 -4
- package/dist/src/table/types.gen.d.ts.map +1 -1
- package/dist/src/task/client.d.ts.map +1 -1
- package/dist/src/task/types.gen.d.ts +7 -7
- package/dist/src/task/types.gen.d.ts.map +1 -1
- package/dist/src/user/client.d.ts +2 -2
- package/dist/src/user/client.d.ts.map +1 -1
- package/dist/src/user/types.gen.d.ts +2 -2
- package/dist/src/view/client.d.ts.map +1 -1
- package/dist/src/view/types.gen.d.ts +2 -2
- package/dist/src/workspace/client.d.ts.map +1 -1
- package/dist/src/workspace/types.gen.d.ts +3 -3
- package/dist/src/workspace/types.gen.d.ts.map +1 -1
- package/package.json +10 -9
- package/src/access/policy/client.ts +4 -7
- package/src/access/role/client.ts +6 -26
- package/src/actions/actions.spec.ts +229 -0
- package/src/actions/actions.ts +104 -0
- package/src/actions/external.ts +10 -0
- package/src/actions/index.ts +10 -0
- package/src/arc/client.ts +3 -7
- package/src/arc/compiler/types.gen.ts +2 -1
- package/src/arc/ir/types.gen.ts +2 -2
- package/src/arc/lsp.spec.ts +3 -7
- package/src/arc/types/types.gen.ts +3 -3
- package/src/auth/auth.spec.ts +12 -13
- package/src/auth/auth.ts +35 -33
- package/src/channel/batchRetriever.spec.ts +13 -4
- package/src/channel/client.ts +8 -6
- package/src/channel/retriever.ts +7 -16
- package/src/channel/writer.ts +4 -20
- package/src/connection/checker.ts +4 -5
- package/src/connection/connection.spec.ts +5 -8
- package/src/device/client.ts +5 -8
- package/src/device/types.gen.ts +4 -4
- package/src/errors.ts +8 -9
- package/src/framer/adapter.ts +2 -4
- package/src/framer/codec.spec.ts +53 -3
- package/src/framer/codec.ts +58 -25
- package/src/framer/deleter.ts +2 -8
- package/src/framer/iterator.ts +42 -39
- package/src/framer/streamProxy.ts +11 -12
- package/src/framer/streamer.spec.ts +1 -1
- package/src/framer/streamer.ts +2 -7
- package/src/framer/types.gen.ts +20 -0
- package/src/framer/writer.spec.ts +77 -0
- package/src/framer/writer.ts +53 -28
- package/src/group/client.ts +4 -7
- package/src/index.ts +3 -2
- package/src/label/client.ts +6 -16
- package/src/lineplot/client.ts +6 -21
- package/src/log/client.ts +6 -21
- package/src/ontology/client.ts +2 -3
- package/src/ontology/types.gen.ts +0 -1
- package/src/ontology/writer.ts +4 -7
- package/src/rack/client.ts +4 -7
- package/src/ranger/alias/client.ts +6 -11
- package/src/ranger/client.ts +2 -3
- package/src/ranger/kv/client.ts +4 -7
- package/src/ranger/writer.ts +4 -17
- package/src/schematic/access.spec.ts +6 -6
- package/src/schematic/actions.gen.ts +200 -0
- package/src/schematic/actions.spec.ts +699 -0
- package/src/schematic/actions.ts +168 -0
- package/src/schematic/client.ts +34 -30
- package/src/schematic/external.ts +2 -0
- package/src/schematic/schematic.spec.ts +233 -69
- package/src/schematic/symbol/client.ts +6 -11
- package/src/schematic/symbol/types.gen.ts +1 -10
- package/src/schematic/types.gen.ts +55 -6
- package/src/status/client.ts +4 -10
- package/src/table/access.spec.ts +0 -6
- package/src/table/actions.gen.ts +243 -0
- package/src/table/actions.ts +255 -0
- package/src/table/client.ts +21 -25
- package/src/table/external.ts +2 -0
- package/src/table/table.spec.ts +588 -43
- package/src/table/types.gen.ts +58 -5
- package/src/task/client.ts +7 -11
- package/src/task/types.gen.ts +8 -6
- package/src/user/client.ts +6 -11
- package/src/view/client.ts +4 -7
- package/src/workspace/client.ts +6 -16
- 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
|
|
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
|
|
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
|
|
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:
|
|
26
|
+
outputMemoryBases: record.nullishToEmpty(z.string(), z.uint32()),
|
|
26
27
|
});
|
|
27
28
|
export interface Output extends z.infer<typeof outputZ> {}
|
package/src/arc/ir/types.gen.ts
CHANGED
|
@@ -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:
|
|
79
|
+
channels: record.nullishToEmpty(z.uint32(), zod.uint8),
|
|
80
80
|
});
|
|
81
81
|
export interface Authorities extends z.infer<typeof authoritiesZ> {}
|
|
82
82
|
|
package/src/arc/lsp.spec.ts
CHANGED
|
@@ -27,7 +27,7 @@ type JSONRPCResponse =
|
|
|
27
27
|
};
|
|
28
28
|
|
|
29
29
|
type LSPReceiver = {
|
|
30
|
-
receive: () => Promise<
|
|
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
|
|
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
|
|
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:
|
|
51
|
+
read: record.nullishToEmpty(z.uint32(), z.string()),
|
|
52
52
|
/** write contains writable channel indices mapped to parameter names. */
|
|
53
|
-
write:
|
|
53
|
+
write: record.nullishToEmpty(z.uint32(), z.string()),
|
|
54
54
|
});
|
|
55
55
|
export interface Channels extends z.infer<typeof channelsZ> {}
|
|
56
56
|
|
package/src/auth/auth.spec.ts
CHANGED
|
@@ -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 () =>
|
|
37
|
-
expect(res).toEqual(
|
|
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
|
-
|
|
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
|
|
70
|
+
const res = await mw(DUMMY_CTX, async () => {
|
|
72
71
|
if (isFirst) {
|
|
73
72
|
isFirst = false;
|
|
74
73
|
tkOne = client.token;
|
|
75
|
-
|
|
74
|
+
throw new ErrorType();
|
|
76
75
|
}
|
|
77
76
|
tkTwo = client.token;
|
|
78
|
-
return
|
|
77
|
+
return DUMMY_CTX;
|
|
79
78
|
});
|
|
80
|
-
expect(
|
|
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
|
-
|
|
96
|
-
DUMMY_CTX,
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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,
|
|
10
|
+
import { type Middleware, type UnaryClient } from "@synnaxlabs/freighter";
|
|
11
11
|
import { 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
|
|
18
|
-
interface
|
|
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:
|
|
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:
|
|
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
|
|
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 ??=
|
|
103
|
-
|
|
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
|
-
|
|
106
|
+
credentialsZ,
|
|
108
107
|
tokenResponseZ,
|
|
109
|
-
)
|
|
110
|
-
.
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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 err instanceof Error ? err : new Error(String(err));
|
|
117
|
+
}
|
|
118
|
+
})();
|
|
122
119
|
const err = await this.authenticating;
|
|
123
|
-
if (err != null)
|
|
120
|
+
if (err != null) throw err;
|
|
124
121
|
}
|
|
125
122
|
reqCtx.params.Authorization = `Bearer ${this.token}`;
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
this.
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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 mw(reqCtx, next);
|
|
133
|
+
}
|
|
134
|
+
this.retryCount = 0;
|
|
135
|
+
throw err;
|
|
132
136
|
}
|
|
133
|
-
this.retryCount = 0;
|
|
134
|
-
return [resCtx, err];
|
|
135
137
|
};
|
|
136
138
|
return mw;
|
|
137
139
|
}
|