@dronedeploy/rocos-js-sdk 4.4.7 → 4.4.9

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.
@@ -10,8 +10,12 @@ export interface IAcquiredStream<T extends IBaseStream> {
10
10
  /**
11
11
  * Sole owner of stream lifecycle. Streams are shared per identifier and refcounted: acquireStream
12
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.
13
+ * tears the stream down when the last holder leaves.
14
+ *
15
+ * A held stream can also be stopped and replaced underneath its holders, by reportStreamDead, when
16
+ * the stream can no longer carry anything. Holders keep the instance they were handed; their
17
+ * release() finds the entry gone and does nothing, so a replacement never inherits a departing
18
+ * holder's refcount and a refcount can not go negative.
15
19
  */
16
20
  export declare class StreamRegister {
17
21
  private entries;
@@ -22,6 +26,7 @@ export declare class StreamRegister {
22
26
  static getIdentifier($identifier: string, scope?: string): string;
23
27
  acquireStream<T extends IBaseStream>(identifier: string, create: () => T, init: (stream: T) => Promise<void>): IAcquiredStream<T>;
24
28
  getStream(identifier: string): IBaseStream | undefined;
29
+ reportStreamDead(identifier: string, stream: IBaseStream): void;
25
30
  removeAllStreams(): boolean;
26
31
  private release;
27
32
  private evict;
@@ -6,8 +6,12 @@ const RocosLogger_1 = require("../logger/RocosLogger");
6
6
  /**
7
7
  * Sole owner of stream lifecycle. Streams are shared per identifier and refcounted: acquireStream
8
8
  * gets-or-creates and increments in one synchronous step, and the returned handle's release()
9
- * tears the stream down when the last holder leaves. Teardown never happens outside
10
- * release/removeAllStreams, so a held stream can not be stopped or replaced underneath its holders.
9
+ * tears the stream down when the last holder leaves.
10
+ *
11
+ * A held stream can also be stopped and replaced underneath its holders, by reportStreamDead, when
12
+ * the stream can no longer carry anything. Holders keep the instance they were handed; their
13
+ * release() finds the entry gone and does nothing, so a replacement never inherits a departing
14
+ * holder's refcount and a refcount can not go negative.
11
15
  */
12
16
  class StreamRegister {
13
17
  constructor() {
@@ -62,6 +66,17 @@ class StreamRegister {
62
66
  getStream(identifier) {
63
67
  return this.entries.get(identifier)?.stream;
64
68
  }
69
+ reportStreamDead(identifier, stream) {
70
+ if (this.entries.get(identifier)?.stream !== stream)
71
+ return; // already evicted, or already replaced
72
+ this.logger.warn(`Stream reported dead, evicting so the next acquirer builds a new one. identifier: ${identifier}`);
73
+ try {
74
+ this.evict(identifier, stream);
75
+ }
76
+ catch (e) {
77
+ this.logger.error(`Failed to evict dead stream. identifier: ${identifier}`, e);
78
+ }
79
+ }
65
80
  removeAllStreams() {
66
81
  for (const [, entry] of this.entries.entries()) {
67
82
  try {
@@ -29,6 +29,7 @@ export declare abstract class TelemetryStreamAbstract implements ITelemetryStrea
29
29
  private sendHeartbeatTime;
30
30
  private receivedDataStats;
31
31
  private subscriptions;
32
+ private receiverDead;
32
33
  constructor(config: ITelemetryStreamConfig);
33
34
  protected abstract stopInternal(): void;
34
35
  protected abstract registerReceiver(): () => void;
@@ -41,6 +42,8 @@ export declare abstract class TelemetryStreamAbstract implements ITelemetryStrea
41
42
  stopStream(): void;
42
43
  addSubscription(params: ITelemetryParams): void;
43
44
  removeSubscription(params: ITelemetryParams, terminateReceiverGroup?: boolean): Promise<void>;
45
+ private onBackgroundActionFailure;
46
+ private markReceiverDead;
44
47
  private isQueryLookup;
45
48
  private getSubscribedSources;
46
49
  sendAcknowledgment(uid: string, status: TelemetryAckStatus, noRetry: boolean): boolean;
@@ -55,6 +58,7 @@ export declare abstract class TelemetryStreamAbstract implements ITelemetryStrea
55
58
  private takeTelemetryAction;
56
59
  protected listenMessagesAndRenew(): void;
57
60
  private addTerminateToAction;
61
+ private renewReceiver;
58
62
  private messageTimeoutTryAgain;
59
63
  /**
60
64
  * Auto resubscribe to reduce CPU usage.
@@ -20,6 +20,14 @@ const resolveTelemetryActions = (actions) => {
20
20
  const resolveUnsentTelemetryActions = (queueState) => {
21
21
  resolveTelemetryActions(queueState.pendingActions.splice(0));
22
22
  };
23
+ const isReceiverGoneError = (error) => {
24
+ const code = error?.code;
25
+ if (typeof code === 'number')
26
+ return code === 9; // gRPC FAILED_PRECONDITION
27
+ if (typeof code === 'string')
28
+ return code.toLowerCase() === 'failed_precondition';
29
+ return false;
30
+ };
23
31
  const takeNextTelemetryBatch = (queueState) => {
24
32
  const firstAction = queueState.pendingActions.shift();
25
33
  if (!firstAction)
@@ -53,6 +61,7 @@ class TelemetryStreamAbstract {
53
61
  this.sendHeartbeatTime = 5000;
54
62
  this.receivedDataStats = {};
55
63
  this.subscriptions = new Map();
64
+ this.receiverDead = false;
56
65
  this.identifier = StreamRegister_1.StreamRegister.getIdentifier(identifier_1.IDENTIFIER_NAME_TELEMETRY, config.scope);
57
66
  this.projectId = config.projectId;
58
67
  this.callsignsLookup = config.callsignsLookup;
@@ -129,7 +138,7 @@ class TelemetryStreamAbstract {
129
138
  if (toAdd.length) {
130
139
  this.logger.debug('New query subscriptions added, we need to refresh sources', { toAdd });
131
140
  void this.enqueueTelemetryAction('subscribe', this.callsignsLookup, toAdd).catch((error) => {
132
- this.logger.error(`Failed to subscribe: ${error}`);
141
+ this.onBackgroundActionFailure('subscribe', error);
133
142
  });
134
143
  }
135
144
  }
@@ -142,7 +151,7 @@ class TelemetryStreamAbstract {
142
151
  if (toAdd.callsigns.length && toAdd.sources.length) {
143
152
  this.logger.debug('New subscriptions added, we need to refresh sources', { toAdd });
144
153
  void this.enqueueTelemetryAction('subscribe', new models_1.CallsignsLookup(toAdd.callsigns), toAdd.sources).catch((error) => {
145
- this.logger.error(`Failed to subscribe: ${error}`);
154
+ this.onBackgroundActionFailure('subscribe', error);
146
155
  });
147
156
  }
148
157
  }
@@ -178,7 +187,7 @@ class TelemetryStreamAbstract {
178
187
  await this.enqueueTelemetryAction('unsubscribe', this.callsignsLookup, toRemove, terminateReceiverGroup);
179
188
  }
180
189
  catch (err) {
181
- this.logger.error(`Failed to unsubscribe: ${err}`);
190
+ this.onBackgroundActionFailure('unsubscribe', err);
182
191
  }
183
192
  }
184
193
  return;
@@ -193,10 +202,24 @@ class TelemetryStreamAbstract {
193
202
  await this.enqueueTelemetryAction('unsubscribe', new models_1.CallsignsLookup(toRemove.callsigns), toRemove.sources, terminateReceiverGroup);
194
203
  }
195
204
  catch (err) {
196
- this.logger.error(`Failed to unsubscribe: ${err}`);
205
+ this.onBackgroundActionFailure('unsubscribe', err);
197
206
  }
198
207
  }
199
208
  }
209
+ onBackgroundActionFailure(actionType, error) {
210
+ this.logger.error(`Failed to ${actionType}: ${error}`);
211
+ if (isReceiverGoneError(error)) {
212
+ this.markReceiverDead(error);
213
+ }
214
+ }
215
+ markReceiverDead(error) {
216
+ if (this.receiverDead)
217
+ return;
218
+ this.receiverDead = true;
219
+ this.logger.error('Receiver is no longer registered with the gateway, retiring the stream', error);
220
+ StreamRegister_1.StreamRegister.getInstance().reportStreamDead(this.identifier, this);
221
+ this.messageStream$.error(error);
222
+ }
200
223
  isQueryLookup() {
201
224
  return this.callsignsLookup?.isQuery() ?? false;
202
225
  }
@@ -263,7 +286,7 @@ class TelemetryStreamAbstract {
263
286
  rocosMessage.subscriberId = this.subscriberId;
264
287
  this.logger.info('onData', `subscriberId has been updated - ${subscriberId}`, { subscriberId });
265
288
  void this.enqueueTelemetryAction('subscribe').catch((error) => {
266
- this.logger.error(`Failed to subscribe: ${error}`);
289
+ this.onBackgroundActionFailure('subscribe', error);
267
290
  });
268
291
  this.heartbeat.start(() => this.sendHeartbeat(), this.sendHeartbeatTime);
269
292
  isRobotMessage = false;
@@ -514,14 +537,22 @@ class TelemetryStreamAbstract {
514
537
  unsubscribeOperation,
515
538
  };
516
539
  }
540
+ renewReceiver() {
541
+ try {
542
+ // Remove the received call anyway.
543
+ this.stopInternal();
544
+ this.lastMessageReceived = undefined;
545
+ this.lastRobotMessageReceived = undefined;
546
+ this.registerReceiver();
547
+ }
548
+ catch (e) {
549
+ this.logger.error('Failed to renew the receiver, retrying on the next check', e);
550
+ }
551
+ }
517
552
  messageTimeoutTryAgain() {
518
553
  // Increase retry times.
519
554
  this.retryTimes++;
520
- // Remove the received call anyway.
521
- this.stopInternal();
522
- this.lastMessageReceived = undefined;
523
- this.lastRobotMessageReceived = undefined;
524
- this.registerReceiver();
555
+ this.renewReceiver();
525
556
  }
526
557
  /**
527
558
  * Auto resubscribe to reduce CPU usage.
@@ -529,11 +560,7 @@ class TelemetryStreamAbstract {
529
560
  autoResubscribe() {
530
561
  // Reset
531
562
  this.retryTimes = 0;
532
- // Remove the received call anyway.
533
- this.stopInternal();
534
- this.lastMessageReceived = undefined;
535
- this.lastRobotMessageReceived = undefined;
536
- this.registerReceiver();
563
+ this.renewReceiver();
537
564
  }
538
565
  isFoundSource(source) {
539
566
  return this.sources.indexOf(source) !== -1;
@@ -8,6 +8,10 @@ export interface Map {
8
8
  deployedCount: number;
9
9
  isUploading: boolean;
10
10
  isInCloud: boolean;
11
+ /** Whether this map may be a deploy target. False for the reserved `dronedeploy` system map. */
12
+ isDeployable?: boolean;
13
+ /** Whether this map may be written through the generic maps API. False for reserved system maps. */
14
+ isEditable?: boolean;
11
15
  hash?: string;
12
16
  lastModifiedTimestamp: number;
13
17
  }
@@ -90,11 +90,13 @@ class TelemetryService extends BaseStreamService_1.BaseStreamService {
90
90
  }), (0, operators_1.finalize)(() => {
91
91
  if (params.terminateReceiverGroupOnUnsubscribe) {
92
92
  const removal = stream.removeSubscription(subscriptionParams, true);
93
- void Promise.resolve(removal).finally(release);
93
+ void Promise.resolve(removal)
94
+ .finally(release)
95
+ .catch(() => undefined);
94
96
  }
95
97
  else {
96
98
  release();
97
- void stream.removeSubscription(subscriptionParams);
99
+ void Promise.resolve(stream.removeSubscription(subscriptionParams)).catch(() => undefined);
98
100
  }
99
101
  }));
100
102
  });
@@ -10,8 +10,12 @@ export interface IAcquiredStream<T extends IBaseStream> {
10
10
  /**
11
11
  * Sole owner of stream lifecycle. Streams are shared per identifier and refcounted: acquireStream
12
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.
13
+ * tears the stream down when the last holder leaves.
14
+ *
15
+ * A held stream can also be stopped and replaced underneath its holders, by reportStreamDead, when
16
+ * the stream can no longer carry anything. Holders keep the instance they were handed; their
17
+ * release() finds the entry gone and does nothing, so a replacement never inherits a departing
18
+ * holder's refcount and a refcount can not go negative.
15
19
  */
16
20
  export declare class StreamRegister {
17
21
  private entries;
@@ -22,6 +26,7 @@ export declare class StreamRegister {
22
26
  static getIdentifier($identifier: string, scope?: string): string;
23
27
  acquireStream<T extends IBaseStream>(identifier: string, create: () => T, init: (stream: T) => Promise<void>): IAcquiredStream<T>;
24
28
  getStream(identifier: string): IBaseStream | undefined;
29
+ reportStreamDead(identifier: string, stream: IBaseStream): void;
25
30
  removeAllStreams(): boolean;
26
31
  private release;
27
32
  private evict;
@@ -3,8 +3,12 @@ import { RocosLogger } from '../logger/RocosLogger';
3
3
  /**
4
4
  * Sole owner of stream lifecycle. Streams are shared per identifier and refcounted: acquireStream
5
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.
6
+ * tears the stream down when the last holder leaves.
7
+ *
8
+ * A held stream can also be stopped and replaced underneath its holders, by reportStreamDead, when
9
+ * the stream can no longer carry anything. Holders keep the instance they were handed; their
10
+ * release() finds the entry gone and does nothing, so a replacement never inherits a departing
11
+ * holder's refcount and a refcount can not go negative.
8
12
  */
9
13
  export class StreamRegister {
10
14
  constructor() {
@@ -59,6 +63,17 @@ export class StreamRegister {
59
63
  getStream(identifier) {
60
64
  return this.entries.get(identifier)?.stream;
61
65
  }
66
+ reportStreamDead(identifier, stream) {
67
+ if (this.entries.get(identifier)?.stream !== stream)
68
+ return; // already evicted, or already replaced
69
+ this.logger.warn(`Stream reported dead, evicting so the next acquirer builds a new one. identifier: ${identifier}`);
70
+ try {
71
+ this.evict(identifier, stream);
72
+ }
73
+ catch (e) {
74
+ this.logger.error(`Failed to evict dead stream. identifier: ${identifier}`, e);
75
+ }
76
+ }
62
77
  removeAllStreams() {
63
78
  for (const [, entry] of this.entries.entries()) {
64
79
  try {
@@ -29,6 +29,7 @@ export declare abstract class TelemetryStreamAbstract implements ITelemetryStrea
29
29
  private sendHeartbeatTime;
30
30
  private receivedDataStats;
31
31
  private subscriptions;
32
+ private receiverDead;
32
33
  constructor(config: ITelemetryStreamConfig);
33
34
  protected abstract stopInternal(): void;
34
35
  protected abstract registerReceiver(): () => void;
@@ -41,6 +42,8 @@ export declare abstract class TelemetryStreamAbstract implements ITelemetryStrea
41
42
  stopStream(): void;
42
43
  addSubscription(params: ITelemetryParams): void;
43
44
  removeSubscription(params: ITelemetryParams, terminateReceiverGroup?: boolean): Promise<void>;
45
+ private onBackgroundActionFailure;
46
+ private markReceiverDead;
44
47
  private isQueryLookup;
45
48
  private getSubscribedSources;
46
49
  sendAcknowledgment(uid: string, status: TelemetryAckStatus, noRetry: boolean): boolean;
@@ -55,6 +58,7 @@ export declare abstract class TelemetryStreamAbstract implements ITelemetryStrea
55
58
  private takeTelemetryAction;
56
59
  protected listenMessagesAndRenew(): void;
57
60
  private addTerminateToAction;
61
+ private renewReceiver;
58
62
  private messageTimeoutTryAgain;
59
63
  /**
60
64
  * Auto resubscribe to reduce CPU usage.
@@ -17,6 +17,14 @@ const resolveTelemetryActions = (actions) => {
17
17
  const resolveUnsentTelemetryActions = (queueState) => {
18
18
  resolveTelemetryActions(queueState.pendingActions.splice(0));
19
19
  };
20
+ const isReceiverGoneError = (error) => {
21
+ const code = error?.code;
22
+ if (typeof code === 'number')
23
+ return code === 9; // gRPC FAILED_PRECONDITION
24
+ if (typeof code === 'string')
25
+ return code.toLowerCase() === 'failed_precondition';
26
+ return false;
27
+ };
20
28
  const takeNextTelemetryBatch = (queueState) => {
21
29
  const firstAction = queueState.pendingActions.shift();
22
30
  if (!firstAction)
@@ -50,6 +58,7 @@ export class TelemetryStreamAbstract {
50
58
  this.sendHeartbeatTime = 5000;
51
59
  this.receivedDataStats = {};
52
60
  this.subscriptions = new Map();
61
+ this.receiverDead = false;
53
62
  this.identifier = StreamRegister.getIdentifier(IDENTIFIER_NAME_TELEMETRY, config.scope);
54
63
  this.projectId = config.projectId;
55
64
  this.callsignsLookup = config.callsignsLookup;
@@ -126,7 +135,7 @@ export class TelemetryStreamAbstract {
126
135
  if (toAdd.length) {
127
136
  this.logger.debug('New query subscriptions added, we need to refresh sources', { toAdd });
128
137
  void this.enqueueTelemetryAction('subscribe', this.callsignsLookup, toAdd).catch((error) => {
129
- this.logger.error(`Failed to subscribe: ${error}`);
138
+ this.onBackgroundActionFailure('subscribe', error);
130
139
  });
131
140
  }
132
141
  }
@@ -139,7 +148,7 @@ export class TelemetryStreamAbstract {
139
148
  if (toAdd.callsigns.length && toAdd.sources.length) {
140
149
  this.logger.debug('New subscriptions added, we need to refresh sources', { toAdd });
141
150
  void this.enqueueTelemetryAction('subscribe', new CallsignsLookup(toAdd.callsigns), toAdd.sources).catch((error) => {
142
- this.logger.error(`Failed to subscribe: ${error}`);
151
+ this.onBackgroundActionFailure('subscribe', error);
143
152
  });
144
153
  }
145
154
  }
@@ -175,7 +184,7 @@ export class TelemetryStreamAbstract {
175
184
  await this.enqueueTelemetryAction('unsubscribe', this.callsignsLookup, toRemove, terminateReceiverGroup);
176
185
  }
177
186
  catch (err) {
178
- this.logger.error(`Failed to unsubscribe: ${err}`);
187
+ this.onBackgroundActionFailure('unsubscribe', err);
179
188
  }
180
189
  }
181
190
  return;
@@ -190,10 +199,24 @@ export class TelemetryStreamAbstract {
190
199
  await this.enqueueTelemetryAction('unsubscribe', new CallsignsLookup(toRemove.callsigns), toRemove.sources, terminateReceiverGroup);
191
200
  }
192
201
  catch (err) {
193
- this.logger.error(`Failed to unsubscribe: ${err}`);
202
+ this.onBackgroundActionFailure('unsubscribe', err);
194
203
  }
195
204
  }
196
205
  }
206
+ onBackgroundActionFailure(actionType, error) {
207
+ this.logger.error(`Failed to ${actionType}: ${error}`);
208
+ if (isReceiverGoneError(error)) {
209
+ this.markReceiverDead(error);
210
+ }
211
+ }
212
+ markReceiverDead(error) {
213
+ if (this.receiverDead)
214
+ return;
215
+ this.receiverDead = true;
216
+ this.logger.error('Receiver is no longer registered with the gateway, retiring the stream', error);
217
+ StreamRegister.getInstance().reportStreamDead(this.identifier, this);
218
+ this.messageStream$.error(error);
219
+ }
197
220
  isQueryLookup() {
198
221
  return this.callsignsLookup?.isQuery() ?? false;
199
222
  }
@@ -260,7 +283,7 @@ export class TelemetryStreamAbstract {
260
283
  rocosMessage.subscriberId = this.subscriberId;
261
284
  this.logger.info('onData', `subscriberId has been updated - ${subscriberId}`, { subscriberId });
262
285
  void this.enqueueTelemetryAction('subscribe').catch((error) => {
263
- this.logger.error(`Failed to subscribe: ${error}`);
286
+ this.onBackgroundActionFailure('subscribe', error);
264
287
  });
265
288
  this.heartbeat.start(() => this.sendHeartbeat(), this.sendHeartbeatTime);
266
289
  isRobotMessage = false;
@@ -511,14 +534,22 @@ export class TelemetryStreamAbstract {
511
534
  unsubscribeOperation,
512
535
  };
513
536
  }
537
+ renewReceiver() {
538
+ try {
539
+ // Remove the received call anyway.
540
+ this.stopInternal();
541
+ this.lastMessageReceived = undefined;
542
+ this.lastRobotMessageReceived = undefined;
543
+ this.registerReceiver();
544
+ }
545
+ catch (e) {
546
+ this.logger.error('Failed to renew the receiver, retrying on the next check', e);
547
+ }
548
+ }
514
549
  messageTimeoutTryAgain() {
515
550
  // Increase retry times.
516
551
  this.retryTimes++;
517
- // Remove the received call anyway.
518
- this.stopInternal();
519
- this.lastMessageReceived = undefined;
520
- this.lastRobotMessageReceived = undefined;
521
- this.registerReceiver();
552
+ this.renewReceiver();
522
553
  }
523
554
  /**
524
555
  * Auto resubscribe to reduce CPU usage.
@@ -526,11 +557,7 @@ export class TelemetryStreamAbstract {
526
557
  autoResubscribe() {
527
558
  // Reset
528
559
  this.retryTimes = 0;
529
- // Remove the received call anyway.
530
- this.stopInternal();
531
- this.lastMessageReceived = undefined;
532
- this.lastRobotMessageReceived = undefined;
533
- this.registerReceiver();
560
+ this.renewReceiver();
534
561
  }
535
562
  isFoundSource(source) {
536
563
  return this.sources.indexOf(source) !== -1;
@@ -8,6 +8,10 @@ export interface Map {
8
8
  deployedCount: number;
9
9
  isUploading: boolean;
10
10
  isInCloud: boolean;
11
+ /** Whether this map may be a deploy target. False for the reserved `dronedeploy` system map. */
12
+ isDeployable?: boolean;
13
+ /** Whether this map may be written through the generic maps API. False for reserved system maps. */
14
+ isEditable?: boolean;
11
15
  hash?: string;
12
16
  lastModifiedTimestamp: number;
13
17
  }
@@ -87,11 +87,13 @@ export class TelemetryService extends BaseStreamService {
87
87
  }), finalize(() => {
88
88
  if (params.terminateReceiverGroupOnUnsubscribe) {
89
89
  const removal = stream.removeSubscription(subscriptionParams, true);
90
- void Promise.resolve(removal).finally(release);
90
+ void Promise.resolve(removal)
91
+ .finally(release)
92
+ .catch(() => undefined);
91
93
  }
92
94
  else {
93
95
  release();
94
- void stream.removeSubscription(subscriptionParams);
96
+ void Promise.resolve(stream.removeSubscription(subscriptionParams)).catch(() => undefined);
95
97
  }
96
98
  }));
97
99
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dronedeploy/rocos-js-sdk",
3
- "version": "4.4.7",
3
+ "version": "4.4.9",
4
4
  "description": "Javascript SDK for rocos",
5
5
  "main": "cjs/index.js",
6
6
  "module": "esm/index.js",