@synnaxlabs/client 0.57.2 → 0.58.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/dist/{client-BsVW3qAW.js → client-0I0a7Hkw.js} +10121 -9064
- package/dist/client-0I0a7Hkw.js.map +1 -0
- package/dist/client.js +2061 -978
- package/dist/client.js.map +1 -1
- package/dist/src/access/policy/client.d.ts +1 -2
- package/dist/src/access/policy/client.d.ts.map +1 -1
- package/dist/src/arc/client.d.ts.map +1 -1
- package/dist/src/channel/client.d.ts +2 -1
- package/dist/src/channel/client.d.ts.map +1 -1
- package/dist/src/device/client.d.ts +0 -2
- package/dist/src/device/client.d.ts.map +1 -1
- package/dist/src/framer/cache/cache.d.ts.map +1 -1
- package/dist/src/framer/cache/reader.d.ts.map +1 -1
- package/dist/src/framer/cache/static.d.ts +14 -0
- package/dist/src/framer/cache/static.d.ts.map +1 -1
- package/dist/src/framer/cache/unary.d.ts +3 -0
- package/dist/src/framer/cache/unary.d.ts.map +1 -1
- package/dist/src/framer/feed.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/framer/writer.d.ts.map +1 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/lineplot/actions.d.ts +48 -0
- package/dist/src/lineplot/actions.d.ts.map +1 -1
- package/dist/src/lineplot/actions.gen.d.ts +237 -0
- package/dist/src/lineplot/actions.gen.d.ts.map +1 -1
- package/dist/src/lineplot/types.gen.d.ts +87 -7
- package/dist/src/lineplot/types.gen.d.ts.map +1 -1
- package/dist/src/panel/client.d.ts +1 -2
- package/dist/src/panel/client.d.ts.map +1 -1
- package/dist/src/project/client.d.ts +1 -2
- package/dist/src/project/client.d.ts.map +1 -1
- package/dist/src/query/indexes.d.ts +11 -6
- package/dist/src/query/indexes.d.ts.map +1 -1
- package/dist/src/query/table.d.ts +12 -6
- 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 +10 -4
- package/dist/src/ranger/client.d.ts.map +1 -1
- package/dist/src/schematic/actions.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/src/user/client.d.ts +1 -2
- package/dist/src/user/client.d.ts.map +1 -1
- package/dist/testutil.js +80 -60
- package/dist/testutil.js.map +1 -1
- package/package.json +12 -12
- package/src/access/policy/client.ts +0 -2
- package/src/arc/client.ts +2 -20
- package/src/channel/client.ts +12 -8
- package/src/device/client.ts +4 -7
- package/src/framer/cache/cache.ts +9 -1
- package/src/framer/cache/reader.ts +5 -3
- package/src/framer/cache/static.ts +86 -3
- package/src/framer/cache/unary.ts +7 -0
- package/src/framer/feed.ts +2 -0
- package/src/framer/streamer.ts +71 -6
- package/src/framer/writer.ts +4 -5
- package/src/index.ts +2 -0
- package/src/lineplot/actions.gen.ts +30 -0
- package/src/lineplot/actions.ts +14 -0
- package/src/lineplot/types.gen.ts +59 -21
- package/src/panel/client.ts +0 -2
- package/src/project/client.ts +0 -2
- package/src/query/indexes.ts +18 -7
- package/src/query/table.ts +52 -11
- package/src/rack/client.ts +5 -4
- package/src/ranger/client.ts +43 -12
- package/src/schematic/actions.ts +21 -10
- package/src/status/client.ts +1 -3
- package/src/status/status.ts +105 -1
- package/src/task/client.ts +57 -25
- package/src/testutil/client.ts +5 -1
- package/src/testutil/proxy.ts +59 -6
- package/src/user/client.ts +0 -2
- package/dist/client-BsVW3qAW.js.map +0 -1
package/src/framer/streamer.ts
CHANGED
|
@@ -7,8 +7,13 @@
|
|
|
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 {
|
|
11
|
-
|
|
10
|
+
import {
|
|
11
|
+
EOF,
|
|
12
|
+
type Stream,
|
|
13
|
+
Unreachable,
|
|
14
|
+
type WebSocketClient,
|
|
15
|
+
} from "@synnaxlabs/freighter";
|
|
16
|
+
import { errors, Rate, TimeSpan, zod } from "@synnaxlabs/x";
|
|
12
17
|
import { z } from "zod";
|
|
13
18
|
|
|
14
19
|
import { type channel } from "@/channel";
|
|
@@ -23,6 +28,7 @@ const reqZ = z.object({
|
|
|
23
28
|
downsampleFactor: z.int(),
|
|
24
29
|
throttleRate: Rate.z.optional(),
|
|
25
30
|
excludeGroups: z.uint32().array().optional(),
|
|
31
|
+
keepAlive: TimeSpan.z.optional(),
|
|
26
32
|
});
|
|
27
33
|
|
|
28
34
|
/**
|
|
@@ -31,7 +37,11 @@ const reqZ = z.object({
|
|
|
31
37
|
*/
|
|
32
38
|
export interface StreamerRequest extends z.infer<typeof reqZ> {}
|
|
33
39
|
|
|
34
|
-
const resZ = z.object({
|
|
40
|
+
const resZ = z.object({
|
|
41
|
+
frame: frameZ,
|
|
42
|
+
/** Marks an empty response the Core emits so a dead connection is detectable. */
|
|
43
|
+
keepAlive: z.boolean().optional(),
|
|
44
|
+
});
|
|
35
45
|
|
|
36
46
|
/**
|
|
37
47
|
* Response interface for streaming frames from a Synnax cluster.
|
|
@@ -49,6 +59,10 @@ const intermediateStreamerConfigZ = z.object({
|
|
|
49
59
|
/** excludeGroups sets writer group IDs whose frames should be filtered out by the
|
|
50
60
|
Core. Used for telemetry bypass deduplication. */
|
|
51
61
|
excludeGroups: z.uint32().array().default([]),
|
|
62
|
+
/** Interval at which the Core emits keep-alive responses so a silently dead
|
|
63
|
+
connection fails reads instead of hanging forever. TimeSpan.ZERO disables
|
|
64
|
+
detection. Defaults to 5 seconds. */
|
|
65
|
+
keepAlive: TimeSpan.z.default(TimeSpan.seconds(5)),
|
|
52
66
|
});
|
|
53
67
|
|
|
54
68
|
/** Zod schema for {@link StreamerConfig}. A bare channel list parses as a config. */
|
|
@@ -73,7 +87,11 @@ export interface Streamer extends AsyncIterator<Frame>, AsyncIterable<Frame> {
|
|
|
73
87
|
update: (channels: channel.Params) => Promise<void>;
|
|
74
88
|
/** Close the streamer and free all associated resources. */
|
|
75
89
|
close: () => void;
|
|
76
|
-
/**
|
|
90
|
+
/**
|
|
91
|
+
* Read the next frame of telemetry.
|
|
92
|
+
* @throws {Unreachable} if keep-alives were flowing and the stream then stays silent
|
|
93
|
+
* past the keep-alive deadline: the connection is presumed dead.
|
|
94
|
+
*/
|
|
77
95
|
read: () => Promise<Frame>;
|
|
78
96
|
}
|
|
79
97
|
|
|
@@ -101,14 +119,19 @@ export const createStreamOpener =
|
|
|
101
119
|
cfg.downsampleFactor,
|
|
102
120
|
cfg.throttleRate,
|
|
103
121
|
cfg.excludeGroups,
|
|
122
|
+
cfg.keepAlive,
|
|
104
123
|
);
|
|
105
124
|
stream.send({
|
|
106
125
|
keys: Array.from(adapter.keys),
|
|
107
126
|
downsampleFactor: cfg.downsampleFactor,
|
|
108
127
|
throttleRate: cfg.throttleRate,
|
|
109
128
|
excludeGroups: cfg.excludeGroups,
|
|
129
|
+
keepAlive: cfg.keepAlive,
|
|
110
130
|
});
|
|
111
|
-
|
|
131
|
+
// A keep-alive can beat the open ack onto the wire, so the ack is the first
|
|
132
|
+
// non-keep-alive response.
|
|
133
|
+
let res = await stream.receive();
|
|
134
|
+
while (res.keepAlive === true) res = await stream.receive();
|
|
112
135
|
return streamer;
|
|
113
136
|
};
|
|
114
137
|
|
|
@@ -124,12 +147,20 @@ export const openStreamer = async (
|
|
|
124
147
|
config: StreamerConfig,
|
|
125
148
|
): Promise<Streamer> => await createStreamOpener(retrieveChannels, client)(config);
|
|
126
149
|
|
|
150
|
+
// Missing this many keep-alive intervals in a row fails the pending read: one is normal
|
|
151
|
+
// jitter, three is a dead connection.
|
|
152
|
+
const KEEP_ALIVE_DEADLINE_FACTOR = 3;
|
|
153
|
+
|
|
127
154
|
class BaseStreamer implements Streamer {
|
|
128
155
|
private readonly stream: StreamProxy<typeof reqZ, typeof resZ>;
|
|
129
156
|
private readonly adapter: ReadAdapter;
|
|
130
157
|
private readonly downsampleFactor: number;
|
|
131
158
|
private readonly throttleRate: Rate;
|
|
132
159
|
private readonly excludeGroups: number[];
|
|
160
|
+
private readonly deadline: TimeSpan;
|
|
161
|
+
// Set once the Core proves keep-alive support by sending one, so the deadline never
|
|
162
|
+
// arms against a Core that will not send them.
|
|
163
|
+
private armed = false;
|
|
133
164
|
|
|
134
165
|
constructor(
|
|
135
166
|
stream: Stream<typeof reqZ, typeof resZ>,
|
|
@@ -137,12 +168,16 @@ class BaseStreamer implements Streamer {
|
|
|
137
168
|
downsampleFactor: number = 1,
|
|
138
169
|
throttleRate: Rate = new Rate(0),
|
|
139
170
|
excludeGroups: number[] = [],
|
|
171
|
+
keepAlive: TimeSpan = TimeSpan.ZERO,
|
|
140
172
|
) {
|
|
141
173
|
this.stream = new StreamProxy("Streamer", stream);
|
|
142
174
|
this.adapter = adapter;
|
|
143
175
|
this.downsampleFactor = downsampleFactor;
|
|
144
176
|
this.throttleRate = throttleRate;
|
|
145
177
|
this.excludeGroups = excludeGroups;
|
|
178
|
+
this.deadline = TimeSpan.milliseconds(
|
|
179
|
+
keepAlive.milliseconds * KEEP_ALIVE_DEADLINE_FACTOR,
|
|
180
|
+
);
|
|
146
181
|
}
|
|
147
182
|
|
|
148
183
|
get keys(): channel.Key[] {
|
|
@@ -160,7 +195,37 @@ class BaseStreamer implements Streamer {
|
|
|
160
195
|
}
|
|
161
196
|
|
|
162
197
|
async read(): Promise<Frame> {
|
|
163
|
-
|
|
198
|
+
while (true) {
|
|
199
|
+
const res = await this.receiveWithDeadline();
|
|
200
|
+
if (res.keepAlive === true) {
|
|
201
|
+
if (!this.deadline.isZero) this.armed = true;
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
return this.adapter.adapt(new Frame(res.frame));
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
private async receiveWithDeadline(): Promise<z.infer<typeof resZ>> {
|
|
209
|
+
const received = this.stream.receive();
|
|
210
|
+
if (!this.armed) return await received;
|
|
211
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
212
|
+
const deadline = new Promise<never>((_, reject) => {
|
|
213
|
+
timer = setTimeout(() => {
|
|
214
|
+
const silence = this.deadline.toString();
|
|
215
|
+
const message = `streamer received no response for ${silence}`;
|
|
216
|
+
reject(new Unreachable({ message }));
|
|
217
|
+
}, this.deadline.milliseconds);
|
|
218
|
+
});
|
|
219
|
+
try {
|
|
220
|
+
return await Promise.race([received, deadline]);
|
|
221
|
+
} catch (err) {
|
|
222
|
+
// The read already failed for its caller; a late settle of the losing receive
|
|
223
|
+
// must not surface as an unhandled rejection.
|
|
224
|
+
received.catch(() => {});
|
|
225
|
+
throw errors.fromUnknown(err);
|
|
226
|
+
} finally {
|
|
227
|
+
clearTimeout(timer);
|
|
228
|
+
}
|
|
164
229
|
}
|
|
165
230
|
|
|
166
231
|
async update(channels: channel.Params): Promise<void> {
|
package/src/framer/writer.ts
CHANGED
|
@@ -146,9 +146,9 @@ const authorityParamsZ = z
|
|
|
146
146
|
control.authorityZ.optional(),
|
|
147
147
|
])
|
|
148
148
|
.transform(([value, authority]) => {
|
|
149
|
-
if (control.authorityZ
|
|
150
|
-
return { keys: [], authorities: [value
|
|
151
|
-
if (keyZ.or(nameZ)
|
|
149
|
+
if (z.validate(control.authorityZ, value))
|
|
150
|
+
return { keys: [], authorities: [value] };
|
|
151
|
+
if (z.validate(keyZ.or(nameZ), value)) {
|
|
152
152
|
if (authority == null)
|
|
153
153
|
throw new Error(
|
|
154
154
|
"authority is required when setting authority for a single channel",
|
|
@@ -158,8 +158,7 @@ const authorityParamsZ = z
|
|
|
158
158
|
authorities: [authority],
|
|
159
159
|
};
|
|
160
160
|
}
|
|
161
|
-
|
|
162
|
-
return { keys: Object.keys(oValue), authorities: Object.values(oValue) };
|
|
161
|
+
return { keys: Object.keys(value), authorities: Object.values(value) };
|
|
163
162
|
});
|
|
164
163
|
|
|
165
164
|
/**
|
package/src/index.ts
CHANGED
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
// License, use of this software will be governed by the Apache License, Version 2.0,
|
|
8
8
|
// included in the file licenses/APL.txt.
|
|
9
9
|
|
|
10
|
+
import "zod/compile";
|
|
11
|
+
|
|
10
12
|
export { access } from "@/access";
|
|
11
13
|
export type { Policy } from "@/access/policy/types.gen";
|
|
12
14
|
export type { Role } from "@/access/role/types.gen";
|
|
@@ -16,6 +16,7 @@ import { actions } from "@/actions";
|
|
|
16
16
|
import { channel } from "@/channel";
|
|
17
17
|
import {
|
|
18
18
|
axisKeyZ,
|
|
19
|
+
customRangeZ,
|
|
19
20
|
downsampleModeZ,
|
|
20
21
|
keyZ,
|
|
21
22
|
type LinePlot,
|
|
@@ -154,6 +155,16 @@ export const setRangesPayloadZ = z.object({
|
|
|
154
155
|
|
|
155
156
|
export type SetRangesPayload = z.infer<typeof setRangesPayloadZ>;
|
|
156
157
|
|
|
158
|
+
/**
|
|
159
|
+
* SetCustomRange sets the window the "custom" range key resolves to. A null custom
|
|
160
|
+
* clears the window.
|
|
161
|
+
*/
|
|
162
|
+
export const setCustomRangePayloadZ = z.object({
|
|
163
|
+
custom: zod.nullToUndefined(customRangeZ),
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
export type SetCustomRangePayload = z.infer<typeof setCustomRangePayloadZ>;
|
|
167
|
+
|
|
157
168
|
/** SetAxisLabel sets the label rendered along the axis identified by key. */
|
|
158
169
|
export const setAxisLabelPayloadZ = z.object({
|
|
159
170
|
key: axisKeyZ,
|
|
@@ -380,6 +391,10 @@ export const actionZ = z.discriminatedUnion("type", [
|
|
|
380
391
|
z.object({ type: z.literal("add_range"), addRange: addRangePayloadZ }),
|
|
381
392
|
z.object({ type: z.literal("remove_range"), removeRange: removeRangePayloadZ }),
|
|
382
393
|
z.object({ type: z.literal("set_ranges"), setRanges: setRangesPayloadZ }),
|
|
394
|
+
z.object({
|
|
395
|
+
type: z.literal("set_custom_range"),
|
|
396
|
+
setCustomRange: setCustomRangePayloadZ,
|
|
397
|
+
}),
|
|
383
398
|
z.object({ type: z.literal("set_axis_label"), setAxisLabel: setAxisLabelPayloadZ }),
|
|
384
399
|
z.object({
|
|
385
400
|
type: z.literal("set_axis_label_direction"),
|
|
@@ -536,6 +551,15 @@ export const setRanges = (payload: z.input<typeof setRangesPayloadZ>): Action =>
|
|
|
536
551
|
}),
|
|
537
552
|
});
|
|
538
553
|
|
|
554
|
+
export const setCustomRange = (
|
|
555
|
+
payload: z.input<typeof setCustomRangePayloadZ>,
|
|
556
|
+
): Action => ({
|
|
557
|
+
type: "set_custom_range",
|
|
558
|
+
setCustomRange: zod.parse(setCustomRangePayloadZ, payload, {
|
|
559
|
+
label: "line plot set_custom_range action payload",
|
|
560
|
+
}),
|
|
561
|
+
});
|
|
562
|
+
|
|
539
563
|
export const setAxisLabel = (
|
|
540
564
|
payload: z.input<typeof setAxisLabelPayloadZ>,
|
|
541
565
|
): Action => ({
|
|
@@ -748,6 +772,10 @@ export interface Handlers {
|
|
|
748
772
|
addRange: (state: Draft<LinePlot>, payload: AddRangePayload) => HandlerResult;
|
|
749
773
|
removeRange: (state: Draft<LinePlot>, payload: RemoveRangePayload) => HandlerResult;
|
|
750
774
|
setRanges: (state: Draft<LinePlot>, payload: SetRangesPayload) => HandlerResult;
|
|
775
|
+
setCustomRange: (
|
|
776
|
+
state: Draft<LinePlot>,
|
|
777
|
+
payload: SetCustomRangePayload,
|
|
778
|
+
) => HandlerResult;
|
|
751
779
|
setAxisLabel: (state: Draft<LinePlot>, payload: SetAxisLabelPayload) => HandlerResult;
|
|
752
780
|
setAxisLabelDirection: (
|
|
753
781
|
state: Draft<LinePlot>,
|
|
@@ -830,6 +858,8 @@ export const createReduceAll = (handlers: Handlers) =>
|
|
|
830
858
|
return handlers.removeRange(state, action.removeRange);
|
|
831
859
|
case "set_ranges":
|
|
832
860
|
return handlers.setRanges(state, action.setRanges);
|
|
861
|
+
case "set_custom_range":
|
|
862
|
+
return handlers.setCustomRange(state, action.setCustomRange);
|
|
833
863
|
case "set_axis_label":
|
|
834
864
|
return handlers.setAxisLabel(state, action.setAxisLabel);
|
|
835
865
|
case "set_axis_label_direction":
|
package/src/lineplot/actions.ts
CHANGED
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
setAxisTickSpacing,
|
|
26
26
|
setAxisType,
|
|
27
27
|
setChannels,
|
|
28
|
+
setCustomRange,
|
|
28
29
|
setLegendHidden,
|
|
29
30
|
setLegendPosition,
|
|
30
31
|
setLine,
|
|
@@ -261,6 +262,19 @@ const handlers: Handlers = {
|
|
|
261
262
|
};
|
|
262
263
|
},
|
|
263
264
|
|
|
265
|
+
setCustomRange: (state, payload) => {
|
|
266
|
+
const old = state.ranges.custom;
|
|
267
|
+
state.ranges.custom = payload.custom;
|
|
268
|
+
return {
|
|
269
|
+
inverse: [
|
|
270
|
+
setCustomRange({
|
|
271
|
+
custom: old === undefined ? undefined : actions.snapshotDraft(old),
|
|
272
|
+
}),
|
|
273
|
+
],
|
|
274
|
+
targets: ["range:custom"],
|
|
275
|
+
};
|
|
276
|
+
},
|
|
277
|
+
|
|
264
278
|
setAxisLabel: (state, payload) => {
|
|
265
279
|
const axis = state.axes[payload.key];
|
|
266
280
|
const inverse = [setAxisLabel({ key: payload.key, label: axis.label })];
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
// Code generated by Oracle. DO NOT EDIT.
|
|
11
11
|
|
|
12
|
-
import { color, spatial, text, uuid } from "@synnaxlabs/x";
|
|
12
|
+
import { color, spatial, telem, text, uuid, zod } from "@synnaxlabs/x";
|
|
13
13
|
import { z } from "zod";
|
|
14
14
|
|
|
15
15
|
import { channel } from "@/channel";
|
|
@@ -81,26 +81,6 @@ export const channelsZ = z.object({
|
|
|
81
81
|
});
|
|
82
82
|
export interface Channels extends z.infer<typeof channelsZ> {}
|
|
83
83
|
|
|
84
|
-
/** Ranges binds range keys to each x-axis. */
|
|
85
|
-
export const rangesZ = z.object({
|
|
86
|
-
/**
|
|
87
|
-
* x1 are the range keys plotted against the x1 axis. Range keys are opaque strings
|
|
88
|
-
* rather than UUIDs because the console layers synthetic rolling-window ranges (e.g.
|
|
89
|
-
* "recent", "rolling1m") alongside persisted ranges; the server stores whatever the
|
|
90
|
-
* client sends.
|
|
91
|
-
*/
|
|
92
|
-
x1: z
|
|
93
|
-
.string()
|
|
94
|
-
.array()
|
|
95
|
-
.default(() => []),
|
|
96
|
-
/** x2 are the range keys plotted against the x2 axis. */
|
|
97
|
-
x2: z
|
|
98
|
-
.string()
|
|
99
|
-
.array()
|
|
100
|
-
.default(() => []),
|
|
101
|
-
});
|
|
102
|
-
export interface Ranges extends z.infer<typeof rangesZ> {}
|
|
103
|
-
|
|
104
84
|
/**
|
|
105
85
|
* ManualBounds controls whether an axis uses a manually-set bound on each side
|
|
106
86
|
* independently. When a side is false (the default), the corresponding entry in
|
|
@@ -175,6 +155,42 @@ export interface Rule extends z.infer<typeof ruleZ> {}
|
|
|
175
155
|
export const keyZ = z.uuid();
|
|
176
156
|
export type Key = z.infer<typeof keyZ>;
|
|
177
157
|
|
|
158
|
+
/** DynamicCustomRange is a rolling window. */
|
|
159
|
+
export const dynamicCustomRangeZ = z.object({
|
|
160
|
+
variant: z.literal("dynamic"),
|
|
161
|
+
/** span is the span of the window, which ends now. */
|
|
162
|
+
span: telem.numericTimeSpanZ.default(0),
|
|
163
|
+
});
|
|
164
|
+
export interface DynamicCustomRange extends z.infer<typeof dynamicCustomRangeZ> {}
|
|
165
|
+
|
|
166
|
+
/** StaticCustomRange is a fixed window. */
|
|
167
|
+
export const staticCustomRangeZ = z.object({
|
|
168
|
+
variant: z.literal("static"),
|
|
169
|
+
/** start is the inclusive start of the fixed window. */
|
|
170
|
+
start: telem.stringTimeStampZ,
|
|
171
|
+
/** end is the exclusive end of the fixed window. */
|
|
172
|
+
end: telem.stringTimeStampZ,
|
|
173
|
+
});
|
|
174
|
+
export interface StaticCustomRange extends z.infer<typeof staticCustomRangeZ> {}
|
|
175
|
+
|
|
176
|
+
export const CUSTOM_RANGE_TYPES = ["dynamic", "static"] as const;
|
|
177
|
+
export const customRangeTypeZ = z.enum(CUSTOM_RANGE_TYPES);
|
|
178
|
+
export type CustomRangeType = z.infer<typeof customRangeTypeZ>;
|
|
179
|
+
|
|
180
|
+
/** CustomRange is the window a plot's synthetic "custom" range key resolves to. */
|
|
181
|
+
export const customRangeZ = z.discriminatedUnion("variant", [
|
|
182
|
+
dynamicCustomRangeZ,
|
|
183
|
+
staticCustomRangeZ,
|
|
184
|
+
]);
|
|
185
|
+
export type CustomRange = DynamicCustomRange | StaticCustomRange;
|
|
186
|
+
|
|
187
|
+
export const CUSTOM_RANGE_SCHEMAS: {
|
|
188
|
+
[K in CustomRangeType]: z.ZodType<Extract<CustomRange, { variant: K }>>;
|
|
189
|
+
} = {
|
|
190
|
+
dynamic: dynamicCustomRangeZ,
|
|
191
|
+
static: staticCustomRangeZ,
|
|
192
|
+
};
|
|
193
|
+
|
|
178
194
|
/** Axis is the configuration for a single plot axis. */
|
|
179
195
|
export const axisZ = z.object({
|
|
180
196
|
/** key identifies which of the six axes this configuration applies to. */
|
|
@@ -203,6 +219,28 @@ export const axisZ = z.object({
|
|
|
203
219
|
});
|
|
204
220
|
export interface Axis extends z.infer<typeof axisZ> {}
|
|
205
221
|
|
|
222
|
+
/** Ranges binds range keys to each x-axis. */
|
|
223
|
+
export const rangesZ = z.object({
|
|
224
|
+
/**
|
|
225
|
+
* x1 are the range keys plotted against the x1 axis. Range keys are opaque strings
|
|
226
|
+
* rather than UUIDs because the console layers synthetic rolling-window ranges (e.g.
|
|
227
|
+
* "recent", "rolling1m") alongside persisted ranges; the server stores whatever the
|
|
228
|
+
* client sends.
|
|
229
|
+
*/
|
|
230
|
+
x1: z
|
|
231
|
+
.string()
|
|
232
|
+
.array()
|
|
233
|
+
.default(() => []),
|
|
234
|
+
/** x2 are the range keys plotted against the x2 axis. */
|
|
235
|
+
x2: z
|
|
236
|
+
.string()
|
|
237
|
+
.array()
|
|
238
|
+
.default(() => []),
|
|
239
|
+
/** custom is the window the "custom" range key resolves to. */
|
|
240
|
+
custom: zod.nullToUndefined(customRangeZ),
|
|
241
|
+
});
|
|
242
|
+
export interface Ranges extends z.infer<typeof rangesZ> {}
|
|
243
|
+
|
|
206
244
|
/** Axes bundles configuration for all six fixed plot axes. */
|
|
207
245
|
export const axesZ = z.object({
|
|
208
246
|
/** x1 is the x1 axis configuration. */
|
package/src/panel/client.ts
CHANGED
|
@@ -263,8 +263,6 @@ export class Client extends query.Retriever<typeof retrieveMultiParamsZ, Key, Pa
|
|
|
263
263
|
);
|
|
264
264
|
}
|
|
265
265
|
|
|
266
|
-
async delete(key: Key, opts?: query.WriteOptions): Promise<void>;
|
|
267
|
-
async delete(keys: Key[], opts?: query.WriteOptions): Promise<void>;
|
|
268
266
|
async delete(keys: Key | Key[], opts: query.WriteOptions = {}): Promise<void> {
|
|
269
267
|
const keysArr = array.toArray(keys);
|
|
270
268
|
const drop = () => [
|
package/src/project/client.ts
CHANGED
|
@@ -182,8 +182,6 @@ export class Client extends query.Retriever<typeof retrieveMultiParamsZ, Key, Pr
|
|
|
182
182
|
this.mergeThrough(key, { layout });
|
|
183
183
|
}
|
|
184
184
|
|
|
185
|
-
async delete(key: Key, opts?: query.WriteOptions): Promise<void>;
|
|
186
|
-
async delete(keys: Key[], opts?: query.WriteOptions): Promise<void>;
|
|
187
185
|
async delete(keys: Key | Key[], opts: query.WriteOptions = {}): Promise<void> {
|
|
188
186
|
const keysArr = array.toArray(keys);
|
|
189
187
|
const drop = () => [
|
package/src/query/indexes.ts
CHANGED
|
@@ -11,25 +11,32 @@ import { type record, type state } from "@synnaxlabs/x";
|
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
13
|
* An exact-match secondary index over a table's live entries. `extract` reads one
|
|
14
|
-
* index value from each entry
|
|
15
|
-
* param; the table keeps it current
|
|
16
|
-
* included. Tombstones are never
|
|
14
|
+
* index value from each entry; a null leaves the entry out of the index. Register
|
|
15
|
+
* the index on its owning table's `indexes` param; the table keeps it current
|
|
16
|
+
* across every mutation, rollbacks and resets included. Tombstones are never
|
|
17
|
+
* indexed.
|
|
17
18
|
*/
|
|
18
19
|
export class LookupIndex<
|
|
19
20
|
Key extends record.Key = record.Key,
|
|
20
21
|
Value extends state.State = state.State,
|
|
21
22
|
IndexValue extends record.Key = record.Key,
|
|
22
23
|
> {
|
|
23
|
-
private readonly extract: (value: Value) => IndexValue;
|
|
24
|
+
private readonly extract: (value: Value) => IndexValue | null;
|
|
24
25
|
private readonly buckets = new Map<IndexValue, Map<Key, Value>>();
|
|
25
26
|
private readonly indexed = new Map<Key, IndexValue>();
|
|
26
27
|
|
|
27
|
-
constructor(extract: (value: Value) => IndexValue) {
|
|
28
|
+
constructor(extract: (value: Value) => IndexValue | null) {
|
|
28
29
|
this.extract = extract;
|
|
29
30
|
}
|
|
30
31
|
|
|
31
|
-
/**
|
|
32
|
-
|
|
32
|
+
/**
|
|
33
|
+
* Returns the entries whose extracted index value equals the given value. Given an
|
|
34
|
+
* array, returns the entries matching any of the values, in the order of the values
|
|
35
|
+
* and each entry once.
|
|
36
|
+
*/
|
|
37
|
+
get(value: IndexValue | IndexValue[]): Value[] {
|
|
38
|
+
if (Array.isArray(value))
|
|
39
|
+
return Array.from(new Set(value)).flatMap((v) => this.get(v));
|
|
33
40
|
const bucket = this.buckets.get(value);
|
|
34
41
|
if (bucket == null) return [];
|
|
35
42
|
return Array.from(bucket.values());
|
|
@@ -43,6 +50,10 @@ export class LookupIndex<
|
|
|
43
50
|
const next = this.extract(value);
|
|
44
51
|
const prev = this.indexed.get(key);
|
|
45
52
|
if (prev !== undefined && prev !== next) this.unbucket(prev, key);
|
|
53
|
+
if (next == null) {
|
|
54
|
+
this.indexed.delete(key);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
46
57
|
let bucket = this.buckets.get(next);
|
|
47
58
|
if (bucket == null) {
|
|
48
59
|
bucket = new Map();
|
package/src/query/table.ts
CHANGED
|
@@ -177,7 +177,7 @@ export class Table<
|
|
|
177
177
|
this.equal = equal;
|
|
178
178
|
this.fetchEntries = fetch;
|
|
179
179
|
this.hydrateMode = hydrate;
|
|
180
|
-
this.indexes = indexes;
|
|
180
|
+
this.indexes = [...indexes];
|
|
181
181
|
this.fetchBatcher =
|
|
182
182
|
fetch == null
|
|
183
183
|
? null
|
|
@@ -186,7 +186,27 @@ export class Table<
|
|
|
186
186
|
exec: async (requests) => {
|
|
187
187
|
const keys = new Set<Key>();
|
|
188
188
|
requests.forEach(({ req }) => req.forEach((key) => keys.add(key)));
|
|
189
|
-
|
|
189
|
+
let fetched: Array<Keyed<Key, Value>>;
|
|
190
|
+
try {
|
|
191
|
+
fetched = await fetch(Array.from(keys));
|
|
192
|
+
} catch (exc) {
|
|
193
|
+
if (!NotFoundError.matches(exc) || requests.length === 1)
|
|
194
|
+
throw errors.fromUnknown(exc);
|
|
195
|
+
// A strict fetch rejects the whole batch when any caller's key
|
|
196
|
+
// has vanished. Refetch per caller so each settles exactly as
|
|
197
|
+
// its own request would have, keeping the batch transparent.
|
|
198
|
+
await Promise.all(
|
|
199
|
+
requests.map(async ({ req, resolve, reject }) => {
|
|
200
|
+
try {
|
|
201
|
+
const mine = new Set(req);
|
|
202
|
+
resolve((await fetch(req)).filter(({ key }) => mine.has(key)));
|
|
203
|
+
} catch (exc) {
|
|
204
|
+
reject(exc);
|
|
205
|
+
}
|
|
206
|
+
}),
|
|
207
|
+
);
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
190
210
|
// The window's fetch carries other callers' keys too; each caller
|
|
191
211
|
// hydrates only the entries it asked for.
|
|
192
212
|
requests.forEach(({ req, resolve }) => {
|
|
@@ -207,6 +227,17 @@ export class Table<
|
|
|
207
227
|
for (const index of this.indexes) index.delete(key);
|
|
208
228
|
}
|
|
209
229
|
|
|
230
|
+
/**
|
|
231
|
+
* Registers a secondary index and backfills it from the live entries, for a
|
|
232
|
+
* domain that owns an index's meaning but not the table it reads.
|
|
233
|
+
* @returns the index.
|
|
234
|
+
*/
|
|
235
|
+
index<I extends LookupIndex<Key, Value>>(index: I): I {
|
|
236
|
+
this.entries.forEach((value, key) => index.set(key, value));
|
|
237
|
+
this.indexes.push(index);
|
|
238
|
+
return index;
|
|
239
|
+
}
|
|
240
|
+
|
|
210
241
|
private setOne(
|
|
211
242
|
key: Key,
|
|
212
243
|
value: state.SetArg<Value | undefined>,
|
|
@@ -281,11 +312,10 @@ export class Table<
|
|
|
281
312
|
return this.set(arr);
|
|
282
313
|
}
|
|
283
314
|
|
|
284
|
-
/** Returns every entry in the table. */
|
|
285
|
-
get(): Value[];
|
|
315
|
+
/** Returns every entry in the table, or every entry the filter accepts. */
|
|
316
|
+
get(filter?: (value: Value) => boolean): Value[];
|
|
286
317
|
get(key: Key): Value | undefined;
|
|
287
318
|
get(keys: Key[]): Value[];
|
|
288
|
-
get(filter: (value: Value) => boolean): Value[];
|
|
289
319
|
get(keys?: Key | Key[] | ((value: Value) => boolean)): Value | Value[] | undefined {
|
|
290
320
|
if (keys === undefined) return Array.from(this.entries.values());
|
|
291
321
|
if (typeof keys === "function")
|
|
@@ -327,9 +357,10 @@ export class Table<
|
|
|
327
357
|
/**
|
|
328
358
|
* Resolves the given keys to records: serves cached entries and fetches the misses
|
|
329
359
|
* through the table's fetch, hydrating results under the declared mode. With refresh,
|
|
330
|
-
* every key is fetched regardless of presence
|
|
331
|
-
*
|
|
332
|
-
* omitted. Tables without a fetch
|
|
360
|
+
* every key is fetched regardless of presence and cached entries the fetch omits are
|
|
361
|
+
* tombstoned. Returns the table's entries for the found keys in input order,
|
|
362
|
+
* deduplicated; keys the cluster no longer has are omitted. Tables without a fetch
|
|
363
|
+
* serve cached entries only.
|
|
333
364
|
*/
|
|
334
365
|
async retrieve(keys: Key[], opts: { refresh?: boolean } = {}): Promise<Value[]> {
|
|
335
366
|
if (this.fetchBatcher != null) {
|
|
@@ -338,9 +369,19 @@ export class Table<
|
|
|
338
369
|
if (misses.length > 0) {
|
|
339
370
|
const gen = this.gen;
|
|
340
371
|
const fetched = await this.fetchBatcher.enqueue(misses);
|
|
341
|
-
if (gen === this.gen
|
|
342
|
-
if (opts.refresh === true)
|
|
343
|
-
|
|
372
|
+
if (gen === this.gen)
|
|
373
|
+
if (opts.refresh === true) {
|
|
374
|
+
// A refresh is authoritative for its keys: cached entries the
|
|
375
|
+
// fetch omitted vanished from the cluster and are tombstoned.
|
|
376
|
+
const present = new Set<Key>(fetched.map(({ key }) => key));
|
|
377
|
+
const vanished = misses.filter(
|
|
378
|
+
(key) => !present.has(key) && this.entries.has(key),
|
|
379
|
+
);
|
|
380
|
+
this.batch(() => {
|
|
381
|
+
if (vanished.length > 0) this.delete(vanished);
|
|
382
|
+
if (fetched.length > 0) this.set(fetched);
|
|
383
|
+
});
|
|
384
|
+
} else if (fetched.length > 0) this.ingest(fetched);
|
|
344
385
|
}
|
|
345
386
|
}
|
|
346
387
|
const seen = new Set<Key>();
|
package/src/rack/client.ts
CHANGED
|
@@ -307,11 +307,12 @@ export class Client extends query.Retriever<
|
|
|
307
307
|
}
|
|
308
308
|
|
|
309
309
|
private async fetchSingle(q: SingleQuery): Promise<Rack> {
|
|
310
|
-
// Names are not unique
|
|
311
|
-
//
|
|
310
|
+
// Names are not unique and the table never holds status, so only keyed status-free
|
|
311
|
+
// queries can be served from it.
|
|
312
312
|
if ("key" in q && q.includeStatus !== true) {
|
|
313
|
-
const
|
|
314
|
-
|
|
313
|
+
const racks = await this.store.retrieve([q.key]);
|
|
314
|
+
checkForMultipleOrNoResults("Rack", q, racks, true);
|
|
315
|
+
return this.sugar(racks[0]);
|
|
315
316
|
}
|
|
316
317
|
const racks = await this.execRetrieve(q);
|
|
317
318
|
checkForMultipleOrNoResults("Rack", q, racks, true);
|