diff --git a/packages/@webex/plugin-meetings/src/hashTree/types.ts b/packages/@webex/plugin-meetings/src/hashTree/types.ts index 9bcf7638d00..fee7c7aa043 100644 --- a/packages/@webex/plugin-meetings/src/hashTree/types.ts +++ b/packages/@webex/plugin-meetings/src/hashTree/types.ts @@ -8,12 +8,14 @@ export const ObjectType = { mediaShare: 'mediashare', info: 'info', fullState: 'fullstate', + links: 'links', } as const; export type ObjectType = Enum; // mapping from ObjectType to top level LocusDTO keys export const ObjectTypeToLocusKeyMap = { + [ObjectType.links]: 'links', [ObjectType.info]: 'info', [ObjectType.fullState]: 'fullState', [ObjectType.self]: 'self', diff --git a/packages/@webex/plugin-meetings/src/locus-info/index.ts b/packages/@webex/plugin-meetings/src/locus-info/index.ts index 1eeae9e1072..b32d1ddd6c9 100644 --- a/packages/@webex/plugin-meetings/src/locus-info/index.ts +++ b/packages/@webex/plugin-meetings/src/locus-info/index.ts @@ -38,7 +38,7 @@ import HashTreeParser, { LocusInfoUpdateType, } from '../hashTree/hashTreeParser'; import {ObjectType, ObjectTypeToLocusKeyMap} from '../hashTree/types'; -import {LocusDTO, LocusFullState} from './types'; +import {Links, LocusDTO, LocusFullState} from './types'; export type LocusLLMEvent = { data: { @@ -113,8 +113,7 @@ export default class LocusInfo extends EventsScope { mediaShares: any; replace: any; url: any; - services: any; - resources: any; + links?: Links; mainSessionLocusCache: any; self: any; hashTreeParser?: HashTreeParser; @@ -360,8 +359,7 @@ export default class LocusInfo extends EventsScope { this.updateSelf(locus.self); this.updateHostInfo(locus.host); this.updateMediaShares(locus.mediaShares); - this.updateServices(locus.links?.services); - this.updateResources(locus.links?.resources); + this.updateLinks(locus.links); } /** @@ -596,12 +594,13 @@ export default class LocusInfo extends EventsScope { this.hashTreeObjectId2ParticipantId.delete(object.htMeta.elementId.id); } break; + case ObjectType.links: case ObjectType.info: case ObjectType.fullState: case ObjectType.self: if (!object.data) { // self without data is handled inside HashTreeParser and results in LocusInfoUpdateType.MEETING_ENDED, so we should never get here - // other types like info or fullstate - Locus should never send them without data + // all other types info, fullstate, etc - Locus should never send them without data LoggerProxy.logger.warn( `Locus-info:index#updateLocusFromHashTreeObject --> received ${type} object without data, this is not expected! version=${object.htMeta.elementId.version}` ); @@ -1032,8 +1031,7 @@ export default class LocusInfo extends EventsScope { this.updateMemberShip(locus.membership); this.updateIdentifiers(locus.identities); this.updateEmbeddedApps(locus.embeddedApps); - this.updateServices(locus.links?.services); - this.updateResources(locus.links?.resources); + this.updateLinks(locus.links); this.compareAndUpdate(); // update which required to compare different objects from locus } @@ -1685,17 +1683,19 @@ export default class LocusInfo extends EventsScope { } /** - * @param {Object} services + * Updates links and emits appropriate events if services or resources have changed + * @param {Object} links * @returns {undefined} * @memberof LocusInfo */ - updateServices(services: Record<'breakout' | 'record', {url: string}>) { - if (services && !isEqual(this.services, services)) { - this.services = services; + updateLinks(links?: Links) { + const {services, resources} = links || {}; + + if (services && !isEqual(this.links?.services, services)) { this.emitScoped( { file: 'locus-info', - function: 'updateServices', + function: 'updateLinks', }, LOCUSINFO.EVENTS.LINKS_SERVICES, { @@ -1703,20 +1703,12 @@ export default class LocusInfo extends EventsScope { } ); } - } - /** - * @param {Object} resources - * @returns {undefined} - * @memberof LocusInfo - */ - updateResources(resources: Record<'webcastInstance', {url: string}>) { - if (resources && !isEqual(this.resources, resources)) { - this.resources = resources; + if (resources && !isEqual(this.links?.resources, resources)) { this.emitScoped( { file: 'locus-info', - function: 'updateResources', + function: 'updateLinks', }, LOCUSINFO.EVENTS.LINKS_RESOURCES, { @@ -1724,6 +1716,8 @@ export default class LocusInfo extends EventsScope { } ); } + + this.links = links; } /** diff --git a/packages/@webex/plugin-meetings/src/locus-info/types.ts b/packages/@webex/plugin-meetings/src/locus-info/types.ts index f79c8dca86a..eab09ff0bc8 100644 --- a/packages/@webex/plugin-meetings/src/locus-info/types.ts +++ b/packages/@webex/plugin-meetings/src/locus-info/types.ts @@ -12,6 +12,11 @@ export type LocusFullState = { type: string; }; +export type Links = { + services: Record<'breakout' | 'record', {url: string}>; // there exist also other services, but these are the ones we currently use + resources: Record<'webcastInstance' | 'visibleDataSets', {url: string}>; // there exist also other resources, but these are the ones we currently use +}; + export type LocusDTO = { controls?: any; fullState?: LocusFullState; @@ -27,7 +32,7 @@ export type LocusDTO = { jsSdkMeta?: { removedParticipantIds: string[]; // list of ids of participants that are removed in the last update }; - links?: any; + links?: Links; mediaShares?: any[]; meetings?: any[]; participants: any[]; diff --git a/packages/@webex/plugin-meetings/test/unit/spec/locus-info/index.js b/packages/@webex/plugin-meetings/test/unit/spec/locus-info/index.js index 7e9ca9dccea..6acac212c84 100644 --- a/packages/@webex/plugin-meetings/test/unit/spec/locus-info/index.js +++ b/packages/@webex/plugin-meetings/test/unit/spec/locus-info/index.js @@ -394,6 +394,24 @@ describe('plugin-meetings', () => { }); }); + it('should process locus update correctly when called with updated links', () => { + const newLinks = { + id: 'new-links', + visibleDataSets: ['dataset1', 'dataset2'], + }; + + // simulate an update from the HashTreeParser (normally this would be triggered by incoming locus messages) + locusInfoUpdateCallback(OBJECTS_UPDATED, { + updatedObjects: [{htMeta: {elementId: {type: 'links'}}, data: newLinks}], + }); + + // check onDeltaLocus() was called with correctly updated locus info + assert.calledOnceWithExactly(onDeltaLocusStub, { + ...expectedLocusInfo, + links: newLinks, + }); + }); + it('should process locus update correctly when called with updated LOCUS object', () => { // setup new updated locus that has many things missing const newLocusHtMeta = {elementId: {type: 'locus', version: 42}}; @@ -414,6 +432,7 @@ describe('plugin-meetings', () => { info: {id: 'fake-info'}, fullState: {id: 'fake-full-state'}, self: {id: 'fake-self'}, + links: { id: 'fake-links' }, mediaShares: expectedLocusInfo.mediaShares, // and now the new fields ...newLocus, @@ -459,6 +478,7 @@ describe('plugin-meetings', () => { info: {id: 'fake-info'}, fullState: {id: 'fake-full-state'}, self: {id: 'fake-self'}, + links: { id: 'fake-links' }, mediaShares: expectedLocusInfo.mediaShares, participants: [], // empty means there were no participant updates jsSdkMeta: {removedParticipantIds: []}, // no participants were removed @@ -492,6 +512,7 @@ describe('plugin-meetings', () => { info: {id: 'fake-info'}, fullState: {id: 'fake-full-state'}, self: {id: 'fake-self'}, + links: {id: 'fake-links'}, mediaShares: expectedLocusInfo.mediaShares, // and now the new fields ...newLocus, @@ -2771,7 +2792,7 @@ describe('plugin-meetings', () => { sinon.stub(locusInfo, "updateMemberShip"); sinon.stub(locusInfo, "updateIdentifiers"); sinon.stub(locusInfo, "updateEmbeddedApps"); - sinon.stub(locusInfo, "updateResources"); + sinon.stub(locusInfo, "updateLinks"); sinon.stub(locusInfo, "compareAndUpdate"); locusInfo.updateLocusInfo(locus); @@ -2805,7 +2826,7 @@ describe('plugin-meetings', () => { locusInfo.updateMemberShip = sinon.stub(); locusInfo.updateIdentifiers = sinon.stub(); locusInfo.updateEmbeddedApps = sinon.stub(); - locusInfo.updateResources = sinon.stub(); + locusInfo.updateLinks = sinon.stub(); locusInfo.compareAndUpdate = sinon.stub(); locusInfo.updateLocusInfo(newLocus); @@ -2827,11 +2848,42 @@ describe('plugin-meetings', () => { assert.notCalled(locusInfo.updateMemberShip); assert.notCalled(locusInfo.updateIdentifiers); assert.notCalled(locusInfo.updateEmbeddedApps); - assert.notCalled(locusInfo.updateResources); + assert.notCalled(locusInfo.updateLinks); assert.notCalled(locusInfo.compareAndUpdate); }); + it('#updateLocusInfo puts the Locus DTO top level properties at the right place in LocusInfo class', () => { + // this test verifies that the top-level properties of Locus DTO are copied + // into LocusInfo class and set as top level properties too + // this is important, because the code handling Locus hass trees relies on it, see updateFromHashTree() + const info = {id: 'info id'}; + const fullState = {id: 'fullState id'}; + const links = {services: {id: 'service links'}, resources: {id: 'resource links'}}; + const self = {id: 'self id'}; + const mediaShares = [{id: 'fake media share'}]; + sinon.stub(SelfUtils, 'getSelves').returns({ + current: {}, + previous: {}, + updates: {}, + }); + + const newLocus = { + info, + fullState, + links, + self, + mediaShares, + }; + + locusInfo.updateLocusInfo(newLocus); + + assert.deepEqual(locusInfo.info, newLocus.info); + assert.deepEqual(locusInfo.fullState, newLocus.fullState); + assert.deepEqual(locusInfo.links, newLocus.links); + assert.deepEqual(locusInfo.self, newLocus.self); + assert.deepEqual(locusInfo.mediaShares, newLocus.mediaShares); + }); it('onFullLocus() updates the working-copy of locus parser', () => { const eventType = 'fakeEvent';