@webex/plugin-meetings 3.10.0-next.3 → 3.10.0-next.30

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 (274) hide show
  1. package/dist/annotation/annotation.types.js.map +1 -1
  2. package/dist/annotation/constants.js.map +1 -1
  3. package/dist/annotation/index.js +19 -22
  4. package/dist/annotation/index.js.map +1 -1
  5. package/dist/breakouts/breakout.js +6 -6
  6. package/dist/breakouts/breakout.js.map +1 -1
  7. package/dist/breakouts/collection.js.map +1 -1
  8. package/dist/breakouts/edit-lock-error.js +9 -11
  9. package/dist/breakouts/edit-lock-error.js.map +1 -1
  10. package/dist/breakouts/events.js.map +1 -1
  11. package/dist/breakouts/index.js +126 -127
  12. package/dist/breakouts/index.js.map +1 -1
  13. package/dist/breakouts/request.js +6 -8
  14. package/dist/breakouts/request.js.map +1 -1
  15. package/dist/breakouts/utils.js.map +1 -1
  16. package/dist/common/browser-detection.js.map +1 -1
  17. package/dist/common/collection.js +1 -2
  18. package/dist/common/collection.js.map +1 -1
  19. package/dist/common/config.js.map +1 -1
  20. package/dist/common/errors/captcha-error.js +9 -11
  21. package/dist/common/errors/captcha-error.js.map +1 -1
  22. package/dist/common/errors/intent-to-join.js +10 -12
  23. package/dist/common/errors/intent-to-join.js.map +1 -1
  24. package/dist/common/errors/join-forbidden-error.js +10 -12
  25. package/dist/common/errors/join-forbidden-error.js.map +1 -1
  26. package/dist/common/errors/join-meeting.js +10 -12
  27. package/dist/common/errors/join-meeting.js.map +1 -1
  28. package/dist/common/errors/join-webinar-error.js +9 -11
  29. package/dist/common/errors/join-webinar-error.js.map +1 -1
  30. package/dist/common/errors/media.js +9 -11
  31. package/dist/common/errors/media.js.map +1 -1
  32. package/dist/common/errors/multistream-not-supported-error.js +9 -11
  33. package/dist/common/errors/multistream-not-supported-error.js.map +1 -1
  34. package/dist/common/errors/no-meeting-info.js +9 -11
  35. package/dist/common/errors/no-meeting-info.js.map +1 -1
  36. package/dist/common/errors/parameter.js +11 -14
  37. package/dist/common/errors/parameter.js.map +1 -1
  38. package/dist/common/errors/password-error.js +9 -11
  39. package/dist/common/errors/password-error.js.map +1 -1
  40. package/dist/common/errors/permission.js +9 -11
  41. package/dist/common/errors/permission.js.map +1 -1
  42. package/dist/common/errors/reclaim-host-role-errors.js +32 -38
  43. package/dist/common/errors/reclaim-host-role-errors.js.map +1 -1
  44. package/dist/common/errors/reconnection-not-started.js +5 -6
  45. package/dist/common/errors/reconnection-not-started.js.map +1 -1
  46. package/dist/common/errors/reconnection.js +9 -11
  47. package/dist/common/errors/reconnection.js.map +1 -1
  48. package/dist/common/errors/stats.js +9 -11
  49. package/dist/common/errors/stats.js.map +1 -1
  50. package/dist/common/errors/webex-errors.js +20 -29
  51. package/dist/common/errors/webex-errors.js.map +1 -1
  52. package/dist/common/errors/webex-meetings-error.js +9 -12
  53. package/dist/common/errors/webex-meetings-error.js.map +1 -1
  54. package/dist/common/events/events-scope.js +9 -10
  55. package/dist/common/events/events-scope.js.map +1 -1
  56. package/dist/common/events/events.js +9 -10
  57. package/dist/common/events/events.js.map +1 -1
  58. package/dist/common/events/trigger-proxy.js.map +1 -1
  59. package/dist/common/events/util.js.map +1 -1
  60. package/dist/common/logs/logger-config.js.map +1 -1
  61. package/dist/common/logs/logger-proxy.js.map +1 -1
  62. package/dist/common/logs/request.js +17 -17
  63. package/dist/common/logs/request.js.map +1 -1
  64. package/dist/common/queue.js +1 -2
  65. package/dist/common/queue.js.map +1 -1
  66. package/dist/config.js +2 -2
  67. package/dist/config.js.map +1 -1
  68. package/dist/constants.js +11 -8
  69. package/dist/constants.js.map +1 -1
  70. package/dist/controls-options-manager/constants.js.map +1 -1
  71. package/dist/controls-options-manager/enums.js.map +1 -1
  72. package/dist/controls-options-manager/index.js +1 -2
  73. package/dist/controls-options-manager/index.js.map +1 -1
  74. package/dist/controls-options-manager/types.js.map +1 -1
  75. package/dist/controls-options-manager/util.js +1 -2
  76. package/dist/controls-options-manager/util.js.map +1 -1
  77. package/dist/hashTree/constants.js +20 -0
  78. package/dist/hashTree/constants.js.map +1 -0
  79. package/dist/hashTree/hashTree.js +515 -0
  80. package/dist/hashTree/hashTree.js.map +1 -0
  81. package/dist/hashTree/hashTreeParser.js +1250 -0
  82. package/dist/hashTree/hashTreeParser.js.map +1 -0
  83. package/dist/hashTree/types.js +23 -0
  84. package/dist/hashTree/types.js.map +1 -0
  85. package/dist/hashTree/utils.js +59 -0
  86. package/dist/hashTree/utils.js.map +1 -0
  87. package/dist/index.js +1 -2
  88. package/dist/index.js.map +1 -1
  89. package/dist/interceptors/index.js.map +1 -1
  90. package/dist/interceptors/locusRetry.js +6 -8
  91. package/dist/interceptors/locusRetry.js.map +1 -1
  92. package/dist/interceptors/locusRouteToken.js +26 -12
  93. package/dist/interceptors/locusRouteToken.js.map +1 -1
  94. package/dist/interpretation/collection.js.map +1 -1
  95. package/dist/interpretation/index.js +1 -2
  96. package/dist/interpretation/index.js.map +1 -1
  97. package/dist/interpretation/siLanguage.js +1 -1
  98. package/dist/interpretation/siLanguage.js.map +1 -1
  99. package/dist/locus-info/controlsUtils.js.map +1 -1
  100. package/dist/locus-info/embeddedAppsUtils.js.map +1 -1
  101. package/dist/locus-info/fullState.js.map +1 -1
  102. package/dist/locus-info/hostUtils.js.map +1 -1
  103. package/dist/locus-info/index.js +609 -177
  104. package/dist/locus-info/index.js.map +1 -1
  105. package/dist/locus-info/infoUtils.js.map +1 -1
  106. package/dist/locus-info/mediaSharesUtils.js.map +1 -1
  107. package/dist/locus-info/parser.js +3 -4
  108. package/dist/locus-info/parser.js.map +1 -1
  109. package/dist/locus-info/selfUtils.js.map +1 -1
  110. package/dist/locus-info/types.js +7 -0
  111. package/dist/locus-info/types.js.map +1 -0
  112. package/dist/media/MediaConnectionAwaiter.js +1 -2
  113. package/dist/media/MediaConnectionAwaiter.js.map +1 -1
  114. package/dist/media/index.js +0 -2
  115. package/dist/media/index.js.map +1 -1
  116. package/dist/media/properties.js +15 -17
  117. package/dist/media/properties.js.map +1 -1
  118. package/dist/media/util.js.map +1 -1
  119. package/dist/meeting/brbState.js +8 -9
  120. package/dist/meeting/brbState.js.map +1 -1
  121. package/dist/meeting/connectionStateHandler.js +10 -13
  122. package/dist/meeting/connectionStateHandler.js.map +1 -1
  123. package/dist/meeting/in-meeting-actions.js.map +1 -1
  124. package/dist/meeting/index.js +1576 -1533
  125. package/dist/meeting/index.js.map +1 -1
  126. package/dist/meeting/locusMediaRequest.js +13 -17
  127. package/dist/meeting/locusMediaRequest.js.map +1 -1
  128. package/dist/meeting/muteState.js +11 -12
  129. package/dist/meeting/muteState.js.map +1 -1
  130. package/dist/meeting/request.js +101 -104
  131. package/dist/meeting/request.js.map +1 -1
  132. package/dist/meeting/request.type.js.map +1 -1
  133. package/dist/meeting/state.js.map +1 -1
  134. package/dist/meeting/type.js.map +1 -1
  135. package/dist/meeting/util.js +24 -23
  136. package/dist/meeting/util.js.map +1 -1
  137. package/dist/meeting/voicea-meeting.js +3 -3
  138. package/dist/meeting/voicea-meeting.js.map +1 -1
  139. package/dist/meeting-info/collection.js +7 -10
  140. package/dist/meeting-info/collection.js.map +1 -1
  141. package/dist/meeting-info/index.js +1 -2
  142. package/dist/meeting-info/index.js.map +1 -1
  143. package/dist/meeting-info/meeting-info-v2.js +135 -146
  144. package/dist/meeting-info/meeting-info-v2.js.map +1 -1
  145. package/dist/meeting-info/request.js +1 -2
  146. package/dist/meeting-info/request.js.map +1 -1
  147. package/dist/meeting-info/util.js +36 -37
  148. package/dist/meeting-info/util.js.map +1 -1
  149. package/dist/meeting-info/utilv2.js +30 -31
  150. package/dist/meeting-info/utilv2.js.map +1 -1
  151. package/dist/meetings/collection.js +6 -8
  152. package/dist/meetings/collection.js.map +1 -1
  153. package/dist/meetings/index.js +200 -148
  154. package/dist/meetings/index.js.map +1 -1
  155. package/dist/meetings/meetings.types.js.map +1 -1
  156. package/dist/meetings/request.js +6 -8
  157. package/dist/meetings/request.js.map +1 -1
  158. package/dist/meetings/util.js +36 -30
  159. package/dist/meetings/util.js.map +1 -1
  160. package/dist/member/index.js +1 -2
  161. package/dist/member/index.js.map +1 -1
  162. package/dist/member/types.js +6 -3
  163. package/dist/member/types.js.map +1 -1
  164. package/dist/member/util.js.map +1 -1
  165. package/dist/members/collection.js +1 -2
  166. package/dist/members/collection.js.map +1 -1
  167. package/dist/members/index.js +18 -21
  168. package/dist/members/index.js.map +1 -1
  169. package/dist/members/request.js +8 -11
  170. package/dist/members/request.js.map +1 -1
  171. package/dist/members/types.js.map +1 -1
  172. package/dist/members/util.js.map +1 -1
  173. package/dist/metrics/constants.js +3 -1
  174. package/dist/metrics/constants.js.map +1 -1
  175. package/dist/metrics/index.js +3 -4
  176. package/dist/metrics/index.js.map +1 -1
  177. package/dist/multistream/mediaRequestManager.js +1 -2
  178. package/dist/multistream/mediaRequestManager.js.map +1 -1
  179. package/dist/multistream/receiveSlot.js +34 -45
  180. package/dist/multistream/receiveSlot.js.map +1 -1
  181. package/dist/multistream/receiveSlotManager.js +8 -9
  182. package/dist/multistream/receiveSlotManager.js.map +1 -1
  183. package/dist/multistream/remoteMedia.js +12 -15
  184. package/dist/multistream/remoteMedia.js.map +1 -1
  185. package/dist/multistream/remoteMediaGroup.js +1 -2
  186. package/dist/multistream/remoteMediaGroup.js.map +1 -1
  187. package/dist/multistream/remoteMediaManager.js +122 -123
  188. package/dist/multistream/remoteMediaManager.js.map +1 -1
  189. package/dist/multistream/sendSlotManager.js +29 -30
  190. package/dist/multistream/sendSlotManager.js.map +1 -1
  191. package/dist/personal-meeting-room/index.js +16 -19
  192. package/dist/personal-meeting-room/index.js.map +1 -1
  193. package/dist/personal-meeting-room/request.js +7 -10
  194. package/dist/personal-meeting-room/request.js.map +1 -1
  195. package/dist/personal-meeting-room/util.js.map +1 -1
  196. package/dist/reachability/clusterReachability.js +188 -352
  197. package/dist/reachability/clusterReachability.js.map +1 -1
  198. package/dist/reachability/index.js +206 -206
  199. package/dist/reachability/index.js.map +1 -1
  200. package/dist/reachability/reachability.types.js +14 -1
  201. package/dist/reachability/reachability.types.js.map +1 -1
  202. package/dist/reachability/reachabilityPeerConnection.js +445 -0
  203. package/dist/reachability/reachabilityPeerConnection.js.map +1 -0
  204. package/dist/reachability/request.js.map +1 -1
  205. package/dist/reachability/util.js.map +1 -1
  206. package/dist/reactions/constants.js.map +1 -1
  207. package/dist/reactions/reactions.js.map +1 -1
  208. package/dist/reactions/reactions.type.js.map +1 -1
  209. package/dist/reconnection-manager/index.js +178 -176
  210. package/dist/reconnection-manager/index.js.map +1 -1
  211. package/dist/recording-controller/enums.js.map +1 -1
  212. package/dist/recording-controller/index.js +1 -2
  213. package/dist/recording-controller/index.js.map +1 -1
  214. package/dist/recording-controller/util.js.map +1 -1
  215. package/dist/roap/index.js +12 -15
  216. package/dist/roap/index.js.map +1 -1
  217. package/dist/roap/request.js +24 -26
  218. package/dist/roap/request.js.map +1 -1
  219. package/dist/roap/turnDiscovery.js +75 -76
  220. package/dist/roap/turnDiscovery.js.map +1 -1
  221. package/dist/roap/types.js.map +1 -1
  222. package/dist/transcription/index.js +4 -5
  223. package/dist/transcription/index.js.map +1 -1
  224. package/dist/types/config.d.ts +1 -0
  225. package/dist/types/constants.d.ts +26 -21
  226. package/dist/types/hashTree/constants.d.ts +8 -0
  227. package/dist/types/hashTree/hashTree.d.ts +129 -0
  228. package/dist/types/hashTree/hashTreeParser.d.ts +250 -0
  229. package/dist/types/hashTree/types.d.ts +33 -0
  230. package/dist/types/hashTree/utils.d.ts +16 -0
  231. package/dist/types/interceptors/locusRouteToken.d.ts +1 -0
  232. package/dist/types/locus-info/index.d.ts +98 -80
  233. package/dist/types/locus-info/types.d.ts +54 -0
  234. package/dist/types/meeting/index.d.ts +22 -9
  235. package/dist/types/meetings/index.d.ts +9 -2
  236. package/dist/types/metrics/constants.d.ts +2 -0
  237. package/dist/types/reachability/clusterReachability.d.ts +33 -84
  238. package/dist/types/reachability/reachability.types.d.ts +12 -1
  239. package/dist/types/reachability/reachabilityPeerConnection.d.ts +111 -0
  240. package/dist/webinar/collection.js +1 -2
  241. package/dist/webinar/collection.js.map +1 -1
  242. package/dist/webinar/index.js +148 -158
  243. package/dist/webinar/index.js.map +1 -1
  244. package/package.json +23 -22
  245. package/src/config.ts +1 -0
  246. package/src/constants.ts +13 -1
  247. package/src/hashTree/constants.ts +9 -0
  248. package/src/hashTree/hashTree.ts +463 -0
  249. package/src/hashTree/hashTreeParser.ts +1143 -0
  250. package/src/hashTree/types.ts +39 -0
  251. package/src/hashTree/utils.ts +53 -0
  252. package/src/interceptors/locusRouteToken.ts +16 -4
  253. package/src/locus-info/index.ts +625 -164
  254. package/src/locus-info/types.ts +53 -0
  255. package/src/meeting/index.ts +78 -27
  256. package/src/meeting/util.ts +1 -0
  257. package/src/meetings/index.ts +119 -59
  258. package/src/meetings/util.ts +10 -9
  259. package/src/metrics/constants.ts +2 -0
  260. package/src/reachability/clusterReachability.ts +159 -330
  261. package/src/reachability/index.ts +6 -1
  262. package/src/reachability/reachability.types.ts +15 -1
  263. package/src/reachability/reachabilityPeerConnection.ts +418 -0
  264. package/test/unit/spec/hashTree/hashTree.ts +655 -0
  265. package/test/unit/spec/hashTree/hashTreeParser.ts +1524 -0
  266. package/test/unit/spec/hashTree/utils.ts +140 -0
  267. package/test/unit/spec/interceptors/locusRouteToken.ts +27 -0
  268. package/test/unit/spec/locus-info/index.js +851 -16
  269. package/test/unit/spec/meeting/index.js +120 -20
  270. package/test/unit/spec/meeting/utils.js +77 -0
  271. package/test/unit/spec/meetings/index.js +263 -27
  272. package/test/unit/spec/meetings/utils.js +51 -1
  273. package/test/unit/spec/reachability/clusterReachability.ts +404 -137
  274. package/test/unit/spec/reachability/index.ts +3 -3
