@webex/plugin-meetings 3.12.0-next.9 → 3.12.0-task-refactor.1

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 (201) hide show
  1. package/dist/annotation/index.js +5 -14
  2. package/dist/annotation/index.js.map +1 -1
  3. package/dist/breakouts/breakout.js +1 -1
  4. package/dist/breakouts/index.js +1 -1
  5. package/dist/config.js +2 -8
  6. package/dist/config.js.map +1 -1
  7. package/dist/constants.js +6 -29
  8. package/dist/constants.js.map +1 -1
  9. package/dist/hashTree/hashTreeParser.js +29 -1563
  10. package/dist/hashTree/hashTreeParser.js.map +1 -1
  11. package/dist/hashTree/types.js +3 -13
  12. package/dist/hashTree/types.js.map +1 -1
  13. package/dist/index.js +2 -11
  14. package/dist/index.js.map +1 -1
  15. package/dist/interceptors/index.js +0 -7
  16. package/dist/interceptors/index.js.map +1 -1
  17. package/dist/interceptors/locusRouteToken.js +5 -27
  18. package/dist/interceptors/locusRouteToken.js.map +1 -1
  19. package/dist/interpretation/index.js +2 -2
  20. package/dist/interpretation/index.js.map +1 -1
  21. package/dist/interpretation/siLanguage.js +1 -1
  22. package/dist/locus-info/controlsUtils.js +3 -7
  23. package/dist/locus-info/controlsUtils.js.map +1 -1
  24. package/dist/locus-info/index.js +247 -642
  25. package/dist/locus-info/index.js.map +1 -1
  26. package/dist/locus-info/selfUtils.js +0 -1
  27. package/dist/locus-info/selfUtils.js.map +1 -1
  28. package/dist/locus-info/types.js.map +1 -1
  29. package/dist/media/MediaConnectionAwaiter.js +1 -57
  30. package/dist/media/MediaConnectionAwaiter.js.map +1 -1
  31. package/dist/media/properties.js +2 -4
  32. package/dist/media/properties.js.map +1 -1
  33. package/dist/meeting/in-meeting-actions.js +1 -7
  34. package/dist/meeting/in-meeting-actions.js.map +1 -1
  35. package/dist/meeting/index.js +1036 -1481
  36. package/dist/meeting/index.js.map +1 -1
  37. package/dist/meeting/request.js +0 -50
  38. package/dist/meeting/request.js.map +1 -1
  39. package/dist/meeting/request.type.js.map +1 -1
  40. package/dist/meeting/util.js +3 -133
  41. package/dist/meeting/util.js.map +1 -1
  42. package/dist/meetings/index.js +59 -142
  43. package/dist/meetings/index.js.map +1 -1
  44. package/dist/meetings/util.js +7 -11
  45. package/dist/meetings/util.js.map +1 -1
  46. package/dist/member/index.js +0 -10
  47. package/dist/member/index.js.map +1 -1
  48. package/dist/member/util.js +0 -10
  49. package/dist/member/util.js.map +1 -1
  50. package/dist/metrics/constants.js +1 -7
  51. package/dist/metrics/constants.js.map +1 -1
  52. package/dist/multistream/mediaRequestManager.js +60 -9
  53. package/dist/multistream/mediaRequestManager.js.map +1 -1
  54. package/dist/multistream/remoteMediaManager.js +0 -11
  55. package/dist/multistream/remoteMediaManager.js.map +1 -1
  56. package/dist/multistream/sendSlotManager.js +2 -116
  57. package/dist/multistream/sendSlotManager.js.map +1 -1
  58. package/dist/reachability/clusterReachability.js +18 -171
  59. package/dist/reachability/clusterReachability.js.map +1 -1
  60. package/dist/reachability/index.js +11 -21
  61. package/dist/reachability/index.js.map +1 -1
  62. package/dist/reachability/reachabilityPeerConnection.js +1 -1
  63. package/dist/reachability/reachabilityPeerConnection.js.map +1 -1
  64. package/dist/reactions/reactions.type.js.map +1 -1
  65. package/dist/reconnection-manager/index.js +1 -0
  66. package/dist/reconnection-manager/index.js.map +1 -1
  67. package/dist/types/common/browser-detection.d.ts +0 -1
  68. package/dist/types/common/events/events-scope.d.ts +0 -1
  69. package/dist/types/common/events/events.d.ts +0 -1
  70. package/dist/types/config.d.ts +0 -5
  71. package/dist/types/constants.d.ts +1 -24
  72. package/dist/types/hashTree/hashTreeParser.d.ts +11 -260
  73. package/dist/types/hashTree/types.d.ts +0 -20
  74. package/dist/types/index.d.ts +0 -1
  75. package/dist/types/interceptors/index.d.ts +1 -2
  76. package/dist/types/interceptors/locusRouteToken.d.ts +0 -2
  77. package/dist/types/locus-info/index.d.ts +47 -68
  78. package/dist/types/locus-info/types.d.ts +12 -28
  79. package/dist/types/media/MediaConnectionAwaiter.d.ts +1 -10
  80. package/dist/types/media/properties.d.ts +1 -2
  81. package/dist/types/meeting/in-meeting-actions.d.ts +0 -6
  82. package/dist/types/meeting/index.d.ts +7 -86
  83. package/dist/types/meeting/request.d.ts +1 -16
  84. package/dist/types/meeting/request.type.d.ts +0 -5
  85. package/dist/types/meeting/util.d.ts +0 -31
  86. package/dist/types/meeting-info/util.d.ts +0 -1
  87. package/dist/types/meeting-info/utilv2.d.ts +0 -1
  88. package/dist/types/meetings/index.d.ts +2 -4
  89. package/dist/types/member/index.d.ts +0 -1
  90. package/dist/types/member/types.d.ts +4 -4
  91. package/dist/types/member/util.d.ts +0 -5
  92. package/dist/types/metrics/constants.d.ts +0 -6
  93. package/dist/types/multistream/mediaRequestManager.d.ts +23 -0
  94. package/dist/types/multistream/sendSlotManager.d.ts +1 -23
  95. package/dist/types/reachability/clusterReachability.d.ts +3 -30
  96. package/dist/types/reactions/reactions.type.d.ts +0 -1
  97. package/dist/types/recording-controller/util.d.ts +5 -5
  98. package/dist/types/roap/index.d.ts +1 -1
  99. package/dist/webinar/index.js +163 -438
  100. package/dist/webinar/index.js.map +1 -1
  101. package/package.json +24 -26
  102. package/src/annotation/index.ts +7 -27
  103. package/src/config.ts +0 -5
  104. package/src/constants.ts +1 -30
  105. package/src/hashTree/hashTreeParser.ts +25 -1523
  106. package/src/hashTree/types.ts +1 -24
  107. package/src/index.ts +1 -8
  108. package/src/interceptors/index.ts +1 -2
  109. package/src/interceptors/locusRouteToken.ts +5 -22
  110. package/src/interpretation/index.ts +2 -2
  111. package/src/locus-info/controlsUtils.ts +0 -17
  112. package/src/locus-info/index.ts +213 -707
  113. package/src/locus-info/selfUtils.ts +0 -1
  114. package/src/locus-info/types.ts +12 -27
  115. package/src/media/MediaConnectionAwaiter.ts +1 -41
  116. package/src/media/properties.ts +1 -3
  117. package/src/meeting/in-meeting-actions.ts +0 -12
  118. package/src/meeting/index.ts +84 -461
  119. package/src/meeting/request.ts +0 -42
  120. package/src/meeting/request.type.ts +0 -6
  121. package/src/meeting/util.ts +2 -160
  122. package/src/meetings/index.ts +60 -180
  123. package/src/meetings/util.ts +9 -10
  124. package/src/member/index.ts +0 -10
  125. package/src/member/util.ts +0 -12
  126. package/src/metrics/constants.ts +0 -7
  127. package/src/multistream/mediaRequestManager.ts +54 -4
  128. package/src/multistream/remoteMediaManager.ts +0 -13
  129. package/src/multistream/sendSlotManager.ts +3 -97
  130. package/src/reachability/clusterReachability.ts +27 -153
  131. package/src/reachability/index.ts +1 -15
  132. package/src/reachability/reachabilityPeerConnection.ts +1 -3
  133. package/src/reactions/reactions.type.ts +0 -1
  134. package/src/reconnection-manager/index.ts +1 -0
  135. package/src/webinar/index.ts +6 -265
  136. package/test/unit/spec/annotation/index.ts +7 -69
  137. package/test/unit/spec/interceptors/locusRouteToken.ts +0 -44
  138. package/test/unit/spec/locus-info/controlsUtils.js +1 -56
  139. package/test/unit/spec/locus-info/index.js +90 -1457
  140. package/test/unit/spec/media/MediaConnectionAwaiter.ts +1 -41
  141. package/test/unit/spec/media/properties.ts +3 -12
  142. package/test/unit/spec/meeting/in-meeting-actions.ts +2 -8
  143. package/test/unit/spec/meeting/index.js +128 -981
  144. package/test/unit/spec/meeting/request.js +0 -70
  145. package/test/unit/spec/meeting/utils.js +26 -438
  146. package/test/unit/spec/meetings/index.js +33 -845
  147. package/test/unit/spec/meetings/utils.js +1 -51
  148. package/test/unit/spec/member/index.js +4 -28
  149. package/test/unit/spec/member/util.js +27 -65
  150. package/test/unit/spec/multistream/mediaRequestManager.ts +85 -2
  151. package/test/unit/spec/multistream/remoteMediaManager.ts +0 -30
  152. package/test/unit/spec/multistream/sendSlotManager.ts +36 -135
  153. package/test/unit/spec/reachability/clusterReachability.ts +1 -125
  154. package/test/unit/spec/reachability/index.ts +3 -26
  155. package/test/unit/spec/reconnection-manager/index.js +8 -4
  156. package/test/unit/spec/webinar/index.ts +37 -534
  157. package/dist/aiEnableRequest/index.js +0 -184
  158. package/dist/aiEnableRequest/index.js.map +0 -1
  159. package/dist/aiEnableRequest/utils.js +0 -36
  160. package/dist/aiEnableRequest/utils.js.map +0 -1
  161. package/dist/hashTree/constants.js +0 -22
  162. package/dist/hashTree/constants.js.map +0 -1
  163. package/dist/hashTree/hashTree.js +0 -533
  164. package/dist/hashTree/hashTree.js.map +0 -1
  165. package/dist/hashTree/utils.js +0 -69
  166. package/dist/hashTree/utils.js.map +0 -1
  167. package/dist/interceptors/constant.js +0 -12
  168. package/dist/interceptors/constant.js.map +0 -1
  169. package/dist/interceptors/dataChannelAuthToken.js +0 -290
  170. package/dist/interceptors/dataChannelAuthToken.js.map +0 -1
  171. package/dist/interceptors/utils.js +0 -27
  172. package/dist/interceptors/utils.js.map +0 -1
  173. package/dist/types/aiEnableRequest/index.d.ts +0 -5
  174. package/dist/types/aiEnableRequest/utils.d.ts +0 -2
  175. package/dist/types/hashTree/constants.d.ts +0 -9
  176. package/dist/types/hashTree/hashTree.d.ts +0 -136
  177. package/dist/types/hashTree/utils.d.ts +0 -22
  178. package/dist/types/interceptors/constant.d.ts +0 -5
  179. package/dist/types/interceptors/dataChannelAuthToken.d.ts +0 -43
  180. package/dist/types/interceptors/utils.d.ts +0 -1
  181. package/dist/types/webinar/utils.d.ts +0 -6
  182. package/dist/webinar/utils.js +0 -25
  183. package/dist/webinar/utils.js.map +0 -1
  184. package/src/aiEnableRequest/README.md +0 -84
  185. package/src/aiEnableRequest/index.ts +0 -170
  186. package/src/aiEnableRequest/utils.ts +0 -25
  187. package/src/hashTree/constants.ts +0 -10
  188. package/src/hashTree/hashTree.ts +0 -480
  189. package/src/hashTree/utils.ts +0 -62
  190. package/src/interceptors/constant.ts +0 -6
  191. package/src/interceptors/dataChannelAuthToken.ts +0 -170
  192. package/src/interceptors/utils.ts +0 -16
  193. package/src/webinar/utils.ts +0 -16
  194. package/test/unit/spec/aiEnableRequest/index.ts +0 -981
  195. package/test/unit/spec/aiEnableRequest/utils.ts +0 -130
  196. package/test/unit/spec/hashTree/hashTree.ts +0 -721
  197. package/test/unit/spec/hashTree/hashTreeParser.ts +0 -3670
  198. package/test/unit/spec/hashTree/utils.ts +0 -140
  199. package/test/unit/spec/interceptors/dataChannelAuthToken.ts +0 -210
  200. package/test/unit/spec/interceptors/utils.ts +0 -75
  201. package/test/unit/spec/webinar/utils.ts +0 -39
