@fluidframework/azure-end-to-end-tests 2.70.0-361248 → 2.70.0

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.
@@ -14,6 +14,7 @@ import {
14
14
  } from "@fluidframework/azure-client";
15
15
  import { AttachState } from "@fluidframework/container-definitions";
16
16
  import { ConnectionState } from "@fluidframework/container-loader";
17
+ import type { ITelemetryBaseLogger } from "@fluidframework/core-interfaces";
17
18
  import { LogLevel } from "@fluidframework/core-interfaces";
18
19
  import type { ScopeType } from "@fluidframework/driver-definitions/legacy";
19
20
  import type { ContainerSchema, IFluidContainer } from "@fluidframework/fluid-static";
@@ -32,10 +33,13 @@ import { timeoutPromise } from "@fluidframework/test-utils/internal";
32
33
  import { createAzureTokenProvider } from "../AzureTokenFactory.js";
33
34
  import { TestDataObject } from "../TestDataObject.js";
34
35
 
35
- import type { MessageFromChild, MessageToChild, UserIdAndName } from "./messageTypes.js";
36
+ import type {
37
+ MessageFromChild as MessageToParent,
38
+ MessageToChild as MessageFromParent,
39
+ UserIdAndName,
40
+ EventEntry,
41
+ } from "./messageTypes.js";
36
42
 
37
- type MessageFromParent = MessageToChild;
38
- type MessageToParent = Required<MessageFromChild>;
39
43
  const connectTimeoutMs = 10_000;
40
44
  // Identifier given to child process
41
45
  const process_id = process.argv[2];
@@ -50,41 +54,57 @@ if (useAzure && endPoint === undefined) {
50
54
  throw new Error("Azure Fluid Relay service endpoint is missing");
51
55
  }
52
56
 
57
+ const containerSchema = {
58
+ initialObjects: {
59
+ // A DataObject is added as otherwise fluid-static complains "Container cannot be initialized without any DataTypes"
60
+ _unused: TestDataObject,
61
+ },
62
+ } as const satisfies ContainerSchema;
63
+
64
+ function telemetryEventInterestLevel(eventName: string): "none" | "basic" | "details" {
65
+ if (eventName.includes(":Signal") || eventName.includes(":Join")) {
66
+ return "details";
67
+ } else if (eventName.includes(":Container:") || eventName.includes(":Presence:")) {
68
+ return "basic";
69
+ }
70
+ return "none";
71
+ }
72
+
53
73
  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
- });
74
+ const interest = telemetryEventInterestLevel(event.eventName);
75
+ if (interest === "none") {
76
+ return;
68
77
  }
78
+ const content: Record<string, unknown> = {
79
+ eventName: event.eventName,
80
+ containerConnectionState: event.containerConnectionState,
81
+ };
82
+ if (interest === "details") {
83
+ content.details = event.details;
84
+ }
85
+ console.log(`[${process_id}] [${logLevel ?? LogLevel.default}]`, content);
69
86
  }
70
87
 
71
88
  /**
72
- * Get or create a Fluid container with Presence in initialObjects.
89
+ * Get or create a Fluid container.
73
90
  */
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;
91
+ const getOrCreateContainer = async (params: {
92
+ logger: ITelemetryBaseLogger;
93
+ onDisconnected: () => void;
94
+ containerId?: string;
95
+ user: UserIdAndName;
96
+ scopes?: ScopeType[];
97
+ createScopes?: ScopeType[];
98
+ }): Promise<{
99
+ container: IFluidContainer<typeof containerSchema>;
82
100
  services: AzureContainerServices;
83
101
  client: AzureClient;
84
102
  containerId: string;
103
+ connected: Promise<void>;
85
104
  }> => {
86
- let container: IFluidContainer;
87
- let containerId: string;
105
+ let container: IFluidContainer<typeof containerSchema>;
106
+ let { containerId } = params;
107
+ const { logger, onDisconnected, user, scopes, createScopes } = params;
88
108
  const connectionProps: AzureRemoteConnectionConfig | AzureLocalConnectionConfig = useAzure
89
109
  ? {
90
110
  tenantId,
@@ -104,46 +124,40 @@ const getOrCreatePresenceContainer = async (
104
124
  };
105
125
  const client = new AzureClient({
106
126
  connection: connectionProps,
107
- logger: {
108
- send: verbosity.includes("telem") ? selectiveVerboseLog : () => {},
109
- },
127
+ logger,
110
128
  });
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
129
  let services: AzureContainerServices;
118
- if (id === undefined) {
119
- ({ container, services } = await client.createContainer(schema, "2"));
130
+ if (containerId === undefined) {
131
+ ({ container, services } = await client.createContainer(containerSchema, "2"));
120
132
  containerId = await container.attach();
121
133
  } 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
- });
134
+ ({ container, services } = await client.getContainer(containerId, containerSchema, "2"));
131
135
  }
