@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
@@ -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 = {}));
@@ -99,7 +99,12 @@ class AssetStorageService extends BaseServiceAbstract_1.BaseServiceAbstract {
99
99
  * @param signedURL
100
100
  */
101
101
  async listFlowAssets(projectId, flowId, flowInstance, parent = '/', signedURL = false) {
102
- return this.callGet((0, formatServiceUrl_1.formatServiceUrl)(api_1.API_PROJECT_FLOW_ASSET_PATH_URL, { url: this.config.url, projectId, flowId, flowInstance }, this.config.insecure), `Failed to get asset for ${projectId}, flow ${flowId}, flow instance ${flowInstance}.`, { parent, signedURL });
102
+ return this.callGet((0, formatServiceUrl_1.formatServiceUrl)(api_1.API_PROJECT_FLOW_ASSET_PATH_URL, {
103
+ url: this.config.url,
104
+ projectId,
105
+ flowId: encodeURIComponent(flowId),
106
+ flowInstance: encodeURIComponent(flowInstance),
107
+ }, this.config.insecure), `Failed to get asset for ${projectId}, flow ${flowId}, flow instance ${flowInstance}.`, { parent, signedURL });
103
108
  }
104
109
  /**
105
110
  * List assets for a mission
@@ -44,7 +44,7 @@ class BaseServiceAbstract {
44
44
  requestConfig.body = JSON.stringify(payload);
45
45
  requestConfig.headers = { ...requestConfig.headers, 'Content-Type': 'application/json' };
46
46
  }
47
- return this.fetchOrThrow(formattedUrl, requestConfig, config?.responseType);
47
+ return await this.fetchOrThrow(formattedUrl, requestConfig, config?.responseType);
48
48
  }
49
49
  catch (e) {
50
50
  const message = errorMessage;
@@ -1,3 +1,4 @@
1
+ import { IAcquiredStream } from '../api/StreamRegister';
1
2
  import { IBaseService, IBaseStream, IRocosSDKConfig, IStreamConfig, SubscriberStatusEnum } from '../models';
2
3
  import { ISubscriberStatus } from '../models/ISubscriberStatus';
3
4
  import { Logger } from 'loglevel';
@@ -10,7 +11,17 @@ export declare abstract class BaseStreamService<T extends IBaseStream, C extends
10
11
  protected constructor(name: string, config: IRocosSDKConfig);
11
12
  getStatus(): boolean;
12
13
  protected initStream(stream: T): Promise<void>;
13
- protected createStreamFromConfig(identifier: string, config: C, onAcquire?: (stream: T) => void): Promise<{
14
+ /**
15
+ * Get-or-create the shared stream for this identifier/scope and take a refcount on it,
16
+ * synchronously. Holders that stop consuming must call the handle's release(); the registry
17
+ * tears the stream down when the last holder releases.
18
+ */
19
+ protected acquireStream(identifier: string, config: C): IAcquiredStream<T>;
20
+ /**
21
+ * Acquire-and-init without a paired release: callers of this path hold their refcount for the
22
+ * life of the process, so these streams are only torn down by removeAllStreams.
23
+ */
24
+ protected createStreamFromConfig(identifier: string, config: C): Promise<{
14
25
  stream: T;
15
26
  isNew: boolean;
16
27
  }>;
@@ -1,10 +1,10 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.BaseStreamService = void 0;
4
+ const StreamRegister_1 = require("../api/StreamRegister");
4
5
  const models_1 = require("../models");
5
6
  const RocosLogger_1 = require("../logger/RocosLogger");
6
7
  const RocosStore_1 = require("../store/RocosStore");
7
- const StreamRegister_1 = require("../api/StreamRegister");
8
8
  const rxjs_1 = require("rxjs");
9
9
  class BaseStreamService {
10
10
  constructor(name, config) {
@@ -32,40 +32,29 @@ class BaseStreamService {
32
32
  }
33
33
  authService.startTokenRefreshChecker();
34
34
  }
35
- async createStreamFromConfig(identifier, config, onAcquire) {
35
+ /**
36
+ * Get-or-create the shared stream for this identifier/scope and take a refcount on it,
37
+ * synchronously. Holders that stop consuming must call the handle's release(); the registry
38
+ * tears the stream down when the last holder releases.
39
+ */
40
+ acquireStream(identifier, config) {
36
41
  const identifierWithScope = StreamRegister_1.StreamRegister.getIdentifier(identifier, config.scope);
37
- const streamRegister = StreamRegister_1.StreamRegister.getInstance();
38
- let stream = streamRegister.getStream(identifierWithScope);
39
- let isNew = false;
40
- if (!stream) {
41
- isNew = true;
42
- stream = this.getStream(config);
42
+ return StreamRegister_1.StreamRegister.getInstance().acquireStream(identifierWithScope, () => {
43
+ const stream = this.getStream(config);
43
44
  stream.statusStream$.subscribe((msg) => {
44
45
  this.status = msg === models_1.SubscriberStatusEnum.STOPPED || msg === models_1.SubscriberStatusEnum.ALIVE;
45
46
  this.status$.next(msg);
46
47
  });
47
- streamRegister.addStream(stream);
48
- }
49
- // Acquire synchronously with the lookup/registration above, before any await. A stream is
50
- // stopped and deregistered the instant its refcount hits zero, so a deferred acquire can land on
51
- // a dead instance including a freshly-registered stream torn down during the init await below.
52
- onAcquire?.(stream);
53
- if (isNew) {
54
- try {
55
- await this.initStream(stream);
56
- }
57
- catch (e) {
58
- // Evict eagerly, before the error reaches the subscriber, so a synchronous resubscribe
59
- // can't re-acquire this uninitialised instance.
60
- streamRegister.removeStream(stream);
61
- throw e;
62
- }
63
- // If a concurrent teardown deregistered this stream during init, init() has opened a receiver
64
- // on an instance nothing can reach; close it.
65
- if (streamRegister.getStream(identifierWithScope) !== stream) {
66
- stream.stopStream();
67
- }
68
- }
48
+ return stream;
49
+ }, (stream) => this.initStream(stream));
50
+ }
51
+ /**
52
+ * Acquire-and-init without a paired release: callers of this path hold their refcount for the
53
+ * life of the process, so these streams are only torn down by removeAllStreams.
54
+ */
55
+ async createStreamFromConfig(identifier, config) {
56
+ const { stream, isNew, ready } = this.acquireStream(identifier, config);
57
+ await ready;
69
58
  return { stream, isNew };
70
59
  }
71
60
  }
@@ -21,6 +21,10 @@ export declare class EnvironmentService extends BaseServiceAbstract implements I
21
21
  * CAUTION: This will delete the existing environment and create
22
22
  * a new one. Think of this as a "reset" for the environment.
23
23
  * This will cause ALL edges to other objects (assets, panos, etc) to be removed.
24
+ *
25
+ * @deprecated Prefer {@link update}. The environment has no create endpoint -
26
+ * `PUT /environments` is the only full-replace write, so this method now
27
+ * issues the same request as `update`.
24
28
  */
25
29
  set(projectId: string, scene: unknown): Promise<void>;
26
30
  /**
@@ -36,9 +36,13 @@ class EnvironmentService extends BaseServiceAbstract_1.BaseServiceAbstract {
36
36
  * CAUTION: This will delete the existing environment and create
37
37
  * a new one. Think of this as a "reset" for the environment.
38
38
  * This will cause ALL edges to other objects (assets, panos, etc) to be removed.
39
+ *
40
+ * @deprecated Prefer {@link update}. The environment has no create endpoint -
41
+ * `PUT /environments` is the only full-replace write, so this method now
42
+ * issues the same request as `update`.
39
43
  */
40
44
  async set(projectId, scene) {
41
- return this.callPost((0, formatServiceUrl_1.formatServiceUrl)(api_1.API_PROJECT_ENVIRONMENT_URL, { url: this.config.url, projectId }, this.config.insecure), scene, `Failed to set environment for ${projectId}.`);
45
+ return this.callPut((0, formatServiceUrl_1.formatServiceUrl)(api_1.API_PROJECT_ENVIRONMENT_URL, { url: this.config.url, projectId }, this.config.insecure), scene, `Failed to set environment for ${projectId}.`);
42
46
  }
43
47
  /**
44
48
  * Update Environment
@@ -36,10 +36,22 @@ export declare class MapService extends BaseServiceAbstract implements IBaseServ
36
36
  */
37
37
  listDeployedMaps(projectId: string, callsign: string): Promise<Map[]>;
38
38
  /**
39
- * Creates a new map by merging two existing maps
39
+ * Creates a new map by folding two existing maps together.
40
+ *
41
+ * `options.name` is the mapId of the new target map and must not already
42
+ * exist. `options.callsign` is accepted for signature compatibility and is
43
+ * not sent: the union target is a cloud map, and deploying it to a robot is
44
+ * {@link deploy}'s job.
45
+ *
46
+ * `options.description` needs a second request - the union endpoint takes no
47
+ * metadata. That request is best-effort: a failure is logged and swallowed,
48
+ * because the map itself already exists by then and retrying `merge` would
49
+ * conflict on `targetMapId`. Call {@link updateMetadata} directly to retry
50
+ * the description.
51
+ *
40
52
  * @param projectId Project ID
41
- * @param maps Array of two map IDs to merge
42
- * @param options Options to create the new map
53
+ * @param maps Array of two source map IDs to fold together
54
+ * @param options `name` for the new map, optional `description`
43
55
  */
44
56
  merge(projectId: string, maps: [string, string], options: {
45
57
  name: string;
@@ -55,26 +55,38 @@ class MapService extends BaseServiceAbstract_1.BaseServiceAbstract {
55
55
  return resp.maps;
56
56
  }
57
57
  /**
58
- * Creates a new map by merging two existing maps
58
+ * Creates a new map by folding two existing maps together.
59
+ *
60
+ * `options.name` is the mapId of the new target map and must not already
61
+ * exist. `options.callsign` is accepted for signature compatibility and is
62
+ * not sent: the union target is a cloud map, and deploying it to a robot is
63
+ * {@link deploy}'s job.
64
+ *
65
+ * `options.description` needs a second request - the union endpoint takes no
66
+ * metadata. That request is best-effort: a failure is logged and swallowed,
67
+ * because the map itself already exists by then and retrying `merge` would
68
+ * conflict on `targetMapId`. Call {@link updateMetadata} directly to retry
69
+ * the description.
70
+ *
59
71
  * @param projectId Project ID
60
- * @param maps Array of two map IDs to merge
61
- * @param options Options to create the new map
72
+ * @param maps Array of two source map IDs to fold together
73
+ * @param options `name` for the new map, optional `description`
62
74
  */
63
75
  async merge(projectId, maps, options) {
64
76
  if (maps.length !== 2)
65
77
  throw new Error('You must provide two maps to merge.');
66
- const payload = {
67
- projectID: projectId,
68
- mapIDLeft: maps[0],
69
- mapIDRight: maps[1],
70
- name: options.name,
71
- description: options.description,
72
- callsign: options.callsign,
73
- };
74
- return this.callPost((0, formatServiceUrl_1.formatServiceUrl)(api_1.API_GRAPHS_MAPS_MERGE_URL, {
78
+ await this.callPost((0, formatServiceUrl_1.formatServiceUrl)(api_1.API_GRAPHS_MAPS_UNION_URL, {
75
79
  url: this.config.url,
76
80
  projectId,
77
- }, this.config.insecure), payload, 'Failed to merge maps.');
81
+ }, this.config.insecure), { targetMapId: options.name, sources: [maps[0], maps[1]] }, 'Failed to merge maps.');
82
+ if (options.description) {
83
+ try {
84
+ await this.updateMetadata(projectId, options.name, { description: options.description });
85
+ }
86
+ catch (e) {
87
+ this.logger.error(`Map ${options.name} was created but its description could not be set.`, e);
88
+ }
89
+ }
78
90
  }
79
91
  /** Returns a map in its canonical form
80
92
  *
@@ -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
  }
@@ -39,7 +39,7 @@ class SearchService extends BaseStreamService_1.BaseStreamService {
39
39
  return stream.deleteProjectData(params);
40
40
  }
41
41
  async createStream() {
42
- const newStream = await this.createStreamFromConfig(identifier_1.IDENTIFIER_NAME_FILE_ACCESSOR, {
42
+ const newStream = await this.createStreamFromConfig(identifier_1.IDENTIFIER_NAME_SEARCH, {
43
43
  url: this.config.url,
44
44
  token: this.config.token,
45
45
  scope: 'default',
@@ -56,10 +56,10 @@ class TargetService extends BaseServiceAbstract_1.BaseServiceAbstract {
56
56
  });
57
57
  }
58
58
  async addMedia(projectId, targetId, media) {
59
- await this.callPost((0, formatServiceUrl_1.formatServiceUrl)(api_1.API_GRAPHS_TARGETS_URL, {
59
+ await this.callPut((0, formatServiceUrl_1.formatServiceUrl)(api_1.API_GRAPHS_TARGET_MEDIA_URL, {
60
60
  url: this.config.url,
61
61
  projectId,
62
- targetId,
62
+ targetId: encodeURIComponent(targetId),
63
63
  }, this.config.insecure), media, 'Failed to add media to a target');
64
64
  }
65
65
  }
@@ -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);
@@ -46,6 +45,6 @@ export declare class TelemetryService extends BaseStreamService<ITelemetryStream
46
45
  monitorTelemetryWithTimeout(projectId: string, callsign: string, source: string, heartbeatTimeoutMs?: number, intervalMs?: number): Observable<TelemetryMonitorStatus>;
47
46
  protected initStream(stream: ITelemetryStream): Promise<void>;
48
47
  private buildScope;
49
- private createStream;
48
+ private buildStreamConfig;
50
49
  protected getStream(config: ITelemetryStreamConfig): ITelemetryStream;
51
50
  }
@@ -55,31 +55,42 @@ class TelemetryService extends BaseStreamService_1.BaseStreamService {
55
55
  // scope is used as a key to generate unique streams
56
56
  // ideally there should be just one concurrent stream opened
57
57
  const scope = this.buildScope(params, callsignsLookup);
58
- const callsigns = callsignsLookup.lookupValue;
59
58
  const subscriptionParams = {
60
59
  uniqueId: (0, getUniqueId_1.getUniqueId)(params.projectId, callsignsLookup, sources, scope),
61
- callsigns: callsignsLookup?.lookupType === models_1.CallsignsLookupType.List ? callsignsLookup.lookupValue : [],
60
+ callsigns: callsignsLookup.getCallsigns(),
62
61
  sources,
63
62
  };
64
63
  // `defer` acquires the stream only when subscribed; `finalize` releases it on unsubscribe, error
65
- // and complete — so acquire and release stay balanced on every path.
64
+ // and complete — so acquire and release stay balanced on every path. The registry refcount taken
65
+ // here keeps the stream alive until the finalize, so everything in between is race-free.
66
66
  return (0, rxjs_1.defer)(() => {
67
- let acquired;
68
- const stream$ = this.createStream(params.projectId, callsignsLookup, sources, scope, (stream) => {
69
- acquired = stream;
67
+ const { stream, isNew, ready, release } = this.acquireStream(identifier_1.IDENTIFIER_NAME_TELEMETRY, this.buildStreamConfig(params.projectId, callsignsLookup, sources, scope));
68
+ try {
70
69
  stream.addSubscription(subscriptionParams);
71
- }).then((stream) => {
72
- if (!this.statusSubscription) {
73
- this.statusSubscription = stream.statusStream$.subscribe((msg) => {
74
- this.status = msg === models_1.SubscriberStatusEnum.STOPPED || msg === models_1.SubscriberStatusEnum.ALIVE;
75
- this.status$.next(msg);
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) {
73
+ stream.statusStream$.subscribe((msg) => {
74
+ this.streamStatusSubject$.next({
75
+ scope,
76
+ status: msg,
77
+ });
76
78
  });
77
79
  }
78
- return (0, rxjs_1.from)(stream.messageStream$);
79
- });
80
- return (0, rxjs_1.from)(stream$).pipe((0, rxjs_1.mergeAll)(), (0, operators_1.filter)((message) => {
81
- return callsigns.includes(message.callsign) && sources.includes(message.source);
82
- }), (0, operators_1.finalize)(() => acquired?.removeSubscription(subscriptionParams, params.terminateReceiverGroupOnUnsubscribe)));
80
+ }
81
+ catch (e) {
82
+ // finalize below is not wired up yet, so a throw here must hand the refcount back itself.
83
+ release();
84
+ throw e;
85
+ }
86
+ // status$ / getStatus() are driven by the acquireStream factory (BaseStreamService), not here.
87
+ const messages$ = ready.then(() => (0, rxjs_1.from)(stream.messageStream$));
88
+ return (0, rxjs_1.from)(messages$).pipe((0, rxjs_1.mergeAll)(), (0, operators_1.filter)((message) => {
89
+ return callsignsLookup.includesCallsign(message.callsign) && sources.includes(message.source);
90
+ }), (0, operators_1.finalize)(() => {
91
+ void stream.removeSubscription(subscriptionParams, params.terminateReceiverGroupOnUnsubscribe);
92
+ release();
93
+ }));
83
94
  });
84
95
  }
85
96
  /**
@@ -159,8 +170,8 @@ class TelemetryService extends BaseStreamService_1.BaseStreamService {
159
170
  return `${params.projectId}-default-${(0, flattenCallsignsLookup_1.flattenCallsignsLookup)(callsignsLookup).join()}`;
160
171
  return `${params.projectId}-default`;
161
172
  }
162
- async createStream(projectId, callsignsLookup, sources, scope, onAcquire) {
163
- const newStream = await this.createStreamFromConfig(identifier_1.IDENTIFIER_NAME_TELEMETRY, {
173
+ buildStreamConfig(projectId, callsignsLookup, sources, scope) {
174
+ return {
164
175
  url: this.config.url,
165
176
  projectId,
166
177
  callsignsLookup,
@@ -172,16 +183,7 @@ class TelemetryService extends BaseStreamService_1.BaseStreamService {
172
183
  insecure: this.config.insecure,
173
184
  transport: this.config.transport,
174
185
  fetch: this.config.fetch,
175
- }, onAcquire);
176
- if (!newStream.isNew) {
177
- newStream.stream.statusStream$.subscribe((msg) => {
178
- this.streamStatusSubject$.next({
179
- scope,
180
- status: msg,
181
- });
182
- });
183
- }
184
- return newStream.stream;
186
+ };
185
187
  }
186
188
  getStream(config) {
187
189
  if (config.transport === 'grpc-web') {
@@ -37,11 +37,9 @@ class WebRTCSignallingService {
37
37
  }
38
38
  async createSignallingStream() {
39
39
  const scope = 'default';
40
- const identifier = StreamRegister_1.StreamRegister.getIdentifier(identifier_1.IDENTIFIER_NAME_COMMAND, scope);
41
- const streamRegister = StreamRegister_1.StreamRegister.getInstance();
42
- let stream = streamRegister.getStream(identifier);
43
- if (!stream) {
44
- stream = this.getStream({
40
+ const identifier = StreamRegister_1.StreamRegister.getIdentifier(identifier_1.IDENTIFIER_NAME_WEBRTC_SIGNALLING, scope);
41
+ const { stream, ready } = StreamRegister_1.StreamRegister.getInstance().acquireStream(identifier, () => {
42
+ const newStream = this.getStream({
45
43
  url: this.config.url,
46
44
  token: this.config.token,
47
45
  scope,
@@ -49,13 +47,13 @@ class WebRTCSignallingService {
49
47
  port: this.config.port,
50
48
  insecure: this.config.insecure,
51
49
  });
52
- stream.statusStream$.subscribe((msg) => {
50
+ newStream.statusStream$.subscribe((msg) => {
53
51
  this.status = msg === SubscriberStatusEnum_1.SubscriberStatusEnum.STOPPED || msg === SubscriberStatusEnum_1.SubscriberStatusEnum.ALIVE;
54
52
  this.status$.next(msg);
55
53
  });
56
- streamRegister.addStream(stream);
57
- await this.initStream(stream);
58
- }
54
+ return newStream;
55
+ }, (newStream) => this.initStream(newStream));
56
+ await ready;
59
57
  return stream;
60
58
  }
61
59
  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>;