@dronedeploy/rocos-js-sdk 4.4.4 → 4.4.5

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 (54) 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 +83 -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/services/AssetStorageService.js +6 -1
  16. package/cjs/services/BaseServiceAbstract.js +1 -1
  17. package/cjs/services/BaseStreamService.d.ts +12 -1
  18. package/cjs/services/BaseStreamService.js +19 -30
  19. package/cjs/services/EnvironmentService.d.ts +4 -0
  20. package/cjs/services/EnvironmentService.js +5 -1
  21. package/cjs/services/MapService.d.ts +15 -3
  22. package/cjs/services/MapService.js +25 -13
  23. package/cjs/services/SearchService.js +1 -1
  24. package/cjs/services/TargetService.js +2 -2
  25. package/cjs/services/TelemetryService.d.ts +1 -1
  26. package/cjs/services/TelemetryService.js +29 -22
  27. package/cjs/services/WebRTCSignallingService.js +7 -9
  28. package/esm/api/StreamRegister.d.ts +18 -3
  29. package/esm/api/StreamRegister.js +71 -38
  30. package/esm/api/atoms/StreamHeartbeat.js +2 -2
  31. package/esm/api/streams/telemetry/TelemetryStreamAbstract.d.ts +8 -1
  32. package/esm/api/streams/telemetry/TelemetryStreamAbstract.js +84 -41
  33. package/esm/api/streams/webRTCSignalling/WebRTCSignallingStreamAbstract.js +2 -2
  34. package/esm/constants/api.d.ts +3 -2
  35. package/esm/constants/api.js +3 -2
  36. package/esm/constants/identifier.d.ts +1 -0
  37. package/esm/constants/identifier.js +1 -0
  38. package/esm/helpers/getUniqueId.js +2 -1
  39. package/esm/models/callsigns/CallsignsLookup.d.ts +4 -0
  40. package/esm/models/callsigns/CallsignsLookup.js +19 -0
  41. package/esm/services/AssetStorageService.js +6 -1
  42. package/esm/services/BaseServiceAbstract.js +1 -1
  43. package/esm/services/BaseStreamService.d.ts +12 -1
  44. package/esm/services/BaseStreamService.js +19 -30
  45. package/esm/services/EnvironmentService.d.ts +4 -0
  46. package/esm/services/EnvironmentService.js +5 -1
  47. package/esm/services/MapService.d.ts +15 -3
  48. package/esm/services/MapService.js +26 -14
  49. package/esm/services/SearchService.js +2 -2
  50. package/esm/services/TargetService.js +3 -3
  51. package/esm/services/TelemetryService.d.ts +1 -1
  52. package/esm/services/TelemetryService.js +30 -23
  53. package/esm/services/WebRTCSignallingService.js +8 -10
  54. package/package.json +1 -1
@@ -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
  *
@@ -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
  }
@@ -46,6 +46,6 @@ export declare class TelemetryService extends BaseStreamService<ITelemetryStream
46
46
  monitorTelemetryWithTimeout(projectId: string, callsign: string, source: string, heartbeatTimeoutMs?: number, intervalMs?: number): Observable<TelemetryMonitorStatus>;
47
47
  protected initStream(stream: ITelemetryStream): Promise<void>;
48
48
  private buildScope;
49
- private createStream;
49
+ private buildStreamConfig;
50
50
  protected getStream(config: ITelemetryStreamConfig): ITelemetryStream;
51
51
  }
