@dronedeploy/rocos-js-sdk 4.4.2 → 4.4.3

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.
@@ -19,8 +19,10 @@ class StreamRegister {
19
19
  }
20
20
  addStream(stream) {
21
21
  try {
22
- if (this.teleStreams.has(stream.identifier)) {
23
- this.removeStream(stream);
22
+ // Stop any existing occupant of this key before overwriting it.
23
+ const existing = this.teleStreams.get(stream.identifier);
24
+ if (existing && existing !== stream) {
25
+ existing.stopStream();
24
26
  }
25
27
  this.teleStreams.set(stream.identifier, stream);
26
28
  }
@@ -40,7 +42,11 @@ class StreamRegister {
40
42
  removeStream(stream) {
41
43
  try {
42
44
  stream.stopStream();
43
- this.teleStreams.delete(stream.identifier);
45
+ // Only clear the entry if this instance still owns the key: a replacement may already hold it,
46
+ // and evicting that would leave subscribers forking duplicate streams for the scope.
47
+ if (this.teleStreams.get(stream.identifier) === stream) {
48
+ this.teleStreams.delete(stream.identifier);
49
+ }
44
50
  }
45
51
  catch (e) {
46
52
  this.logger.error(`Failed to remove stream from list. identifier: ${stream.identifier}`, e);
@@ -10,7 +10,7 @@ export declare abstract class BaseStreamService<T extends IBaseStream, C extends
10
10
  protected constructor(name: string, config: IRocosSDKConfig);
11
11
  getStatus(): boolean;
12
12
  protected initStream(stream: T): Promise<void>;
13
- protected createStreamFromConfig(identifier: string, config: C): Promise<{
13
+ protected createStreamFromConfig(identifier: string, config: C, onAcquire?: (stream: T) => void): Promise<{
14
14
  stream: T;
15
15
  isNew: boolean;
16
16
  }>;
@@ -32,7 +32,7 @@ class BaseStreamService {
32
32
  }
33
33
  authService.startTokenRefreshChecker();
34
34
  }
35
- async createStreamFromConfig(identifier, config) {
35
+ async createStreamFromConfig(identifier, config, onAcquire) {
36
36
  const identifierWithScope = StreamRegister_1.StreamRegister.getIdentifier(identifier, config.scope);
37
37
  const streamRegister = StreamRegister_1.StreamRegister.getInstance();
38
38
  let stream = streamRegister.getStream(identifierWithScope);
@@ -45,7 +45,26 @@ class BaseStreamService {
45
45
  this.status$.next(msg);
46
46
  });
47
47
  streamRegister.addStream(stream);
48
- await this.initStream(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
+ }
49
68
  }
50
69
  return { stream, isNew };
51
70
  }
@@ -61,19 +61,26 @@ class TelemetryService extends BaseStreamService_1.BaseStreamService {
61
61
  callsigns: callsignsLookup?.lookupType === models_1.CallsignsLookupType.List ? callsignsLookup.lookupValue : [],
62
62
  sources,
63
63
  };
64
- const stream = this.createStream(params.projectId, callsignsLookup, sources, scope).then((stream) => {
65
- stream.addSubscription(subscriptionParams);
66
- if (!this.statusSubscription) {
67
- this.statusSubscription = stream.statusStream$.subscribe((msg) => {
68
- this.status = msg === models_1.SubscriberStatusEnum.STOPPED || msg === models_1.SubscriberStatusEnum.ALIVE;
69
- this.status$.next(msg);
70
- });
71
- }
72
- return (0, rxjs_1.from)(stream.messageStream$).pipe((0, operators_1.finalize)(() => stream?.removeSubscription(subscriptionParams, params.terminateReceiverGroupOnUnsubscribe)));
64
+ // `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.
66
+ return (0, rxjs_1.defer)(() => {
67
+ let acquired;
68
+ const stream$ = this.createStream(params.projectId, callsignsLookup, sources, scope, (stream) => {
69
+ acquired = stream;
70
+ 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);
76
+ });
77
+ }
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)));
73
83
  });
74
- return (0, rxjs_1.from)(stream).pipe((0, rxjs_1.mergeAll)(), (0, operators_1.filter)((message) => {
75
- return callsigns.includes(message.callsign) && sources.includes(message.source);
76
- }));
77
84
  }