@@ -1,3670 +0,0 @@
1
- import HashTreeParser, {
2
- LocusInfoUpdateType,
3
- MeetingEndedError,
4
- } from '@webex/plugin-meetings/src/hashTree/hashTreeParser';
5
- import HashTree from '@webex/plugin-meetings/src/hashTree/hashTree';
6
- import {expect} from '@webex/test-helper-chai';
7
- import sinon from 'sinon';
8
- import {assert} from '@webex/test-helper-chai';
9
- import {EMPTY_HASH} from '@webex/plugin-meetings/src/hashTree/constants';
10
-
11
- const visibleDataSetsUrl = 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/visibleDataSets';
12
-
13
- const exampleInitialLocus = {
14
- dataSets: [
15
- {
16
- url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/datasets/main',
17
- root: '9bb9d5a911a74d53a915b4dfbec7329f',
18
- version: 1000,
19
- leafCount: 16,
20
- name: 'main',
21
- idleMs: 1000,
22
- backoff: {maxMs: 1000, exponent: 2},
23
- },
24
- {
25
- url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/participant/713e9f99/datasets/self',
26
- root: '5b8cc7ffda1346d2bfb1c0b60b8ab601',
27
- version: 2000,
28
- leafCount: 1,
29
- name: 'self',
30
- idleMs: 1000,
31
- backoff: {maxMs: 1000, exponent: 2},
32
- },
33
- {
34
- url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/datasets/atd-unmuted',
35
- root: '9279d2e149da43a1b8e2cd7cbf77f9f0',
36
- version: 3000,
37
- leafCount: 16,
38
- name: 'atd-unmuted',
39
- idleMs: 1000,
40
- backoff: {maxMs: 1000, exponent: 2},
41
- },
42
- ],
43
- locus: {
44
- url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f',
45
- htMeta: {
46
- elementId: {
47
- type: 'locus',
48
- id: 0,
49
- version: 200,
50
- },
51
- dataSetNames: ['main'],
52
- },
53
- links: {resources: {visibleDataSets: {url: visibleDataSetsUrl}}},
54
- participants: [
55
- {
56
- url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/participant/11941033',
57
- person: {},
58
- htMeta: {
59
- elementId: {
60
- type: 'participant',
61
- id: 14,
62
- version: 300,
63
- },
64
- dataSetNames: ['atd-active', 'attendees', 'atd-unmuted'],
65
- },
66
- },
67
- ],
68
- self: {
69
- url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/participant/11941033',
70
- person: {},
71
- htMeta: {
72
- elementId: {
73
- type: 'self',
74
- id: 4,
75
- version: 100,
76
- },
77
- dataSetNames: ['self'],
78
- },
79
- },
80
- },
81
- };
82
-
83
- const exampleMetadata = {
84
- htMeta: {
85
- elementId: {
86
- type: 'metadata',
87
- id: 5,
88
- version: 50,
89
- },
90
- dataSetNames: ['self'],
91
- },
92
- visibleDataSets: [
93
- {name: 'main', url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/datasets/main'},
94
- {
95
- name: 'self',
96
- url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/participant/713e9f99/datasets/self',
97
- },
98
- {
99
- name: 'atd-unmuted',
100
- url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/datasets/atd-unmuted',
101
- },
102
- ],
103
- };
104
-
105
- function createDataSet(name: string, leafCount: number, version = 1) {
106
- return {
107
- url: `https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/datasets/${name}`,
108
- root: '0'.repeat(32),
109
- version,
110
- leafCount,
111
- name,
112
- idleMs: 1000,
113
- backoff: {maxMs: 1000, exponent: 2},
114
- };
115
- }
116
-
117
- // Helper function to setup a webexRequest mock for getAllDataSetsMetadata
118
- function mockGetAllDataSetsMetadata(webexRequest: sinon.SinonStub, url: string, dataSets: any[]) {
119
- webexRequest
120
- .withArgs(
121
- sinon.match({
122
- method: 'GET',
123
- uri: url,
124
- })
125
- )
126
- .resolves({
127
- body: {dataSets},
128
- });
129
- }
130
-
131
- // Helper function to setup a webexRequest mock for sync requests
132
- function mockSyncRequest(webexRequest: sinon.SinonStub, datasetUrl: string, response: any = null) {
133
- const stub = webexRequest.withArgs(
134
- sinon.match({
135
- method: 'POST',
136
- uri: `${datasetUrl}/sync`,
137
- })
138
- );
139
-
140
- if (response === null) {
141
- stub.resolves({body: {}});
142
- } else {
143
- stub.resolves({body: response});
144
- }
145
- }
146
-
147
- describe('HashTreeParser', () => {
148
- const locusUrl = 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f';
149
-
150
- let clock;
151
- let webexRequest: sinon.SinonStub;
152
- let callback: sinon.SinonStub;
153
- let mathRandomStub: sinon.SinonStub;
154
-
155
- beforeEach(() => {
156
- clock = sinon.useFakeTimers();
157
- webexRequest = sinon.stub();
158
- callback = sinon.stub();
159
- mathRandomStub = sinon.stub(Math, 'random').returns(0);
160
- });
161
- afterEach(() => {
162
- clock.restore();
163
- mathRandomStub.restore();
164
- });
165
-
166
- // Helper to create a HashTreeParser instance with common defaults
167
- function createHashTreeParser(
168
- initialLocus: any = exampleInitialLocus,
169
- metadata: any = exampleMetadata,
170
- excludedDataSets?: string[]
171
- ) {
172
- return new HashTreeParser({
173
- initialLocus,
174
- metadata,
175
- webexRequest,
176
- locusInfoUpdateCallback: callback,
177
- debugId: 'test',
178
- excludedDataSets,
179
- });
180
- }
181
-
182
- // Helper to create a heartbeat message (without locusStateElements)
183
- function createHeartbeatMessage(
184
- dataSetName: string,
185
- leafCount: number,
186
- version: number,
187
- rootHash: string
188
- ) {
189
- return {
190
- dataSets: [
191
- {
192
- ...createDataSet(dataSetName, leafCount, version),
193
- root: rootHash,
194
- },
195
- ],
196
- visibleDataSetsUrl,
197
- locusUrl,
198
- };
199
- }
200
-
201
- // Helper to mock getHashesFromLocus response
202
- function mockGetHashesFromLocusResponse(dataSetUrl: string, hashes: string[], dataSetInfo: any) {
203
- webexRequest
204
- .withArgs(
205
- sinon.match({
206
- method: 'GET',
207
- uri: `${dataSetUrl}/hashtree`,
208
- })
209
- )
210
- .resolves({
211
- body: {
212
- hashes,
213
- dataSet: dataSetInfo,
214
- },
215
- });
216
- }
217
-
218
- // Helper to mock sendSyncRequestToLocus response
219
- function mockSendSyncRequestResponse(dataSetUrl: string, response: any) {
220
- webexRequest
221
- .withArgs(
222
- sinon.match({
223
- method: 'POST',
224
- uri: `${dataSetUrl}/sync`,
225
- })
226
- )
227
- .resolves({
228
- body: response,
229
- });
230
- }
231
-
232
- async function checkAsyncDatasetInitialization(
233
- parser: HashTreeParser,
234
- newDataSet: {name: string; leafCount: number; url: string}
235
- ) {
236
- // immediately we don't have the dataset yet, so it should not be in visibleDataSets
237
- // and no hash tree should exist yet
238
- expect(parser.visibleDataSets.some((vds) => vds.name === newDataSet.name)).to.be.false;
239
- assert.isUndefined(parser.dataSets[newDataSet.name]);
240
-
241
- // Wait for the async initialization to complete (queued as microtask)
242
- await clock.tickAsync(0);
243
-
244
- // The visibleDataSets is updated from the metadata object data
245
- expect(parser.visibleDataSets.some((vds) => vds.name === newDataSet.name)).to.be.true;
246
-
247
- // Verify that a hash tree was created for newDataSet
248
- assert.exists(parser.dataSets[newDataSet.name].hashTree);
249
- assert.equal(parser.dataSets[newDataSet.name].hashTree.numLeaves, newDataSet.leafCount);
250
-
251
- // Verify getAllDataSetsMetadata was called for async initialization
252
- assert.calledWith(
253
- webexRequest,
254
- sinon.match({
255
- method: 'GET',
256
- uri: visibleDataSetsUrl,
257
- })
258
- );
259
-
260
- // Verify sync request was sent for the new dataset
261
- assert.calledWith(
262
- webexRequest,
263
- sinon.match({
264
- method: 'POST',
265
- uri: `${newDataSet.url}/sync`,
266
- })
267
- );
268
- }
269
- it('should correctly initialize trees from initialLocus data', () => {
270
- const parser = createHashTreeParser();
271
-
272
- // verify that visibleDataSetsUrl is read out from inside locus
273
- expect(parser.visibleDataSetsUrl).to.equal(visibleDataSetsUrl);
274
-
275
- // Check that the correct number of trees are created
276
- expect(Object.keys(parser.dataSets).length).to.equal(3);
277
-
278
- // Verify the 'main' tree
279
- const mainTree = parser.dataSets.main.hashTree;
280
- expect(mainTree).to.be.instanceOf(HashTree);
281
- const expectedMainLeaves = new Array(16).fill(null).map(() => ({}));
282
- expectedMainLeaves[0 % 16] = {locus: {0: {type: 'locus', id: 0, version: 200}}};
283
- expect(mainTree.leaves).to.deep.equal(expectedMainLeaves);
284
- expect(mainTree.numLeaves).to.equal(16);
285
-
286
- // Verify the 'self' tree
287
- const selfTree = parser.dataSets.self.hashTree;
288
- expect(selfTree).to.be.instanceOf(HashTree);
289
- const expectedSelfLeaves = new Array(1).fill(null).map(() => ({}));
290
- // Both self (id=4) and metadata (id=5) map to the same leaf (4%1=0, 5%1=0)
291
- expectedSelfLeaves[0] = {
292
- self: {4: {type: 'self', id: 4, version: 100}},
293
- metadata: {5: {type: 'metadata', id: 5, version: 50}},
294
- };
295
- expect(selfTree.leaves).to.deep.equal(expectedSelfLeaves);
296
- expect(selfTree.numLeaves).to.equal(1);
297
-
298
- // Verify the 'atd-unmuted' tree
299
- const atdUnmutedTree = parser.dataSets['atd-unmuted'].hashTree;
300
- expect(atdUnmutedTree).to.be.instanceOf(HashTree);
301
- const expectedAtdUnmutedLeaves = new Array(16).fill(null).map(() => ({}));
302
- expectedAtdUnmutedLeaves[14 % 16] = {
303
- participant: {14: {type: 'participant', id: 14, version: 300}},
304
- };
305
- expect(atdUnmutedTree.leaves).to.deep.equal(expectedAtdUnmutedLeaves);
306
- expect(atdUnmutedTree.numLeaves).to.equal(16);
307
-
308
- // Ensure no other trees were created
309
- expect(parser.dataSets['atd-active']).to.be.undefined;
310
- expect(parser.dataSets.attendees).to.be.undefined;
311
- });
312
-
313
- it('should handle datasets with no corresponding metadata found', () => {
314
- const modifiedLocus = JSON.parse(JSON.stringify(exampleInitialLocus));
315
- // Remove a participant to simulate missing data for 'atd-unmuted'
316
- modifiedLocus.locus.participants = [];
317
- // Add a new dataset that won't have corresponding metadata
318
- modifiedLocus.dataSets.push({
319
- url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/datasets/empty-set',
320
- root: 'f00f00f00f00f00f00f00f00f00f00f0',
321
- version: 5000,
322
- leafCount: 4,
323
- name: 'empty-set',
324
- });
325
-
326
- const parser = createHashTreeParser(modifiedLocus, exampleMetadata);
327
-
328
- expect(Object.keys(parser.dataSets).length).to.equal(4); // main, self, atd-unmuted (now empty), empty-set
329
-
330
- // 'main' and 'self' should be populated as before
331
- const mainTree = parser.dataSets.main.hashTree;
332
- const expectedMainLeaves = new Array(16).fill(null).map(() => ({}));
333
- expectedMainLeaves[0 % 16] = {locus: {0: {type: 'locus', id: 0, version: 200}}};
334
- expect(mainTree.leaves).to.deep.equal(expectedMainLeaves);
335
- expect(mainTree.numLeaves).to.equal(16);
336
-
337
- const selfTree = parser.dataSets.self.hashTree;
338
- const expectedSelfLeaves = new Array(1).fill(null).map(() => ({}));
339
- expectedSelfLeaves[4 % 1] = {
340
- self: {4: {type: 'self', id: 4, version: 100}},
341
- metadata: {5: exampleMetadata.htMeta.elementId},
342
- };
343
- expect(selfTree.leaves).to.deep.equal(expectedSelfLeaves);
344
- expect(selfTree.numLeaves).to.equal(1);
345
-
346
- // 'atd-unmuted' metadata was removed from locus, so leaves should be empty
347
- const atdUnmutedTree = parser.dataSets['atd-unmuted'].hashTree;
348
- expect(atdUnmutedTree).to.be.instanceOf(HashTree);
349
- const expectedAtdUnmutedEmptyLeaves = new Array(16).fill(null).map(() => ({}));
350
- expect(atdUnmutedTree.leaves).to.deep.equal(expectedAtdUnmutedEmptyLeaves);
351
- expect(atdUnmutedTree.numLeaves).to.equal(16); // leafCount from dataSet definition
352
-
353
- // 'empty-set' was added to dataSets but has no metadata in locus and is not among visibleDataSets
354
- // so an entry for it should exist, but hashTree shouldn't be created
355
- const emptySet = parser.dataSets['empty-set'];
356
- expect(emptySet.hashTree).to.be.undefined;
357
- });
358
-
359
- it('should exclude datasets listed in excludedDataSets during initialization', () => {
360
- const parser = createHashTreeParser(exampleInitialLocus, exampleMetadata, ['atd-unmuted']);
361
-
362
- // 'atd-unmuted' should be excluded from visibleDataSets
363
- expect(parser.visibleDataSets.some((vds) => vds.name === 'atd-unmuted')).to.be.false;
364
-
365
- // 'main' and 'self' should still be visible
366
- expect(parser.visibleDataSets.some((vds) => vds.name === 'main')).to.be.true;
367
- expect(parser.visibleDataSets.some((vds) => vds.name === 'self')).to.be.true;
368
-
369
- // 'atd-unmuted' dataset entry should exist but without a hash tree (because it's not visible)
370
- expect(parser.dataSets['atd-unmuted']).to.exist;
371
- expect(parser.dataSets['atd-unmuted'].hashTree).to.be.undefined;
372
-
373
- // 'main' and 'self' should have hash trees
374
- expect(parser.dataSets.main.hashTree).to.be.instanceOf(HashTree);
375
- expect(parser.dataSets.self.hashTree).to.be.instanceOf(HashTree);
376
- });
377
-
378
- it('should exclude datasets listed in excludedDataSets when adding new visible datasets', async () => {
379
- // Create parser without 'atd-unmuted' in initial metadata visibleDataSets
380
- const metadataWithoutAtdUnmuted = {
381
- ...exampleMetadata,
382
- visibleDataSets: exampleMetadata.visibleDataSets.filter((vds) => vds.name !== 'atd-unmuted'),
383
- };
384
- const parser = createHashTreeParser(exampleInitialLocus, metadataWithoutAtdUnmuted, [
385
- 'atd-unmuted',
386
- ]);
387
-
388
- // 'atd-unmuted' should not be in visibleDataSets initially
389
- expect(parser.visibleDataSets.some((vds) => vds.name === 'atd-unmuted')).to.be.false;
390
-
391
- // Now simulate initializeDataSets which calls addToVisibleDataSetsList
392
- const atdUnmutedDataSet = createDataSet('atd-unmuted', 16, 3000);
393
-
394
- mockGetAllDataSetsMetadata(webexRequest, visibleDataSetsUrl, [
395
- createDataSet('main', 16, 1000),
396
- createDataSet('self', 1, 2000),
397
- atdUnmutedDataSet,
398
- ]);
399
-
400
- mockSyncRequest(
401
- webexRequest,
402
- 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/datasets/atd-unmuted'
403
- );
404
-
405
- const message = {
406
- dataSets: [createDataSet('main', 16, 1000)],
407
- visibleDataSetsUrl,
408
- locusUrl,
409
- };
410
- await parser.initializeFromMessage(message);
411
-
412
- // 'atd-unmuted' should still not be in visibleDataSets because it is excluded
413
- expect(parser.visibleDataSets.some((vds) => vds.name === 'atd-unmuted')).to.be.false;
414
- // but 'main' and 'self' should be there
415
- expect(parser.visibleDataSets.some((vds) => vds.name === 'main')).to.be.true;
416
- expect(parser.visibleDataSets.some((vds) => vds.name === 'self')).to.be.true;
417
- });
418
-
419
- // helper method, needed because both initializeFromMessage and initializeFromGetLociResponse
420
- // do almost exactly the same thing
421
- const testInitializationOfDatasetsAndHashTrees = async (testCallback) => {
422
- // Create a parser with minimal initial data
423
- const minimalInitialLocus = {
424
- dataSets: [],
425
- locus: null,
426
- };
427
-
428
- const minimalMetadata = {
429
- htMeta: {
430
- elementId: {
431
- type: 'metadata',
432
- id: 5,
433
- version: 50,
434
- },
435
- dataSetNames: ['self'],
436
- },
437
- visibleDataSets: [
438
- {name: 'main', url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/datasets/main'},
439
- {
440
- name: 'self',
441
- url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/participant/713e9f99/datasets/self',
442
- },
443
- ],
444
- };
445
-
446
- const hashTreeParser = createHashTreeParser(minimalInitialLocus, minimalMetadata);
447
-
448
- // Setup the datasets that will be returned from getAllDataSetsMetadata
449
- const mainDataSet = createDataSet('main', 16, 1100);
450
- const selfDataSet = createDataSet('self', 1, 2100);
451
-
452
- mockGetAllDataSetsMetadata(webexRequest, visibleDataSetsUrl, [mainDataSet, selfDataSet]);
453
-
454
- // Mock sync requests for visible datasets with some updated objects
455
- const mainSyncResponse = {
456
- dataSets: [mainDataSet],
457
- visibleDataSetsUrl,
458
- locusUrl,
459
- locusStateElements: [
460
- {
461
- htMeta: {
462
- elementId: {
463
- type: 'locus',
464
- id: 1,
465
- version: 210,
466
- },
467
- dataSetNames: ['main'],
468
- },
469
- data: {info: {id: 'some-fake-locus-info'}},
470
- },
471
- ],
472
- };
473
-
474
- const selfSyncResponse = {
475
- dataSets: [selfDataSet],
476
- visibleDataSetsUrl,
477
- locusUrl,
478
- locusStateElements: [
479
- {
480
- htMeta: {
481
- elementId: {
482
- type: 'self',
483
- id: 2,
484
- version: 110,
485
- },
486
- dataSetNames: ['self'],
487
- },
488
- data: {person: {name: 'fake self name'}},
489
- },
490
- ],
491
- };
492
-
493
- mockSyncRequest(webexRequest, mainDataSet.url, mainSyncResponse);
494
- mockSyncRequest(webexRequest, selfDataSet.url, selfSyncResponse);
495
-
496
- // call the callback that actually calls the function being tested
497
- await testCallback(hashTreeParser);
498
-
499
- // Verify getAllDataSetsMetadata was called with correct URL
500
- assert.calledWith(
501
- webexRequest,
502
- sinon.match({
503
- method: 'GET',
504
- uri: visibleDataSetsUrl,
505
- })
506
- );
507
-
508
- // verify that visibleDataSetsUrl is set on the parser
509
- expect(hashTreeParser.visibleDataSetsUrl).to.equal(visibleDataSetsUrl);
510
-
511
- // Verify all datasets returned from visibleDataSetsUrl are added to dataSets
512
- expect(hashTreeParser.dataSets.main).to.exist;
513
- expect(hashTreeParser.dataSets.self).to.exist;
514
-
515
- // Verify hash trees are created only for visible datasets
516
- expect(hashTreeParser.dataSets.main.hashTree).to.be.instanceOf(HashTree);
517
- expect(hashTreeParser.dataSets.self.hashTree).to.be.instanceOf(HashTree);
518
-
519
- // Verify hash trees have correct leaf counts
520
- expect(hashTreeParser.dataSets.main.hashTree.numLeaves).to.equal(16);
521
- expect(hashTreeParser.dataSets.self.hashTree.numLeaves).to.equal(1);
522
-
523
- // Verify sync requests were sent for visible datasets
524
- assert.calledWith(
525
- webexRequest,
526
- sinon.match({
527
- method: 'POST',
528
- uri: `${mainDataSet.url}/sync`,
529
- })
530
- );
531
- assert.calledWith(
532
- webexRequest,
533
- sinon.match({
534
- method: 'POST',
535
- uri: `${selfDataSet.url}/sync`,
536
- })
537
- );
538
-
539
- // and no requests for hashes were sent
540
- assert.neverCalledWith(
541
- webexRequest,
542
- sinon.match({
543
- method: 'GET',
544
- uri: `${mainDataSet.url}/hashtree`,
545
- })
546
- );
547
- assert.neverCalledWith(
548
- webexRequest,
549
- sinon.match({
550
- method: 'GET',
551
- uri: `${selfDataSet.url}/hashtree`,
552
- })
553
- );
554
-
555
- // Verify callback was called with OBJECTS_UPDATED and correct updatedObjects list
556
- assert.calledWith(callback, LocusInfoUpdateType.OBJECTS_UPDATED, {
557
- updatedObjects: [
558
- {
559
- htMeta: {
560
- elementId: {
561
- type: 'locus',
562
- id: 1,
563
- version: 210,
564
- },
565
- dataSetNames: ['main'],
566
- },
567
- data: {info: {id: 'some-fake-locus-info'}},
568
- },
569
- {
570
- htMeta: {
571
- elementId: {
572
- type: 'self',
573
- id: 2,
574
- version: 110,
575
- },
576
- dataSetNames: ['self'],
577
- },
578
- data: {person: {name: 'fake self name'}},
579
- },
580
- ],
581
- });
582
-
583
- // verify that sync timers are set for visible datasets
584
- expect(hashTreeParser.dataSets.main.timer).to.not.be.undefined;
585
- expect(hashTreeParser.dataSets.self.timer).to.not.be.undefined;
586
- };
587
-
588
- describe('#initializeFromMessage', () => {
589
- it('fetches datasets metadata and initializes hash trees for visible data sets', async () => {
590
- await testInitializationOfDatasetsAndHashTrees(async (hashTreeParser: HashTreeParser) => {
591
- await hashTreeParser.initializeFromMessage({
592
- dataSets: [],
593
- visibleDataSetsUrl,
594
- locusUrl,
595
- });
596
- });
597
- });
598
-
599
- it('handles sync response that has locusStateElements undefined', async () => {
600
- const minimalInitialLocus = {
601
- dataSets: [],
602
- locus: null,
603
- };
604
-
605
- const parser = createHashTreeParser(minimalInitialLocus, null);
606
-
607
- const mainDataSet = createDataSet('main', 16, 1100);
608
-
609
- // Mock getAllVisibleDataSetsFromLocus to return the main dataset
610
- mockGetAllDataSetsMetadata(webexRequest, visibleDataSetsUrl, [mainDataSet]);
611
-
612
- // Mock the sync response to have locusStateElements: undefined
613
- // This is what sendInitializationSyncRequestToLocus will receive and pass to parseMessage
614
- mockSyncRequest(webexRequest, mainDataSet.url, {
615
- dataSets: [mainDataSet],
616
- visibleDataSetsUrl,
617
- locusUrl,
618
- locusStateElements: undefined,
619
- });
620
-
621
- // Trigger sendInitializationSyncRequestToLocus via initializeFromMessage
622
- await parser.initializeFromMessage({
623
- dataSets: [],
624
- visibleDataSetsUrl,
625
- locusUrl,
626
- });
627
-
628
- // Verify the hash tree was created for main dataset
629
- expect(parser.dataSets.main.hashTree).to.be.instanceOf(HashTree);
630
-
631
- // updateItems should NOT have been called because locusStateElements is undefined
632
- const mainUpdateItemsStub = sinon.spy(parser.dataSets.main.hashTree, 'updateItems');
633
- assert.notCalled(mainUpdateItemsStub);
634
-
635
- // callback should not be called, because there are no updates
636
- assert.notCalled(callback);
637
- });
638
-
639
- [404, 409].forEach((errorCode) => {
640
- it(`emits MeetingEndedError if getting visible datasets returns ${errorCode}`, async () => {
641
- const minimalInitialLocus = {
642
- dataSets: [],
643
- locus: null,
644
- };
645
-
646
- const parser = createHashTreeParser(minimalInitialLocus, null);
647
-
648
- // Mock getAllVisibleDataSetsFromLocus to reject with the error code
649
- const error: any = new Error(`Request failed with status ${errorCode}`);
650
- error.statusCode = errorCode;
651
- if (errorCode === 409) {
652
- error.body = {errorCode: 2403004};
653
- }
654
- webexRequest
655
- .withArgs(
656
- sinon.match({
657
- method: 'GET',
658
- uri: visibleDataSetsUrl,
659
- })
660
- )
661
- .rejects(error);
662
-
663
- // initializeFromMessage should throw MeetingEndedError
664
- let thrownError;
665
- try {
666
- await parser.initializeFromMessage({
667
- dataSets: [],
668
- visibleDataSetsUrl,
669
- locusUrl,
670
- });
671
- } catch (e) {
672
- thrownError = e;
673
- }
674
-
675
- expect(thrownError).to.be.instanceOf(MeetingEndedError);
676
- });
677
- });
678
- });
679
-
680
- describe('#initializeFromGetLociResponse', () => {
681
- it('does nothing if url for visibleDataSets is missing from locus', async () => {
682
- const parser = createHashTreeParser({dataSets: [], locus: {}}, null);
683
-
684
- await parser.initializeFromGetLociResponse({participants: []});
685
-
686
- assert.notCalled(webexRequest);
687
- assert.notCalled(callback);
688
- });
689
- it('fetches datasets metadata and initializes hash trees for visible data sets', async () => {
690
- await testInitializationOfDatasetsAndHashTrees(async (hashTreeParser: HashTreeParser) => {
691
- await hashTreeParser.initializeFromGetLociResponse({
692
- links: {
693
- resources: {
694
- visibleDataSets: {
695
- url: visibleDataSetsUrl,
696
- },
697
- },
698
- },
699
- participants: [],
700
- });
701
- });
702
- });
703
- });
704
-
705
- describe('#handleLocusUpdate', () => {
706
- it('updates hash trees based on provided new locus', () => {
707
- const parser = createHashTreeParser();
708
-
709
- const mainPutItemsSpy = sinon.spy(parser.dataSets.main.hashTree, 'putItems');
710
- const selfPutItemsSpy = sinon.spy(parser.dataSets.self.hashTree, 'putItems');
711
- const atdUnmutedPutItemsSpy = sinon.spy(parser.dataSets['atd-unmuted'].hashTree, 'putItems');
712
-
713
- // Create a locus update with new htMeta information for some things
714
- const locusUpdate = {
715
- dataSets: [
716
- createDataSet('main', 16, 1100),
717
- createDataSet('self', 1, 2100),
718
- createDataSet('atd-unmuted', 16, 3100),
719
- ],
720
- locus: {
721
- url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f',
722
- htMeta: {
723
- elementId: {
724
- type: 'locus',
725
- id: 0,
726
- version: 210, // incremented version
727
- },
728
- dataSetNames: ['main'],
729
- },
730
- participants: [
731
- {
732
- url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/participant/11941033',
733
- person: {},
734
- htMeta: {
735
- elementId: {
736
- type: 'participant',
737
- id: 14,
738
- version: 310, // incremented version
739
- },
740
- dataSetNames: ['atd-unmuted'],
741
- },
742
- },
743
- {
744
- url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/participant/22222222',
745
- person: {},
746
- htMeta: {
747
- elementId: {
748
- type: 'participant',
749
- id: 15,
750
- version: 311, // new participant
751
- },
752
- dataSetNames: ['atd-unmuted'],
753
- },
754
- },
755
- ],
756
- self: {
757
- url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/participant/11941033',
758
- person: {},
759
- htMeta: {
760
- elementId: {
761
- type: 'self',
762
- id: 4,
763
- version: 100, // same version
764
- },
765
- dataSetNames: ['self'],
766
- },
767
- },
768
- },
769
- };
770
-
771
- // Call handleLocusUpdate
772
- parser.handleLocusUpdate(locusUpdate);
773
-
774
- // Verify putItems was called on main hash tree with correct data
775
- assert.calledOnceWithExactly(mainPutItemsSpy, [{type: 'locus', id: 0, version: 210}]);
776
-
777
- // Verify putItems was called on self hash tree with correct data
778
- assert.calledOnceWithExactly(selfPutItemsSpy, [{type: 'self', id: 4, version: 100}]);
779
-
780
- // Verify putItems was called on atd-unmuted hash tree with correct data (2 participants)
781
- assert.calledOnceWithExactly(atdUnmutedPutItemsSpy, [
782
- {type: 'participant', id: 14, version: 310},
783
- {type: 'participant', id: 15, version: 311},
784
- ]);
785
-
786
- // check that the datasets metadata has been updated
787
- expect(parser.dataSets.main.version).to.equal(1100);
788
- expect(parser.dataSets.self.version).to.equal(2100);
789
- expect(parser.dataSets['atd-unmuted'].version).to.equal(3100);
790
-
791
- assert.calledOnceWithExactly(callback, LocusInfoUpdateType.OBJECTS_UPDATED, {
792
- updatedObjects: [
793
- {
794
- htMeta: {
795
- elementId: {
796
- type: 'locus',
797
- id: 0,
798
- version: 210,
799
- },
800
- dataSetNames: ['main'],
801
- },
802
- data: {
803
- url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f',
804
- htMeta: {
805
- elementId: {
806
- type: 'locus',
807
- id: 0,
808
- version: 210,
809
- },
810
- dataSetNames: ['main'],
811
- },
812
- participants: [],
813
- },
814
- },
815
- {
816
- htMeta: {
817
- elementId: {
818
- type: 'participant',
819
- id: 14,
820
- version: 310,
821
- },
822
- dataSetNames: ['atd-unmuted'],
823
- },
824
- data: {
825
- url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/participant/11941033',
826
- person: {},
827
- htMeta: {
828
- elementId: {
829
- type: 'participant',
830
- id: 14,
831
- version: 310,
832
- },
833
- dataSetNames: ['atd-unmuted'],
834
- },
835
- },
836
- },
837
- {
838
- htMeta: {
839
- elementId: {
840
- type: 'participant',
841
- id: 15,
842
- version: 311,
843
- },
844
- dataSetNames: ['atd-unmuted'],
845
- },
846
- data: {
847
- url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/participant/22222222',
848
- person: {},
849
- htMeta: {
850
- elementId: {
851
- type: 'participant',
852
- id: 15,
853
- version: 311,
854
- },
855
- dataSetNames: ['atd-unmuted'],
856
- },
857
- },
858
- },
859
- // self missing, because it had the same version, so no update
860
- ],
861
- });
862
- });
863
-
864
- it('handles updates to control entries correctly', () => {
865
- const parser = createHashTreeParser();
866
-
867
- const mainPutItemsSpy = sinon.spy(parser.dataSets.main.hashTree, 'putItems');
868
-
869
- // Create a locus update with new htMeta information for some things
870
- const locusUpdate = {
871
- dataSets: [
872
- createDataSet('main', 16, 1100),
873
- ],
874
- locus: {
875
- url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f',
876
- htMeta: {
877
- elementId: {
878
- type: 'locus',
879
- id: 0,
880
- version: 200, // same version
881
- },
882
- dataSetNames: ['main'],
883
- },
884
- participants: [],
885
- controls: {
886
- lock: {
887
- locked: true,
888
- htMeta: {
889
- elementId: {
890
- type: 'ControlEntry',
891
- id: 10100,
892
- version: 100,
893
- },
894
- dataSetNames: ['main'],
895
- },
896
- },
897
- stream: {
898
- streaming: true,
899
- htMeta: {
900
- elementId: {
901
- type: 'ControlEntry',
902
- id: 10101,
903
- version: 100,
904
- },
905
- dataSetNames: ['main'],
906
- },
907
- }
908
- }
909
- },
910
- };
911
-
912
- // Call handleLocusUpdate
913
- parser.handleLocusUpdate(locusUpdate);
914
-
915
- // Verify putItems was called on main hash tree with correct data
916
- assert.calledOnceWithExactly(mainPutItemsSpy, [
917
- {type: 'locus', id: 0, version: 200},
918
- {type: 'ControlEntry', id: 10100, version: 100},
919
- {type: 'ControlEntry', id: 10101, version: 100}
920
- ]);
921
-
922
- assert.calledOnceWithExactly(callback, LocusInfoUpdateType.OBJECTS_UPDATED, {
923
- updatedObjects: [
924
- {
925
- htMeta: {
926
- elementId: {
927
- type: 'ControlEntry',
928
- id: 10100,
929
- version: 100,
930
- },
931
- dataSetNames: ['main'],
932
- },
933
- data: {
934
- lock: {
935
- locked: true,
936
- htMeta: {
937
- elementId: {
938
- type: 'ControlEntry',
939
- id: 10100,
940
- version: 100,
941
- },
942
- dataSetNames: ['main'],
943
- },
944
- },
945
- },
946
- },
947
- {
948
- htMeta: {
949
- elementId: {
950
- type: 'ControlEntry',
951
- id: 10101,
952
- version: 100,
953
- },
954
- dataSetNames: ['main'],
955
- },
956
- data: {
957
- stream: {
958
- streaming: true,
959
- htMeta: {
960
- elementId: {
961
- type: 'ControlEntry',
962
- id: 10101,
963
- version: 100,
964
- },
965
- dataSetNames: ['main'],
966
- },
967
- },
968
- },
969
- }
970
- ],
971
- });
972
- });
973
-
974
- it('handles unknown datasets gracefully', () => {
975
- const parser = createHashTreeParser();
976
-
977
- const mainPutItemsSpy = sinon.spy(parser.dataSets.main.hashTree, 'putItems');
978
-
979
- // Create a locus update with data for an unknown dataset
980
- const locusUpdate = {
981
- dataSets: [createDataSet('main', 16)],
982
- locus: {
983
- htMeta: {
984
- elementId: {
985
- type: 'locus',
986
- id: 0,
987
- version: 201,
988
- },
989
- dataSetNames: ['main'],
990
- },
991
- someNewData: 'value',
992
- unknownData: {
993
- htMeta: {
994
- elementId: {
995
- type: 'UNKNOWN',
996
- id: 99,
997
- version: 999,
998
- },
999
- dataSetNames: ['unknown-dataset'], // dataset that doesn't exist
1000
- },
1001
- },
1002
- },
1003
- };
1004
-
1005
- // Call handleLocusUpdate - should not throw
1006
- parser.handleLocusUpdate(locusUpdate);
1007
-
1008
- // Verify putItems was still called for known dataset
1009
- assert.calledOnceWithExactly(mainPutItemsSpy, [{type: 'locus', id: 0, version: 201}]);
1010
-
1011
- // Verify callback was called only for known dataset
1012
- assert.calledOnceWithExactly(callback, LocusInfoUpdateType.OBJECTS_UPDATED, {
1013
- updatedObjects: [
1014
- {
1015
- htMeta: {
1016
- elementId: {
1017
- type: 'locus',
1018
- id: 0,
1019
- version: 201,
1020
- },
1021
- dataSetNames: ['main'],
1022
- },
1023
- data: {
1024
- someNewData: 'value',
1025
- htMeta: {
1026
- elementId: {
1027
- type: 'locus',
1028
- id: 0,
1029
- version: 201,
1030
- },
1031
- dataSetNames: ['main'],
1032
- },
1033
- },
1034
- },
1035
- ],
1036
- });
1037
- });
1038
-
1039
- it('handles metadata updates with new version', async () => {
1040
- const parser = createHashTreeParser();
1041
-
1042
- const selfPutItemSpy = sinon.spy(parser.dataSets.self.hashTree, 'putItem');
1043
-
1044
- // Create a locus update with updated metadata
1045
- const locusUpdate = {
1046
- dataSets: [createDataSet('self', 1, 2100), createDataSet('attendees', 8, 4000)],
1047
- locus: {
1048
- links: {resources: {visibleDataSets: {url: visibleDataSetsUrl}}},
1049
- participants: [
1050
- {
1051
- url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/participant/15',
1052
- person: {},
1053
- htMeta: {
1054
- elementId: {
1055
- type: 'participant',
1056
- id: 15, // new participant
1057
- version: 999,
1058
- },
1059
- dataSetNames: ['attendees'],
1060
- },
1061
- },
1062
- ],
1063
- },
1064
- metadata: {
1065
- htMeta: {
1066
- elementId: {
1067
- type: 'metadata',
1068
- id: 5,
1069
- version: 51, // incremented version
1070
- },
1071
- dataSetNames: ['self'],
1072
- },
1073
- // new visibleDataSets: atd-unmuted removed, "attendees" and "new-dataset" added
1074
- visibleDataSets: [
1075
- {
1076
- name: 'main',
1077
- url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/datasets/main',
1078
- },
1079
- {
1080
- name: 'self',
1081
- url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/participant/713e9f99/datasets/self',
1082
- },
1083
- {
1084
- name: 'new-dataset', // this one is not in dataSets, so will require async initialization
1085
- url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/datasets/new-dataset',
1086
- },
1087
- {
1088
- name: 'attendees', // this one is in dataSets, so should be processed immediately
1089
- url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/datasets/attendees',
1090
- },
1091
- ],
1092
- },
1093
- };
1094
-
1095
- // Mock the async initialization of the new dataset
1096
- const newDataSet = createDataSet('new-dataset', 4, 5000);
1097
- mockGetAllDataSetsMetadata(webexRequest, visibleDataSetsUrl, [newDataSet]);
1098
- mockSyncRequest(webexRequest, newDataSet.url, {
1099
- dataSets: [newDataSet],
1100
- visibleDataSetsUrl,
1101
- locusUrl,
1102
- locusStateElements: [],
1103
- });
1104
-
1105
- // Call handleLocusUpdate
1106
- parser.handleLocusUpdate(locusUpdate);
1107
-
1108
- // Verify putItem was called on self hash tree with metadata
1109
- assert.calledOnceWithExactly(selfPutItemSpy, {type: 'metadata', id: 5, version: 51});
1110
-
1111
- // Verify callback was called with metadata object and removed dataset objects
1112
- assert.calledOnceWithExactly(callback, LocusInfoUpdateType.OBJECTS_UPDATED, {
1113
- updatedObjects: [
1114
- // updated metadata object:
1115
- {
1116
- htMeta: {
1117
- elementId: {
1118
- type: 'metadata',
1119
- id: 5,
1120
- version: 51,
1121
- },
1122
- dataSetNames: ['self'],
1123
- },
1124
- data: {
1125
- htMeta: {
1126
- elementId: {
1127
- type: 'metadata',
1128
- id: 5,
1129
- version: 51,
1130
- },
1131
- dataSetNames: ['self'],
1132
- },
1133
- visibleDataSets: [
1134
- {
1135
- name: 'main',
1136
- url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/datasets/main',
1137
- },
1138
- {
1139
- name: 'self',
1140
- url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/participant/713e9f99/datasets/self',
1141
- },
1142
- {
1143
- name: 'new-dataset',
1144
- url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/datasets/new-dataset',
1145
- },
1146
- {
1147
- name: 'attendees',
1148
- url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/datasets/attendees',
1149
- },
1150
- ],
1151
- },
1152
- },
1153
- // removed participant from a removed dataset 'atd-unmuted':
1154
- {
1155
- htMeta: {
1156
- elementId: {
1157
- type: 'participant',
1158
- id: 14,
1159
- version: 300,
1160
- },
1161
- dataSetNames: ['atd-unmuted'],
1162
- },
1163
- data: null,
1164
- },
1165
- // new participant from a new data set 'attendees':
1166
- {
1167
- htMeta: {
1168
- elementId: {
1169
- type: 'participant',
1170
- id: 15,
1171
- version: 999,
1172
- },
1173
- dataSetNames: ['attendees'],
1174
- },
1175
- data: {
1176
- url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/participant/15',
1177
- person: {},
1178
- htMeta: {
1179
- elementId: {
1180
- type: 'participant',
1181
- id: 15,
1182
- version: 999,
1183
- },
1184
- dataSetNames: ['attendees'],
1185
- },
1186
- },
1187
- },
1188
- ],
1189
- });
1190
-
1191
- // verify also that an async initialization was done for
1192
- await checkAsyncDatasetInitialization(parser, newDataSet);
1193
- });
1194
-
1195
- it('handles metadata updates with same version (no callback)', () => {
1196
- const parser = createHashTreeParser();
1197
-
1198
- const selfPutItemSpy = sinon.spy(parser.dataSets.self.hashTree, 'putItem');
1199
-
1200
- // Create a locus update with metadata that has the same version and same visibleDataSets
1201
- const locusUpdate = {
1202
- dataSets: [createDataSet('self', 1, 2100)],
1203
- locus: {},
1204
- metadata: {
1205
- htMeta: {
1206
- elementId: {
1207
- type: 'metadata',
1208
- id: 5,
1209
- version: 50, // same version as initial
1210
- },
1211
- dataSetNames: ['self'],
1212
- },
1213
- visibleDataSets: [
1214
- {
1215
- name: 'main',
1216
- url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/datasets/main',
1217
- },
1218
- {
1219
- name: 'self',
1220
- url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/participant/713e9f99/datasets/self',
1221
- },
1222
- {
1223
- name: 'atd-unmuted',
1224
- url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/datasets/atd-unmuted',
1225
- },
1226
- ],
1227
- },
1228
- };
1229
-
1230
- // Call handleLocusUpdate
1231
- parser.handleLocusUpdate(locusUpdate);
1232
-
1233
- // Verify putItem was called on self hash tree
1234
- assert.calledOnceWithExactly(selfPutItemSpy, {type: 'metadata', id: 5, version: 50});
1235
-
1236
- // Verify callback was NOT called because version didn't change
1237
- assert.notCalled(callback);
1238
- });
1239
-
1240
- it('handles updates with no dataSets and metadata fields gracefully', () => {
1241
- const parser = createHashTreeParser();
1242
-
1243
- const mainPutItemsSpy = sinon.spy(parser.dataSets.main.hashTree, 'putItems');
1244
- const selfPutItemsSpy = sinon.spy(parser.dataSets.self.hashTree, 'putItems');
1245
- const atdUnmutedPutItemsSpy = sinon.spy(parser.dataSets['atd-unmuted'].hashTree, 'putItems');
1246
-
1247
- // Create a locus update with no dataSets and no metadata
1248
- const locusUpdate = {
1249
- locus: {
1250
- htMeta: {
1251
- elementId: {
1252
- type: 'locus',
1253
- id: 0,
1254
- version: 201,
1255
- },
1256
- dataSetNames: ['main'],
1257
- },
1258
- someData: 'value',
1259
- },
1260
- };
1261
-
1262
- // Call handleLocusUpdate - should not throw
1263
- parser.handleLocusUpdate(locusUpdate);
1264
-
1265
- // Verify putItems was still called for the dataset referenced in locus
1266
- assert.calledOnceWithExactly(mainPutItemsSpy, [{type: 'locus', id: 0, version: 201}]);
1267
-
1268
- // Verify putItems was not called on other hash trees
1269
- assert.notCalled(selfPutItemsSpy);
1270
- assert.notCalled(atdUnmutedPutItemsSpy);
1271
-
1272
- // Verify callback was called with the updated object
1273
- assert.calledOnceWithExactly(callback, LocusInfoUpdateType.OBJECTS_UPDATED, {
1274
- updatedObjects: [
1275
- {
1276
- htMeta: {
1277
- elementId: {
1278
- type: 'locus',
1279
- id: 0,
1280
- version: 201,
1281
- },
1282
- dataSetNames: ['main'],
1283
- },
1284
- data: {
1285
- someData: 'value',
1286
- htMeta: {
1287
- elementId: {
1288
- type: 'locus',
1289
- id: 0,
1290
- version: 201,
1291
- },
1292
- dataSetNames: ['main'],
1293
- },
1294
- },
1295
- },
1296
- ],
1297
- });
1298
-
1299
- // Verify that dataset versions were NOT updated (no dataSets in the update)
1300
- expect(parser.dataSets.main.version).to.equal(1000);
1301
- expect(parser.dataSets.self.version).to.equal(2000);
1302
- expect(parser.dataSets['atd-unmuted'].version).to.equal(3000);
1303
- });
1304
- });
1305
-
1306
- describe('#handleMessage', () => {
1307
- it('handles root hash heartbeat message correctly', async () => {
1308
- const parser = createHashTreeParser();
1309
-
1310
- // Step 1: Send a normal message with locusStateElements to start the sync timer
1311
- const normalMessage = {
1312
- dataSets: [
1313
- {
1314
- ...createDataSet('main', 16, 1100),
1315
- root: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1', // different from our hash
1316
- },
1317
- ],
1318
- visibleDataSetsUrl,
1319
- locusUrl,
1320
- locusStateElements: [
1321
- {
1322
- htMeta: {
1323
- elementId: {
1324
- type: 'locus' as const,
1325
- id: 0,
1326
- version: 201,
1327
- },
1328
- dataSetNames: ['main'],
1329
- },
1330
- data: {someData: 'value'},
1331
- },
1332
- ],
1333
- };
1334
-
1335
- parser.handleMessage(normalMessage, 'initial message');
1336
-
1337
- // Verify the timer was set (the sync algorithm should have started)
1338
- expect(parser.dataSets.main.timer).to.not.be.undefined;
1339
- const firstTimerDelay = parser.dataSets.main.idleMs; // 1000ms base + random backoff
1340
-
1341
- // Step 2: Simulate half of the time passing
1342
- clock.tick(500);
1343
-
1344
- // Verify no webex requests have been made yet
1345
- assert.notCalled(webexRequest);
1346
-
1347
- // Step 3: Send a heartbeat message (no locusStateElements) with mismatched root hash
1348
- const heartbeatMessage = createHeartbeatMessage(
1349
- 'main',
1350
- 16,
1351
- 1101,
1352
- 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb' // still different from our hash
1353
- );
1354
-
1355
- parser.handleMessage(heartbeatMessage, 'heartbeat message');
1356
-
1357
- // Verify the timer was restarted (should still exist)
1358
- expect(parser.dataSets.main.timer).to.not.be.undefined;
1359
-
1360
- // Step 4: Simulate more time passing (another 500ms) - total 1000ms from start
1361
- // This should NOT trigger the sync yet because the timer was restarted
1362
- clock.tick(500);
1363
-
1364
- // Verify still no hash requests or sync requests were sent
1365
- assert.notCalled(webexRequest);
1366
-
1367
- // Step 5: Mock the responses for the sync algorithm
1368
- const mainDataSetUrl = 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/datasets/main';
1369
-
1370
- // Mock getHashesFromLocus response
1371
- mockGetHashesFromLocusResponse(
1372
- mainDataSetUrl,
1373
- new Array(16).fill('00000000000000000000000000000000'),
1374
- createDataSet('main', 16, 1102)
1375
- );
1376
-
1377
- // Mock sendSyncRequestToLocus response - use matching root hash so no new timer is started
1378
- const syncResponseDataSet = createDataSet('main', 16, 1103);
1379
- syncResponseDataSet.root = parser.dataSets.main.hashTree.getRootHash();
1380
- mockSendSyncRequestResponse(mainDataSetUrl, {
1381
- dataSets: [syncResponseDataSet],
1382
- visibleDataSetsUrl,
1383
- locusUrl,
1384
- locusStateElements: [],
1385
- });
1386
-
1387
- // Step 6: Simulate the full delay passing (another 1000ms + 0ms backoff)
1388
- // We need to advance enough time for the restarted timer to expire
1389
- await clock.tickAsync(1000);
1390
-
1391
- // Now verify that the sync algorithm ran:
1392
- // 1. First, getHashesFromLocus should have been called
1393
- assert.calledWith(
1394
- webexRequest,
1395
- sinon.match({
1396
- method: 'GET',
1397
- uri: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/datasets/main/hashtree',
1398
- })
1399
- );
1400
-
1401
- // 2. Then, sendSyncRequestToLocus should have been called
1402
- assert.calledWith(
1403
- webexRequest,
1404
- sinon.match({
1405
- method: 'POST',
1406
- uri: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/datasets/main/sync',
1407
- })
1408
- );
1409
- });
1410
-
1411
- it('handles normal updates to hash trees correctly - updates hash trees', async () => {
1412
- const parser = createHashTreeParser();
1413
-
1414
- // Stub updateItems on hash trees
1415
- const mainUpdateItemsStub = sinon
1416
- .stub(parser.dataSets.main.hashTree, 'updateItems')
1417
- .returns([true]);
1418
- const selfUpdateItemsStub = sinon
1419
- .stub(parser.dataSets.self.hashTree, 'updateItems')
1420
- .returns([true]);
1421
- const atdUnmutedUpdateItemsStub = sinon
1422
- .stub(parser.dataSets['atd-unmuted'].hashTree, 'updateItems')
1423
- .returns([true, true]);
1424
-
1425
- // Create a message with updates to multiple datasets
1426
- const message = {
1427
- dataSets: [
1428
- createDataSet('main', 16, 1100),
1429
- createDataSet('self', 1, 2100),
1430
- createDataSet('atd-unmuted', 16, 3100),
1431
- ],
1432
- visibleDataSetsUrl,
1433
- locusUrl,
1434
- locusStateElements: [
1435
- {
1436
- htMeta: {
1437
- elementId: {
1438
- type: 'locus' as const,
1439
- id: 0,
1440
- version: 201,
1441
- },
1442
- dataSetNames: ['main'],
1443
- },
1444
- data: {info: {id: 'updated-locus-info'}},
1445
- },
1446
- {
1447
- htMeta: {
1448
- elementId: {
1449
- type: 'self' as const,
1450
- id: 4,
1451
- version: 101,
1452
- },
1453
- dataSetNames: ['self'],
1454
- },
1455
- data: {person: {name: 'updated self name'}},
1456
- },
1457
- {
1458
- htMeta: {
1459
- elementId: {
1460
- type: 'participant' as const,
1461
- id: 14,
1462
- version: 301,
1463
- },
1464
- dataSetNames: ['atd-unmuted'],
1465
- },
1466
- data: {person: {name: 'participant name'}},
1467
- },
1468
- {
1469
- htMeta: {
1470
- elementId: {
1471
- type: 'participant' as const,
1472
- id: 15,
1473
- version: 302,
1474
- },
1475
- dataSetNames: ['atd-unmuted'],
1476
- },
1477
- data: {person: {name: 'another participant'}},
1478
- },
1479
- ],
1480
- };
1481
-
1482
- parser.handleMessage(message, 'normal update');
1483
-
1484
- // Verify updateItems was called on main hash tree
1485
- assert.calledOnceWithExactly(mainUpdateItemsStub, [
1486
- {operation: 'update', item: {type: 'locus', id: 0, version: 201}},
1487
- ]);
1488
-
1489
- // Verify updateItems was called on self hash tree
1490
- assert.calledOnceWithExactly(selfUpdateItemsStub, [
1491
- {operation: 'update', item: {type: 'self', id: 4, version: 101}},
1492
- ]);
1493
-
1494
- // Verify updateItems was called on atd-unmuted hash tree with both participants
1495
- assert.calledOnceWithExactly(atdUnmutedUpdateItemsStub, [
1496
- {operation: 'update', item: {type: 'participant', id: 14, version: 301}},
1497
- {operation: 'update', item: {type: 'participant', id: 15, version: 302}},
1498
- ]);
1499
-
1500
- // Verify callback was called with OBJECTS_UPDATED and all updated objects
1501
- assert.calledOnceWithExactly(callback, LocusInfoUpdateType.OBJECTS_UPDATED, {
1502
- updatedObjects: [
1503
- {
1504
- htMeta: {
1505
- elementId: {type: 'locus', id: 0, version: 201},
1506
- dataSetNames: ['main'],
1507
- },
1508
- data: {info: {id: 'updated-locus-info'}},
1509
- },
1510
- {
1511
- htMeta: {
1512
- elementId: {type: 'self', id: 4, version: 101},
1513
- dataSetNames: ['self'],
1514
- },
1515
- data: {person: {name: 'updated self name'}},
1516
- },
1517
- {
1518
- htMeta: {
1519
- elementId: {type: 'participant', id: 14, version: 301},
1520
- dataSetNames: ['atd-unmuted'],
1521
- },
1522
- data: {person: {name: 'participant name'}},
1523
- },
1524
- {
1525
- htMeta: {
1526
- elementId: {type: 'participant', id: 15, version: 302},
1527
- dataSetNames: ['atd-unmuted'],
1528
- },
1529
- data: {person: {name: 'another participant'}},
1530
- },
1531
- ],
1532
- });
1533
- });
1534
-
1535
- describe('handles sentinel messages correctly', () => {
1536
- ['main', 'self', 'unjoined'].forEach((dataSetName) => {
1537
- it('emits MEETING_ENDED for sentinel message with dataset ' + dataSetName, async () => {
1538
- const parser = createHashTreeParser();
1539
-
1540
- // Create a sentinel message: leafCount=1, root=EMPTY_HASH, version higher than current
1541
- const sentinelMessage = createHeartbeatMessage(
1542
- dataSetName,
1543
- 1,
1544
- parser.dataSets[dataSetName]?.version
1545
- ? parser.dataSets[dataSetName].version + 1
1546
- : 10000,
1547
- EMPTY_HASH
1548
- );
1549
-
1550
- // If the dataset doesn't exist yet (e.g. 'unjoined'), create it
1551
- if (!parser.dataSets[dataSetName]) {
1552
- parser.dataSets[dataSetName] = {
1553
- url: `https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/datasets/${dataSetName}`,
1554
- name: dataSetName,
1555
- version: 1,
1556
- leafCount: 16,
1557
- root: '0'.repeat(32),
1558
- idleMs: 1000,
1559
- backoff: {maxMs: 1000, exponent: 2},
1560
- } as any;
1561
- }
1562
-
1563
- parser.handleMessage(sentinelMessage, 'sentinel message');
1564
-
1565
- // Verify callback was called with MEETING_ENDED
1566
- assert.calledOnceWithExactly(callback, LocusInfoUpdateType.MEETING_ENDED, {
1567
- updatedObjects: undefined,
1568
- });
1569
-
1570
- // Verify that all timers were stopped
1571
- Object.values(parser.dataSets).forEach((ds: any) => {
1572
- assert.isUndefined(ds.timer);
1573
- assert.isUndefined(ds.heartbeatWatchdogTimer);
1574
- });
1575
- });
1576
- });
1577
-
1578
- it('emits MEETING_ENDED for sentinel message with unknown dataset', async () => {
1579
- const parser = createHashTreeParser();
1580
-
1581
- // 'unjoined' is a valid sentinel dataset name but is not tracked by the parser
1582
- assert.isUndefined(parser.dataSets['unjoined']);
1583
-
1584
- // Create a sentinel message for 'unjoined' dataset which the parser has never seen
1585
- const sentinelMessage = createHeartbeatMessage('unjoined', 1, 10000, EMPTY_HASH);
1586
-
1587
- parser.handleMessage(sentinelMessage, 'sentinel message');
1588
-
1589
- // Verify callback was called with MEETING_ENDED
1590
- assert.calledOnceWithExactly(callback, LocusInfoUpdateType.MEETING_ENDED, {
1591
- updatedObjects: undefined,
1592
- });
1593
-
1594
- // Verify that all timers were stopped
1595
- Object.values(parser.dataSets).forEach((ds: any) => {
1596
- assert.isUndefined(ds.timer);
1597
- assert.isUndefined(ds.heartbeatWatchdogTimer);
1598
- });
1599
- });
1600
- });
1601
-
1602
- describe('sync algorithm', () => {
1603
- it('runs correctly after a message is received', async () => {
1604
- const parser = createHashTreeParser();
1605
-
1606
- // Create a message with updates and mismatched root hash
1607
- const message = {
1608
- dataSets: [
1609
- {
1610
- ...createDataSet('main', 16, 1100),
1611
- },
1612
- ],
1613
- visibleDataSetsUrl,
1614
- locusUrl,
1615
- locusStateElements: [
1616
- {
1617
- htMeta: {
1618
- elementId: {
1619
- type: 'locus' as const,
1620
- id: 0,
1621
- version: 201,
1622
- },
1623
- dataSetNames: ['main'],
1624
- },
1625
- data: {info: {id: 'initial-update'}},
1626
- },
1627
- ],
1628
- };
1629
-
1630
- parser.handleMessage(message, 'initial message');
1631
-
1632
- // Verify callback was called with initial updates
1633
- assert.calledOnce(callback);
1634
- callback.resetHistory();
1635
-
1636
- // Setup mocks for sync algorithm
1637
- const mainDataSetUrl = parser.dataSets.main.url;
1638
-
1639
- // Mock getHashesFromLocus response
1640
- mockGetHashesFromLocusResponse(
1641
- mainDataSetUrl,
1642
- new Array(16).fill('00000000000000000000000000000000'),
1643
- createDataSet('main', 16, 1101)
1644
- );
1645
-
1646
- // Mock sendSyncRequestToLocus response with matching root hash
1647
- const mainSyncDataSet = createDataSet('main', 16, 1101);
1648
- mainSyncDataSet.root = parser.dataSets.main.hashTree.getRootHash();
1649
- mockSendSyncRequestResponse(mainDataSetUrl, {
1650
- dataSets: [mainSyncDataSet],
1651
- visibleDataSetsUrl,
1652
- locusUrl,
1653
- locusStateElements: [
1654
- {
1655
- htMeta: {
1656
- elementId: {
1657
- type: 'locus' as const,
1658
- id: 1,
1659
- version: 202,
1660
- },
1661
- dataSetNames: ['main'],
1662
- },
1663
- data: {info: {id: 'synced-locus'}},
1664
- },
1665
- ],
1666
- });
1667
-
1668
- // Simulate time passing to trigger sync algorithm (1000ms base + 0 backoff)
1669
- await clock.tickAsync(1000);
1670
-
1671
- // Verify that sync requests were sent for main dataset
1672
- assert.calledWith(
1673
- webexRequest,
1674
- sinon.match({
1675
- method: 'GET',
1676
- uri: `${mainDataSetUrl}/hashtree`,
1677
- })
1678
- );
1679
- assert.calledWith(
1680
- webexRequest,
1681
- sinon.match({
1682
- method: 'POST',
1683
- uri: `${mainDataSetUrl}/sync`,
1684
- })
1685
- );
1686
-
1687
- // Verify that callback was called with synced objects
1688
- assert.calledOnceWithExactly(callback, LocusInfoUpdateType.OBJECTS_UPDATED, {
1689
- updatedObjects: [
1690
- {
1691
- htMeta: {
1692
- elementId: {type: 'locus', id: 1, version: 202},
1693
- dataSetNames: ['main'],
1694
- },
1695
- data: {info: {id: 'synced-locus'}},
1696
- },
1697
- ],
1698
- });
1699
- });
1700
-
1701
- describe('emits MEETING_ENDED', () => {
1702
- [404, 409].forEach((statusCode) => {
1703
- it(`when /hashtree returns ${statusCode}`, async () => {
1704
- const parser = createHashTreeParser();
1705
-
1706
- // Send a message to trigger sync algorithm
1707
- const message = {
1708
- dataSets: [createDataSet('main', 16, 1100)],
1709
- visibleDataSetsUrl,
1710
- locusUrl,
1711
- locusStateElements: [
1712
- {
1713
- htMeta: {
1714
- elementId: {
1715
- type: 'locus' as const,
1716
- id: 0,
1717
- version: 201,
1718
- },
1719
- dataSetNames: ['main'],
1720
- },
1721
- data: {info: {id: 'initial-update'}},
1722
- },
1723
- ],
1724
- };
1725
-
1726
- parser.handleMessage(message, 'initial message');
1727
- callback.resetHistory();
1728
-
1729
- const mainDataSetUrl = parser.dataSets.main.url;
1730
-
1731
- // Mock getHashesFromLocus to reject with the sentinel error
1732
- const error: any = new Error(`Request failed with status ${statusCode}`);
1733
- error.statusCode = statusCode;
1734
- if (statusCode === 409) {
1735
- error.body = {errorCode: 2403004};
1736
- }
1737
- webexRequest
1738
- .withArgs(
1739
- sinon.match({
1740
- method: 'GET',
1741
- uri: `${mainDataSetUrl}/hashtree`,
1742
- })
1743
- )
1744
- .rejects(error);
1745
-
1746
- // Trigger sync by advancing time
1747
- await clock.tickAsync(1000);
1748
-
1749
- // Verify callback was called with MEETING_ENDED
1750
- assert.calledOnceWithExactly(callback, LocusInfoUpdateType.MEETING_ENDED, {
1751
- updatedObjects: undefined,
1752
- });
1753
-
1754
- // Verify all timers are stopped
1755
- Object.values(parser.dataSets).forEach((ds: any) => {
1756
- assert.isUndefined(ds.timer);
1757
- assert.isUndefined(ds.heartbeatWatchdogTimer);
1758
- });
1759
- });
1760
-
1761
- it(`when /sync returns ${statusCode}`, async () => {
1762
- const parser = createHashTreeParser();
1763
-
1764
- // Send a message to trigger sync algorithm
1765
- const message = {
1766
- dataSets: [createDataSet('main', 16, 1100)],
1767
- visibleDataSetsUrl,
1768
- locusUrl,
1769
- locusStateElements: [
1770
- {
1771
- htMeta: {
1772
- elementId: {
1773
- type: 'locus' as const,
1774
- id: 0,
1775
- version: 201,
1776
- },
1777
- dataSetNames: ['main'],
1778
- },
1779
- data: {info: {id: 'initial-update'}},
1780
- },
1781
- ],
1782
- };
1783
-
1784
- parser.handleMessage(message, 'initial message');
1785
- callback.resetHistory();
1786
-
1787
- const mainDataSetUrl = parser.dataSets.main.url;
1788
-
1789
- // Mock getHashesFromLocus to succeed
1790
- mockGetHashesFromLocusResponse(
1791
- mainDataSetUrl,
1792
- new Array(16).fill('00000000000000000000000000000000'),
1793
- createDataSet('main', 16, 1101)
1794
- );
1795
-
1796
- // Mock sendSyncRequestToLocus to reject with the sentinel error
1797
- const error: any = new Error(`Request failed with status ${statusCode}`);
1798
- error.statusCode = statusCode;
1799
- if (statusCode === 409) {
1800
- error.body = {errorCode: 2403004};
1801
- }
1802
- webexRequest
1803
- .withArgs(
1804
- sinon.match({
1805
- method: 'POST',
1806
- uri: `${mainDataSetUrl}/sync`,
1807
- })
1808
- )
1809
- .rejects(error);
1810
-
1811
- // Trigger sync by advancing time
1812
- await clock.tickAsync(1000);
1813
-
1814
- // Verify callback was called with MEETING_ENDED
1815
- assert.calledOnceWithExactly(callback, LocusInfoUpdateType.MEETING_ENDED, {
1816
- updatedObjects: undefined,
1817
- });
1818
-
1819
- // Verify all timers are stopped
1820
- Object.values(parser.dataSets).forEach((ds: any) => {
1821
- assert.isUndefined(ds.timer);
1822
- assert.isUndefined(ds.heartbeatWatchdogTimer);
1823
- });
1824
- });
1825
- });
1826
- });
1827
-
1828
- it('requests only mismatched hashes during sync', async () => {
1829
- const parser = createHashTreeParser();
1830
-
1831
- // Create a message with updates to trigger sync algorithm
1832
- const message = {
1833
- dataSets: [createDataSet('main', 16, 1100)],
1834
- visibleDataSetsUrl,
1835
- locusUrl,
1836
- locusStateElements: [
1837
- {
1838
- htMeta: {
1839
- elementId: {
1840
- type: 'locus' as const,
1841
- id: 0,
1842
- version: 201,
1843
- },
1844
- dataSetNames: ['main'],
1845
- },
1846
- data: {info: {id: 'initial-update'}},
1847
- },
1848
- {
1849
- htMeta: {
1850
- elementId: {
1851
- type: 'participant' as const,
1852
- id: 3,
1853
- version: 301,
1854
- },
1855
- dataSetNames: ['main'],
1856
- },
1857
- data: {id: 'participant with id=3'},
1858
- },
1859
- {
1860
- htMeta: {
1861
- elementId: {
1862
- type: 'participant' as const,
1863
- id: 4,
1864
- version: 301,
1865
- },
1866
- dataSetNames: ['main'],
1867
- },
1868
- data: {id: 'participant with id=4'},
1869
- },
1870
- ],
1871
- };
1872
-
1873
- parser.handleMessage(message, 'initial message');
1874
-
1875
- callback.resetHistory();
1876
-
1877
- // Setup the hash tree to have specific hashes for each leaf
1878
- // We'll make leaf 0 and leaf 4 have mismatched hashes
1879
- const hashTree = parser.dataSets.main.hashTree;
1880
-
1881
- // Get the actual hashes for all leaves after the items were added
1882
- const actualHashes = new Array(16);
1883
- for (let i = 0; i < 16; i++) {
1884
- actualHashes[i] = hashTree.leafHashes[i];
1885
- }
1886
-
1887
- // Mock getHashesFromLocus to return hashes where most match but 0 and 4 don't
1888
- actualHashes[0] = 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb';
1889
- actualHashes[4] = 'cccccccccccccccccccccccccccccccc';
1890
-
1891
- const mainDataSetUrl = parser.dataSets.main.url;
1892
- mockGetHashesFromLocusResponse(
1893
- mainDataSetUrl,
1894
- actualHashes,
1895
- createDataSet('main', 16, 1101)
1896
- );
1897
-
1898
- // Mock sendSyncRequestToLocus response with matching root hash
1899
- const mainSyncDataSet = createDataSet('main', 16, 1101);
1900
- mainSyncDataSet.root = hashTree.getRootHash();
1901
- mockSendSyncRequestResponse(mainDataSetUrl, {
1902
- dataSets: [mainSyncDataSet],
1903
- visibleDataSetsUrl,
1904
- locusUrl,
1905
- locusStateElements: [],
1906
- });
1907
-
1908
- // Trigger the sync algorithm by advancing time
1909
- await clock.tickAsync(1000);
1910
-
1911
- // Verify getHashesFromLocus was called
1912
- assert.calledWith(
1913
- webexRequest,
1914
- sinon.match({
1915
- method: 'GET',
1916
- uri: `${mainDataSetUrl}/hashtree`,
1917
- qs: {
1918
- rootHash: hashTree.getRootHash(),
1919
- },
1920
- })
1921
- );
1922
-
1923
- // Verify sendSyncRequestToLocus was called with only the mismatched leaf indices 0 and 4
1924
- assert.calledWith(webexRequest, {
1925
- method: 'POST',
1926
- uri: `${mainDataSetUrl}/sync`,
1927
- qs: {rootHash: hashTree.getRootHash()},
1928
- body: {
1929
- leafCount: 16,
1930
- leafDataEntries: [
1931
- {leafIndex: 0, elementIds: [{type: 'locus', id: 0, version: 201}]},
1932
- {leafIndex: 4, elementIds: [{type: 'participant', id: 4, version: 301}]},
1933
- ],
1934
- },
1935
- });
1936
- });
1937
-
1938
- it('does not get the hashes if leafCount === 1', async () => {
1939
- const parser = createHashTreeParser();
1940
-
1941
- // Create a message with updates to self dataset
1942
- const message = {
1943
- dataSets: [createDataSet('self', 1, 2001)],
1944
- visibleDataSetsUrl,
1945
- locusUrl,
1946
- locusStateElements: [
1947
- {
1948
- htMeta: {
1949
- elementId: {
1950
- type: 'self' as const,
1951
- id: 4,
1952
- version: 102,
1953
- },
1954
- dataSetNames: ['self'],
1955
- },
1956
- data: {id: 'updated self'},
1957
- },
1958
- ],
1959
- };
1960
-
1961
- parser.handleMessage(message, 'message with self update');
1962
-
1963
- callback.resetHistory();
1964
-
1965
- // Trigger the sync algorithm by advancing time
1966
- await clock.tickAsync(1000);
1967
-
1968
- // self data set has only 1 leaf, so sync should skip the step of getting hashes
1969
- assert.neverCalledWith(
1970
- webexRequest,
1971
- sinon.match({
1972
- method: 'GET',
1973
- uri: `${parser.dataSets.self.url}/hashtree`,
1974
- })
1975
- );
1976
-
1977
- // Verify sendSyncRequestToLocus was called with the single leaf
1978
- assert.calledWith(webexRequest, {
1979
- method: 'POST',
1980
- uri: `${parser.dataSets.self.url}/sync`,
1981
- qs: {rootHash: parser.dataSets.self.hashTree.getRootHash()},
1982
- body: {
1983
- leafCount: 1,
1984
- leafDataEntries: [
1985
- {
1986
- leafIndex: 0,
1987
- elementIds: [
1988
- {type: 'self', id: 4, version: 102},
1989
- {type: 'metadata', id: 5, version: 50},
1990
- ],
1991
- },
1992
- ],
1993
- },
1994
- });
1995
- });
1996
- });
1997
-
1998
- describe('handles visible data sets changes correctly', () => {
1999
- it('handles addition of visible data set (one that does not require async initialization)', async () => {
2000
- // Create a parser with visible datasets
2001
- const parser = createHashTreeParser();
2002
-
2003
- // Stub updateItems on self hash tree to return true
2004
- sinon.stub(parser.dataSets.self.hashTree, 'updateItems').returns([true]);
2005
-
2006
- // Send a message with Metadata object that has a new visibleDataSets list
2007
- const message = {
2008
- dataSets: [createDataSet('self', 1, 2100), createDataSet('attendees', 8, 4000)],
2009
- visibleDataSetsUrl,
2010
- locusUrl,
2011
- locusStateElements: [
2012
- {
2013
- htMeta: {
2014
- elementId: {
2015
- type: 'metadata' as const,
2016
- id: 5,
2017
- version: 51,
2018
- },
2019
- dataSetNames: ['self'],
2020
- },
2021
- data: {
2022
- visibleDataSets: [
2023
- {
2024
- name: 'main',
2025
- url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/datasets/main',
2026
- },
2027
- {
2028
- name: 'self',
2029
- url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/participant/713e9f99/datasets/self',
2030
- },
2031
- {
2032
- name: 'atd-unmuted',
2033
- url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/datasets/atd-unmuted',
2034
- },
2035
- {
2036
- name: 'attendees',
2037
- url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/datasets/attendees',
2038
- },
2039
- ], // added 'attendees'
2040
- },
2041
- },
2042
- ],
2043
- };
2044
-
2045
- parser.handleMessage(message, 'add visible dataset');
2046
-
2047
- // Verify that 'attendees' was added to visibleDataSets
2048
- expect(parser.visibleDataSets.some((vds) => vds.name === 'attendees')).to.be.true;
2049
-
2050
- // Verify that a hash tree was created for 'attendees'
2051
- assert.exists(parser.dataSets.attendees.hashTree);
2052
- assert.equal(parser.dataSets.attendees.hashTree.numLeaves, 8);
2053
-
2054
- // Verify callback was called with the metadata update (appears twice - processed once for visible dataset changes, once in main loop)
2055
- assert.calledOnceWithExactly(callback, LocusInfoUpdateType.OBJECTS_UPDATED, {
2056
- updatedObjects: [
2057
- {
2058
- htMeta: {
2059
- elementId: {type: 'metadata', id: 5, version: 51},
2060
- dataSetNames: ['self'],
2061
- },
2062
- data: {
2063
- visibleDataSets: [
2064
- {
2065
- name: 'main',
2066
- url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/datasets/main',
2067
- },
2068
- {
2069
- name: 'self',
2070
- url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/participant/713e9f99/datasets/self',
2071
- },
2072
- {
2073
- name: 'atd-unmuted',
2074
- url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/datasets/atd-unmuted',
2075
- },
2076
- {
2077
- name: 'attendees',
2078
- url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/datasets/attendees',
2079
- },
2080
- ],
2081
- },
2082
- },
2083
- {
2084
- htMeta: {
2085
- elementId: {type: 'metadata', id: 5, version: 51},
2086
- dataSetNames: ['self'],
2087
- },
2088
- data: {
2089
- visibleDataSets: [
2090
- {
2091
- name: 'main',
2092
- url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/datasets/main',
2093
- },
2094
- {
2095
- name: 'self',
2096
- url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/participant/713e9f99/datasets/self',
2097
- },
2098
- {
2099
- name: 'atd-unmuted',
2100
- url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/datasets/atd-unmuted',
2101
- },
2102
- {
2103
- name: 'attendees',
2104
- url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/datasets/attendees',
2105
- },
2106
- ],
2107
- },
2108
- },
2109
- ],
2110
- });
2111
- });
2112
-
2113
- it('handles addition of visible data set (one that requires async initialization)', async () => {
2114
- // Create a parser with visible datasets
2115
- const parser = createHashTreeParser();
2116
-
2117
- // Stub updateItems on self hash tree to return true
2118
- sinon.stub(parser.dataSets.self.hashTree, 'updateItems').returns([true]);
2119
-
2120
- // Send a message with Metadata object that has a new visibleDataSets list (adding 'new-dataset')
2121
- // but WITHOUT providing info about the new dataset in dataSets array
2122
- const message = {
2123
- dataSets: [createDataSet('self', 1, 2100)],
2124
- visibleDataSetsUrl,
2125
- locusUrl,
2126
- locusStateElements: [
2127
- {
2128
- htMeta: {
2129
- elementId: {
2130
- type: 'metadata' as const,
2131
- id: 5,
2132
- version: 51,
2133
- },
2134
- dataSetNames: ['self'],
2135
- },
2136
- data: {
2137
- visibleDataSets: [
2138
- {
2139
- name: 'main',
2140
- url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/datasets/main',
2141
- },
2142
- {
2143
- name: 'self',
2144
- url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/participant/713e9f99/datasets/self',
2145
- },
2146
- {
2147
- name: 'atd-unmuted',
2148
- url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/datasets/atd-unmuted',
2149
- },
2150
- {
2151
- name: 'new-dataset',
2152
- url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/datasets/new-dataset',
2153
- },
2154
- ],
2155
- },
2156
- },
2157
- ],
2158
- };
2159
-
2160
- // Mock the async initialization of the new dataset
2161
- const newDataSet = createDataSet('new-dataset', 4, 5000);
2162
- mockGetAllDataSetsMetadata(webexRequest, visibleDataSetsUrl, [newDataSet]);
2163
- mockSyncRequest(webexRequest, newDataSet.url, {
2164
- dataSets: [newDataSet],
2165
- visibleDataSetsUrl,
2166
- locusUrl,
2167
- locusStateElements: [],
2168
- });
2169
-
2170
- parser.handleMessage(message, 'add new dataset requiring async init');
2171
-
2172
- await checkAsyncDatasetInitialization(parser, newDataSet);
2173
- });
2174
-
2175
- it('emits MEETING_ENDED if async init of a new visible dataset fails with 404', async () => {
2176
- const parser = createHashTreeParser();
2177
-
2178
- // Stub updateItems on self hash tree to return true
2179
- sinon.stub(parser.dataSets.self.hashTree, 'updateItems').returns([true]);
2180
-
2181
- // Send a message with Metadata object that adds a new visible dataset
2182
- const message = {
2183
- dataSets: [createDataSet('self', 1, 2100)],
2184
- visibleDataSetsUrl,
2185
- locusUrl,
2186
- locusStateElements: [
2187
- {
2188
- htMeta: {
2189
- elementId: {
2190
- type: 'metadata' as const,
2191
- id: 5,
2192
- version: 51,
2193
- },
2194
- dataSetNames: ['self'],
2195
- },
2196
- data: {
2197
- visibleDataSets: [
2198
- {
2199
- name: 'main',
2200
- url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/datasets/main',
2201
- },
2202
- {
2203
- name: 'self',
2204
- url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/participant/713e9f99/datasets/self',
2205
- },
2206
- {
2207
- name: 'atd-unmuted',
2208
- url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/datasets/atd-unmuted',
2209
- },
2210
- {
2211
- name: 'new-dataset',
2212
- url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/datasets/new-dataset',
2213
- },
2214
- ],
2215
- },
2216
- },
2217
- ],
2218
- };
2219
-
2220
- // Mock getAllDataSetsMetadata to reject with 404
2221
- const error: any = new Error('Request failed with status 404');
2222
- error.statusCode = 404;
2223
- webexRequest
2224
- .withArgs(
2225
- sinon.match({
2226
- method: 'GET',
2227
- uri: visibleDataSetsUrl,
2228
- })
2229
- )
2230
- .rejects(error);
2231
-
2232
- parser.handleMessage(message, 'add new dataset triggering 404');
2233
-
2234
- // The first callback call is from parseMessage with the metadata update
2235
- callback.resetHistory();
2236
-
2237
- // Wait for the async initialization (queueMicrotask) to complete
2238
- await clock.tickAsync(0);
2239
-
2240
- // Verify callback was called with MEETING_ENDED
2241
- assert.calledOnceWithExactly(callback, LocusInfoUpdateType.MEETING_ENDED, {
2242
- updatedObjects: undefined,
2243
- });
2244
- });
2245
-
2246
- it('handles removal of visible data set', async () => {
2247
- // Create a parser with visible datasets
2248
- const parser = createHashTreeParser();
2249
-
2250
- // Store the initial hash tree for atd-unmuted to verify it gets deleted
2251
- const atdUnmutedHashTree = parser.dataSets['atd-unmuted'].hashTree;
2252
- assert.exists(atdUnmutedHashTree);
2253
-
2254
- // Stub getLeafData to return some items that will be marked as removed
2255
- // It's called for each leaf (16 leaves), so return an array for leaf 14 and empty for others
2256
- const getLeafDataStub = sinon.stub(atdUnmutedHashTree, 'getLeafData');
2257
- getLeafDataStub.withArgs(14).returns([{type: 'participant', id: 14, version: 301}]);
2258
- getLeafDataStub.returns([]);
2259
-
2260
- // Stub updateItems on self hash tree to return true
2261
- sinon.stub(parser.dataSets.self.hashTree, 'updateItems').returns([true]);
2262
-
2263
- // Send a message with Metadata object that has removed 'atd-unmuted' from visibleDataSets
2264
- const message = {
2265
- dataSets: [createDataSet('self', 1, 2100)],
2266
- visibleDataSetsUrl,
2267
- locusUrl,
2268
- locusStateElements: [
2269
- {
2270
- htMeta: {
2271
- elementId: {
2272
- type: 'metadata' as const,
2273
- id: 5,
2274
- version: 51,
2275
- },
2276
- dataSetNames: ['self'],
2277
- },
2278
- data: {
2279
- visibleDataSets: [
2280
- {
2281
- name: 'main',
2282
- url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/datasets/main',
2283
- },
2284
- {
2285
- name: 'self',
2286
- url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/participant/713e9f99/datasets/self',
2287
- },
2288
- ], // removed 'atd-unmuted'
2289
- },
2290
- },
2291
- ],
2292
- };
2293
-
2294
- parser.handleMessage(message, 'remove visible dataset');
2295
-
2296
- // Verify that 'atd-unmuted' was removed from visibleDataSets
2297
- expect(parser.visibleDataSets.some((vds) => vds.name === 'atd-unmuted')).to.be.false;
2298
-
2299
- // Verify that the hash tree for 'atd-unmuted' was deleted
2300
- assert.isUndefined(parser.dataSets['atd-unmuted'].hashTree);
2301
-
2302
- // Verify that the timer was cleared
2303
- assert.isUndefined(parser.dataSets['atd-unmuted'].timer);
2304
-
2305
- // Verify callback was called with the metadata update and the removed objects (metadata appears twice - processed once for dataset changes, once in main loop)
2306
- assert.calledOnceWithExactly(callback, LocusInfoUpdateType.OBJECTS_UPDATED, {
2307
- updatedObjects: [
2308
- {
2309
- htMeta: {
2310
- elementId: {type: 'metadata', id: 5, version: 51},
2311
- dataSetNames: ['self'],
2312
- },
2313
- data: {
2314
- visibleDataSets: [
2315
- {
2316
- name: 'main',
2317
- url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/datasets/main',
2318
- },
2319
- {
2320
- name: 'self',
2321
- url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/participant/713e9f99/datasets/self',
2322
- },
2323
- ],
2324
- },
2325
- },
2326
- {
2327
- htMeta: {
2328
- elementId: {type: 'participant', id: 14, version: 301},
2329
- dataSetNames: ['atd-unmuted'],
2330
- },
2331
- data: null,
2332
- },
2333
- {
2334
- htMeta: {
2335
- elementId: {type: 'metadata', id: 5, version: 51},
2336
- dataSetNames: ['self'],
2337
- },
2338
- data: {
2339
- visibleDataSets: [
2340
- {
2341
- name: 'main',
2342
- url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/datasets/main',
2343
- },
2344
- {
2345
- name: 'self',
2346
- url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/participant/713e9f99/datasets/self',
2347
- },
2348
- ],
2349
- },
2350
- },
2351
- ],
2352
- });
2353
- });
2354
- it('ignores data if it is not in a visible data set', async () => {
2355
- // Create a parser with attendees in datasets but not in visibleDataSets
2356
- const parser = createHashTreeParser({
2357
- dataSets: [
2358
- ...exampleInitialLocus.dataSets,
2359
- {
2360
- url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/datasets/attendees',
2361
- root: '0'.repeat(32),
2362
- version: 4000,
2363
- leafCount: 8,
2364
- name: 'attendees',
2365
- idleMs: 1000,
2366
- backoff: {maxMs: 1000, exponent: 2},
2367
- },
2368
- ],
2369
- locus: {...exampleInitialLocus.locus},
2370
- });
2371
-
2372
- // Verify attendees is NOT in visibleDataSets
2373
- expect(parser.visibleDataSets.some((vds) => vds.name === 'attendees')).to.be.false;
2374
-
2375
- // Send a message with attendees data
2376
- const message = {
2377
- dataSets: [createDataSet('attendees', 8, 4001)],
2378
- visibleDataSetsUrl,
2379
- locusUrl,
2380
- locusStateElements: [
2381
- {
2382
- htMeta: {
2383
- elementId: {
2384
- type: 'participant' as const,
2385
- id: 20,
2386
- version: 303,
2387
- },
2388
- dataSetNames: ['attendees'],
2389
- },
2390
- data: {person: {name: 'participant in attendees'}},
2391
- },
2392
- ],
2393
- };
2394
-
2395
- parser.handleMessage(message, 'message with non-visible dataset');
2396
-
2397
- // Verify that no hash tree was created for attendees
2398
- assert.isUndefined(parser.dataSets.attendees.hashTree);
2399
-
2400
- // Verify callback was NOT called (no updates for non-visible datasets)
2401
- assert.notCalled(callback);
2402
- });
2403
- });
2404
-
2405
- describe('heartbeat watchdog', () => {
2406
- it('initiates sync immediately only for the specific data set whose heartbeat watchdog fires', async () => {
2407
- const parser = createHashTreeParser();
2408
- const heartbeatIntervalMs = 5000;
2409
-
2410
- // Send initial heartbeat message for 'main' only
2411
- const heartbeatMessage = {
2412
- dataSets: [
2413
- {
2414
- ...createDataSet('main', 16, 1100),
2415
- root: parser.dataSets.main.hashTree.getRootHash(),
2416
- },
2417
- ],
2418
- visibleDataSetsUrl,
2419
- locusUrl,
2420
- heartbeatIntervalMs,
2421
- };
2422
-
2423
- parser.handleMessage(heartbeatMessage, 'initial heartbeat');
2424
-
2425
- // Verify only 'main' watchdog timer is set
2426
- expect(parser.dataSets.main.heartbeatWatchdogTimer).to.not.be.undefined;
2427
- expect(parser.dataSets.self.heartbeatWatchdogTimer).to.be.undefined;
2428
- expect(parser.dataSets['atd-unmuted'].heartbeatWatchdogTimer).to.be.undefined;
2429
-
2430
- // Mock responses for performSync (GET hashtree then POST sync for leafCount > 1)
2431
- const mainDataSetUrl = parser.dataSets.main.url;
2432
- mockGetHashesFromLocusResponse(
2433
- mainDataSetUrl,
2434
- new Array(16).fill('00000000000000000000000000000000'),
2435
- createDataSet('main', 16, 1101)
2436
- );
2437
- mockSendSyncRequestResponse(mainDataSetUrl, null);
2438
-
2439
- // Advance time past heartbeatIntervalMs + backoff (Math.random returns 0, so backoff = 0)
2440
- // performSync is called immediately when the watchdog fires - no additional delay
2441
- await clock.tickAsync(heartbeatIntervalMs);
2442
-
2443
- // Verify sync request was sent immediately for 'main' (GET hashtree + POST sync)
2444
- assert.calledWith(
2445
- webexRequest,
2446
- sinon.match({
2447
- method: 'GET',
2448
- uri: `${mainDataSetUrl}/hashtree`,
2449
- })
2450
- );
2451
-
2452
- // Verify no sync requests were sent for other datasets
2453
- assert.neverCalledWith(
2454
- webexRequest,
2455
- sinon.match({
2456
- method: 'POST',
2457
- uri: `${parser.dataSets.self.url}/sync`,
2458
- })
2459
- );
2460
- assert.neverCalledWith(
2461
- webexRequest,
2462
- sinon.match({
2463
- method: 'GET',
2464
- uri: `${parser.dataSets['atd-unmuted'].url}/hashtree`,
2465
- })
2466
- );
2467
- });
2468
-
2469
- it('calls POST sync directly for leafCount === 1 data sets', async () => {
2470
- const parser = createHashTreeParser();
2471
- const heartbeatIntervalMs = 5000;
2472
-
2473
- // Send heartbeat for 'self' (leafCount === 1)
2474
- const heartbeatMessage = {
2475
- dataSets: [
2476
- {
2477
- ...createDataSet('self', 1, 2100),
2478
- url: parser.dataSets.self.url,
2479
- root: parser.dataSets.self.hashTree.getRootHash(),
2480
- },
2481
- ],
2482
- visibleDataSetsUrl,
2483
- locusUrl,
2484
- heartbeatIntervalMs,
2485
- };
2486
-
2487
- parser.handleMessage(heartbeatMessage, 'self heartbeat');
2488
-
2489
- // Mock sync response for self
2490
- mockSendSyncRequestResponse(parser.dataSets.self.url, null);
2491
-
2492
- // Advance time past watchdog delay
2493
- await clock.tickAsync(heartbeatIntervalMs);
2494
-
2495
- // For leafCount === 1, performSync skips GET hashtree and goes straight to POST sync
2496
- assert.neverCalledWith(
2497
- webexRequest,
2498
- sinon.match({
2499
- method: 'GET',
2500
- uri: `${parser.dataSets.self.url}/hashtree`,
2501
- })
2502
- );
2503
- assert.calledWith(
2504
- webexRequest,
2505
- sinon.match({
2506
- method: 'POST',
2507
- uri: `${parser.dataSets.self.url}/sync`,
2508
- })
2509
- );
2510
- });
2511
-
2512
- it('sets watchdog timers for each data set in the message', async () => {
2513
- const parser = createHashTreeParser();
2514
- const heartbeatIntervalMs = 5000;
2515
-
2516
- // Send heartbeat with multiple datasets
2517
- const heartbeatMessage = {
2518
- dataSets: [
2519
- {
2520
- ...createDataSet('main', 16, 1100),
2521
- root: parser.dataSets.main.hashTree.getRootHash(),
2522
- },
2523
- {
2524
- ...createDataSet('self', 1, 2100),
2525
- url: parser.dataSets.self.url,
2526
- root: parser.dataSets.self.hashTree.getRootHash(),
2527
- },
2528
- ],
2529
- visibleDataSetsUrl,
2530
- locusUrl,
2531
- heartbeatIntervalMs,
2532
- };
2533
-
2534
- parser.handleMessage(heartbeatMessage, 'multi-dataset heartbeat');
2535
-
2536
- // Watchdog timers should be set for both datasets in the message
2537
- expect(parser.dataSets.main.heartbeatWatchdogTimer).to.not.be.undefined;
2538
- expect(parser.dataSets.self.heartbeatWatchdogTimer).to.not.be.undefined;
2539
- // But not for datasets not in the message
2540
- expect(parser.dataSets['atd-unmuted'].heartbeatWatchdogTimer).to.be.undefined;
2541
- });
2542
-
2543
- it('resets the watchdog timer for a specific data set when a new heartbeat for it is received', async () => {
2544
- const parser = createHashTreeParser();
2545
- const heartbeatIntervalMs = 5000;
2546
-
2547
- // Send first heartbeat for 'main'
2548
- const heartbeat1 = {
2549
- dataSets: [
2550
- {
2551
- ...createDataSet('main', 16, 1100),
2552
- root: parser.dataSets.main.hashTree.getRootHash(),
2553
- },
2554
- ],
2555
- visibleDataSetsUrl,
2556
- locusUrl,
2557
- heartbeatIntervalMs,
2558
- };
2559
-
2560
- parser.handleMessage(heartbeat1, 'first heartbeat');
2561
-
2562
- const firstTimer = parser.dataSets.main.heartbeatWatchdogTimer;
2563
- expect(firstTimer).to.not.be.undefined;
2564
-
2565
- // Advance time to just before the watchdog would fire
2566
- clock.tick(4000);
2567
-
2568
- // Send second heartbeat for 'main' - this should reset the watchdog
2569
- const heartbeat2 = {
2570
- dataSets: [
2571
- {
2572
- ...createDataSet('main', 16, 1101),
2573
- root: parser.dataSets.main.hashTree.getRootHash(),
2574
- },
2575
- ],
2576
- visibleDataSetsUrl,
2577
- locusUrl,
2578
- heartbeatIntervalMs,
2579
- };
2580
-
2581
- parser.handleMessage(heartbeat2, 'second heartbeat');
2582
-
2583
- const secondTimer = parser.dataSets.main.heartbeatWatchdogTimer;
2584
- expect(secondTimer).to.not.be.undefined;
2585
- expect(secondTimer).to.not.equal(firstTimer);
2586
-
2587
- // Advance another 4000ms (total 8000ms from start, but only 4000ms since last heartbeat)
2588
- // The watchdog should NOT fire yet
2589
- await clock.tickAsync(4000);
2590
-
2591
- // No sync requests should have been sent
2592
- assert.notCalled(webexRequest);
2593
- });
2594
-
2595
- it('resets the watchdog timer when a normal message (with locusStateElements) is received', async () => {
2596
- const parser = createHashTreeParser();
2597
- const heartbeatIntervalMs = 5000;
2598
-
2599
- // Send initial heartbeat to start the watchdog for 'main'
2600
- const heartbeat = {
2601
- dataSets: [
2602
- {
2603
- ...createDataSet('main', 16, 1100),
2604
- root: parser.dataSets.main.hashTree.getRootHash(),
2605
- },
2606
- ],
2607
- visibleDataSetsUrl,
2608
- locusUrl,
2609
- heartbeatIntervalMs,
2610
- };
2611
-
2612
- parser.handleMessage(heartbeat, 'initial heartbeat');
2613
-
2614
- const firstTimer = parser.dataSets.main.heartbeatWatchdogTimer;
2615
- expect(firstTimer).to.not.be.undefined;
2616
-
2617
- // Advance time partially
2618
- clock.tick(3000);
2619
-
2620
- // Stub updateItems so the normal message is processed
2621
- sinon.stub(parser.dataSets.main.hashTree, 'updateItems').returns([true]);
2622
-
2623
- // Send a normal message (with locusStateElements) for 'main' - should also reset watchdog
2624
- const normalMessage = {
2625
- dataSets: [createDataSet('main', 16, 1101)],
2626
- visibleDataSetsUrl,
2627
- locusUrl,
2628
- locusStateElements: [
2629
- {
2630
- htMeta: {
2631
- elementId: {type: 'locus' as const, id: 0, version: 201},
2632
- dataSetNames: ['main'],
2633
- },
2634
- data: {someData: 'value'},
2635
- },
2636
- ],
2637
- heartbeatIntervalMs,
2638
- };
2639
-
2640
- parser.handleMessage(normalMessage, 'normal message');
2641
-
2642
- const secondTimer = parser.dataSets.main.heartbeatWatchdogTimer;
2643
- expect(secondTimer).to.not.be.undefined;
2644
- expect(secondTimer).to.not.equal(firstTimer);
2645
- });
2646
-
2647
- it('does not set the watchdog timer when heartbeatIntervalMs is not set', async () => {
2648
- const parser = createHashTreeParser();
2649
-
2650
- // Send a heartbeat message without heartbeatIntervalMs
2651
- const heartbeatMessage = createHeartbeatMessage(
2652
- 'main',
2653
- 16,
2654
- 1100,
2655
- parser.dataSets.main.hashTree.getRootHash()
2656
- );
2657
-
2658
- parser.handleMessage(heartbeatMessage, 'heartbeat without interval');
2659
-
2660
- expect(parser.dataSets.main.heartbeatWatchdogTimer).to.be.undefined;
2661
- });
2662
-
2663
- it('stops all watchdog timers when meeting ends via sentinel message', async () => {
2664
- const parser = createHashTreeParser();
2665
- const heartbeatIntervalMs = 5000;
2666
-
2667
- // Send heartbeat for multiple datasets
2668
- const heartbeat = {
2669
- dataSets: [
2670
- {
2671
- ...createDataSet('main', 16, 1100),
2672
- root: parser.dataSets.main.hashTree.getRootHash(),
2673
- },
2674
- {
2675
- ...createDataSet('self', 1, 2100),
2676
- url: parser.dataSets.self.url,
2677
- root: parser.dataSets.self.hashTree.getRootHash(),
2678
- },
2679
- ],
2680
- visibleDataSetsUrl,
2681
- locusUrl,
2682
- heartbeatIntervalMs,
2683
- };
2684
-
2685
- parser.handleMessage(heartbeat, 'initial heartbeat');
2686
-
2687
- expect(parser.dataSets.main.heartbeatWatchdogTimer).to.not.be.undefined;
2688
- expect(parser.dataSets.self.heartbeatWatchdogTimer).to.not.be.undefined;
2689
-
2690
- // Send a sentinel END MEETING message
2691
- const sentinelMessage = createHeartbeatMessage(
2692
- 'main',
2693
- 1,
2694
- parser.dataSets.main.version + 1,
2695
- EMPTY_HASH
2696
- );
2697
-
2698
- parser.handleMessage(sentinelMessage as any, 'sentinel message');
2699
-
2700
- // All watchdog timers should have been stopped
2701
- expect(parser.dataSets.main.heartbeatWatchdogTimer).to.be.undefined;
2702
- expect(parser.dataSets.self.heartbeatWatchdogTimer).to.be.undefined;
2703
- });
2704
-
2705
- it("uses each data set's own backoff for its watchdog delay", async () => {
2706
- // Create a parser where datasets have different backoff configs
2707
- const initialLocus = {
2708
- dataSets: [
2709
- {
2710
- ...createDataSet('main', 16, 1000),
2711
- backoff: {maxMs: 500, exponent: 2},
2712
- },
2713
- {
2714
- ...createDataSet('self', 1, 2000),
2715
- url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/participant/713e9f99/datasets/self',
2716
- backoff: {maxMs: 2000, exponent: 3},
2717
- },
2718
- ],
2719
- locus: {
2720
- ...exampleInitialLocus.locus,
2721
- },
2722
- };
2723
-
2724
- const metadata = {
2725
- ...exampleMetadata,
2726
- visibleDataSets: [
2727
- {name: 'main', url: initialLocus.dataSets[0].url},
2728
- {name: 'self', url: initialLocus.dataSets[1].url},
2729
- ],
2730
- };
2731
-
2732
- const parser = createHashTreeParser(initialLocus, metadata);
2733
- const heartbeatIntervalMs = 5000;
2734
-
2735
- // Set Math.random to return 1 so that backoff = 1^exponent * maxMs = maxMs
2736
- mathRandomStub.returns(1);
2737
-
2738
- // Send heartbeat for both datasets
2739
- const heartbeat = {
2740
- dataSets: [
2741
- {
2742
- ...createDataSet('main', 16, 1100),
2743
- backoff: {maxMs: 500, exponent: 2},
2744
- root: parser.dataSets.main.hashTree.getRootHash(),
2745
- },
2746
- {
2747
- ...createDataSet('self', 1, 2100),
2748
- url: parser.dataSets.self.url,
2749
- backoff: {maxMs: 2000, exponent: 3},
2750
- root: parser.dataSets.self.hashTree.getRootHash(),
2751
- },
2752
- ],
2753
- visibleDataSetsUrl,
2754
- locusUrl,
2755
- heartbeatIntervalMs,
2756
- };
2757
-
2758
- parser.handleMessage(heartbeat, 'heartbeat');
2759
-
2760
- // 'main' watchdog delay = 5000 + 1^2 * 500 = 5500ms
2761
- // 'self' watchdog delay = 5000 + 1^3 * 2000 = 7000ms
2762
-
2763
- // Mock sync responses
2764
- mockGetHashesFromLocusResponse(
2765
- parser.dataSets.main.url,
2766
- new Array(16).fill('00000000000000000000000000000000'),
2767
- createDataSet('main', 16, 1101)
2768
- );
2769
- mockSendSyncRequestResponse(parser.dataSets.main.url, null);
2770
- mockSendSyncRequestResponse(parser.dataSets.self.url, null);
2771
-
2772
- // At 5499ms, neither watchdog should have fired
2773
- await clock.tickAsync(5499);
2774
- assert.notCalled(webexRequest);
2775
-
2776
- // At 5500ms, 'main' watchdog fires and performSync runs immediately
2777
- await clock.tickAsync(1);
2778
-
2779
- // main sync should have triggered immediately (GET hashtree + POST sync)
2780
- assert.calledWith(
2781
- webexRequest,
2782
- sinon.match({
2783
- method: 'GET',
2784
- uri: `${parser.dataSets.main.url}/hashtree`,
2785
- })
2786
- );
2787
-
2788
- webexRequest.resetHistory();
2789
-
2790
- // At 7000ms, 'self' watchdog fires and performSync runs immediately
2791
- await clock.tickAsync(1500);
2792
-
2793
- // self sync should have also triggered (POST sync only, leafCount === 1)
2794
- assert.calledWith(
2795
- webexRequest,
2796
- sinon.match({
2797
- method: 'POST',
2798
- uri: `${parser.dataSets.self.url}/sync`,
2799
- })
2800
- );
2801
- });
2802
-
2803
- it('does not set watchdog for data sets without a hash tree', async () => {
2804
- const parser = createHashTreeParser();
2805
- const heartbeatIntervalMs = 5000;
2806
-
2807
- // 'atd-active' is in the initial locus but is not visible (no hash tree)
2808
- // Send heartbeat mentioning a non-visible dataset
2809
- const heartbeatMessage = {
2810
- dataSets: [
2811
- {
2812
- ...createDataSet('main', 16, 1100),
2813
- root: parser.dataSets.main.hashTree.getRootHash(),
2814
- },
2815
- createDataSet('atd-active', 16, 4000),
2816
- ],
2817
- visibleDataSetsUrl,
2818
- locusUrl,
2819
- heartbeatIntervalMs,
2820
- };
2821
-
2822
- parser.handleMessage(heartbeatMessage, 'heartbeat with non-visible dataset');
2823
-
2824
- // Watchdog set for main (visible) but not for atd-active (no hash tree)
2825
- expect(parser.dataSets.main.heartbeatWatchdogTimer).to.not.be.undefined;
2826
- expect(parser.dataSets['atd-active']?.heartbeatWatchdogTimer).to.be.undefined;
2827
- });
2828
- });
2829
- });
2830
-
2831
- describe('#callLocusInfoUpdateCallback filtering', () => {
2832
- // Helper to setup parser with initial objects and reset callback history
2833
- function setupParserWithObjects(locusStateElements: any[]) {
2834
- const parser = createHashTreeParser();
2835
-
2836
- if (locusStateElements.length > 0) {
2837
- // Determine which datasets to include based on the objects' dataSetNames
2838
- const dataSetNames = new Set<string>();
2839
- locusStateElements.forEach((element) => {
2840
- element.htMeta?.dataSetNames?.forEach((name) => dataSetNames.add(name));
2841
- });
2842
-
2843
- const dataSets = [];
2844
- if (dataSetNames.has('main')) dataSets.push(createDataSet('main', 16, 1100));
2845
- if (dataSetNames.has('self')) dataSets.push(createDataSet('self', 1, 2100));
2846
- if (dataSetNames.has('atd-unmuted')) dataSets.push(createDataSet('atd-unmuted', 16, 3100));
2847
-
2848
- const setupMessage = {
2849
- dataSets,
2850
- visibleDataSetsUrl,
2851
- locusUrl,
2852
- locusStateElements,
2853
- };
2854
-
2855
- parser.handleMessage(setupMessage, 'setup');
2856
- }
2857
-
2858
- callback.resetHistory();
2859
- return parser;
2860
- }
2861
-
2862
- it('filters out updates when a dataset has a higher version', () => {
2863
- const parser = setupParserWithObjects([
2864
- {
2865
- htMeta: {
2866
- elementId: {type: 'locus' as const, id: 5, version: 100},
2867
- dataSetNames: ['main'],
2868
- },
2869
- data: {existingField: 'existing'},
2870
- },
2871
- ]);
2872
-
2873
- // Try to update with an older version (90)
2874
- const updateMessage = {
2875
- dataSets: [createDataSet('main', 16, 1101)],
2876
- visibleDataSetsUrl,
2877
- locusUrl,
2878
- locusStateElements: [
2879
- {
2880
- htMeta: {
2881
- elementId: {type: 'locus' as const, id: 5, version: 90},
2882
- dataSetNames: ['main'],
2883
- },
2884
- data: {someField: 'value'},
2885
- },
2886
- ],
2887
- };
2888
-
2889
- parser.handleMessage(updateMessage, 'update with older version');
2890
-
2891
- // Callback should not be called because the update was filtered out
2892
- assert.notCalled(callback);
2893
- });
2894
-
2895
- it('allows updates when version is newer than existing', () => {
2896
- const parser = setupParserWithObjects([
2897
- {
2898
- htMeta: {
2899
- elementId: {type: 'locus' as const, id: 5, version: 100},
2900
- dataSetNames: ['main'],
2901
- },
2902
- data: {existingField: 'existing'},
2903
- },
2904
- ]);
2905
-
2906
- // Try to update with a newer version (110)
2907
- const updateMessage = {
2908
- dataSets: [createDataSet('main', 16, 1101)],
2909
- visibleDataSetsUrl,
2910
- locusUrl,
2911
- locusStateElements: [
2912
- {
2913
- htMeta: {
2914
- elementId: {type: 'locus' as const, id: 5, version: 110},
2915
- dataSetNames: ['main'],
2916
- },
2917
- data: {someField: 'new value'},
2918
- },
2919
- ],
2920
- };
2921
-
2922
- parser.handleMessage(updateMessage, 'update with newer version');
2923
-
2924
- // Callback should be called with the update
2925
- assert.calledOnceWithExactly(callback, LocusInfoUpdateType.OBJECTS_UPDATED, {
2926
- updatedObjects: [
2927
- {
2928
- htMeta: {
2929
- elementId: {type: 'locus', id: 5, version: 110},
2930
- dataSetNames: ['main'],
2931
- },
2932
- data: {someField: 'new value'},
2933
- },
2934
- ],
2935
- });
2936
- });
2937
-
2938
- it('filters out removal when object still exists in any dataset', () => {
2939
- const parser = setupParserWithObjects([
2940
- {
2941
- htMeta: {
2942
- elementId: {type: 'participant' as const, id: 10, version: 50},
2943
- dataSetNames: ['main', 'atd-unmuted'],
2944
- },
2945
- data: {name: 'participant'},
2946
- },
2947
- ]);
2948
-
2949
- // Try to remove the object from main only (it still exists in atd-unmuted)
2950
- const removalMessage = {
2951
- dataSets: [createDataSet('main', 16, 1101)],
2952
- visibleDataSetsUrl,
2953
- locusUrl,
2954
- locusStateElements: [
2955
- {
2956
- htMeta: {
2957
- elementId: {type: 'participant' as const, id: 10, version: 50},
2958
- dataSetNames: ['main'],
2959
- },
2960
- data: null, // removal
2961
- },
2962
- ],
2963
- };
2964
-
2965
- parser.handleMessage(removalMessage, 'removal from one dataset');
2966
-
2967
- // Callback should not be called because object still exists in atd-unmuted
2968
- assert.notCalled(callback);
2969
- });
2970
-
2971
- it('allows removal when object does not exist in any dataset', () => {
2972
- const parser = setupParserWithObjects([]);
2973
-
2974
- // Stub updateItems to return true (simulating that the removal was "applied")
2975
- sinon.stub(parser.dataSets.main.hashTree, 'updateItems').returns([true]);
2976
-
2977
- // Try to remove an object that doesn't exist anywhere
2978
- const removalMessage = {
2979
- dataSets: [createDataSet('main', 16, 1100)],
2980
- visibleDataSetsUrl,
2981
- locusUrl,
2982
- locusStateElements: [
2983
- {
2984
- htMeta: {
2985
- elementId: {type: 'participant' as const, id: 99, version: 10},
2986
- dataSetNames: ['main'],
2987
- },
2988
- data: null, // removal
2989
- },
2990
- ],
2991
- };
2992
-
2993
- parser.handleMessage(removalMessage, 'removal of non-existent object');
2994
-
2995
- // Callback should be called with the removal
2996
- assert.calledOnceWithExactly(callback, LocusInfoUpdateType.OBJECTS_UPDATED, {
2997
- updatedObjects: [
2998
- {
2999
- htMeta: {
3000
- elementId: {type: 'participant', id: 99, version: 10},
3001
- dataSetNames: ['main'],
3002
- },
3003
- data: null,
3004
- },
3005
- ],
3006
- });
3007
- });
3008
-
3009
- it('filters out removal when object exists in another dataset with newer version', () => {
3010
- const parser = createHashTreeParser();
3011
-
3012
- // Setup: Add object to main with version 40
3013
- parser.handleMessage(
3014
- {
3015
- dataSets: [createDataSet('main', 16, 1100)],
3016
- visibleDataSetsUrl,
3017
- locusUrl,
3018
- locusStateElements: [
3019
- {
3020
- htMeta: {
3021
- elementId: {type: 'participant' as const, id: 10, version: 40},
3022
- dataSetNames: ['main'],
3023
- },
3024
- data: {name: 'participant v40'},
3025
- },
3026
- ],
3027
- },
3028
- 'setup main'
3029
- );
3030
-
3031
- // Add object to atd-unmuted with version 50
3032
- parser.handleMessage(
3033
- {
3034
- dataSets: [createDataSet('atd-unmuted', 16, 3100)],
3035
- visibleDataSetsUrl,
3036
- locusUrl,
3037
- locusStateElements: [
3038
- {
3039
- htMeta: {
3040
- elementId: {type: 'participant' as const, id: 10, version: 50},
3041
- dataSetNames: ['atd-unmuted'],
3042
- },
3043
- data: {name: 'participant v50'},
3044
- },
3045
- ],
3046
- },
3047
- 'setup atd-unmuted'
3048
- );
3049
- callback.resetHistory();
3050
-
3051
- // Try to remove with version 40 from main
3052
- const removalMessage = {
3053
- dataSets: [createDataSet('main', 16, 1101)],
3054
- visibleDataSetsUrl,
3055
- locusUrl,
3056
- locusStateElements: [
3057
- {
3058
- htMeta: {
3059
- elementId: {type: 'participant' as const, id: 10, version: 40},
3060
- dataSetNames: ['main'],
3061
- },
3062
- data: null, // removal
3063
- },
3064
- ],
3065
- };
3066
-
3067
- parser.handleMessage(removalMessage, 'removal with older version');
3068
-
3069
- // Callback should not be called because object still exists with newer version
3070
- assert.notCalled(callback);
3071
- });
3072
-
3073
- it('filters mixed updates correctly - some pass, some filtered', () => {
3074
- const parser = setupParserWithObjects([
3075
- {
3076
- htMeta: {
3077
- elementId: {type: 'participant' as const, id: 1, version: 100},
3078
- dataSetNames: ['main'],
3079
- },
3080
- data: {name: 'participant 1'},
3081
- },
3082
- {
3083
- htMeta: {
3084
- elementId: {type: 'participant' as const, id: 2, version: 50},
3085
- dataSetNames: ['atd-unmuted'],
3086
- },
3087
- data: {name: 'participant 2'},
3088
- },
3089
- ]);
3090
-
3091
- // Send mixed updates
3092
- const mixedMessage = {
3093
- dataSets: [createDataSet('main', 16, 1101)],
3094
- visibleDataSetsUrl,
3095
- locusUrl,
3096
- locusStateElements: [
3097
- {
3098
- htMeta: {
3099
- elementId: {type: 'participant' as const, id: 1, version: 110}, // newer version - should pass
3100
- dataSetNames: ['main'],
3101
- },
3102
- data: {name: 'updated'},
3103
- },
3104
- {
3105
- htMeta: {
3106
- elementId: {type: 'participant' as const, id: 1, version: 90}, // older version - should be filtered
3107
- dataSetNames: ['main'],
3108
- },
3109
- data: {name: 'old'},
3110
- },
3111
- {
3112
- htMeta: {
3113
- elementId: {type: 'participant' as const, id: 3, version: 10}, // new object - should pass
3114
- dataSetNames: ['main'],
3115
- },
3116
- data: {name: 'new'},
3117
- },
3118
- {
3119
- htMeta: {
3120
- elementId: {type: 'participant' as const, id: 2, version: 50}, // removal but exists in atd-unmuted - should be filtered
3121
- dataSetNames: ['main'],
3122
- },
3123
- data: null,
3124
- },
3125
- ],
3126
- };
3127
-
3128
- parser.handleMessage(mixedMessage, 'mixed updates');
3129
-
3130
- // Callback should be called with only the valid updates (participant 1 v110 and participant 3 v10)
3131
- assert.calledOnceWithExactly(callback, LocusInfoUpdateType.OBJECTS_UPDATED, {
3132
- updatedObjects: [
3133
- {
3134
- htMeta: {
3135
- elementId: {type: 'participant', id: 1, version: 110},
3136
- dataSetNames: ['main'],
3137
- },
3138
- data: {name: 'updated'},
3139
- },
3140
- {
3141
- htMeta: {
3142
- elementId: {type: 'participant', id: 3, version: 10},
3143
- dataSetNames: ['main'],
3144
- },
3145
- data: {name: 'new'},
3146
- },
3147
- ],
3148
- });
3149
- });
3150
-
3151
- it('does not call callback when all updates are filtered out', () => {
3152
- const parser = setupParserWithObjects([
3153
- {
3154
- htMeta: {
3155
- elementId: {type: 'locus' as const, id: 5, version: 100},
3156
- dataSetNames: ['main'],
3157
- },
3158
- data: {existingField: 'existing'},
3159
- },
3160
- ]);
3161
-
3162
- // Try to update with older versions (all should be filtered)
3163
- const updateMessage = {
3164
- dataSets: [createDataSet('main', 16, 1101)],
3165
- visibleDataSetsUrl,
3166
- locusUrl,
3167
- locusStateElements: [
3168
- {
3169
- htMeta: {
3170
- elementId: {type: 'locus' as const, id: 5, version: 80},
3171
- dataSetNames: ['main'],
3172
- },
3173
- data: {someField: 'value'},
3174
- },
3175
- {
3176
- htMeta: {
3177
- elementId: {type: 'locus' as const, id: 5, version: 90},
3178
- dataSetNames: ['main'],
3179
- },
3180
- data: {someField: 'another value'},
3181
- },
3182
- ],
3183
- };
3184
-
3185
- parser.handleMessage(updateMessage, 'all filtered updates');
3186
-
3187
- // Callback should not be called at all
3188
- assert.notCalled(callback);
3189
- });
3190
-
3191
- it('checks all visible datasets when filtering', () => {
3192
- const parser = createHashTreeParser();
3193
-
3194
- // Setup: Add same object to multiple datasets with different versions
3195
- parser.handleMessage(
3196
- {
3197
- dataSets: [createDataSet('main', 16, 1100)],
3198
- visibleDataSetsUrl,
3199
- locusUrl,
3200
- locusStateElements: [
3201
- {
3202
- htMeta: {
3203
- elementId: {type: 'participant' as const, id: 10, version: 100},
3204
- dataSetNames: ['main'],
3205
- },
3206
- data: {name: 'v100'},
3207
- },
3208
- ],
3209
- },
3210
- 'setup main'
3211
- );
3212
-
3213
- parser.handleMessage(
3214
- {
3215
- dataSets: [createDataSet('self', 1, 2100)],
3216
- visibleDataSetsUrl,
3217
- locusUrl,
3218
- locusStateElements: [
3219
- {
3220
- htMeta: {
3221
- elementId: {type: 'participant' as const, id: 10, version: 120}, // highest
3222
- dataSetNames: ['self'],
3223
- },
3224
- data: {name: 'v120'},
3225
- },
3226
- ],
3227
- },
3228
- 'setup self'
3229
- );
3230
-
3231
- parser.handleMessage(
3232
- {
3233
- dataSets: [createDataSet('atd-unmuted', 16, 3100)],
3234
- visibleDataSetsUrl,
3235
- locusUrl,
3236
- locusStateElements: [
3237
- {
3238
- htMeta: {
3239
- elementId: {type: 'participant' as const, id: 10, version: 110},
3240
- dataSetNames: ['atd-unmuted'],
3241
- },
3242
- data: {name: 'v110'},
3243
- },
3244
- ],
3245
- },
3246
- 'setup atd-unmuted'
3247
- );
3248
- callback.resetHistory();
3249
-
3250
- // Try to update with version 115 (newer than main and atd-unmuted, but older than self)
3251
- const updateMessage = {
3252
- dataSets: [createDataSet('main', 16, 1101)],
3253
- visibleDataSetsUrl,
3254
- locusUrl,
3255
- locusStateElements: [
3256
- {
3257
- htMeta: {
3258
- elementId: {type: 'participant' as const, id: 10, version: 115},
3259
- dataSetNames: ['main'],
3260
- },
3261
- data: {name: 'update'},
3262
- },
3263
- ],
3264
- };
3265
-
3266
- parser.handleMessage(updateMessage, 'update with v115');
3267
-
3268
- // Should be filtered out because self dataset has version 120
3269
- assert.notCalled(callback);
3270
- });
3271
-
3272
- it('does not call callback for empty locusStateElements', () => {
3273
- const parser = setupParserWithObjects([]);
3274
-
3275
- const emptyMessage = {
3276
- dataSets: [createDataSet('main', 16, 1100)],
3277
- visibleDataSetsUrl,
3278
- locusUrl,
3279
- locusStateElements: [],
3280
- };
3281
-
3282
- parser.handleMessage(emptyMessage, 'empty elements');
3283
-
3284
- assert.notCalled(callback);
3285
- });
3286
-
3287
- it('always calls callback for MEETING_ENDED regardless of filtering', () => {
3288
- const parser = setupParserWithObjects([
3289
- {
3290
- htMeta: {
3291
- elementId: {type: 'locus' as const, id: 0, version: 100},
3292
- dataSetNames: ['main'],
3293
- },
3294
- data: {info: 'data'},
3295
- },
3296
- ]);
3297
-
3298
- // Send a sentinel END MEETING message
3299
- const sentinelMessage = createHeartbeatMessage(
3300
- 'main',
3301
- 1,
3302
- parser.dataSets.main.version + 1,
3303
- EMPTY_HASH
3304
- );
3305
-
3306
- parser.handleMessage(sentinelMessage as any, 'sentinel message');
3307
-
3308
- // Callback should be called with MEETING_ENDED
3309
- assert.calledOnceWithExactly(callback, LocusInfoUpdateType.MEETING_ENDED, {
3310
- updatedObjects: undefined,
3311
- });
3312
- });
3313
- });
3314
-
3315
- describe('#state', () => {
3316
- it('should be initialized to active', () => {
3317
- const parser = createHashTreeParser();
3318
-
3319
- expect(parser.state).to.equal('active');
3320
- });
3321
- });
3322
-
3323
- describe('#stop', () => {
3324
- it('should set state to stopped', () => {
3325
- const parser = createHashTreeParser();
3326
-
3327
- parser.stop();
3328
-
3329
- expect(parser.state).to.equal('stopped');
3330
- });
3331
-
3332
- it('should clear all hash trees', () => {
3333
- const parser = createHashTreeParser();
3334
-
3335
- expect(parser.dataSets.main.hashTree).to.be.instanceOf(HashTree);
3336
- expect(parser.dataSets.self.hashTree).to.be.instanceOf(HashTree);
3337
-
3338
- parser.stop();
3339
-
3340
- expect(parser.dataSets.main.hashTree).to.be.undefined;
3341
- expect(parser.dataSets.self.hashTree).to.be.undefined;
3342
- expect(parser.dataSets['atd-unmuted'].hashTree).to.be.undefined;
3343
- });
3344
-
3345
- it('should clear visibleDataSets', () => {
3346
- const parser = createHashTreeParser();
3347
-
3348
- expect(parser.visibleDataSets).to.have.length.greaterThan(0);
3349
-
3350
- parser.stop();
3351
-
3352
- expect(parser.visibleDataSets).to.deep.equal([]);
3353
- });
3354
-
3355
- it('should stop all timers', () => {
3356
- const parser = createHashTreeParser();
3357
-
3358
- // manually set timers on data sets
3359
- parser.dataSets.main.timer = setTimeout(() => {}, 10000);
3360
- parser.dataSets.main.heartbeatWatchdogTimer = setTimeout(() => {}, 10000);
3361
-
3362
- parser.stop();
3363
-
3364
- expect(parser.dataSets.main.timer).to.be.undefined;
3365
- expect(parser.dataSets.main.heartbeatWatchdogTimer).to.be.undefined;
3366
- });
3367
-
3368
- it('should not call locusInfoUpdateCallback when async initialization of a new visible dataset completes after stop()', async () => {
3369
- const parser = createHashTreeParser();
3370
-
3371
- // Stub updateItems on self hash tree to return true so the metadata update is applied
3372
- sinon.stub(parser.dataSets.self.hashTree, 'updateItems').returns([true]);
3373
-
3374
- // Send a message with Metadata that adds a new visible dataset requiring async initialization
3375
- // (the new dataset is NOT in parser.dataSets, so it will go through queueInitForNewVisibleDataSets)
3376
- const message = {
3377
- dataSets: [createDataSet('self', 1, 2100)],
3378
- visibleDataSetsUrl,
3379
- locusUrl,
3380
- locusStateElements: [
3381
- {
3382
- htMeta: {
3383
- elementId: {
3384
- type: 'metadata' as const,
3385
- id: 5,
3386
- version: 51,
3387
- },
3388
- dataSetNames: ['self'],
3389
- },
3390
- data: {
3391
- visibleDataSets: [
3392
- {
3393
- name: 'main',
3394
- url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/datasets/main',
3395
- },
3396
- {
3397
- name: 'self',
3398
- url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/participant/713e9f99/datasets/self',
3399
- },
3400
- {
3401
- name: 'atd-unmuted',
3402
- url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/datasets/atd-unmuted',
3403
- },
3404
- {
3405
- name: 'new-dataset',
3406
- url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/datasets/new-dataset',
3407
- },
3408
- ],
3409
- },
3410
- },
3411
- ],
3412
- };
3413
-
3414
- // Mock the async initialization - getAllVisibleDataSetsFromLocus and sync request
3415
- const newDataSet = createDataSet('new-dataset', 4, 5000);
3416
- mockGetAllDataSetsMetadata(webexRequest, visibleDataSetsUrl, [newDataSet]);
3417
- mockSyncRequest(webexRequest, newDataSet.url, {
3418
- dataSets: [newDataSet],
3419
- visibleDataSetsUrl,
3420
- locusUrl,
3421
- locusStateElements: [
3422
- {
3423
- htMeta: {
3424
- elementId: {type: 'participant' as const, id: 20, version: 100},
3425
- dataSetNames: ['new-dataset'],
3426
- },
3427
- data: {person: {name: 'some participant'}},
3428
- },
3429
- ],
3430
- });
3431
-
3432
- // handleMessage triggers queueInitForNewVisibleDataSets (via queueMicrotask)
3433
- parser.handleMessage(message, 'add new dataset then stop');
3434
-
3435
- // callback is called once synchronously by handleMessage for the metadata update
3436
- callback.resetHistory();
3437
-
3438
- // Stop the parser before the async initialization completes
3439
- parser.stop();
3440
-
3441
- // Let the queued microtask and async initialization complete
3442
- await clock.tickAsync(0);
3443
-
3444
- // The callback should NOT have been called again after stop()
3445
- assert.notCalled(callback);
3446
-
3447
- // parseMessage should not have processed the sync response data,
3448
- // so no hash tree should exist for new-dataset (stop() clears all hash trees)
3449
- assert.isUndefined(parser.dataSets['new-dataset']?.hashTree);
3450
- });
3451
-
3452
- it('should not call locusInfoUpdateCallback when initializeFromMessage completes after stop()', async () => {
3453
- const minimalInitialLocus = {
3454
- dataSets: [],
3455
- locus: null,
3456
- };
3457
- const parser = createHashTreeParser(minimalInitialLocus, null);
3458
-
3459
- const mainDataSet = createDataSet('main', 16, 1100);
3460
-
3461
- // Use a deferred promise so we can control when getAllVisibleDataSetsFromLocus resolves
3462
- let resolveGetDataSets;
3463
- webexRequest
3464
- .withArgs(
3465
- sinon.match({
3466
- method: 'GET',
3467
- uri: visibleDataSetsUrl,
3468
- })
3469
- )
3470
- .returns(
3471
- new Promise((resolve) => {
3472
- resolveGetDataSets = resolve;
3473
- })
3474
- );
3475
-
3476
- mockSyncRequest(webexRequest, mainDataSet.url, {
3477
- dataSets: [mainDataSet],
3478
- visibleDataSetsUrl,
3479
- locusUrl,
3480
- locusStateElements: [
3481
- {
3482
- htMeta: {
3483
- elementId: {type: 'locus' as const, id: 1, version: 210},
3484
- dataSetNames: ['main'],
3485
- },
3486
- data: {info: {id: 'some-locus-info'}},
3487
- },
3488
- ],
3489
- });
3490
-
3491
- // Start initializeFromMessage but don't await it
3492
- const initPromise = parser.initializeFromMessage({
3493
- dataSets: [],
3494
- visibleDataSetsUrl,
3495
- locusUrl,
3496
- });
3497
-
3498
- // Stop the parser before the GET response arrives
3499
- parser.stop();
3500
-
3501
- // Now resolve the pending GET request
3502
- resolveGetDataSets({body: {dataSets: [mainDataSet]}});
3503
-
3504
- // Wait for the initializeFromMessage to finish
3505
- await initPromise;
3506
-
3507
- // The callback should NOT have been called because the parser was stopped
3508
- assert.notCalled(callback);
3509
-
3510
- // Even though initializeDataSets may create a hash tree entry, parseMessage
3511
- // should have returned [] without processing the sync response objects.
3512
- // After stop(), hash trees are cleared, so verify that main has no hash tree.
3513
- assert.isUndefined(parser.dataSets.main?.hashTree);
3514
- });
3515
- });
3516
-
3517
- describe('#resume', () => {
3518
- const createResumeMessage = (visibleDataSets?, dataSets?) => ({
3519
- locusUrl,
3520
- visibleDataSetsUrl,
3521
- dataSets: dataSets || [
3522
- createDataSet('main', 16, 2000),
3523
- createDataSet('self', 1, 3000),
3524
- ],
3525
- locusStateElements: [
3526
- {
3527
- htMeta: {elementId: {type: 'metadata' as const, id: 5, version: 60}, dataSetNames: ['self']},
3528
- data: {
3529
- visibleDataSets: visibleDataSets || [
3530
- {name: 'main', url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/datasets/main'},
3531
- {name: 'self', url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/participant/713e9f99/datasets/self'},
3532
- ],
3533
- },
3534
- },
3535
- ],
3536
- });
3537
-
3538
- it('should set state back to active', () => {
3539
- const parser = createHashTreeParser();
3540
- parser.stop();
3541
-
3542
- expect(parser.state).to.equal('stopped');
3543
-
3544
- parser.resume(createResumeMessage());
3545
-
3546
- expect(parser.state).to.equal('active');
3547
- });
3548
-
3549
- it('should not resume if message is missing metadata with visibleDataSets', () => {
3550
- const parser = createHashTreeParser();
3551
- parser.stop();
3552
-
3553
- parser.resume({
3554
- locusUrl,
3555
- visibleDataSetsUrl,
3556
- dataSets: [createDataSet('main', 16, 2000)],
3557
- locusStateElements: [],
3558
- });
3559
-
3560
- expect(parser.state).to.equal('stopped');
3561
- });
3562
-
3563
- it('should re-initialize dataSets from the message', () => {
3564
- const parser = createHashTreeParser();
3565
- parser.stop();
3566
-
3567
- const newDataSets = [
3568
- createDataSet('main', 8, 5000),
3569
- createDataSet('self', 2, 6000),
3570
- ];
3571
-
3572
- parser.resume(createResumeMessage(undefined, newDataSets));
3573
-
3574
- expect(Object.keys(parser.dataSets)).to.have.lengthOf(2);
3575
- expect(parser.dataSets.main.leafCount).to.equal(8);
3576
- expect(parser.dataSets.main.version).to.equal(5000);
3577
- expect(parser.dataSets.self.leafCount).to.equal(2);
3578
- });
3579
-
3580
- it('should create hash trees only for visible data sets', () => {
3581
- const parser = createHashTreeParser();
3582
- parser.stop();
3583
-
3584
- const dataSets = [
3585
- createDataSet('main', 16, 2000),
3586
- createDataSet('self', 1, 3000),
3587
- createDataSet('atd-unmuted', 16, 4000),
3588
- ];
3589
- const visibleDataSets = [
3590
- {name: 'main', url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/datasets/main'},
3591
- {name: 'self', url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/participant/713e9f99/datasets/self'},
3592
- ];
3593
-
3594
- parser.resume(createResumeMessage(visibleDataSets, dataSets));
3595
-
3596
- expect(parser.dataSets.main.hashTree).to.be.instanceOf(HashTree);
3597
- expect(parser.dataSets.self.hashTree).to.be.instanceOf(HashTree);
3598
- expect(parser.dataSets['atd-unmuted'].hashTree).to.be.undefined;
3599
- });
3600
-
3601
- it('should call handleMessage with the resume message', () => {
3602
- const parser = createHashTreeParser();
3603
- parser.stop();
3604
-
3605
- const handleMessageStub = sinon.stub(parser, 'handleMessage');
3606
-
3607
- const message = createResumeMessage();
3608
- parser.resume(message);
3609
-
3610
- assert.calledOnceWithExactly(handleMessageStub, message, 'on resume');
3611
- });
3612
-
3613
- it('should set visibleDataSets from message metadata filtered by excludedDataSets', () => {
3614
- const parser = createHashTreeParser(exampleInitialLocus, exampleMetadata, ['atd-unmuted']);
3615
- parser.stop();
3616
-
3617
- const dataSets = [
3618
- createDataSet('main', 16, 2000),
3619
- createDataSet('self', 1, 3000),
3620
- createDataSet('atd-unmuted', 16, 4000),
3621
- ];
3622
- const visibleDataSets = [
3623
- {name: 'main', url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/datasets/main'},
3624
- {name: 'self', url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/participant/713e9f99/datasets/self'},
3625
- {name: 'atd-unmuted', url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/datasets/atd-unmuted'},
3626
- ];
3627
-
3628
- parser.resume(createResumeMessage(visibleDataSets, dataSets));
3629
-
3630
- expect(parser.visibleDataSets.some((vds) => vds.name === 'atd-unmuted')).to.be.false;
3631
- expect(parser.visibleDataSets.some((vds) => vds.name === 'main')).to.be.true;
3632
- expect(parser.visibleDataSets.some((vds) => vds.name === 'self')).to.be.true;
3633
- });
3634
- });
3635
-
3636
- describe('#handleLocusUpdate when stopped', () => {
3637
- it('should return early without processing when parser is stopped', () => {
3638
- const parser = createHashTreeParser();
3639
- parser.stop();
3640
-
3641
- parser.handleLocusUpdate({
3642
- dataSets: [createDataSet('main', 16, 2000)],
3643
- locus: {participants: []},
3644
- });
3645
-
3646
- assert.notCalled(callback);
3647
- });
3648
- });
3649
-
3650
- describe('#handleMessage when stopped', () => {
3651
- it('should return early without processing when parser is stopped', () => {
3652
- const parser = createHashTreeParser();
3653
- parser.stop();
3654
-
3655
- parser.handleMessage({
3656
- dataSets: [createDataSet('main', 16, 2000)],
3657
- visibleDataSetsUrl,
3658
- locusUrl,
3659
- locusStateElements: [
3660
- {
3661
- htMeta: {elementId: {type: 'self' as const, id: 4, version: 200}, dataSetNames: ['self']},
3662
- data: {id: 'new-self'},
3663
- },
3664
- ],
3665
- });
3666
-
3667
- assert.notCalled(callback);
3668
- });
3669
- });
3670
- });