@dronedeploy/rocos-js-sdk 4.4.6 → 4.4.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/cjs/api/StreamRegister.js +4 -4
  2. package/cjs/api/streams/caller/CallerStreamAbstract.js +2 -1
  3. package/cjs/api/streams/command/CommandStreamAbstract.js +2 -1
  4. package/cjs/api/streams/control/ControlStreamAbstract.js +2 -1
  5. package/cjs/api/streams/fileAccessor/FileAccessorStreamAbstract.js +2 -1
  6. package/cjs/api/streams/search/SearchStreamAbstract.js +2 -1
  7. package/cjs/api/streams/telemetry/TelemetryStream.js +1 -0
  8. package/cjs/api/streams/telemetry/TelemetryStreamAbstract.js +126 -25
  9. package/cjs/api/streams/telemetry/TelemetryStreamConnect.js +1 -0
  10. package/cjs/api/streams/telemetry/TelemetryStreamNode.js +1 -0
  11. package/cjs/api/streams/webRTCSignalling/WebRTCSignallingStreamAbstract.js +2 -1
  12. package/cjs/models/maps/Map.d.ts +1 -1
  13. package/cjs/services/SearchService.js +2 -7
  14. package/cjs/services/TelemetryService.d.ts +4 -0
  15. package/cjs/services/TelemetryService.js +12 -2
  16. package/esm/api/StreamRegister.js +4 -4
  17. package/esm/api/streams/caller/CallerStreamAbstract.js +2 -1
  18. package/esm/api/streams/command/CommandStreamAbstract.js +2 -1
  19. package/esm/api/streams/control/ControlStreamAbstract.js +2 -1
  20. package/esm/api/streams/fileAccessor/FileAccessorStreamAbstract.js +2 -1
  21. package/esm/api/streams/search/SearchStreamAbstract.js +2 -1
  22. package/esm/api/streams/telemetry/TelemetryStream.js +1 -0
  23. package/esm/api/streams/telemetry/TelemetryStreamAbstract.js +127 -26
  24. package/esm/api/streams/telemetry/TelemetryStreamConnect.js +1 -0
  25. package/esm/api/streams/telemetry/TelemetryStreamNode.js +1 -0
  26. package/esm/api/streams/webRTCSignalling/WebRTCSignallingStreamAbstract.js +2 -1
  27. package/esm/models/maps/Map.d.ts +1 -1
  28. package/esm/services/SearchService.js +2 -7
  29. package/esm/services/TelemetryService.d.ts +4 -0
  30. package/esm/services/TelemetryService.js +12 -2
  31. package/package.json +1 -1
@@ -82,13 +82,13 @@ class StreamRegister {
82
82
  return;
83
83
  // Wait for init to settle before tearing down, so init can never open resources on an already
84
84
  // stopped stream. A re-acquire in the meantime revives the entry and cancels the teardown.
85
- void entry.ready
86
- .catch(() => undefined)
87
- .then(() => {
85
+ const evictIfUnheld = () => {
88
86
  if (this.entries.get(identifier) === entry && entry.refCount <= 0) {
89
87
  this.evict(identifier, entry.stream);
90
88
  }
91
- })
89
+ };
90
+ void entry.ready
91
+ .then(() => evictIfUnheld(), () => evictIfUnheld())
92
92
  .catch((e) => this.logger.error(`Failed to evict stream. identifier: ${identifier}`, e));
93
93
  }
