@fluidframework/azure-end-to-end-tests 2.53.1 → 2.61.0-355054

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 (44) hide show
  1. package/.eslintrc.cjs +27 -0
  2. package/CHANGELOG.md +4 -0
  3. package/lib/test/AzureClientFactory.js +2 -2
  4. package/lib/test/AzureClientFactory.js.map +1 -1
  5. package/lib/test/AzureTokenFactory.js +2 -2
  6. package/lib/test/AzureTokenFactory.js.map +1 -1
  7. package/lib/test/TestDataObject.js +4 -2
  8. package/lib/test/TestDataObject.js.map +1 -1
  9. package/lib/test/audience.spec.js +1 -1
  10. package/lib/test/audience.spec.js.map +1 -1
  11. package/lib/test/containerCreate.spec.js +6 -4
  12. package/lib/test/containerCreate.spec.js.map +1 -1
  13. package/lib/test/ddsTests.spec.js +1 -1
  14. package/lib/test/ddsTests.spec.js.map +1 -1
  15. package/lib/test/multiprocess/childClient.tool.js +454 -0
  16. package/lib/test/multiprocess/childClient.tool.js.map +1 -0
  17. package/lib/test/multiprocess/messageTypes.js.map +1 -1
  18. package/lib/test/multiprocess/orchestratorUtils.js +381 -0
  19. package/lib/test/multiprocess/orchestratorUtils.js.map +1 -0
  20. package/lib/test/multiprocess/presenceTest.spec.js +306 -191
  21. package/lib/test/multiprocess/presenceTest.spec.js.map +1 -1
  22. package/lib/test/tree.spec.js +3 -2
  23. package/lib/test/tree.spec.js.map +1 -1
  24. package/lib/test/utils.js.map +1 -1
  25. package/lib/test/viewContainerVersion.spec.js +1 -1
  26. package/lib/test/viewContainerVersion.spec.js.map +1 -1
  27. package/package.json +29 -29
  28. package/src/test/.mocharc.cjs +0 -1
  29. package/src/test/AzureClientFactory.ts +6 -8
  30. package/src/test/AzureTokenFactory.ts +3 -2
  31. package/src/test/TestDataObject.ts +6 -5
  32. package/src/test/audience.spec.ts +1 -1
  33. package/src/test/containerCreate.spec.ts +6 -5
  34. package/src/test/ddsTests.spec.ts +1 -1
  35. package/src/test/multiprocess/childClient.tool.ts +575 -0
  36. package/src/test/multiprocess/messageTypes.ts +138 -4
  37. package/src/test/multiprocess/orchestratorUtils.ts +573 -0
  38. package/src/test/multiprocess/presenceTest.spec.ts +454 -270
  39. package/src/test/tree.spec.ts +4 -10
  40. package/src/test/utils.ts +1 -1
  41. package/src/test/viewContainerVersion.spec.ts +1 -1
  42. package/lib/test/multiprocess/childClient.js +0 -169
  43. package/lib/test/multiprocess/childClient.js.map +0 -1
  44. package/src/test/multiprocess/childClient.ts +0 -227
