feat: added support for breakouts to Locus hash trees (5k webinars)#4775
feat: added support for breakouts to Locus hash trees (5k webinars)#4775marcin-bazyl merged 9 commits intowebex:nextfrom
Conversation
|
This pull request is automatically being deployed by Amplify Hosting (learn more). |
026c946 to
e8664b3
Compare
| this.visibleDataSets = ( | ||
| cloneDeep(options.metadata?.visibleDataSets || []) as VisibleDataSetInfo[] | ||
| ).filter((vds) => !this.isExcludedDataSet(vds.name)); | ||
| this.setVisibleDataSets(options.metadata?.visibleDataSets || [], dataSets); |
There was a problem hiding this comment.
extracted this block of code into a setVisibleDataSets() method, because I needed to have the same logic in resume() method
| locus: LocusDTO; // this LocusDTO here might not be the full one (for example it won't have all the participants, but it should have self) | ||
| metadata?: Metadata; | ||
| } | ||
| | LocusDTO; // when we invoke APIs on the whole Locus like "mute all" backend returns the whole Locus in the response like this |
There was a problem hiding this comment.
the fact that some API responses don't have dataSets and metadata but the body is just a locus object is something I found needed handling while working on breakouts, it's not strictly related to breakouts, but I've kept it in this PR.
|
|
||
| export type HashTreeParserEntry = { | ||
| parser: HashTreeParser; | ||
| replacedAt?: string; |
There was a problem hiding this comment.
we need to store replacedAt to handle cases where user moves: BO 1 -> BO 2 -> BO 1 again - we need to check that replacedAt is newer when moving back to BO 1
| export type HashTreeParserEntry = { | ||
| parser: HashTreeParser; | ||
| replacedAt?: string; | ||
| initializedFromHashTree: boolean; |
There was a problem hiding this comment.
this flag is needed, because we need to clear Locus when switching parsers. I was debating whether that state belonged more into HashTreeParser itself or LocusInfo, but decided it belongs in LocusInfo, because in some cases (like when we get full locus) HashTreeParser will not know that LocusInfo has all the data already initialized
| initialLocus: { | ||
| locus: null, | ||
| dataSets: [], // empty, because they will be populated in initializeFromMessage() call // dataSets: data.hashTreeMessage.dataSets, | ||
| dataSets: data.hashTreeMessage.dataSets, |
There was a problem hiding this comment.
we need to pass dataSets from message now here, because they're needed by HashTreeParser constructor when it sets visible data sets - see HashTreeParser.setVisibleDataSets(). I think this is a cleaner way of dealing with edge cases where we get messages from Locus with visible data sets that are not listed in dataSets list
| this.participants?.forEach((participant) => { | ||
| // participant.htMeta is set only for hash tree based locus | ||
| if (participant.htMeta?.elementId.id) { | ||
| if (typeof participant.htMeta?.elementId.id === 'number') { |
There was a problem hiding this comment.
this is a fix for a bug I found during breakout testing - we were not handling the case of participant's id being equal to zero correctly here
There was a problem hiding this comment.
nitpick: should we be checking for undefined instead of checking the type of the id?
There was a problem hiding this comment.
hmm not sure, this was suggested by ai and I kind of like the fact that we're checking that it's a number, so if the id was null or anything unexpected it would also fail the check
| const associateBreakoutLocus = this.breakoutLocusForHandleLater[existIndex]; | ||
| this.handleLocusEvent({ | ||
| eventType: LOCUSEVENT.SDK_NO_EVENT, | ||
| eventType: LOCUSEVENT.SDK_LOCUS_FROM_SYNC_MEETINGS, |
There was a problem hiding this comment.
here I've realised that breakoutLocusForHandleLater stores locus objects we got from /sync call so we should set the eventType accordingly
| * @memberof Meeting | ||
| */ | ||
|
|
||
| export function storeEventForDebugging( |
There was a problem hiding this comment.
this is something I've been using when testing, it's very handy, because it allows me to easily see all received messages (both from LLM and mercury) from a specific point in time (for example after moving to a breakout - I just do window.locusEvents=[] just before initiating the breakout move). I forgot to remove it when raising the PR, but now I'm thinking that maybe it could be useful for others too, so maybe leave it here?
There was a problem hiding this comment.
yeah I think it is fine to leave it here for now. I agree, useful for debugging
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: e8664b3fb0
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| if (this.hashTreeParsers.size > 0) { | ||
| this.handleHashTreeMessage( | ||
| meeting, | ||
| data.eventType, |
There was a problem hiding this comment.
Handle breakout loci from syncMeetings in hash-tree mode
When a meeting is created from GET /loci, it already has a hashTreeParsers entry. checkHandleBreakoutLocus() now replays the associated breakout DTO as SDK_LOCUS_FROM_SYNC_MEETINGS, but this branch still routes every subsequent event through handleHashTreeMessage(), which immediately rejects anything except HASH_TREE_DATA_UPDATED. On reconnect/refresh while the user is already in breakout, the replayed breakout locus from the sync response is therefore dropped, so the SDK stays on the main-session state until some later Mercury/LLM move event arrives.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
I've created https://jira-eng-gpk2.cisco.com/jira/browse/SPARK-790957 to deal with this in a separate jira
| if (!hashTreeParserEntry.initializedFromHashTree) { | ||
| // this is the first time we're getting an update for this locusUrl, | ||
| // so it's probably a move to/from breakout. We need to start from a clean state, | ||
| // so empty locus and we rely on Locus giving us sufficient data in the updates to populate it. | ||
| LoggerProxy.logger.info( |
There was a problem hiding this comment.
Seed the first switched-parser update with the new locus URL
On the first update for a newly created or resumed parser, this path intentionally starts from an empty locus and forces member replacement. The problem is that the empty state does not carry locusUrl, and hash-tree messages are often partial (the meeting creation path above explicitly notes they may contain only self). If that first post-move update has no Locus object, updateLocusUrl() never fires, so the meeting keeps the previous session URL and follow-up operations like leave, lock, reactions, or token refresh still target the old breakout until a later full locus update happens.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
I think it's fine, because Locus is supposed to always send us the main LOCUS object with SELF in the 1st message for the new breakout session
| * @param {string} deviceUrl - The URL of the user's device | ||
| * @returns {any} The replace information if available, otherwise undefined | ||
| */ | ||
| function getReplaceInfoFromSelf(self: any, deviceUrl: string) { |
There was a problem hiding this comment.
nitpick: could we add ReplacesInfo as the return type?
| @@ -513,10 +625,11 @@ export default class LocusInfo extends EventsScope { | |||
| 'Locus-info:index#initialSetup --> creating HashTreeParser from message' | |||
| ); | |||
| // first create the HashTreeParser, but don't initialize it with any data yet | |||
There was a problem hiding this comment.
question: this is comment still valid?
There was a problem hiding this comment.
yes it's still valid
| dataSet.heartbeatWatchdogTimer = undefined; | ||
| } | ||
| }); | ||
| this.state = 'stopped'; |
There was a problem hiding this comment.
issue: move to stop() function
| * @param {Meeting} meeting - The meeting object | ||
| * @param {eventType} eventType - The event type | ||
| * @param {HashTreeMessage} message incoming hash tree message | ||
| * @param {any} replaces - if this message indicates that a new locusUrl replaces some old locusUrl, this object contains info about the old locus |
There was a problem hiding this comment.
issue: replaces is not part of the parameters
| this.participants?.forEach((participant) => { | ||
| // participant.htMeta is set only for hash tree based locus | ||
| if (participant.htMeta?.elementId.id) { | ||
| if (typeof participant.htMeta?.elementId.id === 'number') { |
There was a problem hiding this comment.
nitpick: should we be checking for undefined instead of checking the type of the id?
| onDeltaLocus(locus: any) { | ||
| const isReplaceMembers = ControlsUtils.isNeedReplaceMembers(this.controls, locus.controls); | ||
| const isReplaceMembers = | ||
| locus.jsSdkMeta?.forceReplaceMembers !== undefined |
There was a problem hiding this comment.
if forceReplaceMembers is set to false, isReplaceMembers always be false and it won't check isNeedReplaceMembers. Is this expected?
There was a problem hiding this comment.
yes, that's the intention - if the forceReplaceMembers field is set, it should override whatever comes from ControlsUtils.isNeedReplaceMembers() - basically ControlsUtils.isNeedReplaceMembers() shouldn't be even called
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 9c02016398
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 37e5f89253
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 154e75fa53
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: af67d7934f
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| stateElementsMessage?: HashTreeMessage; | ||
| } | ||
| ) { | ||
| if ((window as any)?.locusEvents) { |
There was a problem hiding this comment.
Guard debug event storage with typeof window
Fresh evidence: this implementation uses (window as any)?.locusEvents, but optional chaining does not protect against an undeclared global identifier, so in Node/SSR/test environments resolving window still throws ReferenceError before locus processing continues. This turns an optional debug path into a runtime failure whenever non-browser clients receive hash-tree events.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
I've added a config entry for this, so only customers who use SDK in the browser and want to debug the hash tree events (so in essence only our web app) will use this
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: e347f03d3d
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| // the message matches a stopped parser, we need to check if maybe this is a new "replacement" and we need to re-activate the parser | ||
| // this happens when you move from breakout A -> breakout B -> back to breakout A | ||
| if (replaces) { | ||
| if (replaces.replacedAt > (entry.replacedAt || '')) { |
There was a problem hiding this comment.
Preserve replacement ordering before resuming stopped parser
When a parser is stopped without recording replacedAt (the new-parser path only logs a warning if replaces is missing), this resume gate compares against '', so any later delayed message from the old locus with a non-empty replaces.replacedAt is treated as newer and reactivates the old parser. In out-of-order breakout move traffic, that can switch the meeting back to a stale locus and cause subsequent state updates/actions to target the wrong session.
Useful? React with 👍 / 👎.
Thank you for your contribution! |
COMPLETES #SPARK-758861
This pull request addresses
Breakouts support for 5k webinars. Locus wiki explaining how it should work:
https://sqbu-github.cisco.com/pages/WebExSquared/locus/guides/convergedWebinar5k/breakoutMoves.html
by making the following changes
Changed LocusInfo so that it now has a map of HashTreeParsers instead of just a single parser. We have a separate parser for each locusUrl now, so when we move between breakouts and locusUrl changes, we change the parsers. The old one is stopped. It can be resumed if user goes back. Right now stop() clears all the hash trees and resume() starts from scratch, because that's what Locus team wants clients to do. We can potentially change that if needed in the future and keep the old hash trees. We may still be getting some messages for the old locusUrl, so it's good to keep the old HashTreeParser and route the messages to it and the parser just ignores them when it's stopped. Otherwise, if there was no parser to handle them, the messages could potentially cause a creation of a new meeting object.
When testing, you will see that when moving between sessions there is a delay before the participant list is populated. This is because Locus doesn't send us participant list in the 1st message for the new session and we rely on the sync algorithm, so the messages arrive a bit later or if they don't arrive, we do the sync. This is by design. Locus team have been informed that the delay is unacceptably long right now and they are considering various solutions to this, but they want the client implementation to remain as is - so no initial sync, no proactive getting of participant list.
All unit tests were created solely by AI, I've looked through them, they're not perfect, but pretty good, I think good enough.
Change Type
The following scenarios were tested
unit tests
The GAI Coding Policy And Copyright Annotation Best Practices
I certified that
I have read and followed contributing guidelines
I discussed changes with code owners prior to submitting this pull request
I have not skipped any automated checks
All existing and new tests passed
I have updated the documentation accordingly
Make sure to have followed the contributing guidelines before submitting.