@@ -55,20 +55,33 @@ 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) => {
70
+ if (!isNew) {
71
+ stream.statusStream$.subscribe((msg) => {
72
+ this.streamStatusSubject$.next({
73
+ scope,
74
+ status: msg,
75
+ });
76
+ });
77
+ }
78
+ }
79
+ catch (e) {
80
+ // finalize below is not wired up yet, so a throw here must hand the refcount back itself.
81
+ release();
82
+ throw e;
83
+ }
84
+ const messages$ = ready.then(() => {
72
85
  if (!this.statusSubscription) {
73
86
  this.statusSubscription = stream.statusStream$.subscribe((msg) => {
74
87
  this.status = msg === models_1.SubscriberStatusEnum.STOPPED || msg === models_1.SubscriberStatusEnum.ALIVE;
@@ -77,9 +90,12 @@ class TelemetryService extends BaseStreamService_1.BaseStreamService {
77
90
  }
78
91
  return (0, rxjs_1.from)(stream.messageStream$);
79
92
  });
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)));
93
+ return (0, rxjs_1.from)(messages$).pipe((0, rxjs_1.mergeAll)(), (0, operators_1.filter)((message) => {
94
+ return callsignsLookup.includesCallsign(message.callsign) && sources.includes(message.source);
95
+ }), (0, operators_1.finalize)(() => {
96
+ void stream.removeSubscription(subscriptionParams, params.terminateReceiverGroupOnUnsubscribe);
97
+ release();
98
+ }));
83
99
  });
84
100
  }
85
101
  /**
@@ -159,8 +175,8 @@ class TelemetryService extends BaseStreamService_1.BaseStreamService {
159
175
  return `${params.projectId}-default-${(0, flattenCallsignsLookup_1.flattenCallsignsLookup)(callsignsLookup).join()}`;
160
176
  return `${params.projectId}-default`;
161
177
  }
162
- async createStream(projectId, callsignsLookup, sources, scope, onAcquire) {
163
- const newStream = await this.createStreamFromConfig(identifier_1.IDENTIFIER_NAME_TELEMETRY, {
178
+ buildStreamConfig(projectId, callsignsLookup, sources, scope) {
179
+ return {
164
180
  url: this.config.url,
165
181
  projectId,
166
182
  callsignsLookup,
@@ -172,16 +188,7 @@ class TelemetryService extends BaseStreamService_1.BaseStreamService {
172
188
  insecure: this.config.insecure,
173
189
  transport: this.config.transport,
174
190
  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;
191
+ };
185
192
  }
186
193
  getStream(config) {
187
194
  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) {
@@ -1,13 +1,28 @@
1
1
  import { IBaseStream } from '../models';
2
+ export interface IAcquiredStream<T extends IBaseStream> {
3
+ stream: T;
4
+ isNew: boolean;
5
+ /** Settles when the stream's one-time init completes; rejects (and evicts) if init fails. */
6
+ ready: Promise<void>;
7
+ /** Releases this holder's refcount. One-shot: extra calls are ignored. */
8
+ release: () => void;
9
+ }
10
+ /**
11
+ * Sole owner of stream lifecycle. Streams are shared per identifier and refcounted: acquireStream
12
+ * gets-or-creates and increments in one synchronous step, and the returned handle's release()
13
+ * tears the stream down when the last holder leaves. Teardown never happens outside
14
+ * release/removeAllStreams, so a held stream can not be stopped or replaced underneath its holders.
15
+ */
2
16
  export declare class StreamRegister {
3
- private teleStreams;
17
+ private entries;
4
18
  private logger;
5
19
  private static instance;
6
20
  private constructor();
7
21
  static getInstance(): StreamRegister;
8
22
  static getIdentifier($identifier: string, scope?: string): string;
9
- addStream(stream: IBaseStream): void;
23
+ acquireStream<T extends IBaseStream>(identifier: string, create: () => T, init: (stream: T) => Promise<void>): IAcquiredStream<T>;
10
24
  getStream(identifier: string): IBaseStream | undefined;
11
- removeStream(stream: IBaseStream): void;
12
25
  removeAllStreams(): boolean;
26
+ private release;
27
+ private evict;
13
28
  }
@@ -1,8 +1,14 @@
1
1
  import { RocosError, errorCodes } from '../models';
2
2
  import { RocosLogger } from '../logger/RocosLogger';