@@ -0,0 +1,1143 @@
1
+ import {cloneDeep, isEmpty, zip} from 'lodash';
2
+ import HashTree, {LeafDataItem} from './hashTree';
3
+ import LoggerProxy from '../common/logs/logger-proxy';
4
+ import {Enum, HTTP_VERBS} from '../constants';
5
+ import {DataSetNames, EMPTY_HASH} from './constants';
6
+ import {ObjectType, HtMeta, HashTreeObject} from './types';
7
+ import {LocusDTO} from '../locus-info/types';
8
+ import {deleteNestedObjectsWithHtMeta, isSelf} from './utils';
9
+
10
+ export interface DataSet {
11
+ url: string;
12
+ root: string;
13
+ version: number;
14
+ leafCount: number;
15
+ name: string;
16
+ idleMs: number;
17
+ backoff: {
18
+ maxMs: number;
19
+ exponent: number;
20
+ };
21
+ }
22
+
23
+ export interface RootHashMessage {
24
+ dataSets: Array<DataSet>;
25
+ }
26
+ export interface HashTreeMessage {
27
+ dataSets: Array<DataSet>;
28
+ visibleDataSetsUrl: string; // url from which we can get more info about all data sets - now it seems to be visibleDataSetsUrl
29
+ locusStateElements?: Array<HashTreeObject>;
30
+ locusSessionId?: string;
31
+ locusUrl: string;
32
+ }
33
+
34
+ interface InternalDataSet extends DataSet {
35
+ hashTree?: HashTree; // set only for visible data sets
36
+ timer?: ReturnType<typeof setTimeout>;
37
+ }
38
+
39
+ type WebexRequestMethod = (options: Record<string, any>) => Promise<any>;
40
+
41
+ export const LocusInfoUpdateType = {
42
+ OBJECTS_UPDATED: 'OBJECTS_UPDATED',
43
+ MEETING_ENDED: 'MEETING_ENDED',
44
+ } as const;
45
+
46
+ export type LocusInfoUpdateType = Enum<typeof LocusInfoUpdateType>;
47
+ export type LocusInfoUpdateCallback = (
48
+ updateType: LocusInfoUpdateType,
49
+ data?: {updatedObjects: HashTreeObject[]}
50
+ ) => void;
51
+
52
+ /**
53
+ * This error is thrown if we receive information that the meeting has ended while we're processing some hash messages.
54
+ * It's handled internally by HashTreeParser and results in MEETING_ENDED being sent up.
55
+ */
56
+ class MeetingEndedError extends Error {}
57
+
58
+ /**
59
+ * Parses hash tree eventing locus data
60
+ */
61
+ class HashTreeParser {
62
+ dataSets: Record<string, InternalDataSet> = {};
63
+ visibleDataSetsUrl: string; // url from which we can get info about all data sets
64
+ webexRequest: WebexRequestMethod;
65
+ locusInfoUpdateCallback: LocusInfoUpdateCallback;
66
+ visibleDataSets: string[];
67
+ debugId: string;
68
+
69
+ /**
70
+ * Constructor for HashTreeParser
71
+ * @param {Object} options
72
+ * @param {Object} options.initialLocus The initial locus data containing the hash tree information
73
+ */
74
+ constructor(options: {
75
+ initialLocus: {
76
+ dataSets: Array<DataSet>;
77
+ locus: any;
78
+ };
79
+ webexRequest: WebexRequestMethod;
80
+ locusInfoUpdateCallback: LocusInfoUpdateCallback;
81
+ debugId: string;
82
+ }) {
83
+ const {dataSets, locus} = options.initialLocus; // extract dataSets from initialLocus
84
+
85
+ this.debugId = options.debugId;
86
+ this.webexRequest = options.webexRequest;
87
+ this.locusInfoUpdateCallback = options.locusInfoUpdateCallback;
88
+ this.visibleDataSets = locus?.self?.visibleDataSets || [];
89
+
90
+ if (this.visibleDataSets.length === 0) {
91
+ LoggerProxy.logger.warn(
92
+ `HashTreeParser#constructor --> ${this.debugId} No visibleDataSets found in locus.self`
93
+ );
94
+ }
95
+ // object mapping dataset names to arrays of leaf data
96
+ const leafData = this.analyzeLocusHtMeta(locus);
97
+
98
+ LoggerProxy.logger.info(
99
+ `HashTreeParser#constructor --> creating HashTreeParser for datasets: ${JSON.stringify(
100
+ dataSets.map((ds) => ds.name)
101
+ )}`
102
+ );
103
+
104
+ for (const dataSet of dataSets) {
105
+ const {name, leafCount} = dataSet;
106
+
107
+ this.dataSets[name] = {
108
+ ...dataSet,
109
+ hashTree: this.visibleDataSets.includes(name)
110
+ ? new HashTree(leafData[name] || [], leafCount)
111
+ : undefined,
112
+ };
113
+ }
114
+ }
115
+
116
+ /**
117
+ * Initializes a new visible data set by creating a hash tree for it, adding it to all the internal structures,
118
+ * and sending an initial sync request to Locus with empty leaf data - that will trigger Locus to gives us all the data
119
+ * from that dataset (in the response or via messages).
120
+ *
121
+ * @param {DataSet} dataSet The new data set to be added
122
+ * @returns {Promise}
123
+ */
124
+ private initializeNewVisibleDataSet(
125
+ dataSet: DataSet
126
+ ): Promise<{updateType: LocusInfoUpdateType; updatedObjects?: HashTreeObject[]}> {
127
+ if (this.visibleDataSets.includes(dataSet.name)) {
128
+ LoggerProxy.logger.info(
129
+ `HashTreeParser#initializeNewVisibleDataSet --> ${this.debugId} Data set "${dataSet.name}" already exists, skipping init`
130
+ );
131
+
132
+ return Promise.resolve({updateType: LocusInfoUpdateType.OBJECTS_UPDATED, updatedObjects: []});
133
+ }
134
+
135
+ LoggerProxy.logger.info(
136
+ `HashTreeParser#initializeNewVisibleDataSet --> ${this.debugId} Adding visible data set "${dataSet.name}"`
137
+ );
138
+
139
+ this.visibleDataSets.push(dataSet.name);
140
+
141
+ const hashTree = new HashTree([], dataSet.leafCount);
142
+
143
+ this.dataSets[dataSet.name] = {
144
+ ...dataSet,
145
+ hashTree,
146
+ };
147
+
148
+ return this.sendInitializationSyncRequestToLocus(dataSet.name, 'new visible data set');
149
+ }
150
+
151
+ /**
152
+ * Sends a special sync request to Locus with all leaves empty - this is a way to get all the data for a given dataset.
153
+ *
154
+ * @param {string} datasetName - name of the dataset for which to send the request
155
+ * @param {string} debugText - text to include in logs
156
+ * @returns {Promise}
157
+ */
158
+ private sendInitializationSyncRequestToLocus(
159
+ datasetName: string,
160
+ debugText: string
161
+ ): Promise<{updateType: LocusInfoUpdateType; updatedObjects?: HashTreeObject[]}> {
162
+ const dataset = this.dataSets[datasetName];
163
+
164
+ if (!dataset) {
165
+ LoggerProxy.logger.warn(
166
+ `HashTreeParser#sendInitializationSyncRequestToLocus --> ${this.debugId} No data set found for ${datasetName}, cannot send the request for leaf data`
167
+ );
168
+
169
+ return Promise.resolve(null);
170
+ }
171
+
172
+ const emptyLeavesData = new Array(dataset.leafCount).fill([]);
173
+
174
+ LoggerProxy.logger.info(
175
+ `HashTreeParser#sendInitializationSyncRequestToLocus --> ${this.debugId} Sending initial sync request to Locus for data set "${datasetName}" with empty leaf data`
176
+ );
177
+
178
+ return this.sendSyncRequestToLocus(this.dataSets[datasetName], emptyLeavesData).then(
179
+ (syncResponse) => {
180
+ if (syncResponse) {
181
+ return this.parseMessage(
182
+ syncResponse,
183
+ `via empty leaves /sync API call for ${debugText}`
184
+ );
185
+ }
186
+
187
+ return {updateType: LocusInfoUpdateType.OBJECTS_UPDATED, updatedObjects: []};
188
+ }
189
+ );
190
+ }
191
+
192
+ /**
193
+ * Queries Locus for information about all the data sets
194
+ *
195
+ * @param {string} url - url from which we can get info about all data sets
196
+ * @returns {Promise}
197
+ */
198
+ private getAllDataSetsMetadata(url) {
199
+ return this.webexRequest({
200
+ method: HTTP_VERBS.GET,
201
+ uri: url,
202
+ }).then((response) => {
203
+ return response.body.dataSets as Array<DataSet>;
204
+ });
205
+ }
206
+
207
+ /**
208
+ * Initializes the hash tree parser from a message received from Locus.
209
+ *
210
+ * @param {HashTreeMessage} message - initial hash tree message received from Locus
211
+ * @returns {Promise}
212
+ */
213
+ async initializeFromMessage(message: HashTreeMessage) {
214
+ LoggerProxy.logger.info(
215
+ `HashTreeParser#initializeFromMessage --> ${this.debugId} visibleDataSetsUrl=${message.visibleDataSetsUrl}`
216
+ );
217
+ const dataSets = await this.getAllDataSetsMetadata(message.visibleDataSetsUrl);
218
+
219
+ await this.initializeDataSets(dataSets, 'initialization from message');
220
+ }
221
+
222
+ /**
223
+ * Initializes the hash tree parser from GET /loci API response by fetching all data sets metadata
224
+ * first and then doing an initialization sync on each data set
225
+ *
226
+ * This function requires that this.visibleDataSets have been already populated correctly by the constructor.
227
+ *
228
+ * @param {LocusDTO} locus - locus object received from GET /loci
229
+ * @returns {Promise}
230
+ */
231
+ async initializeFromGetLociResponse(locus: LocusDTO) {
232
+ if (!locus?.links?.resources?.visibleDataSets?.url) {
233
+ LoggerProxy.logger.warn(
234
+ `HashTreeParser#initializeFromGetLociResponse --> ${this.debugId} missing visibleDataSets url in GET Loci response, cannot initialize hash trees`
235
+ );
236
+
237
+ return;
238
+ }
239
+
240
+ LoggerProxy.logger.info(
241
+ `HashTreeParser#initializeFromGetLociResponse --> ${this.debugId} visibleDataSets url: ${locus.links.resources.visibleDataSets.url}`
242
+ );
243
+
244
+ const dataSets = await this.getAllDataSetsMetadata(locus.links.resources.visibleDataSets.url);
245
+
246
+ await this.initializeDataSets(dataSets, 'initialization from GET /loci response');
247
+ }
248
+
249
+ /**
250
+ * Initializes data sets by doing an initialization sync on each visible data set that doesn't have a hash tree yet.
251
+ *
252
+ * @param {DataSet[]} dataSets Array of DataSet objects to initialize
253
+ * @param {string} debugText Text to include in logs for debugging purposes
254
+ * @returns {Promise}
255
+ */
256
+ private async initializeDataSets(dataSets: Array<DataSet>, debugText: string) {
257
+ const updatedObjects: HashTreeObject[] = [];
258
+
259
+ for (const dataSet of dataSets) {
260
+ const {name, leafCount} = dataSet;
261
+
262
+ if (!this.dataSets[name]) {
263
+ LoggerProxy.logger.info(
264
+ `HashTreeParser#initializeDataSets --> ${this.debugId} initializing dataset "${name}" (${debugText})`
265
+ );
266
+
267
+ this.dataSets[name] = {
268
+ ...dataSet,
269
+ };
270
+ } else {
271
+ LoggerProxy.logger.info(
272
+ `HashTreeParser#initializeDataSets --> ${this.debugId} dataset "${name}" already exists (${debugText})`
273
+ );
274
+ }
275
+
276
+ if (this.visibleDataSets.includes(name) && !this.dataSets[name].hashTree) {
277
+ LoggerProxy.logger.info(
278
+ `HashTreeParser#initializeDataSets --> ${this.debugId} creating hash tree for visible dataset "${name}" (${debugText})`
279
+ );
280
+ this.dataSets[name].hashTree = new HashTree([], leafCount);
281
+
282
+ // eslint-disable-next-line no-await-in-loop
283
+ const data = await this.sendInitializationSyncRequestToLocus(name, debugText);
284
+
285
+ if (data.updateType === LocusInfoUpdateType.MEETING_ENDED) {
286
+ LoggerProxy.logger.warn(
287
+ `HashTreeParser#initializeDataSets --> ${this.debugId} meeting ended while initializing new visible data set "${name}"`
288
+ );
289
+
290
+ // throw an error, it will be caught higher up and the meeting will be destroyed
291
+ throw new MeetingEndedError();
292
+ }
293
+
294
+ if (data.updateType === LocusInfoUpdateType.OBJECTS_UPDATED) {
295
+ updatedObjects.push(...(data.updatedObjects || []));
296
+ }
297
+ }
298
+ }
299
+
300
+ this.callLocusInfoUpdateCallback({
301
+ updateType: LocusInfoUpdateType.OBJECTS_UPDATED,
302
+ updatedObjects,
303
+ });
304
+ }
305
+
306
+ /**
307
+ * Each dataset exists at a different place in the dto
308
+ * iterate recursively over the locus and if it has a htMeta key,
309
+ * create an object with the type, id and version and add it to the appropriate leafData array
310
+ *
311
+ * @param {any} locus - The current part of the locus being processed
312
+ * @param {Object} [options]
313
+ * @param {boolean} [options.copyData=false] - Whether to copy the data for each leaf into returned result
314
+ * @returns {any} - An object mapping dataset names to arrays of leaf data
315
+ */
316
+ private analyzeLocusHtMeta(locus: any, options?: {copyData?: boolean}) {
317
+ const {copyData = false} = options || {};
318
+ // object mapping dataset names to arrays of leaf data
319
+ const leafInfo: Record<
320
+ string,
321
+ Array<{type: ObjectType; id: number; version: number; data?: any}>
322
+ > = {};
323
+
324
+ const findAndStoreMetaData = (currentLocusPart: any) => {
325
+ if (typeof currentLocusPart !== 'object' || currentLocusPart === null) {
326
+ return;
327
+ }
328
+
329
+ if (currentLocusPart.htMeta && currentLocusPart.htMeta.dataSetNames) {
330
+ const {type, id, version} = currentLocusPart.htMeta.elementId;
331
+ const {dataSetNames} = currentLocusPart.htMeta;
332
+ const newLeafInfo: {type: ObjectType; id: number; version: number; data?: any} = {
333
+ type,
334
+ id,
335
+ version,
336
+ };
337
+
338
+ if (copyData) {
339
+ newLeafInfo.data = cloneDeep(currentLocusPart);
340
+
341
+ // remove any nested other objects that have their own htMeta
342
+ deleteNestedObjectsWithHtMeta(newLeafInfo.data);
343
+ }
344
+
345
+ for (const dataSetName of dataSetNames) {
346
+ if (!leafInfo[dataSetName]) {
347
+ leafInfo[dataSetName] = [];
348
+ }
349
+ leafInfo[dataSetName].push(newLeafInfo);
350
+ }
351
+ }
352
+
353
+ if (Array.isArray(currentLocusPart)) {
354
+ for (const item of currentLocusPart) {
355
+ findAndStoreMetaData(item);
356
+ }
357
+ } else {
358
+ for (const key of Object.keys(currentLocusPart)) {
359
+ if (Object.prototype.hasOwnProperty.call(currentLocusPart, key)) {
360
+ findAndStoreMetaData(currentLocusPart[key]);
361
+ }
362
+ }
363
+ }
364
+ };
365
+
366
+ findAndStoreMetaData(locus);
367
+
368
+ return leafInfo;
369
+ }
370
+
371
+ /**
372
+ * Checks if the provided hash tree message indicates the end of the meeting and that there won't be any more updates.
373
+ *
374
+ * @param {HashTreeMessage} message - The hash tree message to check
375
+ * @returns {boolean} - Returns true if the message indicates the end of the meeting, false otherwise
376
+ */
377
+ private isEndMessage(message: HashTreeMessage) {
378
+ const mainDataSet = message.dataSets.find(
379
+ (dataSet) => dataSet.name.toLowerCase() === DataSetNames.MAIN
380
+ );
381
+
382
+ if (
383
+ mainDataSet &&
384
+ mainDataSet.leafCount === 1 &&
385
+ mainDataSet.root === EMPTY_HASH &&
386
+ this.dataSets[DataSetNames.MAIN].version < mainDataSet.version
387
+ ) {
388
+ // this is a special way for Locus to indicate that this meeting has ended
389
+ return true;
390
+ }
391
+
392
+ return false;
393
+ }
394
+
395
+ /**
396
+ * Handles the root hash heartbeat message
397
+ *
398
+ * @param {RootHashMessage} message - The root hash heartbeat message
399
+ * @returns {void}
400
+ */
401
+ private handleRootHashHeartBeatMessage(message: RootHashMessage): void {
402
+ const {dataSets} = message;
403
+
404
+ LoggerProxy.logger.info(
405
+ `HashTreeParser#handleRootHashMessage --> ${
406
+ this.debugId
407
+ } Received heartbeat root hash message with data sets: ${JSON.stringify(
408
+ dataSets.map(({name, root, leafCount, version}) => ({
409
+ name,
410
+ root,
411
+ leafCount,
412
+ version,
413
+ }))
414
+ )}`
415
+ );
416
+
417
+ dataSets.forEach((dataSet) => {
418
+ this.updateDataSetInfo(dataSet);
419
+ this.runSyncAlgorithm(dataSet);
420
+ });
421
+ }
422
+
423
+ /**
424
+ * This method should be called when we receive a partial locus DTO that contains dataSets and htMeta information
425
+ * It updates the hash trees with the new leaf data based on the received Locus
426
+ *
427
+ * @param {Object} update - The locus update containing data sets and locus information
428
+ * @returns {void}
429
+ */
430
+ handleLocusUpdate(update: {dataSets?: Array<DataSet>; locus: any}): void {
431
+ const {dataSets, locus} = update;
432
+
433
+ if (!dataSets) {
434
+ LoggerProxy.logger.warn(
435
+ `HashTreeParser#handleLocusUpdate --> ${this.debugId} received hash tree update without dataSets`
436
+ );
437
+ }
438
+ for (const dataSet of dataSets) {
439
+ this.updateDataSetInfo(dataSet);
440
+ }
441
+ const updatedObjects: HashTreeObject[] = [];
442
+
443
+ // first, analyze the locus object to extract the hash tree objects' htMeta and data from it
444
+ const leafInfo = this.analyzeLocusHtMeta(locus, {copyData: true});
445
+
446
+ // then process the data in hash trees, if it is a new version, then add it to updatedObjects
447
+ Object.keys(leafInfo).forEach((dataSetName) => {
448
+ if (this.dataSets[dataSetName]) {
449
+ if (this.dataSets[dataSetName].hashTree) {
450
+ const appliedChangesList = this.dataSets[dataSetName].hashTree.putItems(
451
+ leafInfo[dataSetName].map((leaf) => ({
452
+ id: leaf.id,
453
+ type: leaf.type,
454
+ version: leaf.version,
455
+ }))
456
+ );
457
+
458
+ zip(appliedChangesList, leafInfo[dataSetName]).forEach(([changeApplied, leaf]) => {
459
+ if (changeApplied) {
460
+ updatedObjects.push({
461
+ htMeta: {
462
+ elementId: {
463
+ type: leaf.type,
464
+ id: leaf.id,
465
+ version: leaf.version,
466
+ },
467
+ dataSetNames: [dataSetName],
468
+ },
469
+ data: leaf.data,
470
+ });
471
+ }
472
+ });
473
+ } else {
474
+ // no hash tree means that the data set is not visible
475
+ LoggerProxy.logger.warn(
476
+ `HashTreeParser#handleLocusUpdate --> ${this.debugId} received leaf data for data set "${dataSetName}" that has no hash tree created, ignoring`
477
+ );
478
+ }
479
+ } else {
480
+ LoggerProxy.logger.warn(
481
+ `HashTreeParser#handleLocusUpdate --> ${this.debugId} received leaf data for unknown data set "${dataSetName}", ignoring`
482
+ );
483
+ }
484
+ });
485
+
486
+ if (updatedObjects.length === 0) {
487
+ LoggerProxy.logger.info(
488
+ `HashTreeParser#handleLocusUpdate --> ${this.debugId} No objects updated as a result of received API response`
489
+ );
490
+ } else {
491
+ this.callLocusInfoUpdateCallback({
492
+ updateType: LocusInfoUpdateType.OBJECTS_UPDATED,
493
+ updatedObjects,
494
+ });
495
+ }
496
+
497
+ // todo: once Locus design on how visible data sets will be communicated in subsequent API responses is confirmed,
498
+ // we'll need to check here if visible data sets have changed and update this.visibleDataSets, remove/create hash trees etc
499
+ }
500
+
501
+ /**
502
+ * Updates the internal data set information based on the received data set from Locus.
503
+ *
504
+ * @param {DataSet} receivedDataSet - The latest data set information received from Locus to update the internal state.
505
+ * @returns {void}
506
+ */
507
+ private updateDataSetInfo(receivedDataSet: DataSet) {
508
+ if (!this.dataSets[receivedDataSet.name]) {
509
+ this.dataSets[receivedDataSet.name] = {
510
+ ...receivedDataSet,
511
+ };
512
+
513
+ LoggerProxy.logger.info(
514
+ `HashTreeParser#handleMessage --> ${this.debugId} created entry for "${receivedDataSet.name}" dataset: version=${receivedDataSet.version}, root=${receivedDataSet.root}`
515
+ );
516
+
517
+ return;
518
+ }
519
+ // update our version of the dataSet
520
+ if (this.dataSets[receivedDataSet.name].version < receivedDataSet.version) {
521
+ this.dataSets[receivedDataSet.name].version = receivedDataSet.version;
522
+ this.dataSets[receivedDataSet.name].root = receivedDataSet.root;
523
+ this.dataSets[receivedDataSet.name].idleMs = receivedDataSet.idleMs;
524
+ this.dataSets[receivedDataSet.name].backoff = {
525
+ maxMs: receivedDataSet.backoff.maxMs,
526
+ exponent: receivedDataSet.backoff.exponent,
527
+ };
528
+ LoggerProxy.logger.info(
529
+ `HashTreeParser#handleMessage --> ${this.debugId} updated "${receivedDataSet.name}" to version=${receivedDataSet.version}, root=${receivedDataSet.root}`
530
+ );
531
+ }
532
+ }
533
+
534
+ /**
535
+ * Checks for changes in the visible data sets based on the updated objects.
536
+ * @param {HashTreeObject[]} updatedObjects - The list of updated hash tree objects.
537
+ * @returns {Object} An object containing the removed and added visible data sets.
538
+ */
539
+ private checkForVisibleDataSetChanges(updatedObjects: HashTreeObject[]) {
540
+ let removedDataSets: string[] = [];
541
+ let addedDataSets: string[] = [];
542
+
543
+ // visibleDataSets can only be changed by self object updates
544
+ updatedObjects.forEach((object) => {
545
+ // todo: in the future visibleDataSets will be in "Metadata" object, not in "self"
546
+ if (isSelf(object) && object.data?.visibleDataSets) {
547
+ const newVisibleDataSets = object.data.visibleDataSets;
548
+
549
+ removedDataSets = this.visibleDataSets.filter((ds) => !newVisibleDataSets.includes(ds));
550
+ addedDataSets = newVisibleDataSets.filter((ds) => !this.visibleDataSets.includes(ds));
551
+
552
+ if (removedDataSets.length > 0 || addedDataSets.length > 0) {
553
+ LoggerProxy.logger.info(
554
+ `HashTreeParser#checkForVisibleDataSetChanges --> ${
555
+ this.debugId
556
+ } visible data sets change: removed: ${removedDataSets.join(
557
+ ', '
558
+ )}, added: ${addedDataSets.join(', ')}`
559
+ );
560
+ }
561
+ }
562
+ });
563
+
564
+ return {
565
+ changeDetected: removedDataSets.length > 0 || addedDataSets.length > 0,
566
+ removedDataSets,
567
+ addedDataSets,
568
+ };
569
+ }
570
+
571
+ /**
572
+ * Deletes the hash tree for the specified data set.
573
+ *
574
+ * @param {string} dataSetName name of the data set to delete
575
+ * @returns {void}
576
+ */
577
+ private deleteHashTree(dataSetName: string) {
578
+ this.dataSets[dataSetName].hashTree = undefined;
579
+
580
+ // we also need to stop the timer as there is no hash tree anymore to sync
581
+ if (this.dataSets[dataSetName].timer) {
582
+ clearTimeout(this.dataSets[dataSetName].timer);
583
+ this.dataSets[dataSetName].timer = undefined;
584
+ }
585
+ }
586
+
587
+ /**
588
+ * Adds entries to the passed in updateObjects array
589
+ * for the changes that result from removing visible data sets and creates hash
590
+ * trees for the new visible data sets, but without populating the hash trees.
591
+ *
592
+ * This function is synchronous. If we are missing information about some new
593
+ * visible data sets and they require async initialization, the names of these data sets
594
+ * are returned in an array.
595
+ *
596
+ * @param {string[]} removedDataSets - The list of removed data sets.
597
+ * @param {string[]} addedDataSets - The list of added data sets.
598
+ * @param {HashTreeObject[]} updatedObjects - The list of updated hash tree objects to which changes will be added.
599
+ * @returns {string[]} names of data sets that couldn't be initialized synchronously
600
+ */
601
+ private processVisibleDataSetChanges(
602
+ removedDataSets: string[],
603
+ addedDataSets: string[],
604
+ updatedObjects: HashTreeObject[]
605
+ ): string[] {
606
+ const dataSetsRequiringInitialization = [];
607
+
608
+ // if a visible data set was removed, we need to tell our client that all objects from it are removed
609
+ const removedObjects: HashTreeObject[] = [];
610
+
611
+ removedDataSets.forEach((ds) => {
612
+ if (this.dataSets[ds]?.hashTree) {
613
+ for (let i = 0; i < this.dataSets[ds].hashTree.numLeaves; i += 1) {
614
+ removedObjects.push(
615
+ ...this.dataSets[ds].hashTree.getLeafData(i).map((elementId) => ({
616
+ htMeta: {
617
+ elementId,
618
+ dataSetNames: [ds],
619
+ },
620
+ data: null,
621
+ }))
622
+ );
623
+ }
624
+
625
+ this.deleteHashTree(ds);
626
+ }
627
+ });
628
+ this.visibleDataSets = this.visibleDataSets.filter((vds) => !removedDataSets.includes(vds));
629
+ updatedObjects.push(...removedObjects);
630
+
631
+ // now setup the new visible data sets
632
+ for (const ds of addedDataSets) {
633
+ const dataSetInfo = this.dataSets[ds];
634
+
635
+ if (dataSetInfo) {
636
+ if (this.visibleDataSets.includes(dataSetInfo.name)) {
637
+ LoggerProxy.logger.info(
638
+ `HashTreeParser#processVisibleDataSetChanges --> ${this.debugId} Data set "${ds}" is already visible, skipping`
639
+ );
640
+
641
+ // eslint-disable-next-line no-continue
642
+ continue;
643
+ }
644
+
645
+ LoggerProxy.logger.info(
646
+ `HashTreeParser#processVisibleDataSetChanges --> ${this.debugId} Adding visible data set "${ds}"`
647
+ );
648
+
649
+ this.visibleDataSets.push(ds);
650
+
651
+ const hashTree = new HashTree([], dataSetInfo.leafCount);
652
+
653
+ this.dataSets[dataSetInfo.name] = {
654
+ ...dataSetInfo,
655
+ hashTree,
656
+ };
657
+ } else {
658
+ LoggerProxy.logger.info(
659
+ `HashTreeParser#processVisibleDataSetChanges --> ${this.debugId} visible data set "${ds}" added but no info about it in our dataSets structures`
660
+ );
661
+ // todo: add a metric here
662
+ dataSetsRequiringInitialization.push(ds);
663
+ }
664
+ }
665
+
666
+ return dataSetsRequiringInitialization;
667
+ }
668
+
669
+ /**
670
+ * Adds entries to the passed in updateObjects array
671
+ * for the changes that result from adding and removing visible data sets.
672
+ *
673
+ * @param {HashTreeMessage} message - The hash tree message that triggered the visible data set changes.
674
+ * @param {string[]} addedDataSets - The list of added data sets.
675
+ * @returns {Promise<void>}
676
+ */
677
+ private async initializeNewVisibleDataSets(
678
+ message: HashTreeMessage,
679
+ addedDataSets: string[]
680
+ ): Promise<void> {
681
+ const allDataSets = await this.getAllDataSetsMetadata(message.visibleDataSetsUrl);
682
+
683
+ for (const ds of addedDataSets) {
684
+ const dataSetInfo = allDataSets.find((d) => d.name === ds);
685
+
686
+ LoggerProxy.logger.info(
687
+ `HashTreeParser#initializeNewVisibleDataSets --> ${this.debugId} initializing data set "${ds}"`
688
+ );
689
+
690
+ if (!dataSetInfo) {
691
+ LoggerProxy.logger.warn(
692
+ `HashTreeParser#handleHashTreeMessage --> ${this.debugId} missing info about data set "${ds}" in Locus response from visibleDataSetsUrl`
693
+ );
694
+ } else {
695
+ // we're awaiting in a loop, because in practice there will be only one new data set at a time,
696
+ // so no point in trying to parallelize this
697
+ // eslint-disable-next-line no-await-in-loop
698
+ const updates = await this.initializeNewVisibleDataSet(dataSetInfo);
699
+
700
+ this.callLocusInfoUpdateCallback(updates);
701
+ }
702
+ }
703
+ }
704
+
705
+ /**
706
+ * Parses incoming hash tree messages, updates the hash trees and returns information about the changes
707
+ *
708
+ * @param {HashTreeMessage} message - The hash tree message containing data sets and objects to be processed
709
+ * @param {string} [debugText] - Optional debug text to include in logs
710
+ * @returns {Promise}
711
+ */
712
+ private async parseMessage(
713
+ message: HashTreeMessage,
714
+ debugText?: string
715
+ ): Promise<{updateType: LocusInfoUpdateType; updatedObjects?: HashTreeObject[]}> {
716
+ const {dataSets, visibleDataSetsUrl} = message;
717
+
718
+ LoggerProxy.logger.info(
719
+ `HashTreeParser#parseMessage --> ${this.debugId} received message ${debugText || ''}:`,
720
+ message
721
+ );
722
+ if (message.locusStateElements?.length === 0) {
723
+ LoggerProxy.logger.warn(
724
+ `HashTreeParser#parseMessage --> ${this.debugId} got empty locusStateElements!!!`
725
+ );
726
+ // todo: send a metric
727
+ }
728
+
729
+ // first, update our metadata about the datasets with info from the message
730
+ this.visibleDataSetsUrl = visibleDataSetsUrl;
731
+ dataSets.forEach((dataSet) => this.updateDataSetInfo(dataSet));
732
+
733
+ if (this.isEndMessage(message)) {
734
+ LoggerProxy.logger.info(
735
+ `HashTreeParser#parseMessage --> ${this.debugId} received END message`
736
+ );
737
+ this.stopAllTimers();
738
+
739
+ return {updateType: LocusInfoUpdateType.MEETING_ENDED};
740
+ }
741
+
742
+ let isRosterDropped = false;
743
+ const updatedObjects: HashTreeObject[] = [];
744
+
745
+ // when we detect new visible datasets, it may be that the metadata about them is not
746
+ // available in the message, they will require separate async initialization
747
+ let dataSetsRequiringInitialization = [];
748
+
749
+ // first find out if there are any visible data set changes - they're signalled in SELF object updates
750
+ const selfUpdates = (message.locusStateElements || []).filter((object) =>
751
+ // todo: SPARK-744859 once Locus supports it, we will filter for "Metadata" type here instead of "self"
752
+ isSelf(object)
753
+ );
754
+
755
+ if (selfUpdates.length > 0) {
756
+ const updatedSelfObjects = [];
757
+
758
+ selfUpdates.forEach((object) => {
759
+ // todo: once Locus supports it, we will use the "view" field here instead of dataSetNames
760
+ for (const dataSetName of object.htMeta.dataSetNames) {
761
+ const hashTree = this.dataSets[dataSetName]?.hashTree;
762
+
763
+ if (hashTree && object.data) {
764
+ if (hashTree.putItem(object.htMeta.elementId)) {
765
+ updatedSelfObjects.push(object);
766
+ }
767
+ }
768
+ }
769
+ });
770
+
771
+ updatedObjects.push(...updatedSelfObjects);
772
+
773
+ const {changeDetected, removedDataSets, addedDataSets} =
774
+ this.checkForVisibleDataSetChanges(updatedSelfObjects);
775
+
776
+ if (changeDetected) {
777
+ dataSetsRequiringInitialization = this.processVisibleDataSetChanges(
778
+ removedDataSets,
779
+ addedDataSets,
780
+ updatedObjects
781
+ );
782
+ }
783
+ }
784
+
785
+ // by this point we now have this.dataSets setup for data sets from this message
786
+ // and hash trees created for the new visible data sets,
787
+ // so we can now process all the updates from the message
788
+ dataSets.forEach((dataSet) => {
789
+ if (this.dataSets[dataSet.name]) {
790
+ const {hashTree} = this.dataSets[dataSet.name];
791
+
792
+ if (hashTree) {
793
+ const locusStateElementsForThisSet = message.locusStateElements.filter((object) =>
794
+ object.htMeta.dataSetNames.includes(dataSet.name)
795
+ );
796
+
797
+ const appliedChangesList = hashTree.updateItems(
798
+ locusStateElementsForThisSet.map((object) =>
799
+ object.data
800
+ ? {operation: 'update', item: object.htMeta.elementId}
801
+ : {operation: 'remove', item: object.htMeta.elementId}
802
+ )
803
+ );
804
+
805
+ zip(appliedChangesList, locusStateElementsForThisSet).forEach(
806
+ ([changeApplied, object]) => {
807
+ if (changeApplied) {
808
+ if (isSelf(object) && !object.data) {
809
+ isRosterDropped = true;
810
+ }
811
+ // add to updatedObjects so that our locus DTO will get updated with the new object
812
+ updatedObjects.push(object);
813
+ }
814
+ }
815
+ );
816
+ } else {
817
+ LoggerProxy.logger.info(
818
+ `Locus-info:index#parseMessage --> ${this.debugId} unexpected (not visible) dataSet ${dataSet.name} received in hash tree message`
819
+ );
820
+ }
821
+ }
822
+
823
+ if (!isRosterDropped) {
824
+ this.runSyncAlgorithm(dataSet);
825
+ }
826
+ });
827
+
828
+ if (isRosterDropped) {
829
+ LoggerProxy.logger.info(
830
+ `HashTreeParser#parseMessage --> ${this.debugId} detected roster drop`
831
+ );
832
+ this.stopAllTimers();
833
+
834
+ // in case of roster drop we don't care about other updates
835
+ return {updateType: LocusInfoUpdateType.MEETING_ENDED};
836
+ }
837
+
838
+ if (dataSetsRequiringInitialization.length > 0) {
839
+ // there are some data sets that we need to initialize asynchronously
840
+ queueMicrotask(() => {
841
+ this.initializeNewVisibleDataSets(message, dataSetsRequiringInitialization);
842
+ });
843
+ }
844
+
845
+ if (updatedObjects.length === 0) {
846
+ LoggerProxy.logger.info(
847
+ `HashTreeParser#parseMessage --> ${this.debugId} No objects updated as a result of received message`
848
+ );
849
+ }
850
+
851
+ return {updateType: LocusInfoUpdateType.OBJECTS_UPDATED, updatedObjects};
852
+ }
853
+
854
+ /**
855
+ * Handles incoming hash tree messages, updates the hash trees and calls locusInfoUpdateCallback
856
+ *
857
+ * @param {HashTreeMessage} message - The hash tree message containing data sets and objects to be processed
858
+ * @param {string} [debugText] - Optional debug text to include in logs
859
+ * @returns {void}
860
+ */
861
+ async handleMessage(message: HashTreeMessage, debugText?: string): Promise<void> {
862
+ if (message.locusStateElements === undefined) {
863
+ this.handleRootHashHeartBeatMessage(message);
864
+ } else {
865
+ const updates = await this.parseMessage(message, debugText);
866
+
867
+ this.callLocusInfoUpdateCallback(updates);
868
+ }
869
+ }
870
+
871
+ /**
872
+ * Calls the updateInfo callback if there are any updates to report
873
+ *
874
+ * @param {Object} updates parsed from a Locus message
875
+ * @returns {void}
876
+ */
877
+ private callLocusInfoUpdateCallback(updates: {
878
+ updateType: LocusInfoUpdateType;
879
+ updatedObjects?: HashTreeObject[];
880
+ }) {
881
+ const {updateType, updatedObjects} = updates;
882
+
883
+ if (updateType !== LocusInfoUpdateType.OBJECTS_UPDATED || updatedObjects?.length > 0) {
884
+ this.locusInfoUpdateCallback(updateType, {updatedObjects});
885
+ }
886
+ }
887
+
888
+ /**
889
+ * Calculates a weighted backoff time that should be used for syncs
890
+ *
891
+ * @param {Object} backoff - The backoff configuration containing maxMs and exponent
892
+ * @returns {number} - A weighted backoff time based on the provided configuration, using algorithm supplied by Locus team
893
+ */
894
+ private getWeightedBackoffTime(backoff: {maxMs: number; exponent: number}): number {
895
+ const {maxMs, exponent} = backoff;
896
+
897
+ const randomValue = Math.random();
898
+
899
+ return Math.round(randomValue ** exponent * maxMs);
900
+ }
901
+
902
+ /**
903
+ * Runs the sync algorithm for the given data set.
904
+ *
905
+ * @param {DataSet} receivedDataSet - The data set to run the sync algorithm for.
906
+ * @returns {void}
907
+ */
908
+ private runSyncAlgorithm(receivedDataSet: DataSet) {
909
+ const dataSet = this.dataSets[receivedDataSet.name];
910
+
911
+ if (!dataSet) {
912
+ LoggerProxy.logger.warn(
913
+ `HashTreeParser#runSyncAlgorithm --> ${this.debugId} No data set found for ${receivedDataSet.name}, skipping sync algorithm`
914
+ );
915
+
916
+ return;
917
+ }
918
+
919
+ if (!dataSet.hashTree) {
920
+ LoggerProxy.logger.info(
921
+ `HashTreeParser#runSyncAlgorithm --> ${this.debugId} Data set "${dataSet.name}" has no hash tree, skipping sync algorithm`
922
+ );
923
+
924
+ return;
925
+ }
926
+
927
+ dataSet.hashTree.resize(receivedDataSet.leafCount);
928
+
929
+ // temporary log for the workshop // todo: remove
930
+ const ourCurrentRootHash = dataSet.hashTree.getRootHash();
931
+ LoggerProxy.logger.info(
932
+ `HashTreeParser#runSyncAlgorithm --> ${this.debugId} dataSet="${dataSet.name}" version=${dataSet.version} hashes before starting timer: ours=${ourCurrentRootHash} Locus=${dataSet.root}`
933
+ );
934
+
935
+ const delay = dataSet.idleMs + this.getWeightedBackoffTime(dataSet.backoff);
936
+
937
+ if (delay > 0) {
938
+ if (dataSet.timer) {
939
+ clearTimeout(dataSet.timer);
940
+ }
941
+
942
+ LoggerProxy.logger.info(
943
+ `HashTreeParser#runSyncAlgorithm --> ${this.debugId} setting "${dataSet.name}" sync timer for ${delay}`
944
+ );
945
+
946
+ dataSet.timer = setTimeout(async () => {
947
+ dataSet.timer = undefined;
948
+
949
+ if (!dataSet.hashTree) {
950
+ LoggerProxy.logger.warn(
951
+ `HashTreeParser#runSyncAlgorithm --> ${this.debugId} Data set "${dataSet.name}" no longer has a hash tree, cannot run sync algorithm`
952
+ );
953
+
954
+ return;
955
+ }
956
+
957
+ const rootHash = dataSet.hashTree.getRootHash();
958
+
959
+ if (dataSet.root !== rootHash) {
960
+ LoggerProxy.logger.info(
961
+ `HashTreeParser#runSyncAlgorithm --> ${this.debugId} Root hash mismatch: received=${dataSet.root}, ours=${rootHash}, syncing data set "${dataSet.name}"`
962
+ );
963
+
964
+ const mismatchedLeavesData: Record<number, LeafDataItem[]> = {};
965
+
966
+ if (dataSet.leafCount !== 1) {
967
+ let receivedHashes;
968
+
969
+ try {
970
+ // request hashes from sender
971
+ const {hashes, dataSet: latestDataSetInfo} = await this.getHashesFromLocus(
972
+ dataSet.name
973
+ );
974
+
975
+ receivedHashes = hashes;
976
+
977
+ dataSet.hashTree.resize(latestDataSetInfo.leafCount);
978
+ } catch (error) {
979
+ if (error.statusCode === 409) {
980
+ // this is a leaf count mismatch, we should do nothing, just wait for another heartbeat message from Locus
981
+ LoggerProxy.logger.info(
982
+ `HashTreeParser#getHashesFromLocus --> ${this.debugId} Got 409 when fetching hashes for data set "${dataSet.name}": ${error.message}`
983
+ );
984
+
985
+ return;
986
+ }
987
+ throw error;
988
+ }
989
+
990
+ // identify mismatched leaves
991
+ const mismatchedLeaveIndexes = dataSet.hashTree.diffHashes(receivedHashes);
992
+
993
+ mismatchedLeaveIndexes.forEach((index) => {
994
+ mismatchedLeavesData[index] = dataSet.hashTree.getLeafData(index);
995
+ });
996
+ } else {
997
+ mismatchedLeavesData[0] = dataSet.hashTree.getLeafData(0);
998
+ }
999
+ // request sync for mismatched leaves
1000
+ if (Object.keys(mismatchedLeavesData).length > 0) {
1001
+ const syncResponse = await this.sendSyncRequestToLocus(dataSet, mismatchedLeavesData);
1002
+
1003
+ // sync API may return nothing (in that case data will arrive via messages)
1004
+ // or it may return a response in the same format as messages
1005
+ if (syncResponse) {
1006
+ this.handleMessage(syncResponse, 'via sync API');
1007
+ }
1008
+ }
1009
+ } else {
1010
+ LoggerProxy.logger.info(
1011
+ `HashTreeParser#runSyncAlgorithm --> ${this.debugId} "${dataSet.name}" root hash matching: ${rootHash}, version=${dataSet.version}`
1012
+ );
1013
+ }
1014
+ }, delay);
1015
+ } else {
1016
+ LoggerProxy.logger.info(
1017
+ `HashTreeParser#runSyncAlgorithm --> ${this.debugId} No delay for "${dataSet.name}" data set, skipping sync timer reset/setup`
1018
+ );
1019
+ }
1020
+ }
1021
+
1022
+ /**
1023
+ * Stops all timers for the data sets to prevent any further sync attempts.
1024
+ * @returns {void}
1025
+ */
1026
+ private stopAllTimers() {
1027
+ Object.values(this.dataSets).forEach((dataSet) => {
1028
+ if (dataSet.timer) {
1029
+ clearTimeout(dataSet.timer);
1030
+ dataSet.timer = undefined;
1031
+ }
1032
+ });
1033
+ }
1034
+
1035
+ /**
1036
+ * Gets the current hashes from the locus for a specific data set.
1037
+ * @param {string} dataSetName
1038
+ * @returns {string[]}
1039
+ */
1040
+ private getHashesFromLocus(dataSetName: string) {
1041
+ LoggerProxy.logger.info(
1042
+ `HashTreeParser#getHashesFromLocus --> ${this.debugId} Requesting hashes for data set "${dataSetName}"`
1043
+ );
1044
+
1045
+ const dataSet = this.dataSets[dataSetName];
1046
+
1047
+ const url = `${dataSet.url}/hashtree`;
1048
+
1049
+ return this.webexRequest({
1050
+ method: HTTP_VERBS.GET,
1051
+ uri: url,
1052
+ })
1053
+ .then((response) => {
1054
+ const hashes = response.body?.hashes as string[] | undefined;
1055
+ const dataSetFromResponse = response.body?.dataSet;
1056
+
1057
+ if (!hashes || !Array.isArray(hashes)) {
1058
+ LoggerProxy.logger.warn(
1059
+ `HashTreeParser#getHashesFromLocus --> ${this.debugId} Locus returned invalid hashes, response body=`,
1060
+ response.body
1061
+ );
1062
+ throw new Error(`Locus returned invalid hashes: ${hashes}`);
1063
+ }
1064
+
1065
+ LoggerProxy.logger.info(
1066
+ `HashTreeParser#getHashesFromLocus --> ${
1067
+ this.debugId
1068
+ } Received hashes for data set "${dataSetName}": ${JSON.stringify(hashes)}`
1069
+ );
1070
+
1071
+ return {
1072
+ hashes,
1073
+ dataSet: dataSetFromResponse as DataSet,
1074
+ };
1075
+ })
1076
+ .catch((error) => {
1077
+ LoggerProxy.logger.error(
1078
+ `HashTreeParser#getHashesFromLocus --> ${this.debugId} Error ${error.statusCode} fetching hashes for data set "${dataSetName}":`,
1079
+ error
1080
+ );
1081
+ throw error;
1082
+ });
1083
+ }
1084
+
1085
+ /**
1086
+ * Sends a sync request to Locus for the specified data set.
1087
+ *
1088
+ * @param {InternalDataSet} dataSet The data set to sync.
1089
+ * @param {Record<number, LeafDataItem[]>} mismatchedLeavesData The mismatched leaves data to include in the sync request.
1090
+ * @returns {Promise<HashTreeMessage|null>}
1091
+ */
1092
+ private sendSyncRequestToLocus(
1093
+ dataSet: InternalDataSet,
1094
+ mismatchedLeavesData: Record<number, LeafDataItem[]>
1095
+ ): Promise<HashTreeMessage | null> {
1096
+ LoggerProxy.logger.info(
1097
+ `HashTreeParser#sendSyncRequestToLocus --> ${this.debugId} Sending sync request for data set "${dataSet.name}"`
1098
+ );
1099
+
1100
+ const url = `${dataSet.url}/sync`;
1101
+ const body = {
1102
+ leafCount: dataSet.leafCount,
1103
+ leafDataEntries: [],
1104
+ };
1105
+
1106
+ Object.keys(mismatchedLeavesData).forEach((index) => {
1107
+ body.leafDataEntries.push({
1108
+ leafIndex: parseInt(index, 10),
1109
+ elementIds: mismatchedLeavesData[index],
1110
+ });
1111
+ });
1112
+
1113
+ return this.webexRequest({
1114
+ method: HTTP_VERBS.POST,
1115
+ uri: url,
1116
+ body,
1117
+ })
1118
+ .then((resp) => {
1119
+ LoggerProxy.logger.info(
1120
+ `HashTreeParser#sendSyncRequestToLocus --> ${this.debugId} Sync request succeeded for "${dataSet.name}"`
1121
+ );
1122
+
1123
+ if (!resp.body || isEmpty(resp.body)) {
1124
+ LoggerProxy.logger.info(
1125
+ `HashTreeParser#sendSyncRequestToLocus --> ${this.debugId} Got ${resp.statusCode} with empty body for sync request for data set "${dataSet.name}", data should arrive via messages`
1126
+ );
1127
+
1128
+ return null;
1129
+ }
1130
+
1131
+ return resp.body as HashTreeMessage;
1132
+ })
1133
+ .catch((error) => {
1134
+ LoggerProxy.logger.error(
1135
+ `HashTreeParser#sendSyncRequestToLocus --> ${this.debugId} Error ${error.statusCode} sending sync request for data set "${dataSet.name}":`,
1136
+ error
1137
+ );
1138
+ throw error;
1139
+ });
1140
+ }
1141
+ }
1142
+
1143
+ export default HashTreeParser;