@dronedeploy/rocos-js-sdk 4.4.5 → 4.4.7

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 (49) hide show
  1. package/cjs/api/StreamRegister.js +4 -4
  2. package/cjs/api/streams/caller/CallerStreamAbstract.js +2 -1
  3. package/cjs/api/streams/command/CommandStreamAbstract.js +2 -1
  4. package/cjs/api/streams/control/ControlStreamAbstract.js +2 -1
  5. package/cjs/api/streams/fileAccessor/FileAccessorStreamAbstract.js +2 -1
  6. package/cjs/api/streams/search/SearchStreamAbstract.js +2 -1
  7. package/cjs/api/streams/telemetry/TelemetryStream.js +1 -0
  8. package/cjs/api/streams/telemetry/TelemetryStreamAbstract.js +130 -25
  9. package/cjs/api/streams/telemetry/TelemetryStreamConnect.js +1 -0
  10. package/cjs/api/streams/telemetry/TelemetryStreamNode.js +1 -0
  11. package/cjs/api/streams/webRTCSignalling/WebRTCSignallingStreamAbstract.js +2 -1
  12. package/cjs/models/maps/Map.d.ts +1 -1
  13. package/cjs/models/robot-status/RobotStatus.d.ts +23 -0
  14. package/cjs/models/robot-status/RobotStatusEnums.d.ts +18 -6
  15. package/cjs/models/robot-status/RobotStatusEnums.js +18 -6
  16. package/cjs/services/RobotStatusService.d.ts +20 -1
  17. package/cjs/services/RobotStatusService.js +5 -0
  18. package/cjs/services/SearchService.js +2 -7
  19. package/cjs/services/TelemetryService.d.ts +4 -1
  20. package/cjs/services/TelemetryService.js +17 -12
  21. package/cjs/services/robot-status/connectivity.d.ts +16 -12
  22. package/cjs/services/robot-status/connectivity.js +26 -15
  23. package/cjs/services/robot-status/reportedStatus.d.ts +14 -1
  24. package/cjs/services/robot-status/reportedStatus.js +36 -1
  25. package/esm/api/StreamRegister.js +4 -4
  26. package/esm/api/streams/caller/CallerStreamAbstract.js +2 -1
  27. package/esm/api/streams/command/CommandStreamAbstract.js +2 -1
  28. package/esm/api/streams/control/ControlStreamAbstract.js +2 -1
  29. package/esm/api/streams/fileAccessor/FileAccessorStreamAbstract.js +2 -1
  30. package/esm/api/streams/search/SearchStreamAbstract.js +2 -1
  31. package/esm/api/streams/telemetry/TelemetryStream.js +1 -0
  32. package/esm/api/streams/telemetry/TelemetryStreamAbstract.js +131 -26
  33. package/esm/api/streams/telemetry/TelemetryStreamConnect.js +1 -0
  34. package/esm/api/streams/telemetry/TelemetryStreamNode.js +1 -0
  35. package/esm/api/streams/webRTCSignalling/WebRTCSignallingStreamAbstract.js +2 -1
  36. package/esm/models/maps/Map.d.ts +1 -1
  37. package/esm/models/robot-status/RobotStatus.d.ts +23 -0
  38. package/esm/models/robot-status/RobotStatusEnums.d.ts +18 -6
  39. package/esm/models/robot-status/RobotStatusEnums.js +18 -6
  40. package/esm/services/RobotStatusService.d.ts +20 -1
  41. package/esm/services/RobotStatusService.js +6 -1
  42. package/esm/services/SearchService.js +2 -7
  43. package/esm/services/TelemetryService.d.ts +4 -1
  44. package/esm/services/TelemetryService.js +17 -12
  45. package/esm/services/robot-status/connectivity.d.ts +16 -12
  46. package/esm/services/robot-status/connectivity.js +26 -15
  47. package/esm/services/robot-status/reportedStatus.d.ts +14 -1
  48. package/esm/services/robot-status/reportedStatus.js +35 -1
  49. package/package.json +7 -2
@@ -82,13 +82,13 @@ class StreamRegister {
82
82
  return;
83
83
  // Wait for init to settle before tearing down, so init can never open resources on an already
84
84
  // stopped stream. A re-acquire in the meantime revives the entry and cancels the teardown.
85
- void entry.ready
86
- .catch(() => undefined)
87
- .then(() => {
85
+ const evictIfUnheld = () => {
88
86
  if (this.entries.get(identifier) === entry && entry.refCount <= 0) {
89
87
  this.evict(identifier, entry.stream);
90
88
  }
91
- })
89
+ };
90
+ void entry.ready
91
+ .then(() => evictIfUnheld(), () => evictIfUnheld())
92
92
  .catch((e) => this.logger.error(`Failed to evict stream. identifier: ${identifier}`, e));
93
93
  }