136
+ container.on("disconnected", onDisconnected);
137
+
138
+ const connected =
139
+ container.connectionState === ConnectionState.Connected
140
+ ? Promise.resolve()
141
+ : timeoutPromise((resolve) => container.once("connected", () => resolve()), {
142
+ durationMs: connectTimeoutMs,
143
+ errorMsg: "container connect() timeout",
144
+ });
145
+
132
146
  assert.strictEqual(
133
147
  container.attachState,
134
148
  AttachState.Attached,
135
149
  "Container is not attached after attach is called",
136
150
  );
137
151
 
138
- const presence = getPresence(container);
139
152
  return {
140
153
  client,
141
154
  container,
142
- presence,
143
155
  services,
144
156
  containerId,
157
+ connected,
145
158
  };
146
159
  };
160
+
147
161
  function createSendFunction(): (msg: MessageToParent) => void {
148
162
  if (process.send) {
149
163
  const sendFn = process.send.bind(process);
@@ -160,23 +174,6 @@ function createSendFunction(): (msg: MessageToParent) => void {
160
174
 
161
175
  const send = createSendFunction();
162
176
 
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
177
  function isStringOrNumberRecord(value: unknown): value is Record<string, string | number> {
181
178
  if (value === null || typeof value !== "object" || Array.isArray(value)) {
182
179
  return false;
@@ -216,17 +213,79 @@ type WorkspaceSchema = {
216
213
  const WorkspaceSchema: WorkspaceSchema = {};
217
214
 
218
215
  class MessageHandler {
219
- public presence: Presence | undefined;
220
- public container: IFluidContainer | undefined;
221
- public containerId: string | undefined;
216
+ private readonly log: EventEntry[] = [];
217
+ private msgQueue: undefined | Exclude<MessageFromParent, { command: "ping" | "connect" }>[];
218
+ private container: IFluidContainer | undefined;
219
+ private presence: Presence | undefined;
222
220
  private readonly workspaces = new Map<string, StatesWorkspace<WorkspaceSchema>>();
223
221
 
222
+ private send(msg: MessageToParent): void {
223
+ this.log.push({
224
+ timestamp: Date.now(),
225
+ agentId: process_id,
226
+ eventCategory: "messageSent",
227
+ eventName: msg.event,
228
+ details:
229
+ msg.event === "debugReportComplete" && msg.log
230
+ ? JSON.stringify({ logLength: msg.log.length })
231
+ : JSON.stringify(msg),
232
+ });
233
+ send(msg);
234
+ }
235
+
236
+ private readonly sendAttendeeConnected = (attendee: Attendee): void => {
237
+ this.send({
238
+ event: "attendeeConnected",
239
+ attendeeId: attendee.attendeeId,
240
+ });
241
+ };
242
+ private readonly sendAttendeeDisconnected = (attendee: Attendee): void => {
243
+ this.send({
244
+ event: "attendeeDisconnected",
245
+ attendeeId: attendee.attendeeId,
246
+ });
247
+ };
248
+
249
+ private readonly logger: ITelemetryBaseLogger = {
250
+ send: (event: ITelemetryBaseEvent, logLevel?: LogLevel) => {
251
+ // Special case unexpected telemetry event
252
+ if (event.eventName.endsWith(":JoinResponseWhenAlone")) {
253
+ this.send({
254
+ event: "error",
255
+ error: `Unexpected ClientJoin response. Details: ${JSON.stringify(event.details)}`,
256
+ });
257
+ // Keep going
258
+ }
259
+
260
+ const interest = telemetryEventInterestLevel(event.eventName);
261
+ if (interest === "none") {
262
+ return;
263
+ }
264
+ this.log.push({
265
+ timestamp: Date.now(),
266
+ agentId: process_id,
267
+ eventCategory: "telemetry",
268
+ eventName: event.eventName,
269
+ details:
270
+ typeof event.details === "string" ? event.details : JSON.stringify(event.details),
271
+ });
272
+ if (verbosity.includes("telem")) {
273
+ selectiveVerboseLog(event, logLevel);
274
+ }
275
+ },
276
+ };
277
+
278
+ private readonly onDisconnected = (): void => {
279
+ // Test state is a bit fragile and does not account for reconnections.
280
+ this.send({ event: "error", error: `${process_id}: Container disconnected` });
281
+ };
282
+
224
283
  private registerWorkspace(
225
284
  workspaceId: string,
226
285
  options: { latest?: boolean; latestMap?: boolean },
227
286
  ): void {
228
287
  if (!this.presence) {
229
- send({ event: "error", error: `${process_id} is not connected to presence` });
288
+ this.send({ event: "error", error: `${process_id} is not connected to presence` });
230
289
  return;
231
290
  }
232
291
  const { latest, latestMap } = options;
@@ -244,7 +303,7 @@ class MessageHandler {
244
303
  // TODO: AB#47518
245
304
  const latestState = workspace.states.latest as LatestRaw<{ value: string }>;
246
305
  latestState.events.on("remoteUpdated", (update) => {
247
- send({
306
+ this.send({
248
307
  event: "latestValueUpdated",
249
308
  workspaceId,
250
309
  attendeeId: update.attendee.attendeeId,
@@ -252,7 +311,7 @@ class MessageHandler {
252
311
  });
253
312
  });
254
313
  for (const remote of latestState.getRemotes()) {
255
- send({
314
+ this.send({
256
315
  event: "latestValueUpdated",
257
316
  workspaceId,
258
317
  attendeeId: remote.attendee.attendeeId,
@@ -276,7 +335,7 @@ class MessageHandler {
276
335
  >;
277
336
  latestMapState.events.on("remoteUpdated", (update) => {
278
337
  for (const [key, valueWithMetadata] of update.items) {
279
- send({
338
+ this.send({
280
339
  event: "latestMapValueUpdated",
281
340
  workspaceId,
282
341
  attendeeId: update.attendee.attendeeId,
@@ -287,7 +346,7 @@ class MessageHandler {
287
346
  });
288
347
  for (const remote of latestMapState.getRemotes()) {
289
348
  for (const [key, valueWithMetadata] of remote.items) {
290
- send({
349
+ this.send({
291
350
  event: "latestMapValueUpdated",
292
351
  workspaceId,
293
352
  attendeeId: remote.attendee.attendeeId,
@@ -299,7 +358,7 @@ class MessageHandler {
299
358
  }
300
359
 
301
360
  this.workspaces.set(workspaceId, workspace);
302
- send({
361
+ this.send({
303
362
  event: "workspaceRegistered",
304
363
  workspaceId,
305
364
  latest: latest ?? false,
@@ -309,15 +368,40 @@ class MessageHandler {
309
368
 
310
369
  public async onMessage(msg: MessageFromParent): Promise<void> {
311
370
  if (verbosity.includes("msgs")) {
371
+ this.log.push({
372
+ timestamp: Date.now(),
373
+ agentId: process_id,
374
+ eventCategory: "messageReceived",
375
+ eventName: msg.command,
376
+ });
312
377
  console.log(`[${process_id}] Received`, msg);
313
378
  }
379
+
380
+ if (msg.command === "ping") {
381
+ this.handlePing();
382
+ return;
383
+ }
384
+
385
+ if (msg.command === "connect") {
386
+ await this.handleConnect(msg);
387
+ return;
388
+ }
389
+
390
+ // All other message must wait if connect is in progress
391
+ if (this.msgQueue !== undefined) {
392
+ this.msgQueue.push(msg);
393
+ return;
394
+ }
395
+
396
+ this.processMessage(msg);
397
+ }
398
+
399
+ private processMessage(
400
+ msg: Exclude<MessageFromParent, { command: "ping" | "connect" }>,
401
+ ): void {
314
402
  switch (msg.command) {
315
- case "ping": {
316
- this.handlePing();
317
- break;
318
- }
319
- case "connect": {
320
- await this.handleConnect(msg);
403
+ case "debugReport": {
404
+ this.handleDebugReport(msg);
321
405
  break;
322
406
  }
323
407
  case "disconnectSelf": {
@@ -348,68 +432,118 @@ class MessageHandler {
348
432
  break;
349
433
  }
350
434
  default: {
351
- console.error(`${process_id}: Unknown command`);
352
- send({ event: "error", error: `${process_id} Unknown command` });
435
+ console.error(`${process_id}: Unknown command:`, msg);
436
+ this.send({
437
+ event: "error",
438
+ error: `${process_id} Unknown command: ${JSON.stringify(msg)}`,
439
+ });
353
440
  }
354
441
  }
355
442
  }
356
443
 
357
444
  private handlePing(): void {
358
- send({ event: "ack" });
445
+ this.send({ event: "ack" });
359
446
  }
360
447
 
361
448
  private async handleConnect(
362
449
  msg: Extract<MessageFromParent, { command: "connect" }>,
363
450
  ): Promise<void> {
364
451
  if (!msg.user) {
365
- send({ event: "error", error: `${process_id}: No azure user information given` });
452
+ this.send({ event: "error", error: `${process_id}: No azure user information given` });
366
453
  return;
367
454
  }
368
- if (isConnected(this.container)) {
369
- send({ event: "error", error: `${process_id}: Already connected to container` });
455
+ if (this.container) {
456
+ this.send({ event: "error", error: `${process_id}: Container already loaded` });
370
457
  return;
371
458
  }
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
459
 
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);
460
+ // Prevent reentrance. Queue messages until after connect is fully processed.
461
+ this.msgQueue = [];
462
+
463
+ try {
464
+ const { container, containerId, connected } = await getOrCreateContainer({
465
+ ...msg,
466
+ logger: this.logger,
467
+ onDisconnected: this.onDisconnected,
468
+ });
469
+ this.container = container;
470
+ const presence = getPresence(container);
471
+ this.presence = presence;
472
+
473
+ // wait for 'ConnectionState.Connected'
474
+ await connected;
475
+
476
+ // Acknowledge connection before sending current attendee information
477
+ this.send({
478
+ event: "connected",
479
+ containerId,
480
+ attendeeId: presence.attendees.getMyself().attendeeId,
481
+ });
482
+
483
+ // Send existing attendees excluding self to parent/orchestrator
484
+ const self = presence.attendees.getMyself();
485
+ for (const attendee of presence.attendees.getAttendees()) {
486
+ if (attendee !== self && attendee.getConnectionStatus() === "Connected") {
487
+ this.sendAttendeeConnected(attendee);
488
+ }
489
+ }
490
+
491
+ // Listen for presence events to notify parent/orchestrator when a new attendee joins or leaves the session.
492
+ presence.attendees.events.on("attendeeConnected", this.sendAttendeeConnected);
493
+ presence.attendees.events.on("attendeeDisconnected", this.sendAttendeeDisconnected);
494
+ } finally {
495
+ // Process any queued messages received while connecting
496
+ for (const queuedMsg of this.msgQueue) {
497
+ this.processMessage(queuedMsg);
498
+ }
499
+ this.msgQueue = undefined;
500
+ }
501
+ }
502
+
503
+ private handleDebugReport(
504
+ msg: Extract<MessageFromParent, { command: "debugReport" }>,
505
+ ): void {
506
+ if (msg.reportAttendees) {
507
+ if (this.presence) {
508
+ const attendees = this.presence.attendees.getAttendees();
509
+ let connectedCount = 0;
510
+ for (const attendee of attendees) {
511
+ if (attendee.getConnectionStatus() === "Connected") {
512
+ connectedCount++;
513
+ }
514
+ }
515
+ console.log(
516
+ `[${process_id}] Report: ${attendees.size} attendees, ${connectedCount} connected`,
517
+ );
518
+ } else {
519
+ this.send({ event: "error", error: `${process_id} is not connected to presence` });
394
520
  }
395
521
  }
396
522
 
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);
523
+ const debugReport: Extract<MessageToParent, { event: "debugReportComplete" }> = {
524
+ event: "debugReportComplete",
525
+ };
526
+ if (msg.sendEventLog) {
527
+ debugReport.log = this.log;
528
+ }
529
+ this.send(debugReport);
400
530
  }
401
531
 
402
532
  private handleDisconnectSelf(): void {
403
533
  if (!this.container) {
404
- send({ event: "error", error: `${process_id} is not connected to container` });
534
+ this.send({ event: "error", error: `${process_id} is not connected to container` });
405
535
  return;
406
536
  }
537
+ // There are no current scenarios where disconnect without presence is expected.
407
538
  if (!this.presence) {
408
- send({ event: "error", error: `${process_id} is not connected to presence` });
539
+ this.send({ event: "error", error: `${process_id} is not connected to presence` });
409
540
  return;
410
541
  }
542
+ // Disconnect event is treated as an error in normal handling.
543
+ // Remove listener as this disconnect is intentional.
544
+ this.container.off("disconnected", this.onDisconnected);
411
545
  this.container.disconnect();
412
- send({
546
+ this.send({
413
547
  event: "disconnectedSelf",
414
548
  attendeeId: this.presence.attendees.getMyself().attendeeId,
415
549
  });
@@ -419,19 +553,22 @@ class MessageHandler {
419
553
  msg: Extract<MessageFromParent, { command: "setLatestValue" }>,
420
554
  ): void {
421
555
  if (!this.presence) {
422
- send({ event: "error", error: `${process_id} is not connected to presence` });
556
+ this.send({ event: "error", error: `${process_id} is not connected to presence` });
423
557
  return;
424
558
  }
425
559
  const workspace = this.workspaces.get(msg.workspaceId);
426
560
  if (!workspace) {
427
- send({ event: "error", error: `${process_id} workspace ${msg.workspaceId} not found` });
561
+ this.send({
562
+ event: "error",
563
+ error: `${process_id} workspace ${msg.workspaceId} not found`,
564
+ });
428
565
  return;
429
566
  }
430
567
  // Cast required due to optional keys in WorkspaceSchema
431
568
  // TODO: AB#47518
432
569
  const latestState = workspace.states.latest as LatestRaw<{ value: string }> | undefined;
433
570
  if (!latestState) {
434
- send({
571
+ this.send({
435
572
  event: "error",
436
573
  error: `${process_id} latest state not registered for workspace ${msg.workspaceId}`,
437
574
  });
@@ -447,16 +584,19 @@ class MessageHandler {
447
584
  msg: Extract<MessageFromParent, { command: "setLatestMapValue" }>,
448
585
  ): void {
449
586
  if (!this.presence) {
450
- send({ event: "error", error: `${process_id} is not connected to presence` });
587
+ this.send({ event: "error", error: `${process_id} is not connected to presence` });
451
588
  return;
452
589
  }
453
590
  if (typeof msg.key !== "string") {
454
- send({ event: "error", error: `${process_id} invalid key type` });
591
+ this.send({ event: "error", error: `${process_id} invalid key type` });
455
592
  return;
456
593
  }
457
594
  const workspace = this.workspaces.get(msg.workspaceId);
458
595
  if (!workspace) {
459
- send({ event: "error", error: `${process_id} workspace ${msg.workspaceId} not found` });
596
+ this.send({
597
+ event: "error",
598
+ error: `${process_id} workspace ${msg.workspaceId} not found`,
599
+ });
460
600
  return;
461
601
  }
462
602
  // Cast required due to optional keys in WorkspaceSchema
@@ -465,7 +605,7 @@ class MessageHandler {
465
605
  | LatestMapRaw<{ value: Record<string, string | number> }, string>
466
606
  | undefined;
467
607
  if (!latestMapState) {
468
- send({
608
+ this.send({
469
609
  event: "error",
470
610
  error: `${process_id} latestMap state not registered for workspace ${msg.workspaceId}`,
471
611
  });
@@ -481,19 +621,22 @@ class MessageHandler {
481
621
  msg: Extract<MessageFromParent, { command: "getLatestValue" }>,
482
622
  ): void {
483
623
  if (!this.presence) {
484
- send({ event: "error", error: `${process_id} is not connected to presence` });
624
+ this.send({ event: "error", error: `${process_id} is not connected to presence` });
485
625
  return;
486
626
  }
487
627
  const workspace = this.workspaces.get(msg.workspaceId);
488
628
  if (!workspace) {
489
- send({ event: "error", error: `${process_id} workspace ${msg.workspaceId} not found` });
629
+ this.send({
630
+ event: "error",
631
+ error: `${process_id} workspace ${msg.workspaceId} not found`,
632
+ });
490
633
  return;
491
634
  }
492
635
  // Cast required due to optional keys in WorkspaceSchema
493
636
  // TODO: AB#47518
494
637
  const latestState = workspace.states.latest as LatestRaw<{ value: string }> | undefined;
495
638
  if (!latestState) {
496
- send({
639
+ this.send({
497
640
  event: "error",
498
641
  error: `${process_id} latest state not registered for workspace ${msg.workspaceId}`,
499
642
  });
@@ -507,7 +650,7 @@ class MessageHandler {
507
650
  } else {
508
651
  value = latestState.local;
509
652
  }
510
- send({
653
+ this.send({
511
654
  event: "latestValueGetResponse",
512
655
  workspaceId: msg.workspaceId,
513
656
  attendeeId: msg.attendeeId,
@@ -519,16 +662,19 @@ class MessageHandler {
519
662
  msg: Extract<MessageFromParent, { command: "getLatestMapValue" }>,
520
663
  ): void {
521
664
  if (!this.presence) {
522
- send({ event: "error", error: `${process_id} is not connected to presence` });
665
+ this.send({ event: "error", error: `${process_id} is not connected to presence` });
523
666
  return;
524
667
  }
525
668
  if (typeof msg.key !== "string") {
526
- send({ event: "error", error: `${process_id} invalid key type` });
669
+ this.send({ event: "error", error: `${process_id} invalid key type` });
527
670
  return;
528
671
  }
529
672
  const workspace = this.workspaces.get(msg.workspaceId);
530
673
  if (!workspace) {
531
- send({ event: "error", error: `${process_id} workspace ${msg.workspaceId} not found` });
674
+ this.send({
675
+ event: "error",
676
+ error: `${process_id} workspace ${msg.workspaceId} not found`,
677
+ });
532
678
  return;
533
679
  }
534
680
  // Cast required due to optional keys in WorkspaceSchema
@@ -537,7 +683,7 @@ class MessageHandler {
537
683
  | LatestMapRaw<{ value: Record<string, string | number> }, string>
538
684
  | undefined;
539
685
  if (!latestMapState) {
540
- send({
686
+ this.send({
541
687
  event: "error",
542
688
  error: `${process_id} latestMap state not registered for workspace ${msg.workspaceId}`,
543
689
  });
@@ -552,7 +698,7 @@ class MessageHandler {
552
698
  } else {
553
699
  value = latestMapState.local.get(msg.key);
554
700
  }
555
- send({
701
+ this.send({
556
702
  event: "latestMapValueGetResponse",
557
703
  workspaceId: msg.workspaceId,
558
704
  attendeeId: msg.attendeeId,