94
94
  evict(identifier, stream) {
@@ -29,7 +29,8 @@ class CallerStreamAbstract {
29
29
  this.token = token;
30
30
  }
31
31
  stopStream() {
32
- // do nothing for now
32
+ this.statusStream$.next(models_1.SubscriberStatusEnum.STOPPED);
33
+ this.statusStream$.complete();
33
34
  }
34
35
  invokeRequest(params) {
35
36
  this.logger.info('invokeRequest', params);
@@ -31,7 +31,8 @@ class CommandStreamAbstract {
31
31
  this.token = token;
32
32
  }
33
33
  stopStream() {
34
- // do nothing for now
34
+ this.statusStream$.next(models_1.SubscriberStatusEnum.STOPPED);
35
+ this.statusStream$.complete();
35
36
  }
36
37
  invokeRequest(params) {
37
38
  if (params.timeoutMs === undefined || params.timeoutMs === null) {
@@ -28,7 +28,8 @@ class ControlStreamAbstract {
28
28
  this.token = token;
29
29
  }
30
30
  stopStream() {
31
- // do nothing for now
31
+ this.statusStream$.next(models_1.SubscriberStatusEnum.STOPPED);
32
+ this.statusStream$.complete();
32
33
  }
33
34
  takeActionWithAck(action, params, ackTimeoutInSeconds = 15) {
34
35
  return new rxjs_1.Observable((emitter) => {
@@ -27,7 +27,8 @@ class FileAccessorStreamAbstract {
27
27
  this.token = token;
28
28
  }
29
29
  stopStream() {
30
- // do nothing for now
30
+ this.statusStream$.next(models_1.SubscriberStatusEnum.STOPPED);
31
+ this.statusStream$.complete();
31
32
  }
32
33
  async serverReqOp(params) {
33
34
  return this.serverReqOpInternal(params);
@@ -28,7 +28,8 @@ class SearchStreamAbstract {
28
28
  this.token = token;
29
29
  }
30
30
  stopStream() {
31
- // do nothing for now
31
+ this.statusStream$.next(models_1.SubscriberStatusEnum.STOPPED);
32
+ this.statusStream$.complete();
32
33
  }
33
34
  searchStream(params) {
34
35
  const request = this.convertToQueryMessage(params.query);
@@ -24,6 +24,7 @@ class TelemetryStream extends TelemetryStreamAbstract_1.TelemetryStreamAbstract
24
24
  this.client = new teletubby_pb_client_1.TelemetryGatewayClient(transport);
25
25
  }
26
26
  stopInternal() {
27
+ this.subscriberId = undefined;
27
28
  if (this.abortController) {
28
29
  this.logger.info('Starting stop stream process');
29
30
  this.abortController.abort();
@@ -13,6 +13,33 @@ const arrayRemove_1 = require("../../../helpers/arrayRemove");
13
13
  const arrayUnique_1 = require("../../../helpers/arrayUnique");
14
14
  const operators_1 = require("rxjs/operators");
15
15
  const getSubscriptionsDifference_1 = require("../../../helpers/getSubscriptionsDifference");
16
+ const telemetryActionQueueStates = new WeakMap();
17
+ const resolveTelemetryActions = (actions) => {
18
+ actions.forEach((action) => action.resolve());
19
+ };
20
+ const resolveUnsentTelemetryActions = (queueState) => {
21
+ resolveTelemetryActions(queueState.pendingActions.splice(0));
22
+ };
23
+ const takeNextTelemetryBatch = (queueState) => {
24
+ const firstAction = queueState.pendingActions.shift();
25
+ if (!firstAction)
26
+ return [];
27
+ const activeBatch = [firstAction];
28
+ if (firstAction.callsignsLookup.isQuery() || firstAction.terminate)
29
+ return activeBatch;
30
+ while (queueState.pendingActions.length > 0) {
31
+ const [nextAction] = queueState.pendingActions;
32
+ if (nextAction.callsignsLookup.isQuery() ||
33
+ nextAction.terminate ||
34
+ nextAction.actionType !== firstAction.actionType ||
35
+ nextAction.subscriberId !== firstAction.subscriberId ||
36
+ nextAction.registration !== firstAction.registration) {
37
+ break;
38
+ }
39
+ activeBatch.push(queueState.pendingActions.shift());
40
+ }
41
+ return activeBatch;
42
+ };
16
43
  class TelemetryStreamAbstract {
17
44
  constructor(config) {
18
45
  this.telemetryActionQueue = Promise.resolve();
@@ -38,6 +65,14 @@ class TelemetryStreamAbstract {
38
65
  this.messageStream$ = new rxjs_1.Subject();
39
66
  this.statusStream$ = new rxjs_1.BehaviorSubject(models_1.SubscriberStatusEnum.STOPPED);
40
67
  this.heartbeat = new StreamHeartbeat_1.StreamHeartbeat(this.identifier);
68
+ telemetryActionQueueStates.set(this, {
69
+ pendingActions: [],
70
+ activeBatch: [],
71
+ registration: 0,
72
+ drainScheduled: false,
73
+ requestInFlight: false,
74
+ stopped: false,
75
+ });
41
76
  RocosStore_1.RocosStore.getChangeSubject()
42
77
  .pipe((0, operators_1.filter)((message) => {
43
78
  return message.type === 'token' && message.url === this.url;
@@ -55,6 +90,12 @@ class TelemetryStreamAbstract {
55
90
  this.registerReceiver();
56
91
  }
57
92
  stopStream() {
93
+ const queueState = telemetryActionQueueStates.get(this);
94
+ if (queueState) {
95
+ queueState.stopped = true;
96
+ resolveUnsentTelemetryActions(queueState);
97
+ }
98
+ this.subscriberId = undefined;
58
99
  this.heartbeat?.stop();
59
100
  this.stopInternal();
60
101
  if (this.messagesTimer) {
@@ -87,7 +128,9 @@ class TelemetryStreamAbstract {
87
128
  const toAdd = (0, arrayRemove_1.arrayRemove)(this.sources, sourcesBefore);
88
129
  if (toAdd.length) {
89
130
  this.logger.debug('New query subscriptions added, we need to refresh sources', { toAdd });
90
- void this.enqueueTelemetryAction('subscribe', this.callsignsLookup, toAdd);
131
+ void this.enqueueTelemetryAction('subscribe', this.callsignsLookup, toAdd).catch((error) => {
132
+ this.logger.error(`Failed to subscribe: ${error}`);
133
+ });
91
134
  }
92
135
  }
93
136
  else {
@@ -98,7 +141,9 @@ class TelemetryStreamAbstract {
98
141
  const { toAdd } = (0, getSubscriptionsDifference_1.getSubscriptionsDifference)(before, after);
99
142
  if (toAdd.callsigns.length && toAdd.sources.length) {
100
143
  this.logger.debug('New subscriptions added, we need to refresh sources', { toAdd });
101
- void this.enqueueTelemetryAction('subscribe', new models_1.CallsignsLookup(toAdd.callsigns), toAdd.sources);
144
+ void this.enqueueTelemetryAction('subscribe', new models_1.CallsignsLookup(toAdd.callsigns), toAdd.sources).catch((error) => {
145
+ this.logger.error(`Failed to subscribe: ${error}`);
146
+ });
102
147
  }
103
148
  }
104
149
  }
@@ -204,14 +249,22 @@ class TelemetryStreamAbstract {
204
249
  // Subscribe message
205
250
  case grpc_1.GRPC_SOURCE_SUBSCRIBED: {
206
251
  const { subscriberId } = rocosMessage.payload;
252
+ const queueState = telemetryActionQueueStates.get(this);
253
+ if (queueState?.stopped)
254
+ return;
207
255
  if (this.subscriberId && subscriberId) {
208
256
  this.logger.warn(`Replacing old subscriberId (${this.subscriberId}) with a new one (${subscriberId})`);
209
257
  }
210
- // Update the subscriberId
258
+ if (queueState) {
259
+ queueState.registration++;
260
+ resolveUnsentTelemetryActions(queueState);
261
+ }
211
262
  this.subscriberId = subscriberId;
212
263
  rocosMessage.subscriberId = this.subscriberId;
213
264
  this.logger.info('onData', `subscriberId has been updated - ${subscriberId}`, { subscriberId });
214
- void this.enqueueTelemetryAction('subscribe');
265
+ void this.enqueueTelemetryAction('subscribe').catch((error) => {
266
+ this.logger.error(`Failed to subscribe: ${error}`);
267
+ });
215
268
  this.heartbeat.start(() => this.sendHeartbeat(), this.sendHeartbeatTime);
216
269
  isRobotMessage = false;
217
270
  break;
@@ -253,21 +306,69 @@ class TelemetryStreamAbstract {
253
306
  * unsubscribe landing last strands a subscribe and nothing re-adds it.
254
307
  */
255
308
  enqueueTelemetryAction(actionType, callsignsLookup, sources, terminate) {
256
- const takeAction = () => this.takeTelemetryAction(actionType, callsignsLookup, sources, terminate);
257
- const run = this.telemetryActionQueue.then(takeAction);
258
- // The tail only signals whose turn it is, so it must never reject, otherwise one failed op
259
- // would wedge every op queued behind it. Callers still see the failure through `run`.
260
- this.telemetryActionQueue = run.then(() => undefined, () => undefined);
261
- return run;
309
+ const queueState = telemetryActionQueueStates.get(this);
310
+ if (!queueState || queueState.stopped)
311
+ return Promise.resolve();
312
+ const completion = new Promise((resolve, reject) => {
313
+ queueState.pendingActions.push({
314
+ actionType,
315
+ callsignsLookup: callsignsLookup ?? this.callsignsLookup,
316
+ sources: [...(sources ?? this.sources)],
317
+ terminate: terminate ?? false,
318
+ subscriberId: this.subscriberId,
319
+ registration: queueState.registration,
320
+ resolve,
321
+ reject,
322
+ });
323
+ });
324
+ const scheduleDrain = () => {
325
+ if (queueState.stopped ||
326
+ queueState.requestInFlight ||
327
+ queueState.drainScheduled ||
328
+ queueState.pendingActions.length === 0) {
329
+ return;
330
+ }
331
+ queueState.drainScheduled = true;
332
+ this.telemetryActionQueue = this.telemetryActionQueue.then(async () => {
333
+ queueState.drainScheduled = false;
334
+ if (queueState.stopped || queueState.requestInFlight)
335
+ return;
336
+ const activeBatch = takeNextTelemetryBatch(queueState);
337
+ const [firstAction] = activeBatch;
338
+ if (!firstAction)
339
+ return;
340
+ queueState.activeBatch = activeBatch;
341
+ queueState.requestInFlight = true;
342
+ try {
343
+ await this.takeTelemetryAction(firstAction.actionType, firstAction.callsignsLookup, firstAction.sources);
344
+ resolveTelemetryActions(activeBatch);
345
+ }
346
+ catch (error) {
347
+ activeBatch.forEach((action) => action.reject(error));
348
+ }
349
+ finally {
350
+ queueState.activeBatch = [];
351
+ queueState.requestInFlight = false;
352
+ scheduleDrain();
353
+ }
354
+ });
355
+ };
356
+ scheduleDrain();
357
+ return completion;
262
358
  }
263
- async takeTelemetryAction(actionType, callsignsLookup, sources, terminate) {
359
+ async takeTelemetryAction(actionType, callsignsLookup, sources) {
264
360
  this.logger.debug('takeTelemetryAction', actionType);
361
+ const queueState = telemetryActionQueueStates.get(this);
362
+ const activeBatch = queueState?.activeBatch ?? [];
363
+ const [firstAction] = activeBatch;
265
364
  // A config fault, not a race: this stream can never address a request.
266
365
  if (!this.projectId) {
267
366
  this.logger.warn('takeTelemetryAction', actionType, `${actionType} without a projectId - dropped`);
268
367
  return Promise.resolve();
269
368
  }
270
- if (!this.subscriberId) {
369
+ if (!firstAction?.subscriberId ||
370
+ firstAction.subscriberId !== this.subscriberId ||
371
+ firstAction.registration !== queueState?.registration) {
271
372
  // Deferred, not lost: `onData` re-asserts `this.sources` when the subscriberId arrives.
272
373
  this.logger.debug('takeTelemetryAction', actionType, `${actionType} before the receiver was registered - deferred to the post-registration re-assert`);
273
374
  return Promise.resolve();
@@ -275,20 +376,20 @@ class TelemetryStreamAbstract {
275
376
  const actionSources = sources ?? this.sources;
276
377
  const actionCallsigns = callsignsLookup ?? this.callsignsLookup;
277
378
  if (!actionCallsigns.isQuery()) {
278
- // Compose the request message
279
379
  const req = teletubby_pb_1.TelemetryRequest.create({
280
- subscriberId: this.subscriberId,
281
- requestedActions: [
282
- {
283
- operation: actionType,
284
- callsigns: actionCallsigns.getCallsigns(),
285
- sources: actionSources,
286
- },
287
- ],
380
+ subscriberId: firstAction.subscriberId,
381
+ requestedActions: activeBatch.map((action) => {
382
+ const telemetryAction = teletubby_pb_1.TelemetryAction.create({
383
+ operation: action.actionType,
384
+ callsigns: action.callsignsLookup.getCallsigns(),
385
+ sources: action.sources,
386
+ });
387
+ if (action.actionType === 'unsubscribe' && action.terminate) {
388
+ this.addTerminateToAction(telemetryAction);
389
+ }
390
+ return telemetryAction;
391
+ }),
288
392
  });
289
- if (actionType === 'unsubscribe' && terminate) {
290
- this.addTerminateToAction(req.requestedActions[0]);
291
- }
292
393
  this.logger.debug('takeTelemetryAction', actionType, 'client.requestTelemetry', this.subscriberId);
293
394
  // Send the message to back-end.
294
395
  return this.requestTelemetry(req);
@@ -313,7 +414,7 @@ class TelemetryStreamAbstract {
313
414
  });
314
415
  // Compose the request message
315
416
  const req = teletubby_pb_1.TelemetryQueryRequest.create({
316
- subscriberId: this.subscriberId,
417
+ subscriberId: firstAction.subscriberId,
317
418
  sources: actionSources,
318
419
  operation: actionType,
319
420
  callsignQuery: {
@@ -40,6 +40,7 @@ class TelemetryStreamConnect extends TelemetryStreamAbstract_1.TelemetryStreamAb
40
40
  this.client = (0, connect_1.createClient)(teletubby_pb_1.TelemetryGateway, transport);
41
41
  }
42
42
  stopInternal() {
43
+ this.subscriberId = undefined;
43
44
  if (this.abortController) {
44
45
  this.logger.info('Starting stop stream process');
45
46
  this.abortController.abort();
@@ -48,6 +48,7 @@ class TelemetryStreamNode extends TelemetryStreamAbstract_1.TelemetryStreamAbstr
48
48
  this.credentials = config.insecure ? grpc.credentials.createInsecure() : grpc.credentials.createSsl();
49
49
  }
50
50
  stopInternal() {
51
+ this.subscriberId = undefined;
51
52
  try {
52
53
  this.logger.info('Starting stop stream process');
53
54
  this.logger.info('Stopping stream');
@@ -28,7 +28,8 @@ class WebRTCSignallingStreamAbstract {
28
28
  this.token = token;
29
29
  }
30
30
  stopStream() {
31
- // do nothing for now
31
+ this.statusStream$.next(models_1.SubscriberStatusEnum.STOPPED);
32
+ this.statusStream$.complete();
32
33
  }
33
34
  /**
34
35
  * Connect operator
@@ -8,6 +8,6 @@ export interface Map {
8
8
  deployedCount: number;
9
9
  isUploading: boolean;
10
10
  isInCloud: boolean;
11
- hash: string;
11
+ hash?: string;
12
12
  lastModifiedTimestamp: number;
13
13
  }
@@ -1,7 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.SearchService = void 0;
4
- const models_1 = require("../models");
5
4
  const rxjs_1 = require("rxjs");
6
5
  const BaseStreamService_1 = require("./BaseStreamService");
7
6
  const identifier_1 = require("../constants/identifier");
@@ -47,12 +46,8 @@ class SearchService extends BaseStreamService_1.BaseStreamService {
47
46
  port: this.config.port,
48
47
  insecure: this.config.insecure,
49
48
  });
50
- if (newStream.isNew) {
51
- newStream.stream.statusStream$.subscribe((msg) => {
52
- this.status = msg === models_1.SubscriberStatusEnum.STOPPED || msg === models_1.SubscriberStatusEnum.ALIVE;
53
- this.status$.next(msg);
54
- });
55
- }
49
+ // The status forwarder is wired once per stream by BaseStreamService.acquireStream's factory
50
+ // (and released now that the stream completes statusStream$ on stop), so no per-service one here.
56
51
  return newStream.stream;
57
52
  }
58
53
  getStream(config) {
@@ -28,6 +28,10 @@ export declare class TelemetryService extends BaseStreamService<ITelemetryStream
28
28
  getStatusChanges(): Observable<IStreamStatusMessage>;
29
29
  /**
30
30
  * Subscribes to the heartbeat telemetry and checks we receive one at least every 5s
31
+ *
32
+ * @deprecated Superseded by {@link RobotStatusService.getRobotStatusChanges}, which reports the
33
+ * normalised robot status (reported health/readiness plus platform connectivity) rather than only
34
+ * heartbeat-derived online/offline liveness.
31
35
  * @param projectId
32
36
  * @param callsign
33
37
  * @param heartbeatTimeoutMs optionally override the deadline for heartbeats (in ms). Default 5000ms
@@ -88,8 +88,14 @@ class TelemetryService extends BaseStreamService_1.BaseStreamService {
88
88
  return (0, rxjs_1.from)(messages$).pipe((0, rxjs_1.mergeAll)(), (0, operators_1.filter)((message) => {
89
89
  return callsignsLookup.includesCallsign(message.callsign) && sources.includes(message.source);
90
90
  }), (0, operators_1.finalize)(() => {
91
- void stream.removeSubscription(subscriptionParams, params.terminateReceiverGroupOnUnsubscribe);
92
- release();
91
+ if (params.terminateReceiverGroupOnUnsubscribe) {
92
+ const removal = stream.removeSubscription(subscriptionParams, true);
93
+ void Promise.resolve(removal).finally(release);
94
+ }
95
+ else {
96
+ release();
97
+ void stream.removeSubscription(subscriptionParams);
98
+ }
93
99
  }));
94
100
  });
95
101
  }
@@ -111,6 +117,10 @@ class TelemetryService extends BaseStreamService_1.BaseStreamService {
111
117
  }
112
118
  /**
113
119
  * Subscribes to the heartbeat telemetry and checks we receive one at least every 5s
120
+ *
121
+ * @deprecated Superseded by {@link RobotStatusService.getRobotStatusChanges}, which reports the
122
+ * normalised robot status (reported health/readiness plus platform connectivity) rather than only
123
+ * heartbeat-derived online/offline liveness.
114
124
  * @param projectId
115
125
  * @param callsign
116
126
  * @param heartbeatTimeoutMs optionally override the deadline for heartbeats (in ms). Default 5000ms
@@ -79,13 +79,13 @@ export class StreamRegister {
79
79
  return;
80
80
  // Wait for init to settle before tearing down, so init can never open resources on an already
81
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(() => {
82
+ const evictIfUnheld = () => {
85
83
  if (this.entries.get(identifier) === entry && entry.refCount <= 0) {
86
84
  this.evict(identifier, entry.stream);
87
85
  }
88
- })
86
+ };
87
+ void entry.ready
88
+ .then(() => evictIfUnheld(), () => evictIfUnheld())
89
89
  .catch((e) => this.logger.error(`Failed to evict stream. identifier: ${identifier}`, e));
90
90
  }
91
91
  evict(identifier, stream) {
@@ -26,7 +26,8 @@ export class CallerStreamAbstract {
26
26
  this.token = token;
27
27
  }
28
28
  stopStream() {
29
- // do nothing for now
29
+ this.statusStream$.next(SubscriberStatusEnum.STOPPED);
30
+ this.statusStream$.complete();
30
31
  }
31
32
  invokeRequest(params) {
32
33
  this.logger.info('invokeRequest', params);
@@ -28,7 +28,8 @@ export class CommandStreamAbstract {
28
28
  this.token = token;
29
29
  }
30
30
  stopStream() {
31
- // do nothing for now
31
+ this.statusStream$.next(SubscriberStatusEnum.STOPPED);
32
+ this.statusStream$.complete();
32
33
  }
33
34
  invokeRequest(params) {
34
35
  if (params.timeoutMs === undefined || params.timeoutMs === null) {
@@ -25,7 +25,8 @@ export class ControlStreamAbstract {
25
25
  this.token = token;
26
26
  }
27
27
  stopStream() {
28
- // do nothing for now
28
+ this.statusStream$.next(SubscriberStatusEnum.STOPPED);
29
+ this.statusStream$.complete();
29
30
  }
30
31
  takeActionWithAck(action, params, ackTimeoutInSeconds = 15) {
31
32
  return new Observable((emitter) => {
@@ -24,7 +24,8 @@ export class FileAccessorStreamAbstract {
24
24
  this.token = token;
25
25
  }
26
26
  stopStream() {
27
- // do nothing for now
27
+ this.statusStream$.next(SubscriberStatusEnum.STOPPED);
28
+ this.statusStream$.complete();
28
29
  }
29
30
  async serverReqOp(params) {
30
31
  return this.serverReqOpInternal(params);
@@ -25,7 +25,8 @@ export class SearchStreamAbstract {
25
25
  this.token = token;
26
26
  }
27
27
  stopStream() {
28
- // do nothing for now
28
+ this.statusStream$.next(SubscriberStatusEnum.STOPPED);
29
+ this.statusStream$.complete();
29
30
  }
30
31
  searchStream(params) {
31
32
  const request = this.convertToQueryMessage(params.query);
@@ -21,6 +21,7 @@ export class TelemetryStream extends TelemetryStreamAbstract {
21
21
  this.client = new TelemetryGatewayClient(transport);
22
22
  }
23
23
  stopInternal() {
24
+ this.subscriberId = undefined;
24
25
  if (this.abortController) {
25
26
  this.logger.info('Starting stop stream process');
26
27
  this.abortController.abort();
@@ -1,7 +1,7 @@
1
1
  import { BehaviorSubject, Subject } from 'rxjs';
2
2
  import { CallsignsLookup, RocosTelemetryMessage, StreamOptions, SubscriberStatusEnum, } from '../../../models';
3
3
  import { GRPC_SOURCE_NOOP, GRPC_SOURCE_SUBSCRIBED } from '../../../constants/grpc';
4
- import { QueryOrPredicate, TelemetryAckStatus, TelemetryQueryRequest, TelemetryRequest, UnsubscribeOperation, } from '../../../grpc/teletubby_pb';
4
+ import { QueryOrPredicate, TelemetryAckStatus, TelemetryAction, TelemetryQueryRequest, TelemetryRequest, UnsubscribeOperation, } from '../../../grpc/teletubby_pb';
5
5
  import { IDENTIFIER_NAME_TELEMETRY } from '../../../constants/identifier';
6
6
  import { RocosStore } from '../../../store/RocosStore';
7
7
  import { StreamHeartbeat } from '../../atoms/StreamHeartbeat';
@@ -10,6 +10,33 @@ import { arrayRemove } from '../../../helpers/arrayRemove';
10
10
  import { arrayUnique } from '../../../helpers/arrayUnique';
11
11
  import { filter } from 'rxjs/operators';
12
12
  import { getSubscriptionsDifference } from '../../../helpers/getSubscriptionsDifference';
13
+ const telemetryActionQueueStates = new WeakMap();
14
+ const resolveTelemetryActions = (actions) => {
15
+ actions.forEach((action) => action.resolve());
16
+ };
17
+ const resolveUnsentTelemetryActions = (queueState) => {
18
+ resolveTelemetryActions(queueState.pendingActions.splice(0));
19
+ };
20
+ const takeNextTelemetryBatch = (queueState) => {
21
+ const firstAction = queueState.pendingActions.shift();
22
+ if (!firstAction)
23
+ return [];
24
+ const activeBatch = [firstAction];
25
+ if (firstAction.callsignsLookup.isQuery() || firstAction.terminate)
26
+ return activeBatch;
27
+ while (queueState.pendingActions.length > 0) {
28
+ const [nextAction] = queueState.pendingActions;
29
+ if (nextAction.callsignsLookup.isQuery() ||
30
+ nextAction.terminate ||
31
+ nextAction.actionType !== firstAction.actionType ||
32
+ nextAction.subscriberId !== firstAction.subscriberId ||
33
+ nextAction.registration !== firstAction.registration) {
34
+ break;
35
+ }
36
+ activeBatch.push(queueState.pendingActions.shift());
37
+ }
38
+ return activeBatch;
39
+ };
13
40
  export class TelemetryStreamAbstract {
14
41
  constructor(config) {
15
42
  this.telemetryActionQueue = Promise.resolve();
@@ -35,6 +62,14 @@ export class TelemetryStreamAbstract {
35
62
  this.messageStream$ = new Subject();
36
63
  this.statusStream$ = new BehaviorSubject(SubscriberStatusEnum.STOPPED);
37
64
  this.heartbeat = new StreamHeartbeat(this.identifier);
65
+ telemetryActionQueueStates.set(this, {
66
+ pendingActions: [],
67
+ activeBatch: [],
68
+ registration: 0,
69
+ drainScheduled: false,
70
+ requestInFlight: false,
71
+ stopped: false,
72
+ });
38
73
  RocosStore.getChangeSubject()
39
74
  .pipe(filter((message) => {
40
75
  return message.type === 'token' && message.url === this.url;
@@ -52,6 +87,12 @@ export class TelemetryStreamAbstract {
52
87
  this.registerReceiver();
53
88
  }
54
89
  stopStream() {
90
+ const queueState = telemetryActionQueueStates.get(this);
91
+ if (queueState) {
92
+ queueState.stopped = true;
93
+ resolveUnsentTelemetryActions(queueState);
94
+ }
95
+ this.subscriberId = undefined;
55
96
  this.heartbeat?.stop();
56
97
  this.stopInternal();
57
98
  if (this.messagesTimer) {
@@ -84,7 +125,9 @@ export class TelemetryStreamAbstract {
84
125
  const toAdd = arrayRemove(this.sources, sourcesBefore);
85
126
  if (toAdd.length) {
86
127
  this.logger.debug('New query subscriptions added, we need to refresh sources', { toAdd });
87
- void this.enqueueTelemetryAction('subscribe', this.callsignsLookup, toAdd);
128
+ void this.enqueueTelemetryAction('subscribe', this.callsignsLookup, toAdd).catch((error) => {
129
+ this.logger.error(`Failed to subscribe: ${error}`);
130
+ });
88
131
  }
89
132
  }
90
133
  else {
@@ -95,7 +138,9 @@ export class TelemetryStreamAbstract {
95
138
  const { toAdd } = getSubscriptionsDifference(before, after);
96
139
  if (toAdd.callsigns.length && toAdd.sources.length) {
97
140
  this.logger.debug('New subscriptions added, we need to refresh sources', { toAdd });
98
- void this.enqueueTelemetryAction('subscribe', new CallsignsLookup(toAdd.callsigns), toAdd.sources);
141
+ void this.enqueueTelemetryAction('subscribe', new CallsignsLookup(toAdd.callsigns), toAdd.sources).catch((error) => {
142
+ this.logger.error(`Failed to subscribe: ${error}`);
143
+ });
99
144
  }
100
145
  }
101
146
  }
@@ -201,14 +246,22 @@ export class TelemetryStreamAbstract {
201
246
  // Subscribe message
202
247
  case GRPC_SOURCE_SUBSCRIBED: {
203
248
  const { subscriberId } = rocosMessage.payload;
249
+ const queueState = telemetryActionQueueStates.get(this);
250
+ if (queueState?.stopped)
251
+ return;
204
252
  if (this.subscriberId && subscriberId) {
205
253
  this.logger.warn(`Replacing old subscriberId (${this.subscriberId}) with a new one (${subscriberId})`);
206
254
  }
207
- // Update the subscriberId
255
+ if (queueState) {
256
+ queueState.registration++;
257
+ resolveUnsentTelemetryActions(queueState);
258
+ }
208
259
  this.subscriberId = subscriberId;
209
260
  rocosMessage.subscriberId = this.subscriberId;
210
261
  this.logger.info('onData', `subscriberId has been updated - ${subscriberId}`, { subscriberId });
211
- void this.enqueueTelemetryAction('subscribe');
262
+ void this.enqueueTelemetryAction('subscribe').catch((error) => {
263
+ this.logger.error(`Failed to subscribe: ${error}`);
264
+ });
212
265
  this.heartbeat.start(() => this.sendHeartbeat(), this.sendHeartbeatTime);
213
266
  isRobotMessage = false;
214
267
  break;
@@ -250,21 +303,69 @@ export class TelemetryStreamAbstract {
250
303
  * unsubscribe landing last strands a subscribe and nothing re-adds it.
251
304
  */
252
305
  enqueueTelemetryAction(actionType, callsignsLookup, sources, terminate) {
253
- const takeAction = () => this.takeTelemetryAction(actionType, callsignsLookup, sources, terminate);
254
- const run = this.telemetryActionQueue.then(takeAction);
255
- // The tail only signals whose turn it is, so it must never reject, otherwise one failed op
256
- // would wedge every op queued behind it. Callers still see the failure through `run`.
257
- this.telemetryActionQueue = run.then(() => undefined, () => undefined);
258
- return run;
306
+ const queueState = telemetryActionQueueStates.get(this);
307
+ if (!queueState || queueState.stopped)
308
+ return Promise.resolve();
309
+ const completion = new Promise((resolve, reject) => {
310
+ queueState.pendingActions.push({
311
+ actionType,
312
+ callsignsLookup: callsignsLookup ?? this.callsignsLookup,
313
+ sources: [...(sources ?? this.sources)],
314
+ terminate: terminate ?? false,
315
+ subscriberId: this.subscriberId,
316
+ registration: queueState.registration,
317
+ resolve,
318
+ reject,
319
+ });
320
+ });
321
+ const scheduleDrain = () => {
322
+ if (queueState.stopped ||
323
+ queueState.requestInFlight ||
324
+ queueState.drainScheduled ||
325
+ queueState.pendingActions.length === 0) {
326
+ return;
327
+ }
328
+ queueState.drainScheduled = true;
329
+ this.telemetryActionQueue = this.telemetryActionQueue.then(async () => {
330
+ queueState.drainScheduled = false;
331
+ if (queueState.stopped || queueState.requestInFlight)
332
+ return;
333
+ const activeBatch = takeNextTelemetryBatch(queueState);
334
+ const [firstAction] = activeBatch;
335
+ if (!firstAction)
336
+ return;
337
+ queueState.activeBatch = activeBatch;
338
+ queueState.requestInFlight = true;
339
+ try {
340
+ await this.takeTelemetryAction(firstAction.actionType, firstAction.callsignsLookup, firstAction.sources);
341
+ resolveTelemetryActions(activeBatch);
342
+ }
343
+ catch (error) {
344
+ activeBatch.forEach((action) => action.reject(error));
345
+ }
346
+ finally {
347
+ queueState.activeBatch = [];
348
+ queueState.requestInFlight = false;
349
+ scheduleDrain();
350
+ }
351
+ });
352
+ };
353
+ scheduleDrain();
354
+ return completion;
259
355
  }
260
- async takeTelemetryAction(actionType, callsignsLookup, sources, terminate) {
356
+ async takeTelemetryAction(actionType, callsignsLookup, sources) {
261
357
  this.logger.debug('takeTelemetryAction', actionType);
358
+ const queueState = telemetryActionQueueStates.get(this);
359
+ const activeBatch = queueState?.activeBatch ?? [];
360
+ const [firstAction] = activeBatch;
262
361
  // A config fault, not a race: this stream can never address a request.
263
362
  if (!this.projectId) {
264
363
  this.logger.warn('takeTelemetryAction', actionType, `${actionType} without a projectId - dropped`);
265
364
  return Promise.resolve();
266
365
  }
267
- if (!this.subscriberId) {
366
+ if (!firstAction?.subscriberId ||
367
+ firstAction.subscriberId !== this.subscriberId ||
368
+ firstAction.registration !== queueState?.registration) {
268
369
  // Deferred, not lost: `onData` re-asserts `this.sources` when the subscriberId arrives.
269
370
  this.logger.debug('takeTelemetryAction', actionType, `${actionType} before the receiver was registered - deferred to the post-registration re-assert`);
270
371
  return Promise.resolve();
@@ -272,20 +373,20 @@ export class TelemetryStreamAbstract {
272
373
  const actionSources = sources ?? this.sources;
273
374
  const actionCallsigns = callsignsLookup ?? this.callsignsLookup;
274
375
  if (!actionCallsigns.isQuery()) {
275
- // Compose the request message
276
376
  const req = TelemetryRequest.create({
277
- subscriberId: this.subscriberId,
278
- requestedActions: [
279
- {
280
- operation: actionType,
281
- callsigns: actionCallsigns.getCallsigns(),
282
- sources: actionSources,
283
- },
284
- ],
377
+ subscriberId: firstAction.subscriberId,
378
+ requestedActions: activeBatch.map((action) => {
379
+ const telemetryAction = TelemetryAction.create({
380
+ operation: action.actionType,
381
+ callsigns: action.callsignsLookup.getCallsigns(),
382
+ sources: action.sources,
383
+ });
384
+ if (action.actionType === 'unsubscribe' && action.terminate) {
385
+ this.addTerminateToAction(telemetryAction);
386
+ }
387
+ return telemetryAction;
388
+ }),
285
389
  });
286
- if (actionType === 'unsubscribe' && terminate) {
287
- this.addTerminateToAction(req.requestedActions[0]);
288
- }
289
390
  this.logger.debug('takeTelemetryAction', actionType, 'client.requestTelemetry', this.subscriberId);
290
391
  // Send the message to back-end.
291
392
  return this.requestTelemetry(req);
@@ -310,7 +411,7 @@ export class TelemetryStreamAbstract {
310
411
  });
311
412
  // Compose the request message
312
413
  const req = TelemetryQueryRequest.create({
313
- subscriberId: this.subscriberId,
414
+ subscriberId: firstAction.subscriberId,
314
415
  sources: actionSources,
315
416
  operation: actionType,
316
417
  callsignQuery: {
@@ -37,6 +37,7 @@ export class TelemetryStreamConnect extends TelemetryStreamAbstract {
37
37
  this.client = createClient(TelemetryGateway, transport);
38
38
  }
39
39
  stopInternal() {
40
+ this.subscriberId = undefined;
40
41
  if (this.abortController) {
41
42
  this.logger.info('Starting stop stream process');
42
43
  this.abortController.abort();
@@ -12,6 +12,7 @@ export class TelemetryStreamNode extends TelemetryStreamAbstract {
12
12
  this.credentials = config.insecure ? grpc.credentials.createInsecure() : grpc.credentials.createSsl();
13
13
  }
14
14
  stopInternal() {
15
+ this.subscriberId = undefined;
15
16
  try {
16
17
  this.logger.info('Starting stop stream process');
17
18
  this.logger.info('Stopping stream');
@@ -25,7 +25,8 @@ export class WebRTCSignallingStreamAbstract {
25
25
  this.token = token;
26
26
  }
27
27
  stopStream() {
28
- // do nothing for now
28
+ this.statusStream$.next(SubscriberStatusEnum.STOPPED);
29
+ this.statusStream$.complete();
29
30
  }
30
31
  /**
31
32
  * Connect operator
@@ -8,6 +8,6 @@ export interface Map {
8
8
  deployedCount: number;
9
9
  isUploading: boolean;
10
10
  isInCloud: boolean;
11
- hash: string;
11
+ hash?: string;
12
12
  lastModifiedTimestamp: number;
13
13
  }
@@ -1,4 +1,3 @@
1
- import { SubscriberStatusEnum, } from '../models';
2
1
  import { Subject } from 'rxjs';
3
2
  import { BaseStreamService } from './BaseStreamService';
4
3
  import { IDENTIFIER_NAME_SEARCH } from '../constants/identifier';
@@ -44,12 +43,8 @@ export class SearchService extends BaseStreamService {
44
43
  port: this.config.port,
45
44
  insecure: this.config.insecure,
46
45
  });
47
- if (newStream.isNew) {
48
- newStream.stream.statusStream$.subscribe((msg) => {
49
- this.status = msg === SubscriberStatusEnum.STOPPED || msg === SubscriberStatusEnum.ALIVE;
50
- this.status$.next(msg);
51
- });
52
- }
46
+ // The status forwarder is wired once per stream by BaseStreamService.acquireStream's factory
47
+ // (and released now that the stream completes statusStream$ on stop), so no per-service one here.
53
48
  return newStream.stream;
54
49
  }
55
50
  getStream(config) {
@@ -28,6 +28,10 @@ export declare class TelemetryService extends BaseStreamService<ITelemetryStream
28
28
  getStatusChanges(): Observable<IStreamStatusMessage>;
29
29
  /**
30
30
  * Subscribes to the heartbeat telemetry and checks we receive one at least every 5s
31
+ *
32
+ * @deprecated Superseded by {@link RobotStatusService.getRobotStatusChanges}, which reports the
33
+ * normalised robot status (reported health/readiness plus platform connectivity) rather than only
34
+ * heartbeat-derived online/offline liveness.
31
35
  * @param projectId
32
36
  * @param callsign
33
37
  * @param heartbeatTimeoutMs optionally override the deadline for heartbeats (in ms). Default 5000ms
@@ -85,8 +85,14 @@ export class TelemetryService extends BaseStreamService {
85
85
  return from(messages$).pipe(mergeAll(), filter((message) => {
86
86
  return callsignsLookup.includesCallsign(message.callsign) && sources.includes(message.source);
87
87
  }), finalize(() => {
88
- void stream.removeSubscription(subscriptionParams, params.terminateReceiverGroupOnUnsubscribe);
89
- release();
88
+ if (params.terminateReceiverGroupOnUnsubscribe) {
89
+ const removal = stream.removeSubscription(subscriptionParams, true);
90
+ void Promise.resolve(removal).finally(release);
91
+ }
92
+ else {
93
+ release();
94
+ void stream.removeSubscription(subscriptionParams);
95
+ }
90
96
  }));
91
97
  });
92
98
  }
@@ -108,6 +114,10 @@ export class TelemetryService extends BaseStreamService {
108
114
  }
109
115
  /**
110
116
  * Subscribes to the heartbeat telemetry and checks we receive one at least every 5s
117
+ *
118
+ * @deprecated Superseded by {@link RobotStatusService.getRobotStatusChanges}, which reports the
119
+ * normalised robot status (reported health/readiness plus platform connectivity) rather than only
120
+ * heartbeat-derived online/offline liveness.
111
121
  * @param projectId
112
122
  * @param callsign
113
123
  * @param heartbeatTimeoutMs optionally override the deadline for heartbeats (in ms). Default 5000ms
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dronedeploy/rocos-js-sdk",
3
- "version": "4.4.6",
3
+ "version": "4.4.7",
4
4
  "description": "Javascript SDK for rocos",
5
5
  "main": "cjs/index.js",
6
6
  "module": "esm/index.js",