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

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,70 @@ 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
+ const interest = telemetryEventInterestLevel(event.eventName);
252
+ if (interest === "none") {
253
+ return;
254
+ }
255
+ this.log.push({
256
+ timestamp: Date.now(),
257
+ agentId: process_id,
258
+ eventCategory: "telemetry",
259
+ eventName: event.eventName,
260
+ details:
261
+ typeof event.details === "string" ? event.details : JSON.stringify(event.details),
262
+ });
263
+ if (verbosity.includes("telem")) {
264
+ selectiveVerboseLog(event, logLevel);
265
+ }
266
+ },
267
+ };
268
+
269
+ private readonly onDisconnected = (): void => {
270
+ // Test state is a bit fragile and does not account for reconnections.
271
+ this.send({ event: "error", error: `${process_id}: Container disconnected` });
272
+ };
273
+
224
274
  private registerWorkspace(
225
275
  workspaceId: string,
226
276
  options: { latest?: boolean; latestMap?: boolean },
227
277
  ): void {
228
278
  if (!this.presence) {
229
- send({ event: "error", error: `${process_id} is not connected to presence` });
279
+ this.send({ event: "error", error: `${process_id} is not connected to presence` });
230
280
  return;
231
281
  }
232
282
  const { latest, latestMap } = options;
@@ -244,7 +294,7 @@ class MessageHandler {
244
294
  // TODO: AB#47518
245
295
  const latestState = workspace.states.latest as LatestRaw<{ value: string }>;
246
296
  latestState.events.on("remoteUpdated", (update) => {
247
- send({
297
+ this.send({
248
298
  event: "latestValueUpdated",
249
299
  workspaceId,
250
300
  attendeeId: update.attendee.attendeeId,
@@ -252,7 +302,7 @@ class MessageHandler {
252
302
  });
253
303
  });
254
304
  for (const remote of latestState.getRemotes()) {
255
- send({
305
+ this.send({
256
306
  event: "latestValueUpdated",
257
307
  workspaceId,
258
308
  attendeeId: remote.attendee.attendeeId,
@@ -276,7 +326,7 @@ class MessageHandler {
276
326
  >;
277
327
  latestMapState.events.on("remoteUpdated", (update) => {
278
328
  for (const [key, valueWithMetadata] of update.items) {
279
- send({
329
+ this.send({
280
330
  event: "latestMapValueUpdated",
281
331
  workspaceId,
282
332
  attendeeId: update.attendee.attendeeId,
@@ -287,7 +337,7 @@ class MessageHandler {
287
337
  });
288
338
  for (const remote of latestMapState.getRemotes()) {
289
339
  for (const [key, valueWithMetadata] of remote.items) {
290
- send({
340
+ this.send({
291
341
  event: "latestMapValueUpdated",
292
342
  workspaceId,
293
343
  attendeeId: remote.attendee.attendeeId,
@@ -299,7 +349,7 @@ class MessageHandler {
299
349
  }
300
350
 
301
351
  this.workspaces.set(workspaceId, workspace);
302
- send({
352
+ this.send({
303
353
  event: "workspaceRegistered",
304
354
  workspaceId,
305
355
  latest: latest ?? false,
@@ -309,15 +359,40 @@ class MessageHandler {
309
359
 
310
360
  public async onMessage(msg: MessageFromParent): Promise<void> {
311
361
  if (verbosity.includes("msgs")) {
362
+ this.log.push({
363
+ timestamp: Date.now(),
364
+ agentId: process_id,
365
+ eventCategory: "messageReceived",
366
+ eventName: msg.command,
367
+ });
312
368
  console.log(`[${process_id}] Received`, msg);
313
369
  }
370
+
371
+ if (msg.command === "ping") {
372
+ this.handlePing();
373
+ return;
374
+ }
375
+
376
+ if (msg.command === "connect") {
377
+ await this.handleConnect(msg);
378
+ return;
379
+ }
380
+
381
+ // All other message must wait if connect is in progress
382
+ if (this.msgQueue !== undefined) {
383
+ this.msgQueue.push(msg);
384
+ return;
385
+ }
386
+
387
+ this.processMessage(msg);
388
+ }
389
+
390
+ private processMessage(
391
+ msg: Exclude<MessageFromParent, { command: "ping" | "connect" }>,
392
+ ): void {
314
393
  switch (msg.command) {
315
- case "ping": {
316
- this.handlePing();
317
- break;
318
- }
319
- case "connect": {
320
- await this.handleConnect(msg);
394
+ case "debugReport": {
395
+ this.handleDebugReport(msg);
321
396
  break;
322
397
  }
323
398
  case "disconnectSelf": {
@@ -348,68 +423,118 @@ class MessageHandler {
348
423
  break;
349
424
  }
350
425
  default: {
351
- console.error(`${process_id}: Unknown command`);
352
- send({ event: "error", error: `${process_id} Unknown command` });
426
+ console.error(`${process_id}: Unknown command:`, msg);
427
+ this.send({
428
+ event: "error",
429
+ error: `${process_id} Unknown command: ${JSON.stringify(msg)}`,
430
+ });
353
431
  }
354
432
  }
355
433
  }
356
434
 
357
435
  private handlePing(): void {
358
- send({ event: "ack" });
436
+ this.send({ event: "ack" });
359
437
  }
360
438
 
361
439
  private async handleConnect(
362
440
  msg: Extract<MessageFromParent, { command: "connect" }>,
363
441
  ): Promise<void> {
364
442
  if (!msg.user) {
365
- send({ event: "error", error: `${process_id}: No azure user information given` });
443
+ this.send({ event: "error", error: `${process_id}: No azure user information given` });
366
444
  return;
367
445
  }
368
- if (isConnected(this.container)) {
369
- send({ event: "error", error: `${process_id}: Already connected to container` });
446
+ if (this.container) {
447
+ this.send({ event: "error", error: `${process_id}: Container already loaded` });
370
448
  return;
371
449
  }
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
450
 
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);
451
+ // Prevent reentrance. Queue messages until after connect is fully processed.
452
+ this.msgQueue = [];
453
+
454
+ try {
455
+ const { container, containerId, connected } = await getOrCreateContainer({
456
+ ...msg,
457
+ logger: this.logger,
458
+ onDisconnected: this.onDisconnected,
459
+ });
460
+ this.container = container;
461
+ const presence = getPresence(container);
462
+ this.presence = presence;
463
+
464
+ // wait for 'ConnectionState.Connected'
465
+ await connected;
466
+
467
+ // Acknowledge connection before sending current attendee information
468
+ this.send({
469
+ event: "connected",
470
+ containerId,
471
+ attendeeId: presence.attendees.getMyself().attendeeId,
472
+ });
473
+
474
+ // Send existing attendees excluding self to parent/orchestrator
475
+ const self = presence.attendees.getMyself();
476
+ for (const attendee of presence.attendees.getAttendees()) {
477
+ if (attendee !== self && attendee.getConnectionStatus() === "Connected") {
478
+ this.sendAttendeeConnected(attendee);
479
+ }
480
+ }
481
+
482
+ // Listen for presence events to notify parent/orchestrator when a new attendee joins or leaves the session.
483
+ presence.attendees.events.on("attendeeConnected", this.sendAttendeeConnected);
484
+ presence.attendees.events.on("attendeeDisconnected", this.sendAttendeeDisconnected);
485
+ } finally {
486
+ // Process any queued messages received while connecting
487
+ for (const queuedMsg of this.msgQueue) {
488
+ this.processMessage(queuedMsg);
489
+ }
490
+ this.msgQueue = undefined;
491
+ }
492
+ }
493
+
494
+ private handleDebugReport(
495
+ msg: Extract<MessageFromParent, { command: "debugReport" }>,
496
+ ): void {
497
+ if (msg.reportAttendees) {
498
+ if (this.presence) {
499
+ const attendees = this.presence.attendees.getAttendees();
500
+ let connectedCount = 0;
501
+ for (const attendee of attendees) {
502
+ if (attendee.getConnectionStatus() === "Connected") {
503
+ connectedCount++;
504
+ }
505
+ }
506
+ console.log(
507
+ `[${process_id}] Report: ${attendees.size} attendees, ${connectedCount} connected`,
508
+ );
509
+ } else {
510
+ this.send({ event: "error", error: `${process_id} is not connected to presence` });
394
511
  }
395
512
  }
396
513
 
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);
514
+ const debugReport: Extract<MessageToParent, { event: "debugReportComplete" }> = {
515
+ event: "debugReportComplete",
516
+ };
517
+ if (msg.sendEventLog) {
518
+ debugReport.log = this.log;
519
+ }
520
+ this.send(debugReport);
400
521
  }
401
522
 
402
523
  private handleDisconnectSelf(): void {
403
524
  if (!this.container) {
404
- send({ event: "error", error: `${process_id} is not connected to container` });
525
+ this.send({ event: "error", error: `${process_id} is not connected to container` });
405
526
  return;
406
527
  }
528
+ // There are no current scenarios where disconnect without presence is expected.
407
529
  if (!this.presence) {
408
- send({ event: "error", error: `${process_id} is not connected to presence` });
530
+ this.send({ event: "error", error: `${process_id} is not connected to presence` });
409
531
  return;
410
532
  }
533
+ // Disconnect event is treated as an error in normal handling.
534
+ // Remove listener as this disconnect is intentional.
535
+ this.container.off("disconnected", this.onDisconnected);
411
536
  this.container.disconnect();
412
- send({
537
+ this.send({
413
538
  event: "disconnectedSelf",
414
539
  attendeeId: this.presence.attendees.getMyself().attendeeId,
415
540
  });
@@ -419,19 +544,22 @@ class MessageHandler {
419
544
  msg: Extract<MessageFromParent, { command: "setLatestValue" }>,
420
545
  ): void {
421
546
  if (!this.presence) {
422
- send({ event: "error", error: `${process_id} is not connected to presence` });
547
+ this.send({ event: "error", error: `${process_id} is not connected to presence` });
423
548
  return;
424
549
  }
425
550
  const workspace = this.workspaces.get(msg.workspaceId);
426
551
  if (!workspace) {
427
- send({ event: "error", error: `${process_id} workspace ${msg.workspaceId} not found` });
552
+ this.send({
553
+ event: "error",
554
+ error: `${process_id} workspace ${msg.workspaceId} not found`,
555
+ });
428
556
  return;
429
557
  }
430
558
  // Cast required due to optional keys in WorkspaceSchema
431
559
  // TODO: AB#47518
432
560
  const latestState = workspace.states.latest as LatestRaw<{ value: string }> | undefined;
433
561
  if (!latestState) {
434
- send({
562
+ this.send({
435
563
  event: "error",
436
564
  error: `${process_id} latest state not registered for workspace ${msg.workspaceId}`,
437
565
  });
@@ -447,16 +575,19 @@ class MessageHandler {
447
575
  msg: Extract<MessageFromParent, { command: "setLatestMapValue" }>,
448
576
  ): void {
449
577
  if (!this.presence) {
450
- send({ event: "error", error: `${process_id} is not connected to presence` });
578
+ this.send({ event: "error", error: `${process_id} is not connected to presence` });
451
579
  return;
452
580
  }
453
581
  if (typeof msg.key !== "string") {
454
- send({ event: "error", error: `${process_id} invalid key type` });
582
+ this.send({ event: "error", error: `${process_id} invalid key type` });
455
583
  return;
456
584
  }
457
585
  const workspace = this.workspaces.get(msg.workspaceId);
458
586
  if (!workspace) {
459
- send({ event: "error", error: `${process_id} workspace ${msg.workspaceId} not found` });
587
+ this.send({
588
+ event: "error",
589
+ error: `${process_id} workspace ${msg.workspaceId} not found`,
590
+ });
460
591
  return;
461
592
  }
462
593
  // Cast required due to optional keys in WorkspaceSchema
@@ -465,7 +596,7 @@ class MessageHandler {
465
596
  | LatestMapRaw<{ value: Record<string, string | number> }, string>
466
597
  | undefined;
467
598
  if (!latestMapState) {
468
- send({
599
+ this.send({
469
600
  event: "error",
470
601
  error: `${process_id} latestMap state not registered for workspace ${msg.workspaceId}`,
471
602
  });
@@ -481,19 +612,22 @@ class MessageHandler {
481
612
  msg: Extract<MessageFromParent, { command: "getLatestValue" }>,
482
613
  ): void {
483
614
  if (!this.presence) {
484
- send({ event: "error", error: `${process_id} is not connected to presence` });
615
+ this.send({ event: "error", error: `${process_id} is not connected to presence` });
485
616
  return;
486
617
  }
487
618
  const workspace = this.workspaces.get(msg.workspaceId);
488
619
  if (!workspace) {
489
- send({ event: "error", error: `${process_id} workspace ${msg.workspaceId} not found` });
620
+ this.send({
621
+ event: "error",
622
+ error: `${process_id} workspace ${msg.workspaceId} not found`,
623
+ });
490
624
  return;
491
625
  }
492
626
  // Cast required due to optional keys in WorkspaceSchema
493
627
  // TODO: AB#47518
494
628
  const latestState = workspace.states.latest as LatestRaw<{ value: string }> | undefined;
495
629
  if (!latestState) {
496
- send({
630
+ this.send({
497
631
  event: "error",
498
632
  error: `${process_id} latest state not registered for workspace ${msg.workspaceId}`,
499
633
  });
@@ -507,7 +641,7 @@ class MessageHandler {
507
641
  } else {
508
642
  value = latestState.local;
509
643
  }
510
- send({
644
+ this.send({
511
645
  event: "latestValueGetResponse",
512
646
  workspaceId: msg.workspaceId,
513
647
  attendeeId: msg.attendeeId,
@@ -519,16 +653,19 @@ class MessageHandler {
519
653
  msg: Extract<MessageFromParent, { command: "getLatestMapValue" }>,
520
654
  ): void {
521
655
  if (!this.presence) {
522
- send({ event: "error", error: `${process_id} is not connected to presence` });
656
+ this.send({ event: "error", error: `${process_id} is not connected to presence` });
523
657
  return;
524
658
  }
525
659
  if (typeof msg.key !== "string") {
526
- send({ event: "error", error: `${process_id} invalid key type` });
660
+ this.send({ event: "error", error: `${process_id} invalid key type` });
527
661
  return;
528
662
  }
529
663
  const workspace = this.workspaces.get(msg.workspaceId);
530
664
  if (!workspace) {
531
- send({ event: "error", error: `${process_id} workspace ${msg.workspaceId} not found` });
665
+ this.send({
666
+ event: "error",
667
+ error: `${process_id} workspace ${msg.workspaceId} not found`,
668
+ });
532
669
  return;
533
670
  }
534
671
  // Cast required due to optional keys in WorkspaceSchema
@@ -537,7 +674,7 @@ class MessageHandler {
537
674
  | LatestMapRaw<{ value: Record<string, string | number> }, string>
538
675
  | undefined;
539
676
  if (!latestMapState) {
540
- send({
677
+ this.send({
541
678
  event: "error",
542
679
  error: `${process_id} latestMap state not registered for workspace ${msg.workspaceId}`,
543
680
  });
@@ -552,7 +689,7 @@ class MessageHandler {
552
689
  } else {
553
690
  value = latestMapState.local.get(msg.key);
554
691
  }
555
- send({
692
+ this.send({
556
693
  event: "latestMapValueGetResponse",
557
694
  workspaceId: msg.workspaceId,
558
695
  attendeeId: msg.attendeeId,