@devicechain/dashboards 0.22.0 → 0.23.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/channel-snapshots.d.ts +1 -0
- package/dist/chunks/recorded-extra-C2BHFOCB.js +104 -0
- package/dist/chunks/recorded-extra-C2BHFOCB.js.map +7 -0
- package/dist/history.d.ts +3 -1
- package/dist/hub.d.ts +3 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +229 -64
- package/dist/index.js.map +3 -3
- package/dist/members.d.ts +1 -0
- package/dist/queries.d.ts +20 -0
- package/dist/recorded-extra.d.ts +17 -0
- package/dist/recorded.d.ts +11 -1
- package/dist/recording.d.ts +15 -4
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -1615,6 +1615,19 @@ var DEVICES_FOR_ANCHOR = `
|
|
|
1615
1615
|
}
|
|
1616
1616
|
}
|
|
1617
1617
|
`;
|
|
1618
|
+
var CUSTOMER_DEVICES = `
|
|
1619
|
+
query DashboardCustomerDevices($criteria: CustomerDeviceSearchCriteria!) {
|
|
1620
|
+
customerDevices(criteria: $criteria) {
|
|
1621
|
+
results {
|
|
1622
|
+
token
|
|
1623
|
+
name
|
|
1624
|
+
}
|
|
1625
|
+
pagination {
|
|
1626
|
+
totalRecords
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1629
|
+
}
|
|
1630
|
+
`;
|
|
1618
1631
|
var DEVICES_BY_TOKEN = `
|
|
1619
1632
|
query DashboardDevicesByToken($tokens: [String!]!) {
|
|
1620
1633
|
devicesByToken(tokens: $tokens) {
|
|
@@ -1815,7 +1828,7 @@ var ConnectionTracker = class {
|
|
|
1815
1828
|
};
|
|
1816
1829
|
|
|
1817
1830
|
// src/hub.ts
|
|
1818
|
-
import { gql as gql2, isForbiddenError, subscribe } from "@devicechain/client";
|
|
1831
|
+
import { gql as gql2, isForbiddenError, isScopeUnsupportedError, subscribe } from "@devicechain/client";
|
|
1819
1832
|
|
|
1820
1833
|
// src/internal/alarm-doc.ts
|
|
1821
1834
|
var ALARMS_QUERY = `
|
|
@@ -2069,7 +2082,11 @@ var DashboardHub = class {
|
|
|
2069
2082
|
sink.roster(roster);
|
|
2070
2083
|
for (const member of roster.members) {
|
|
2071
2084
|
detachers.push(
|
|
2072
|
-
this.attach(member.token, names, {
|
|
2085
|
+
this.attach(member.token, names, {
|
|
2086
|
+
next: (sample) => sink.next(sample),
|
|
2087
|
+
error: (err) => sink.error?.(err),
|
|
2088
|
+
reconnected: () => sink.reconnected?.()
|
|
2089
|
+
})
|
|
2073
2090
|
);
|
|
2074
2091
|
}
|
|
2075
2092
|
}).catch((err) => {
|
|
@@ -2483,6 +2500,19 @@ var DashboardHub = class {
|
|
|
2483
2500
|
return true;
|
|
2484
2501
|
}
|
|
2485
2502
|
}
|
|
2503
|
+
// isDatasourceSupported reports whether the viewer is offered the binding at all: false only
|
|
2504
|
+
// when resolving an anchor is refused as SCOPE_UNSUPPORTED (an area or asset anchor, for a
|
|
2505
|
+
// customer user). The resolution is the same memoized one the widget's channel makes, so a
|
|
2506
|
+
// supported anchor costs no second read. Every other answer, including a failure, is true.
|
|
2507
|
+
async isDatasourceSupported(datasource) {
|
|
2508
|
+
if (datasource?.kind !== "anchor") return true;
|
|
2509
|
+
try {
|
|
2510
|
+
await this.resolver.devicesForAnchor(datasource.anchor);
|
|
2511
|
+
return true;
|
|
2512
|
+
} catch (err) {
|
|
2513
|
+
return !isScopeUnsupportedError(err);
|
|
2514
|
+
}
|
|
2515
|
+
}
|
|
2486
2516
|
// availabilityToken returns the single device token whose existence gates a widget's
|
|
2487
2517
|
// availability, or undefined when there is nothing device-specific to validate (an
|
|
2488
2518
|
// anchor's membership is self-validating; an unbound slot is a placeholder; a reserved
|
|
@@ -2568,14 +2598,18 @@ var DashboardHub = class {
|
|
|
2568
2598
|
const open = () => {
|
|
2569
2599
|
closeSocket = subscribe(EVENT_AREA, MEASUREMENT_STREAM, variables, adapter);
|
|
2570
2600
|
};
|
|
2601
|
+
const resumed = () => {
|
|
2602
|
+
stream.subscribers.forEach((s) => s.sink.reconnected?.());
|
|
2603
|
+
};
|
|
2571
2604
|
const adapter = {
|
|
2572
2605
|
next: (data) => {
|
|
2573
2606
|
ended = 0;
|
|
2574
2607
|
this.fanout(deviceToken, data.measurementStream);
|
|
2575
2608
|
},
|
|
2576
|
-
connected: () => {
|
|
2609
|
+
connected: (wasRetry) => {
|
|
2577
2610
|
up = true;
|
|
2578
2611
|
handle.connected();
|
|
2612
|
+
if (wasRetry) resumed();
|
|
2579
2613
|
},
|
|
2580
2614
|
closed: () => {
|
|
2581
2615
|
up = false;
|
|
@@ -2593,7 +2627,10 @@ var DashboardHub = class {
|
|
|
2593
2627
|
complete: () => {
|
|
2594
2628
|
if (!operationOpen || !up) return;
|
|
2595
2629
|
closeSocket?.();
|
|
2596
|
-
reopen = setTimeout(
|
|
2630
|
+
reopen = setTimeout(() => {
|
|
2631
|
+
open();
|
|
2632
|
+
resumed();
|
|
2633
|
+
}, Math.min(1e3 * 2 ** ended++, 3e4));
|
|
2597
2634
|
},
|
|
2598
2635
|
error: (err) => {
|
|
2599
2636
|
if (this.streams.get(deviceToken) === stream) this.streams.delete(deviceToken);
|
|
@@ -2940,7 +2977,8 @@ var SyntheticDataSource = class {
|
|
|
2940
2977
|
};
|
|
2941
2978
|
|
|
2942
2979
|
// src/recording.ts
|
|
2943
|
-
var RECORDING_FORMAT_VERSION =
|
|
2980
|
+
var RECORDING_FORMAT_VERSION = 2;
|
|
2981
|
+
var RECORDING_FORMAT_VERSIONS = [1, 2];
|
|
2944
2982
|
var RECORDING_KIND = "sitepulse-board-recording";
|
|
2945
2983
|
var ALARM_STATES = ["ACTIVE", "CLEARED"];
|
|
2946
2984
|
var ALARM_SEVERITIES = ["CRITICAL", "MAJOR", "MINOR", "WARNING"];
|
|
@@ -3076,10 +3114,11 @@ function parseBoardRecording(input) {
|
|
|
3076
3114
|
if (root.kind !== RECORDING_KIND) {
|
|
3077
3115
|
throw new RecordingFormatError("$.kind", `expected ${JSON.stringify(RECORDING_KIND)}`);
|
|
3078
3116
|
}
|
|
3079
|
-
|
|
3117
|
+
const version = root.formatVersion;
|
|
3118
|
+
if (version !== 1 && version !== 2) {
|
|
3080
3119
|
throw new RecordingFormatError(
|
|
3081
3120
|
"$.formatVersion",
|
|
3082
|
-
`unsupported version ${JSON.stringify(root.formatVersion)}; this reader understands ${
|
|
3121
|
+
`unsupported version ${JSON.stringify(root.formatVersion)}; this reader understands ${RECORDING_FORMAT_VERSIONS.join(", ")}`
|
|
3083
3122
|
);
|
|
3084
3123
|
}
|
|
3085
3124
|
const durationMs = num2(root.durationMs, "$.durationMs");
|
|
@@ -3107,8 +3146,8 @@ function parseBoardRecording(input) {
|
|
|
3107
3146
|
throw new RecordingFormatError("$.devices", "duplicate device id or token");
|
|
3108
3147
|
}
|
|
3109
3148
|
const ch = strict(root.channels, "$.channels", ["measurements", "alarms", "locations", "commands"]);
|
|
3110
|
-
if (ch.locations !== false) {
|
|
3111
|
-
throw new RecordingFormatError("$.channels.locations", "
|
|
3149
|
+
if (version === 1 && ch.locations !== false) {
|
|
3150
|
+
throw new RecordingFormatError("$.channels.locations", "version 1 records no positions; must be false");
|
|
3112
3151
|
}
|
|
3113
3152
|
if (ch.commands !== false) {
|
|
3114
3153
|
throw new RecordingFormatError("$.channels.commands", "this version records no command data; must be false");
|
|
@@ -3116,7 +3155,7 @@ function parseBoardRecording(input) {
|
|
|
3116
3155
|
const channels = {
|
|
3117
3156
|
measurements: column(ch.measurements, "$.channels.measurements", str),
|
|
3118
3157
|
alarms: bool(ch.alarms, "$.channels.alarms"),
|
|
3119
|
-
locations:
|
|
3158
|
+
locations: bool(ch.locations, "$.channels.locations"),
|
|
3120
3159
|
commands: false
|
|
3121
3160
|
};
|
|
3122
3161
|
if (new Set(channels.measurements).size !== channels.measurements.length) {
|
|
@@ -3135,8 +3174,9 @@ function parseBoardRecording(input) {
|
|
|
3135
3174
|
if (key === "__proto__") throw new RecordingFormatError(`$.sourceHashes.${key}`, "reserved key");
|
|
3136
3175
|
sourceHashes[key] = matching(sh[key], `$.sourceHashes.${key}`, HEX64, "a lowercase hex SHA-256");
|
|
3137
3176
|
}
|
|
3138
|
-
const cv = strict(root.converter, "$.converter", ["version"]);
|
|
3177
|
+
const cv = strict(root.converter, "$.converter", ["version"], version >= 2 ? ["name"] : []);
|
|
3139
3178
|
const converter = { version: num2(cv.version, "$.converter.version") };
|
|
3179
|
+
if (cv.name !== void 0) converter.name = str(cv.name, "$.converter.name");
|
|
3140
3180
|
let excerpt;
|
|
3141
3181
|
if (root.excerpt !== void 0) {
|
|
3142
3182
|
const e = strict(root.excerpt, "$.excerpt", ["fromMs", "toMs"]);
|
|
@@ -3158,13 +3198,17 @@ function parseBoardRecording(input) {
|
|
|
3158
3198
|
if (i > 0 && c.tMs < chapters[i - 1].tMs) throw new RecordingFormatError(`$.chapters[${i}].tMs`, "chapters must not decrease in time");
|
|
3159
3199
|
});
|
|
3160
3200
|
const m = strict(root.measurements, "$.measurements", ["d", "n", "t", "v", "s"]);
|
|
3161
|
-
const
|
|
3162
|
-
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
|
|
3166
|
-
|
|
3167
|
-
|
|
3201
|
+
const times = (v, path) => {
|
|
3202
|
+
const col = column(v, path, num2);
|
|
3203
|
+
let prev = lo;
|
|
3204
|
+
col.forEach((x, i) => {
|
|
3205
|
+
if (x < lo || x > hi) throw new RecordingFormatError(`${path}[${i}]`, `${x} is outside [${lo}, ${hi}]`);
|
|
3206
|
+
if (x < prev) throw new RecordingFormatError(`${path}[${i}]`, "time column must not decrease");
|
|
3207
|
+
prev = x;
|
|
3208
|
+
});
|
|
3209
|
+
return col;
|
|
3210
|
+
};
|
|
3211
|
+
const t = times(m.t, "$.measurements.t");
|
|
3168
3212
|
const index = (name, limit) => (x, p) => {
|
|
3169
3213
|
const n = num2(x, p);
|
|
3170
3214
|
if (!Number.isInteger(n) || n < 0 || n >= limit) throw new RecordingFormatError(p, `${name} index out of range`);
|
|
@@ -3191,6 +3235,9 @@ function parseBoardRecording(input) {
|
|
|
3191
3235
|
const dev = str(o.dev, `${p}.dev`);
|
|
3192
3236
|
if (!tokens.has(dev)) throw new RecordingFormatError(`${p}.dev`, `device '${dev}' is not in $.devices`);
|
|
3193
3237
|
if (o.ack !== void 0 && o.ack !== true) throw new RecordingFormatError(`${p}.ack`, "present only when true");
|
|
3238
|
+
if (o.val !== void 0 && (typeof o.val !== "number" || !Number.isFinite(o.val))) {
|
|
3239
|
+
throw new RecordingFormatError(`${p}.val`, "expected a finite number");
|
|
3240
|
+
}
|
|
3194
3241
|
const out = {
|
|
3195
3242
|
id,
|
|
3196
3243
|
dev,
|
|
@@ -3201,16 +3248,18 @@ function parseBoardRecording(input) {
|
|
|
3201
3248
|
occ: instant(o.occ, `${p}.occ`)
|
|
3202
3249
|
};
|
|
3203
3250
|
if (o.ack === true) out.ack = true;
|
|
3251
|
+
if (typeof o.val === "number") out.val = o.val;
|
|
3204
3252
|
return out;
|
|
3205
3253
|
};
|
|
3206
3254
|
const ALARM_KEYS = ["id", "dev", "key", "metric", "state", "sev", "occ"];
|
|
3255
|
+
const ALARM_OPTIONAL = version >= 2 ? ["ack", "val"] : ["ack"];
|
|
3207
3256
|
const checkTime = (tMs, p) => {
|
|
3208
3257
|
if (tMs < 0 || tMs > hi) throw new RecordingFormatError(p, `${tMs} is outside [0, ${hi}]`);
|
|
3209
3258
|
return tMs;
|
|
3210
3259
|
};
|
|
3211
3260
|
const snapshots = column(al.snapshots, "$.alarms.snapshots", (x, p) => {
|
|
3212
3261
|
const o = strict(x, p, ["tMs", "requestedAtMs", "total", "alarms"]);
|
|
3213
|
-
const alarms = column(o.alarms, `${p}.alarms`, (a, ap) => alarm(strict(a, ap, ALARM_KEYS,
|
|
3262
|
+
const alarms = column(o.alarms, `${p}.alarms`, (a, ap) => alarm(strict(a, ap, ALARM_KEYS, ALARM_OPTIONAL), ap));
|
|
3214
3263
|
const ids = /* @__PURE__ */ new Set();
|
|
3215
3264
|
alarms.forEach((a, i) => {
|
|
3216
3265
|
if (ids.has(a.id)) throw new RecordingFormatError(`${p}.alarms[${i}].id`, `duplicate alarm ${a.id} in one snapshot`);
|
|
@@ -3227,7 +3276,7 @@ function parseBoardRecording(input) {
|
|
|
3227
3276
|
return { tMs, requestedAtMs, total, alarms };
|
|
3228
3277
|
});
|
|
3229
3278
|
const events = column(al.events, "$.alarms.events", (x, p) => {
|
|
3230
|
-
const o = strict(x, p, [...ALARM_KEYS, "tMs"],
|
|
3279
|
+
const o = strict(x, p, [...ALARM_KEYS, "tMs"], ALARM_OPTIONAL);
|
|
3231
3280
|
return { ...alarm(o, p), tMs: checkTime(num2(o.tMs, `${p}.tMs`), `${p}.tMs`) };
|
|
3232
3281
|
});
|
|
3233
3282
|
for (const [name, list] of [["snapshots", snapshots], ["events", events]]) {
|
|
@@ -3250,12 +3299,36 @@ function parseBoardRecording(input) {
|
|
|
3250
3299
|
if (!channels.alarms && (snapshots.length > 0 || events.length > 0)) {
|
|
3251
3300
|
throw new RecordingFormatError("$.alarms", "history present but $.channels.alarms is false");
|
|
3252
3301
|
}
|
|
3253
|
-
|
|
3254
|
-
|
|
3302
|
+
let locations;
|
|
3303
|
+
if (version === 1) {
|
|
3304
|
+
if (!Array.isArray(root.locations) || root.locations.length !== 0) {
|
|
3305
|
+
throw new RecordingFormatError("$.locations", "version 1 carries no positions; must be an empty array");
|
|
3306
|
+
}
|
|
3307
|
+
locations = { d: [], t: [], lat: [], lon: [], hdg: [], spd: [] };
|
|
3308
|
+
} else {
|
|
3309
|
+
const lc = strict(root.locations, "$.locations", ["d", "t", "lat", "lon", "hdg", "spd"]);
|
|
3310
|
+
const lt = times(lc.t, "$.locations.t");
|
|
3311
|
+
const within = (min, max, what) => (x, p) => {
|
|
3312
|
+
const n = num2(x, p);
|
|
3313
|
+
if (n < min || n > max) throw new RecordingFormatError(p, `${what} ${n} is outside [${min}, ${max}]`);
|
|
3314
|
+
return n;
|
|
3315
|
+
};
|
|
3316
|
+
const optional = (min, max, what) => (x, p) => x === null ? null : within(min, max, what)(x, p);
|
|
3317
|
+
locations = {
|
|
3318
|
+
d: column(lc.d, "$.locations.d", index("device", devices.length), lt.length),
|
|
3319
|
+
t: lt,
|
|
3320
|
+
lat: column(lc.lat, "$.locations.lat", within(-90, 90, "latitude"), lt.length),
|
|
3321
|
+
lon: column(lc.lon, "$.locations.lon", within(-180, 180, "longitude"), lt.length),
|
|
3322
|
+
hdg: column(lc.hdg, "$.locations.hdg", optional(0, 360, "heading"), lt.length),
|
|
3323
|
+
spd: column(lc.spd, "$.locations.spd", optional(0, Number.MAX_VALUE, "speed"), lt.length)
|
|
3324
|
+
};
|
|
3325
|
+
if (!channels.locations && lt.length > 0) {
|
|
3326
|
+
throw new RecordingFormatError("$.locations", "positions present but $.channels.locations is false");
|
|
3327
|
+
}
|
|
3255
3328
|
}
|
|
3256
3329
|
const rec = {
|
|
3257
3330
|
kind: RECORDING_KIND,
|
|
3258
|
-
formatVersion:
|
|
3331
|
+
formatVersion: version,
|
|
3259
3332
|
runId: str(root.runId, "$.runId"),
|
|
3260
3333
|
startedAtUtc: instant(root.startedAtUtc, "$.startedAtUtc"),
|
|
3261
3334
|
durationMs,
|
|
@@ -3272,7 +3345,7 @@ function parseBoardRecording(input) {
|
|
|
3272
3345
|
chapters,
|
|
3273
3346
|
measurements,
|
|
3274
3347
|
alarms: { snapshots, events },
|
|
3275
|
-
locations
|
|
3348
|
+
locations
|
|
3276
3349
|
};
|
|
3277
3350
|
if (excerpt) rec.excerpt = excerpt;
|
|
3278
3351
|
return rec;
|
|
@@ -3436,6 +3509,9 @@ function createRecordedClock(rec, options = {}) {
|
|
|
3436
3509
|
const { fromMs, toMs } = recordingBounds(rec);
|
|
3437
3510
|
return new RecordedClock(toMs, { minMs: fromMs, ticker: options.ticker });
|
|
3438
3511
|
}
|
|
3512
|
+
function retimeShiftMs(rec, cursorMs, nowMs = Date.now()) {
|
|
3513
|
+
return Math.round(nowMs - recordedWallTimeMs(rec, cursorMs));
|
|
3514
|
+
}
|
|
3439
3515
|
function sameAnchor(a, b) {
|
|
3440
3516
|
return a.relationship === b.relationship && a.targetType === b.targetType && a.targetToken === b.targetToken;
|
|
3441
3517
|
}
|
|
@@ -3475,6 +3551,7 @@ function createRecordedLister(rec, options = {}) {
|
|
|
3475
3551
|
return site && site.targetType === kind ? [{ token: site.targetToken, name: null }] : [];
|
|
3476
3552
|
};
|
|
3477
3553
|
}
|
|
3554
|
+
var loadExtra = () => import("./chunks/recorded-extra-C2BHFOCB.js");
|
|
3478
3555
|
var indexCache = /* @__PURE__ */ new WeakMap();
|
|
3479
3556
|
function indexOf(rec) {
|
|
3480
3557
|
const cached = indexCache.get(rec);
|
|
@@ -3506,7 +3583,8 @@ function indexOf(rec) {
|
|
|
3506
3583
|
}
|
|
3507
3584
|
var RecordedDataSource = class {
|
|
3508
3585
|
// `clock` is the shared replay clock; the view starts at its current time. `options`
|
|
3509
|
-
// names the site anchors this recording's devices belong to (see RecordedSiteOptions)
|
|
3586
|
+
// names the site anchors this recording's devices belong to (see RecordedSiteOptions)
|
|
3587
|
+
// and the time shift, if any (see "Re-timing" above).
|
|
3510
3588
|
constructor(rec, clock, options = {}) {
|
|
3511
3589
|
this.subs = /* @__PURE__ */ new Set();
|
|
3512
3590
|
this.stale = false;
|
|
@@ -3514,6 +3592,9 @@ var RecordedDataSource = class {
|
|
|
3514
3592
|
this.site = declaredSite(options);
|
|
3515
3593
|
this.index = indexOf(rec);
|
|
3516
3594
|
this.cursor = clock.timeMs;
|
|
3595
|
+
const shift = options.timeShiftMs ?? 0;
|
|
3596
|
+
if (!Number.isFinite(shift)) throw new RangeError(`RecordedDataSource: timeShiftMs must be finite, got ${shift}`);
|
|
3597
|
+
this.shiftMs = shift;
|
|
3517
3598
|
this.startMs = Date.parse(rec.startedAtUtc);
|
|
3518
3599
|
this.devices = new Set(rec.devices.map((d) => d.token));
|
|
3519
3600
|
this.unlisten = clock.subscribe((ev) => this.onClock(ev));
|
|
@@ -3523,10 +3604,14 @@ var RecordedDataSource = class {
|
|
|
3523
3604
|
get isStale() {
|
|
3524
3605
|
return this.stale;
|
|
3525
3606
|
}
|
|
3526
|
-
// The
|
|
3527
|
-
// from here, not from the wall clock.
|
|
3607
|
+
// The instant the view is at: samples are dated by the run (moved by the shift, if any), so a
|
|
3608
|
+
// time span is measured from here, not from the wall clock.
|
|
3528
3609
|
now() {
|
|
3529
|
-
return this.startMs + this.cursor;
|
|
3610
|
+
return this.startMs + this.cursor + this.shiftMs;
|
|
3611
|
+
}
|
|
3612
|
+
// The shift every instant is moved by (0 for the run's own times).
|
|
3613
|
+
get timeShiftMs() {
|
|
3614
|
+
return this.shiftMs;
|
|
3530
3615
|
}
|
|
3531
3616
|
onClock(ev) {
|
|
3532
3617
|
if (this.stale) return;
|
|
@@ -3540,7 +3625,13 @@ var RecordedDataSource = class {
|
|
|
3540
3625
|
for (const sub of [...this.subs]) sub.advance(this.cursor);
|
|
3541
3626
|
}
|
|
3542
3627
|
iso(offsetMs) {
|
|
3543
|
-
return new Date(this.startMs + offsetMs).toISOString();
|
|
3628
|
+
return new Date(this.startMs + offsetMs + this.shiftMs).toISOString();
|
|
3629
|
+
}
|
|
3630
|
+
// A recorded instant (an alarm's raise or clear time), moved by the shift. Unshifted, the
|
|
3631
|
+
// recording's own string is kept, digits and all.
|
|
3632
|
+
shifted(instant2) {
|
|
3633
|
+
if (instant2 === null || this.shiftMs === 0) return instant2;
|
|
3634
|
+
return new Date(Date.parse(instant2) + this.shiftMs).toISOString();
|
|
3544
3635
|
}
|
|
3545
3636
|
// open tracks a subscription and returns its disposer. `start` runs once, in a
|
|
3546
3637
|
// microtask, so the disposer is in the caller's hands before anything is emitted (the
|
|
@@ -3745,6 +3836,9 @@ var RecordedDataSource = class {
|
|
|
3745
3836
|
// Each record's own `ack` decides; acknowledgement is not carried over from an
|
|
3746
3837
|
// earlier record of the same alarm.
|
|
3747
3838
|
ack: a.ack === true,
|
|
3839
|
+
// The reading that raised the alarm. A cleared alarm keeps its raise's reading, as the
|
|
3840
|
+
// platform's row does (a resolve carries no value).
|
|
3841
|
+
val: a.val ?? (a.state === "CLEARED" ? prior?.val ?? null : null),
|
|
3748
3842
|
asOfMs
|
|
3749
3843
|
});
|
|
3750
3844
|
};
|
|
@@ -3777,7 +3871,10 @@ var RecordedDataSource = class {
|
|
|
3777
3871
|
const signature = JSON.stringify([matches.length, page]);
|
|
3778
3872
|
if (signature === lastSignature || isDisposed()) return;
|
|
3779
3873
|
lastSignature = signature;
|
|
3780
|
-
const snapshot = {
|
|
3874
|
+
const snapshot = {
|
|
3875
|
+
alarms: page.map((a) => toAlarmRow({ ...a, raised: this.shifted(a.raised), cleared: this.shifted(a.cleared) })),
|
|
3876
|
+
total: matches.length
|
|
3877
|
+
};
|
|
3781
3878
|
sink.next(snapshot);
|
|
3782
3879
|
};
|
|
3783
3880
|
return this.open({ start: emit, advance: emit });
|
|
@@ -3788,10 +3885,14 @@ var RecordedDataSource = class {
|
|
|
3788
3885
|
return this.failLater(new NotInRecordingError("commands"), sink);
|
|
3789
3886
|
}
|
|
3790
3887
|
// ── locations ─────────────────────────────────────────────────────────────
|
|
3791
|
-
//
|
|
3792
|
-
//
|
|
3793
|
-
//
|
|
3794
|
-
//
|
|
3888
|
+
// A selector that names no location series asks for nothing, and one that resolves to no
|
|
3889
|
+
// device has nothing to place: both get the empty snapshot, as from the hub. Naming a
|
|
3890
|
+
// location series for devices that exist, on a recording with no positions, is a request
|
|
3891
|
+
// for data the recording does not have, which is an error, never an empty map.
|
|
3892
|
+
//
|
|
3893
|
+
// With positions recorded, each snapshot is every bound device's LAST position at the
|
|
3894
|
+
// cursor, as device-state answers the live map: a device not yet located is absent (the
|
|
3895
|
+
// service's contract), and the snapshot is sent again whenever one of them moves on.
|
|
3795
3896
|
subscribeLocations(subscription, sink) {
|
|
3796
3897
|
const ds = subscription.datasource;
|
|
3797
3898
|
let tokens = [];
|
|
@@ -3801,18 +3902,58 @@ var RecordedDataSource = class {
|
|
|
3801
3902
|
} catch (err) {
|
|
3802
3903
|
return this.failLater(err, sink);
|
|
3803
3904
|
}
|
|
3804
|
-
if (tokens.length > 0) {
|
|
3905
|
+
if (tokens.length > 0 && !this.rec.channels.locations) {
|
|
3805
3906
|
return this.failLater(new NotInRecordingError("locations", "no positions were recorded"), sink);
|
|
3806
3907
|
}
|
|
3807
3908
|
}
|
|
3909
|
+
let extra = null;
|
|
3910
|
+
let lastSignature = null;
|
|
3911
|
+
const emit = (cursor, isDisposed) => {
|
|
3912
|
+
if (!extra) return;
|
|
3913
|
+
const { signature, locations } = extra.lastPositions(this.rec, tokens, cursor, (t) => this.iso(t));
|
|
3914
|
+
if (signature === lastSignature || isDisposed()) return;
|
|
3915
|
+
lastSignature = signature;
|
|
3916
|
+
sink.next({ kind: "positions", deviceTokens: tokens, locations });
|
|
3917
|
+
};
|
|
3808
3918
|
return this.open({
|
|
3809
3919
|
start: (_cursor, isDisposed) => {
|
|
3810
|
-
if (
|
|
3920
|
+
if (tokens.length === 0) {
|
|
3921
|
+
if (!isDisposed()) sink.next({ kind: "positions", deviceTokens: [], locations: [] });
|
|
3922
|
+
return;
|
|
3923
|
+
}
|
|
3924
|
+
loadExtra().then(
|
|
3925
|
+
(m) => {
|
|
3926
|
+
extra = m;
|
|
3927
|
+
if (!this.stale) emit(this.cursor, isDisposed);
|
|
3928
|
+
},
|
|
3929
|
+
(err) => {
|
|
3930
|
+
if (!isDisposed()) sink.error?.(err);
|
|
3931
|
+
}
|
|
3932
|
+
);
|
|
3811
3933
|
},
|
|
3812
|
-
advance:
|
|
3813
|
-
}
|
|
3934
|
+
advance: emit
|
|
3814
3935
|
});
|
|
3815
3936
|
}
|
|
3937
|
+
// ── aggregates ────────────────────────────────────────────────────────────
|
|
3938
|
+
// The buckets a stat widget folds into a span's aggregates, computed from the recording's
|
|
3939
|
+
// own rows: every (device, measurement) row whose time falls in the window AND at or
|
|
3940
|
+
// before the cursor (a replay must not know what happens next), grouped into buckets
|
|
3941
|
+
// `intervalSeconds` wide aligned on the epoch like the event store's, each with its exact
|
|
3942
|
+
// count, minimum, maximum and sum. Like the store, a bucket that holds no reading is not
|
|
3943
|
+
// returned. The window is in the source's own (shifted) time and is moved back by the
|
|
3944
|
+
// shift before the rows are read, so a re-timed board aggregates the same rows.
|
|
3945
|
+
async bucketMeasurements(datasource, window2) {
|
|
3946
|
+
const groups = this.resolve(datasource);
|
|
3947
|
+
for (const g of groups) {
|
|
3948
|
+
for (const name of g.names) {
|
|
3949
|
+
if (!this.rec.channels.measurements.includes(name)) {
|
|
3950
|
+
throw new NotInRecordingError("measurements", `no '${name}' series was recorded`);
|
|
3951
|
+
}
|
|
3952
|
+
}
|
|
3953
|
+
}
|
|
3954
|
+
const { foldBuckets } = await loadExtra();
|
|
3955
|
+
return foldBuckets(this.rec, groups, this.index.byDevice, window2, this.startMs + this.shiftMs, this.cursor);
|
|
3956
|
+
}
|
|
3816
3957
|
// ── availability ──────────────────────────────────────────────────────────
|
|
3817
3958
|
// A device selector is available iff the run has that device. Anchors, unbound slots and
|
|
3818
3959
|
// no datasource have a legitimate empty state and are available, as on the hub.
|
|
@@ -3841,9 +3982,10 @@ function toAlarmRow(a) {
|
|
|
3841
3982
|
// The recording notes THAT an alarm was acknowledged, not when or by whom.
|
|
3842
3983
|
acknowledgedTime: null,
|
|
3843
3984
|
acknowledgedBy: null,
|
|
3844
|
-
// The
|
|
3845
|
-
// invent the integrator's state.
|
|
3846
|
-
|
|
3985
|
+
// The reading that raised it, as the recording holds it (recording.ts: `val`). A recording
|
|
3986
|
+
// without one plays with none; a nearby measurement would invent the integrator's state.
|
|
3987
|
+
// Null renders as an em dash.
|
|
3988
|
+
lastValue: a.val
|
|
3847
3989
|
};
|
|
3848
3990
|
}
|
|
3849
3991
|
|
|
@@ -4575,6 +4717,39 @@ function duplicateWidget(def, id) {
|
|
|
4575
4717
|
// src/resolver.ts
|
|
4576
4718
|
import { gql as gql3 } from "@devicechain/client";
|
|
4577
4719
|
var ANCHOR_PAGE_SIZE = 500;
|
|
4720
|
+
var trueSize = (reported, page) => Math.max(reported ?? page, page);
|
|
4721
|
+
function readAnchorPage(anchor) {
|
|
4722
|
+
if (anchor.targetType === "customer") {
|
|
4723
|
+
return gql3("device-management", CUSTOMER_DEVICES, {
|
|
4724
|
+
criteria: {
|
|
4725
|
+
pageNumber: 1,
|
|
4726
|
+
pageSize: ANCHOR_PAGE_SIZE,
|
|
4727
|
+
customer: anchor.targetToken,
|
|
4728
|
+
relationshipType: anchor.relationship
|
|
4729
|
+
}
|
|
4730
|
+
}).then((r) => {
|
|
4731
|
+
const rows = r.customerDevices.results;
|
|
4732
|
+
return {
|
|
4733
|
+
tokens: rows.map((d) => d.token),
|
|
4734
|
+
total: trueSize(r.customerDevices.pagination?.totalRecords, rows.length),
|
|
4735
|
+
names: new Map(rows.map((d) => [d.token, d.name]))
|
|
4736
|
+
};
|
|
4737
|
+
});
|
|
4738
|
+
}
|
|
4739
|
+
return gql3("device-management", DEVICES_FOR_ANCHOR, {
|
|
4740
|
+
criteria: {
|
|
4741
|
+
pageNumber: 1,
|
|
4742
|
+
pageSize: ANCHOR_PAGE_SIZE,
|
|
4743
|
+
sourceType: "device",
|
|
4744
|
+
targetType: anchor.targetType,
|
|
4745
|
+
target: anchor.targetToken,
|
|
4746
|
+
relationshipType: anchor.relationship
|
|
4747
|
+
}
|
|
4748
|
+
}).then((r) => {
|
|
4749
|
+
const tokens = r.entityRelationships.results.map((rel) => rel.source.token);
|
|
4750
|
+
return { tokens, total: trueSize(r.entityRelationships.pagination?.totalRecords, tokens.length) };
|
|
4751
|
+
});
|
|
4752
|
+
}
|
|
4578
4753
|
function createDeviceResolver() {
|
|
4579
4754
|
const anchorCache = /* @__PURE__ */ new Map();
|
|
4580
4755
|
const memberCache = /* @__PURE__ */ new Map();
|
|
@@ -4586,20 +4761,7 @@ function createDeviceResolver() {
|
|
|
4586
4761
|
const key = anchorKey(anchor);
|
|
4587
4762
|
let pending = anchorCache.get(key);
|
|
4588
4763
|
if (!pending) {
|
|
4589
|
-
pending =
|
|
4590
|
-
criteria: {
|
|
4591
|
-
pageNumber: 1,
|
|
4592
|
-
pageSize: ANCHOR_PAGE_SIZE,
|
|
4593
|
-
sourceType: "device",
|
|
4594
|
-
targetType: anchor.targetType,
|
|
4595
|
-
target: anchor.targetToken,
|
|
4596
|
-
relationshipType: anchor.relationship
|
|
4597
|
-
}
|
|
4598
|
-
}).then((r) => {
|
|
4599
|
-
const tokens = r.entityRelationships.results.map((rel) => rel.source.token);
|
|
4600
|
-
const total = r.entityRelationships.pagination?.totalRecords ?? tokens.length;
|
|
4601
|
-
return { tokens, total: Math.max(total, tokens.length) };
|
|
4602
|
-
}).catch((err) => {
|
|
4764
|
+
pending = readAnchorPage(anchor).catch((err) => {
|
|
4603
4765
|
anchorCache.delete(key);
|
|
4604
4766
|
throw err;
|
|
4605
4767
|
});
|
|
@@ -4611,17 +4773,17 @@ function createDeviceResolver() {
|
|
|
4611
4773
|
devicesForAnchor(anchor) {
|
|
4612
4774
|
return anchorPage(anchor).then((page) => page.tokens);
|
|
4613
4775
|
},
|
|
4614
|
-
// The members of an anchor with their names and the anchor's true size. One
|
|
4615
|
-
//
|
|
4616
|
-
// failed name read is not a failed roster: the tokens still stream, and
|
|
4617
|
-
// them by token.
|
|
4776
|
+
// The members of an anchor with their names and the anchor's true size. One page (shared with
|
|
4777
|
+
// devicesForAnchor) plus, when that page carried no names, ONE batched name read, whatever the
|
|
4778
|
+
// size of the fleet. A failed name read is not a failed roster: the tokens still stream, and
|
|
4779
|
+
// the widget labels them by token.
|
|
4618
4780
|
anchorMembers(anchor) {
|
|
4619
4781
|
const key = anchorKey(anchor);
|
|
4620
4782
|
let pending = memberCache.get(key);
|
|
4621
4783
|
if (!pending) {
|
|
4622
|
-
pending = anchorPage(anchor).then(async ({ tokens, total }) => {
|
|
4623
|
-
const names = /* @__PURE__ */ new Map();
|
|
4624
|
-
if (tokens.length > 0) {
|
|
4784
|
+
pending = anchorPage(anchor).then(async ({ tokens, total, names: listed }) => {
|
|
4785
|
+
const names = listed ?? /* @__PURE__ */ new Map();
|
|
4786
|
+
if (!listed && tokens.length > 0) {
|
|
4625
4787
|
try {
|
|
4626
4788
|
const r = await gql3("device-management", DEVICE_NAMES, { tokens });
|
|
4627
4789
|
for (const d of r.devicesByToken) names.set(d.token, d.name);
|
|
@@ -4672,7 +4834,7 @@ function historyWindowFor(spanSeconds, end) {
|
|
|
4672
4834
|
function defaultHistoryWindow() {
|
|
4673
4835
|
return historyWindowFor(DEFAULT_TIME_RANGE_SECONDS, Date.now());
|
|
4674
4836
|
}
|
|
4675
|
-
async function fetchWidgetHistory(widget, window2) {
|
|
4837
|
+
async function fetchWidgetHistory(widget, window2, options = {}) {
|
|
4676
4838
|
const ds = widget.datasource;
|
|
4677
4839
|
if (!ds || ds.kind !== "device") return [];
|
|
4678
4840
|
try {
|
|
@@ -4700,7 +4862,8 @@ async function fetchWidgetHistory(widget, window2) {
|
|
|
4700
4862
|
value: b.avg,
|
|
4701
4863
|
classifier: null
|
|
4702
4864
|
})).sort((a, b) => a.occurredTime < b.occurredTime ? -1 : 1);
|
|
4703
|
-
} catch {
|
|
4865
|
+
} catch (err) {
|
|
4866
|
+
if (options.rethrow) throw err;
|
|
4704
4867
|
return [];
|
|
4705
4868
|
}
|
|
4706
4869
|
}
|
|
@@ -5457,6 +5620,7 @@ export {
|
|
|
5457
5620
|
PUBLISH_BLOCKING_LAYOUT_CODES,
|
|
5458
5621
|
RECORDED_HISTORY_WINDOW_MS,
|
|
5459
5622
|
RECORDING_FORMAT_VERSION,
|
|
5623
|
+
RECORDING_FORMAT_VERSIONS,
|
|
5460
5624
|
RECORDING_KIND,
|
|
5461
5625
|
RESERVED_PLACEMENT_MODES,
|
|
5462
5626
|
RecordedClock,
|
|
@@ -5581,6 +5745,7 @@ export {
|
|
|
5581
5745
|
resolveSlotCandidates,
|
|
5582
5746
|
resolveWidgetDatasource,
|
|
5583
5747
|
resumeRange,
|
|
5748
|
+
retimeShiftMs,
|
|
5584
5749
|
sameBinding,
|
|
5585
5750
|
sameLabel,
|
|
5586
5751
|
sealHistory,
|