78
85
  /**
79
86
  * A method to keep track of the current subscriptions
@@ -152,7 +159,7 @@ class TelemetryService extends BaseStreamService_1.BaseStreamService {
152
159
  return `${params.projectId}-default-${(0, flattenCallsignsLookup_1.flattenCallsignsLookup)(callsignsLookup).join()}`;
153
160
  return `${params.projectId}-default`;
154
161
  }
155
- async createStream(projectId, callsignsLookup, sources, scope) {
162
+ async createStream(projectId, callsignsLookup, sources, scope, onAcquire) {
156
163
  const newStream = await this.createStreamFromConfig(identifier_1.IDENTIFIER_NAME_TELEMETRY, {
157
164
  url: this.config.url,
158
165
  projectId,
@@ -165,7 +172,7 @@ class TelemetryService extends BaseStreamService_1.BaseStreamService {
165
172
  insecure: this.config.insecure,
166
173
  transport: this.config.transport,
167
174
  fetch: this.config.fetch,
168
- });
175
+ }, onAcquire);
169
176
  if (!newStream.isNew) {
170
177
  newStream.stream.statusStream$.subscribe((msg) => {
171
178
  this.streamStatusSubject$.next({
@@ -16,8 +16,10 @@ export class StreamRegister {
16
16
  }
17
17
  addStream(stream) {
18
18
  try {
19
- if (this.teleStreams.has(stream.identifier)) {
20
- this.removeStream(stream);
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();
21
23
  }
22
24
  this.teleStreams.set(stream.identifier, stream);
23
25
  }
@@ -37,7 +39,11 @@ export class StreamRegister {
37
39
  removeStream(stream) {
38
40
  try {
39
41
  stream.stopStream();
40
- this.teleStreams.delete(stream.identifier);
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
+ }
41
47
  }
42
48
  catch (e) {
43
49
  this.logger.error(`Failed to remove stream from list. identifier: ${stream.identifier}`, e);
@@ -10,7 +10,7 @@ export declare abstract class BaseStreamService<T extends IBaseStream, C extends
10
10
  protected constructor(name: string, config: IRocosSDKConfig);
11
11
  getStatus(): boolean;
12
12
  protected initStream(stream: T): Promise<void>;
13
- protected createStreamFromConfig(identifier: string, config: C): Promise<{
13
+ protected createStreamFromConfig(identifier: string, config: C, onAcquire?: (stream: T) => void): Promise<{
14
14
  stream: T;
15
15
  isNew: boolean;
16
16
  }>;
@@ -29,7 +29,7 @@ export class BaseStreamService {
29
29
  }
30
30
  authService.startTokenRefreshChecker();
31
31
  }
32
- async createStreamFromConfig(identifier, config) {
32
+ async createStreamFromConfig(identifier, config, onAcquire) {
33
33
  const identifierWithScope = StreamRegister.getIdentifier(identifier, config.scope);
34
34
  const streamRegister = StreamRegister.getInstance();
35
35
  let stream = streamRegister.getStream(identifierWithScope);
@@ -42,7 +42,26 @@ export class BaseStreamService {
42
42
  this.status$.next(msg);
43
43
  });
44
44
  streamRegister.addStream(stream);
45
- await this.initStream(stream);
45
+ }
46
+ // Acquire synchronously with the lookup/registration above, before any await. A stream is
47
+ // stopped and deregistered the instant its refcount hits zero, so a deferred acquire can land on
48
+ // a dead instance — including a freshly-registered stream torn down during the init await below.
49
+ onAcquire?.(stream);
50
+ if (isNew) {
51
+ try {
52
+ await this.initStream(stream);
53
+ }
54
+ catch (e) {
55
+ // Evict eagerly, before the error reaches the subscriber, so a synchronous resubscribe
56
+ // can't re-acquire this uninitialised instance.
57
+ streamRegister.removeStream(stream);
58
+ throw e;
59
+ }
60
+ // If a concurrent teardown deregistered this stream during init, init() has opened a receiver
61
+ // on an instance nothing can reach; close it.
62
+ if (streamRegister.getStream(identifierWithScope) !== stream) {
63
+ stream.stopStream();
64
+ }
46
65
  }
47
66
  return { stream, isNew };
48
67
  }
@@ -1,4 +1,4 @@
1
- import { BehaviorSubject, catchError, combineLatest, distinctUntilChanged, from, interval, map, mergeAll, of, startWith, } from 'rxjs';
1
+ import { BehaviorSubject, catchError, combineLatest, defer, distinctUntilChanged, from, interval, map, mergeAll, of, startWith, } from 'rxjs';
2
2
  import { CallsignStatus, CallsignsLookup, CallsignsLookupType, RocosError, SubscriberStatusEnum, TelemetryMonitorStatus, errorCodes, } from '../models';
3
3
  import { filter, finalize } from 'rxjs/operators';
4
4
  import { BaseStreamService } from './BaseStreamService';
@@ -58,19 +58,26 @@ export class TelemetryService extends BaseStreamService {
58
58
  callsigns: callsignsLookup?.lookupType === CallsignsLookupType.List ? callsignsLookup.lookupValue : [],
59
59
  sources,
60
60
  };
61
- const stream = this.createStream(params.projectId, callsignsLookup, sources, scope).then((stream) => {
62
- stream.addSubscription(subscriptionParams);
63
- if (!this.statusSubscription) {
64
- this.statusSubscription = stream.statusStream$.subscribe((msg) => {
65
- this.status = msg === SubscriberStatusEnum.STOPPED || msg === SubscriberStatusEnum.ALIVE;
66
- this.status$.next(msg);
67
- });
68
- }
69
- return from(stream.messageStream$).pipe(finalize(() => stream?.removeSubscription(subscriptionParams, params.terminateReceiverGroupOnUnsubscribe)));
61
+ // `defer` acquires the stream only when subscribed; `finalize` releases it on unsubscribe, error
62
+ // and complete — so acquire and release stay balanced on every path.
63
+ return defer(() => {
64
+ let acquired;
65
+ const stream$ = this.createStream(params.projectId, callsignsLookup, sources, scope, (stream) => {
66
+ acquired = stream;
67
+ stream.addSubscription(subscriptionParams);
68
+ }).then((stream) => {
69
+ if (!this.statusSubscription) {
70
+ this.statusSubscription = stream.statusStream$.subscribe((msg) => {
71
+ this.status = msg === SubscriberStatusEnum.STOPPED || msg === SubscriberStatusEnum.ALIVE;
72
+ this.status$.next(msg);
73
+ });
74
+ }
75
+ return from(stream.messageStream$);
76
+ });
77
+ return from(stream$).pipe(mergeAll(), filter((message) => {
78
+ return callsigns.includes(message.callsign) && sources.includes(message.source);
79
+ }), finalize(() => acquired?.removeSubscription(subscriptionParams, params.terminateReceiverGroupOnUnsubscribe)));
70
80
  });
71
- return from(stream).pipe(mergeAll(), filter((message) => {
72
- return callsigns.includes(message.callsign) && sources.includes(message.source);
73
- }));
74
81
  }
75
82
  /**
76
83
  * A method to keep track of the current subscriptions
@@ -149,7 +156,7 @@ export class TelemetryService extends BaseStreamService {
149
156
  return `${params.projectId}-default-${flattenCallsignsLookup(callsignsLookup).join()}`;
150
157
  return `${params.projectId}-default`;
151
158
  }
152
- async createStream(projectId, callsignsLookup, sources, scope) {
159
+ async createStream(projectId, callsignsLookup, sources, scope, onAcquire) {
153
160
  const newStream = await this.createStreamFromConfig(IDENTIFIER_NAME_TELEMETRY, {
154
161
  url: this.config.url,
155
162
  projectId,
@@ -162,7 +169,7 @@ export class TelemetryService extends BaseStreamService {
162
169
  insecure: this.config.insecure,
163
170
  transport: this.config.transport,
164
171
  fetch: this.config.fetch,
165
- });
172
+ }, onAcquire);
166
173
  if (!newStream.isNew) {
167
174
  newStream.stream.statusStream$.subscribe((msg) => {
168
175
  this.streamStatusSubject$.next({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dronedeploy/rocos-js-sdk",
3
- "version": "4.4.2",
3
+ "version": "4.4.3",
4
4
  "description": "Javascript SDK for rocos",
5
5
  "main": "cjs/index.js",
6
6
  "module": "esm/index.js",