@synnaxlabs/client 0.57.2 → 0.57.3
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/dist/{client-BsVW3qAW.js → client-CX6CMvCc.js} +3617 -3528
- package/dist/client-CX6CMvCc.js.map +1 -0
- package/dist/client.js +1 -1
- package/dist/src/arc/client.d.ts.map +1 -1
- package/dist/src/channel/client.d.ts +1 -0
- package/dist/src/channel/client.d.ts.map +1 -1
- package/dist/src/device/client.d.ts.map +1 -1
- package/dist/src/framer/streamer.d.ts +34 -18
- package/dist/src/framer/streamer.d.ts.map +1 -1
- package/dist/src/query/indexes.d.ts +10 -4
- package/dist/src/query/indexes.d.ts.map +1 -1
- package/dist/src/query/table.d.ts +10 -3
- package/dist/src/query/table.d.ts.map +1 -1
- package/dist/src/rack/client.d.ts.map +1 -1
- package/dist/src/ranger/client.d.ts +9 -1
- package/dist/src/ranger/client.d.ts.map +1 -1
- package/dist/src/status/client.d.ts.map +1 -1
- package/dist/src/status/status.d.ts +2 -0
- package/dist/src/status/status.d.ts.map +1 -1
- package/dist/src/task/client.d.ts +19 -1
- package/dist/src/task/client.d.ts.map +1 -1
- package/dist/src/testutil/client.d.ts +2 -0
- package/dist/src/testutil/client.d.ts.map +1 -1
- package/dist/src/testutil/proxy.d.ts +8 -0
- package/dist/src/testutil/proxy.d.ts.map +1 -1
- package/dist/testutil.js +80 -60
- package/dist/testutil.js.map +1 -1
- package/package.json +3 -3
- package/src/arc/client.ts +2 -20
- package/src/channel/client.ts +11 -6
- package/src/device/client.ts +4 -3
- package/src/framer/streamer.ts +71 -6
- package/src/query/indexes.ts +19 -6
- package/src/query/table.ts +50 -8
- package/src/rack/client.ts +5 -4
- package/src/ranger/client.ts +39 -8
- package/src/status/client.ts +1 -3
- package/src/status/status.ts +14 -0
- package/src/task/client.ts +54 -19
- package/src/testutil/client.ts +5 -1
- package/src/testutil/proxy.ts +59 -6
- package/dist/client-BsVW3qAW.js.map +0 -1
|
@@ -15,6 +15,14 @@ export interface SeverableProxy {
|
|
|
15
15
|
port: number;
|
|
16
16
|
/** Drops every live connection and refuses new ones, as if the cluster died. */
|
|
17
17
|
sever: () => Promise<void>;
|
|
18
|
+
/**
|
|
19
|
+
* Silently kills every live WebSocket connection, modeling a half-open socket (NAT
|
|
20
|
+
* timeout, VPN drop, laptop sleep): the cluster side is destroyed while the client
|
|
21
|
+
* side is held open with its writes discarded, so no close or reset ever reaches the
|
|
22
|
+
* client. New connections still forward. Returns how many connections were
|
|
23
|
+
* blackholed.
|
|
24
|
+
*/
|
|
25
|
+
blackholeStreams: () => number;
|
|
18
26
|
/** Accepts connections again on the same port. */
|
|
19
27
|
restore: () => Promise<void>;
|
|
20
28
|
/** Shuts the proxy down for good. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"proxy.d.ts","sourceRoot":"","sources":["../../../src/testutil/proxy.ts"],"names":[],"mappings":"AAWA,OAAO,EAAE,KAAK,OAAO,EAAY,MAAM,eAAe,CAAC;AAIvD;;;;;GAKG;AACH,eAAO,MAAM,UAAU,EAAE,OAAO,CAAC,MAIhC,CAAC;AAEF,MAAM,WAAW,oBAAoB;IACnC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,cAAc;IAC7B,oEAAoE;IACpE,IAAI,EAAE,MAAM,CAAC;IACb,gFAAgF;IAChF,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3B,kDAAkD;IAClD,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7B,qCAAqC;IACrC,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC5B;
|
|
1
|
+
{"version":3,"file":"proxy.d.ts","sourceRoot":"","sources":["../../../src/testutil/proxy.ts"],"names":[],"mappings":"AAWA,OAAO,EAAE,KAAK,OAAO,EAAY,MAAM,eAAe,CAAC;AAIvD;;;;;GAKG;AACH,eAAO,MAAM,UAAU,EAAE,OAAO,CAAC,MAIhC,CAAC;AAEF,MAAM,WAAW,oBAAoB;IACnC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,cAAc;IAC7B,oEAAoE;IACpE,IAAI,EAAE,MAAM,CAAC;IACb,gFAAgF;IAChF,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3B;;;;;;OAMG;IACH,gBAAgB,EAAE,MAAM,MAAM,CAAC;IAC/B,kDAAkD;IAClD,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7B,qCAAqC;IACrC,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC5B;AASD;;;;GAIG;AACH,eAAO,MAAM,oBAAoB,GAC/B,SAAQ,oBAAyB,KAChC,OAAO,CAAC,cAAc,CA6FxB,CAAC"}
|
package/dist/testutil.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { B as e, G as t, K as n, V as r, W as i, Z as a, d as o, t as s, z as c } from "./client-
|
|
1
|
+
import { B as e, G as t, K as n, V as r, W as i, Z as a, d as o, t as s, z as c } from "./client-CX6CMvCc.js";
|
|
2
2
|
import { DataType as l, TimeSpan as u, TimeStamp as d, array as f, errors as p, id as m } from "@synnaxlabs/x";
|
|
3
|
-
import { afterAll as h } from "vitest";
|
|
4
|
-
import { connect as
|
|
3
|
+
import { afterAll as h, vi as g } from "vitest";
|
|
4
|
+
import { connect as _, createServer as v } from "node:net";
|
|
5
5
|
//#region src/testutil/client.ts
|
|
6
|
-
var
|
|
6
|
+
var y = {
|
|
7
7
|
host: "localhost",
|
|
8
8
|
port: 9090,
|
|
9
9
|
username: "synnax",
|
|
@@ -13,32 +13,32 @@ var v = {
|
|
|
13
13
|
baseInterval: u.seconds(1),
|
|
14
14
|
scale: 1.5
|
|
15
15
|
}
|
|
16
|
-
},
|
|
16
|
+
}, b = [];
|
|
17
17
|
h(async () => {
|
|
18
|
-
let e = (await Promise.allSettled(
|
|
18
|
+
let e = (await Promise.allSettled(b.map((e) => e.close()))).filter((e) => e.status === "rejected");
|
|
19
19
|
if (e.length > 0) throw AggregateError(e.map((e) => e.reason), "failed to close test clients");
|
|
20
20
|
});
|
|
21
|
-
var
|
|
21
|
+
var x = (e) => {
|
|
22
22
|
let t = new s({
|
|
23
|
-
...
|
|
23
|
+
...y,
|
|
24
24
|
...e
|
|
25
25
|
});
|
|
26
|
-
return
|
|
27
|
-
},
|
|
28
|
-
let n =
|
|
26
|
+
return b.push(t), t;
|
|
27
|
+
}, S = (e, t = "transport") => g.spyOn(e[t].unary, "send"), C = (e) => n.matches(e) || e instanceof Error && e.cause != null && C(e.cause), w = async (e, t) => {
|
|
28
|
+
let n = x({
|
|
29
29
|
username: e,
|
|
30
30
|
password: "test",
|
|
31
31
|
onInternalError: (e) => {
|
|
32
|
-
|
|
32
|
+
C(e) || console.error(e);
|
|
33
33
|
}
|
|
34
34
|
});
|
|
35
35
|
try {
|
|
36
36
|
await n.access.policies.retrieveForSubject(c(t));
|
|
37
37
|
} catch (e) {
|
|
38
|
-
if (!
|
|
38
|
+
if (!C(e)) throw p.fromUnknown(e);
|
|
39
39
|
}
|
|
40
40
|
return n;
|
|
41
|
-
},
|
|
41
|
+
}, T = async (e) => {
|
|
42
42
|
let t = m.create();
|
|
43
43
|
return {
|
|
44
44
|
username: t,
|
|
@@ -49,30 +49,30 @@ var b = (e) => {
|
|
|
49
49
|
lastName: "test"
|
|
50
50
|
})).key
|
|
51
51
|
};
|
|
52
|
-
},
|
|
53
|
-
let i = await
|
|
52
|
+
}, E = async (t, n) => {
|
|
53
|
+
let i = await T(t), a = await Promise.all(f.toArray(n).map(async (e) => await t.access.policies.create(e))), o = await t.access.roles.create({
|
|
54
54
|
name: "test",
|
|
55
55
|
description: "test"
|
|
56
56
|
});
|
|
57
57
|
return await t.ontology.addChildren(e(o.key), ...a.map(({ key: e }) => r(e))), await t.access.roles.assign({
|
|
58
58
|
user: i.key,
|
|
59
59
|
role: o.key
|
|
60
|
-
}), await
|
|
61
|
-
},
|
|
60
|
+
}), await w(i.username, i.key);
|
|
61
|
+
}, D = [
|
|
62
62
|
"Owner",
|
|
63
63
|
"Engineer",
|
|
64
64
|
"Host",
|
|
65
65
|
"Operator",
|
|
66
66
|
"Viewer"
|
|
67
|
-
],
|
|
67
|
+
], O = async (e, t) => {
|
|
68
68
|
let n = (await e.access.roles.retrieve({})).find((e) => e.name === t);
|
|
69
69
|
if (n == null) throw new a(`no built-in role named ${t}`);
|
|
70
|
-
let r = await
|
|
70
|
+
let r = await T(e);
|
|
71
71
|
return await e.access.roles.assign({
|
|
72
72
|
user: r.key,
|
|
73
73
|
role: n.key
|
|
74
|
-
}), await
|
|
75
|
-
},
|
|
74
|
+
}), await w(r.username, r.key);
|
|
75
|
+
}, k = class {
|
|
76
76
|
client;
|
|
77
77
|
byRole = /* @__PURE__ */ new Map();
|
|
78
78
|
constructor(e) {
|
|
@@ -81,10 +81,10 @@ var b = (e) => {
|
|
|
81
81
|
async get(e) {
|
|
82
82
|
let t = this.byRole.get(e);
|
|
83
83
|
if (t != null) return await t;
|
|
84
|
-
let n =
|
|
84
|
+
let n = O(this.client, e);
|
|
85
85
|
return this.byRole.set(e, n), await n;
|
|
86
86
|
}
|
|
87
|
-
},
|
|
87
|
+
}, A = async (e) => {
|
|
88
88
|
let t = await e.channels.create({
|
|
89
89
|
leaseholder: 1,
|
|
90
90
|
name: m.create(),
|
|
@@ -97,7 +97,7 @@ var b = (e) => {
|
|
|
97
97
|
dataType: l.FLOAT64,
|
|
98
98
|
index: t.key
|
|
99
99
|
})];
|
|
100
|
-
},
|
|
100
|
+
}, j = async (e) => {
|
|
101
101
|
let t = await e.channels.create({
|
|
102
102
|
leaseholder: 1,
|
|
103
103
|
name: m.create(),
|
|
@@ -110,80 +110,100 @@ var b = (e) => {
|
|
|
110
110
|
dataType: l.BOOLEAN,
|
|
111
111
|
index: t.key
|
|
112
112
|
})];
|
|
113
|
-
},
|
|
113
|
+
}, M = async (e) => await e.channels.create({
|
|
114
114
|
name: m.create(),
|
|
115
115
|
dataType: l.FLOAT64,
|
|
116
116
|
virtual: !0
|
|
117
|
-
}),
|
|
117
|
+
}), N = async (e) => await e.channels.create({
|
|
118
118
|
name: m.create(),
|
|
119
119
|
dataType: l.BOOLEAN,
|
|
120
120
|
virtual: !0
|
|
121
|
-
}),
|
|
121
|
+
}), P = async (e, t, n = u.seconds(5)) => t(e.status) ? e.status : await new Promise((r, i) => {
|
|
122
122
|
let a = setTimeout(() => {
|
|
123
123
|
o(), i(/* @__PURE__ */ Error("timed out waiting for connection status"));
|
|
124
124
|
}, n.milliseconds), o = e.onChange((e) => {
|
|
125
125
|
t(e) && (clearTimeout(a), o(), r(e));
|
|
126
126
|
});
|
|
127
|
-
}),
|
|
127
|
+
}), F = async (e, t) => await P(e, ({ variant: e, details: t }) => e === "success" && t.streamLive, t), I = async (e) => {
|
|
128
128
|
let t = await e.projects.create({
|
|
129
129
|
name: `project-${m.create()}`,
|
|
130
130
|
layout: {}
|
|
131
131
|
});
|
|
132
132
|
return o(t.key);
|
|
133
|
-
},
|
|
133
|
+
}, L = {
|
|
134
134
|
host: "localhost",
|
|
135
135
|
port: 9090
|
|
136
|
-
},
|
|
136
|
+
}, R = {
|
|
137
137
|
baseInterval: u.milliseconds(10),
|
|
138
138
|
maxInterval: u.milliseconds(50),
|
|
139
139
|
scale: 1.5
|
|
140
|
-
},
|
|
140
|
+
}, z = async (e = {}) => {
|
|
141
141
|
let { host: t, port: n } = {
|
|
142
|
-
...
|
|
142
|
+
...L,
|
|
143
143
|
...e
|
|
144
|
-
}, r = /* @__PURE__ */ new Set(), i = !1,
|
|
145
|
-
if (
|
|
144
|
+
}, r = /* @__PURE__ */ new Set(), i = /* @__PURE__ */ new Set(), a = !1, o = v((e) => {
|
|
145
|
+
if (a) {
|
|
146
146
|
e.destroy();
|
|
147
147
|
return;
|
|
148
148
|
}
|
|
149
|
-
let
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
149
|
+
let i = _(n, t), o = {
|
|
150
|
+
downstream: e,
|
|
151
|
+
upstream: i,
|
|
152
|
+
ws: !1
|
|
153
153
|
};
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
154
|
+
r.add(o), e.on("data", (e) => {
|
|
155
|
+
!o.ws && /upgrade:\s*websocket/i.test(e.toString("latin1")) && (o.ws = !0);
|
|
156
|
+
});
|
|
157
|
+
let s = () => {
|
|
158
|
+
r.delete(o), e.destroy(), i.destroy();
|
|
159
|
+
};
|
|
160
|
+
e.on("error", s), i.on("error", s), e.on("close", s), i.on("close", s), e.pipe(i), i.pipe(e);
|
|
161
|
+
}), s = async (e) => await new Promise((t, n) => {
|
|
162
|
+
o.once("error", n), o.listen(e, () => {
|
|
163
|
+
o.removeListener("error", n), t();
|
|
158
164
|
});
|
|
159
165
|
});
|
|
160
|
-
await
|
|
161
|
-
let
|
|
162
|
-
if (
|
|
163
|
-
let
|
|
164
|
-
|
|
165
|
-
let e = new Promise((e) =>
|
|
166
|
-
r.forEach((
|
|
166
|
+
await s(0);
|
|
167
|
+
let c = o.address();
|
|
168
|
+
if (c == null || typeof c == "string") throw Error("proxy failed to bind a port");
|
|
169
|
+
let l = c.port, u = async () => {
|
|
170
|
+
a = !0;
|
|
171
|
+
let e = new Promise((e) => o.close(() => e()));
|
|
172
|
+
r.forEach(({ downstream: e, upstream: t }) => {
|
|
173
|
+
e.destroy(), t.destroy();
|
|
174
|
+
}), r.clear(), i.forEach((e) => e.destroy()), i.clear(), await e;
|
|
167
175
|
};
|
|
168
176
|
return {
|
|
169
|
-
port:
|
|
170
|
-
sever:
|
|
177
|
+
port: l,
|
|
178
|
+
sever: u,
|
|
179
|
+
blackholeStreams: () => {
|
|
180
|
+
let e = 0;
|
|
181
|
+
for (let t of [...r]) {
|
|
182
|
+
if (!t.ws) continue;
|
|
183
|
+
e += 1;
|
|
184
|
+
let { downstream: n, upstream: a } = t;
|
|
185
|
+
n.unpipe(a), a.unpipe(n);
|
|
186
|
+
for (let e of [n, a]) e.removeAllListeners("close"), e.removeAllListeners("error"), e.on("error", () => {});
|
|
187
|
+
n.on("data", () => {}), n.resume(), a.destroy(), r.delete(t), i.add(n);
|
|
188
|
+
}
|
|
189
|
+
return e;
|
|
190
|
+
},
|
|
171
191
|
restore: async () => {
|
|
172
|
-
|
|
192
|
+
a = !1, await s(l);
|
|
173
193
|
},
|
|
174
|
-
close:
|
|
194
|
+
close: u
|
|
175
195
|
};
|
|
176
|
-
},
|
|
196
|
+
}, B = (e) => {
|
|
177
197
|
if (!i(e)) throw Error("expected a live cached answer");
|
|
178
198
|
return e;
|
|
179
|
-
},
|
|
199
|
+
}, V = (e) => {
|
|
180
200
|
if (!t.matches(e)) throw Error("expected a deleted answer");
|
|
181
201
|
return e;
|
|
182
|
-
},
|
|
183
|
-
resolveDataType: (e) =>
|
|
184
|
-
convert: (e, t) =>
|
|
202
|
+
}, H = (e, t) => Array.from({ length: t }, (t, n) => e + n).map((e) => d.seconds(e)), U = (e) => e.isVariable || e.equals(l.UINT8), W = {
|
|
203
|
+
resolveDataType: (e) => U(e) ? e : l.FLOAT32,
|
|
204
|
+
convert: (e, t) => U(e.dataType) ? e : (t == null && e.dataType.usesBigInt && e.length > 0 && (t = BigInt(e.data[0])), e.convert(l.FLOAT32, t))
|
|
185
205
|
};
|
|
186
206
|
//#endregion
|
|
187
|
-
export {
|
|
207
|
+
export { D as BUILT_IN_ROLES, R as FAST_RETRY, k as RoleClients, y as TEST_CLIENT_PARAMS, I as createPanelParent, z as createSeverableProxy, x as createTestClient, E as createTestClientWithPolicy, O as createTestClientWithRole, V as expectDeleted, B as expectLive, W as glTransform, i as isLive, j as newIndexedBoolPair, A as newIndexedPair, N as newVirtualBoolChannel, M as newVirtualChannel, H as secondsLinspace, S as spyOnSend, P as waitForStatus, F as waitForStreamLive };
|
|
188
208
|
|
|
189
209
|
//# sourceMappingURL=testutil.js.map
|
package/dist/testutil.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"testutil.js","names":[],"sources":["../src/testutil/client.ts","../src/testutil/access.ts","../src/testutil/channels.ts","../src/testutil/connection.ts","../src/testutil/panels.ts","../src/testutil/proxy.ts","../src/testutil/query.ts","../src/testutil/telem.ts"],"sourcesContent":["// Copyright 2026 Synnax Labs, Inc.\n//\n// Use of this software is governed by the Business Source License included in the file\n// licenses/BSL.txt.\n//\n// As of the Change Date specified in that file, in accordance with the Business Source\n// License, use of this software will be governed by the Apache License, Version 2.0,\n// included in the file licenses/APL.txt.\n\nimport { TimeSpan } from \"@synnaxlabs/x\";\nimport { afterAll } from \"vitest\";\n\nimport Synnax, { type SynnaxParams } from \"@/client\";\n\nexport const TEST_CLIENT_PARAMS: SynnaxParams = {\n host: \"localhost\",\n port: 9090,\n username: \"synnax\",\n password: \"seldon\",\n retry: {\n maxRetries: 4,\n baseInterval: TimeSpan.seconds(1),\n scale: 1.5,\n },\n};\n\n// A hook registered mid-test is silently dropped by vitest, so per-client afterAll\n// calls leak clients created inside test bodies. This module-level hook registers\n// during spec collection (import time) and covers every call site.\nconst openClients: Synnax[] = [];\n// Awaited so every close finishes before vitest tears down the worker; a close\n// still in flight at teardown races its logging against the closing RPC channel.\nafterAll(async () => {\n const results = await Promise.allSettled(openClients.map((client) => client.close()));\n const failures = results.filter((result) => result.status === \"rejected\");\n if (failures.length > 0)\n throw new AggregateError(\n failures.map((failure) => failure.reason),\n \"failed to close test clients\",\n );\n});\n\n/**\n * Creates a client connected to the local test cluster. The client is closed\n * automatically once every test in the current spec file has finished, so callers do\n * not need to close it themselves.\n */\nexport const createTestClient = (params?: Partial<SynnaxParams>): Synnax => {\n const client = new Synnax({ ...TEST_CLIENT_PARAMS, ...params });\n openClients.push(client);\n return client;\n};\n","// Copyright 2026 Synnax Labs, Inc.\n//\n// Use of this software is governed by the Business Source License included in the file\n// licenses/BSL.txt.\n//\n// As of the Change Date specified in that file, in accordance with the Business Source\n// License, use of this software will be governed by the Apache License, Version 2.0,\n// included in the file licenses/APL.txt.\n\nimport { array, errors, id } from \"@synnaxlabs/x\";\n\nimport { policy } from \"@/access/policy\";\nimport { role } from \"@/access/role\";\nimport type Synnax from \"@/client\";\nimport { AccessDeniedError, NotFoundError } from \"@/errors\";\nimport { createTestClient } from \"@/testutil/client\";\nimport { user } from \"@/user\";\n\n/** Reports whether the error, or any error in its cause chain, is an access denial. */\nconst isDenial = (err: unknown): boolean =>\n AccessDeniedError.matches(err) ||\n (err instanceof Error && err.cause != null && isDenial(err.cause));\n\n/**\n * Connects as a new user and primes its policy cache. Denials are tolerated: the Core\n * refuses restricted subjects by design, and the change-stream refusal logs after\n * teardown, which vitest counts as an unhandled error.\n */\nconst connectAs = async (username: string, key: user.Key) => {\n const client = createTestClient({\n username,\n password: \"test\",\n onInternalError: (err) => {\n if (!isDenial(err)) console.error(err);\n },\n });\n try {\n await client.access.policies.retrieveForSubject(user.ontologyID(key));\n } catch (err) {\n if (!isDenial(err)) throw errors.fromUnknown(err);\n }\n return client;\n};\n\nconst createUser = async (client: Synnax) => {\n const username = id.create();\n const u = await client.users.create({\n username,\n password: \"test\",\n firstName: \"test\",\n lastName: \"test\",\n });\n return { username, key: u.key };\n};\n\n/**\n * Creates a client authenticated as a new user holding a role that carries the\n * policies. Pass several to grant different actions on different objects, which one\n * policy cannot express.\n */\nexport const createTestClientWithPolicy = async (\n client: Synnax,\n pol: policy.New | policy.New[],\n) => {\n const u = await createUser(client);\n const policies = await Promise.all(\n array.toArray(pol).map(async (p) => await client.access.policies.create(p)),\n );\n const r = await client.access.roles.create({\n name: \"test\",\n description: \"test\",\n });\n await client.ontology.addChildren(\n role.ontologyID(r.key),\n ...policies.map(({ key }) => policy.ontologyID(key)),\n );\n await client.access.roles.assign({ user: u.key, role: r.key });\n return await connectAs(u.username, u.key);\n};\n\n/** The roles the Core provisions on startup. */\nexport const BUILT_IN_ROLES = [\n \"Owner\",\n \"Engineer\",\n \"Host\",\n \"Operator\",\n \"Viewer\",\n] as const;\n\nexport type BuiltInRole = (typeof BUILT_IN_ROLES)[number];\n\n/**\n * Creates a client authenticated as a new user holding one of the Core's built-in\n * roles, for exercising a real role's permission set instead of a synthetic policy.\n * @param name - The built-in role to assign.\n * @throws {NotFoundError} if the Core provisions no role under the name.\n */\nexport const createTestClientWithRole = async (\n client: Synnax,\n name: BuiltInRole,\n): Promise<Synnax> => {\n const roles = await client.access.roles.retrieve({});\n const r = roles.find((r) => r.name === name);\n if (r == null) throw new NotFoundError(`no built-in role named ${name}`);\n const u = await createUser(client);\n await client.access.roles.assign({ user: u.key, role: r.key });\n return await connectAs(u.username, u.key);\n};\n\n/**\n * Caches one client per built-in role, so a spec covering many roles pays the user\n * creation cost once each. Construct one per spec file.\n */\nexport class RoleClients {\n private readonly client: Synnax;\n private readonly byRole = new Map<BuiltInRole, Promise<Synnax>>();\n\n constructor(client: Synnax) {\n this.client = client;\n }\n\n /** Returns a client authenticated as a user holding the role. */\n async get(name: BuiltInRole): Promise<Synnax> {\n const existing = this.byRole.get(name);\n if (existing != null) return await existing;\n const created = createTestClientWithRole(this.client, name);\n this.byRole.set(name, created);\n return await created;\n }\n}\n","// Copyright 2026 Synnax Labs, Inc.\n//\n// Use of this software is governed by the Business Source License included in the file\n// licenses/BSL.txt.\n//\n// As of the Change Date specified in that file, in accordance with the Business Source\n// License, use of this software will be governed by the Apache License, Version 2.0,\n// included in the file licenses/APL.txt.\n\nimport { DataType, id } from \"@synnaxlabs/x\";\n\nimport { type channel } from \"@/channel\";\nimport type Synnax from \"@/client\";\n\nexport const newIndexedPair = async (\n client: Synnax,\n): Promise<[channel.Channel, channel.Channel]> => {\n const index = await client.channels.create({\n leaseholder: 1,\n name: id.create(),\n dataType: DataType.TIMESTAMP,\n isIndex: true,\n });\n const data = await client.channels.create({\n leaseholder: 1,\n name: id.create(),\n dataType: DataType.FLOAT64,\n index: index.key,\n });\n return [index, data];\n};\n\nexport const newIndexedBoolPair = async (\n client: Synnax,\n): Promise<[channel.Channel, channel.Channel]> => {\n const index = await client.channels.create({\n leaseholder: 1,\n name: id.create(),\n dataType: DataType.TIMESTAMP,\n isIndex: true,\n });\n const data = await client.channels.create({\n leaseholder: 1,\n name: id.create(),\n dataType: DataType.BOOLEAN,\n index: index.key,\n });\n return [index, data];\n};\n\nexport const newVirtualChannel = async (client: Synnax): Promise<channel.Channel> => {\n const ch = await client.channels.create({\n name: id.create(),\n dataType: DataType.FLOAT64,\n virtual: true,\n });\n return ch;\n};\n\nexport const newVirtualBoolChannel = async (\n client: Synnax,\n): Promise<channel.Channel> => {\n const ch = await client.channels.create({\n name: id.create(),\n dataType: DataType.BOOLEAN,\n virtual: true,\n });\n return ch;\n};\n","// Copyright 2026 Synnax Labs, Inc.\n//\n// Use of this software is governed by the Business Source License included in the file\n// licenses/BSL.txt.\n//\n// As of the Change Date specified in that file, in accordance with the Business Source\n// License, use of this software will be governed by the Apache License, Version 2.0,\n// included in the file licenses/APL.txt.\n\nimport { TimeSpan } from \"@synnaxlabs/x\";\n\nimport { type connection } from \"@/connection\";\n\n/** Resolves once the handle's status satisfies the predicate. */\nexport const waitForStatus = async (\n handle: connection.Handle,\n predicate: (status: connection.Status) => boolean,\n timeout: TimeSpan = TimeSpan.seconds(5),\n): Promise<connection.Status> => {\n if (predicate(handle.status)) return handle.status;\n return await new Promise<connection.Status>((resolve, reject) => {\n const timer = setTimeout(() => {\n detach();\n reject(new Error(\"timed out waiting for connection status\"));\n }, timeout.milliseconds);\n const detach = handle.onChange((status) => {\n if (!predicate(status)) return;\n clearTimeout(timer);\n detach();\n resolve(status);\n });\n });\n};\n\n/**\n * Resolves once the handle's change stream is live, so every later cluster write\n * reaches the client's cache.\n */\nexport const waitForStreamLive = async (\n handle: connection.Handle,\n timeout?: TimeSpan,\n): Promise<connection.Status> =>\n await waitForStatus(\n handle,\n ({ variant, details }) => variant === \"success\" && details.streamLive,\n timeout,\n );\n","// Copyright 2026 Synnax Labs, Inc.\n//\n// Use of this software is governed by the Business Source License included in the file\n// licenses/BSL.txt.\n//\n// As of the Change Date specified in that file, in accordance with the Business Source\n// License, use of this software will be governed by the Apache License, Version 2.0,\n// included in the file licenses/APL.txt.\n\nimport { id } from \"@synnaxlabs/x\";\n\nimport type Synnax from \"@/client\";\nimport { type ontology } from \"@/ontology\";\nimport { project } from \"@/project\";\n\n/**\n * Creates a throwaway project and returns its ontology ID. Panel creation requires a\n * parent, so tests that mint panels directly parent them here.\n */\nexport const createPanelParent = async (client: Synnax): Promise<ontology.ID> => {\n const proj = await client.projects.create({\n name: `project-${id.create()}`,\n layout: {},\n });\n return project.ontologyID(proj.key);\n};\n","// Copyright 2026 Synnax Labs, Inc.\n//\n// Use of this software is governed by the Business Source License included in the file\n// licenses/BSL.txt.\n//\n// As of the Change Date specified in that file, in accordance with the Business Source\n// License, use of this software will be governed by the Apache License, Version 2.0,\n// included in the file licenses/APL.txt.\n\nimport { connect, createServer, type Socket } from \"node:net\";\n\nimport { type breaker, TimeSpan } from \"@synnaxlabs/x\";\n\nconst DEFAULT_TARGET = { host: \"localhost\", port: 9090 };\n\n/**\n * Retry policy for a client pointed at a severable proxy. A spec drives its own\n * retries, so the client's must finish well inside one assertion budget: the\n * default policy waits seconds per request and outlasts a single `expect.poll`\n * attempt, which reports as a timeout rather than the real answer.\n */\nexport const FAST_RETRY: breaker.Config = {\n baseInterval: TimeSpan.milliseconds(10),\n maxInterval: TimeSpan.milliseconds(50),\n scale: 1.5,\n};\n\nexport interface SeverableProxyTarget {\n host?: string;\n port?: number;\n}\n\nexport interface SeverableProxy {\n /** Port the proxy accepts connections on. Point the client here. */\n port: number;\n /** Drops every live connection and refuses new ones, as if the cluster died. */\n sever: () => Promise<void>;\n /** Accepts connections again on the same port. */\n restore: () => Promise<void>;\n /** Shuts the proxy down for good. */\n close: () => Promise<void>;\n}\n\n/**\n * Creates a TCP proxy in front of the live test cluster so specs can simulate\n * cluster downtime without touching the cluster itself. Connect a client to\n * the returned port, then sever() and restore() the link.\n */\nexport const createSeverableProxy = async (\n target: SeverableProxyTarget = {},\n): Promise<SeverableProxy> => {\n const { host, port } = { ...DEFAULT_TARGET, ...target };\n const sockets = new Set<Socket>();\n let severed = false;\n const server = createServer((downstream) => {\n // A connection the kernel accepted before the sever is still delivered after it,\n // and forwarding it would let a request through a severed link.\n if (severed) {\n downstream.destroy();\n return;\n }\n const upstream = connect(port, host);\n sockets.add(downstream).add(upstream);\n const destroy = (): void => {\n sockets.delete(downstream);\n sockets.delete(upstream);\n downstream.destroy();\n upstream.destroy();\n };\n downstream.on(\"error\", destroy);\n upstream.on(\"error\", destroy);\n downstream.on(\"close\", destroy);\n upstream.on(\"close\", destroy);\n downstream.pipe(upstream);\n upstream.pipe(downstream);\n });\n const listen = async (listenPort: number): Promise<void> =>\n await new Promise((resolve, reject) => {\n server.once(\"error\", reject);\n server.listen(listenPort, () => {\n server.removeListener(\"error\", reject);\n resolve();\n });\n });\n await listen(0);\n const address = server.address();\n if (address == null || typeof address === \"string\")\n throw new Error(\"proxy failed to bind a port\");\n const boundPort = address.port;\n const sever = async (): Promise<void> => {\n severed = true;\n const closed = new Promise<void>((resolve) => server.close(() => resolve()));\n sockets.forEach((socket) => socket.destroy());\n sockets.clear();\n await closed;\n };\n return {\n port: boundPort,\n sever,\n restore: async () => {\n severed = false;\n await listen(boundPort);\n },\n close: sever,\n };\n};\n","// Copyright 2026 Synnax Labs, Inc.\n//\n// Use of this software is governed by the Business Source License included in the file\n// licenses/BSL.txt.\n//\n// As of the Change Date specified in that file, in accordance with the Business Source\n// License, use of this software will be governed by the Apache License, Version 2.0,\n// included in the file licenses/APL.txt.\n\nimport { query } from \"@/query\";\n\nexport { isLive } from \"@/query/query\";\n\n/** Asserts the cached answer is live (present and not deleted) and returns it. */\nexport const expectLive = <D extends query.Data>(\n value: query.Cached<D> | undefined,\n): D => {\n if (!query.isLive<D>(value)) throw new Error(\"expected a live cached answer\");\n return value;\n};\n\n/** Asserts the cached answer is a deletion and returns it. */\nexport const expectDeleted = <D extends query.Data>(\n value: query.Cached<D> | undefined,\n): query.Deleted<D> => {\n if (!query.Deleted.matches<D>(value)) throw new Error(\"expected a deleted answer\");\n return value;\n};\n","// Copyright 2026 Synnax Labs, Inc.\n//\n// Use of this software is governed by the Business Source License included in the file\n// licenses/BSL.txt.\n//\n// As of the Change Date specified in that file, in accordance with the Business Source\n// License, use of this software will be governed by the Apache License, Version 2.0,\n// included in the file licenses/APL.txt.\n\nimport { DataType, type Series, TimeStamp } from \"@synnaxlabs/x\";\n\nimport { type framer } from \"@/framer\";\n\nexport const secondsLinspace = (start: number, n: number): TimeStamp[] =>\n Array.from({ length: n }, (_, i) => start + i).map((n) => TimeStamp.seconds(n));\n\nconst isRaw = (dt: DataType): boolean => dt.isVariable || dt.equals(DataType.UINT8);\n\n/** Narrows every numeric series to float32, anchoring bigints on an offset. Mirrors\n * the transform the visualization layer injects for WebGL rendering. */\nexport const glTransform: framer.Transform = {\n resolveDataType: (dt: DataType) => (isRaw(dt) ? dt : DataType.FLOAT32),\n convert: (series: Series, offset) => {\n if (isRaw(series.dataType)) return series;\n if (offset == null && series.dataType.usesBigInt && series.length > 0)\n offset = BigInt(series.data[0]);\n return series.convert(DataType.FLOAT32, offset);\n },\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"testutil.js","names":[],"sources":["../src/testutil/client.ts","../src/testutil/access.ts","../src/testutil/channels.ts","../src/testutil/connection.ts","../src/testutil/panels.ts","../src/testutil/proxy.ts","../src/testutil/query.ts","../src/testutil/telem.ts"],"sourcesContent":["// Copyright 2026 Synnax Labs, Inc.\n//\n// Use of this software is governed by the Business Source License included in the file\n// licenses/BSL.txt.\n//\n// As of the Change Date specified in that file, in accordance with the Business Source\n// License, use of this software will be governed by the Apache License, Version 2.0,\n// included in the file licenses/APL.txt.\n\nimport { TimeSpan } from \"@synnaxlabs/x\";\nimport { afterAll, vi } from \"vitest\";\n\nimport Synnax, { type SynnaxParams } from \"@/client\";\n\nexport const TEST_CLIENT_PARAMS: SynnaxParams = {\n host: \"localhost\",\n port: 9090,\n username: \"synnax\",\n password: \"seldon\",\n retry: {\n maxRetries: 4,\n baseInterval: TimeSpan.seconds(1),\n scale: 1.5,\n },\n};\n\n// A hook registered mid-test is silently dropped by vitest, so per-client afterAll\n// calls leak clients created inside test bodies. This module-level hook registers\n// during spec collection (import time) and covers every call site.\nconst openClients: Synnax[] = [];\n// Awaited so every close finishes before vitest tears down the worker; a close\n// still in flight at teardown races its logging against the closing RPC channel.\nafterAll(async () => {\n const results = await Promise.allSettled(openClients.map((client) => client.close()));\n const failures = results.filter((result) => result.status === \"rejected\");\n if (failures.length > 0)\n throw new AggregateError(\n failures.map((failure) => failure.reason),\n \"failed to close test clients\",\n );\n});\n\n/**\n * Creates a client connected to the local test cluster. The client is closed\n * automatically once every test in the current spec file has finished, so callers do\n * not need to close it themselves.\n */\nexport const createTestClient = (params?: Partial<SynnaxParams>): Synnax => {\n const client = new Synnax({ ...TEST_CLIENT_PARAMS, ...params });\n openClients.push(client);\n return client;\n};\n\n/** Spies on the client's unary send so specs can count requests on the wire. */\nexport const spyOnSend = (client: Synnax, field: \"transport\" = \"transport\") =>\n vi.spyOn(client[field].unary, \"send\");\n","// Copyright 2026 Synnax Labs, Inc.\n//\n// Use of this software is governed by the Business Source License included in the file\n// licenses/BSL.txt.\n//\n// As of the Change Date specified in that file, in accordance with the Business Source\n// License, use of this software will be governed by the Apache License, Version 2.0,\n// included in the file licenses/APL.txt.\n\nimport { array, errors, id } from \"@synnaxlabs/x\";\n\nimport { policy } from \"@/access/policy\";\nimport { role } from \"@/access/role\";\nimport type Synnax from \"@/client\";\nimport { AccessDeniedError, NotFoundError } from \"@/errors\";\nimport { createTestClient } from \"@/testutil/client\";\nimport { user } from \"@/user\";\n\n/** Reports whether the error, or any error in its cause chain, is an access denial. */\nconst isDenial = (err: unknown): boolean =>\n AccessDeniedError.matches(err) ||\n (err instanceof Error && err.cause != null && isDenial(err.cause));\n\n/**\n * Connects as a new user and primes its policy cache. Denials are tolerated: the Core\n * refuses restricted subjects by design, and the change-stream refusal logs after\n * teardown, which vitest counts as an unhandled error.\n */\nconst connectAs = async (username: string, key: user.Key) => {\n const client = createTestClient({\n username,\n password: \"test\",\n onInternalError: (err) => {\n if (!isDenial(err)) console.error(err);\n },\n });\n try {\n await client.access.policies.retrieveForSubject(user.ontologyID(key));\n } catch (err) {\n if (!isDenial(err)) throw errors.fromUnknown(err);\n }\n return client;\n};\n\nconst createUser = async (client: Synnax) => {\n const username = id.create();\n const u = await client.users.create({\n username,\n password: \"test\",\n firstName: \"test\",\n lastName: \"test\",\n });\n return { username, key: u.key };\n};\n\n/**\n * Creates a client authenticated as a new user holding a role that carries the\n * policies. Pass several to grant different actions on different objects, which one\n * policy cannot express.\n */\nexport const createTestClientWithPolicy = async (\n client: Synnax,\n pol: policy.New | policy.New[],\n) => {\n const u = await createUser(client);\n const policies = await Promise.all(\n array.toArray(pol).map(async (p) => await client.access.policies.create(p)),\n );\n const r = await client.access.roles.create({\n name: \"test\",\n description: \"test\",\n });\n await client.ontology.addChildren(\n role.ontologyID(r.key),\n ...policies.map(({ key }) => policy.ontologyID(key)),\n );\n await client.access.roles.assign({ user: u.key, role: r.key });\n return await connectAs(u.username, u.key);\n};\n\n/** The roles the Core provisions on startup. */\nexport const BUILT_IN_ROLES = [\n \"Owner\",\n \"Engineer\",\n \"Host\",\n \"Operator\",\n \"Viewer\",\n] as const;\n\nexport type BuiltInRole = (typeof BUILT_IN_ROLES)[number];\n\n/**\n * Creates a client authenticated as a new user holding one of the Core's built-in\n * roles, for exercising a real role's permission set instead of a synthetic policy.\n * @param name - The built-in role to assign.\n * @throws {NotFoundError} if the Core provisions no role under the name.\n */\nexport const createTestClientWithRole = async (\n client: Synnax,\n name: BuiltInRole,\n): Promise<Synnax> => {\n const roles = await client.access.roles.retrieve({});\n const r = roles.find((r) => r.name === name);\n if (r == null) throw new NotFoundError(`no built-in role named ${name}`);\n const u = await createUser(client);\n await client.access.roles.assign({ user: u.key, role: r.key });\n return await connectAs(u.username, u.key);\n};\n\n/**\n * Caches one client per built-in role, so a spec covering many roles pays the user\n * creation cost once each. Construct one per spec file.\n */\nexport class RoleClients {\n private readonly client: Synnax;\n private readonly byRole = new Map<BuiltInRole, Promise<Synnax>>();\n\n constructor(client: Synnax) {\n this.client = client;\n }\n\n /** Returns a client authenticated as a user holding the role. */\n async get(name: BuiltInRole): Promise<Synnax> {\n const existing = this.byRole.get(name);\n if (existing != null) return await existing;\n const created = createTestClientWithRole(this.client, name);\n this.byRole.set(name, created);\n return await created;\n }\n}\n","// Copyright 2026 Synnax Labs, Inc.\n//\n// Use of this software is governed by the Business Source License included in the file\n// licenses/BSL.txt.\n//\n// As of the Change Date specified in that file, in accordance with the Business Source\n// License, use of this software will be governed by the Apache License, Version 2.0,\n// included in the file licenses/APL.txt.\n\nimport { DataType, id } from \"@synnaxlabs/x\";\n\nimport { type channel } from \"@/channel\";\nimport type Synnax from \"@/client\";\n\nexport const newIndexedPair = async (\n client: Synnax,\n): Promise<[channel.Channel, channel.Channel]> => {\n const index = await client.channels.create({\n leaseholder: 1,\n name: id.create(),\n dataType: DataType.TIMESTAMP,\n isIndex: true,\n });\n const data = await client.channels.create({\n leaseholder: 1,\n name: id.create(),\n dataType: DataType.FLOAT64,\n index: index.key,\n });\n return [index, data];\n};\n\nexport const newIndexedBoolPair = async (\n client: Synnax,\n): Promise<[channel.Channel, channel.Channel]> => {\n const index = await client.channels.create({\n leaseholder: 1,\n name: id.create(),\n dataType: DataType.TIMESTAMP,\n isIndex: true,\n });\n const data = await client.channels.create({\n leaseholder: 1,\n name: id.create(),\n dataType: DataType.BOOLEAN,\n index: index.key,\n });\n return [index, data];\n};\n\nexport const newVirtualChannel = async (client: Synnax): Promise<channel.Channel> => {\n const ch = await client.channels.create({\n name: id.create(),\n dataType: DataType.FLOAT64,\n virtual: true,\n });\n return ch;\n};\n\nexport const newVirtualBoolChannel = async (\n client: Synnax,\n): Promise<channel.Channel> => {\n const ch = await client.channels.create({\n name: id.create(),\n dataType: DataType.BOOLEAN,\n virtual: true,\n });\n return ch;\n};\n","// Copyright 2026 Synnax Labs, Inc.\n//\n// Use of this software is governed by the Business Source License included in the file\n// licenses/BSL.txt.\n//\n// As of the Change Date specified in that file, in accordance with the Business Source\n// License, use of this software will be governed by the Apache License, Version 2.0,\n// included in the file licenses/APL.txt.\n\nimport { TimeSpan } from \"@synnaxlabs/x\";\n\nimport { type connection } from \"@/connection\";\n\n/** Resolves once the handle's status satisfies the predicate. */\nexport const waitForStatus = async (\n handle: connection.Handle,\n predicate: (status: connection.Status) => boolean,\n timeout: TimeSpan = TimeSpan.seconds(5),\n): Promise<connection.Status> => {\n if (predicate(handle.status)) return handle.status;\n return await new Promise<connection.Status>((resolve, reject) => {\n const timer = setTimeout(() => {\n detach();\n reject(new Error(\"timed out waiting for connection status\"));\n }, timeout.milliseconds);\n const detach = handle.onChange((status) => {\n if (!predicate(status)) return;\n clearTimeout(timer);\n detach();\n resolve(status);\n });\n });\n};\n\n/**\n * Resolves once the handle's change stream is live, so every later cluster write\n * reaches the client's cache.\n */\nexport const waitForStreamLive = async (\n handle: connection.Handle,\n timeout?: TimeSpan,\n): Promise<connection.Status> =>\n await waitForStatus(\n handle,\n ({ variant, details }) => variant === \"success\" && details.streamLive,\n timeout,\n );\n","// Copyright 2026 Synnax Labs, Inc.\n//\n// Use of this software is governed by the Business Source License included in the file\n// licenses/BSL.txt.\n//\n// As of the Change Date specified in that file, in accordance with the Business Source\n// License, use of this software will be governed by the Apache License, Version 2.0,\n// included in the file licenses/APL.txt.\n\nimport { id } from \"@synnaxlabs/x\";\n\nimport type Synnax from \"@/client\";\nimport { type ontology } from \"@/ontology\";\nimport { project } from \"@/project\";\n\n/**\n * Creates a throwaway project and returns its ontology ID. Panel creation requires a\n * parent, so tests that mint panels directly parent them here.\n */\nexport const createPanelParent = async (client: Synnax): Promise<ontology.ID> => {\n const proj = await client.projects.create({\n name: `project-${id.create()}`,\n layout: {},\n });\n return project.ontologyID(proj.key);\n};\n","// Copyright 2026 Synnax Labs, Inc.\n//\n// Use of this software is governed by the Business Source License included in the file\n// licenses/BSL.txt.\n//\n// As of the Change Date specified in that file, in accordance with the Business Source\n// License, use of this software will be governed by the Apache License, Version 2.0,\n// included in the file licenses/APL.txt.\n\nimport { connect, createServer, type Socket } from \"node:net\";\n\nimport { type breaker, TimeSpan } from \"@synnaxlabs/x\";\n\nconst DEFAULT_TARGET = { host: \"localhost\", port: 9090 };\n\n/**\n * Retry policy for a client pointed at a severable proxy. A spec drives its own\n * retries, so the client's must finish well inside one assertion budget: the\n * default policy waits seconds per request and outlasts a single `expect.poll`\n * attempt, which reports as a timeout rather than the real answer.\n */\nexport const FAST_RETRY: breaker.Config = {\n baseInterval: TimeSpan.milliseconds(10),\n maxInterval: TimeSpan.milliseconds(50),\n scale: 1.5,\n};\n\nexport interface SeverableProxyTarget {\n host?: string;\n port?: number;\n}\n\nexport interface SeverableProxy {\n /** Port the proxy accepts connections on. Point the client here. */\n port: number;\n /** Drops every live connection and refuses new ones, as if the cluster died. */\n sever: () => Promise<void>;\n /**\n * Silently kills every live WebSocket connection, modeling a half-open socket (NAT\n * timeout, VPN drop, laptop sleep): the cluster side is destroyed while the client\n * side is held open with its writes discarded, so no close or reset ever reaches the\n * client. New connections still forward. Returns how many connections were\n * blackholed.\n */\n blackholeStreams: () => number;\n /** Accepts connections again on the same port. */\n restore: () => Promise<void>;\n /** Shuts the proxy down for good. */\n close: () => Promise<void>;\n}\n\ninterface ProxiedPair {\n downstream: Socket;\n upstream: Socket;\n /** Whether a WebSocket upgrade was observed on this connection. */\n ws: boolean;\n}\n\n/**\n * Creates a TCP proxy in front of the live test cluster so specs can simulate\n * cluster downtime without touching the cluster itself. Connect a client to\n * the returned port, then sever() and restore() the link.\n */\nexport const createSeverableProxy = async (\n target: SeverableProxyTarget = {},\n): Promise<SeverableProxy> => {\n const { host, port } = { ...DEFAULT_TARGET, ...target };\n const pairs = new Set<ProxiedPair>();\n // Client-side sockets of blackholed connections: held open, silent, until the proxy\n // is severed or closed.\n const zombies = new Set<Socket>();\n let severed = false;\n const server = createServer((downstream) => {\n // A connection the kernel accepted before the sever is still delivered after it,\n // and forwarding it would let a request through a severed link.\n if (severed) {\n downstream.destroy();\n return;\n }\n const upstream = connect(port, host);\n const pair: ProxiedPair = { downstream, upstream, ws: false };\n pairs.add(pair);\n // The upgrade can arrive on any request of a pooled keep-alive connection, not just\n // the first, so every chunk is sniffed until one is seen.\n downstream.on(\"data\", (chunk: Buffer) => {\n if (!pair.ws && /upgrade:\\s*websocket/i.test(chunk.toString(\"latin1\")))\n pair.ws = true;\n });\n const destroy = (): void => {\n pairs.delete(pair);\n downstream.destroy();\n upstream.destroy();\n };\n downstream.on(\"error\", destroy);\n upstream.on(\"error\", destroy);\n downstream.on(\"close\", destroy);\n upstream.on(\"close\", destroy);\n downstream.pipe(upstream);\n upstream.pipe(downstream);\n });\n const listen = async (listenPort: number): Promise<void> =>\n await new Promise((resolve, reject) => {\n server.once(\"error\", reject);\n server.listen(listenPort, () => {\n server.removeListener(\"error\", reject);\n resolve();\n });\n });\n await listen(0);\n const address = server.address();\n if (address == null || typeof address === \"string\")\n throw new Error(\"proxy failed to bind a port\");\n const boundPort = address.port;\n const sever = async (): Promise<void> => {\n severed = true;\n const closed = new Promise<void>((resolve) => server.close(() => resolve()));\n pairs.forEach(({ downstream, upstream }) => {\n downstream.destroy();\n upstream.destroy();\n });\n pairs.clear();\n zombies.forEach((socket) => socket.destroy());\n zombies.clear();\n await closed;\n };\n const blackholeStreams = (): number => {\n let count = 0;\n for (const pair of [...pairs]) {\n if (!pair.ws) continue;\n count += 1;\n const { downstream, upstream } = pair;\n downstream.unpipe(upstream);\n upstream.unpipe(downstream);\n for (const socket of [downstream, upstream]) {\n socket.removeAllListeners(\"close\");\n socket.removeAllListeners(\"error\");\n socket.on(\"error\", () => {});\n }\n // Discard anything the client still writes so backpressure cannot close the\n // socket on the proxy's behalf.\n downstream.on(\"data\", () => {});\n downstream.resume();\n upstream.destroy();\n pairs.delete(pair);\n zombies.add(downstream);\n }\n return count;\n };\n return {\n port: boundPort,\n sever,\n blackholeStreams,\n restore: async () => {\n severed = false;\n await listen(boundPort);\n },\n close: sever,\n };\n};\n","// Copyright 2026 Synnax Labs, Inc.\n//\n// Use of this software is governed by the Business Source License included in the file\n// licenses/BSL.txt.\n//\n// As of the Change Date specified in that file, in accordance with the Business Source\n// License, use of this software will be governed by the Apache License, Version 2.0,\n// included in the file licenses/APL.txt.\n\nimport { query } from \"@/query\";\n\nexport { isLive } from \"@/query/query\";\n\n/** Asserts the cached answer is live (present and not deleted) and returns it. */\nexport const expectLive = <D extends query.Data>(\n value: query.Cached<D> | undefined,\n): D => {\n if (!query.isLive<D>(value)) throw new Error(\"expected a live cached answer\");\n return value;\n};\n\n/** Asserts the cached answer is a deletion and returns it. */\nexport const expectDeleted = <D extends query.Data>(\n value: query.Cached<D> | undefined,\n): query.Deleted<D> => {\n if (!query.Deleted.matches<D>(value)) throw new Error(\"expected a deleted answer\");\n return value;\n};\n","// Copyright 2026 Synnax Labs, Inc.\n//\n// Use of this software is governed by the Business Source License included in the file\n// licenses/BSL.txt.\n//\n// As of the Change Date specified in that file, in accordance with the Business Source\n// License, use of this software will be governed by the Apache License, Version 2.0,\n// included in the file licenses/APL.txt.\n\nimport { DataType, type Series, TimeStamp } from \"@synnaxlabs/x\";\n\nimport { type framer } from \"@/framer\";\n\nexport const secondsLinspace = (start: number, n: number): TimeStamp[] =>\n Array.from({ length: n }, (_, i) => start + i).map((n) => TimeStamp.seconds(n));\n\nconst isRaw = (dt: DataType): boolean => dt.isVariable || dt.equals(DataType.UINT8);\n\n/** Narrows every numeric series to float32, anchoring bigints on an offset. Mirrors\n * the transform the visualization layer injects for WebGL rendering. */\nexport const glTransform: framer.Transform = {\n resolveDataType: (dt: DataType) => (isRaw(dt) ? dt : DataType.FLOAT32),\n convert: (series: Series, offset) => {\n if (isRaw(series.dataType)) return series;\n if (offset == null && series.dataType.usesBigInt && series.length > 0)\n offset = BigInt(series.data[0]);\n return series.convert(DataType.FLOAT32, offset);\n },\n};\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@synnaxlabs/client",
|
|
3
|
-
"version": "0.57.
|
|
3
|
+
"version": "0.57.3",
|
|
4
4
|
"description": "The Synnax Client Library",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"synnax",
|
|
@@ -25,9 +25,9 @@
|
|
|
25
25
|
"dependencies": {
|
|
26
26
|
"immer": "^11.1.15",
|
|
27
27
|
"zod": "^4.4.3",
|
|
28
|
-
"@synnaxlabs/
|
|
28
|
+
"@synnaxlabs/alamos": "^0.57.0",
|
|
29
29
|
"@synnaxlabs/x": "^0.57.1",
|
|
30
|
-
"@synnaxlabs/
|
|
30
|
+
"@synnaxlabs/freighter": "^0.57.0"
|
|
31
31
|
},
|
|
32
32
|
"peerDependencies": {
|
|
33
33
|
"vitest": "^4.1.10"
|
package/src/arc/client.ts
CHANGED
|
@@ -120,24 +120,6 @@ const isTaskChild = (rel: ontology.Relationship, arcKey: Key): boolean =>
|
|
|
120
120
|
to: { type: "task" },
|
|
121
121
|
});
|
|
122
122
|
|
|
123
|
-
const taskStatusZ = z.object({ details: z.object({ task: task.keyZ }) });
|
|
124
|
-
|
|
125
|
-
// Task statuses may arrive under any status key; the referenced task lives in
|
|
126
|
-
// the details, with the "task:<key>" status key as a fallback.
|
|
127
|
-
const affectedTaskKeys = (
|
|
128
|
-
event: query.TableEvent<status.Key, status.Status>,
|
|
129
|
-
): task.Key[] | null => {
|
|
130
|
-
const keys: task.Key[] = [];
|
|
131
|
-
if (event.variant === "set") {
|
|
132
|
-
const parsed = taskStatusZ.safeParse(event.value);
|
|
133
|
-
if (parsed.success) keys.push(parsed.data.details.task);
|
|
134
|
-
}
|
|
135
|
-
const [type, key] = event.key.split(":");
|
|
136
|
-
if (type === "task" && primitive.isNonZero(key) && !keys.includes(key))
|
|
137
|
-
keys.push(key);
|
|
138
|
-
return keys.length === 0 ? null : keys;
|
|
139
|
-
};
|
|
140
|
-
|
|
141
123
|
export interface ClientConfig {
|
|
142
124
|
unary: UnaryClient;
|
|
143
125
|
stream: StreamClient;
|
|
@@ -210,7 +192,7 @@ export class Client extends query.Retriever<
|
|
|
210
192
|
compose: (record) => this.composeTask(record),
|
|
211
193
|
equal: (a, b) => deep.equal(a.payload, b.payload),
|
|
212
194
|
watch: [
|
|
213
|
-
query.deriveWatch(this.cfg.statusStore, (event) =>
|
|
195
|
+
query.deriveWatch(this.cfg.statusStore, (event) => task.affectedKeys(event)),
|
|
214
196
|
],
|
|
215
197
|
});
|
|
216
198
|
this.taskAnswers = cache.queries<Key, task.Task | null, task.Key, task.Task>({
|
|
@@ -473,7 +455,7 @@ export class Client extends query.Retriever<
|
|
|
473
455
|
const cachedStatus = this.cfg.statusStore.get(task.statusKey(cached.key));
|
|
474
456
|
const payload = cached.payload;
|
|
475
457
|
if (cachedStatus == null) return this.cfg.tasks.sugar(payload);
|
|
476
|
-
const parsed = task.
|
|
458
|
+
const parsed = task.defaultStatusZ.safeParse(cachedStatus);
|
|
477
459
|
if (!parsed.success) return this.cfg.tasks.sugar(payload);
|
|
478
460
|
return this.cfg.tasks.sugar({ ...payload, status: parsed.data });
|
|
479
461
|
}
|
package/src/channel/client.ts
CHANGED
|
@@ -389,12 +389,14 @@ export class Client extends query.Retriever<
|
|
|
389
389
|
private readonly cfg: ClientConfig;
|
|
390
390
|
readonly writer: Writer;
|
|
391
391
|
readonly store: query.Table<Key, Channel>;
|
|
392
|
+
private readonly byName: query.LookupIndex<Key, Channel>;
|
|
392
393
|
|
|
393
394
|
constructor(cfg: ClientConfig) {
|
|
394
395
|
const { writer, statuses, ranges, cache } = cfg;
|
|
395
396
|
const statusStore = statuses.store;
|
|
396
397
|
const aliasStore = ranges.aliases;
|
|
397
398
|
const sugar = (payload: Payload): Channel => this.sugar(payload);
|
|
399
|
+
const byName = new query.LookupIndex<Key, Channel>((ch) => ch.name);
|
|
398
400
|
const store = cache.createTable<Key, Channel>({
|
|
399
401
|
name: "channels",
|
|
400
402
|
equal: (a, b) => deep.equal(a.payload, b.payload),
|
|
@@ -406,6 +408,7 @@ export class Client extends query.Retriever<
|
|
|
406
408
|
}),
|
|
407
409
|
query.createDeleteListener(DELETE_CHANNEL_NAME, keyZ),
|
|
408
410
|
],
|
|
411
|
+
indexes: [byName],
|
|
409
412
|
});
|
|
410
413
|
const composed = cache.derive<Key, Channel, Channel>({
|
|
411
414
|
name: "channel.composed",
|
|
@@ -461,6 +464,7 @@ export class Client extends query.Retriever<
|
|
|
461
464
|
this.cfg = cfg;
|
|
462
465
|
this.writer = writer;
|
|
463
466
|
this.store = store;
|
|
467
|
+
this.byName = byName;
|
|
464
468
|
}
|
|
465
469
|
|
|
466
470
|
/**
|
|
@@ -640,7 +644,7 @@ export class Client extends query.Retriever<
|
|
|
640
644
|
}
|
|
641
645
|
const names = normalized;
|
|
642
646
|
await this.writer.delete({ names });
|
|
643
|
-
const cached = this.
|
|
647
|
+
const cached = this.byName.get(names);
|
|
644
648
|
if (cached.length > 0) this.store.delete(cached.map((ch) => ch.key));
|
|
645
649
|
}
|
|
646
650
|
|
|
@@ -765,10 +769,11 @@ export class Client extends query.Retriever<
|
|
|
765
769
|
const { key, rangeKey } = query;
|
|
766
770
|
let ch = this.store.get(key);
|
|
767
771
|
if (ch == null) {
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
+
// Through the table, so concurrent misses coalesce into one request. A plot
|
|
773
|
+
// panel resolves one channel per telemetry source, all in the same tick.
|
|
774
|
+
const fetched = await this.store.retrieve([key]);
|
|
775
|
+
checkForMultipleOrNoResults("channel", key, fetched, true);
|
|
776
|
+
[ch] = fetched;
|
|
772
777
|
}
|
|
773
778
|
// A cached calculated channel without a cached status is ambiguous: the
|
|
774
779
|
// status may not exist, or may simply never have been fetched.
|
|
@@ -794,7 +799,7 @@ export class Client extends query.Retriever<
|
|
|
794
799
|
const resolved: Channel[] = [];
|
|
795
800
|
const missing: string[] = [];
|
|
796
801
|
for (const name of new Set(names)) {
|
|
797
|
-
const matches = this.
|
|
802
|
+
const matches = this.byName.get(name);
|
|
798
803
|
if (matches.length > 1) return null;
|
|
799
804
|
if (matches.length === 0) missing.push(name);
|
|
800
805
|
else resolved.push(matches[0]);
|
package/src/device/client.ts
CHANGED
|
@@ -424,10 +424,11 @@ export class Client extends query.Retriever<
|
|
|
424
424
|
}
|
|
425
425
|
|
|
426
426
|
private async fetchSingle(q: SingleQuery): Promise<Device> {
|
|
427
|
-
//
|
|
427
|
+
// Status-bearing queries bypass the table, which never holds status.
|
|
428
428
|
if (q.includeStatus !== true) {
|
|
429
|
-
const
|
|
430
|
-
|
|
429
|
+
const devices = await this.store.retrieve([q.key]);
|
|
430
|
+
checkForMultipleOrNoResults("Device", q, devices, true);
|
|
431
|
+
return devices[0];
|
|
431
432
|
}
|
|
432
433
|
const devices = await this.execRetrieve(q);
|
|
433
434
|
checkForMultipleOrNoResults("Device", q, devices, true);
|