@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.
Files changed (72) hide show
  1. package/LICENSE +3 -0
  2. package/cjs/api/StreamRegister.d.ts +18 -3
  3. package/cjs/api/StreamRegister.js +71 -38
  4. package/cjs/api/atoms/StreamHeartbeat.js +2 -2
  5. package/cjs/api/streams/telemetry/TelemetryStreamAbstract.d.ts +8 -1
  6. package/cjs/api/streams/telemetry/TelemetryStreamAbstract.js +87 -40
  7. package/cjs/api/streams/webRTCSignalling/WebRTCSignallingStreamAbstract.js +1 -1
  8. package/cjs/constants/api.d.ts +3 -2
  9. package/cjs/constants/api.js +5 -4
  10. package/cjs/constants/identifier.d.ts +1 -0
  11. package/cjs/constants/identifier.js +2 -1
  12. package/cjs/helpers/getUniqueId.js +2 -1
  13. package/cjs/models/callsigns/CallsignsLookup.d.ts +4 -0
  14. package/cjs/models/callsigns/CallsignsLookup.js +19 -0
  15. package/cjs/models/robot-status/RobotStatus.d.ts +23 -0
  16. package/cjs/models/robot-status/RobotStatusEnums.d.ts +18 -6
  17. package/cjs/models/robot-status/RobotStatusEnums.js +18 -6
  18. package/cjs/services/AssetStorageService.js +6 -1
  19. package/cjs/services/BaseServiceAbstract.js +1 -1
  20. package/cjs/services/BaseStreamService.d.ts +12 -1
  21. package/cjs/services/BaseStreamService.js +19 -30
  22. package/cjs/services/EnvironmentService.d.ts +4 -0
  23. package/cjs/services/EnvironmentService.js +5 -1
  24. package/cjs/services/MapService.d.ts +15 -3
  25. package/cjs/services/MapService.js +25 -13
  26. package/cjs/services/RobotStatusService.d.ts +20 -1
  27. package/cjs/services/RobotStatusService.js +5 -0
  28. package/cjs/services/SearchService.js +1 -1
  29. package/cjs/services/TargetService.js +2 -2
  30. package/cjs/services/TelemetryService.d.ts +1 -2
  31. package/cjs/services/TelemetryService.js +30 -28
  32. package/cjs/services/WebRTCSignallingService.js +7 -9
  33. package/cjs/services/robot-status/connectivity.d.ts +16 -12
  34. package/cjs/services/robot-status/connectivity.js +26 -15
  35. package/cjs/services/robot-status/reportedStatus.d.ts +14 -1
  36. package/cjs/services/robot-status/reportedStatus.js +36 -1
  37. package/esm/api/StreamRegister.d.ts +18 -3
  38. package/esm/api/StreamRegister.js +71 -38
  39. package/esm/api/atoms/StreamHeartbeat.js +2 -2
  40. package/esm/api/streams/telemetry/TelemetryStreamAbstract.d.ts +8 -1
  41. package/esm/api/streams/telemetry/TelemetryStreamAbstract.js +88 -41
  42. package/esm/api/streams/webRTCSignalling/WebRTCSignallingStreamAbstract.js +2 -2
  43. package/esm/constants/api.d.ts +3 -2
  44. package/esm/constants/api.js +3 -2
  45. package/esm/constants/identifier.d.ts +1 -0
  46. package/esm/constants/identifier.js +1 -0
  47. package/esm/helpers/getUniqueId.js +2 -1
  48. package/esm/models/callsigns/CallsignsLookup.d.ts +4 -0
  49. package/esm/models/callsigns/CallsignsLookup.js +19 -0
  50. package/esm/models/robot-status/RobotStatus.d.ts +23 -0
  51. package/esm/models/robot-status/RobotStatusEnums.d.ts +18 -6
  52. package/esm/models/robot-status/RobotStatusEnums.js +18 -6
  53. package/esm/services/AssetStorageService.js +6 -1
  54. package/esm/services/BaseServiceAbstract.js +1 -1
  55. package/esm/services/BaseStreamService.d.ts +12 -1
  56. package/esm/services/BaseStreamService.js +19 -30
  57. package/esm/services/EnvironmentService.d.ts +4 -0
  58. package/esm/services/EnvironmentService.js +5 -1
  59. package/esm/services/MapService.d.ts +15 -3
  60. package/esm/services/MapService.js +26 -14
  61. package/esm/services/RobotStatusService.d.ts +20 -1
  62. package/esm/services/RobotStatusService.js +6 -1
  63. package/esm/services/SearchService.js +2 -2
  64. package/esm/services/TargetService.js +3 -3
  65. package/esm/services/TelemetryService.d.ts +1 -2
  66. package/esm/services/TelemetryService.js +31 -29
  67. package/esm/services/WebRTCSignallingService.js +8 -10
  68. package/esm/services/robot-status/connectivity.d.ts +16 -12
  69. package/esm/services/robot-status/connectivity.js +26 -15
  70. package/esm/services/robot-status/reportedStatus.d.ts +14 -1
  71. package/esm/services/robot-status/reportedStatus.js +35 -1
  72. package/package.json +7 -2