3
+ /**
4
+ * Sole owner of stream lifecycle. Streams are shared per identifier and refcounted: acquireStream
5
+ * gets-or-creates and increments in one synchronous step, and the returned handle's release()
6
+ * tears the stream down when the last holder leaves. Teardown never happens outside
7
+ * release/removeAllStreams, so a held stream can not be stopped or replaced underneath its holders.
8
+ */
3
9
  export class StreamRegister {
4
10
  constructor() {
5
- this.teleStreams = new Map();
11
+ this.entries = new Map();
6
12
  this.logger = RocosLogger.getInstance('StreamRegister');
7
13
  }
8
14
  static getInstance() {
@@ -14,55 +20,83 @@ export class StreamRegister {
14
20
  static getIdentifier($identifier, scope) {
15
21
  return `${$identifier}-${scope ?? ''}`;
16
22
  }
17
- addStream(stream) {
18
- try {
19
- // Stop any existing occupant of this key before overwriting it.
20
- const existing = this.teleStreams.get(stream.identifier);
21
- if (existing && existing !== stream) {
22
- existing.stopStream();
23
- }
24
- this.teleStreams.set(stream.identifier, stream);
23
+ acquireStream(identifier, create, init) {
24
+ let entry = this.entries.get(identifier);
25
+ let isNew = false;
26
+ if (entry) {
27
+ entry.refCount++;
25
28
  }
26
- catch (e) {
27
- this.logger.error(`Failed to add stream to list. identifier: ${stream.identifier}`, e);
28
- if (e instanceof Error || typeof e === 'string') {
29
- throw new RocosError(e, errorCodes.STREAM_LISTENER_ERROR);
30
- }
31
- else {
32
- throw e;
33
- }
29
+ else {
30
+ isNew = true;
31
+ const stream = create();
32
+ const ready = init(stream).catch((error) => {
33
+ this.logger.error(`Stream init failed, evicting. identifier: ${identifier}`, error);
34
+ try {
35
+ this.evict(identifier, stream);
36
+ }
37
+ catch (evictionError) {
38
+ this.logger.error(`Failed to evict after init failure. identifier: ${identifier}`, evictionError);
39
+ }
40
+ throw error;
41
+ });
42
+ // `ready` is stored and shared; this discarded catch pins a rejection observer so an init
43
+ // failure can never surface as an unhandled rejection, while holders still see the rejection.
44
+ void ready.catch(() => undefined);
45
+ entry = { stream, refCount: 1, ready };
46
+ this.entries.set(identifier, entry);
34
47
  }
48
+ const held = entry;
49
+ let released = false;
50
+ const release = () => {
51
+ // One-shot so a double release can never consume another holder's refcount.
52
+ if (released)
53
+ return;
54
+ released = true;
55
+ this.release(identifier, held);
56
+ };
57
+ return { stream: entry.stream, isNew, ready: entry.ready, release };
35
58
  }
36
59
  getStream(identifier) {
37
- return this.teleStreams.get(identifier);
60
+ return this.entries.get(identifier)?.stream;
38
61
  }
39
- removeStream(stream) {
40
- try {
41
- stream.stopStream();
42
- // Only clear the entry if this instance still owns the key: a replacement may already hold it,
43
- // and evicting that would leave subscribers forking duplicate streams for the scope.
44
- if (this.teleStreams.get(stream.identifier) === stream) {
45
- this.teleStreams.delete(stream.identifier);
46
- }
47
- }
48
- catch (e) {
49
- this.logger.error(`Failed to remove stream from list. identifier: ${stream.identifier}`, e);
50
- if (e instanceof Error || typeof e === 'string') {
51
- throw new RocosError(e, errorCodes.STREAM_LISTENER_ERROR);
62
+ removeAllStreams() {
63
+ for (const [, entry] of this.entries.entries()) {
64
+ try {
65
+ entry.stream.stopStream();
52
66
  }
53
- else {
54
- throw e;
67
+ catch (e) {
68
+ this.logger.error(`error removing stream, ${e}`);
55
69
  }
56
70
  }
71
+ this.entries.clear();
72
+ return true;
57
73
  }
58
- removeAllStreams() {
74
+ release(identifier, entry) {
75
+ if (this.entries.get(identifier) !== entry)
76
+ return; // already evicted, or force-removed
77
+ entry.refCount--;
78
+ if (entry.refCount > 0)
79
+ return;
80
+ // Wait for init to settle before tearing down, so init can never open resources on an already
81
+ // stopped stream. A re-acquire in the meantime revives the entry and cancels the teardown.
82
+ void entry.ready
83
+ .catch(() => undefined)
84
+ .then(() => {
85
+ if (this.entries.get(identifier) === entry && entry.refCount <= 0) {
86
+ this.evict(identifier, entry.stream);
87
+ }
88
+ })
89
+ .catch((e) => this.logger.error(`Failed to evict stream. identifier: ${identifier}`, e));
90
+ }
91
+ evict(identifier, stream) {
59
92
  try {
60
- for (const [, stream] of this.teleStreams.entries()) {
61
- this.removeStream(stream);
93
+ if (this.entries.get(identifier)?.stream === stream) {
94
+ this.entries.delete(identifier);
62
95
  }
96
+ stream.stopStream();
63
97
  }
64
98
  catch (e) {
65
- this.logger.error(`error removing stream, ${e}`);
99
+ this.logger.error(`Failed to remove stream from list. identifier: ${identifier}`, e);
66
100
  if (e instanceof Error || typeof e === 'string') {
67
101
  throw new RocosError(e, errorCodes.STREAM_LISTENER_ERROR);
68
102
  }
@@ -70,6 +104,5 @@ export class StreamRegister {
70
104
  throw e;
71
105
  }
72
106
  }
73
- return true;
74
107
  }
75
108
  }
@@ -35,7 +35,7 @@ export class StreamHeartbeat {
35
35
  this.callback = callback;
36
36
  this.timeout = timeout;
37
37
  this.logger.info('Creating stream heartbeat');
38
- this.interval = setInterval(this.callback, this.timeout);
38
+ this.interval = setInterval(() => this.callback?.(), this.timeout);
39
39
  }
40
40
  else {
41
41
  this.logger.warn('Stream heartbeat already exists');
@@ -55,7 +55,7 @@ export class StreamHeartbeat {
55
55
  // if the health check has not started yet, we initialise it
56
56
  if (!this.healthInterval) {
57
57
  this.logger.debug('Starting health check timer');
58
- this.healthInterval = setInterval(this.checkHealth, this.healthTimeout);
58
+ this.healthInterval = setInterval(() => void this.checkHealth(), this.healthTimeout);
59
59
  this.healthLastSeenDate = new Date();
60
60
  this.healthMisses = 0;
61
61
  }
@@ -14,6 +14,7 @@ export declare abstract class TelemetryStreamAbstract implements ITelemetryStrea
14
14
  protected token?: string;
15
15
  private scope;
16
16
  protected url: string;
17
+ private telemetryActionQueue;
17
18
  private timerIntervalInSec;
18
19
  protected subscriberStatus: SubscriberStatusEnum;
19
20
  private checkerStartedAt?;
@@ -40,10 +41,17 @@ export declare abstract class TelemetryStreamAbstract implements ITelemetryStrea
40
41
  stopStream(): void;
41
42
  addSubscription(params: ITelemetryParams): void;
42
43
  removeSubscription(params: ITelemetryParams, terminateReceiverGroup?: boolean): Promise<void>;
44
+ private isQueryLookup;
45
+ private getSubscribedSources;
43
46
  sendAcknowledgment(uid: string, status: TelemetryAckStatus, noRetry: boolean): boolean;
44
47
  private getSubscriptions;
45
48
  protected onData(message: TelemetryStreamMessage, isStream: true): void;
46
49
  protected onData(message: TelemetryMessage, isStream: false): void;
50
+ /**
51
+ * Gateway subscription state is a set mutated by deltas, so ordering is correctness: a concurrent
52
+ * unsubscribe landing last strands a subscribe and nothing re-adds it.
53
+ */
54
+ private enqueueTelemetryAction;
47
55
  private takeTelemetryAction;
48
56
  protected listenMessagesAndRenew(): void;
49
57
  private addTerminateToAction;
@@ -52,7 +60,6 @@ export declare abstract class TelemetryStreamAbstract implements ITelemetryStrea
52
60
  * Auto resubscribe to reduce CPU usage.
53
61
  */
54
62
  private autoResubscribe;
55
- private isFoundCallsign;
56
63
  private isFoundSource;
57
64
  private isRegisteredMessage;
58
65
  private updateReceivedDataStatsWithMessage;