94
94
  evict(identifier, stream) {
@@ -29,7 +29,8 @@ class CallerStreamAbstract {
29
29
  this.token = token;
30
30
  }
31
31
  stopStream() {
32
- // do nothing for now
32
+ this.statusStream$.next(models_1.SubscriberStatusEnum.STOPPED);
33
+ this.statusStream$.complete();
33
34
  }
34
35
  invokeRequest(params) {
35
36
  this.logger.info('invokeRequest', params);
@@ -31,7 +31,8 @@ class CommandStreamAbstract {
31
31
  this.token = token;
32
32
  }
33
33
  stopStream() {
34
- // do nothing for now
34
+ this.statusStream$.next(models_1.SubscriberStatusEnum.STOPPED);
35
+ this.statusStream$.complete();
35
36
  }
36
37
  invokeRequest(params) {
37
38
  if (params.timeoutMs === undefined || params.timeoutMs === null) {
@@ -28,7 +28,8 @@ class ControlStreamAbstract {
28
28
  this.token = token;
29
29
  }
30
30
  stopStream() {
31
- // do nothing for now
31
+ this.statusStream$.next(models_1.SubscriberStatusEnum.STOPPED);
32
+ this.statusStream$.complete();
32
33
  }
33
34
  takeActionWithAck(action, params, ackTimeoutInSeconds = 15) {
34
35
  return new rxjs_1.Observable((emitter) => {
@@ -27,7 +27,8 @@ class FileAccessorStreamAbstract {
27
27
  this.token = token;
28
28
  }
29
29
  stopStream() {
30
- // do nothing for now
30
+ this.statusStream$.next(models_1.SubscriberStatusEnum.STOPPED);
31
+ this.statusStream$.complete();
31
32
  }
32
33
  async serverReqOp(params) {
33
34
  return this.serverReqOpInternal(params);
@@ -28,7 +28,8 @@ class SearchStreamAbstract {
28
28
  this.token = token;
29
29
  }
30
30
  stopStream() {
31
- // do nothing for now
31
+ this.statusStream$.next(models_1.SubscriberStatusEnum.STOPPED);
32
+ this.statusStream$.complete();
32
33
  }
33
34
  searchStream(params) {
34
35
  const request = this.convertToQueryMessage(params.query);
@@ -24,6 +24,7 @@ class TelemetryStream extends TelemetryStreamAbstract_1.TelemetryStreamAbstract
24
24
  this.client = new teletubby_pb_client_1.TelemetryGatewayClient(transport);
25
25
  }
26
26
  stopInternal() {
27
+ this.subscriberId = undefined;
27
28
  if (this.abortController) {
28
29
  this.logger.info('Starting stop stream process');
29
30
  this.abortController.abort();
@@ -13,6 +13,33 @@ const arrayRemove_1 = require("../../../helpers/arrayRemove");
13
13
  const arrayUnique_1 = require("../../../helpers/arrayUnique");
14
14
  const operators_1 = require("rxjs/operators");
15
15
  const getSubscriptionsDifference_1 = require("../../../helpers/getSubscriptionsDifference");
16
+ const telemetryActionQueueStates = new WeakMap();
17
+ const resolveTelemetryActions = (actions) => {
18
+ actions.forEach((action) => action.resolve());
19
+ };
20
+ const resolveUnsentTelemetryActions = (queueState) => {
21
+ resolveTelemetryActions(queueState.pendingActions.splice(0));
22
+ };
23
+ const takeNextTelemetryBatch = (queueState) => {
24
+ const firstAction = queueState.pendingActions.shift();
25
+ if (!firstAction)
26
+ return [];
27
+ const activeBatch = [firstAction];
28
+ if (firstAction.callsignsLookup.isQuery() || firstAction.terminate)
29
+ return activeBatch;
30
+ while (queueState.pendingActions.length > 0) {
31
+ const [nextAction] = queueState.pendingActions;
32
+ if (nextAction.callsignsLookup.isQuery() ||
33
+ nextAction.terminate ||
34
+ nextAction.actionType !== firstAction.actionType ||
35
+ nextAction.subscriberId !== firstAction.subscriberId ||
36
+ nextAction.registration !== firstAction.registration) {
37
+ break;
38
+ }
39
+ activeBatch.push(queueState.pendingActions.shift());
40
+ }
41
+ return activeBatch;
42
+ };
16
43
  class TelemetryStreamAbstract {
17
44
  constructor(config) {
18
45
  this.telemetryActionQueue = Promise.resolve();
@@ -38,6 +65,14 @@ class TelemetryStreamAbstract {
38
65
  this.messageStream$ = new rxjs_1.Subject();
39
66
  this.statusStream$ = new rxjs_1.BehaviorSubject(models_1.SubscriberStatusEnum.STOPPED);
40
67
  this.heartbeat = new StreamHeartbeat_1.StreamHeartbeat(this.identifier);
68
+ telemetryActionQueueStates.set(this, {
69
+ pendingActions: [],
70
+ activeBatch: [],
71
+ registration: 0,
72
+ drainScheduled: false,
73
+ requestInFlight: false,
74
+ stopped: false,
75
+ });
41
76
  RocosStore_1.RocosStore.getChangeSubject()
42
77
  .pipe((0, operators_1.filter)((message) => {
43
78
  return message.type === 'token' && message.url === this.url;
@@ -55,6 +90,12 @@ class TelemetryStreamAbstract {
55
90
  this.registerReceiver();
56
91
  }
57
92
  stopStream() {
93
+ const queueState = telemetryActionQueueStates.get(this);
94
+ if (queueState) {
95
+ queueState.stopped = true;
96
+ resolveUnsentTelemetryActions(queueState);
97
+ }
98
+ this.subscriberId = undefined;
58
99
  this.heartbeat?.stop();
59
100
  this.stopInternal();
60
101
  if (this.messagesTimer) {
@@ -62,6 +103,10 @@ class TelemetryStreamAbstract {
62
103
  }
63
104
  this.subscriberStatus = models_1.SubscriberStatusEnum.STOPPED;
64
105
  this.statusStream$.next(this.subscriberStatus);
106
+ // The stream is being torn down (the registry only stops a stream when it is evicted), so
107
+ // complete the status stream: this releases every service-level forwarder subscribed to it
108
+ // rather than leaving them attached to a dead instance.
109
+ this.statusStream$.complete();
65
110
  }
66
111
  addSubscription(params) {
67
112
  this.logger.info('Adding subscriptions from stream', params.uniqueId);
@@ -83,7 +128,9 @@ class TelemetryStreamAbstract {
83
128
  const toAdd = (0, arrayRemove_1.arrayRemove)(this.sources, sourcesBefore);
84
129
  if (toAdd.length) {
85
130
  this.logger.debug('New query subscriptions added, we need to refresh sources', { toAdd });
86
- void this.enqueueTelemetryAction('subscribe', this.callsignsLookup, toAdd);
131
+ void this.enqueueTelemetryAction('subscribe', this.callsignsLookup, toAdd).catch((error) => {
132
+ this.logger.error(`Failed to subscribe: ${error}`);
133
+ });
87
134
  }
88
135
  }
89
136
  else {
@@ -94,7 +141,9 @@ class TelemetryStreamAbstract {
94
141
  const { toAdd } = (0, getSubscriptionsDifference_1.getSubscriptionsDifference)(before, after);
95
142
  if (toAdd.callsigns.length && toAdd.sources.length) {
96
143
  this.logger.debug('New subscriptions added, we need to refresh sources', { toAdd });
97
- void this.enqueueTelemetryAction('subscribe', new models_1.CallsignsLookup(toAdd.callsigns), toAdd.sources);
144
+ void this.enqueueTelemetryAction('subscribe', new models_1.CallsignsLookup(toAdd.callsigns), toAdd.sources).catch((error) => {
145
+ this.logger.error(`Failed to subscribe: ${error}`);
146
+ });
98
147
  }
99
148
  }
100
149
  }
@@ -200,14 +249,22 @@ class TelemetryStreamAbstract {
200
249
  // Subscribe message
201
250
  case grpc_1.GRPC_SOURCE_SUBSCRIBED: {
202
251
  const { subscriberId } = rocosMessage.payload;
252
+ const queueState = telemetryActionQueueStates.get(this);
253
+ if (queueState?.stopped)
254
+ return;
203
255
  if (this.subscriberId && subscriberId) {
204
256
  this.logger.warn(`Replacing old subscriberId (${this.subscriberId}) with a new one (${subscriberId})`);
205
257
  }
206
- // Update the subscriberId
258
+ if (queueState) {
259
+ queueState.registration++;
260
+ resolveUnsentTelemetryActions(queueState);
261
+ }
207
262
  this.subscriberId = subscriberId;
208
263
  rocosMessage.subscriberId = this.subscriberId;
209
264
  this.logger.info('onData', `subscriberId has been updated - ${subscriberId}`, { subscriberId });
210
- void this.enqueueTelemetryAction('subscribe');
265
+ void this.enqueueTelemetryAction('subscribe').catch((error) => {
266
+ this.logger.error(`Failed to subscribe: ${error}`);
267
+ });
211
268
  this.heartbeat.start(() => this.sendHeartbeat(), this.sendHeartbeatTime);
212
269
  isRobotMessage = false;
213
270
  break;
@@ -249,21 +306,69 @@ class TelemetryStreamAbstract {
249
306
  * unsubscribe landing last strands a subscribe and nothing re-adds it.
250
307
  */
251
308
  enqueueTelemetryAction(actionType, callsignsLookup, sources, terminate) {
252
- const takeAction = () => this.takeTelemetryAction(actionType, callsignsLookup, sources, terminate);
253
- const run = this.telemetryActionQueue.then(takeAction);
254
- // The tail only signals whose turn it is, so it must never reject, otherwise one failed op
255
- // would wedge every op queued behind it. Callers still see the failure through `run`.
256
- this.telemetryActionQueue = run.then(() => undefined, () => undefined);
257
- return run;
309
+ const queueState = telemetryActionQueueStates.get(this);
310
+ if (!queueState || queueState.stopped)
311
+ return Promise.resolve();
312
+ const completion = new Promise((resolve, reject) => {
313
+ queueState.pendingActions.push({
314
+ actionType,
315
+ callsignsLookup: callsignsLookup ?? this.callsignsLookup,
316
+ sources: [...(sources ?? this.sources)],
317
+ terminate: terminate ?? false,
318
+ subscriberId: this.subscriberId,
319
+ registration: queueState.registration,
320
+ resolve,
321
+ reject,
322
+ });
323
+ });
324
+ const scheduleDrain = () => {
325
+ if (queueState.stopped ||
326
+ queueState.requestInFlight ||
327
+ queueState.drainScheduled ||
328
+ queueState.pendingActions.length === 0) {
329
+ return;
330
+ }
331
+ queueState.drainScheduled = true;
332
+ this.telemetryActionQueue = this.telemetryActionQueue.then(async () => {
333
+ queueState.drainScheduled = false;
334
+ if (queueState.stopped || queueState.requestInFlight)
335
+ return;
336
+ const activeBatch = takeNextTelemetryBatch(queueState);
337
+ const [firstAction] = activeBatch;
338
+ if (!firstAction)
339
+ return;
340
+ queueState.activeBatch = activeBatch;
341
+ queueState.requestInFlight = true;
342
+ try {
343
+ await this.takeTelemetryAction(firstAction.actionType, firstAction.callsignsLookup, firstAction.sources);
344
+ resolveTelemetryActions(activeBatch);
345
+ }
346
+ catch (error) {
347
+ activeBatch.forEach((action) => action.reject(error));
348
+ }
349
+ finally {
350
+ queueState.activeBatch = [];
351
+ queueState.requestInFlight = false;
352
+ scheduleDrain();
353
+ }
354
+ });
355
+ };
356
+ scheduleDrain();
357
+ return completion;
258
358
  }
259
- async takeTelemetryAction(actionType, callsignsLookup, sources, terminate) {
359
+ async takeTelemetryAction(actionType, callsignsLookup, sources) {
260
360
  this.logger.debug('takeTelemetryAction', actionType);
361
+ const queueState = telemetryActionQueueStates.get(this);
362
+ const activeBatch = queueState?.activeBatch ?? [];
363
+ const [firstAction] = activeBatch;
261
364
  // A config fault, not a race: this stream can never address a request.
262
365
  if (!this.projectId) {
263
366
  this.logger.warn('takeTelemetryAction', actionType, `${actionType} without a projectId - dropped`);
264
367
  return Promise.resolve();
265
368
  }
266
- if (!this.subscriberId) {
369
+ if (!firstAction?.subscriberId ||
370
+ firstAction.subscriberId !== this.subscriberId ||
371
+ firstAction.registration !== queueState?.registration) {
267
372
  // Deferred, not lost: `onData` re-asserts `this.sources` when the subscriberId arrives.
268
373
  this.logger.debug('takeTelemetryAction', actionType, `${actionType} before the receiver was registered - deferred to the post-registration re-assert`);
269
374
  return Promise.resolve();
@@ -271,20 +376,20 @@ class TelemetryStreamAbstract {
271
376
  const actionSources = sources ?? this.sources;
272
377
  const actionCallsigns = callsignsLookup ?? this.callsignsLookup;
273
378
  if (!actionCallsigns.isQuery()) {
274
- // Compose the request message
275
379
  const req = teletubby_pb_1.TelemetryRequest.create({
276
- subscriberId: this.subscriberId,
277
- requestedActions: [
278
- {
279
- operation: actionType,
280
- callsigns: actionCallsigns.getCallsigns(),
281
- sources: actionSources,
282
- },
283
- ],
380
+ subscriberId: firstAction.subscriberId,
381
+ requestedActions: activeBatch.map((action) => {
382
+ const telemetryAction = teletubby_pb_1.TelemetryAction.create({
383
+ operation: action.actionType,
384
+ callsigns: action.callsignsLookup.getCallsigns(),
385
+ sources: action.sources,
386
+ });
387
+ if (action.actionType === 'unsubscribe' && action.terminate) {
388
+ this.addTerminateToAction(telemetryAction);
389
+ }
390
+ return telemetryAction;
391
+ }),
284
392
  });
285
- if (actionType === 'unsubscribe' && terminate) {
286
- this.addTerminateToAction(req.requestedActions[0]);
287
- }
288
393
  this.logger.debug('takeTelemetryAction', actionType, 'client.requestTelemetry', this.subscriberId);
289
394
  // Send the message to back-end.
290
395
  return this.requestTelemetry(req);
@@ -309,7 +414,7 @@ class TelemetryStreamAbstract {
309
414
  });
310
415
  // Compose the request message
311
416
  const req = teletubby_pb_1.TelemetryQueryRequest.create({
312
- subscriberId: this.subscriberId,
417
+ subscriberId: firstAction.subscriberId,
313
418
  sources: actionSources,
314
419
  operation: actionType,
315
420
  callsignQuery: {
@@ -40,6 +40,7 @@ class TelemetryStreamConnect extends TelemetryStreamAbstract_1.TelemetryStreamAb
40
40
  this.client = (0, connect_1.createClient)(teletubby_pb_1.TelemetryGateway, transport);
41
41
  }
42
42
  stopInternal() {
43
+ this.subscriberId = undefined;
43
44
  if (this.abortController) {
44
45
  this.logger.info('Starting stop stream process');
45
46
  this.abortController.abort();
@@ -48,6 +48,7 @@ class TelemetryStreamNode extends TelemetryStreamAbstract_1.TelemetryStreamAbstr
48
48
  this.credentials = config.insecure ? grpc.credentials.createInsecure() : grpc.credentials.createSsl();
49
49
  }
50
50
  stopInternal() {
51
+ this.subscriberId = undefined;
51
52
  try {
52
53
  this.logger.info('Starting stop stream process');
53
54
  this.logger.info('Stopping stream');
@@ -28,7 +28,8 @@ class WebRTCSignallingStreamAbstract {
28
28
  this.token = token;
29
29
  }
30
30
  stopStream() {
31
- // do nothing for now
31
+ this.statusStream$.next(models_1.SubscriberStatusEnum.STOPPED);
32
+ this.statusStream$.complete();
32
33
  }
33
34
  /**
34
35
  * Connect operator
@@ -8,6 +8,6 @@ export interface Map {
8
8
  deployedCount: number;
9
9
  isUploading: boolean;
10
10
  isInCloud: boolean;
11
- hash: string;
11
+ hash?: string;
12
12
  lastModifiedTimestamp: number;
13
13
  }
@@ -41,3 +41,26 @@ export interface RobotStatus {
41
41
  connectivity: RobotConnectivityStatus;
42
42
  colour: RobotStatusColour;
43
43
  }
44
+ /**
45
+ * One contributor to a reported-status axis, from the agent's
46
+ * `robot/status/contributors` topic. `status` is the contributor's own verdict
47
+ * for that axis — the integer wire value mapped onto the enum.
48
+ */
49
+ export interface RobotStatusContributor<TStatus = RobotHealth | RobotReadiness> {
50
+ /** Identifier of the contributor, e.g. `battery`. */
51
+ id: string;
52
+ /** The agent component that raised it, e.g. `dji`. */
53
+ component: string;
54
+ /** Human-readable explanation, e.g. `battery below 20%`; empty when none was given. */
55
+ reason: string;
56
+ /** The contributor's verdict for its axis. */
57
+ status: TStatus;
58
+ }
59
+ /**
60
+ * The per-contributor breakdown behind a robot's reported health and readiness,
61
+ * from the agent's `robot/status/contributors` topic.
62
+ */
63
+ export interface RobotStatusContributors {
64
+ health: RobotStatusContributor<RobotHealth>[];
65
+ readiness: RobotStatusContributor<RobotReadiness>[];
66
+ }
@@ -30,13 +30,22 @@ export declare enum RobotReadiness {
30
30
  BUSY = "busy"
31
31
  }
32
32
  /**
33
- * Whether the robot is connected to a single cloud service, as reported by that
34
- * service's `connections` endpoint (or, for `telemetry-liveness`, derived from
35
- * heartbeat freshness).
33
+ * Whether the robot is connected to a single cloud service — reported by that
34
+ * service's `connections` endpoint, derived from heartbeat freshness (for
35
+ * `telemetry-liveness`), or supplied by a caller-injected signal
36
+ * (`RobotStatusOptions.extraServices`), the only source that can report {@link FAILED}.
36
37
  */
37
38
  export declare enum ServiceConnection {
38
39
  CONNECTED = "connected",
39
40
  DISCONNECTED = "disconnected",
41
+ /**
42
+ * The service reports an explicit fault — a stronger signal than {@link DISCONNECTED} (which just
43
+ * means "not connected", as expected of a robot that is simply off). A failed virtual-robot
44
+ * deployment is the motivating case. Any `FAILED` service forces the aggregate to
45
+ * {@link RobotConnectivity.DEGRADED}, so a fault raises attention even when nothing else is
46
+ * connected — see `aggregateConnectivity`.
47
+ */
48
+ FAILED = "failed",
40
49
  /** The service's connectivity has not yet been determined (e.g. not polled). */
41
50
  UNKNOWN = "unknown"
42
51
  }
@@ -45,10 +54,13 @@ export declare enum ServiceConnection {
45
54
  * expected services.
46
55
  */
47
56
  export declare enum RobotConnectivity {
48
- /** At least one expected service is connected and none is disconnected. */
57
+ /** At least one expected service is connected and none is disconnected or failed. */
49
58
  ONLINE = "online",
50
- /** Some, but not all, expected services report connected. */
59
+ /**
60
+ * Any service reports a fault ({@link ServiceConnection.FAILED}), or a genuine mix — some
61
+ * connected, some disconnected. A `FAILED` service forces this even when nothing is connected.
62
+ */
51
63
  DEGRADED = "degraded",
52
- /** No expected service reports connected. */
64
+ /** Nothing connected, and nothing failed. */
53
65
  OFFLINE = "offline"
54
66
  }
@@ -35,14 +35,23 @@ var RobotReadiness;
35
35
  RobotReadiness["BUSY"] = "busy";
36
36
  })(RobotReadiness || (exports.RobotReadiness = RobotReadiness = {}));
37
37
  /**
38
- * Whether the robot is connected to a single cloud service, as reported by that
39
- * service's `connections` endpoint (or, for `telemetry-liveness`, derived from
40
- * heartbeat freshness).
38
+ * Whether the robot is connected to a single cloud service — reported by that
39
+ * service's `connections` endpoint, derived from heartbeat freshness (for
40
+ * `telemetry-liveness`), or supplied by a caller-injected signal
41
+ * (`RobotStatusOptions.extraServices`), the only source that can report {@link FAILED}.
41
42
  */
42
43
  var ServiceConnection;
43
44
  (function (ServiceConnection) {
44
45
  ServiceConnection["CONNECTED"] = "connected";
45
46
  ServiceConnection["DISCONNECTED"] = "disconnected";
47
+ /**
48
+ * The service reports an explicit fault — a stronger signal than {@link DISCONNECTED} (which just
49
+ * means "not connected", as expected of a robot that is simply off). A failed virtual-robot
50
+ * deployment is the motivating case. Any `FAILED` service forces the aggregate to
51
+ * {@link RobotConnectivity.DEGRADED}, so a fault raises attention even when nothing else is
52
+ * connected — see `aggregateConnectivity`.
53
+ */
54
+ ServiceConnection["FAILED"] = "failed";
46
55
  /** The service's connectivity has not yet been determined (e.g. not polled). */
47
56
  ServiceConnection["UNKNOWN"] = "unknown";
48
57
  })(ServiceConnection || (exports.ServiceConnection = ServiceConnection = {}));
@@ -52,10 +61,13 @@ var ServiceConnection;
52
61
  */
53
62
  var RobotConnectivity;
54
63
  (function (RobotConnectivity) {
55
- /** At least one expected service is connected and none is disconnected. */
64
+ /** At least one expected service is connected and none is disconnected or failed. */
56
65
  RobotConnectivity["ONLINE"] = "online";
57
- /** Some, but not all, expected services report connected. */
66
+ /**
67
+ * Any service reports a fault ({@link ServiceConnection.FAILED}), or a genuine mix — some
68
+ * connected, some disconnected. A `FAILED` service forces this even when nothing is connected.
69
+ */
58
70
  RobotConnectivity["DEGRADED"] = "degraded";
59
- /** No expected service reports connected. */
71
+ /** Nothing connected, and nothing failed. */
60
72
  RobotConnectivity["OFFLINE"] = "offline";
61
73
  })(RobotConnectivity || (exports.RobotConnectivity = RobotConnectivity = {}));
@@ -1,4 +1,4 @@
1
- import { IBaseService, IRocosSDKConfig, RobotConnectivityStatus, RobotStatus, RocosError } from '../models';
1
+ import { IBaseService, IRocosSDKConfig, RobotConnectivityStatus, RobotStatus, RobotStatusContributors, RocosError, ServiceConnection } from '../models';
2
2
  import { Observable } from 'rxjs';
3
3
  import { ReportedRobotStatus } from './robot-status/reportedStatus';
4
4
  import { BaseServiceAbstract } from './BaseServiceAbstract';
@@ -10,6 +10,23 @@ export interface RobotStatusOptions {
10
10
  heartbeatTimeoutMs?: number;
11
11
  /** How often the connections endpoints are polled (ms, floored internally). */
12
12
  connectivityPollMs?: number;
13
+ /**
14
+ * Extra per-robot connectivity signals, keyed by service id, folded into
15
+ * {@link RobotConnectivityStatus.services} and the aggregate on equal footing with the built-in
16
+ * services. Lets a caller inject a signal the SDK can't derive itself (e.g. virtual-robot
17
+ * deployment status); emit {@link ServiceConnection.FAILED} to surface a fault as `DEGRADED`.
18
+ *
19
+ * A key that collides with a built-in service id (`telemetry-liveness`, `robot-configs`,
20
+ * `robot-services`) is ignored in favour of the built-in.
21
+ *
22
+ * An error from an injected observable is contained as {@link ServiceConnection.UNKNOWN} so it
23
+ * can't tear down the overall status stream — but it is *terminal* for that signal: unlike the
24
+ * built-in HTTP poller (which recovers on its next tick), the errored observable is not
25
+ * resubscribed, so the service stays `UNKNOWN` for the life of the stream (and `getConnectivity`
26
+ * then waits out its timeout). To report a recoverable fault, emit `FAILED` / `UNKNOWN` as a
27
+ * value rather than letting the observable error.
28
+ */
29
+ extraServices?: Record<string, Observable<ServiceConnection>>;
13
30
  }
14
31
  /**
15
32
  * The single source of truth for a robot's status. It composes the reported
@@ -22,6 +39,8 @@ export declare class RobotStatusService extends BaseServiceAbstract implements I
22
39
  getStatus(): boolean;
23
40
  /** Health + readiness as reported by the robot (new topic, legacy heartbeat fallback). */
24
41
  getReportedStatusChanges(projectId: string, callsign: string): Observable<ReportedRobotStatus>;
42
+ /** Per-contributor breakdown behind the reported health and readiness, with the reason for each. */
43
+ getStatusContributorsChanges(projectId: string, callsign: string): Observable<RobotStatusContributors>;
25
44
  /** Per-service connectivity plus the aggregate verdict, polled. */
26
45
  getConnectivityChanges(projectId: string, callsign: string, opts?: RobotStatusOptions): Observable<RobotConnectivityStatus>;
27
46
  /** One-shot connectivity snapshot: resolves once every expected service is known, or after a timeout. */
@@ -42,6 +42,10 @@ class RobotStatusService extends BaseServiceAbstract_1.BaseServiceAbstract {
42
42
  getReportedStatusChanges(projectId, callsign) {
43
43
  return (0, reportedStatus_1.getReportedStatusChanges)(this.telemetryService, projectId, callsign);
44
44
  }
45
+ /** Per-contributor breakdown behind the reported health and readiness, with the reason for each. */
46
+ getStatusContributorsChanges(projectId, callsign) {
47
+ return (0, reportedStatus_1.getStatusContributorsChanges)(this.telemetryService, projectId, callsign);
48
+ }
45
49
  /** Per-service connectivity plus the aggregate verdict, polled. */
46
50
  getConnectivityChanges(projectId, callsign, opts) {
47
51
  return (0, connectivity_1.getConnectivityChanges)(this.connectivityDeps(projectId, callsign, opts), projectId, callsign, opts?.expectedServices, opts?.connectivityPollMs);
@@ -88,6 +92,7 @@ class RobotStatusService extends BaseServiceAbstract_1.BaseServiceAbstract {
88
92
  baseUrl: this.config.url,
89
93
  insecure: this.config.insecure,
90
94
  telemetryLiveness$: (0, reportedStatus_1.getTelemetryLiveness)(this.telemetryService, projectId, callsign, opts?.heartbeatTimeoutMs),
95
+ extraServices: opts?.extraServices,
91
96
  };
92
97
  }
93
98
  }
@@ -1,7 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.SearchService = void 0;
4
- const models_1 = require("../models");
5
4
  const rxjs_1 = require("rxjs");
6
5
  const BaseStreamService_1 = require("./BaseStreamService");
7
6
  const identifier_1 = require("../constants/identifier");
@@ -47,12 +46,8 @@ class SearchService extends BaseStreamService_1.BaseStreamService {
47
46
  port: this.config.port,
48
47
  insecure: this.config.insecure,
49
48
  });
50
- if (newStream.isNew) {
51
- newStream.stream.statusStream$.subscribe((msg) => {
52
- this.status = msg === models_1.SubscriberStatusEnum.STOPPED || msg === models_1.SubscriberStatusEnum.ALIVE;
53
- this.status$.next(msg);
54
- });
55
- }
49
+ // The status forwarder is wired once per stream by BaseStreamService.acquireStream's factory
50
+ // (and released now that the stream completes statusStream$ on stop), so no per-service one here.
56
51
  return newStream.stream;
57
52
  }
58
53
  getStream(config) {
@@ -3,7 +3,6 @@ import { CallsignStatus, IRocosSDKConfig, IRocosTelemetryMessage, IStreamStatusM
3
3
  import { BaseStreamService } from './BaseStreamService';
4
4
  export declare class TelemetryService extends BaseStreamService<ITelemetryStream, ITelemetryStreamConfig> {
5
5
  private static grpcWebDeprecationWarned;
6
- private statusSubscription;
7
6
  private changeStream$;
8
7
  private streamStatusSubject$;
9
8
  constructor(config: IRocosSDKConfig);
@@ -29,6 +28,10 @@ export declare class TelemetryService extends BaseStreamService<ITelemetryStream
29
28
  getStatusChanges(): Observable<IStreamStatusMessage>;
30
29
  /**
31
30
  * Subscribes to the heartbeat telemetry and checks we receive one at least every 5s
31
+ *
32
+ * @deprecated Superseded by {@link RobotStatusService.getRobotStatusChanges}, which reports the
33
+ * normalised robot status (reported health/readiness plus platform connectivity) rather than only
34
+ * heartbeat-derived online/offline liveness.
32
35
  * @param projectId
33
36
  * @param callsign
34
37
  * @param heartbeatTimeoutMs optionally override the deadline for heartbeats (in ms). Default 5000ms
@@ -67,7 +67,9 @@ class TelemetryService extends BaseStreamService_1.BaseStreamService {
67
67
  const { stream, isNew, ready, release } = this.acquireStream(identifier_1.IDENTIFIER_NAME_TELEMETRY, this.buildStreamConfig(params.projectId, callsignsLookup, sources, scope));
68
68
  try {
69
69
  stream.addSubscription(subscriptionParams);
70
- if (!isNew) {
70
+ // Forward the stream's status onto the per-scope subject once, at creation; it ends when the
71
+ // stream stops (statusStream$ completes), so it is not re-created or leaked across subscribers.
72
+ if (isNew) {
71
73
  stream.statusStream$.subscribe((msg) => {
72
74
  this.streamStatusSubject$.next({
73
75
  scope,
@@ -81,20 +83,19 @@ class TelemetryService extends BaseStreamService_1.BaseStreamService {
81
83
  release();
82
84
  throw e;
83
85
  }
84
- const messages$ = ready.then(() => {
85
- if (!this.statusSubscription) {
86
- this.statusSubscription = stream.statusStream$.subscribe((msg) => {
87
- this.status = msg === models_1.SubscriberStatusEnum.STOPPED || msg === models_1.SubscriberStatusEnum.ALIVE;
88
- this.status$.next(msg);
89
- });
90
- }
91
- return (0, rxjs_1.from)(stream.messageStream$);
92
- });
86
+ // status$ / getStatus() are driven by the acquireStream factory (BaseStreamService), not here.
87
+ const messages$ = ready.then(() => (0, rxjs_1.from)(stream.messageStream$));
93
88
  return (0, rxjs_1.from)(messages$).pipe((0, rxjs_1.mergeAll)(), (0, operators_1.filter)((message) => {
94
89
  return callsignsLookup.includesCallsign(message.callsign) && sources.includes(message.source);
95
90
  }), (0, operators_1.finalize)(() => {
96
- void stream.removeSubscription(subscriptionParams, params.terminateReceiverGroupOnUnsubscribe);
97
- release();
91
+ if (params.terminateReceiverGroupOnUnsubscribe) {
92
+ const removal = stream.removeSubscription(subscriptionParams, true);
93
+ void Promise.resolve(removal).finally(release);
94
+ }
95
+ else {
96
+ release();
97
+ void stream.removeSubscription(subscriptionParams);
98
+ }
98
99
  }));
99
100
  });
100
101
  }
@@ -116,6 +117,10 @@ class TelemetryService extends BaseStreamService_1.BaseStreamService {
116
117
  }
117
118
  /**
118
119
  * Subscribes to the heartbeat telemetry and checks we receive one at least every 5s
120
+ *
121
+ * @deprecated Superseded by {@link RobotStatusService.getRobotStatusChanges}, which reports the
122
+ * normalised robot status (reported health/readiness plus platform connectivity) rather than only
123
+ * heartbeat-derived online/offline liveness.
119
124
  * @param projectId
120
125
  * @param callsign
121
126
  * @param heartbeatTimeoutMs optionally override the deadline for heartbeats (in ms). Default 5000ms