@@ -0,0 +1,575 @@
1
+ /*!
2
+ * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
3
+ * Licensed under the MIT License.
4
+ */
5
+
6
+ import { strict as assert } from "node:assert";
7
+
8
+ import {
9
+ AzureClient,
10
+ type AzureContainerServices,
11
+ type AzureLocalConnectionConfig,
12
+ type AzureRemoteConnectionConfig,
13
+ type ITelemetryBaseEvent,
14
+ } from "@fluidframework/azure-client";
15
+ import { AttachState } from "@fluidframework/container-definitions";
16
+ import { ConnectionState } from "@fluidframework/container-loader";
17
+ import { LogLevel } from "@fluidframework/core-interfaces";
18
+ import type { ScopeType } from "@fluidframework/driver-definitions/legacy";
19
+ import type { ContainerSchema, IFluidContainer } from "@fluidframework/fluid-static";
20
+ import {
21
+ getPresence,
22
+ type Attendee,
23
+ type Presence,
24
+ StateFactory,
25
+ type LatestRaw,
26
+ type LatestMapRaw,
27
+ type StatesWorkspace,
28
+ } from "@fluidframework/presence/beta";
29
+ import { InsecureTokenProvider } from "@fluidframework/test-runtime-utils/internal";
30
+ import { timeoutPromise } from "@fluidframework/test-utils/internal";
31
+
32
+ import { createAzureTokenProvider } from "../AzureTokenFactory.js";
33
+ import { TestDataObject } from "../TestDataObject.js";
34
+
35
+ import type { MessageFromChild, MessageToChild, UserIdAndName } from "./messageTypes.js";
36
+
37
+ type MessageFromParent = MessageToChild;
38
+ type MessageToParent = Required<MessageFromChild>;
39
+ const connectTimeoutMs = 10_000;
40
+ // Identifier given to child process
41
+ const process_id = process.argv[2];
42
+ const verbosity = process.argv[3] ?? "";
43
+
44
+ const useAzure = process.env.FLUID_CLIENT === "azure";
45
+ const tenantId = useAzure
46
+ ? (process.env.azure__fluid__relay__service__tenantId as string)
47
+ : "frs-client-tenant";
48
+ const endPoint = process.env.azure__fluid__relay__service__endpoint as string;
49
+ if (useAzure && endPoint === undefined) {
50
+ throw new Error("Azure Fluid Relay service endpoint is missing");
51
+ }
52
+
53
+ function selectiveVerboseLog(event: ITelemetryBaseEvent, logLevel?: LogLevel): void {
54
+ if (event.eventName.includes(":Signal") || event.eventName.includes(":Join")) {
55
+ console.log(`[${process_id}] [${logLevel ?? LogLevel.default}]`, {
56
+ eventName: event.eventName,
57
+ details: event.details,
58
+ containerConnectionState: event.containerConnectionState,
59
+ });
60
+ } else if (
61
+ event.eventName.includes(":Container:") ||
62
+ event.eventName.includes(":Presence:")
63
+ ) {
64
+ console.log(`[${process_id}] [${logLevel ?? LogLevel.default}]`, {
65
+ eventName: event.eventName,
66
+ containerConnectionState: event.containerConnectionState,
67
+ });
68
+ }
69
+ }
70
+
71
+ /**
72
+ * Get or create a Fluid container with Presence in initialObjects.
73
+ */
74
+ const getOrCreatePresenceContainer = async (
75
+ id: string | undefined,
76
+ user: UserIdAndName,
77
+ scopes?: ScopeType[],
78
+ createScopes?: ScopeType[],
79
+ ): Promise<{
80
+ container: IFluidContainer;
81
+ presence: Presence;
82
+ services: AzureContainerServices;
83
+ client: AzureClient;
84
+ containerId: string;
85
+ }> => {
86
+ let container: IFluidContainer;
87
+ let containerId: string;
88
+ const connectionProps: AzureRemoteConnectionConfig | AzureLocalConnectionConfig = useAzure
89
+ ? {
90
+ tenantId,
91
+ tokenProvider: createAzureTokenProvider(
92
+ user.id ?? "foo",
93
+ user.name ?? "bar",
94
+ scopes,
95
+ createScopes,
96
+ ),
97
+ endpoint: endPoint,
98
+ type: "remote",
99
+ }
100
+ : {
101
+ tokenProvider: new InsecureTokenProvider("fooBar", user, scopes, createScopes),
102
+ endpoint: "http://localhost:7071",
103
+ type: "local",
104
+ };
105
+ const client = new AzureClient({
106
+ connection: connectionProps,
107
+ logger: {
108
+ send: verbosity.includes("telem") ? selectiveVerboseLog : () => {},
109
+ },
110
+ });
111
+ const schema: ContainerSchema = {
112
+ initialObjects: {
113
+ // A DataObject is added as otherwise fluid-static complains "Container cannot be initialized without any DataTypes"
114
+ _unused: TestDataObject,
115
+ },
116
+ };
117
+ let services: AzureContainerServices;
118
+ if (id === undefined) {
119
+ ({ container, services } = await client.createContainer(schema, "2"));
120
+ containerId = await container.attach();
121
+ } else {
122
+ containerId = id;
123
+ ({ container, services } = await client.getContainer(containerId, schema, "2"));
124
+ }
125
+ // wait for 'ConnectionState.Connected' so we return with client connected to container
126
+ if (container.connectionState !== ConnectionState.Connected) {
127
+ await timeoutPromise((resolve) => container.once("connected", () => resolve()), {
128
+ durationMs: connectTimeoutMs,
129
+ errorMsg: "container connect() timeout",
130
+ });
131
+ }
132
+ assert.strictEqual(
133
+ container.attachState,
134
+ AttachState.Attached,
135
+ "Container is not attached after attach is called",
136
+ );
137
+
138
+ const presence = getPresence(container);
139
+ return {
140
+ client,
141
+ container,
142
+ presence,
143
+ services,
144
+ containerId,
145
+ };
146
+ };
147
+ function createSendFunction(): (msg: MessageToParent) => void {
148
+ if (process.send) {
149
+ const sendFn = process.send.bind(process);
150
+ if (verbosity.includes("msgs")) {
151
+ return (msg: MessageToParent) => {
152
+ console.log(`[${process_id}] Sending`, msg);
153
+ sendFn(msg);
154
+ };
155
+ }
156
+ return sendFn;
157
+ }
158
+ throw new Error("process.send is not defined");
159
+ }
160
+
161
+ const send = createSendFunction();
162
+
163
+ function sendAttendeeConnected(attendee: Attendee): void {
164
+ send({
165
+ event: "attendeeConnected",
166
+ attendeeId: attendee.attendeeId,
167
+ });
168
+ }
169
+ function sendAttendeeDisconnected(attendee: Attendee): void {
170
+ send({
171
+ event: "attendeeDisconnected",
172
+ attendeeId: attendee.attendeeId,
173
+ });
174
+ }
175
+
176
+ function isConnected(container: IFluidContainer | undefined): boolean {
177
+ return container !== undefined && container.connectionState === ConnectionState.Connected;
178
+ }
179
+
180
+ function isStringOrNumberRecord(value: unknown): value is Record<string, string | number> {
181
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
182
+ return false;
183
+ }
184
+
185
+ const stringKeys = Object.keys(value);
186
+ const allKeys = Reflect.ownKeys(value);
187
+
188
+ if (stringKeys.length !== allKeys.length) {
189
+ // If there are non-string/symbol keys, return false
190
+ return false;
191
+ }
192
+ for (const key of stringKeys) {
193
+ if (!(typeof value[key] === "string" || typeof value[key] === "number")) {
194
+ return false;
195
+ }
196
+ }
197
+ return true;
198
+ }
199
+
200
+ // NOTE:
201
+ // - This schema intentionally uses optional keys (latest?, latestMap?) so tests can register
202
+ // states conditionally at runtime.
203
+ // - Optional keys are not explicitly supported in StatesWorkspace typing today, which means
204
+ // workspace.states.<key> is typed as any. As a result, usages below require casts
205
+ // (e.g., to LatestRaw / LatestMapRaw) to recover concrete types.
206
+ // - Track adding proper optional-key support to Presence state workspace typing here:
207
+ // Work item: AB#47518
208
+ // - Fallout: Until the above is addressed, keep the casts in place and document new usages accordingly.
209
+ // eslint-disable-next-line @typescript-eslint/consistent-type-definitions
210
+ type WorkspaceSchema = {
211
+ latest?: ReturnType<typeof StateFactory.latest<{ value: string }>>;
212
+ latestMap?: ReturnType<
213
+ typeof StateFactory.latestMap<{ value: Record<string, string | number> }, string>
214
+ >;
215
+ };
216
+ const WorkspaceSchema: WorkspaceSchema = {};
217
+
218
+ class MessageHandler {
219
+ public presence: Presence | undefined;
220
+ public container: IFluidContainer | undefined;
221
+ public containerId: string | undefined;
222
+ private readonly workspaces = new Map<string, StatesWorkspace<WorkspaceSchema>>();
223
+
224
+ private registerWorkspace(
225
+ workspaceId: string,
226
+ options: { latest?: boolean; latestMap?: boolean },
227
+ ): void {
228
+ if (!this.presence) {
229
+ send({ event: "error", error: `${process_id} is not connected to presence` });
230
+ return;
231
+ }
232
+ const { latest, latestMap } = options;
233
+ const workspace: StatesWorkspace<WorkspaceSchema> = this.presence.states.getWorkspace(
234
+ `test:${workspaceId}`,
235
+ WorkspaceSchema,
236
+ );
237
+
238
+ if (latest && !workspace.states.latest) {
239
+ workspace.add(
240
+ "latest",
241
+ StateFactory.latest<{ value: string }>({ local: { value: "initial" } }),
242
+ );
243
+ // Cast required due to optional keys in WorkspaceSchema
244
+ // TODO: AB#47518
245
+ const latestState = workspace.states.latest as LatestRaw<{ value: string }>;
246
+ latestState.events.on("remoteUpdated", (update) => {
247
+ send({
248
+ event: "latestValueUpdated",
249
+ workspaceId,
250
+ attendeeId: update.attendee.attendeeId,
251
+ value: update.value.value,
252
+ });
253
+ });
254
+ for (const remote of latestState.getRemotes()) {
255
+ send({
256
+ event: "latestValueUpdated",
257
+ workspaceId,
258
+ attendeeId: remote.attendee.attendeeId,
259
+ value: remote.value.value,
260
+ });
261
+ }
262
+ }
263
+
264
+ if (latestMap && !workspace.states.latestMap) {
265
+ workspace.add(
266
+ "latestMap",
267
+ StateFactory.latestMap<{ value: Record<string, string | number> }, string>({
268
+ local: {},
269
+ }),
270
+ );
271
+ // Cast required due to optional keys in WorkspaceSchema
272
+ // TODO: AB#47518
273
+ const latestMapState = workspace.states.latestMap as LatestMapRaw<
274
+ { value: Record<string, string | number> },
275
+ string
276
+ >;
277
+ latestMapState.events.on("remoteUpdated", (update) => {
278
+ for (const [key, valueWithMetadata] of update.items) {
279
+ send({
280
+ event: "latestMapValueUpdated",
281
+ workspaceId,
282
+ attendeeId: update.attendee.attendeeId,
283
+ key: String(key),
284
+ value: valueWithMetadata.value.value,
285
+ });
286
+ }
287
+ });
288
+ for (const remote of latestMapState.getRemotes()) {
289
+ for (const [key, valueWithMetadata] of remote.items) {
290
+ send({
291
+ event: "latestMapValueUpdated",
292
+ workspaceId,
293
+ attendeeId: remote.attendee.attendeeId,
294
+ key: String(key),
295
+ value: valueWithMetadata.value.value,
296
+ });
297
+ }
298
+ }
299
+ }
300
+
301
+ this.workspaces.set(workspaceId, workspace);
302
+ send({
303
+ event: "workspaceRegistered",
304
+ workspaceId,
305
+ latest: latest ?? false,
306
+ latestMap: latestMap ?? false,
307
+ });
308
+ }
309
+
310
+ public async onMessage(msg: MessageFromParent): Promise<void> {
311
+ if (verbosity.includes("msgs")) {
312
+ console.log(`[${process_id}] Received`, msg);
313
+ }
314
+ switch (msg.command) {
315
+ case "ping": {
316
+ this.handlePing();
317
+ break;
318
+ }
319
+ case "connect": {
320
+ await this.handleConnect(msg);
321
+ break;
322
+ }
323
+ case "disconnectSelf": {
324
+ this.handleDisconnectSelf();
325
+ break;
326
+ }
327
+ case "setLatestValue": {
328
+ this.handleSetLatestValue(msg);
329
+ break;
330
+ }
331
+ case "setLatestMapValue": {
332
+ this.handleSetLatestMapValue(msg);
333
+ break;
334
+ }
335
+ case "getLatestValue": {
336
+ this.handleGetLatestValue(msg);
337
+ break;
338
+ }
339
+ case "getLatestMapValue": {
340
+ this.handleGetLatestMapValue(msg);
341
+ break;
342
+ }
343
+ case "registerWorkspace": {
344
+ this.registerWorkspace(msg.workspaceId, {
345
+ latest: msg.latest,
346
+ latestMap: msg.latestMap,
347
+ });
348
+ break;
349
+ }
350
+ default: {
351
+ console.error(`${process_id}: Unknown command`);
352
+ send({ event: "error", error: `${process_id} Unknown command` });
353
+ }
354
+ }
355
+ }
356
+
357
+ private handlePing(): void {
358
+ send({ event: "ack" });
359
+ }
360
+
361
+ private async handleConnect(
362
+ msg: Extract<MessageFromParent, { command: "connect" }>,
363
+ ): Promise<void> {
364
+ if (!msg.user) {
365
+ send({ event: "error", error: `${process_id}: No azure user information given` });
366
+ return;
367
+ }
368
+ if (isConnected(this.container)) {
369
+ send({ event: "error", error: `${process_id}: Already connected to container` });
370
+ return;
371
+ }
372
+ const { container, presence, containerId } = await getOrCreatePresenceContainer(
373
+ msg.containerId,
374
+ msg.user,
375
+ msg.scopes,
376
+ msg.createScopes,
377
+ );
378
+ this.container = container;
379
+ this.presence = presence;
380
+ this.containerId = containerId;
381
+
382
+ // Acknowledge connection before sending current attendee information
383
+ send({
384
+ event: "connected",
385
+ containerId,
386
+ attendeeId: presence.attendees.getMyself().attendeeId,
387
+ });
388
+
389
+ // Send existing attendees excluding self to parent/orchestrator
390
+ const self = presence.attendees.getMyself();
391
+ for (const attendee of presence.attendees.getAttendees()) {
392
+ if (attendee !== self) {
393
+ sendAttendeeConnected(attendee);
394
+ }
395
+ }
396
+
397
+ // Listen for presence events to notify parent/orchestrator when a new attendee joins or leaves the session.
398
+ presence.attendees.events.on("attendeeConnected", sendAttendeeConnected);
399
+ presence.attendees.events.on("attendeeDisconnected", sendAttendeeDisconnected);
400
+ }
401
+
402
+ private handleDisconnectSelf(): void {
403
+ if (!this.container) {
404
+ send({ event: "error", error: `${process_id} is not connected to container` });
405
+ return;
406
+ }
407
+ if (!this.presence) {
408
+ send({ event: "error", error: `${process_id} is not connected to presence` });
409
+ return;
410
+ }
411
+ this.container.disconnect();
412
+ send({
413
+ event: "disconnectedSelf",
414
+ attendeeId: this.presence.attendees.getMyself().attendeeId,
415
+ });
416
+ }
417
+
418
+ private handleSetLatestValue(
419
+ msg: Extract<MessageFromParent, { command: "setLatestValue" }>,
420
+ ): void {
421
+ if (!this.presence) {
422
+ send({ event: "error", error: `${process_id} is not connected to presence` });
423
+ return;
424
+ }
425
+ const workspace = this.workspaces.get(msg.workspaceId);
426
+ if (!workspace) {
427
+ send({ event: "error", error: `${process_id} workspace ${msg.workspaceId} not found` });
428
+ return;
429
+ }
430
+ // Cast required due to optional keys in WorkspaceSchema
431
+ // TODO: AB#47518
432
+ const latestState = workspace.states.latest as LatestRaw<{ value: string }> | undefined;
433
+ if (!latestState) {
434
+ send({
435
+ event: "error",
436
+ error: `${process_id} latest state not registered for workspace ${msg.workspaceId}`,
437
+ });
438
+ return;
439
+ }
440
+ if (typeof msg.value !== "string") {
441
+ return;
442
+ }
443
+ latestState.local = { value: msg.value };
444
+ }
445
+
446
+ private handleSetLatestMapValue(
447
+ msg: Extract<MessageFromParent, { command: "setLatestMapValue" }>,
448
+ ): void {
449
+ if (!this.presence) {
450
+ send({ event: "error", error: `${process_id} is not connected to presence` });
451
+ return;
452
+ }
453
+ if (typeof msg.key !== "string") {
454
+ send({ event: "error", error: `${process_id} invalid key type` });
455
+ return;
456
+ }
457
+ const workspace = this.workspaces.get(msg.workspaceId);
458
+ if (!workspace) {
459
+ send({ event: "error", error: `${process_id} workspace ${msg.workspaceId} not found` });
460
+ return;
461
+ }
462
+ // Cast required due to optional keys in WorkspaceSchema
463
+ // TODO: AB#47518
464
+ const latestMapState = workspace.states.latestMap as
465
+ | LatestMapRaw<{ value: Record<string, string | number> }, string>
466
+ | undefined;
467
+ if (!latestMapState) {
468
+ send({
469
+ event: "error",
470
+ error: `${process_id} latestMap state not registered for workspace ${msg.workspaceId}`,
471
+ });
472
+ return;
473
+ }
474
+ if (!isStringOrNumberRecord(msg.value)) {
475
+ return;
476
+ }
477
+ latestMapState.local.set(msg.key, { value: msg.value });
478
+ }
479
+
480
+ private handleGetLatestValue(
481
+ msg: Extract<MessageFromParent, { command: "getLatestValue" }>,
482
+ ): void {
483
+ if (!this.presence) {
484
+ send({ event: "error", error: `${process_id} is not connected to presence` });
485
+ return;
486
+ }
487
+ const workspace = this.workspaces.get(msg.workspaceId);
488
+ if (!workspace) {
489
+ send({ event: "error", error: `${process_id} workspace ${msg.workspaceId} not found` });
490
+ return;
491
+ }
492
+ // Cast required due to optional keys in WorkspaceSchema
493
+ // TODO: AB#47518
494
+ const latestState = workspace.states.latest as LatestRaw<{ value: string }> | undefined;
495
+ if (!latestState) {
496
+ send({
497
+ event: "error",
498
+ error: `${process_id} latest state not registered for workspace ${msg.workspaceId}`,
499
+ });
500
+ return;
501
+ }
502
+ let value: { value: string };
503
+ if (msg.attendeeId) {
504
+ const attendee = this.presence.attendees.getAttendee(msg.attendeeId);
505
+ const remoteData = latestState.getRemote(attendee);
506
+ value = remoteData.value;
507
+ } else {
508
+ value = latestState.local;
509
+ }
510
+ send({
511
+ event: "latestValueGetResponse",
512
+ workspaceId: msg.workspaceId,
513
+ attendeeId: msg.attendeeId,
514
+ value: value.value,
515
+ });
516
+ }
517
+
518
+ private handleGetLatestMapValue(
519
+ msg: Extract<MessageFromParent, { command: "getLatestMapValue" }>,
520
+ ): void {
521
+ if (!this.presence) {
522
+ send({ event: "error", error: `${process_id} is not connected to presence` });
523
+ return;
524
+ }
525
+ if (typeof msg.key !== "string") {
526
+ send({ event: "error", error: `${process_id} invalid key type` });
527
+ return;
528
+ }
529
+ const workspace = this.workspaces.get(msg.workspaceId);
530
+ if (!workspace) {
531
+ send({ event: "error", error: `${process_id} workspace ${msg.workspaceId} not found` });
532
+ return;
533
+ }
534
+ // Cast required due to optional keys in WorkspaceSchema
535
+ // TODO: AB#47518
536
+ const latestMapState = workspace.states.latestMap as
537
+ | LatestMapRaw<{ value: Record<string, string | number> }, string>
538
+ | undefined;
539
+ if (!latestMapState) {
540
+ send({
541
+ event: "error",
542
+ error: `${process_id} latestMap state not registered for workspace ${msg.workspaceId}`,
543
+ });
544
+ return;
545
+ }
546
+ let value: { value: Record<string, string | number> } | undefined;
547
+ if (msg.attendeeId) {
548
+ const attendee = this.presence.attendees.getAttendee(msg.attendeeId);
549
+ const remoteData = latestMapState.getRemote(attendee);
550
+ const keyData = remoteData.get(msg.key);
551
+ value = keyData?.value;
552
+ } else {
553
+ value = latestMapState.local.get(msg.key);
554
+ }
555
+ send({
556
+ event: "latestMapValueGetResponse",
557
+ workspaceId: msg.workspaceId,
558
+ attendeeId: msg.attendeeId,
559
+ key: msg.key,
560
+ value: value?.value,
561
+ });
562
+ }
563
+ }
564
+
565
+ function setupMessageHandler(): void {
566
+ const messageHandler = new MessageHandler();
567
+ process.on("message", (msg: MessageFromParent) => {
568
+ messageHandler.onMessage(msg).catch((error: Error) => {
569
+ console.error(`Error in client ${process_id}`, error);
570
+ send({ event: "error", error: `${process_id}: ${error.message}` });
571
+ });
572
+ });
573
+ }
574
+
575
+ setupMessageHandler();