@dronedeploy/rocos-js-sdk 4.4.4 → 4.4.6
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/LICENSE +3 -0
- package/cjs/api/StreamRegister.d.ts +18 -3
- package/cjs/api/StreamRegister.js +71 -38
- package/cjs/api/atoms/StreamHeartbeat.js +2 -2
- package/cjs/api/streams/telemetry/TelemetryStreamAbstract.d.ts +8 -1
- package/cjs/api/streams/telemetry/TelemetryStreamAbstract.js +87 -40
- package/cjs/api/streams/webRTCSignalling/WebRTCSignallingStreamAbstract.js +1 -1
- package/cjs/constants/api.d.ts +3 -2
- package/cjs/constants/api.js +5 -4
- package/cjs/constants/identifier.d.ts +1 -0
- package/cjs/constants/identifier.js +2 -1
- package/cjs/helpers/getUniqueId.js +2 -1
- package/cjs/models/callsigns/CallsignsLookup.d.ts +4 -0
- package/cjs/models/callsigns/CallsignsLookup.js +19 -0
- package/cjs/models/robot-status/RobotStatus.d.ts +23 -0
- package/cjs/models/robot-status/RobotStatusEnums.d.ts +18 -6
- package/cjs/models/robot-status/RobotStatusEnums.js +18 -6
- package/cjs/services/AssetStorageService.js +6 -1
- package/cjs/services/BaseServiceAbstract.js +1 -1
- package/cjs/services/BaseStreamService.d.ts +12 -1
- package/cjs/services/BaseStreamService.js +19 -30
- package/cjs/services/EnvironmentService.d.ts +4 -0
- package/cjs/services/EnvironmentService.js +5 -1
- package/cjs/services/MapService.d.ts +15 -3
- package/cjs/services/MapService.js +25 -13
- package/cjs/services/RobotStatusService.d.ts +20 -1
- package/cjs/services/RobotStatusService.js +5 -0
- package/cjs/services/SearchService.js +1 -1
- package/cjs/services/TargetService.js +2 -2
- package/cjs/services/TelemetryService.d.ts +1 -2
- package/cjs/services/TelemetryService.js +30 -28
- package/cjs/services/WebRTCSignallingService.js +7 -9
- package/cjs/services/robot-status/connectivity.d.ts +16 -12
- package/cjs/services/robot-status/connectivity.js +26 -15
- package/cjs/services/robot-status/reportedStatus.d.ts +14 -1
- package/cjs/services/robot-status/reportedStatus.js +36 -1
- package/esm/api/StreamRegister.d.ts +18 -3
- package/esm/api/StreamRegister.js +71 -38
- package/esm/api/atoms/StreamHeartbeat.js +2 -2
- package/esm/api/streams/telemetry/TelemetryStreamAbstract.d.ts +8 -1
- package/esm/api/streams/telemetry/TelemetryStreamAbstract.js +88 -41
- package/esm/api/streams/webRTCSignalling/WebRTCSignallingStreamAbstract.js +2 -2
- package/esm/constants/api.d.ts +3 -2
- package/esm/constants/api.js +3 -2
- package/esm/constants/identifier.d.ts +1 -0
- package/esm/constants/identifier.js +1 -0
- package/esm/helpers/getUniqueId.js +2 -1
- package/esm/models/callsigns/CallsignsLookup.d.ts +4 -0
- package/esm/models/callsigns/CallsignsLookup.js +19 -0
- package/esm/models/robot-status/RobotStatus.d.ts +23 -0
- package/esm/models/robot-status/RobotStatusEnums.d.ts +18 -6
- package/esm/models/robot-status/RobotStatusEnums.js +18 -6
- package/esm/services/AssetStorageService.js +6 -1
- package/esm/services/BaseServiceAbstract.js +1 -1
- package/esm/services/BaseStreamService.d.ts +12 -1
- package/esm/services/BaseStreamService.js +19 -30
- package/esm/services/EnvironmentService.d.ts +4 -0
- package/esm/services/EnvironmentService.js +5 -1
- package/esm/services/MapService.d.ts +15 -3
- package/esm/services/MapService.js +26 -14
- package/esm/services/RobotStatusService.d.ts +20 -1
- package/esm/services/RobotStatusService.js +6 -1
- package/esm/services/SearchService.js +2 -2
- package/esm/services/TargetService.js +3 -3
- package/esm/services/TelemetryService.d.ts +1 -2
- package/esm/services/TelemetryService.js +31 -29
- package/esm/services/WebRTCSignallingService.js +8 -10
- package/esm/services/robot-status/connectivity.d.ts +16 -12
- package/esm/services/robot-status/connectivity.js +26 -15
- package/esm/services/robot-status/reportedStatus.d.ts +14 -1
- package/esm/services/robot-status/reportedStatus.js +35 -1
- package/package.json +7 -2
|
@@ -58,21 +58,24 @@ const mapConnectionStatus = (connection) => {
|
|
|
58
58
|
exports.mapConnectionStatus = mapConnectionStatus;
|
|
59
59
|
/**
|
|
60
60
|
* Aggregates per-service connection states into an overall verdict:
|
|
61
|
-
* - `
|
|
62
|
-
*
|
|
61
|
+
* - `DEGRADED` — any service reports an explicit `FAILED` fault, or a genuine mix (some connected,
|
|
62
|
+
* some disconnected).
|
|
63
|
+
* - `ONLINE` — at least one service connected and none disconnected or failed.
|
|
63
64
|
* - `OFFLINE` — nothing connected.
|
|
64
65
|
*
|
|
65
|
-
* `
|
|
66
|
-
*
|
|
66
|
+
* A `FAILED` service forces `DEGRADED` even when nothing else is connected, so a fault (e.g. a failed
|
|
67
|
+
* deployment) raises attention rather than reading as a plain, switched-off `OFFLINE`.
|
|
67
68
|
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
* `
|
|
71
|
-
*
|
|
72
|
-
*
|
|
69
|
+
* `UNKNOWN` services (e.g. not yet polled) never force `DEGRADED`, so startup settles on `OFFLINE`
|
|
70
|
+
* (gray) rather than a spurious flashing-red degraded state. This optimism also means a service held
|
|
71
|
+
* `UNKNOWN` indefinitely (e.g. an endpoint whose polls keep failing) does not by itself pull the
|
|
72
|
+
* aggregate below `ONLINE` — a deliberate tradeoff to keep transient errors from flapping the
|
|
73
|
+
* indicator; a service that wants to be surfaced reports `FAILED` explicitly.
|
|
73
74
|
*/
|
|
74
75
|
const aggregateConnectivity = (services) => {
|
|
75
76
|
const values = Object.values(services);
|
|
77
|
+
if (values.includes(models_1.ServiceConnection.FAILED))
|
|
78
|
+
return models_1.RobotConnectivity.DEGRADED;
|
|
76
79
|
const hasConnected = values.includes(models_1.ServiceConnection.CONNECTED);
|
|
77
80
|
const hasDisconnected = values.includes(models_1.ServiceConnection.DISCONNECTED);
|
|
78
81
|
if (hasConnected && hasDisconnected)
|
|
@@ -107,24 +110,32 @@ const connectivityEquals = (a, b) => {
|
|
|
107
110
|
};
|
|
108
111
|
/**
|
|
109
112
|
* Emits the robot's {@link RobotConnectivityStatus} — the per-service breakdown
|
|
110
|
-
* plus the aggregate verdict — for the configured `expectedServices
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
+
* plus the aggregate verdict — for the configured `expectedServices`, plus any
|
|
114
|
+
* caller-injected `extraServices` (folded in on equal footing). HTTP services
|
|
115
|
+
* are polled; `telemetry-liveness` and injected services use their supplied
|
|
116
|
+
* observable; any unrecognised service id resolves to `UNKNOWN`.
|
|
113
117
|
*/
|
|
114
118
|
const getConnectivityChanges = (deps, projectId, callsign, expectedServices = exports.DEFAULT_EXPECTED_SERVICES, pollMs = DEFAULT_CONNECTIVITY_POLL_MS) => {
|
|
119
|
+
const extraServices = deps.extraServices ?? {};
|
|
120
|
+
const serviceIds = [...expectedServices, ...Object.keys(extraServices).filter((id) => !expectedServices.includes(id))];
|
|
115
121
|
// combineLatest([]) completes without emitting; emit an explicit empty/offline status instead.
|
|
116
|
-
if (
|
|
122
|
+
if (serviceIds.length === 0) {
|
|
117
123
|
return (0, rxjs_1.of)({ overall: models_1.RobotConnectivity.OFFLINE, services: {} });
|
|
118
124
|
}
|
|
119
|
-
const perService =
|
|
125
|
+
const perService = serviceIds.map((serviceId) => {
|
|
120
126
|
let source$;
|
|
121
127
|
if (serviceId === exports.TELEMETRY_LIVENESS_SERVICE) {
|
|
122
128
|
source$ = deps.telemetryLiveness$;
|
|
123
129
|
}
|
|
124
|
-
else if (
|
|
130
|
+
else if (Object.prototype.hasOwnProperty.call(exports.CONNECTION_SERVICE_PREFIXES, serviceId)) {
|
|
125
131
|
const url = (0, exports.buildConnectionsUrl)(serviceId, deps.baseUrl, projectId, callsign, deps.insecure);
|
|
126
132
|
source$ = (0, exports.getServiceConnectionChanges)(deps.httpGet, url, pollMs);
|
|
127
133
|
}
|
|
134
|
+
else if (Object.prototype.hasOwnProperty.call(extraServices, serviceId)) {
|
|
135
|
+
// Contain an injected observable's errors as UNKNOWN, mirroring the built-in sources, so a
|
|
136
|
+
// faulty caller signal can't error out combineLatest and tear down the whole aggregate.
|
|
137
|
+
source$ = extraServices[serviceId].pipe((0, rxjs_1.catchError)(() => (0, rxjs_1.of)(models_1.ServiceConnection.UNKNOWN)));
|
|
138
|
+
}
|
|
128
139
|
else {
|
|
129
140
|
source$ = (0, rxjs_1.of)(models_1.ServiceConnection.UNKNOWN);
|
|
130
141
|
}
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { Observable } from 'rxjs';
|
|
2
|
-
import { RobotHealth, RobotReadiness, ServiceConnection } from '../../models';
|
|
2
|
+
import { RobotHealth, RobotReadiness, RobotStatusContributors, ServiceConnection } from '../../models';
|
|
3
3
|
import { TelemetryService } from '../TelemetryService';
|
|
4
4
|
/** The new agent topic carrying health + readiness (replaces the heartbeat). */
|
|
5
5
|
export declare const ROBOT_STATUS_SOURCE = "/diagnostics/robot/status";
|
|
6
6
|
/** The legacy heartbeat topic — liveness only, no status payload. */
|
|
7
7
|
export declare const LEGACY_HEARTBEAT_SOURCE = "/rocos/agent/telemetry/heartbeat";
|
|
8
|
+
/** The per-contributor breakdown behind the reported status (health + readiness). */
|
|
9
|
+
export declare const ROBOT_STATUS_CONTRIBUTORS_SOURCE = "/diagnostics/robot/status/contributors";
|
|
8
10
|
export interface ReportedRobotStatus {
|
|
9
11
|
health: RobotHealth;
|
|
10
12
|
readiness: RobotReadiness;
|
|
@@ -28,6 +30,17 @@ export declare const parseRobotReadiness: (value: unknown) => RobotReadiness;
|
|
|
28
30
|
* new topic.
|
|
29
31
|
*/
|
|
30
32
|
export declare const getReportedStatusChanges: (telemetry: TelemetryService, projectId: string, callsign: string) => Observable<ReportedRobotStatus>;
|
|
33
|
+
/**
|
|
34
|
+
* Emits the per-contributor breakdown behind the robot's reported health and
|
|
35
|
+
* readiness, from the agent's `robot/status/contributors` topic. Each
|
|
36
|
+
* contributor's integer wire value is mapped onto {@link RobotHealth} /
|
|
37
|
+
* {@link RobotReadiness}; a missing `health`/`readiness` array becomes `[]`.
|
|
38
|
+
*
|
|
39
|
+
* Starts with an empty breakdown so consumers combining this with other streams
|
|
40
|
+
* emit immediately, and — like {@link getReportedStatusChanges} — a telemetry
|
|
41
|
+
* error falls back to an empty breakdown rather than terminating the stream.
|
|
42
|
+
*/
|
|
43
|
+
export declare const getStatusContributorsChanges: (telemetry: TelemetryService, projectId: string, callsign: string) => Observable<RobotStatusContributors>;
|
|
31
44
|
/**
|
|
32
45
|
* Emits the `telemetry-liveness` connectivity signal: whether a heartbeat (from
|
|
33
46
|
* either the new `robot/status` topic or the legacy heartbeat) has been received
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.getTelemetryLiveness = exports.getReportedStatusChanges = exports.parseRobotReadiness = exports.parseRobotHealth = exports.LEGACY_HEARTBEAT_SOURCE = exports.ROBOT_STATUS_SOURCE = void 0;
|
|
3
|
+
exports.getTelemetryLiveness = exports.getStatusContributorsChanges = exports.getReportedStatusChanges = exports.parseRobotReadiness = exports.parseRobotHealth = exports.ROBOT_STATUS_CONTRIBUTORS_SOURCE = exports.LEGACY_HEARTBEAT_SOURCE = exports.ROBOT_STATUS_SOURCE = void 0;
|
|
4
4
|
const rxjs_1 = require("rxjs");
|
|
5
5
|
const models_1 = require("../../models");
|
|
6
6
|
/** The new agent topic carrying health + readiness (replaces the heartbeat). */
|
|
7
7
|
exports.ROBOT_STATUS_SOURCE = '/diagnostics/robot/status';
|
|
8
8
|
/** The legacy heartbeat topic — liveness only, no status payload. */
|
|
9
9
|
exports.LEGACY_HEARTBEAT_SOURCE = '/rocos/agent/telemetry/heartbeat';
|
|
10
|
+
/** The per-contributor breakdown behind the reported status (health + readiness). */
|
|
11
|
+
exports.ROBOT_STATUS_CONTRIBUTORS_SOURCE = `${exports.ROBOT_STATUS_SOURCE}/contributors`;
|
|
10
12
|
const DEFAULT_HEARTBEAT_TIMEOUT_MS = 5000;
|
|
11
13
|
const DEFAULT_INTERVAL_MS = 2000;
|
|
12
14
|
/**
|
|
@@ -84,6 +86,39 @@ const getReportedStatusChanges = (telemetry, projectId, callsign) => {
|
|
|
84
86
|
(0, rxjs_1.catchError)(() => (0, rxjs_1.of)({ health: models_1.RobotHealth.UNKNOWN, readiness: models_1.RobotReadiness.UNKNOWN })));
|
|
85
87
|
};
|
|
86
88
|
exports.getReportedStatusChanges = getReportedStatusChanges;
|
|
89
|
+
const toContributor = (raw, parse) => ({
|
|
90
|
+
id: raw.id ?? '',
|
|
91
|
+
component: raw.component ?? '',
|
|
92
|
+
reason: raw.reason ?? '',
|
|
93
|
+
status: parse(raw.value),
|
|
94
|
+
});
|
|
95
|
+
const EMPTY_CONTRIBUTORS = { health: [], readiness: [] };
|
|
96
|
+
/**
|
|
97
|
+
* Emits the per-contributor breakdown behind the robot's reported health and
|
|
98
|
+
* readiness, from the agent's `robot/status/contributors` topic. Each
|
|
99
|
+
* contributor's integer wire value is mapped onto {@link RobotHealth} /
|
|
100
|
+
* {@link RobotReadiness}; a missing `health`/`readiness` array becomes `[]`.
|
|
101
|
+
*
|
|
102
|
+
* Starts with an empty breakdown so consumers combining this with other streams
|
|
103
|
+
* emit immediately, and — like {@link getReportedStatusChanges} — a telemetry
|
|
104
|
+
* error falls back to an empty breakdown rather than terminating the stream.
|
|
105
|
+
*/
|
|
106
|
+
const getStatusContributorsChanges = (telemetry, projectId, callsign) => {
|
|
107
|
+
return telemetry
|
|
108
|
+
.subscribe({
|
|
109
|
+
projectId,
|
|
110
|
+
callsigns: [callsign],
|
|
111
|
+
sources: [exports.ROBOT_STATUS_CONTRIBUTORS_SOURCE],
|
|
112
|
+
})
|
|
113
|
+
.pipe((0, rxjs_1.map)((message) => {
|
|
114
|
+
const payload = message.payload ?? {};
|
|
115
|
+
return {
|
|
116
|
+
health: (payload.health ?? []).map((c) => toContributor(c, exports.parseRobotHealth)),
|
|
117
|
+
readiness: (payload.readiness ?? []).map((c) => toContributor(c, exports.parseRobotReadiness)),
|
|
118
|
+
};
|
|
119
|
+
}), (0, rxjs_1.startWith)(EMPTY_CONTRIBUTORS), (0, rxjs_1.distinctUntilChanged)((a, b) => JSON.stringify(a) === JSON.stringify(b)), (0, rxjs_1.catchError)(() => (0, rxjs_1.of)(EMPTY_CONTRIBUTORS)));
|
|
120
|
+
};
|
|
121
|
+
exports.getStatusContributorsChanges = getStatusContributorsChanges;
|
|
87
122
|
/**
|
|
88
123
|
* Emits the `telemetry-liveness` connectivity signal: whether a heartbeat (from
|
|
89
124
|
* either the new `robot/status` topic or the legacy heartbeat) has been received
|
|
@@ -1,13 +1,28 @@
|
|
|
1
1
|
import { IBaseStream } from '../models';
|
|
2
|
+
export interface IAcquiredStream<T extends IBaseStream> {
|
|
3
|
+
stream: T;
|
|
4
|
+
isNew: boolean;
|
|
5
|
+
/** Settles when the stream's one-time init completes; rejects (and evicts) if init fails. */
|
|
6
|
+
ready: Promise<void>;
|
|
7
|
+
/** Releases this holder's refcount. One-shot: extra calls are ignored. */
|
|
8
|
+
release: () => void;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Sole owner of stream lifecycle. Streams are shared per identifier and refcounted: acquireStream
|
|
12
|
+
* gets-or-creates and increments in one synchronous step, and the returned handle's release()
|
|
13
|
+
* tears the stream down when the last holder leaves. Teardown never happens outside
|
|
14
|
+
* release/removeAllStreams, so a held stream can not be stopped or replaced underneath its holders.
|
|
15
|
+
*/
|
|
2
16
|
export declare class StreamRegister {
|
|
3
|
-
private
|
|
17
|
+
private entries;
|
|
4
18
|
private logger;
|
|
5
19
|
private static instance;
|
|
6
20
|
private constructor();
|
|
7
21
|
static getInstance(): StreamRegister;
|
|
8
22
|
static getIdentifier($identifier: string, scope?: string): string;
|
|
9
|
-
|
|
23
|
+
acquireStream<T extends IBaseStream>(identifier: string, create: () => T, init: (stream: T) => Promise<void>): IAcquiredStream<T>;
|
|
10
24
|
getStream(identifier: string): IBaseStream | undefined;
|
|
11
|
-
removeStream(stream: IBaseStream): void;
|
|
12
25
|
removeAllStreams(): boolean;
|
|
26
|
+
private release;
|
|
27
|
+
private evict;
|
|
13
28
|
}
|
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
import { RocosError, errorCodes } from '../models';
|
|
2
2
|
import { RocosLogger } from '../logger/RocosLogger';
|
|
3
|
+
/**
|
|
4
|
+
* Sole owner of stream lifecycle. Streams are shared per identifier and refcounted: acquireStream
|
|
5
|
+
* gets-or-creates and increments in one synchronous step, and the returned handle's release()
|
|
6
|
+
* tears the stream down when the last holder leaves. Teardown never happens outside
|
|
7
|
+
* release/removeAllStreams, so a held stream can not be stopped or replaced underneath its holders.
|
|
8
|
+
*/
|
|
3
9
|
export class StreamRegister {
|
|
4
10
|
constructor() {
|
|
5
|
-
this.
|
|
11
|
+
this.entries = new Map();
|
|
6
12
|
this.logger = RocosLogger.getInstance('StreamRegister');
|
|
7
13
|
}
|
|
8
14
|
static getInstance() {
|
|
@@ -14,55 +20,83 @@ export class StreamRegister {
|
|
|
14
20
|
static getIdentifier($identifier, scope) {
|
|
15
21
|
return `${$identifier}-${scope ?? ''}`;
|
|
16
22
|
}
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
existing.stopStream();
|
|
23
|
-
}
|
|
24
|
-
this.teleStreams.set(stream.identifier, stream);
|
|
23
|
+
acquireStream(identifier, create, init) {
|
|
24
|
+
let entry = this.entries.get(identifier);
|
|
25
|
+
let isNew = false;
|
|
26
|
+
if (entry) {
|
|
27
|
+
entry.refCount++;
|
|
25
28
|
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
29
|
+
else {
|
|
30
|
+
isNew = true;
|
|
31
|
+
const stream = create();
|
|
32
|
+
const ready = init(stream).catch((error) => {
|
|
33
|
+
this.logger.error(`Stream init failed, evicting. identifier: ${identifier}`, error);
|
|
34
|
+
try {
|
|
35
|
+
this.evict(identifier, stream);
|
|
36
|
+
}
|
|
37
|
+
catch (evictionError) {
|
|
38
|
+
this.logger.error(`Failed to evict after init failure. identifier: ${identifier}`, evictionError);
|
|
39
|
+
}
|
|
40
|
+
throw error;
|
|
41
|
+
});
|
|
42
|
+
// `ready` is stored and shared; this discarded catch pins a rejection observer so an init
|
|
43
|
+
// failure can never surface as an unhandled rejection, while holders still see the rejection.
|
|
44
|
+
void ready.catch(() => undefined);
|
|
45
|
+
entry = { stream, refCount: 1, ready };
|
|
46
|
+
this.entries.set(identifier, entry);
|
|
34
47
|
}
|
|
48
|
+
const held = entry;
|
|
49
|
+
let released = false;
|
|
50
|
+
const release = () => {
|
|
51
|
+
// One-shot so a double release can never consume another holder's refcount.
|
|
52
|
+
if (released)
|
|
53
|
+
return;
|
|
54
|
+
released = true;
|
|
55
|
+
this.release(identifier, held);
|
|
56
|
+
};
|
|
57
|
+
return { stream: entry.stream, isNew, ready: entry.ready, release };
|
|
35
58
|
}
|
|
36
59
|
getStream(identifier) {
|
|
37
|
-
return this.
|
|
60
|
+
return this.entries.get(identifier)?.stream;
|
|
38
61
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
// and evicting that would leave subscribers forking duplicate streams for the scope.
|
|
44
|
-
if (this.teleStreams.get(stream.identifier) === stream) {
|
|
45
|
-
this.teleStreams.delete(stream.identifier);
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
catch (e) {
|
|
49
|
-
this.logger.error(`Failed to remove stream from list. identifier: ${stream.identifier}`, e);
|
|
50
|
-
if (e instanceof Error || typeof e === 'string') {
|
|
51
|
-
throw new RocosError(e, errorCodes.STREAM_LISTENER_ERROR);
|
|
62
|
+
removeAllStreams() {
|
|
63
|
+
for (const [, entry] of this.entries.entries()) {
|
|
64
|
+
try {
|
|
65
|
+
entry.stream.stopStream();
|
|
52
66
|
}
|
|
53
|
-
|
|
54
|
-
|
|
67
|
+
catch (e) {
|
|
68
|
+
this.logger.error(`error removing stream, ${e}`);
|
|
55
69
|
}
|
|
56
70
|
}
|
|
71
|
+
this.entries.clear();
|
|
72
|
+
return true;
|
|
57
73
|
}
|
|
58
|
-
|
|
74
|
+
release(identifier, entry) {
|
|
75
|
+
if (this.entries.get(identifier) !== entry)
|
|
76
|
+
return; // already evicted, or force-removed
|
|
77
|
+
entry.refCount--;
|
|
78
|
+
if (entry.refCount > 0)
|
|
79
|
+
return;
|
|
80
|
+
// Wait for init to settle before tearing down, so init can never open resources on an already
|
|
81
|
+
// stopped stream. A re-acquire in the meantime revives the entry and cancels the teardown.
|
|
82
|
+
void entry.ready
|
|
83
|
+
.catch(() => undefined)
|
|
84
|
+
.then(() => {
|
|
85
|
+
if (this.entries.get(identifier) === entry && entry.refCount <= 0) {
|
|
86
|
+
this.evict(identifier, entry.stream);
|
|
87
|
+
}
|
|
88
|
+
})
|
|
89
|
+
.catch((e) => this.logger.error(`Failed to evict stream. identifier: ${identifier}`, e));
|
|
90
|
+
}
|
|
91
|
+
evict(identifier, stream) {
|
|
59
92
|
try {
|
|
60
|
-
|
|
61
|
-
this.
|
|
93
|
+
if (this.entries.get(identifier)?.stream === stream) {
|
|
94
|
+
this.entries.delete(identifier);
|
|
62
95
|
}
|
|
96
|
+
stream.stopStream();
|
|
63
97
|
}
|
|
64
98
|
catch (e) {
|
|
65
|
-
this.logger.error(`
|
|
99
|
+
this.logger.error(`Failed to remove stream from list. identifier: ${identifier}`, e);
|
|
66
100
|
if (e instanceof Error || typeof e === 'string') {
|
|
67
101
|
throw new RocosError(e, errorCodes.STREAM_LISTENER_ERROR);
|
|
68
102
|
}
|
|
@@ -70,6 +104,5 @@ export class StreamRegister {
|
|
|
70
104
|
throw e;
|
|
71
105
|
}
|
|
72
106
|
}
|
|
73
|
-
return true;
|
|
74
107
|
}
|
|
75
108
|
}
|
|
@@ -35,7 +35,7 @@ export class StreamHeartbeat {
|
|
|
35
35
|
this.callback = callback;
|
|
36
36
|
this.timeout = timeout;
|
|
37
37
|
this.logger.info('Creating stream heartbeat');
|
|
38
|
-
this.interval = setInterval(this.callback, this.timeout);
|
|
38
|
+
this.interval = setInterval(() => this.callback?.(), this.timeout);
|
|
39
39
|
}
|
|
40
40
|
else {
|
|
41
41
|
this.logger.warn('Stream heartbeat already exists');
|
|
@@ -55,7 +55,7 @@ export class StreamHeartbeat {
|
|
|
55
55
|
// if the health check has not started yet, we initialise it
|
|
56
56
|
if (!this.healthInterval) {
|
|
57
57
|
this.logger.debug('Starting health check timer');
|
|
58
|
-
this.healthInterval = setInterval(this.checkHealth, this.healthTimeout);
|
|
58
|
+
this.healthInterval = setInterval(() => void this.checkHealth(), this.healthTimeout);
|
|
59
59
|
this.healthLastSeenDate = new Date();
|
|
60
60
|
this.healthMisses = 0;
|
|
61
61
|
}
|
|
@@ -14,6 +14,7 @@ export declare abstract class TelemetryStreamAbstract implements ITelemetryStrea
|
|
|
14
14
|
protected token?: string;
|
|
15
15
|
private scope;
|
|
16
16
|
protected url: string;
|
|
17
|
+
private telemetryActionQueue;
|
|
17
18
|
private timerIntervalInSec;
|
|
18
19
|
protected subscriberStatus: SubscriberStatusEnum;
|
|
19
20
|
private checkerStartedAt?;
|
|
@@ -40,10 +41,17 @@ export declare abstract class TelemetryStreamAbstract implements ITelemetryStrea
|
|
|
40
41
|
stopStream(): void;
|
|
41
42
|
addSubscription(params: ITelemetryParams): void;
|
|
42
43
|
removeSubscription(params: ITelemetryParams, terminateReceiverGroup?: boolean): Promise<void>;
|
|
44
|
+
private isQueryLookup;
|
|
45
|
+
private getSubscribedSources;
|
|
43
46
|
sendAcknowledgment(uid: string, status: TelemetryAckStatus, noRetry: boolean): boolean;
|
|
44
47
|
private getSubscriptions;
|
|
45
48
|
protected onData(message: TelemetryStreamMessage, isStream: true): void;
|
|
46
49
|
protected onData(message: TelemetryMessage, isStream: false): void;
|
|
50
|
+
/**
|
|
51
|
+
* Gateway subscription state is a set mutated by deltas, so ordering is correctness: a concurrent
|
|
52
|
+
* unsubscribe landing last strands a subscribe and nothing re-adds it.
|
|
53
|
+
*/
|
|
54
|
+
private enqueueTelemetryAction;
|
|
47
55
|
private takeTelemetryAction;
|
|
48
56
|
protected listenMessagesAndRenew(): void;
|
|
49
57
|
private addTerminateToAction;
|
|
@@ -52,7 +60,6 @@ export declare abstract class TelemetryStreamAbstract implements ITelemetryStrea
|
|
|
52
60
|
* Auto resubscribe to reduce CPU usage.
|
|
53
61
|
*/
|
|
54
62
|
private autoResubscribe;
|
|
55
|
-
private isFoundCallsign;
|
|
56
63
|
private isFoundSource;
|
|
57
64
|
private isRegisteredMessage;
|
|
58
65
|
private updateReceivedDataStatsWithMessage;
|
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
import { BehaviorSubject, Subject } from 'rxjs';
|
|
2
|
-
import { CallsignsLookup,
|
|
2
|
+
import { CallsignsLookup, RocosTelemetryMessage, StreamOptions, SubscriberStatusEnum, } from '../../../models';
|
|
3
3
|
import { GRPC_SOURCE_NOOP, GRPC_SOURCE_SUBSCRIBED } from '../../../constants/grpc';
|
|
4
4
|
import { QueryOrPredicate, TelemetryAckStatus, TelemetryQueryRequest, TelemetryRequest, UnsubscribeOperation, } from '../../../grpc/teletubby_pb';
|
|
5
5
|
import { IDENTIFIER_NAME_TELEMETRY } from '../../../constants/identifier';
|
|
6
6
|
import { RocosStore } from '../../../store/RocosStore';
|
|
7
7
|
import { StreamHeartbeat } from '../../atoms/StreamHeartbeat';
|
|
8
8
|
import { StreamRegister } from '../../StreamRegister';
|
|
9
|
+
import { arrayRemove } from '../../../helpers/arrayRemove';
|
|
9
10
|
import { arrayUnique } from '../../../helpers/arrayUnique';
|
|
10
11
|
import { filter } from 'rxjs/operators';
|
|
11
12
|
import { getSubscriptionsDifference } from '../../../helpers/getSubscriptionsDifference';
|
|
12
13
|
export class TelemetryStreamAbstract {
|
|
13
14
|
constructor(config) {
|
|
15
|
+
this.telemetryActionQueue = Promise.resolve();
|
|
14
16
|
// /////////////////
|
|
15
17
|
// Subscriber Check
|
|
16
18
|
this.timerIntervalInSec = 1;
|
|
@@ -57,27 +59,44 @@ export class TelemetryStreamAbstract {
|
|
|
57
59
|
}
|
|
58
60
|
this.subscriberStatus = SubscriberStatusEnum.STOPPED;
|
|
59
61
|
this.statusStream$.next(this.subscriberStatus);
|
|
62
|
+
// The stream is being torn down (the registry only stops a stream when it is evicted), so
|
|
63
|
+
// complete the status stream: this releases every service-level forwarder subscribed to it
|
|
64
|
+
// rather than leaving them attached to a dead instance.
|
|
65
|
+
this.statusStream$.complete();
|
|
60
66
|
}
|
|
61
67
|
addSubscription(params) {
|
|
62
68
|
this.logger.info('Adding subscriptions from stream', params.uniqueId);
|
|
63
69
|
if (!this.subscriptions.has(params.uniqueId)) {
|
|
70
|
+
const isQuery = this.isQueryLookup();
|
|
64
71
|
// get subscriptions before change
|
|
65
|
-
const before = this.getSubscriptions();
|
|
72
|
+
const before = isQuery ? {} : this.getSubscriptions();
|
|
66
73
|
this.subscriptions.set(params.uniqueId, {
|
|
67
74
|
...params,
|
|
68
75
|
count: 1, // set the count to one just in case it's set in param
|
|
69
76
|
});
|
|
70
77
|
// get subscriptions after change
|
|
71
|
-
const after = this.getSubscriptions();
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
78
|
+
const after = isQuery ? {} : this.getSubscriptions();
|
|
79
|
+
if (isQuery) {
|
|
80
|
+
// A query stream selects its callsigns server-side, so the callsign-keyed view above holds
|
|
81
|
+
// nothing for it: keep the query and diff on sources alone.
|
|
82
|
+
const sourcesBefore = this.sources;
|
|
83
|
+
this.sources = this.getSubscribedSources();
|
|
84
|
+
const toAdd = arrayRemove(this.sources, sourcesBefore);
|
|
85
|
+
if (toAdd.length) {
|
|
86
|
+
this.logger.debug('New query subscriptions added, we need to refresh sources', { toAdd });
|
|
87
|
+
void this.enqueueTelemetryAction('subscribe', this.callsignsLookup, toAdd);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
// assign the current values from state after change
|
|
92
|
+
this.callsignsLookup = new CallsignsLookup(Object.keys(after));
|
|
93
|
+
this.sources = arrayUnique(Object.values(after).reduce((a, v) => a.concat(v), []));
|
|
94
|
+
// compare before and after subscriptions and only add the difference
|
|
95
|
+
const { toAdd } = getSubscriptionsDifference(before, after);
|
|
96
|
+
if (toAdd.callsigns.length && toAdd.sources.length) {
|
|
97
|
+
this.logger.debug('New subscriptions added, we need to refresh sources', { toAdd });
|
|
98
|
+
void this.enqueueTelemetryAction('subscribe', new CallsignsLookup(toAdd.callsigns), toAdd.sources);
|
|
99
|
+
}
|
|
81
100
|
}
|
|
82
101
|
}
|
|
83
102
|
else {
|
|
@@ -97,16 +116,24 @@ export class TelemetryStreamAbstract {
|
|
|
97
116
|
if (subs.count)
|
|
98
117
|
return;
|
|
99
118
|
this.logger.info('Removing subscriptions from stream registry', params.uniqueId);
|
|
100
|
-
const
|
|
119
|
+
const isQuery = this.isQueryLookup();
|
|
120
|
+
const before = isQuery ? {} : this.getSubscriptions();
|
|
101
121
|
this.subscriptions.delete(params.uniqueId);
|
|
102
|
-
const after = this.getSubscriptions();
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
this.
|
|
106
|
-
this.
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
122
|
+
const after = isQuery ? {} : this.getSubscriptions();
|
|
123
|
+
if (isQuery) {
|
|
124
|
+
const sourcesBefore = this.sources;
|
|
125
|
+
this.sources = this.getSubscribedSources();
|
|
126
|
+
const toRemove = arrayRemove(sourcesBefore, this.sources);
|
|
127
|
+
if (toRemove.length) {
|
|
128
|
+
this.logger.debug('Query subscriptions removed, we need to refresh sources', { toRemove });
|
|
129
|
+
try {
|
|
130
|
+
await this.enqueueTelemetryAction('unsubscribe', this.callsignsLookup, toRemove, terminateReceiverGroup);
|
|
131
|
+
}
|
|
132
|
+
catch (err) {
|
|
133
|
+
this.logger.error(`Failed to unsubscribe: ${err}`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return;
|
|
110
137
|
}
|
|
111
138
|
// assign the current values from state after change
|
|
112
139
|
this.callsignsLookup = new CallsignsLookup(Object.keys(after));
|
|
@@ -115,13 +142,23 @@ export class TelemetryStreamAbstract {
|
|
|
115
142
|
if (toRemove.callsigns.length && toRemove.sources.length) {
|
|
116
143
|
this.logger.debug('Subscriptions removed, we need to refresh sources', { toRemove });
|
|
117
144
|
try {
|
|
118
|
-
await this.
|
|
145
|
+
await this.enqueueTelemetryAction('unsubscribe', new CallsignsLookup(toRemove.callsigns), toRemove.sources, terminateReceiverGroup);
|
|
119
146
|
}
|
|
120
147
|
catch (err) {
|
|
121
148
|
this.logger.error(`Failed to unsubscribe: ${err}`);
|
|
122
149
|
}
|
|
123
150
|
}
|
|
124
151
|
}
|
|
152
|
+
isQueryLookup() {
|
|
153
|
+
return this.callsignsLookup?.isQuery() ?? false;
|
|
154
|
+
}
|
|
155
|
+
getSubscribedSources() {
|
|
156
|
+
const sources = [];
|
|
157
|
+
this.subscriptions.forEach((data) => {
|
|
158
|
+
sources.push(...data.sources);
|
|
159
|
+
});
|
|
160
|
+
return arrayUnique(sources);
|
|
161
|
+
}
|
|
125
162
|
sendAcknowledgment(uid, status, noRetry) {
|
|
126
163
|
return this.sendAcknowledgmentInternal(uid, status, noRetry);
|
|
127
164
|
}
|
|
@@ -171,8 +208,8 @@ export class TelemetryStreamAbstract {
|
|
|
171
208
|
this.subscriberId = subscriberId;
|
|
172
209
|
rocosMessage.subscriberId = this.subscriberId;
|
|
173
210
|
this.logger.info('onData', `subscriberId has been updated - ${subscriberId}`, { subscriberId });
|
|
174
|
-
void this.
|
|
175
|
-
this.heartbeat.start(this.sendHeartbeat, this.sendHeartbeatTime);
|
|
211
|
+
void this.enqueueTelemetryAction('subscribe');
|
|
212
|
+
this.heartbeat.start(() => this.sendHeartbeat(), this.sendHeartbeatTime);
|
|
176
213
|
isRobotMessage = false;
|
|
177
214
|
break;
|
|
178
215
|
}
|
|
@@ -208,25 +245,40 @@ export class TelemetryStreamAbstract {
|
|
|
208
245
|
});
|
|
209
246
|
}
|
|
210
247
|
}
|
|
248
|
+
/**
|
|
249
|
+
* Gateway subscription state is a set mutated by deltas, so ordering is correctness: a concurrent
|
|
250
|
+
* unsubscribe landing last strands a subscribe and nothing re-adds it.
|
|
251
|
+
*/
|
|
252
|
+
enqueueTelemetryAction(actionType, callsignsLookup, sources, terminate) {
|
|
253
|
+
const takeAction = () => this.takeTelemetryAction(actionType, callsignsLookup, sources, terminate);
|
|
254
|
+
const run = this.telemetryActionQueue.then(takeAction);
|
|
255
|
+
// The tail only signals whose turn it is, so it must never reject, otherwise one failed op
|
|
256
|
+
// would wedge every op queued behind it. Callers still see the failure through `run`.
|
|
257
|
+
this.telemetryActionQueue = run.then(() => undefined, () => undefined);
|
|
258
|
+
return run;
|
|
259
|
+
}
|
|
211
260
|
async takeTelemetryAction(actionType, callsignsLookup, sources, terminate) {
|
|
212
261
|
this.logger.debug('takeTelemetryAction', actionType);
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
262
|
+
// A config fault, not a race: this stream can never address a request.
|
|
263
|
+
if (!this.projectId) {
|
|
264
|
+
this.logger.warn('takeTelemetryAction', actionType, `${actionType} without a projectId - dropped`);
|
|
265
|
+
return Promise.resolve();
|
|
266
|
+
}
|
|
267
|
+
if (!this.subscriberId) {
|
|
268
|
+
// Deferred, not lost: `onData` re-asserts `this.sources` when the subscriberId arrives.
|
|
269
|
+
this.logger.debug('takeTelemetryAction', actionType, `${actionType} before the receiver was registered - deferred to the post-registration re-assert`);
|
|
218
270
|
return Promise.resolve();
|
|
219
271
|
}
|
|
220
272
|
const actionSources = sources ?? this.sources;
|
|
221
273
|
const actionCallsigns = callsignsLookup ?? this.callsignsLookup;
|
|
222
|
-
if (
|
|
274
|
+
if (!actionCallsigns.isQuery()) {
|
|
223
275
|
// Compose the request message
|
|
224
276
|
const req = TelemetryRequest.create({
|
|
225
277
|
subscriberId: this.subscriberId,
|
|
226
278
|
requestedActions: [
|
|
227
279
|
{
|
|
228
280
|
operation: actionType,
|
|
229
|
-
callsigns: actionCallsigns.
|
|
281
|
+
callsigns: actionCallsigns.getCallsigns(),
|
|
230
282
|
sources: actionSources,
|
|
231
283
|
},
|
|
232
284
|
],
|
|
@@ -238,12 +290,12 @@ export class TelemetryStreamAbstract {
|
|
|
238
290
|
// Send the message to back-end.
|
|
239
291
|
return this.requestTelemetry(req);
|
|
240
292
|
}
|
|
241
|
-
|
|
242
|
-
|
|
293
|
+
const query = actionCallsigns.getQuery();
|
|
294
|
+
if (query) {
|
|
243
295
|
// We don't support nested query at the moment
|
|
244
296
|
const queryOrPredicatesList = [];
|
|
245
297
|
// Loop through user provided predicates to construct a telemetry query request
|
|
246
|
-
|
|
298
|
+
query.predicates.forEach((callsignsPredicate) => {
|
|
247
299
|
const queryOrPredicate = QueryOrPredicate.create({
|
|
248
300
|
content: {
|
|
249
301
|
oneofKind: 'predicate',
|
|
@@ -262,7 +314,7 @@ export class TelemetryStreamAbstract {
|
|
|
262
314
|
sources: actionSources,
|
|
263
315
|
operation: actionType,
|
|
264
316
|
callsignQuery: {
|
|
265
|
-
operation:
|
|
317
|
+
operation: query.operation.valueOf(),
|
|
266
318
|
queryOrPredicates: queryOrPredicatesList,
|
|
267
319
|
},
|
|
268
320
|
});
|
|
@@ -379,16 +431,11 @@ export class TelemetryStreamAbstract {
|
|
|
379
431
|
this.lastRobotMessageReceived = undefined;
|
|
380
432
|
this.registerReceiver();
|
|
381
433
|
}
|
|
382
|
-
isFoundCallsign(callsign) {
|
|
383
|
-
return this.callsignsLookup.lookupType === CallsignsLookupType.List
|
|
384
|
-
? this.callsignsLookup.lookupValue.indexOf(callsign) !== -1
|
|
385
|
-
: true; // If callsigns lookup option is query, we always return true
|
|
386
|
-
}
|
|
387
434
|
isFoundSource(source) {
|
|
388
435
|
return this.sources.indexOf(source) !== -1;
|
|
389
436
|
}
|
|
390
437
|
isRegisteredMessage(callsign, source) {
|
|
391
|
-
return this.
|
|
438
|
+
return this.callsignsLookup.includesCallsign(callsign) && this.isFoundSource(source);
|
|
392
439
|
}
|
|
393
440
|
updateReceivedDataStatsWithMessage(msg) {
|
|
394
441
|
const { source } = msg;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { AddIceCandidateRequest, GetDetailsRequest, OfferRequest, OperatorConnectRequest, } from '../../../grpc/pigeon_pb';
|
|
2
2
|
import { SubscriberStatusEnum, } from '../../../models';
|
|
3
3
|
import { BehaviorSubject } from 'rxjs';
|
|
4
|
-
import {
|
|
4
|
+
import { IDENTIFIER_NAME_WEBRTC_SIGNALLING } from '../../../constants/identifier';
|
|
5
5
|
import { RocosStore } from '../../../store/RocosStore';
|
|
6
6
|
import { StreamRegister } from '../../StreamRegister';
|
|
7
7
|
import { filter } from 'rxjs/operators';
|
|
@@ -9,7 +9,7 @@ export class WebRTCSignallingStreamAbstract {
|
|
|
9
9
|
constructor(config) {
|
|
10
10
|
this.subscriberStatus = SubscriberStatusEnum.STOPPED;
|
|
11
11
|
this.scope = config.scope;
|
|
12
|
-
this.identifier = StreamRegister.getIdentifier(
|
|
12
|
+
this.identifier = StreamRegister.getIdentifier(IDENTIFIER_NAME_WEBRTC_SIGNALLING, this.scope);
|
|
13
13
|
this.token = config.token;
|
|
14
14
|
this.url = config.url;
|
|
15
15
|
this.statusStream$ = new BehaviorSubject(SubscriberStatusEnum.STOPPED);
|