@@ -1,5 +1,5 @@
1
1
  import { BehaviorSubject, catchError, combineLatest, defer, distinctUntilChanged, from, interval, map, mergeAll, of, startWith, } from 'rxjs';
2
- import { CallsignStatus, CallsignsLookup, CallsignsLookupType, RocosError, SubscriberStatusEnum, TelemetryMonitorStatus, errorCodes, } from '../models';
2
+ import { CallsignStatus, CallsignsLookup, RocosError, SubscriberStatusEnum, TelemetryMonitorStatus, errorCodes, } from '../models';
3
3
  import { filter, finalize } from 'rxjs/operators';
4
4
  import { BaseStreamService } from './BaseStreamService';
5
5
  import { IDENTIFIER_NAME_TELEMETRY } from '../constants/identifier';
@@ -52,31 +52,42 @@ export class TelemetryService extends BaseStreamService {
52
52
  // scope is used as a key to generate unique streams
53
53
  // ideally there should be just one concurrent stream opened
54
54
  const scope = this.buildScope(params, callsignsLookup);
55
- const callsigns = callsignsLookup.lookupValue;
56
55
  const subscriptionParams = {
57
56
  uniqueId: getUniqueId(params.projectId, callsignsLookup, sources, scope),
58
- callsigns: callsignsLookup?.lookupType === CallsignsLookupType.List ? callsignsLookup.lookupValue : [],
57
+ callsigns: callsignsLookup.getCallsigns(),
59
58
  sources,
60
59
  };
61
60
  // `defer` acquires the stream only when subscribed; `finalize` releases it on unsubscribe, error
62
- // and complete — so acquire and release stay balanced on every path.
61
+ // and complete — so acquire and release stay balanced on every path. The registry refcount taken
62
+ // here keeps the stream alive until the finalize, so everything in between is race-free.
63
63
  return defer(() => {
64
- let acquired;
65
- const stream$ = this.createStream(params.projectId, callsignsLookup, sources, scope, (stream) => {
66
- acquired = stream;
64
+ const { stream, isNew, ready, release } = this.acquireStream(IDENTIFIER_NAME_TELEMETRY, this.buildStreamConfig(params.projectId, callsignsLookup, sources, scope));
65
+ try {
67
66
  stream.addSubscription(subscriptionParams);
68
- }).then((stream) => {
69
- if (!this.statusSubscription) {
70
- this.statusSubscription = stream.statusStream$.subscribe((msg) => {
71
- this.status = msg === SubscriberStatusEnum.STOPPED || msg === SubscriberStatusEnum.ALIVE;
72
- this.status$.next(msg);
67
+ // Forward the stream's status onto the per-scope subject once, at creation; it ends when the
68
+ // stream stops (statusStream$ completes), so it is not re-created or leaked across subscribers.
69
+ if (isNew) {
70
+ stream.statusStream$.subscribe((msg) => {
71
+ this.streamStatusSubject$.next({
72
+ scope,
73
+ status: msg,
74
+ });
73
75
  });
74
76
  }
75
- return from(stream.messageStream$);
76
- });
77
- return from(stream$).pipe(mergeAll(), filter((message) => {
78
- return callsigns.includes(message.callsign) && sources.includes(message.source);
79
- }), finalize(() => acquired?.removeSubscription(subscriptionParams, params.terminateReceiverGroupOnUnsubscribe)));
77
+ }
78
+ catch (e) {
79
+ // finalize below is not wired up yet, so a throw here must hand the refcount back itself.
80
+ release();
81
+ throw e;
82
+ }
83
+ // status$ / getStatus() are driven by the acquireStream factory (BaseStreamService), not here.
84
+ const messages$ = ready.then(() => from(stream.messageStream$));
85
+ return from(messages$).pipe(mergeAll(), filter((message) => {
86
+ return callsignsLookup.includesCallsign(message.callsign) && sources.includes(message.source);
87
+ }), finalize(() => {
88
+ void stream.removeSubscription(subscriptionParams, params.terminateReceiverGroupOnUnsubscribe);
89
+ release();
90
+ }));
80
91
  });
81
92
  }
82
93
  /**
@@ -156,8 +167,8 @@ export class TelemetryService extends BaseStreamService {
156
167
  return `${params.projectId}-default-${flattenCallsignsLookup(callsignsLookup).join()}`;
157
168
  return `${params.projectId}-default`;
158
169
  }
159
- async createStream(projectId, callsignsLookup, sources, scope, onAcquire) {
160
- const newStream = await this.createStreamFromConfig(IDENTIFIER_NAME_TELEMETRY, {
170
+ buildStreamConfig(projectId, callsignsLookup, sources, scope) {
171
+ return {
161
172
  url: this.config.url,
162
173
  projectId,
163
174
  callsignsLookup,
@@ -169,16 +180,7 @@ export class TelemetryService extends BaseStreamService {
169
180
  insecure: this.config.insecure,
170
181
  transport: this.config.transport,
171
182
  fetch: this.config.fetch,
172
- }, onAcquire);
173
- if (!newStream.isNew) {
174
- newStream.stream.statusStream$.subscribe((msg) => {
175
- this.streamStatusSubject$.next({
176
- scope,
177
- status: msg,
178
- });
179
- });
180
- }
181
- return newStream.stream;
183
+ };
182
184
  }
183
185
  getStream(config) {
184
186
  if (config.transport === 'grpc-web') {
@@ -1,4 +1,4 @@
1
- import { IDENTIFIER_NAME_COMMAND } from '../constants/identifier';
1
+ import { IDENTIFIER_NAME_WEBRTC_SIGNALLING } from '../constants/identifier';
2
2
  import { RocosLogger } from '../logger/RocosLogger';
3
3
  import { RocosStore } from '../store/RocosStore';
4
4
  import { StreamRegister } from '../api/StreamRegister';
@@ -34,11 +34,9 @@ export class WebRTCSignallingService {
34
34
  }
35
35
  async createSignallingStream() {
36
36
  const scope = 'default';
37
- const identifier = StreamRegister.getIdentifier(IDENTIFIER_NAME_COMMAND, scope);
38
- const streamRegister = StreamRegister.getInstance();
39
- let stream = streamRegister.getStream(identifier);
40
- if (!stream) {
41
- stream = this.getStream({
37
+ const identifier = StreamRegister.getIdentifier(IDENTIFIER_NAME_WEBRTC_SIGNALLING, scope);
38
+ const { stream, ready } = StreamRegister.getInstance().acquireStream(identifier, () => {
39
+ const newStream = this.getStream({
42
40
  url: this.config.url,
43
41
  token: this.config.token,
44
42
  scope,
@@ -46,13 +44,13 @@ export class WebRTCSignallingService {
46
44
  port: this.config.port,
47
45
  insecure: this.config.insecure,
48
46
  });
49
- stream.statusStream$.subscribe((msg) => {
47
+ newStream.statusStream$.subscribe((msg) => {
50
48
  this.status = msg === SubscriberStatusEnum.STOPPED || msg === SubscriberStatusEnum.ALIVE;
51
49
  this.status$.next(msg);
52
50
  });
53
- streamRegister.addStream(stream);
54
- await this.initStream(stream);
55
- }
51
+ return newStream;
52
+ }, (newStream) => this.initStream(newStream));
53
+ await ready;
56
54
  return stream;
57
55
  }
58
56
  async initStream(stream) {
@@ -36,18 +36,19 @@ export declare const buildConnectionsUrl: (serviceId: string, baseUrl: string, p
36
36
  export declare const mapConnectionStatus: (connection?: IConnectedCallsign | null) => ServiceConnection;
37
37
  /**
38
38
  * Aggregates per-service connection states into an overall verdict:
39
- * - `ONLINE` at least one service connected and none disconnected.
40
- * - `DEGRADED` — a genuine mix: some connected, some disconnected.
39
+ * - `DEGRADED` any service reports an explicit `FAILED` fault, or a genuine mix (some connected,
40
+ * some disconnected).
41
+ * - `ONLINE` — at least one service connected and none disconnected or failed.
41
42
  * - `OFFLINE` — nothing connected.
42
43
  *
43
- * `UNKNOWN` services (e.g. not yet polled) never force `DEGRADED`, so startup
44
- * settles on `OFFLINE` (gray) rather than a spurious flashing-red degraded state.
44
+ * A `FAILED` service forces `DEGRADED` even when nothing else is connected, so a fault (e.g. a failed
45
+ * deployment) raises attention rather than reading as a plain, switched-off `OFFLINE`.
45
46
  *
46
- * Note: this optimism also means a service held `UNKNOWN` indefinitely (e.g. an
47
- * endpoint whose polls keep failing) does not by itself pull the aggregate below
48
- * `ONLINE` a deliberate v1 tradeoff to keep transient errors from flapping the
49
- * indicator. Surfacing a long-lived failure distinctly (likely via health rather
50
- * than connectivity) is left as a future refinement.
47
+ * `UNKNOWN` services (e.g. not yet polled) never force `DEGRADED`, so startup settles on `OFFLINE`
48
+ * (gray) rather than a spurious flashing-red degraded state. This optimism also means a service held
49
+ * `UNKNOWN` indefinitely (e.g. an endpoint whose polls keep failing) does not by itself pull the
50
+ * aggregate below `ONLINE` — a deliberate tradeoff to keep transient errors from flapping the
51
+ * indicator; a service that wants to be surfaced reports `FAILED` explicitly.
51
52
  */
52
53
  export declare const aggregateConnectivity: (services: Record<string, ServiceConnection>) => RobotConnectivity;
53
54
  /**
@@ -66,11 +67,14 @@ export interface ConnectivityDeps {
66
67
  insecure?: boolean;
67
68
  /** The telemetry-liveness signal (from `getTelemetryLiveness`), folded in as one service. */
68
69
  telemetryLiveness$: Observable<ServiceConnection>;
70
+ /** Caller-injected connectivity signals, keyed by service id, folded in on equal footing. */
71
+ extraServices?: Record<string, Observable<ServiceConnection>>;
69
72
  }
70
73
  /**
71
74
  * Emits the robot's {@link RobotConnectivityStatus} — the per-service breakdown
72
- * plus the aggregate verdict — for the configured `expectedServices`. HTTP
73
- * services are polled; `telemetry-liveness` uses the supplied observable; any
74
- * unrecognised service id resolves to `UNKNOWN`.
75
+ * plus the aggregate verdict — for the configured `expectedServices`, plus any
76
+ * caller-injected `extraServices` (folded in on equal footing). HTTP services
77
+ * are polled; `telemetry-liveness` and injected services use their supplied
78
+ * observable; any unrecognised service id resolves to `UNKNOWN`.
75
79
  */
76
80
  export declare const getConnectivityChanges: (deps: ConnectivityDeps, projectId: string, callsign: string, expectedServices?: string[], pollMs?: number) => Observable<RobotConnectivityStatus>;
@@ -52,21 +52,24 @@ export const mapConnectionStatus = (connection) => {
52
52
  };
53
53
  /**
54
54
  * Aggregates per-service connection states into an overall verdict:
55
- * - `ONLINE` at least one service connected and none disconnected.
56
- * - `DEGRADED` — a genuine mix: some connected, some disconnected.
55
+ * - `DEGRADED` any service reports an explicit `FAILED` fault, or a genuine mix (some connected,
56
+ * some disconnected).
57
+ * - `ONLINE` — at least one service connected and none disconnected or failed.
57
58
  * - `OFFLINE` — nothing connected.
58
59
  *
59
- * `UNKNOWN` services (e.g. not yet polled) never force `DEGRADED`, so startup
60
- * settles on `OFFLINE` (gray) rather than a spurious flashing-red degraded state.
60
+ * A `FAILED` service forces `DEGRADED` even when nothing else is connected, so a fault (e.g. a failed
61
+ * deployment) raises attention rather than reading as a plain, switched-off `OFFLINE`.
61
62
  *
62
- * Note: this optimism also means a service held `UNKNOWN` indefinitely (e.g. an
63
- * endpoint whose polls keep failing) does not by itself pull the aggregate below
64
- * `ONLINE` a deliberate v1 tradeoff to keep transient errors from flapping the
65
- * indicator. Surfacing a long-lived failure distinctly (likely via health rather
66
- * than connectivity) is left as a future refinement.
63
+ * `UNKNOWN` services (e.g. not yet polled) never force `DEGRADED`, so startup settles on `OFFLINE`
64
+ * (gray) rather than a spurious flashing-red degraded state. This optimism also means a service held
65
+ * `UNKNOWN` indefinitely (e.g. an endpoint whose polls keep failing) does not by itself pull the
66
+ * aggregate below `ONLINE` — a deliberate tradeoff to keep transient errors from flapping the
67
+ * indicator; a service that wants to be surfaced reports `FAILED` explicitly.
67
68
  */
68
69
  export const aggregateConnectivity = (services) => {
69
70
  const values = Object.values(services);
71
+ if (values.includes(ServiceConnection.FAILED))
72
+ return RobotConnectivity.DEGRADED;
70
73
  const hasConnected = values.includes(ServiceConnection.CONNECTED);
71
74
  const hasDisconnected = values.includes(ServiceConnection.DISCONNECTED);
72
75
  if (hasConnected && hasDisconnected)
@@ -99,24 +102,32 @@ const connectivityEquals = (a, b) => {
99
102
  };
100
103
  /**
101
104
  * Emits the robot's {@link RobotConnectivityStatus} — the per-service breakdown
102
- * plus the aggregate verdict — for the configured `expectedServices`. HTTP
103
- * services are polled; `telemetry-liveness` uses the supplied observable; any
104
- * unrecognised service id resolves to `UNKNOWN`.
105
+ * plus the aggregate verdict — for the configured `expectedServices`, plus any
106
+ * caller-injected `extraServices` (folded in on equal footing). HTTP services
107
+ * are polled; `telemetry-liveness` and injected services use their supplied
108
+ * observable; any unrecognised service id resolves to `UNKNOWN`.
105
109
  */
106
110
  export const getConnectivityChanges = (deps, projectId, callsign, expectedServices = DEFAULT_EXPECTED_SERVICES, pollMs = DEFAULT_CONNECTIVITY_POLL_MS) => {
111
+ const extraServices = deps.extraServices ?? {};
112
+ const serviceIds = [...expectedServices, ...Object.keys(extraServices).filter((id) => !expectedServices.includes(id))];
107
113
  // combineLatest([]) completes without emitting; emit an explicit empty/offline status instead.
108
- if (expectedServices.length === 0) {
114
+ if (serviceIds.length === 0) {
109
115
  return of({ overall: RobotConnectivity.OFFLINE, services: {} });
110
116
  }
111
- const perService = expectedServices.map((serviceId) => {
117
+ const perService = serviceIds.map((serviceId) => {
112
118
  let source$;
113
119
  if (serviceId === TELEMETRY_LIVENESS_SERVICE) {
114
120
  source$ = deps.telemetryLiveness$;
115
121
  }
116
- else if (serviceId in CONNECTION_SERVICE_PREFIXES) {
122
+ else if (Object.prototype.hasOwnProperty.call(CONNECTION_SERVICE_PREFIXES, serviceId)) {
117
123
  const url = buildConnectionsUrl(serviceId, deps.baseUrl, projectId, callsign, deps.insecure);
118
124
  source$ = getServiceConnectionChanges(deps.httpGet, url, pollMs);
119
125
  }
126
+ else if (Object.prototype.hasOwnProperty.call(extraServices, serviceId)) {
127
+ // Contain an injected observable's errors as UNKNOWN, mirroring the built-in sources, so a
128
+ // faulty caller signal can't error out combineLatest and tear down the whole aggregate.
129
+ source$ = extraServices[serviceId].pipe(catchError(() => of(ServiceConnection.UNKNOWN)));
130
+ }
120
131
  else {
121
132
  source$ = of(ServiceConnection.UNKNOWN);
122
133
  }
@@ -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,9 +1,11 @@
1
1
  import { catchError, combineLatest, distinctUntilChanged, filter, interval, map, merge, of, scan, startWith, } from 'rxjs';
2
- import { RobotHealth, RobotReadiness, ServiceConnection } from '../../models';
2
+ import { RobotHealth, RobotReadiness, ServiceConnection, } from '../../models';
3
3
  /** The new agent topic carrying health + readiness (replaces the heartbeat). */
4
4
  export const ROBOT_STATUS_SOURCE = '/diagnostics/robot/status';
5
5
  /** The legacy heartbeat topic — liveness only, no status payload. */
6
6
  export const LEGACY_HEARTBEAT_SOURCE = '/rocos/agent/telemetry/heartbeat';
7
+ /** The per-contributor breakdown behind the reported status (health + readiness). */
8
+ export const ROBOT_STATUS_CONTRIBUTORS_SOURCE = `${ROBOT_STATUS_SOURCE}/contributors`;
7
9
  const DEFAULT_HEARTBEAT_TIMEOUT_MS = 5000;
8
10
  const DEFAULT_INTERVAL_MS = 2000;
9
11
  /**
@@ -78,6 +80,38 @@ export const getReportedStatusChanges = (telemetry, projectId, callsign) => {
78
80
  // UNKNOWN/UNKNOWN (matching getTelemetryLiveness) so subscribers get an honest value.
79
81
  catchError(() => of({ health: RobotHealth.UNKNOWN, readiness: RobotReadiness.UNKNOWN })));
80
82
  };
83
+ const toContributor = (raw, parse) => ({
84
+ id: raw.id ?? '',
85
+ component: raw.component ?? '',
86
+ reason: raw.reason ?? '',
87
+ status: parse(raw.value),
88
+ });
89
+ const EMPTY_CONTRIBUTORS = { health: [], readiness: [] };
90
+ /**
91
+ * Emits the per-contributor breakdown behind the robot's reported health and
92
+ * readiness, from the agent's `robot/status/contributors` topic. Each
93
+ * contributor's integer wire value is mapped onto {@link RobotHealth} /
94
+ * {@link RobotReadiness}; a missing `health`/`readiness` array becomes `[]`.
95
+ *
96
+ * Starts with an empty breakdown so consumers combining this with other streams
97
+ * emit immediately, and — like {@link getReportedStatusChanges} — a telemetry
98
+ * error falls back to an empty breakdown rather than terminating the stream.
99
+ */
100
+ export const getStatusContributorsChanges = (telemetry, projectId, callsign) => {
101
+ return telemetry
102
+ .subscribe({
103
+ projectId,
104
+ callsigns: [callsign],
105
+ sources: [ROBOT_STATUS_CONTRIBUTORS_SOURCE],
106
+ })
107
+ .pipe(map((message) => {
108
+ const payload = message.payload ?? {};
109
+ return {
110
+ health: (payload.health ?? []).map((c) => toContributor(c, parseRobotHealth)),
111
+ readiness: (payload.readiness ?? []).map((c) => toContributor(c, parseRobotReadiness)),
112
+ };
113
+ }), startWith(EMPTY_CONTRIBUTORS), distinctUntilChanged((a, b) => JSON.stringify(a) === JSON.stringify(b)), catchError(() => of(EMPTY_CONTRIBUTORS)));
114
+ };
81
115
  /**
82
116
  * Emits the `telemetry-liveness` connectivity signal: whether a heartbeat (from
83
117
  * either the new `robot/status` topic or the legacy heartbeat) has been received
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dronedeploy/rocos-js-sdk",
3
- "version": "4.4.4",
3
+ "version": "4.4.6",
4
4
  "description": "Javascript SDK for rocos",
5
5
  "main": "cjs/index.js",
6
6
  "module": "esm/index.js",
@@ -42,5 +42,10 @@
42
42
  }
43
43
  },
44
44
  "repository": {},
45
- "private": false
45
+ "private": false,
46
+ "overrides": {
47
+ "openapi-typescript": {
48
+ "typescript": "$typescript"
49
+ }
50
+ }
46
51
  }