@synnaxlabs/client 0.57.3 → 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-CX6CMvCc.js → client-0I0a7Hkw.js} +10152 -9184
- 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/channel/client.d.ts +1 -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/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 +4 -5
- package/dist/src/query/indexes.d.ts.map +1 -1
- package/dist/src/query/table.d.ts +2 -3
- package/dist/src/query/table.d.ts.map +1 -1
- package/dist/src/ranger/client.d.ts +1 -3
- package/dist/src/ranger/client.d.ts.map +1 -1
- package/dist/src/schematic/actions.d.ts.map +1 -1
- package/dist/src/status/status.d.ts.map +1 -1
- package/dist/src/task/client.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 +1 -1
- package/package.json +12 -12
- package/src/access/policy/client.ts +0 -2
- package/src/channel/client.ts +1 -2
- package/src/device/client.ts +0 -4
- 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/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 +3 -5
- package/src/query/table.ts +2 -3
- package/src/ranger/client.ts +5 -5
- package/src/schematic/actions.ts +21 -10
- package/src/status/status.ts +91 -1
- package/src/task/client.ts +4 -7
- package/src/user/client.ts +0 -2
- package/dist/client-CX6CMvCc.js.map +0 -1
|
@@ -46,12 +46,19 @@ export interface StaticProps {
|
|
|
46
46
|
* be marked as stale and subject to garbage collection.
|
|
47
47
|
* @default TimeSpan.seconds(20) */
|
|
48
48
|
staleEntryThreshold?: TimeSpan;
|
|
49
|
+
/**
|
|
50
|
+
* Sets how long a fetched span counts as answered before it is refetched. Bounds
|
|
51
|
+
* the refetch rate of spans that stay empty, as every refetch opens a short-lived
|
|
52
|
+
* iterator connection.
|
|
53
|
+
* @default TimeSpan.minutes(10) */
|
|
54
|
+
staleCoverageThreshold?: TimeSpan;
|
|
49
55
|
}
|
|
50
56
|
|
|
51
57
|
export const DEFAULT_STATIC_PROPS: Required<StaticProps> = {
|
|
52
58
|
instrumentation: alamos.NOOP,
|
|
53
59
|
transform: IDENTITY_TRANSFORM,
|
|
54
60
|
staleEntryThreshold: TimeSpan.seconds(20),
|
|
61
|
+
staleCoverageThreshold: TimeSpan.minutes(10),
|
|
55
62
|
};
|
|
56
63
|
|
|
57
64
|
interface CacheEntry {
|
|
@@ -61,6 +68,13 @@ interface CacheEntry {
|
|
|
61
68
|
addedAt: TimeStamp;
|
|
62
69
|
}
|
|
63
70
|
|
|
71
|
+
interface CoveredRange {
|
|
72
|
+
/** The fetched span, held even when the fetch returned no samples. */
|
|
73
|
+
range: TimeRange;
|
|
74
|
+
/** When the fetch completed. Coverage answers gaps until it goes stale. */
|
|
75
|
+
addedAt: TimeStamp;
|
|
76
|
+
}
|
|
77
|
+
|
|
64
78
|
/**
|
|
65
79
|
* A cache for historical channel data that will not be modified after it is written.
|
|
66
80
|
* Fetched and streamed entries are held apart because they measure position in
|
|
@@ -71,6 +85,9 @@ interface CacheEntry {
|
|
|
71
85
|
export class Static {
|
|
72
86
|
private fetched: CacheEntry[] = [];
|
|
73
87
|
private streamed: CacheEntry[] = [];
|
|
88
|
+
// Sorted, non-overlapping spans already fetched, kept apart from the entries so a
|
|
89
|
+
// span that returned no samples still counts as answered and is not refetched.
|
|
90
|
+
private covered: CoveredRange[] = [];
|
|
74
91
|
private readonly props: Required<StaticProps>;
|
|
75
92
|
|
|
76
93
|
constructor(props: StaticProps) {
|
|
@@ -78,8 +95,14 @@ export class Static {
|
|
|
78
95
|
instrumentation = DEFAULT_STATIC_PROPS.instrumentation,
|
|
79
96
|
transform = DEFAULT_STATIC_PROPS.transform,
|
|
80
97
|
staleEntryThreshold = DEFAULT_STATIC_PROPS.staleEntryThreshold,
|
|
98
|
+
staleCoverageThreshold = DEFAULT_STATIC_PROPS.staleCoverageThreshold,
|
|
81
99
|
} = props;
|
|
82
|
-
this.props = {
|
|
100
|
+
this.props = {
|
|
101
|
+
instrumentation,
|
|
102
|
+
transform,
|
|
103
|
+
staleEntryThreshold,
|
|
104
|
+
staleCoverageThreshold,
|
|
105
|
+
};
|
|
83
106
|
}
|
|
84
107
|
|
|
85
108
|
/**
|
|
@@ -98,6 +121,36 @@ export class Static {
|
|
|
98
121
|
this.repairIntegrity(series, entries);
|
|
99
122
|
}
|
|
100
123
|
|
|
124
|
+
/**
|
|
125
|
+
* Records tr as fetched, merging it into the covered set. A covered span counts
|
|
126
|
+
* as answered even when empty, so it is refetched once it goes stale, not per read.
|
|
127
|
+
*/
|
|
128
|
+
markFetched(tr: TimeRange): void {
|
|
129
|
+
if (!tr.isValid || tr.span.isZero) return;
|
|
130
|
+
let { start, end } = tr;
|
|
131
|
+
// Live merges keep the oldest stamp so a rolling read cannot refresh its own
|
|
132
|
+
// coverage forever. Stale records drop, so a refetch restarts their clock.
|
|
133
|
+
let addedAt = TimeStamp.now();
|
|
134
|
+
const keep: CoveredRange[] = [];
|
|
135
|
+
for (const c of this.covered) {
|
|
136
|
+
if (!this.isLive(c)) continue;
|
|
137
|
+
if (c.range.end.before(start) || c.range.start.after(end)) keep.push(c);
|
|
138
|
+
else {
|
|
139
|
+
if (c.range.start.before(start)) start = c.range.start;
|
|
140
|
+
if (c.range.end.after(end)) end = c.range.end;
|
|
141
|
+
if (c.addedAt.before(addedAt)) addedAt = c.addedAt;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
keep.push({ range: new TimeRange(start, end), addedAt });
|
|
145
|
+
keep.sort((a, b) => TimeRange.sort(a.range, b.range));
|
|
146
|
+
this.covered = keep;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Coverage answers gaps only until it goes stale. gc merely prunes dead records.
|
|
150
|
+
private isLive(c: CoveredRange): boolean {
|
|
151
|
+
return TimeStamp.since(c.addedAt).lessThan(this.props.staleCoverageThreshold);
|
|
152
|
+
}
|
|
153
|
+
|
|
101
154
|
// Containment, not overlap: a fetch stamped wider than the data it returned
|
|
102
155
|
// (an uncommitted tail) must not evict streamed samples it did not replace.
|
|
103
156
|
private evictStreamed(written: MultiSeries): void {
|
|
@@ -125,11 +178,12 @@ export class Static {
|
|
|
125
178
|
entries.filter((e) => e.data.timeRange.overlapsWith(tr)).map((e) => e.data);
|
|
126
179
|
const fetched = overlapping(this.fetched);
|
|
127
180
|
const series = [...fetched, ...overlapping(this.streamed)];
|
|
128
|
-
if (fetched.length === 0)
|
|
181
|
+
if (fetched.length === 0)
|
|
182
|
+
return { series: new MultiSeries(series), gaps: this.subtractCovered(tr) };
|
|
129
183
|
const gaps: TimeRange[] = [];
|
|
130
184
|
const pushGap = (start: TimeStamp, end: TimeStamp): void => {
|
|
131
185
|
const gap = new TimeRange(start, end);
|
|
132
|
-
if (gap.isValid && !gap.span.isZero) gaps.push(gap);
|
|
186
|
+
if (gap.isValid && !gap.span.isZero) gaps.push(...this.subtractCovered(gap));
|
|
133
187
|
};
|
|
134
188
|
pushGap(tr.start, fetched[0].timeRange.start);
|
|
135
189
|
for (let i = 1; i < fetched.length; i++)
|
|
@@ -138,6 +192,29 @@ export class Static {
|
|
|
138
192
|
return { series: new MultiSeries(series), gaps };
|
|
139
193
|
}
|
|
140
194
|
|
|
195
|
+
// Removes the covered portions of gap, returning the still-unanswered remainder.
|
|
196
|
+
private subtractCovered(gap: TimeRange): TimeRange[] {
|
|
197
|
+
let remaining = [gap];
|
|
198
|
+
for (const c of this.covered) {
|
|
199
|
+
if (!this.isLive(c)) continue;
|
|
200
|
+
const { range } = c;
|
|
201
|
+
const next: TimeRange[] = [];
|
|
202
|
+
for (const r of remaining) {
|
|
203
|
+
if (!range.overlapsWith(r)) {
|
|
204
|
+
next.push(r);
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
const before = new TimeRange(r.start, range.start);
|
|
208
|
+
const after = new TimeRange(range.end, r.end);
|
|
209
|
+
if (before.isValid && !before.span.isZero) next.push(before);
|
|
210
|
+
if (after.isValid && !after.span.isZero) next.push(after);
|
|
211
|
+
}
|
|
212
|
+
remaining = next;
|
|
213
|
+
if (remaining.length === 0) break;
|
|
214
|
+
}
|
|
215
|
+
return remaining;
|
|
216
|
+
}
|
|
217
|
+
|
|
141
218
|
/**
|
|
142
219
|
* Garbage collects the cache, removing any stale entries.
|
|
143
220
|
* @returns metrics about the garbage collection.
|
|
@@ -158,6 +235,11 @@ export class Static {
|
|
|
158
235
|
});
|
|
159
236
|
this.fetched = collect(this.fetched);
|
|
160
237
|
this.streamed = collect(this.streamed);
|
|
238
|
+
// The client cannot tell "no data yet" from "no data ever" (backfill is legal),
|
|
239
|
+
// so stale coverage re-opens an empty span to one cheap fetch per staleness
|
|
240
|
+
// window instead of one per read (space-heater mode). Series purging above is not
|
|
241
|
+
// affected.
|
|
242
|
+
this.covered = this.covered.filter((c) => this.isLive(c));
|
|
161
243
|
return res;
|
|
162
244
|
}
|
|
163
245
|
|
|
@@ -165,6 +247,7 @@ export class Static {
|
|
|
165
247
|
close(): void {
|
|
166
248
|
this.fetched = [];
|
|
167
249
|
this.streamed = [];
|
|
250
|
+
this.covered = [];
|
|
168
251
|
}
|
|
169
252
|
|
|
170
253
|
private writeOne(series: Series, entries: CacheEntry[]): void {
|
|
@@ -70,6 +70,13 @@ export class Unary {
|
|
|
70
70
|
this.static.write(series);
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
/** Records tr as fetched so reads stop reporting it as a gap even when it holds no
|
|
74
|
+
* samples. Call after a fetch for tr completes. */
|
|
75
|
+
markFetched(tr: TimeRange): void {
|
|
76
|
+
this.checkOpen("markFetched");
|
|
77
|
+
this.static.markFetched(tr);
|
|
78
|
+
}
|
|
79
|
+
|
|
73
80
|
/**
|
|
74
81
|
* Reads cached data overlapping the given time range. The result includes the live
|
|
75
82
|
* leading buffer when the data it actually holds overlaps the range, judged by the
|
package/src/framer/feed.ts
CHANGED
|
@@ -52,6 +52,7 @@ export class Feed {
|
|
|
52
52
|
dynamicBufferSize,
|
|
53
53
|
gcInterval,
|
|
54
54
|
staleEntryThreshold,
|
|
55
|
+
staleCoverageThreshold,
|
|
55
56
|
removalDelay,
|
|
56
57
|
breaker,
|
|
57
58
|
batchDebounce,
|
|
@@ -62,6 +63,7 @@ export class Feed {
|
|
|
62
63
|
dynamicBufferSize,
|
|
63
64
|
gcInterval,
|
|
64
65
|
staleEntryThreshold,
|
|
66
|
+
staleCoverageThreshold,
|
|
65
67
|
instrumentation: instrumentation?.child("cache"),
|
|
66
68
|
});
|
|
67
69
|
this.reader = new Reader({
|
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
|
@@ -29,13 +29,11 @@ export class LookupIndex<
|
|
|
29
29
|
this.extract = extract;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
/** Returns the entries whose extracted index value equals the given value. */
|
|
33
|
-
get(value: IndexValue): Value[];
|
|
34
32
|
/**
|
|
35
|
-
* Returns the entries
|
|
36
|
-
* values
|
|
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.
|
|
37
36
|
*/
|
|
38
|
-
get(values: IndexValue[]): Value[];
|
|
39
37
|
get(value: IndexValue | IndexValue[]): Value[] {
|
|
40
38
|
if (Array.isArray(value))
|
|
41
39
|
return Array.from(new Set(value)).flatMap((v) => this.get(v));
|
package/src/query/table.ts
CHANGED
|
@@ -312,11 +312,10 @@ export class Table<
|
|
|
312
312
|
return this.set(arr);
|
|
313
313
|
}
|
|
314
314
|
|
|
315
|
-
/** Returns every entry in the table. */
|
|
316
|
-
get(): Value[];
|
|
315
|
+
/** Returns every entry in the table, or every entry the filter accepts. */
|
|
316
|
+
get(filter?: (value: Value) => boolean): Value[];
|
|
317
317
|
get(key: Key): Value | undefined;
|
|
318
318
|
get(keys: Key[]): Value[];
|
|
319
|
-
get(filter: (value: Value) => boolean): Value[];
|
|
320
319
|
get(keys?: Key | Key[] | ((value: Value) => boolean)): Value | Value[] | undefined {
|
|
321
320
|
if (keys === undefined) return Array.from(this.entries.values());
|
|
322
321
|
if (typeof keys === "function")
|
package/src/ranger/client.ts
CHANGED
|
@@ -444,7 +444,7 @@ export class Client extends query.Retriever<
|
|
|
444
444
|
table: composed,
|
|
445
445
|
fetch: async (query) => [(await this.fetchSingle(query)).key],
|
|
446
446
|
compose: ([record]) => record,
|
|
447
|
-
keyOf: (query) => (
|
|
447
|
+
keyOf: (query) => (z.validate(keyZ, query) ? query : null),
|
|
448
448
|
matches: (r, query) => r.key === query || r.name === query,
|
|
449
449
|
single: true,
|
|
450
450
|
});
|
|
@@ -576,9 +576,9 @@ export class Client extends query.Retriever<
|
|
|
576
576
|
}
|
|
577
577
|
|
|
578
578
|
async retrieve(params: Key | Name): Promise<Range>;
|
|
579
|
-
async retrieve(
|
|
580
|
-
|
|
581
|
-
|
|
579
|
+
async retrieve(
|
|
580
|
+
params: Key[] | Name[] | CrudeTimeRange | RetrieveRequest,
|
|
581
|
+
): Promise<Range[]>;
|
|
582
582
|
async retrieve(params: RetrieveParams): Promise<Range | Range[]> {
|
|
583
583
|
// The branches narrow params onto different base overloads.
|
|
584
584
|
if (typeof params === "string") return await super.retrieve(params);
|
|
@@ -694,7 +694,7 @@ export class Client extends query.Retriever<
|
|
|
694
694
|
private async fetchSingle(query: Key | Name): Promise<Range> {
|
|
695
695
|
const cached = this.store.get(query);
|
|
696
696
|
if (cached != null) return this.composeOne(cached);
|
|
697
|
-
if (
|
|
697
|
+
if (z.validate(keyZ, query)) {
|
|
698
698
|
const ranges = await this.store.retrieve([query]);
|
|
699
699
|
checkForMultipleOrNoResults("Range", query, ranges, true);
|
|
700
700
|
return this.composeOne(ranges[0]);
|
package/src/schematic/actions.ts
CHANGED
|
@@ -23,6 +23,9 @@ import {
|
|
|
23
23
|
setNodePosition,
|
|
24
24
|
} from "@/schematic/actions.gen";
|
|
25
25
|
|
|
26
|
+
// Must match the pluto groupBox symbol's config variant.
|
|
27
|
+
const GROUP_BOX_VARIANT = "groupBox";
|
|
28
|
+
|
|
26
29
|
const handlers: Handlers = {
|
|
27
30
|
create: (state, payload) => {
|
|
28
31
|
Object.assign(state, payload.schematic);
|
|
@@ -83,16 +86,24 @@ const handlers: Handlers = {
|
|
|
83
86
|
oldConfigRaw != null ? actions.snapshotDraft(oldConfigRaw) : undefined;
|
|
84
87
|
state.nodes.splice(idx, 1);
|
|
85
88
|
delete state.configs[payload.key];
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
oldConfig
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
89
|
+
const inverse: Action[] = [
|
|
90
|
+
setNode(
|
|
91
|
+
oldConfig != null
|
|
92
|
+
? { node: oldNode, config: oldConfig }
|
|
93
|
+
: { node: oldNode, config: undefined },
|
|
94
|
+
),
|
|
95
|
+
];
|
|
96
|
+
const targets = [payload.key];
|
|
97
|
+
for (const [key, config] of Object.entries(state.configs)) {
|
|
98
|
+
if (config?.variant !== GROUP_BOX_VARIANT) continue;
|
|
99
|
+
const members = config.members;
|
|
100
|
+
if (!Array.isArray(members) || !members.includes(payload.key)) continue;
|
|
101
|
+
const oldMembers = actions.snapshotDraft(members);
|
|
102
|
+
config.members = members.filter((m) => m !== payload.key);
|
|
103
|
+
inverse.push(setConfig({ key, config: { members: oldMembers } }));
|
|
104
|
+
targets.push(key);
|
|
105
|
+
}
|
|
106
|
+
return { inverse, targets };
|
|
96
107
|
},
|
|
97
108
|
addEdge: (state, payload) => {
|
|
98
109
|
if (state.edges.some((e) => e.key === payload.edge.key))
|
package/src/status/status.ts
CHANGED
|
@@ -80,6 +80,96 @@ export const exceptionDetailsSchema = z
|
|
|
80
80
|
})
|
|
81
81
|
.and(record.unknownZ());
|
|
82
82
|
|
|
83
|
+
// Bounds the clone check so it stays cheap even when errors are created in a tight
|
|
84
|
+
// loop. An exhausted budget reports un-cloneable, which safely over-rebuilds.
|
|
85
|
+
const CLONE_CHECK_BUDGET = 64;
|
|
86
|
+
|
|
87
|
+
const isCloneableValue = (
|
|
88
|
+
v: unknown,
|
|
89
|
+
seen: Set<object>,
|
|
90
|
+
budget: { left: number },
|
|
91
|
+
): boolean => {
|
|
92
|
+
if (--budget.left < 0) return false;
|
|
93
|
+
if (v == null) return true;
|
|
94
|
+
const t = typeof v;
|
|
95
|
+
if (t === "function" || t === "symbol") return false;
|
|
96
|
+
if (t !== "object") return true;
|
|
97
|
+
const o = v;
|
|
98
|
+
if (seen.has(o)) return true;
|
|
99
|
+
seen.add(o);
|
|
100
|
+
const values = (vals: unknown[]): boolean =>
|
|
101
|
+
vals.every((e) => isCloneableValue(e, seen, budget));
|
|
102
|
+
if (o instanceof Error)
|
|
103
|
+
return isCloneableValue(o.cause, seen, budget) && values(Object.values(o));
|
|
104
|
+
if (Array.isArray(o)) return values(o);
|
|
105
|
+
const proto = Object.getPrototypeOf(o);
|
|
106
|
+
if (proto === Object.prototype || proto === null) return values(Object.values(o));
|
|
107
|
+
if (o instanceof Date || o instanceof ArrayBuffer || ArrayBuffer.isView(o))
|
|
108
|
+
return true;
|
|
109
|
+
// Host objects (an Event, a DOM node) and exotic containers land here.
|
|
110
|
+
return false;
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
// Object.values can trip an enumerable getter that throws. Treat a throwing value
|
|
114
|
+
// as not cloneable rather than throwing while already handling an error.
|
|
115
|
+
const isCloneable = (v: unknown): boolean => {
|
|
116
|
+
try {
|
|
117
|
+
return isCloneableValue(v, new Set(), { left: CLONE_CHECK_BUDGET });
|
|
118
|
+
} catch {
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
// Returns a cloneable stand-in for an un-cloneable value.
|
|
124
|
+
const cloneSafeValue = (v: unknown, depth: number): unknown => {
|
|
125
|
+
if (isCloneable(v)) return v;
|
|
126
|
+
if (v instanceof Error) return cloneSafeError(v, depth);
|
|
127
|
+
if (depth <= 0) return errors.fromUnknown(v).message;
|
|
128
|
+
try {
|
|
129
|
+
if (Array.isArray(v)) return v.map((e) => cloneSafeValue(e, depth - 1));
|
|
130
|
+
if (typeof v === "object" && v != null) {
|
|
131
|
+
const out: Record<string, unknown> = {};
|
|
132
|
+
for (const [key, value] of Object.entries(v))
|
|
133
|
+
if (typeof value !== "function" && typeof value !== "symbol")
|
|
134
|
+
out[key] = cloneSafeValue(value, depth - 1);
|
|
135
|
+
return out;
|
|
136
|
+
}
|
|
137
|
+
} catch {
|
|
138
|
+
// Tolerate throwing enumerable getters.
|
|
139
|
+
}
|
|
140
|
+
return errors.fromUnknown(v).message;
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Returns err unchanged when it survives structured cloning. Otherwise rebuilds it
|
|
145
|
+
* with the same name, message, stack, and `type`, sanitizing un-cloneable fields and
|
|
146
|
+
* causes. Statuses cross the worker boundary, where an un-cloneable error kills the
|
|
147
|
+
* worker.
|
|
148
|
+
*/
|
|
149
|
+
const cloneSafeError = (err: Error, depth: number = 8): Error => {
|
|
150
|
+
if (isCloneable(err)) return err;
|
|
151
|
+
const safe = new Error(err.message);
|
|
152
|
+
safe.name = err.name;
|
|
153
|
+
safe.stack = err.stack;
|
|
154
|
+
const { type } = err as { type?: unknown };
|
|
155
|
+
if (typeof type === "string") (safe as { type?: string }).type = type;
|
|
156
|
+
try {
|
|
157
|
+
for (const [key, value] of Object.entries(err))
|
|
158
|
+
if (key !== "cause" && typeof value !== "function" && typeof value !== "symbol")
|
|
159
|
+
(safe as unknown as Record<string, unknown>)[key] = cloneSafeValue(
|
|
160
|
+
value,
|
|
161
|
+
depth - 1,
|
|
162
|
+
);
|
|
163
|
+
} catch {
|
|
164
|
+
// Tolerate throwing enumerable getters.
|
|
165
|
+
}
|
|
166
|
+
const { cause } = err;
|
|
167
|
+
if (cause !== undefined && depth > 0)
|
|
168
|
+
if (cause instanceof Error) safe.cause = cloneSafeError(cause, depth - 1);
|
|
169
|
+
else safe.cause = cloneSafeValue(cause, depth - 1);
|
|
170
|
+
return safe;
|
|
171
|
+
};
|
|
172
|
+
|
|
83
173
|
/** Flattens an error's cause chain into one readable line. */
|
|
84
174
|
const causeChain = (err: Error): string | undefined => {
|
|
85
175
|
const parts: string[] = [];
|
|
@@ -109,7 +199,7 @@ export const fromException = (
|
|
|
109
199
|
message != null
|
|
110
200
|
? [err.message, detail].filter((part) => part != null).join(": ")
|
|
111
201
|
: detail,
|
|
112
|
-
details: { stack: err.stack ?? "", error: err },
|
|
202
|
+
details: { stack: err.stack ?? "", error: cloneSafeError(err) },
|
|
113
203
|
};
|
|
114
204
|
// Probe the original (pre-coercion) value so a non-Error throwable with a custom
|
|
115
205
|
// `toStatus()` method still contributes its status fields.
|