@webex/internal-plugin-locus 2.59.3-next.1 → 2.59.4

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.
package/src/locus.js CHANGED
@@ -1,672 +1,672 @@
1
- /*!
2
- * Copyright (c) 2015-2020 Cisco Systems, Inc. See LICENSE file.
3
- */
4
-
5
- import {WebexPlugin, WebexHttpError} from '@webex/webex-core';
6
- import {cloneDeep, difference, first, last, memoize} from 'lodash';
7
- import uuid from 'uuid';
8
-
9
- export const USE_INCOMING = 'USE_INCOMING';
10
- export const USE_CURRENT = 'USE_CURRENT';
11
- export const EQUAL = 'EQUAL';
12
- export const FETCH = 'FETCH';
13
- export const GREATER_THAN = 'GREATER_THAN';
14
- export const LESS_THAN = 'LESS_THAN';
15
- export const DESYNC = 'DESYNC';
16
-
17
- /**
18
- * Transates the result of a sequence comparison into an intended behavior
19
- * @param {string} result
20
- * @private
21
- * @returns {string}
22
- */
23
- function compareToAction(result) {
24
- switch (result) {
25
- case EQUAL:
26
- case GREATER_THAN:
27
- return USE_CURRENT;
28
- case LESS_THAN:
29
- return USE_INCOMING;
30
- case DESYNC:
31
- return FETCH;
32
- default:
33
- throw new Error(`${result} is not a recognized sequence comparison result`);
34
- }
35
- }
36
-
37
- /**
38
- * @class
39
- */
40
- const Locus = WebexPlugin.extend({
41
- namespace: 'Locus',
42
-
43
- /**
44
- * Alert the specified locus that the local user has been notified of the
45
- * locus's active state
46
- * @instance
47
- * @memberof Locus
48
- * @param {Types~Locus} locus
49
- * @returns {Promise}
50
- */
51
- alert(locus) {
52
- return this.request({
53
- method: 'PUT',
54
- uri: `${locus.url}/participant/alert`,
55
- body: {
56
- deviceUrl: this.webex.internal.device.url,
57
- sequence: locus.sequence,
58
- },
59
- }).then((res) => res.body);
60
- },
61
-
62
- /**
63
- * Compares two loci to determine which one contains the most recent state
64
- * @instance
65
- * @memberof Locus
66
- * @param {Types~Locus} current
67
- * @param {Types~Locus} incoming
68
- * @returns {string} one of USE_INCOMING, USE_CURRENT, or FETCH
69
- */
70
- compare(current, incoming) {
71
- /**
72
- * Determines if a paricular locus's sequence is empty
73
- * @param {Types~Locus} locus
74
- * @private
75
- * @returns {bool}
76
- */
77
- function isEmpty(locus) {
78
- const {sequence} = locus;
79
-
80
- return (
81
- (!sequence.entries || !sequence.entries.length) &&
82
- sequence.rangeStart === 0 &&
83
- sequence.rangeEnd === 0
84
- );
85
- }
86
-
87
- if (isEmpty(current) || isEmpty(incoming)) {
88
- return USE_INCOMING;
89
- }
90
-
91
- if (incoming.baseSequence) {
92
- return this.compareDelta(current, incoming);
93
- }
94
-
95
- return compareToAction(this.compareSequence(current.sequence, incoming.sequence));
96
- },
97
-
98
- /**
99
- * Compares two loci sequences (with delta params) and indicates what action
100
- * to take.
101
- * @instance
102
- * @memberof Locus
103
- * @param {Types~Locus} current
104
- * @param {Types~Locus} incoming
105
- * @private
106
- * @returns {string} one of USE_INCOMING, USE_CURRENT, or FETCH
107
- */
108
- compareDelta(current, incoming) {
109
- let ret = this.compareSequence(current.sequence, incoming.sequence);
110
-
111
- if (ret !== LESS_THAN) {
112
- return compareToAction(ret);
113
- }
114
-
115
- ret = this.compareSequence(current.sequence, incoming.baseSequence);
116
-
117
- switch (ret) {
118
- case GREATER_THAN:
119
- case EQUAL:
120
- return USE_INCOMING;
121
- default:
122
- return FETCH;
123
- }
124
- },
125
-
126
- /**
127
- * Compares two Locus sequences
128
- * @instance
129
- * @memberof Locus
130
- * @param {LocusSequence} current
131
- * @param {LocusSequence} incoming
132
- * @returns {string} one of LESS_THAN, GREATER_THAN, EQUAL, or DESYNC
133
- */
134
- compareSequence(current, incoming) {
135
- if (!current) {
136
- throw new Error('`current` is required');
137
- }
138
-
139
- if (!incoming) {
140
- throw new Error('`incoming` is required');
141
- }
142
- // complexity here is unavoidable
143
- /* eslint complexity: [0] */
144
- /* eslint max-statements: [0] */
145
-
146
- // must pick one of arrow-body-style or no-confusing-arrow to disable
147
- /* eslint arrow-body-style: [0] */
148
-
149
- // after running the #compare() test suite in a loop, there doesn't seem to
150
- // be any appreciable difference when used with or without memoize; since
151
- // real locus sequences are likely to contain more sequence numbers than
152
- // those in the test suite, I have to assume memoize can only help and the
153
- // overhead of memoizing these methods is not a problem.
154
-
155
- const getEntriesFirstValue = memoize((sequence) => {
156
- return sequence.entries.length === 0 ? 0 : first(sequence.entries);
157
- });
158
- const getEntriesLastValue = memoize((sequence) => {
159
- return sequence.entries.length === 0 ? 0 : last(sequence.entries);
160
- });
161
- const getCompareFirstValue = memoize((sequence) => {
162
- return sequence.rangeStart || getEntriesFirstValue(sequence);
163
- });
164
- const getCompareLastValue = memoize((sequence) => {
165
- return getEntriesLastValue(sequence) || sequence.rangeEnd;
166
- });
167
-
168
- /**
169
- * @param {number} entry
170
- * @param {LocusSequence} sequence
171
- * @private
172
- * @returns {Boolean}
173
- */
174
- function inRange(entry, sequence) {
175
- return entry >= sequence.rangeStart && entry <= sequence.rangeEnd;
176
- }
177
-
178
- if (getCompareFirstValue(current) > getCompareLastValue(incoming)) {
179
- return GREATER_THAN;
180
- }
181
-
182
- if (getCompareLastValue(current) < getCompareFirstValue(incoming)) {
183
- return LESS_THAN;
184
- }
185
-
186
- const currentOnlyEntries = difference(current.entries, incoming.entries);
187
- const incomingOnlyEntries = difference(incoming.entries, current.entries);
188
- const currentOnly = [];
189
- const incomingOnly = [];
190
-
191
- for (const i of currentOnlyEntries) {
192
- if (!inRange(i, incoming)) {
193
- currentOnly.push(i);
194
- }
195
- }
196
- for (const i of incomingOnlyEntries) {
197
- if (!inRange(i, current)) {
198
- incomingOnly.push(i);
199
- }
200
- }
201
-
202
- if (!currentOnly.length && !incomingOnly.length) {
203
- if (
204
- current.rangeEnd - getCompareFirstValue(current) >
205
- incoming.rangeEnd - getCompareFirstValue(incoming)
206
- ) {
207
- return GREATER_THAN;
208
- }
209
-
210
- if (
211
- current.rangeEnd - getCompareFirstValue(current) <
212
- incoming.rangeEnd - getCompareFirstValue(incoming)
213
- ) {
214
- return LESS_THAN;
215
- }
216
-
217
- return EQUAL;
218
- }
219
-
220
- if (currentOnly.length && !incomingOnly.length) {
221
- return GREATER_THAN;
222
- }
223
-
224
- if (!currentOnly.length && incomingOnly.length) {
225
- return LESS_THAN;
226
- }
227
-
228
- if (!current.rangeStart && !current.rangeEnd && !incoming.rangeStart && !incoming.rangeEnd) {
229
- return DESYNC;
230
- }
231
-
232
- for (const i of currentOnly) {
233
- if (getCompareFirstValue(incoming) < i && i < getCompareLastValue(incoming)) {
234
- return DESYNC;
235
- }
236
- }
237
-
238
- for (const i of incomingOnly) {
239
- if (getCompareFirstValue(current) < i && i < getCompareLastValue(current)) {
240
- return DESYNC;
241
- }
242
- }
243
-
244
- if (currentOnly[0] > incomingOnly[0]) {
245
- return GREATER_THAN;
246
- }
247
-
248
- return LESS_THAN;
249
- },
250
-
251
- /**
252
- * Calls the specified invitee and offers the specified media via
253
- * options.localSdp
254
- * @instance
255
- * @memberof Locus
256
- * @param {string} invitee
257
- * @param {Object} options
258
- * @param {Object} options.localSdp
259
- * @returns {Promise<Types~Locus>}
260
- */
261
- create(invitee, options = {}) {
262
- const {correlationId} = options;
263
-
264
- if (!correlationId) {
265
- throw new Error('options.correlationId is required');
266
- }
267
-
268
- return (
269
- this.request({
270
- method: 'POST',
271
- service: 'locus',
272
- resource: 'loci/call',
273
- body: {
274
- correlationId,
275
- deviceUrl: this.webex.internal.device.url,
276
- invitee: {
277
- invitee,
278
- },
279
- localMedias: [
280
- {
281
- localSdp: JSON.stringify({
282
- type: 'SDP',
283
- sdp: options.localSdp,
284
- }),
285
- },
286
- ],
287
- sequence: {
288
- entries: [],
289
- rangeStart: 0,
290
- rangeEnd: 0,
291
- },
292
- },
293
- })
294
- // res.body.mediaConnections is deprecated so just return the locus
295
- .then((res) => {
296
- res.body.locus.self.devices.map((item, index) => {
297
- item.mediaConnections = [res.body.mediaConnections[index]];
298
-
299
- return item;
300
- });
301
-
302
- return res.body.locus;
303
- })
304
- );
305
- },
306
-
307
- /**
308
- * This is mostly an internal function to simplify the phone plugin. Decides
309
- * which path to call based on the type of the thing being joined.
310
- * @instance
311
- * @memberof Locus
312
- * @param {Object|Types~Locus} target
313
- * @param {Object} options
314
- * @private
315
- * @returns {Promise<Types~Locus>}
316
- */
317
- createOrJoin(target, options) {
318
- if (target.url) {
319
- return this.join(target, options);
320
- }
321
-
322
- return this.create(target, options);
323
- },
324
-
325
- /**
326
- * Decline to join the specified Locus
327
- * @instance
328
- * @memberof Locus
329
- * @param {Types~Locus} locus
330
- * @returns {Promise<Types~Locus>}
331
- */
332
- decline(locus) {
333
- return this.request({
334
- method: 'PUT',
335
- uri: `${locus.url}/participant/decline`,
336
- body: {
337
- deviceUrl: this.webex.internal.device.url,
338
- sequence: locus.sequence,
339
- },
340
- })
341
- .then((res) => res.body)
342
- .catch((reason) => {
343
- if (reason instanceof WebexHttpError.Conflict) {
344
- return this.get(locus);
345
- }
346
-
347
- return Promise.reject(reason);
348
- });
349
- },
350
-
351
- /**
352
- * Retrieves a single Locus
353
- * @instance
354
- * @memberof Locus
355
- * @param {Types~Locus} locus
356
- * @returns {Types~Locus}
357
- */
358
- get(locus) {
359
- return this.request({
360
- method: 'GET',
361
- uri: `${locus.url}`,
362
- }).then((res) => res.body);
363
- },
364
-
365
- /**
366
- * Retrieves the call history for the current user
367
- * @instance
368
- * @memberof Locus
369
- * @param {Object} options
370
- * @param {Date|number} options.from
371
- * @returns {Promise<Object>}
372
- */
373
- getCallHistory(options = {}) {
374
- const from = new Date(options.from || Date.now()).toISOString();
375
-
376
- return this.request({
377
- method: 'GET',
378
- service: 'janus',
379
- resource: 'history/userSessions',
380
- qs: {from},
381
- }).then((res) => res.body);
382
- },
383
-
384
- /**
385
- * Join the specified Locus and offer to send it media
386
- * @instance
387
- * @memberof Locus
388
- * @param {Types~Locus} locus
389
- * @param {Object} options
390
- * @param {Object} options.localSdp
391
- * @returns {Types~Locus}
392
- */
393
- join(locus, options = {}) {
394
- const correlationId = locus.correlationId || options.correlationId;
395
-
396
- if (!correlationId) {
397
- throw new Error('locus.correlationId or options.correlationId is required');
398
- }
399
-
400
- return (
401
- this.request({
402
- method: 'POST',
403
- uri: `${locus.url}/participant`,
404
- body: {
405
- correlationId,
406
- deviceUrl: this.webex.internal.device.url,
407
- localMedias: [
408
- {
409
- localSdp: JSON.stringify({
410
- type: 'SDP',
411
- sdp: options.localSdp,
412
- }),
413
- },
414
- ],
415
- sequence: locus.sequence || {
416
- entries: [],
417
- rangeStart: 0,
418
- rangeEnd: 0,
419
- },
420
- },
421
- })
422
- // The mediaConnections object is deprecated, so just return the locus
423
- .then((res) => {
424
- res.body.locus.self.devices.map((item, index) => {
425
- item.mediaConnections = [res.body.mediaConnections[index]];
426
-
427
- return item;
428
- });
429
-
430
- return res.body.locus;
431
- })
432
- );
433
- },
434
-
435
- /**
436
- * Leave the specified Locus
437
- * @instance
438
- * @memberof Locus
439
- * @param {Types~Locus} locus
440
- * @returns {Promise<Types~Locus>}
441
- */
442
- leave(locus) {
443
- return this.request({
444
- method: 'PUT',
445
- uri: `${locus.self.url}/leave`,
446
- body: {
447
- deviceUrl: this.webex.internal.device.url,
448
- sequence: locus.sequence,
449
- },
450
- })
451
- .then((res) => res.body.locus)
452
- .catch((reason) => {
453
- if (reason instanceof WebexHttpError.Conflict) {
454
- return this.get(locus);
455
- }
456
-
457
- return Promise.reject(reason);
458
- });
459
- },
460
-
461
- /**
462
- * Lists active loci
463
- * @instance
464
- * @memberof Locus
465
- * @returns {Promise<Array<Types~Locus>>}
466
- */
467
- list() {
468
- return this.request({
469
- method: 'GET',
470
- service: 'locus',
471
- resource: 'loci',
472
- }).then((res) => res.body.loci);
473
- },
474
-
475
- /**
476
- * Merges two locus DTOs (for the same locus)
477
- * @instance
478
- * @memberof Locus
479
- * @param {Types~Locus} current
480
- * @param {Types~Locus|Types~LocusDelta} incoming
481
- * @returns {Type~Locus}
482
- */
483
- merge(current, incoming) {
484
- // if incoming is not a delta event, treat it as a new full locus.
485
- if (!incoming.baseSequence) {
486
- return incoming;
487
- }
488
-
489
- const next = cloneDeep(current);
490
-
491
- // 1. All non-null elements in the delta event except the "baseSequence" and
492
- // the "participants" collection should be used to replace their existing
493
- // values.
494
- Object.keys(incoming).forEach((key) => {
495
- if (key === 'baseSequence' || key === 'participants') {
496
- return;
497
- }
498
-
499
- next[key] = incoming[key] || next[key];
500
- });
501
-
502
- // 2. The "baseSequence" in the delta event can be discarded (it doesn't
503
- // need to be maintained in the local working copy).
504
-
505
- if (incoming.participants || incoming.participants.length) {
506
- const toRemove = new Set();
507
- const toUpsert = new Map();
508
-
509
- incoming.participants.forEach((p) => {
510
- if (p.removed) {
511
- // Elements of the delta event's "participants" list with the
512
- // attribute `removed=true` should be removed from the working copy's
513
- // "participants" collection.
514
- toRemove.add(p.url);
515
- } else {
516
- // Elements of the delta events "participants" list that are absent
517
- // from the local working copy should be added to that collection.
518
- toUpsert.set(p.url, p);
519
- }
520
- });
521
-
522
- // The "participants" collection in the delta event should be merged with
523
- // that of the local working copy of the Locus such that elements in the
524
- // delta event's "participants" replace those with the same url value in
525
- // the working copy "participants" collection.
526
- const participants = next.participants.reduce((acc, p) => {
527
- if (!toRemove.has(p.url)) {
528
- acc[p.url] = p;
529
- }
530
-
531
- return acc;
532
- }, {});
533
-
534
- toUpsert.forEach((value, key) => {
535
- participants[key] = value;
536
- });
537
-
538
- next.participants = Object.values(participants);
539
- }
540
-
541
- return next;
542
- },
543
-
544
- /**
545
- * Signals to locus that the current user is done sharing their additional
546
- * media stream
547
- * @param {Types~Locus} locus
548
- * @param {Types~MediaShare} share
549
- * @returns {Promise}
550
- */
551
- releaseFloorGrant(locus, share) {
552
- return this.webex
553
- .request({
554
- uri: share.url,
555
- method: 'PUT',
556
- body: {
557
- floor: {
558
- disposition: 'RELEASED',
559
- },
560
- },
561
- })
562
- .then(({body}) => body);
563
- },
564
-
565
- /**
566
- * Signals to locus that the current user would like to share an additional
567
- * media stream
568
- * @param {Types~Locus} locus
569
- * @param {Types~MediaShare} share
570
- * @returns {Promise}
571
- */
572
- requestFloorGrant(locus, share) {
573
- return this.webex
574
- .request({
575
- uri: share.url,
576
- method: 'PUT',
577
- body: {
578
- floor: {
579
- beneficiary: {
580
- url: locus.self.url,
581
- devices: [{url: this.webex.internal.device.url}],
582
- },
583
- disposition: 'GRANTED',
584
- },
585
- },
586
- })
587
- .then(({body}) => body);
588
- },
589
-
590
- /**
591
- * Sends a string of DTMF tones to the locus
592
- * @instance
593
- * @memberof Locus
594
- * @param {Types~Locus} locus
595
- * @param {string} tones
596
- * @returns {Promise}
597
- */
598
- sendDtmf(locus, tones) {
599
- return this.request({
600
- method: 'POST',
601
- uri: `${locus.self.url}/sendDtmf`,
602
- body: {
603
- deviceUrl: this.webex.internal.device.url,
604
- dtmf: {
605
- correlationId: uuid.v4(),
606
- tones,
607
- },
608
- },
609
- });
610
- },
611
-
612
- /**
613
- * Fetches the delta for the locus from its syncUrl. *Does not merge*
614
- * @instance
615
- * @memberof Locus
616
- * @param {Types~Locus} locus
617
- * @returns {Types~LocusDelta}
618
- */
619
- sync(locus) {
620
- return (
621
- this.request({
622
- method: 'GET',
623
- uri: locus.syncUrl,
624
- })
625
- // the api may return a 204 no content, so we'll give back an empty
626
- // object in that case.
627
- .then((res) => res.body || {})
628
- );
629
- },
630
-
631
- /**
632
- * Send a new sdp to Linus via the Locus API to update media state (e.g. to
633
- * start or stop sending audio or video)
634
- * @instance
635
- * @memberof Locus
636
- * @param {Types~Locus} locus
637
- * @param {Object} options
638
- * @param {string} options.localSdp
639
- * @param {string} options.mediaId
640
- * @param {Boolean} options.audioMuted
641
- * @param {Boolean} options.videoMuted
642
- * @returns {Promise<Types~Locus>}
643
- */
644
- updateMedia(locus, {sdp, audioMuted, videoMuted, mediaId}) {
645
- const localSdp = {
646
- audioMuted,
647
- videoMuted,
648
- };
649
-
650
- if (sdp) {
651
- localSdp.type = 'SDP';
652
- localSdp.sdp = sdp;
653
- }
654
-
655
- return this.request({
656
- method: 'PUT',
657
- uri: `${locus.self.url}/media`,
658
- body: {
659
- deviceUrl: this.webex.internal.device.url,
660
- localMedias: [
661
- {
662
- localSdp: JSON.stringify(localSdp),
663
- mediaId,
664
- },
665
- ],
666
- sequence: locus.sequence,
667
- },
668
- }).then((res) => res.body.locus);
669
- },
670
- });
671
-
672
- export default Locus;
1
+ /*!
2
+ * Copyright (c) 2015-2020 Cisco Systems, Inc. See LICENSE file.
3
+ */
4
+
5
+ import {WebexPlugin, WebexHttpError} from '@webex/webex-core';
6
+ import {cloneDeep, difference, first, last, memoize} from 'lodash';
7
+ import uuid from 'uuid';
8
+
9
+ export const USE_INCOMING = 'USE_INCOMING';
10
+ export const USE_CURRENT = 'USE_CURRENT';
11
+ export const EQUAL = 'EQUAL';
12
+ export const FETCH = 'FETCH';
13
+ export const GREATER_THAN = 'GREATER_THAN';
14
+ export const LESS_THAN = 'LESS_THAN';
15
+ export const DESYNC = 'DESYNC';
16
+
17
+ /**
18
+ * Transates the result of a sequence comparison into an intended behavior
19
+ * @param {string} result
20
+ * @private
21
+ * @returns {string}
22
+ */
23
+ function compareToAction(result) {
24
+ switch (result) {
25
+ case EQUAL:
26
+ case GREATER_THAN:
27
+ return USE_CURRENT;
28
+ case LESS_THAN:
29
+ return USE_INCOMING;
30
+ case DESYNC:
31
+ return FETCH;
32
+ default:
33
+ throw new Error(`${result} is not a recognized sequence comparison result`);
34
+ }
35
+ }
36
+
37
+ /**
38
+ * @class
39
+ */
40
+ const Locus = WebexPlugin.extend({
41
+ namespace: 'Locus',
42
+
43
+ /**
44
+ * Alert the specified locus that the local user has been notified of the
45
+ * locus's active state
46
+ * @instance
47
+ * @memberof Locus
48
+ * @param {Types~Locus} locus
49
+ * @returns {Promise}
50
+ */
51
+ alert(locus) {
52
+ return this.request({
53
+ method: 'PUT',
54
+ uri: `${locus.url}/participant/alert`,
55
+ body: {
56
+ deviceUrl: this.webex.internal.device.url,
57
+ sequence: locus.sequence,
58
+ },
59
+ }).then((res) => res.body);
60
+ },
61
+
62
+ /**
63
+ * Compares two loci to determine which one contains the most recent state
64
+ * @instance
65
+ * @memberof Locus
66
+ * @param {Types~Locus} current
67
+ * @param {Types~Locus} incoming
68
+ * @returns {string} one of USE_INCOMING, USE_CURRENT, or FETCH
69
+ */
70
+ compare(current, incoming) {
71
+ /**
72
+ * Determines if a paricular locus's sequence is empty
73
+ * @param {Types~Locus} locus
74
+ * @private
75
+ * @returns {bool}
76
+ */
77
+ function isEmpty(locus) {
78
+ const {sequence} = locus;
79
+
80
+ return (
81
+ (!sequence.entries || !sequence.entries.length) &&
82
+ sequence.rangeStart === 0 &&
83
+ sequence.rangeEnd === 0
84
+ );
85
+ }
86
+
87
+ if (isEmpty(current) || isEmpty(incoming)) {
88
+ return USE_INCOMING;
89
+ }
90
+
91
+ if (incoming.baseSequence) {
92
+ return this.compareDelta(current, incoming);
93
+ }
94
+
95
+ return compareToAction(this.compareSequence(current.sequence, incoming.sequence));
96
+ },
97
+
98
+ /**
99
+ * Compares two loci sequences (with delta params) and indicates what action
100
+ * to take.
101
+ * @instance
102
+ * @memberof Locus
103
+ * @param {Types~Locus} current
104
+ * @param {Types~Locus} incoming
105
+ * @private
106
+ * @returns {string} one of USE_INCOMING, USE_CURRENT, or FETCH
107
+ */
108
+ compareDelta(current, incoming) {
109
+ let ret = this.compareSequence(current.sequence, incoming.sequence);
110
+
111
+ if (ret !== LESS_THAN) {
112
+ return compareToAction(ret);
113
+ }
114
+
115
+ ret = this.compareSequence(current.sequence, incoming.baseSequence);
116
+
117
+ switch (ret) {
118
+ case GREATER_THAN:
119
+ case EQUAL:
120
+ return USE_INCOMING;
121
+ default:
122
+ return FETCH;
123
+ }
124
+ },
125
+
126
+ /**
127
+ * Compares two Locus sequences
128
+ * @instance
129
+ * @memberof Locus
130
+ * @param {LocusSequence} current
131
+ * @param {LocusSequence} incoming
132
+ * @returns {string} one of LESS_THAN, GREATER_THAN, EQUAL, or DESYNC
133
+ */
134
+ compareSequence(current, incoming) {
135
+ if (!current) {
136
+ throw new Error('`current` is required');
137
+ }
138
+
139
+ if (!incoming) {
140
+ throw new Error('`incoming` is required');
141
+ }
142
+ // complexity here is unavoidable
143
+ /* eslint complexity: [0] */
144
+ /* eslint max-statements: [0] */
145
+
146
+ // must pick one of arrow-body-style or no-confusing-arrow to disable
147
+ /* eslint arrow-body-style: [0] */
148
+
149
+ // after running the #compare() test suite in a loop, there doesn't seem to
150
+ // be any appreciable difference when used with or without memoize; since
151
+ // real locus sequences are likely to contain more sequence numbers than
152
+ // those in the test suite, I have to assume memoize can only help and the
153
+ // overhead of memoizing these methods is not a problem.
154
+
155
+ const getEntriesFirstValue = memoize((sequence) => {
156
+ return sequence.entries.length === 0 ? 0 : first(sequence.entries);
157
+ });
158
+ const getEntriesLastValue = memoize((sequence) => {
159
+ return sequence.entries.length === 0 ? 0 : last(sequence.entries);
160
+ });
161
+ const getCompareFirstValue = memoize((sequence) => {
162
+ return sequence.rangeStart || getEntriesFirstValue(sequence);
163
+ });
164
+ const getCompareLastValue = memoize((sequence) => {
165
+ return getEntriesLastValue(sequence) || sequence.rangeEnd;
166
+ });
167
+
168
+ /**
169
+ * @param {number} entry
170
+ * @param {LocusSequence} sequence
171
+ * @private
172
+ * @returns {Boolean}
173
+ */
174
+ function inRange(entry, sequence) {
175
+ return entry >= sequence.rangeStart && entry <= sequence.rangeEnd;
176
+ }
177
+
178
+ if (getCompareFirstValue(current) > getCompareLastValue(incoming)) {
179
+ return GREATER_THAN;
180
+ }
181
+
182
+ if (getCompareLastValue(current) < getCompareFirstValue(incoming)) {
183
+ return LESS_THAN;
184
+ }
185
+
186
+ const currentOnlyEntries = difference(current.entries, incoming.entries);
187
+ const incomingOnlyEntries = difference(incoming.entries, current.entries);
188
+ const currentOnly = [];
189
+ const incomingOnly = [];
190
+
191
+ for (const i of currentOnlyEntries) {
192
+ if (!inRange(i, incoming)) {
193
+ currentOnly.push(i);
194
+ }
195
+ }
196
+ for (const i of incomingOnlyEntries) {
197
+ if (!inRange(i, current)) {
198
+ incomingOnly.push(i);
199
+ }
200
+ }
201
+
202
+ if (!currentOnly.length && !incomingOnly.length) {
203
+ if (
204
+ current.rangeEnd - getCompareFirstValue(current) >
205
+ incoming.rangeEnd - getCompareFirstValue(incoming)
206
+ ) {
207
+ return GREATER_THAN;
208
+ }
209
+
210
+ if (
211
+ current.rangeEnd - getCompareFirstValue(current) <
212
+ incoming.rangeEnd - getCompareFirstValue(incoming)
213
+ ) {
214
+ return LESS_THAN;
215
+ }
216
+
217
+ return EQUAL;
218
+ }
219
+
220
+ if (currentOnly.length && !incomingOnly.length) {
221
+ return GREATER_THAN;
222
+ }
223
+
224
+ if (!currentOnly.length && incomingOnly.length) {
225
+ return LESS_THAN;
226
+ }
227
+
228
+ if (!current.rangeStart && !current.rangeEnd && !incoming.rangeStart && !incoming.rangeEnd) {
229
+ return DESYNC;
230
+ }
231
+
232
+ for (const i of currentOnly) {
233
+ if (getCompareFirstValue(incoming) < i && i < getCompareLastValue(incoming)) {
234
+ return DESYNC;
235
+ }
236
+ }
237
+
238
+ for (const i of incomingOnly) {
239
+ if (getCompareFirstValue(current) < i && i < getCompareLastValue(current)) {
240
+ return DESYNC;
241
+ }
242
+ }
243
+
244
+ if (currentOnly[0] > incomingOnly[0]) {
245
+ return GREATER_THAN;
246
+ }
247
+
248
+ return LESS_THAN;
249
+ },
250
+
251
+ /**
252
+ * Calls the specified invitee and offers the specified media via
253
+ * options.localSdp
254
+ * @instance
255
+ * @memberof Locus
256
+ * @param {string} invitee
257
+ * @param {Object} options
258
+ * @param {Object} options.localSdp
259
+ * @returns {Promise<Types~Locus>}
260
+ */
261
+ create(invitee, options = {}) {
262
+ const {correlationId} = options;
263
+
264
+ if (!correlationId) {
265
+ throw new Error('options.correlationId is required');
266
+ }
267
+
268
+ return (
269
+ this.request({
270
+ method: 'POST',
271
+ service: 'locus',
272
+ resource: 'loci/call',
273
+ body: {
274
+ correlationId,
275
+ deviceUrl: this.webex.internal.device.url,
276
+ invitee: {
277
+ invitee,
278
+ },
279
+ localMedias: [
280
+ {
281
+ localSdp: JSON.stringify({
282
+ type: 'SDP',
283
+ sdp: options.localSdp,
284
+ }),
285
+ },
286
+ ],
287
+ sequence: {
288
+ entries: [],
289
+ rangeStart: 0,
290
+ rangeEnd: 0,
291
+ },
292
+ },
293
+ })
294
+ // res.body.mediaConnections is deprecated so just return the locus
295
+ .then((res) => {
296
+ res.body.locus.self.devices.map((item, index) => {
297
+ item.mediaConnections = [res.body.mediaConnections[index]];
298
+
299
+ return item;
300
+ });
301
+
302
+ return res.body.locus;
303
+ })
304
+ );
305
+ },
306
+
307
+ /**
308
+ * This is mostly an internal function to simplify the phone plugin. Decides
309
+ * which path to call based on the type of the thing being joined.
310
+ * @instance
311
+ * @memberof Locus
312
+ * @param {Object|Types~Locus} target
313
+ * @param {Object} options
314
+ * @private
315
+ * @returns {Promise<Types~Locus>}
316
+ */
317
+ createOrJoin(target, options) {
318
+ if (target.url) {
319
+ return this.join(target, options);
320
+ }
321
+
322
+ return this.create(target, options);
323
+ },
324
+
325
+ /**
326
+ * Decline to join the specified Locus
327
+ * @instance
328
+ * @memberof Locus
329
+ * @param {Types~Locus} locus
330
+ * @returns {Promise<Types~Locus>}
331
+ */
332
+ decline(locus) {
333
+ return this.request({
334
+ method: 'PUT',
335
+ uri: `${locus.url}/participant/decline`,
336
+ body: {
337
+ deviceUrl: this.webex.internal.device.url,
338
+ sequence: locus.sequence,
339
+ },
340
+ })
341
+ .then((res) => res.body)
342
+ .catch((reason) => {
343
+ if (reason instanceof WebexHttpError.Conflict) {
344
+ return this.get(locus);
345
+ }
346
+
347
+ return Promise.reject(reason);
348
+ });
349
+ },
350
+
351
+ /**
352
+ * Retrieves a single Locus
353
+ * @instance
354
+ * @memberof Locus
355
+ * @param {Types~Locus} locus
356
+ * @returns {Types~Locus}
357
+ */
358
+ get(locus) {
359
+ return this.request({
360
+ method: 'GET',
361
+ uri: `${locus.url}`,
362
+ }).then((res) => res.body);
363
+ },
364
+
365
+ /**
366
+ * Retrieves the call history for the current user
367
+ * @instance
368
+ * @memberof Locus
369
+ * @param {Object} options
370
+ * @param {Date|number} options.from
371
+ * @returns {Promise<Object>}
372
+ */
373
+ getCallHistory(options = {}) {
374
+ const from = new Date(options.from || Date.now()).toISOString();
375
+
376
+ return this.request({
377
+ method: 'GET',
378
+ service: 'janus',
379
+ resource: 'history/userSessions',
380
+ qs: {from},
381
+ }).then((res) => res.body);
382
+ },
383
+
384
+ /**
385
+ * Join the specified Locus and offer to send it media
386
+ * @instance
387
+ * @memberof Locus
388
+ * @param {Types~Locus} locus
389
+ * @param {Object} options
390
+ * @param {Object} options.localSdp
391
+ * @returns {Types~Locus}
392
+ */
393
+ join(locus, options = {}) {
394
+ const correlationId = locus.correlationId || options.correlationId;
395
+
396
+ if (!correlationId) {
397
+ throw new Error('locus.correlationId or options.correlationId is required');
398
+ }
399
+
400
+ return (
401
+ this.request({
402
+ method: 'POST',
403
+ uri: `${locus.url}/participant`,
404
+ body: {
405
+ correlationId,
406
+ deviceUrl: this.webex.internal.device.url,
407
+ localMedias: [
408
+ {
409
+ localSdp: JSON.stringify({
410
+ type: 'SDP',
411
+ sdp: options.localSdp,
412
+ }),
413
+ },
414
+ ],
415
+ sequence: locus.sequence || {
416
+ entries: [],
417
+ rangeStart: 0,
418
+ rangeEnd: 0,
419
+ },
420
+ },
421
+ })
422
+ // The mediaConnections object is deprecated, so just return the locus
423
+ .then((res) => {
424
+ res.body.locus.self.devices.map((item, index) => {
425
+ item.mediaConnections = [res.body.mediaConnections[index]];
426
+
427
+ return item;
428
+ });
429
+
430
+ return res.body.locus;
431
+ })
432
+ );
433
+ },
434
+
435
+ /**
436
+ * Leave the specified Locus
437
+ * @instance
438
+ * @memberof Locus
439
+ * @param {Types~Locus} locus
440
+ * @returns {Promise<Types~Locus>}
441
+ */
442
+ leave(locus) {
443
+ return this.request({
444
+ method: 'PUT',
445
+ uri: `${locus.self.url}/leave`,
446
+ body: {
447
+ deviceUrl: this.webex.internal.device.url,
448
+ sequence: locus.sequence,
449
+ },
450
+ })
451
+ .then((res) => res.body.locus)
452
+ .catch((reason) => {
453
+ if (reason instanceof WebexHttpError.Conflict) {
454
+ return this.get(locus);
455
+ }
456
+
457
+ return Promise.reject(reason);
458
+ });
459
+ },
460
+
461
+ /**
462
+ * Lists active loci
463
+ * @instance
464
+ * @memberof Locus
465
+ * @returns {Promise<Array<Types~Locus>>}
466
+ */
467
+ list() {
468
+ return this.request({
469
+ method: 'GET',
470
+ service: 'locus',
471
+ resource: 'loci',
472
+ }).then((res) => res.body.loci);
473
+ },
474
+
475
+ /**
476
+ * Merges two locus DTOs (for the same locus)
477
+ * @instance
478
+ * @memberof Locus
479
+ * @param {Types~Locus} current
480
+ * @param {Types~Locus|Types~LocusDelta} incoming
481
+ * @returns {Type~Locus}
482
+ */
483
+ merge(current, incoming) {
484
+ // if incoming is not a delta event, treat it as a new full locus.
485
+ if (!incoming.baseSequence) {
486
+ return incoming;
487
+ }
488
+
489
+ const next = cloneDeep(current);
490
+
491
+ // 1. All non-null elements in the delta event except the "baseSequence" and
492
+ // the "participants" collection should be used to replace their existing
493
+ // values.
494
+ Object.keys(incoming).forEach((key) => {
495
+ if (key === 'baseSequence' || key === 'participants') {
496
+ return;
497
+ }
498
+
499
+ next[key] = incoming[key] || next[key];
500
+ });
501
+
502
+ // 2. The "baseSequence" in the delta event can be discarded (it doesn't
503
+ // need to be maintained in the local working copy).
504
+
505
+ if (incoming.participants || incoming.participants.length) {
506
+ const toRemove = new Set();
507
+ const toUpsert = new Map();
508
+
509
+ incoming.participants.forEach((p) => {
510
+ if (p.removed) {
511
+ // Elements of the delta event's "participants" list with the
512
+ // attribute `removed=true` should be removed from the working copy's
513
+ // "participants" collection.
514
+ toRemove.add(p.url);
515
+ } else {
516
+ // Elements of the delta events "participants" list that are absent
517
+ // from the local working copy should be added to that collection.
518
+ toUpsert.set(p.url, p);
519
+ }
520
+ });
521
+
522
+ // The "participants" collection in the delta event should be merged with
523
+ // that of the local working copy of the Locus such that elements in the
524
+ // delta event's "participants" replace those with the same url value in
525
+ // the working copy "participants" collection.
526
+ const participants = next.participants.reduce((acc, p) => {
527
+ if (!toRemove.has(p.url)) {
528
+ acc[p.url] = p;
529
+ }
530
+
531
+ return acc;
532
+ }, {});
533
+
534
+ toUpsert.forEach((value, key) => {
535
+ participants[key] = value;
536
+ });
537
+
538
+ next.participants = Object.values(participants);
539
+ }
540
+
541
+ return next;
542
+ },
543
+
544
+ /**
545
+ * Signals to locus that the current user is done sharing their additional
546
+ * media stream
547
+ * @param {Types~Locus} locus
548
+ * @param {Types~MediaShare} share
549
+ * @returns {Promise}
550
+ */
551
+ releaseFloorGrant(locus, share) {
552
+ return this.webex
553
+ .request({
554
+ uri: share.url,
555
+ method: 'PUT',
556
+ body: {
557
+ floor: {
558
+ disposition: 'RELEASED',
559
+ },
560
+ },
561
+ })
562
+ .then(({body}) => body);
563
+ },
564
+
565
+ /**
566
+ * Signals to locus that the current user would like to share an additional
567
+ * media stream
568
+ * @param {Types~Locus} locus
569
+ * @param {Types~MediaShare} share
570
+ * @returns {Promise}
571
+ */
572
+ requestFloorGrant(locus, share) {
573
+ return this.webex
574
+ .request({
575
+ uri: share.url,
576
+ method: 'PUT',
577
+ body: {
578
+ floor: {
579
+ beneficiary: {
580
+ url: locus.self.url,
581
+ devices: [{url: this.webex.internal.device.url}],
582
+ },
583
+ disposition: 'GRANTED',
584
+ },
585
+ },
586
+ })
587
+ .then(({body}) => body);
588
+ },
589
+
590
+ /**
591
+ * Sends a string of DTMF tones to the locus
592
+ * @instance
593
+ * @memberof Locus
594
+ * @param {Types~Locus} locus
595
+ * @param {string} tones
596
+ * @returns {Promise}
597
+ */
598
+ sendDtmf(locus, tones) {
599
+ return this.request({
600
+ method: 'POST',
601
+ uri: `${locus.self.url}/sendDtmf`,
602
+ body: {
603
+ deviceUrl: this.webex.internal.device.url,
604
+ dtmf: {
605
+ correlationId: uuid.v4(),
606
+ tones,
607
+ },
608
+ },
609
+ });
610
+ },
611
+
612
+ /**
613
+ * Fetches the delta for the locus from its syncUrl. *Does not merge*
614
+ * @instance
615
+ * @memberof Locus
616
+ * @param {Types~Locus} locus
617
+ * @returns {Types~LocusDelta}
618
+ */
619
+ sync(locus) {
620
+ return (
621
+ this.request({
622
+ method: 'GET',
623
+ uri: locus.syncUrl,
624
+ })
625
+ // the api may return a 204 no content, so we'll give back an empty
626
+ // object in that case.
627
+ .then((res) => res.body || {})
628
+ );
629
+ },
630
+
631
+ /**
632
+ * Send a new sdp to Linus via the Locus API to update media state (e.g. to
633
+ * start or stop sending audio or video)
634
+ * @instance
635
+ * @memberof Locus
636
+ * @param {Types~Locus} locus
637
+ * @param {Object} options
638
+ * @param {string} options.localSdp
639
+ * @param {string} options.mediaId
640
+ * @param {Boolean} options.audioMuted
641
+ * @param {Boolean} options.videoMuted
642
+ * @returns {Promise<Types~Locus>}
643
+ */
644
+ updateMedia(locus, {sdp, audioMuted, videoMuted, mediaId}) {
645
+ const localSdp = {
646
+ audioMuted,
647
+ videoMuted,
648
+ };
649
+
650
+ if (sdp) {
651
+ localSdp.type = 'SDP';
652
+ localSdp.sdp = sdp;
653
+ }
654
+
655
+ return this.request({
656
+ method: 'PUT',
657
+ uri: `${locus.self.url}/media`,
658
+ body: {
659
+ deviceUrl: this.webex.internal.device.url,
660
+ localMedias: [
661
+ {
662
+ localSdp: JSON.stringify(localSdp),
663
+ mediaId,
664
+ },
665
+ ],
666
+ sequence: locus.sequence,
667
+ },
668
+ }).then((res) => res.body.locus);
669
+ },
670
+ });
671
+
672
+ export default Locus;