@fluidframework/azure-end-to-end-tests 2.53.0-350190 → 2.53.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # @fluidframework/azure-end-to-end-tests
2
2
 
3
+ ## 2.53.0
4
+
5
+ Dependency updates only.
6
+
3
7
  ## 2.52.0
4
8
 
5
9
  Dependency updates only.
@@ -6,9 +6,11 @@ import { strict as assert } from "node:assert";
6
6
  import { AzureClient, } from "@fluidframework/azure-client";
7
7
  import { AttachState } from "@fluidframework/container-definitions";
8
8
  import { ConnectionState } from "@fluidframework/container-loader";
9
- import { getPresence, ExperimentalPresenceManager,
10
9
  // eslint-disable-next-line import/no-internal-modules
11
- } from "@fluidframework/presence/alpha";
10
+ import { ExperimentalPresenceManager } from "@fluidframework/presence/alpha";
11
+ import { getPresence,
12
+ // eslint-disable-next-line import/no-internal-modules
13
+ } from "@fluidframework/presence/beta";
12
14
  import { InsecureTokenProvider } from "@fluidframework/test-runtime-utils/internal";
13
15
  import { timeoutPromise } from "@fluidframework/test-utils/internal";
14
16
  import { createAzureTokenProvider } from "../AzureTokenFactory.js";
@@ -87,6 +89,10 @@ function isConnected(container) {
87
89
  class MessageHandler {
88
90
  async onMessage(msg) {
89
91
  switch (msg.command) {
92
+ case "ping": {
93
+ send({ event: "ack" });
94
+ break;
95
+ }
90
96
  // Respond to connect command by connecting to Fluid container with the provided user information.
91
97
  case "connect": {
92
98
  // Check if valid user information has been provided by parent/orchestrator
@@ -119,7 +125,7 @@ class MessageHandler {
119
125
  send(m);
120
126
  });
121
127
  send({
122
- event: "ready",
128
+ event: "connected",
123
129
  containerId,
124
130
  attendeeId: presence.attendees.getMyself().attendeeId,
125
131
  });
@@ -1 +1 @@
1
- {"version":3,"file":"childClient.js","sourceRoot":"","sources":["../../../src/test/multiprocess/childClient.ts"],"names":[],"mappings":"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","sourcesContent":["/*!\n * Copyright (c) Microsoft Corporation and contributors. All rights reserved.\n * Licensed under the MIT License.\n */\n\nimport { strict as assert } from \"node:assert\";\n\nimport {\n\tAzureClient,\n\ttype AzureContainerServices,\n\ttype AzureLocalConnectionConfig,\n\ttype AzureRemoteConnectionConfig,\n} from \"@fluidframework/azure-client\";\nimport { AttachState } from \"@fluidframework/container-definitions\";\nimport { ConnectionState } from \"@fluidframework/container-loader\";\nimport type { ContainerSchema, IFluidContainer } from \"@fluidframework/fluid-static\";\nimport {\n\tgetPresence,\n\ttype Attendee,\n\tExperimentalPresenceManager,\n\ttype Presence,\n\t// eslint-disable-next-line import/no-internal-modules\n} from \"@fluidframework/presence/alpha\";\nimport { InsecureTokenProvider } from \"@fluidframework/test-runtime-utils/internal\";\nimport { timeoutPromise } from \"@fluidframework/test-utils/internal\";\n\nimport type { ScopeType } from \"../AzureClientFactory.js\";\nimport { createAzureTokenProvider } from \"../AzureTokenFactory.js\";\nimport type { configProvider } from \"../utils.js\";\n\nimport type { MessageFromChild, MessageToChild } from \"./messageTypes.js\";\n\ntype MessageFromParent = MessageToChild;\ntype MessageToParent = Required<MessageFromChild>;\ninterface UserIdAndName {\n\tid: string;\n\tname: string;\n}\n\nconst connectTimeoutMs = 10_000;\n// Identifier given to child process\nconst process_id = process.argv[2];\n\nconst useAzure = process.env.FLUID_CLIENT === \"azure\";\nconst tenantId = useAzure\n\t? (process.env.azure__fluid__relay__service__tenantId as string)\n\t: \"frs-client-tenant\";\nconst endPoint = process.env.azure__fluid__relay__service__endpoint as string;\nif (useAzure && endPoint === undefined) {\n\tthrow new Error(\"Azure Fluid Relay service endpoint is missing\");\n}\n\n/**\n * Get or create a Fluid container with Presence in initialObjects.\n */\nconst getOrCreatePresenceContainer = async (\n\tid: string | undefined,\n\tuser: UserIdAndName,\n\tconfig?: ReturnType<typeof configProvider>,\n\tscopes?: ScopeType[],\n): Promise<{\n\tcontainer: IFluidContainer;\n\tpresence: Presence;\n\tservices: AzureContainerServices;\n\tclient: AzureClient;\n\tcontainerId: string;\n}> => {\n\tlet container: IFluidContainer;\n\tlet containerId: string;\n\tconst connectionProps: AzureRemoteConnectionConfig | AzureLocalConnectionConfig = useAzure\n\t\t? {\n\t\t\t\ttenantId,\n\t\t\t\ttokenProvider: createAzureTokenProvider(user.id ?? \"foo\", user.name ?? \"bar\", scopes),\n\t\t\t\tendpoint: endPoint,\n\t\t\t\ttype: \"remote\",\n\t\t\t}\n\t\t: {\n\t\t\t\ttokenProvider: new InsecureTokenProvider(\"fooBar\", user, scopes),\n\t\t\t\tendpoint: \"http://localhost:7071\",\n\t\t\t\ttype: \"local\",\n\t\t\t};\n\tconst client = new AzureClient({ connection: connectionProps });\n\tconst schema: ContainerSchema = {\n\t\tinitialObjects: {\n\t\t\t// A DataObject is added as otherwise fluid-static complains \"Container cannot be initialized without any DataTypes\"\n\t\t\t_unused: ExperimentalPresenceManager,\n\t\t},\n\t};\n\tlet services: AzureContainerServices;\n\tif (id === undefined) {\n\t\t({ container, services } = await client.createContainer(schema, \"2\"));\n\t\tcontainerId = await container.attach();\n\t} else {\n\t\tcontainerId = id;\n\t\t({ container, services } = await client.getContainer(containerId, schema, \"2\"));\n\t}\n\t// wait for 'ConnectionState.Connected' so we return with client connected to container\n\tif (container.connectionState !== ConnectionState.Connected) {\n\t\tawait timeoutPromise((resolve) => container.once(\"connected\", () => resolve()), {\n\t\t\tdurationMs: connectTimeoutMs,\n\t\t\terrorMsg: \"container connect() timeout\",\n\t\t});\n\t}\n\tassert.strictEqual(\n\t\tcontainer.attachState,\n\t\tAttachState.Attached,\n\t\t\"Container is not attached after attach is called\",\n\t);\n\n\tconst presence = getPresence(container);\n\treturn {\n\t\tclient,\n\t\tcontainer,\n\t\tpresence,\n\t\tservices,\n\t\tcontainerId,\n\t};\n};\nfunction createSendFunction(): (msg: MessageToParent) => void {\n\tif (process.send) {\n\t\treturn process.send.bind(process);\n\t}\n\tthrow new Error(\"process.send is not defined\");\n}\n\nconst send = createSendFunction();\n\nfunction isConnected(container: IFluidContainer | undefined): boolean {\n\treturn container !== undefined && container.connectionState === ConnectionState.Connected;\n}\n\nclass MessageHandler {\n\tpublic presence: Presence | undefined;\n\tpublic container: IFluidContainer | undefined;\n\tpublic containerId: string | undefined;\n\n\tpublic async onMessage(msg: MessageFromParent): Promise<void> {\n\t\tswitch (msg.command) {\n\t\t\t// Respond to connect command by connecting to Fluid container with the provided user information.\n\t\t\tcase \"connect\": {\n\t\t\t\t// Check if valid user information has been provided by parent/orchestrator\n\t\t\t\tif (!msg.user) {\n\t\t\t\t\tsend({ event: \"error\", error: `${process_id}: No azure user information given` });\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\t// Check if already connected to container\n\t\t\t\tif (isConnected(this.container)) {\n\t\t\t\t\tsend({ event: \"error\", error: `${process_id}: Already connected to container` });\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tconst { container, presence, containerId } = await getOrCreatePresenceContainer(\n\t\t\t\t\tmsg.containerId,\n\t\t\t\t\tmsg.user,\n\t\t\t\t);\n\t\t\t\tthis.container = container;\n\t\t\t\tthis.presence = presence;\n\t\t\t\tthis.containerId = containerId;\n\n\t\t\t\t// Listen for presence events to notify parent/orchestrator when a new attendee joins or leaves the session.\n\t\t\t\tpresence.attendees.events.on(\"attendeeConnected\", (attendee: Attendee) => {\n\t\t\t\t\tconst m: MessageToParent = {\n\t\t\t\t\t\tevent: \"attendeeConnected\",\n\t\t\t\t\t\tattendeeId: attendee.attendeeId,\n\t\t\t\t\t};\n\t\t\t\t\tsend(m);\n\t\t\t\t});\n\t\t\t\tpresence.attendees.events.on(\"attendeeDisconnected\", (attendee: Attendee) => {\n\t\t\t\t\tconst m: MessageToParent = {\n\t\t\t\t\t\tevent: \"attendeeDisconnected\",\n\t\t\t\t\t\tattendeeId: attendee.attendeeId,\n\t\t\t\t\t};\n\t\t\t\t\tsend(m);\n\t\t\t\t});\n\t\t\t\tsend({\n\t\t\t\t\tevent: \"ready\",\n\t\t\t\t\tcontainerId,\n\t\t\t\t\tattendeeId: presence.attendees.getMyself().attendeeId,\n\t\t\t\t});\n\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\t// Respond to disconnect command by disconnecting self from Fluid container.\n\t\t\tcase \"disconnectSelf\": {\n\t\t\t\tif (!this.container) {\n\t\t\t\t\tsend({ event: \"error\", error: `${process_id} is not connected to container` });\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tif (!this.presence) {\n\t\t\t\t\tsend({ event: \"error\", error: `${process_id} is not connected to presence` });\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\n\t\t\t\tthis.container.disconnect();\n\t\t\t\tsend({\n\t\t\t\t\tevent: \"disconnectedSelf\",\n\t\t\t\t\tattendeeId: this.presence.attendees.getMyself().attendeeId,\n\t\t\t\t});\n\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tdefault: {\n\t\t\t\tconsole.error(`${process_id}: Unknown command`);\n\t\t\t\tsend({ event: \"error\", error: `${process_id} Unknown command` });\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunction setupMessageHandler(): void {\n\tconst messageHandler = new MessageHandler();\n\tprocess.on(\"message\", (msg: MessageFromParent) => {\n\t\tmessageHandler.onMessage(msg).catch((error: Error) => {\n\t\t\tconsole.error(`Error in client ${process_id}`, error);\n\t\t\tsend({ event: \"error\", error: `${process_id}: ${error.message}` });\n\t\t});\n\t});\n}\n\nsetupMessageHandler();\n"]}
1
+ {"version":3,"file":"childClient.js","sourceRoot":"","sources":["../../../src/test/multiprocess/childClient.ts"],"names":[],"mappings":"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","sourcesContent":["/*!\n * Copyright (c) Microsoft Corporation and contributors. All rights reserved.\n * Licensed under the MIT License.\n */\n\nimport { strict as assert } from \"node:assert\";\n\nimport {\n\tAzureClient,\n\ttype AzureContainerServices,\n\ttype AzureLocalConnectionConfig,\n\ttype AzureRemoteConnectionConfig,\n} from \"@fluidframework/azure-client\";\nimport { AttachState } from \"@fluidframework/container-definitions\";\nimport { ConnectionState } from \"@fluidframework/container-loader\";\nimport type { ContainerSchema, IFluidContainer } from \"@fluidframework/fluid-static\";\n// eslint-disable-next-line import/no-internal-modules\nimport { ExperimentalPresenceManager } from \"@fluidframework/presence/alpha\";\nimport {\n\tgetPresence,\n\ttype Attendee,\n\ttype Presence,\n\t// eslint-disable-next-line import/no-internal-modules\n} from \"@fluidframework/presence/beta\";\nimport { InsecureTokenProvider } from \"@fluidframework/test-runtime-utils/internal\";\nimport { timeoutPromise } from \"@fluidframework/test-utils/internal\";\n\nimport type { ScopeType } from \"../AzureClientFactory.js\";\nimport { createAzureTokenProvider } from \"../AzureTokenFactory.js\";\nimport type { configProvider } from \"../utils.js\";\n\nimport type { MessageFromChild, MessageToChild } from \"./messageTypes.js\";\n\ntype MessageFromParent = MessageToChild;\ntype MessageToParent = Required<MessageFromChild>;\ninterface UserIdAndName {\n\tid: string;\n\tname: string;\n}\n\nconst connectTimeoutMs = 10_000;\n// Identifier given to child process\nconst process_id = process.argv[2];\n\nconst useAzure = process.env.FLUID_CLIENT === \"azure\";\nconst tenantId = useAzure\n\t? (process.env.azure__fluid__relay__service__tenantId as string)\n\t: \"frs-client-tenant\";\nconst endPoint = process.env.azure__fluid__relay__service__endpoint as string;\nif (useAzure && endPoint === undefined) {\n\tthrow new Error(\"Azure Fluid Relay service endpoint is missing\");\n}\n\n/**\n * Get or create a Fluid container with Presence in initialObjects.\n */\nconst getOrCreatePresenceContainer = async (\n\tid: string | undefined,\n\tuser: UserIdAndName,\n\tconfig?: ReturnType<typeof configProvider>,\n\tscopes?: ScopeType[],\n): Promise<{\n\tcontainer: IFluidContainer;\n\tpresence: Presence;\n\tservices: AzureContainerServices;\n\tclient: AzureClient;\n\tcontainerId: string;\n}> => {\n\tlet container: IFluidContainer;\n\tlet containerId: string;\n\tconst connectionProps: AzureRemoteConnectionConfig | AzureLocalConnectionConfig = useAzure\n\t\t? {\n\t\t\t\ttenantId,\n\t\t\t\ttokenProvider: createAzureTokenProvider(user.id ?? \"foo\", user.name ?? \"bar\", scopes),\n\t\t\t\tendpoint: endPoint,\n\t\t\t\ttype: \"remote\",\n\t\t\t}\n\t\t: {\n\t\t\t\ttokenProvider: new InsecureTokenProvider(\"fooBar\", user, scopes),\n\t\t\t\tendpoint: \"http://localhost:7071\",\n\t\t\t\ttype: \"local\",\n\t\t\t};\n\tconst client = new AzureClient({ connection: connectionProps });\n\tconst schema: ContainerSchema = {\n\t\tinitialObjects: {\n\t\t\t// A DataObject is added as otherwise fluid-static complains \"Container cannot be initialized without any DataTypes\"\n\t\t\t_unused: ExperimentalPresenceManager,\n\t\t},\n\t};\n\tlet services: AzureContainerServices;\n\tif (id === undefined) {\n\t\t({ container, services } = await client.createContainer(schema, \"2\"));\n\t\tcontainerId = await container.attach();\n\t} else {\n\t\tcontainerId = id;\n\t\t({ container, services } = await client.getContainer(containerId, schema, \"2\"));\n\t}\n\t// wait for 'ConnectionState.Connected' so we return with client connected to container\n\tif (container.connectionState !== ConnectionState.Connected) {\n\t\tawait timeoutPromise((resolve) => container.once(\"connected\", () => resolve()), {\n\t\t\tdurationMs: connectTimeoutMs,\n\t\t\terrorMsg: \"container connect() timeout\",\n\t\t});\n\t}\n\tassert.strictEqual(\n\t\tcontainer.attachState,\n\t\tAttachState.Attached,\n\t\t\"Container is not attached after attach is called\",\n\t);\n\n\tconst presence = getPresence(container);\n\treturn {\n\t\tclient,\n\t\tcontainer,\n\t\tpresence,\n\t\tservices,\n\t\tcontainerId,\n\t};\n};\nfunction createSendFunction(): (msg: MessageToParent) => void {\n\tif (process.send) {\n\t\treturn process.send.bind(process);\n\t}\n\tthrow new Error(\"process.send is not defined\");\n}\n\nconst send = createSendFunction();\n\nfunction isConnected(container: IFluidContainer | undefined): boolean {\n\treturn container !== undefined && container.connectionState === ConnectionState.Connected;\n}\n\nclass MessageHandler {\n\tpublic presence: Presence | undefined;\n\tpublic container: IFluidContainer | undefined;\n\tpublic containerId: string | undefined;\n\n\tpublic async onMessage(msg: MessageFromParent): Promise<void> {\n\t\tswitch (msg.command) {\n\t\t\tcase \"ping\": {\n\t\t\t\tsend({ event: \"ack\" });\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\t// Respond to connect command by connecting to Fluid container with the provided user information.\n\t\t\tcase \"connect\": {\n\t\t\t\t// Check if valid user information has been provided by parent/orchestrator\n\t\t\t\tif (!msg.user) {\n\t\t\t\t\tsend({ event: \"error\", error: `${process_id}: No azure user information given` });\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\t// Check if already connected to container\n\t\t\t\tif (isConnected(this.container)) {\n\t\t\t\t\tsend({ event: \"error\", error: `${process_id}: Already connected to container` });\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tconst { container, presence, containerId } = await getOrCreatePresenceContainer(\n\t\t\t\t\tmsg.containerId,\n\t\t\t\t\tmsg.user,\n\t\t\t\t);\n\t\t\t\tthis.container = container;\n\t\t\t\tthis.presence = presence;\n\t\t\t\tthis.containerId = containerId;\n\n\t\t\t\t// Listen for presence events to notify parent/orchestrator when a new attendee joins or leaves the session.\n\t\t\t\tpresence.attendees.events.on(\"attendeeConnected\", (attendee: Attendee) => {\n\t\t\t\t\tconst m: MessageToParent = {\n\t\t\t\t\t\tevent: \"attendeeConnected\",\n\t\t\t\t\t\tattendeeId: attendee.attendeeId,\n\t\t\t\t\t};\n\t\t\t\t\tsend(m);\n\t\t\t\t});\n\t\t\t\tpresence.attendees.events.on(\"attendeeDisconnected\", (attendee: Attendee) => {\n\t\t\t\t\tconst m: MessageToParent = {\n\t\t\t\t\t\tevent: \"attendeeDisconnected\",\n\t\t\t\t\t\tattendeeId: attendee.attendeeId,\n\t\t\t\t\t};\n\t\t\t\t\tsend(m);\n\t\t\t\t});\n\t\t\t\tsend({\n\t\t\t\t\tevent: \"connected\",\n\t\t\t\t\tcontainerId,\n\t\t\t\t\tattendeeId: presence.attendees.getMyself().attendeeId,\n\t\t\t\t});\n\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\t// Respond to disconnect command by disconnecting self from Fluid container.\n\t\t\tcase \"disconnectSelf\": {\n\t\t\t\tif (!this.container) {\n\t\t\t\t\tsend({ event: \"error\", error: `${process_id} is not connected to container` });\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tif (!this.presence) {\n\t\t\t\t\tsend({ event: \"error\", error: `${process_id} is not connected to presence` });\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\n\t\t\t\tthis.container.disconnect();\n\t\t\t\tsend({\n\t\t\t\t\tevent: \"disconnectedSelf\",\n\t\t\t\t\tattendeeId: this.presence.attendees.getMyself().attendeeId,\n\t\t\t\t});\n\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tdefault: {\n\t\t\t\tconsole.error(`${process_id}: Unknown command`);\n\t\t\t\tsend({ event: \"error\", error: `${process_id} Unknown command` });\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunction setupMessageHandler(): void {\n\tconst messageHandler = new MessageHandler();\n\tprocess.on(\"message\", (msg: MessageFromParent) => {\n\t\tmessageHandler.onMessage(msg).catch((error: Error) => {\n\t\t\tconsole.error(`Error in client ${process_id}`, error);\n\t\t\tsend({ event: \"error\", error: `${process_id}: ${error.message}` });\n\t\t});\n\t});\n}\n\nsetupMessageHandler();\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"messageTypes.js","sourceRoot":"","sources":["../../../src/test/multiprocess/messageTypes.ts"],"names":[],"mappings":"AAAA;;;GAGG","sourcesContent":["/*!\n * Copyright (c) Microsoft Corporation and contributors. All rights reserved.\n * Licensed under the MIT License.\n */\n\nimport type { AzureUser } from \"@fluidframework/azure-client/internal\";\n// eslint-disable-next-line import/no-internal-modules\nimport type { AttendeeId } from \"@fluidframework/presence/beta\";\n\nexport type MessageToChild = ConnectCommand | DisconnectSelfCommand;\ninterface ConnectCommand {\n\tcommand: \"connect\";\n\tuser: AzureUser;\n\tcontainerId?: string;\n}\n\ninterface DisconnectSelfCommand {\n\tcommand: \"disconnectSelf\";\n}\n\nexport type MessageFromChild =\n\t| AttendeeDisconnectedEvent\n\t| attendeeConnectedEvent\n\t| ReadyEvent\n\t| DisconnectedSelfEvent\n\t| ErrorEvent;\ninterface AttendeeDisconnectedEvent {\n\tevent: \"attendeeDisconnected\";\n\tattendeeId: AttendeeId;\n}\n\ninterface attendeeConnectedEvent {\n\tevent: \"attendeeConnected\";\n\tattendeeId: AttendeeId;\n}\n\ninterface ReadyEvent {\n\tevent: \"ready\";\n\tcontainerId: string;\n\tattendeeId: AttendeeId;\n}\n\ninterface DisconnectedSelfEvent {\n\tevent: \"disconnectedSelf\";\n\tattendeeId: AttendeeId;\n}\ninterface ErrorEvent {\n\tevent: \"error\";\n\terror: string;\n}\n"]}
1
+ {"version":3,"file":"messageTypes.js","sourceRoot":"","sources":["../../../src/test/multiprocess/messageTypes.ts"],"names":[],"mappings":"AAAA;;;GAGG","sourcesContent":["/*!\n * Copyright (c) Microsoft Corporation and contributors. All rights reserved.\n * Licensed under the MIT License.\n */\n\nimport type { AzureUser } from \"@fluidframework/azure-client/internal\";\n// eslint-disable-next-line import/no-internal-modules\nimport type { AttendeeId } from \"@fluidframework/presence/beta\";\n\n/**\n * Message types sent from the orchestrator to the child processes\n */\nexport type MessageToChild = ConnectCommand | DisconnectSelfCommand | PingCommand;\n\n/**\n * Can be sent to check child responsiveness.\n * An {@link AcknowledgeEvent} should be expected in response.\n */\ninterface PingCommand {\n\tcommand: \"ping\";\n}\n\n/**\n * Instructs a child process to connect to a Fluid container.\n * A {@link ConnectedEvent} should be expected in response.\n */\nexport interface ConnectCommand {\n\tcommand: \"connect\";\n\tuser: AzureUser;\n\t/**\n\t * The ID of the Fluid container to connect to.\n\t * If not provided, a new Fluid container will be created.\n\t */\n\tcontainerId?: string;\n}\n\n/**\n * Instructs a child process to disconnect from a Fluid container.\n * A {@link DisconnectedSelfEvent} should be expected in response.\n */\ninterface DisconnectSelfCommand {\n\tcommand: \"disconnectSelf\";\n}\n\n/**\n * Message types sent from the child processes to the orchestrator\n */\nexport type MessageFromChild =\n\t| AcknowledgeEvent\n\t| AttendeeConnectedEvent\n\t| AttendeeDisconnectedEvent\n\t| ConnectedEvent\n\t| DisconnectedSelfEvent\n\t| ErrorEvent;\n\n/**\n * Sent from the child processes to the orchestrator in response to a {@link PingCommand}.\n */\ninterface AcknowledgeEvent {\n\tevent: \"ack\";\n}\n\n/**\n * Sent arbitrarily to indicate a new attendee has connected.\n */\ninterface AttendeeConnectedEvent {\n\tevent: \"attendeeConnected\";\n\tattendeeId: AttendeeId;\n}\n\n/**\n * Sent arbitrarily to indicate an attendee has disconnected.\n */\ninterface AttendeeDisconnectedEvent {\n\tevent: \"attendeeDisconnected\";\n\tattendeeId: AttendeeId;\n}\n\n/**\n * Sent from the child processes to the orchestrator in response to a {@link ConnectCommand}.\n */\ninterface ConnectedEvent {\n\tevent: \"connected\";\n\tcontainerId: string;\n\tattendeeId: AttendeeId;\n}\n\n/**\n * Sent from the child processes to the orchestrator in response to a {@link DisconnectSelfCommand}.\n */\ninterface DisconnectedSelfEvent {\n\tevent: \"disconnectedSelf\";\n\tattendeeId: AttendeeId;\n}\n\n/**\n * Sent at any time to indicate an error.\n */\ninterface ErrorEvent {\n\tevent: \"error\";\n\terror: string;\n}\n"]}
@@ -4,12 +4,15 @@
4
4
  */
5
5
  import { strict as assert } from "node:assert";
6
6
  import { fork } from "node:child_process";
7
- import { timeoutPromise } from "@fluidframework/test-utils/internal";
7
+ import { timeoutAwait, timeoutPromise } from "@fluidframework/test-utils/internal";
8
8
  /**
9
9
  * This test suite is a prototype for a multi-process end to end test for Fluid using the new Presence API on AzureClient.
10
10
  * In the future we hope to expand and generalize this pattern to broadly test more Fluid features.
11
- * Currently our E2E tests are limited to running multiple clients on a single process which does not effectively
12
- * simulate real-world production scenarios where clients are usually running on different machines.
11
+ * Other E2E tests are limited to running multiple clients on a single process which does not effectively
12
+ * simulate real-world production scenarios where clients are usually running on different machines. Since
13
+ * the Fluid Framework client is designed to carry most of the work burden, multi-process testing from a
14
+ * single machine is also not representative but does at least work past some limitations of a single
15
+ * Node.js process handling multiple clients.
13
16
  *
14
17
  * The pattern demonstrated in this test suite is as follows:
15
18
  *
@@ -26,122 +29,218 @@ import { timeoutPromise } from "@fluidframework/test-utils/internal";
26
29
  * - Listen for command messages from the orchestrator.
27
30
  * - Perform the requested action.
28
31
  * - Send response messages including any relevant data back to the orchestrator to verify expected behavior.
32
+ */
33
+ /**
34
+ * Fork child processes to simulate multiple Fluid clients.
29
35
  *
30
- * This particular test suite tests the following E2E functionality for Presence:
31
- * - Announce 'attendeeConnected' when remote client joins session.
32
- * - Announce 'attendeeDisconnected' when remote client disconnects.
36
+ * @remarks
37
+ * Individual child processes may be scheduled concurrently on a multi-core CPU
38
+ * and separate processes will never share a port when connected to a service.
39
+ *
40
+ * @param numProcesses - The number of child processes to fork.
41
+ * @param cleanUpAccumulator - An array to accumulate cleanup functions for
42
+ * each child process. This is build per instance to accommodate any errors
43
+ * that might occur before completing all forking.
44
+ *
45
+ * @returns A promise that resolves with an object containing the child
46
+ * processes and a promise that rejects on any child process errors.
33
47
  */
34
- describe(`Presence with AzureClient`, () => {
35
- const numClients = 5; // Set the total number of Fluid clients to create
36
- assert(numClients > 1, "Must have at least two clients");
37
- let children = [];
38
- // This promise is used to capture all errors that occur in the child processes.
39
- // It will never resolve successfully, it is only used to reject on child process error.
40
- let childErrorPromise;
41
- // Timeout duration used when waiting for response messages from child processes.
42
- const durationMs = 10_000;
43
- const afterCleanUp = [];
44
- async function connectChildProcesses(childProcesses) {
45
- let containerIdPromise;
46
- let containerCreatorSessionId;
47
- for (const [index, child] of childProcesses.entries()) {
48
- const user = { id: `test-user-id-${index}`, name: `test-user-name-${index}` };
49
- const message = { command: "connect", user };
50
- if (containerIdPromise === undefined) {
51
- // Create a promise that resolves with the containerId from the created container.
52
- containerIdPromise = timeoutPromise((resolve, reject) => {
53
- child.once("message", (msg) => {
54
- if (msg.event === "ready" && msg.containerId) {
55
- containerCreatorSessionId = msg.attendeeId;
56
- resolve(msg.containerId);
57
- }
58
- else {
59
- reject(new Error(`Non-ready message from child0: ${JSON.stringify(msg)}`));
60
- }
61
- });
62
- }, {
63
- durationMs,
64
- errorMsg: "did not receive 'ready' from child process",
48
+ async function forkChildProcesses(numProcesses, cleanUpAccumulator) {
49
+ const children = [];
50
+ const childReadyPromises = [];
51
+ // Collect all child process error promises into this array
52
+ const childErrorPromises = [];
53
+ // Fork child processes
54
+ for (let i = 0; i < numProcesses; i++) {
55
+ const child = fork("./lib/test/multiprocess/childClient.js", [
56
+ `child${i}` /* identifier passed to child process */,
57
+ ]);
58
+ // Register a cleanup function to kill the child process
59
+ cleanUpAccumulator.push(() => {
60
+ child.kill();
61
+ child.removeAllListeners();
62
+ });
63
+ const readyPromise = new Promise((resolve, reject) => {
64
+ child.once("message", (msg) => {
65
+ if (msg.event === "ack") {
66
+ resolve();
67
+ }
68
+ else {
69
+ reject(new Error(`Unexpected (non-"ack") message from child${i}: ${JSON.stringify(msg)}`));
70
+ }
71
+ });
72
+ });
73
+ childReadyPromises.push(readyPromise);
74
+ const errorPromise = new Promise((_, reject) => {
75
+ child.on("error", (error) => {
76
+ reject(new Error(`Child${i} process errored: ${error.message}`));
77
+ });
78
+ });
79
+ childErrorPromises.push(errorPromise);
80
+ child.send({ command: "ping" });
81
+ children.push(child);
82
+ }
83
+ // This race will be used to reject any of the following tests on any child process errors
84
+ const childErrorPromise = Promise.race(childErrorPromises);
85
+ // All children are always expected to connect successfully and acknowledge the ping.
86
+ await Promise.race([Promise.all(childReadyPromises), childErrorPromise]);
87
+ return {
88
+ children,
89
+ childErrorPromise,
90
+ };
91
+ }
92
+ function composeConnectMessage(id) {
93
+ return {
94
+ command: "connect",
95
+ user: {
96
+ id: `test-user-id-${id}`,
97
+ name: `test-user-name-${id}`,
98
+ },
99
+ };
100
+ }
101
+ async function connectChildProcesses(childProcesses, readyTimeoutMs) {
102
+ if (childProcesses.length === 0) {
103
+ throw new Error("No child processes provided for connection.");
104
+ }
105
+ const firstChild = childProcesses[0];
106
+ const containerReadyPromise = new Promise((resolve, reject) => {
107
+ firstChild.once("message", (msg) => {
108
+ if (msg.event === "connected" && msg.containerId) {
109
+ resolve({
110
+ containerCreatorAttendeeId: msg.attendeeId,
111
+ containerId: msg.containerId,
65
112
  });
66
113
  }
67
114
  else {
68
- // For subsequent children, wait for containerId from the promise only when needed.
69
- message.containerId = await containerIdPromise;
115
+ reject(new Error(`Non-connected message from child0: ${JSON.stringify(msg)}`));
70
116
  }
71
- child.send(message);
72
- }
73
- return containerCreatorSessionId;
117
+ });
118
+ });
119
+ {
120
+ firstChild.send(composeConnectMessage(0));
74
121
  }
75
- beforeEach("setup", async () => {
76
- // Collect all child process error promises into this array
77
- const childErrorPromises = [];
78
- // Fork child processes
79
- for (let i = 0; i < numClients; i++) {
80
- const child = fork("./lib/test/multiprocess/childClient.js", [
81
- `child${i}` /* identifier passed to child process */,
82
- ]);
83
- const errorPromise = new Promise((_, reject) => {
84
- child.on("error", (error) => {
85
- reject(new Error(`Child${i} process errored: ${error.message}`));
86
- });
87
- });
88
- childErrorPromises.push(errorPromise);
89
- children.push(child);
90
- // Register cleanup for the child process listeners.
91
- afterCleanUp.push(() => child.removeAllListeners());
122
+ const { containerCreatorAttendeeId, containerId } = await timeoutAwait(containerReadyPromise, {
123
+ durationMs: readyTimeoutMs,
124
+ errorMsg: "did not receive 'connected' from child process",
125
+ });
126
+ const attendeeIdPromises = [];
127
+ for (const [index, child] of childProcesses.entries()) {
128
+ if (index === 0) {
129
+ // The first child process is the container creator, it has already sent the 'connected' message.
130
+ attendeeIdPromises.push(Promise.resolve(containerCreatorAttendeeId));
131
+ continue;
92
132
  }
93
- // This race will be used to reject any of the following tests on any child process errors
94
- childErrorPromise = Promise.race(childErrorPromises);
133
+ const message = composeConnectMessage(index);
134
+ // For subsequent children, send containerId but do not wait for a response.
135
+ message.containerId = containerId;
136
+ attendeeIdPromises.push(new Promise((resolve, reject) => {
137
+ child.once("message", (msg) => {
138
+ if (msg.event === "connected") {
139
+ resolve(msg.attendeeId);
140
+ }
141
+ else if (msg.event === "error") {
142
+ reject(new Error(`Child process error: ${msg.error}`));
143
+ }
144
+ });
145
+ }));
146
+ child.send(message);
147
+ }
148
+ if (containerCreatorAttendeeId === undefined) {
149
+ throw new Error("No container creator session ID received from child processes.");
150
+ }
151
+ return { containerCreatorAttendeeId, attendeeIdPromises };
152
+ }
153
+ /**
154
+ * Connects the child processes and waits for the specified number of attendees to connect.
155
+ * @remarks
156
+ * This function can be used directly as a test. Comments in the functionality describe the
157
+ * breakdown of test blocks.
158
+ *
159
+ * @param children - Array of child processes to connect.
160
+ * @param attendeeCountRequired - The number of attendees that must connect.
161
+ * @param childConnectTimeoutMs - Timeout duration for child process connections.
162
+ * @param attendeesJoinedTimeoutMs - Timeout duration for required attendees to join.
163
+ * @param earlyExitPromise - Promise that resolves/rejects when the test should early exit.
164
+ */
165
+ async function connectAndWaitForAttendees(children, attendeeCountRequired, childConnectTimeoutMs, attendeesJoinedTimeoutMs, earlyExitPromise = Promise.resolve()) {
166
+ // Setup
167
+ const attendeeConnectedPromise = new Promise((resolve) => {
168
+ let attendeesJoinedEvents = 0;
169
+ children[0].on("message", (msg) => {
170
+ if (msg.event === "attendeeConnected") {
171
+ attendeesJoinedEvents++;
172
+ if (attendeesJoinedEvents >= attendeeCountRequired) {
173
+ resolve();
174
+ }
175
+ }
176
+ });
177
+ });
178
+ // Act - connect all child processes
179
+ const connectResult = await connectChildProcesses(children, childConnectTimeoutMs);
180
+ Promise.all(connectResult.attendeeIdPromises)
181
+ .then(() => console.log("All attendees connected."))
182
+ .catch((error) => {
183
+ console.error("Error connecting children:", error);
95
184
  });
96
- // After each test, kill each child process and call any cleanup functions that were registered
185
+ // Verify - wait for all 'attendeeConnected' events
186
+ await timeoutAwait(Promise.race([attendeeConnectedPromise, earlyExitPromise]), {
187
+ durationMs: attendeesJoinedTimeoutMs,
188
+ errorMsg: "did not receive all 'attendeeConnected' events",
189
+ });
190
+ return connectResult;
191
+ }
192
+ /**
193
+ * This particular test suite tests the following E2E functionality for Presence:
194
+ * - Announce 'attendeeConnected' when remote client joins session.
195
+ * - Announce 'attendeeDisconnected' when remote client disconnects.
196
+ */
197
+ describe(`Presence with AzureClient`, () => {
198
+ const afterCleanUp = [];
199
+ // After each test, call any cleanup functions that were registered (kill each child process)
97
200
  afterEach(async () => {
98
- for (const child of children) {
99
- child.kill();
100
- }
101
- children = [];
102
201
  for (const cleanUp of afterCleanUp) {
103
202
  cleanUp();
104
203
  }
105
204
  afterCleanUp.length = 0;
106
205
  });
107
- it("announces 'attendeeConnected' when remote client joins session and 'attendeeDisconnected' when remote client disconnects", async () => {
108
- // Setup
109
- const attendeeConnectedPromise = timeoutPromise((resolve) => {
110
- let attendeesJoinedEvents = 0;
111
- children[0].on("message", (msg) => {
112
- if (msg.event === "attendeeConnected") {
113
- attendeesJoinedEvents++;
114
- if (attendeesJoinedEvents === numClients - 1) {
115
- resolve();
116
- }
117
- }
118
- });
119
- }, {
120
- durationMs,
121
- errorMsg: "did not receive all 'attendeeConnected' events",
206
+ // Note that on slower systems 50+ clients may take too long to join.
207
+ const numClientsForAttendeeTests = [5, 20, 50, 100];
208
+ // TODO: AB#45620: "Presence: perf: update Join pattern for scale" may help, then remove .slice.
209
+ for (const numClients of numClientsForAttendeeTests.slice(0, 2)) {
210
+ assert(numClients > 1, "Must have at least two clients");
211
+ // Timeout duration used when waiting for response messages from child processes.
212
+ const childConnectTimeoutMs = 1000 * numClients;
213
+ const allConnectedTimeoutMs = 2000;
214
+ it(`announces 'attendeeConnected' when remote client joins session [${numClients} clients]`, async () => {
215
+ // Setup
216
+ const { children, childErrorPromise } = await forkChildProcesses(numClients, afterCleanUp);
217
+ // Further Setup with Act and Verify
218
+ await connectAndWaitForAttendees(children, numClients - 1, childConnectTimeoutMs, allConnectedTimeoutMs, childErrorPromise);
122
219
  });
123
- // Act - connect all child processes
124
- const creatorSessionId = await connectChildProcesses(children);
125
- // Verify - wait for all 'attendeeConnected' events
126
- await Promise.race([attendeeConnectedPromise, childErrorPromise]);
127
- // Setup
128
- const waitForDisconnected = children
129
- .filter((_, index) => index !== 0)
130
- .map(async (child, index) => timeoutPromise((resolve) => {
131
- child.on("message", (msg) => {
132
- if (msg.event === "attendeeDisconnected" &&
133
- msg.attendeeId === creatorSessionId) {
134
- resolve();
135
- }
136
- });
137
- }, {
138
- durationMs,
139
- errorMsg: `Attendee[${index}] Disconnected Timeout`,
140
- }));
141
- // Act - disconnect first child process
142
- children[0].send({ command: "disconnectSelf" });
143
- // Verify - wait for all 'attendeeDisconnected' events
144
- await Promise.race([Promise.all(waitForDisconnected), childErrorPromise]);
145
- });
220
+ it(`announces 'attendeeDisconnected' when remote client disconnects [${numClients} clients]`, async () => {
221
+ // Setup
222
+ const { children, childErrorPromise } = await forkChildProcesses(numClients, afterCleanUp);
223
+ const connectResult = await connectAndWaitForAttendees(children, numClients - 1, childConnectTimeoutMs, allConnectedTimeoutMs, childErrorPromise);
224
+ const childDisconnectTimeoutMs = 10_000;
225
+ const waitForDisconnected = children.map(async (child, index) => index === 0
226
+ ? Promise.resolve()
227
+ : timeoutPromise((resolve) => {
228
+ child.on("message", (msg) => {
229
+ if (msg.event === "attendeeDisconnected" &&
230
+ msg.attendeeId === connectResult.containerCreatorAttendeeId) {
231
+ console.log(`Child[${index}] saw creator disconnect`);
232
+ resolve();
233
+ }
234
+ });
235
+ }, {
236
+ durationMs: childDisconnectTimeoutMs,
237
+ errorMsg: `Attendee[${index}] Disconnected Timeout`,
238
+ }));
239
+ // Act - disconnect first child process
240
+ children[0].send({ command: "disconnectSelf" });
241
+ // Verify - wait for all 'attendeeDisconnected' events
242
+ await Promise.race([Promise.all(waitForDisconnected), childErrorPromise]);
243
+ });
244
+ }
146
245
  });
147
246
  //# sourceMappingURL=presenceTest.spec.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"presenceTest.spec.js","sourceRoot":"","sources":["../../../src/test/multiprocess/presenceTest.spec.ts"],"names":[],"mappings":"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","sourcesContent":["/*!\n * Copyright (c) Microsoft Corporation and contributors. All rights reserved.\n * Licensed under the MIT License.\n */\n\nimport { strict as assert } from \"node:assert\";\nimport { fork, type ChildProcess } from \"node:child_process\";\n\nimport { timeoutPromise } from \"@fluidframework/test-utils/internal\";\n\nimport type { MessageFromChild, MessageToChild } from \"./messageTypes.js\";\n\n/**\n * This test suite is a prototype for a multi-process end to end test for Fluid using the new Presence API on AzureClient.\n * In the future we hope to expand and generalize this pattern to broadly test more Fluid features.\n * Currently our E2E tests are limited to running multiple clients on a single process which does not effectively\n * simulate real-world production scenarios where clients are usually running on different machines.\n *\n * The pattern demonstrated in this test suite is as follows:\n *\n * This main test file acts as the 'Orchestrator'. The orchestrator's job includes:\n * - Fork child processes to simulate multiple Fluid clients\n * - Send command messages to child clients to perform specific Fluid actions.\n * - Receive response messages from child clients to verify expected behavior.\n * - Clean up child processes after each test.\n *\n * The child processes are located in the `childClient.ts` file. Each child process simulates a Fluid client.\n *\n * The child client's job includes:\n * - Create/Get + connect to Fluid container.\n * - Listen for command messages from the orchestrator.\n * - Perform the requested action.\n * - Send response messages including any relevant data back to the orchestrator to verify expected behavior.\n *\n * This particular test suite tests the following E2E functionality for Presence:\n * - Announce 'attendeeConnected' when remote client joins session.\n * - Announce 'attendeeDisconnected' when remote client disconnects.\n */\ndescribe(`Presence with AzureClient`, () => {\n\tconst numClients = 5; // Set the total number of Fluid clients to create\n\tassert(numClients > 1, \"Must have at least two clients\");\n\tlet children: ChildProcess[] = [];\n\t// This promise is used to capture all errors that occur in the child processes.\n\t// It will never resolve successfully, it is only used to reject on child process error.\n\tlet childErrorPromise: Promise<void>;\n\t// Timeout duration used when waiting for response messages from child processes.\n\tconst durationMs = 10_000;\n\n\tconst afterCleanUp: (() => void)[] = [];\n\n\tasync function connectChildProcesses(\n\t\tchildProcesses: ChildProcess[],\n\t): Promise<string | undefined> {\n\t\tlet containerIdPromise: Promise<string> | undefined;\n\t\tlet containerCreatorSessionId: string | undefined;\n\t\tfor (const [index, child] of childProcesses.entries()) {\n\t\t\tconst user = { id: `test-user-id-${index}`, name: `test-user-name-${index}` };\n\t\t\tconst message: MessageToChild = { command: \"connect\", user };\n\n\t\t\tif (containerIdPromise === undefined) {\n\t\t\t\t// Create a promise that resolves with the containerId from the created container.\n\t\t\t\tcontainerIdPromise = timeoutPromise<string>(\n\t\t\t\t\t(resolve, reject) => {\n\t\t\t\t\t\tchild.once(\"message\", (msg: MessageFromChild) => {\n\t\t\t\t\t\t\tif (msg.event === \"ready\" && msg.containerId) {\n\t\t\t\t\t\t\t\tcontainerCreatorSessionId = msg.attendeeId;\n\t\t\t\t\t\t\t\tresolve(msg.containerId);\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\treject(new Error(`Non-ready message from child0: ${JSON.stringify(msg)}`));\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t});\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tdurationMs,\n\t\t\t\t\t\terrorMsg: \"did not receive 'ready' from child process\",\n\t\t\t\t\t},\n\t\t\t\t);\n\t\t\t} else {\n\t\t\t\t// For subsequent children, wait for containerId from the promise only when needed.\n\t\t\t\tmessage.containerId = await containerIdPromise;\n\t\t\t}\n\n\t\t\tchild.send(message);\n\t\t}\n\t\treturn containerCreatorSessionId;\n\t}\n\n\tbeforeEach(\"setup\", async () => {\n\t\t// Collect all child process error promises into this array\n\t\tconst childErrorPromises: Promise<void>[] = [];\n\t\t// Fork child processes\n\t\tfor (let i = 0; i < numClients; i++) {\n\t\t\tconst child = fork(\"./lib/test/multiprocess/childClient.js\", [\n\t\t\t\t`child${i}` /* identifier passed to child process */,\n\t\t\t]);\n\t\t\tconst errorPromise = new Promise<void>((_, reject) => {\n\t\t\t\tchild.on(\"error\", (error) => {\n\t\t\t\t\treject(new Error(`Child${i} process errored: ${error.message}`));\n\t\t\t\t});\n\t\t\t});\n\t\t\tchildErrorPromises.push(errorPromise);\n\t\t\tchildren.push(child);\n\t\t\t// Register cleanup for the child process listeners.\n\t\t\tafterCleanUp.push(() => child.removeAllListeners());\n\t\t}\n\t\t// This race will be used to reject any of the following tests on any child process errors\n\t\tchildErrorPromise = Promise.race(childErrorPromises);\n\t});\n\n\t// After each test, kill each child process and call any cleanup functions that were registered\n\tafterEach(async () => {\n\t\tfor (const child of children) {\n\t\t\tchild.kill();\n\t\t}\n\t\tchildren = [];\n\t\tfor (const cleanUp of afterCleanUp) {\n\t\t\tcleanUp();\n\t\t}\n\t\tafterCleanUp.length = 0;\n\t});\n\n\tit(\"announces 'attendeeConnected' when remote client joins session and 'attendeeDisconnected' when remote client disconnects\", async () => {\n\t\t// Setup\n\t\tconst attendeeConnectedPromise = timeoutPromise(\n\t\t\t(resolve) => {\n\t\t\t\tlet attendeesJoinedEvents = 0;\n\t\t\t\tchildren[0].on(\"message\", (msg: MessageFromChild) => {\n\t\t\t\t\tif (msg.event === \"attendeeConnected\") {\n\t\t\t\t\t\tattendeesJoinedEvents++;\n\t\t\t\t\t\tif (attendeesJoinedEvents === numClients - 1) {\n\t\t\t\t\t\t\tresolve();\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\t},\n\t\t\t{\n\t\t\t\tdurationMs,\n\t\t\t\terrorMsg: \"did not receive all 'attendeeConnected' events\",\n\t\t\t},\n\t\t);\n\n\t\t// Act - connect all child processes\n\t\tconst creatorSessionId = await connectChildProcesses(children);\n\n\t\t// Verify - wait for all 'attendeeConnected' events\n\t\tawait Promise.race([attendeeConnectedPromise, childErrorPromise]);\n\n\t\t// Setup\n\t\tconst waitForDisconnected = children\n\t\t\t.filter((_, index) => index !== 0)\n\t\t\t.map(async (child, index) =>\n\t\t\t\ttimeoutPromise(\n\t\t\t\t\t(resolve) => {\n\t\t\t\t\t\tchild.on(\"message\", (msg: MessageFromChild) => {\n\t\t\t\t\t\t\tif (\n\t\t\t\t\t\t\t\tmsg.event === \"attendeeDisconnected\" &&\n\t\t\t\t\t\t\t\tmsg.attendeeId === creatorSessionId\n\t\t\t\t\t\t\t) {\n\t\t\t\t\t\t\t\tresolve();\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t});\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tdurationMs,\n\t\t\t\t\t\terrorMsg: `Attendee[${index}] Disconnected Timeout`,\n\t\t\t\t\t},\n\t\t\t\t),\n\t\t\t);\n\n\t\t// Act - disconnect first child process\n\t\tchildren[0].send({ command: \"disconnectSelf\" });\n\n\t\t// Verify - wait for all 'attendeeDisconnected' events\n\t\tawait Promise.race([Promise.all(waitForDisconnected), childErrorPromise]);\n\t});\n});\n"]}
1
+ {"version":3,"file":"presenceTest.spec.js","sourceRoot":"","sources":["../../../src/test/multiprocess/presenceTest.spec.ts"],"names":[],"mappings":"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","sourcesContent":["/*!\n * Copyright (c) Microsoft Corporation and contributors. All rights reserved.\n * Licensed under the MIT License.\n */\n\nimport { strict as assert } from \"node:assert\";\nimport { fork, type ChildProcess } from \"node:child_process\";\n\n// eslint-disable-next-line import/no-internal-modules\nimport type { AttendeeId } from \"@fluidframework/presence/beta\";\nimport { timeoutAwait, timeoutPromise } from \"@fluidframework/test-utils/internal\";\n\nimport type { ConnectCommand, MessageFromChild } from \"./messageTypes.js\";\n\n/**\n * This test suite is a prototype for a multi-process end to end test for Fluid using the new Presence API on AzureClient.\n * In the future we hope to expand and generalize this pattern to broadly test more Fluid features.\n * Other E2E tests are limited to running multiple clients on a single process which does not effectively\n * simulate real-world production scenarios where clients are usually running on different machines. Since\n * the Fluid Framework client is designed to carry most of the work burden, multi-process testing from a\n * single machine is also not representative but does at least work past some limitations of a single\n * Node.js process handling multiple clients.\n *\n * The pattern demonstrated in this test suite is as follows:\n *\n * This main test file acts as the 'Orchestrator'. The orchestrator's job includes:\n * - Fork child processes to simulate multiple Fluid clients\n * - Send command messages to child clients to perform specific Fluid actions.\n * - Receive response messages from child clients to verify expected behavior.\n * - Clean up child processes after each test.\n *\n * The child processes are located in the `childClient.ts` file. Each child process simulates a Fluid client.\n *\n * The child client's job includes:\n * - Create/Get + connect to Fluid container.\n * - Listen for command messages from the orchestrator.\n * - Perform the requested action.\n * - Send response messages including any relevant data back to the orchestrator to verify expected behavior.\n */\n\n/**\n * Fork child processes to simulate multiple Fluid clients.\n *\n * @remarks\n * Individual child processes may be scheduled concurrently on a multi-core CPU\n * and separate processes will never share a port when connected to a service.\n *\n * @param numProcesses - The number of child processes to fork.\n * @param cleanUpAccumulator - An array to accumulate cleanup functions for\n * each child process. This is build per instance to accommodate any errors\n * that might occur before completing all forking.\n *\n * @returns A promise that resolves with an object containing the child\n * processes and a promise that rejects on any child process errors.\n */\nasync function forkChildProcesses(\n\tnumProcesses: number,\n\tcleanUpAccumulator: (() => void)[],\n): Promise<{\n\tchildren: ChildProcess[];\n\t/**\n\t * Will never resolve successfully, it is only used to reject on child process error.\n\t */\n\tchildErrorPromise: Promise<void>;\n}> {\n\tconst children: ChildProcess[] = [];\n\tconst childReadyPromises: Promise<void>[] = [];\n\t// Collect all child process error promises into this array\n\tconst childErrorPromises: Promise<void>[] = [];\n\t// Fork child processes\n\tfor (let i = 0; i < numProcesses; i++) {\n\t\tconst child = fork(\"./lib/test/multiprocess/childClient.js\", [\n\t\t\t`child${i}` /* identifier passed to child process */,\n\t\t]);\n\t\t// Register a cleanup function to kill the child process\n\t\tcleanUpAccumulator.push(() => {\n\t\t\tchild.kill();\n\t\t\tchild.removeAllListeners();\n\t\t});\n\t\tconst readyPromise = new Promise<void>((resolve, reject) => {\n\t\t\tchild.once(\"message\", (msg: MessageFromChild) => {\n\t\t\t\tif (msg.event === \"ack\") {\n\t\t\t\t\tresolve();\n\t\t\t\t} else {\n\t\t\t\t\treject(\n\t\t\t\t\t\tnew Error(`Unexpected (non-\"ack\") message from child${i}: ${JSON.stringify(msg)}`),\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t});\n\t\t});\n\t\tchildReadyPromises.push(readyPromise);\n\t\tconst errorPromise = new Promise<void>((_, reject) => {\n\t\t\tchild.on(\"error\", (error) => {\n\t\t\t\treject(new Error(`Child${i} process errored: ${error.message}`));\n\t\t\t});\n\t\t});\n\t\tchildErrorPromises.push(errorPromise);\n\n\t\tchild.send({ command: \"ping\" });\n\n\t\tchildren.push(child);\n\t}\n\t// This race will be used to reject any of the following tests on any child process errors\n\tconst childErrorPromise = Promise.race(childErrorPromises);\n\n\t// All children are always expected to connect successfully and acknowledge the ping.\n\tawait Promise.race([Promise.all(childReadyPromises), childErrorPromise]);\n\n\treturn {\n\t\tchildren,\n\t\tchildErrorPromise,\n\t};\n}\n\nfunction composeConnectMessage(id: string | number): ConnectCommand {\n\treturn {\n\t\tcommand: \"connect\",\n\t\tuser: {\n\t\t\tid: `test-user-id-${id}`,\n\t\t\tname: `test-user-name-${id}`,\n\t\t},\n\t};\n}\n\nasync function connectChildProcesses(\n\tchildProcesses: ChildProcess[],\n\treadyTimeoutMs: number,\n): Promise<{\n\tcontainerCreatorAttendeeId: AttendeeId;\n\tattendeeIdPromises: Promise<AttendeeId>[];\n}> {\n\tif (childProcesses.length === 0) {\n\t\tthrow new Error(\"No child processes provided for connection.\");\n\t}\n\tconst firstChild = childProcesses[0];\n\tconst containerReadyPromise = new Promise<{\n\t\tcontainerCreatorAttendeeId: AttendeeId;\n\t\tcontainerId: string;\n\t}>((resolve, reject) => {\n\t\tfirstChild.once(\"message\", (msg: MessageFromChild) => {\n\t\t\tif (msg.event === \"connected\" && msg.containerId) {\n\t\t\t\tresolve({\n\t\t\t\t\tcontainerCreatorAttendeeId: msg.attendeeId,\n\t\t\t\t\tcontainerId: msg.containerId,\n\t\t\t\t});\n\t\t\t} else {\n\t\t\t\treject(new Error(`Non-connected message from child0: ${JSON.stringify(msg)}`));\n\t\t\t}\n\t\t});\n\t});\n\t{\n\t\tfirstChild.send(composeConnectMessage(0));\n\t}\n\tconst { containerCreatorAttendeeId, containerId } = await timeoutAwait(\n\t\tcontainerReadyPromise,\n\t\t{\n\t\t\tdurationMs: readyTimeoutMs,\n\t\t\terrorMsg: \"did not receive 'connected' from child process\",\n\t\t},\n\t);\n\n\tconst attendeeIdPromises: Promise<AttendeeId>[] = [];\n\tfor (const [index, child] of childProcesses.entries()) {\n\t\tif (index === 0) {\n\t\t\t// The first child process is the container creator, it has already sent the 'connected' message.\n\t\t\tattendeeIdPromises.push(Promise.resolve(containerCreatorAttendeeId));\n\t\t\tcontinue;\n\t\t}\n\t\tconst message = composeConnectMessage(index);\n\n\t\t// For subsequent children, send containerId but do not wait for a response.\n\t\tmessage.containerId = containerId;\n\n\t\tattendeeIdPromises.push(\n\t\t\tnew Promise<AttendeeId>((resolve, reject) => {\n\t\t\t\tchild.once(\"message\", (msg: MessageFromChild) => {\n\t\t\t\t\tif (msg.event === \"connected\") {\n\t\t\t\t\t\tresolve(msg.attendeeId);\n\t\t\t\t\t} else if (msg.event === \"error\") {\n\t\t\t\t\t\treject(new Error(`Child process error: ${msg.error}`));\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\t}),\n\t\t);\n\n\t\tchild.send(message);\n\t}\n\n\tif (containerCreatorAttendeeId === undefined) {\n\t\tthrow new Error(\"No container creator session ID received from child processes.\");\n\t}\n\n\treturn { containerCreatorAttendeeId, attendeeIdPromises };\n}\n\n/**\n * Connects the child processes and waits for the specified number of attendees to connect.\n * @remarks\n * This function can be used directly as a test. Comments in the functionality describe the\n * breakdown of test blocks.\n *\n * @param children - Array of child processes to connect.\n * @param attendeeCountRequired - The number of attendees that must connect.\n * @param childConnectTimeoutMs - Timeout duration for child process connections.\n * @param attendeesJoinedTimeoutMs - Timeout duration for required attendees to join.\n * @param earlyExitPromise - Promise that resolves/rejects when the test should early exit.\n */\nasync function connectAndWaitForAttendees(\n\tchildren: ChildProcess[],\n\tattendeeCountRequired: number,\n\tchildConnectTimeoutMs: number,\n\tattendeesJoinedTimeoutMs: number,\n\tearlyExitPromise: Promise<void> = Promise.resolve(),\n): Promise<{ containerCreatorAttendeeId: AttendeeId }> {\n\t// Setup\n\tconst attendeeConnectedPromise = new Promise<void>((resolve) => {\n\t\tlet attendeesJoinedEvents = 0;\n\t\tchildren[0].on(\"message\", (msg: MessageFromChild) => {\n\t\t\tif (msg.event === \"attendeeConnected\") {\n\t\t\t\tattendeesJoinedEvents++;\n\t\t\t\tif (attendeesJoinedEvents >= attendeeCountRequired) {\n\t\t\t\t\tresolve();\n\t\t\t\t}\n\t\t\t}\n\t\t});\n\t});\n\n\t// Act - connect all child processes\n\tconst connectResult = await connectChildProcesses(children, childConnectTimeoutMs);\n\n\tPromise.all(connectResult.attendeeIdPromises)\n\t\t.then(() => console.log(\"All attendees connected.\"))\n\t\t.catch((error) => {\n\t\t\tconsole.error(\"Error connecting children:\", error);\n\t\t});\n\n\t// Verify - wait for all 'attendeeConnected' events\n\tawait timeoutAwait(Promise.race([attendeeConnectedPromise, earlyExitPromise]), {\n\t\tdurationMs: attendeesJoinedTimeoutMs,\n\t\terrorMsg: \"did not receive all 'attendeeConnected' events\",\n\t});\n\n\treturn connectResult;\n}\n\n/**\n * This particular test suite tests the following E2E functionality for Presence:\n * - Announce 'attendeeConnected' when remote client joins session.\n * - Announce 'attendeeDisconnected' when remote client disconnects.\n */\ndescribe(`Presence with AzureClient`, () => {\n\tconst afterCleanUp: (() => void)[] = [];\n\n\t// After each test, call any cleanup functions that were registered (kill each child process)\n\tafterEach(async () => {\n\t\tfor (const cleanUp of afterCleanUp) {\n\t\t\tcleanUp();\n\t\t}\n\t\tafterCleanUp.length = 0;\n\t});\n\n\t// Note that on slower systems 50+ clients may take too long to join.\n\tconst numClientsForAttendeeTests = [5, 20, 50, 100];\n\t// TODO: AB#45620: \"Presence: perf: update Join pattern for scale\" may help, then remove .slice.\n\tfor (const numClients of numClientsForAttendeeTests.slice(0, 2)) {\n\t\tassert(numClients > 1, \"Must have at least two clients\");\n\n\t\t// Timeout duration used when waiting for response messages from child processes.\n\t\tconst childConnectTimeoutMs = 1000 * numClients;\n\t\tconst allConnectedTimeoutMs = 2000;\n\n\t\tit(`announces 'attendeeConnected' when remote client joins session [${numClients} clients]`, async () => {\n\t\t\t// Setup\n\t\t\tconst { children, childErrorPromise } = await forkChildProcesses(\n\t\t\t\tnumClients,\n\t\t\t\tafterCleanUp,\n\t\t\t);\n\n\t\t\t// Further Setup with Act and Verify\n\t\t\tawait connectAndWaitForAttendees(\n\t\t\t\tchildren,\n\t\t\t\tnumClients - 1,\n\t\t\t\tchildConnectTimeoutMs,\n\t\t\t\tallConnectedTimeoutMs,\n\t\t\t\tchildErrorPromise,\n\t\t\t);\n\t\t});\n\n\t\tit(`announces 'attendeeDisconnected' when remote client disconnects [${numClients} clients]`, async () => {\n\t\t\t// Setup\n\t\t\tconst { children, childErrorPromise } = await forkChildProcesses(\n\t\t\t\tnumClients,\n\t\t\t\tafterCleanUp,\n\t\t\t);\n\n\t\t\tconst connectResult = await connectAndWaitForAttendees(\n\t\t\t\tchildren,\n\t\t\t\tnumClients - 1,\n\t\t\t\tchildConnectTimeoutMs,\n\t\t\t\tallConnectedTimeoutMs,\n\t\t\t\tchildErrorPromise,\n\t\t\t);\n\n\t\t\tconst childDisconnectTimeoutMs = 10_000;\n\n\t\t\tconst waitForDisconnected = children.map(async (child, index) =>\n\t\t\t\tindex === 0\n\t\t\t\t\t? Promise.resolve()\n\t\t\t\t\t: timeoutPromise(\n\t\t\t\t\t\t\t(resolve) => {\n\t\t\t\t\t\t\t\tchild.on(\"message\", (msg: MessageFromChild) => {\n\t\t\t\t\t\t\t\t\tif (\n\t\t\t\t\t\t\t\t\t\tmsg.event === \"attendeeDisconnected\" &&\n\t\t\t\t\t\t\t\t\t\tmsg.attendeeId === connectResult.containerCreatorAttendeeId\n\t\t\t\t\t\t\t\t\t) {\n\t\t\t\t\t\t\t\t\t\tconsole.log(`Child[${index}] saw creator disconnect`);\n\t\t\t\t\t\t\t\t\t\tresolve();\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tdurationMs: childDisconnectTimeoutMs,\n\t\t\t\t\t\t\t\terrorMsg: `Attendee[${index}] Disconnected Timeout`,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t),\n\t\t\t);\n\n\t\t\t// Act - disconnect first child process\n\t\t\tchildren[0].send({ command: \"disconnectSelf\" });\n\n\t\t\t// Verify - wait for all 'attendeeDisconnected' events\n\t\t\tawait Promise.race([Promise.all(waitForDisconnected), childErrorPromise]);\n\t\t});\n\t}\n});\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fluidframework/azure-end-to-end-tests",
3
- "version": "2.53.0-350190",
3
+ "version": "2.53.0",
4
4
  "description": "Azure client end to end tests",
5
5
  "homepage": "https://fluidframework.com",
6
6
  "repository": {
@@ -33,29 +33,29 @@
33
33
  "temp-directory": "nyc/.nyc_output"
34
34
  },
35
35
  "dependencies": {
36
- "@fluid-experimental/data-objects": "2.53.0-350190",
37
- "@fluid-internal/client-utils": "2.53.0-350190",
38
- "@fluid-internal/mocha-test-setup": "2.53.0-350190",
39
- "@fluid-private/test-version-utils": "2.53.0-350190",
40
- "@fluidframework/aqueduct": "2.53.0-350190",
41
- "@fluidframework/azure-client": "2.53.0-350190",
36
+ "@fluid-experimental/data-objects": "~2.53.0",
37
+ "@fluid-internal/client-utils": "~2.53.0",
38
+ "@fluid-internal/mocha-test-setup": "~2.53.0",
39
+ "@fluid-private/test-version-utils": "~2.53.0",
40
+ "@fluidframework/aqueduct": "~2.53.0",
41
+ "@fluidframework/azure-client": "~2.53.0",
42
42
  "@fluidframework/azure-client-legacy": "npm:@fluidframework/azure-client@^1.2.0",
43
- "@fluidframework/container-definitions": "2.53.0-350190",
44
- "@fluidframework/container-loader": "2.53.0-350190",
45
- "@fluidframework/core-interfaces": "2.53.0-350190",
46
- "@fluidframework/counter": "2.53.0-350190",
47
- "@fluidframework/datastore-definitions": "2.53.0-350190",
48
- "@fluidframework/fluid-static": "2.53.0-350190",
49
- "@fluidframework/map": "2.53.0-350190",
43
+ "@fluidframework/container-definitions": "~2.53.0",
44
+ "@fluidframework/container-loader": "~2.53.0",
45
+ "@fluidframework/core-interfaces": "~2.53.0",
46
+ "@fluidframework/counter": "~2.53.0",
47
+ "@fluidframework/datastore-definitions": "~2.53.0",
48
+ "@fluidframework/fluid-static": "~2.53.0",
49
+ "@fluidframework/map": "~2.53.0",
50
50
  "@fluidframework/map-legacy": "npm:@fluidframework/map@^1.4.0",
51
- "@fluidframework/matrix": "2.53.0-350190",
52
- "@fluidframework/presence": "2.53.0-350190",
53
- "@fluidframework/runtime-definitions": "2.53.0-350190",
54
- "@fluidframework/sequence": "2.53.0-350190",
55
- "@fluidframework/telemetry-utils": "2.53.0-350190",
56
- "@fluidframework/test-runtime-utils": "2.53.0-350190",
57
- "@fluidframework/test-utils": "2.53.0-350190",
58
- "@fluidframework/tree": "2.53.0-350190",
51
+ "@fluidframework/matrix": "~2.53.0",
52
+ "@fluidframework/presence": "~2.53.0",
53
+ "@fluidframework/runtime-definitions": "~2.53.0",
54
+ "@fluidframework/sequence": "~2.53.0",
55
+ "@fluidframework/telemetry-utils": "~2.53.0",
56
+ "@fluidframework/test-runtime-utils": "~2.53.0",
57
+ "@fluidframework/test-utils": "~2.53.0",
58
+ "@fluidframework/tree": "~2.53.0",
59
59
  "axios": "^1.8.4",
60
60
  "cross-env": "^7.0.3",
61
61
  "mocha": "^10.8.2",
@@ -69,7 +69,7 @@
69
69
  "@biomejs/biome": "~1.9.3",
70
70
  "@fluidframework/build-common": "^2.0.3",
71
71
  "@fluidframework/build-tools": "^0.57.0",
72
- "@fluidframework/driver-definitions": "2.53.0-350190",
72
+ "@fluidframework/driver-definitions": "~2.53.0",
73
73
  "@fluidframework/eslint-config-fluid": "^5.7.4",
74
74
  "@types/mocha": "^10.0.10",
75
75
  "@types/nock": "^9.3.0",
@@ -14,13 +14,14 @@ import {
14
14
  import { AttachState } from "@fluidframework/container-definitions";
15
15
  import { ConnectionState } from "@fluidframework/container-loader";
16
16
  import type { ContainerSchema, IFluidContainer } from "@fluidframework/fluid-static";
17
+ // eslint-disable-next-line import/no-internal-modules
18
+ import { ExperimentalPresenceManager } from "@fluidframework/presence/alpha";
17
19
  import {
18
20
  getPresence,
19
21
  type Attendee,
20
- ExperimentalPresenceManager,
21
22
  type Presence,
22
23
  // eslint-disable-next-line import/no-internal-modules
23
- } from "@fluidframework/presence/alpha";
24
+ } from "@fluidframework/presence/beta";
24
25
  import { InsecureTokenProvider } from "@fluidframework/test-runtime-utils/internal";
25
26
  import { timeoutPromise } from "@fluidframework/test-utils/internal";
26
27
 
@@ -136,6 +137,11 @@ class MessageHandler {
136
137
 
137
138
  public async onMessage(msg: MessageFromParent): Promise<void> {
138
139
  switch (msg.command) {
140
+ case "ping": {
141
+ send({ event: "ack" });
142
+ break;
143
+ }
144
+
139
145
  // Respond to connect command by connecting to Fluid container with the provided user information.
140
146
  case "connect": {
141
147
  // Check if valid user information has been provided by parent/orchestrator
@@ -172,7 +178,7 @@ class MessageHandler {
172
178
  send(m);
173
179
  });
174
180
  send({
175
- event: "ready",
181
+ event: "connected",
176
182
  containerId,
177
183
  attendeeId: presence.attendees.getMyself().attendeeId,
178
184
  });
@@ -7,43 +7,95 @@ import type { AzureUser } from "@fluidframework/azure-client/internal";
7
7
  // eslint-disable-next-line import/no-internal-modules
8
8
  import type { AttendeeId } from "@fluidframework/presence/beta";
9
9
 
10
- export type MessageToChild = ConnectCommand | DisconnectSelfCommand;
11
- interface ConnectCommand {
10
+ /**
11
+ * Message types sent from the orchestrator to the child processes
12
+ */
13
+ export type MessageToChild = ConnectCommand | DisconnectSelfCommand | PingCommand;
14
+
15
+ /**
16
+ * Can be sent to check child responsiveness.
17
+ * An {@link AcknowledgeEvent} should be expected in response.
18
+ */
19
+ interface PingCommand {
20
+ command: "ping";
21
+ }
22
+
23
+ /**
24
+ * Instructs a child process to connect to a Fluid container.
25
+ * A {@link ConnectedEvent} should be expected in response.
26
+ */
27
+ export interface ConnectCommand {
12
28
  command: "connect";
13
29
  user: AzureUser;
30
+ /**
31
+ * The ID of the Fluid container to connect to.
32
+ * If not provided, a new Fluid container will be created.
33
+ */
14
34
  containerId?: string;
15
35
  }
16
36
 
37
+ /**
38
+ * Instructs a child process to disconnect from a Fluid container.
39
+ * A {@link DisconnectedSelfEvent} should be expected in response.
40
+ */
17
41
  interface DisconnectSelfCommand {
18
42
  command: "disconnectSelf";
19
43
  }
20
44
 
45
+ /**
46
+ * Message types sent from the child processes to the orchestrator
47
+ */
21
48
  export type MessageFromChild =
49
+ | AcknowledgeEvent
50
+ | AttendeeConnectedEvent
22
51
  | AttendeeDisconnectedEvent
23
- | attendeeConnectedEvent
24
- | ReadyEvent
52
+ | ConnectedEvent
25
53
  | DisconnectedSelfEvent
26
54
  | ErrorEvent;
27
- interface AttendeeDisconnectedEvent {
28
- event: "attendeeDisconnected";
29
- attendeeId: AttendeeId;
55
+
56
+ /**
57
+ * Sent from the child processes to the orchestrator in response to a {@link PingCommand}.
58
+ */
59
+ interface AcknowledgeEvent {
60
+ event: "ack";
30
61
  }
31
62
 
32
- interface attendeeConnectedEvent {
63
+ /**
64
+ * Sent arbitrarily to indicate a new attendee has connected.
65
+ */
66
+ interface AttendeeConnectedEvent {
33
67
  event: "attendeeConnected";
34
68
  attendeeId: AttendeeId;
35
69
  }
36
70
 
37
- interface ReadyEvent {
38
- event: "ready";
71
+ /**
72
+ * Sent arbitrarily to indicate an attendee has disconnected.
73
+ */
74
+ interface AttendeeDisconnectedEvent {
75
+ event: "attendeeDisconnected";
76
+ attendeeId: AttendeeId;
77
+ }
78
+
79
+ /**
80
+ * Sent from the child processes to the orchestrator in response to a {@link ConnectCommand}.
81
+ */
82
+ interface ConnectedEvent {
83
+ event: "connected";
39
84
  containerId: string;
40
85
  attendeeId: AttendeeId;
41
86
  }
42
87
 
88
+ /**
89
+ * Sent from the child processes to the orchestrator in response to a {@link DisconnectSelfCommand}.
90
+ */
43
91
  interface DisconnectedSelfEvent {
44
92
  event: "disconnectedSelf";
45
93
  attendeeId: AttendeeId;
46
94
  }
95
+
96
+ /**
97
+ * Sent at any time to indicate an error.
98
+ */
47
99
  interface ErrorEvent {
48
100
  event: "error";
49
101
  error: string;
@@ -6,15 +6,20 @@
6
6
  import { strict as assert } from "node:assert";
7
7
  import { fork, type ChildProcess } from "node:child_process";
8
8
 
9
- import { timeoutPromise } from "@fluidframework/test-utils/internal";
9
+ // eslint-disable-next-line import/no-internal-modules
10
+ import type { AttendeeId } from "@fluidframework/presence/beta";
11
+ import { timeoutAwait, timeoutPromise } from "@fluidframework/test-utils/internal";
10
12
 
11
- import type { MessageFromChild, MessageToChild } from "./messageTypes.js";
13
+ import type { ConnectCommand, MessageFromChild } from "./messageTypes.js";
12
14
 
13
15
  /**
14
16
  * This test suite is a prototype for a multi-process end to end test for Fluid using the new Presence API on AzureClient.
15
17
  * In the future we hope to expand and generalize this pattern to broadly test more Fluid features.
16
- * Currently our E2E tests are limited to running multiple clients on a single process which does not effectively
17
- * simulate real-world production scenarios where clients are usually running on different machines.
18
+ * Other E2E tests are limited to running multiple clients on a single process which does not effectively
19
+ * simulate real-world production scenarios where clients are usually running on different machines. Since
20
+ * the Fluid Framework client is designed to carry most of the work burden, multi-process testing from a
21
+ * single machine is also not representative but does at least work past some limitations of a single
22
+ * Node.js process handling multiple clients.
18
23
  *
19
24
  * The pattern demonstrated in this test suite is as follows:
20
25
  *
@@ -31,146 +36,300 @@ import type { MessageFromChild, MessageToChild } from "./messageTypes.js";
31
36
  * - Listen for command messages from the orchestrator.
32
37
  * - Perform the requested action.
33
38
  * - Send response messages including any relevant data back to the orchestrator to verify expected behavior.
39
+ */
40
+
41
+ /**
42
+ * Fork child processes to simulate multiple Fluid clients.
34
43
  *
35
- * This particular test suite tests the following E2E functionality for Presence:
36
- * - Announce 'attendeeConnected' when remote client joins session.
37
- * - Announce 'attendeeDisconnected' when remote client disconnects.
44
+ * @remarks
45
+ * Individual child processes may be scheduled concurrently on a multi-core CPU
46
+ * and separate processes will never share a port when connected to a service.
47
+ *
48
+ * @param numProcesses - The number of child processes to fork.
49
+ * @param cleanUpAccumulator - An array to accumulate cleanup functions for
50
+ * each child process. This is build per instance to accommodate any errors
51
+ * that might occur before completing all forking.
52
+ *
53
+ * @returns A promise that resolves with an object containing the child
54
+ * processes and a promise that rejects on any child process errors.
38
55
  */
39
- describe(`Presence with AzureClient`, () => {
40
- const numClients = 5; // Set the total number of Fluid clients to create
41
- assert(numClients > 1, "Must have at least two clients");
42
- let children: ChildProcess[] = [];
43
- // This promise is used to capture all errors that occur in the child processes.
44
- // It will never resolve successfully, it is only used to reject on child process error.
45
- let childErrorPromise: Promise<void>;
46
- // Timeout duration used when waiting for response messages from child processes.
47
- const durationMs = 10_000;
56
+ async function forkChildProcesses(
57
+ numProcesses: number,
58
+ cleanUpAccumulator: (() => void)[],
59
+ ): Promise<{
60
+ children: ChildProcess[];
61
+ /**
62
+ * Will never resolve successfully, it is only used to reject on child process error.
63
+ */
64
+ childErrorPromise: Promise<void>;
65
+ }> {
66
+ const children: ChildProcess[] = [];
67
+ const childReadyPromises: Promise<void>[] = [];
68
+ // Collect all child process error promises into this array
69
+ const childErrorPromises: Promise<void>[] = [];
70
+ // Fork child processes
71
+ for (let i = 0; i < numProcesses; i++) {
72
+ const child = fork("./lib/test/multiprocess/childClient.js", [
73
+ `child${i}` /* identifier passed to child process */,
74
+ ]);
75
+ // Register a cleanup function to kill the child process
76
+ cleanUpAccumulator.push(() => {
77
+ child.kill();
78
+ child.removeAllListeners();
79
+ });
80
+ const readyPromise = new Promise<void>((resolve, reject) => {
81
+ child.once("message", (msg: MessageFromChild) => {
82
+ if (msg.event === "ack") {
83
+ resolve();
84
+ } else {
85
+ reject(
86
+ new Error(`Unexpected (non-"ack") message from child${i}: ${JSON.stringify(msg)}`),
87
+ );
88
+ }
89
+ });
90
+ });
91
+ childReadyPromises.push(readyPromise);
92
+ const errorPromise = new Promise<void>((_, reject) => {
93
+ child.on("error", (error) => {
94
+ reject(new Error(`Child${i} process errored: ${error.message}`));
95
+ });
96
+ });
97
+ childErrorPromises.push(errorPromise);
48
98
 
49
- const afterCleanUp: (() => void)[] = [];
99
+ child.send({ command: "ping" });
100
+
101
+ children.push(child);
102
+ }
103
+ // This race will be used to reject any of the following tests on any child process errors
104
+ const childErrorPromise = Promise.race(childErrorPromises);
105
+
106
+ // All children are always expected to connect successfully and acknowledge the ping.
107
+ await Promise.race([Promise.all(childReadyPromises), childErrorPromise]);
108
+
109
+ return {
110
+ children,
111
+ childErrorPromise,
112
+ };
113
+ }
50
114
 
51
- async function connectChildProcesses(
52
- childProcesses: ChildProcess[],
53
- ): Promise<string | undefined> {
54
- let containerIdPromise: Promise<string> | undefined;
55
- let containerCreatorSessionId: string | undefined;
56
- for (const [index, child] of childProcesses.entries()) {
57
- const user = { id: `test-user-id-${index}`, name: `test-user-name-${index}` };
58
- const message: MessageToChild = { command: "connect", user };
59
-
60
- if (containerIdPromise === undefined) {
61
- // Create a promise that resolves with the containerId from the created container.
62
- containerIdPromise = timeoutPromise<string>(
63
- (resolve, reject) => {
64
- child.once("message", (msg: MessageFromChild) => {
65
- if (msg.event === "ready" && msg.containerId) {
66
- containerCreatorSessionId = msg.attendeeId;
67
- resolve(msg.containerId);
68
- } else {
69
- reject(new Error(`Non-ready message from child0: ${JSON.stringify(msg)}`));
70
- }
71
- });
72
- },
73
- {
74
- durationMs,
75
- errorMsg: "did not receive 'ready' from child process",
76
- },
77
- );
115
+ function composeConnectMessage(id: string | number): ConnectCommand {
116
+ return {
117
+ command: "connect",
118
+ user: {
119
+ id: `test-user-id-${id}`,
120
+ name: `test-user-name-${id}`,
121
+ },
122
+ };
123
+ }
124
+
125
+ async function connectChildProcesses(
126
+ childProcesses: ChildProcess[],
127
+ readyTimeoutMs: number,
128
+ ): Promise<{
129
+ containerCreatorAttendeeId: AttendeeId;
130
+ attendeeIdPromises: Promise<AttendeeId>[];
131
+ }> {
132
+ if (childProcesses.length === 0) {
133
+ throw new Error("No child processes provided for connection.");
134
+ }
135
+ const firstChild = childProcesses[0];
136
+ const containerReadyPromise = new Promise<{
137
+ containerCreatorAttendeeId: AttendeeId;
138
+ containerId: string;
139
+ }>((resolve, reject) => {
140
+ firstChild.once("message", (msg: MessageFromChild) => {
141
+ if (msg.event === "connected" && msg.containerId) {
142
+ resolve({
143
+ containerCreatorAttendeeId: msg.attendeeId,
144
+ containerId: msg.containerId,
145
+ });
78
146
  } else {
79
- // For subsequent children, wait for containerId from the promise only when needed.
80
- message.containerId = await containerIdPromise;
147
+ reject(new Error(`Non-connected message from child0: ${JSON.stringify(msg)}`));
81
148
  }
149
+ });
150
+ });
151
+ {
152
+ firstChild.send(composeConnectMessage(0));
153
+ }
154
+ const { containerCreatorAttendeeId, containerId } = await timeoutAwait(
155
+ containerReadyPromise,
156
+ {
157
+ durationMs: readyTimeoutMs,
158
+ errorMsg: "did not receive 'connected' from child process",
159
+ },
160
+ );
82
161
 
83
- child.send(message);
162
+ const attendeeIdPromises: Promise<AttendeeId>[] = [];
163
+ for (const [index, child] of childProcesses.entries()) {
164
+ if (index === 0) {
165
+ // The first child process is the container creator, it has already sent the 'connected' message.
166
+ attendeeIdPromises.push(Promise.resolve(containerCreatorAttendeeId));
167
+ continue;
84
168
  }
85
- return containerCreatorSessionId;
86
- }
169
+ const message = composeConnectMessage(index);
170
+
171
+ // For subsequent children, send containerId but do not wait for a response.
172
+ message.containerId = containerId;
87
173
 
88
- beforeEach("setup", async () => {
89
- // Collect all child process error promises into this array
90
- const childErrorPromises: Promise<void>[] = [];
91
- // Fork child processes
92
- for (let i = 0; i < numClients; i++) {
93
- const child = fork("./lib/test/multiprocess/childClient.js", [
94
- `child${i}` /* identifier passed to child process */,
95
- ]);
96
- const errorPromise = new Promise<void>((_, reject) => {
97
- child.on("error", (error) => {
98
- reject(new Error(`Child${i} process errored: ${error.message}`));
174
+ attendeeIdPromises.push(
175
+ new Promise<AttendeeId>((resolve, reject) => {
176
+ child.once("message", (msg: MessageFromChild) => {
177
+ if (msg.event === "connected") {
178
+ resolve(msg.attendeeId);
179
+ } else if (msg.event === "error") {
180
+ reject(new Error(`Child process error: ${msg.error}`));
181
+ }
99
182
  });
100
- });
101
- childErrorPromises.push(errorPromise);
102
- children.push(child);
103
- // Register cleanup for the child process listeners.
104
- afterCleanUp.push(() => child.removeAllListeners());
105
- }
106
- // This race will be used to reject any of the following tests on any child process errors
107
- childErrorPromise = Promise.race(childErrorPromises);
183
+ }),
184
+ );
185
+
186
+ child.send(message);
187
+ }
188
+
189
+ if (containerCreatorAttendeeId === undefined) {
190
+ throw new Error("No container creator session ID received from child processes.");
191
+ }
192
+
193
+ return { containerCreatorAttendeeId, attendeeIdPromises };
194
+ }
195
+
196
+ /**
197
+ * Connects the child processes and waits for the specified number of attendees to connect.
198
+ * @remarks
199
+ * This function can be used directly as a test. Comments in the functionality describe the
200
+ * breakdown of test blocks.
201
+ *
202
+ * @param children - Array of child processes to connect.
203
+ * @param attendeeCountRequired - The number of attendees that must connect.
204
+ * @param childConnectTimeoutMs - Timeout duration for child process connections.
205
+ * @param attendeesJoinedTimeoutMs - Timeout duration for required attendees to join.
206
+ * @param earlyExitPromise - Promise that resolves/rejects when the test should early exit.
207
+ */
208
+ async function connectAndWaitForAttendees(
209
+ children: ChildProcess[],
210
+ attendeeCountRequired: number,
211
+ childConnectTimeoutMs: number,
212
+ attendeesJoinedTimeoutMs: number,
213
+ earlyExitPromise: Promise<void> = Promise.resolve(),
214
+ ): Promise<{ containerCreatorAttendeeId: AttendeeId }> {
215
+ // Setup
216
+ const attendeeConnectedPromise = new Promise<void>((resolve) => {
217
+ let attendeesJoinedEvents = 0;
218
+ children[0].on("message", (msg: MessageFromChild) => {
219
+ if (msg.event === "attendeeConnected") {
220
+ attendeesJoinedEvents++;
221
+ if (attendeesJoinedEvents >= attendeeCountRequired) {
222
+ resolve();
223
+ }
224
+ }
225
+ });
108
226
  });
109
227
 
110
- // After each test, kill each child process and call any cleanup functions that were registered
228
+ // Act - connect all child processes
229
+ const connectResult = await connectChildProcesses(children, childConnectTimeoutMs);
230
+
231
+ Promise.all(connectResult.attendeeIdPromises)
232
+ .then(() => console.log("All attendees connected."))
233
+ .catch((error) => {
234
+ console.error("Error connecting children:", error);
235
+ });
236
+
237
+ // Verify - wait for all 'attendeeConnected' events
238
+ await timeoutAwait(Promise.race([attendeeConnectedPromise, earlyExitPromise]), {
239
+ durationMs: attendeesJoinedTimeoutMs,
240
+ errorMsg: "did not receive all 'attendeeConnected' events",
241
+ });
242
+
243
+ return connectResult;
244
+ }
245
+
246
+ /**
247
+ * This particular test suite tests the following E2E functionality for Presence:
248
+ * - Announce 'attendeeConnected' when remote client joins session.
249
+ * - Announce 'attendeeDisconnected' when remote client disconnects.
250
+ */
251
+ describe(`Presence with AzureClient`, () => {
252
+ const afterCleanUp: (() => void)[] = [];
253
+
254
+ // After each test, call any cleanup functions that were registered (kill each child process)
111
255
  afterEach(async () => {
112
- for (const child of children) {
113
- child.kill();
114
- }
115
- children = [];
116
256
  for (const cleanUp of afterCleanUp) {
117
257
  cleanUp();
118
258
  }
119
259
  afterCleanUp.length = 0;
120
260
  });
121
261
 
122
- it("announces 'attendeeConnected' when remote client joins session and 'attendeeDisconnected' when remote client disconnects", async () => {
123
- // Setup
124
- const attendeeConnectedPromise = timeoutPromise(
125
- (resolve) => {
126
- let attendeesJoinedEvents = 0;
127
- children[0].on("message", (msg: MessageFromChild) => {
128
- if (msg.event === "attendeeConnected") {
129
- attendeesJoinedEvents++;
130
- if (attendeesJoinedEvents === numClients - 1) {
131
- resolve();
132
- }
133
- }
134
- });
135
- },
136
- {
137
- durationMs,
138
- errorMsg: "did not receive all 'attendeeConnected' events",
139
- },
140
- );
262
+ // Note that on slower systems 50+ clients may take too long to join.
263
+ const numClientsForAttendeeTests = [5, 20, 50, 100];
264
+ // TODO: AB#45620: "Presence: perf: update Join pattern for scale" may help, then remove .slice.
265
+ for (const numClients of numClientsForAttendeeTests.slice(0, 2)) {
266
+ assert(numClients > 1, "Must have at least two clients");
141
267
 
142
- // Act - connect all child processes
143
- const creatorSessionId = await connectChildProcesses(children);
144
-
145
- // Verify - wait for all 'attendeeConnected' events
146
- await Promise.race([attendeeConnectedPromise, childErrorPromise]);
147
-
148
- // Setup
149
- const waitForDisconnected = children
150
- .filter((_, index) => index !== 0)
151
- .map(async (child, index) =>
152
- timeoutPromise(
153
- (resolve) => {
154
- child.on("message", (msg: MessageFromChild) => {
155
- if (
156
- msg.event === "attendeeDisconnected" &&
157
- msg.attendeeId === creatorSessionId
158
- ) {
159
- resolve();
160
- }
161
- });
162
- },
163
- {
164
- durationMs,
165
- errorMsg: `Attendee[${index}] Disconnected Timeout`,
166
- },
167
- ),
268
+ // Timeout duration used when waiting for response messages from child processes.
269
+ const childConnectTimeoutMs = 1000 * numClients;
270
+ const allConnectedTimeoutMs = 2000;
271
+
272
+ it(`announces 'attendeeConnected' when remote client joins session [${numClients} clients]`, async () => {
273
+ // Setup
274
+ const { children, childErrorPromise } = await forkChildProcesses(
275
+ numClients,
276
+ afterCleanUp,
168
277
  );
169
278
 
170
- // Act - disconnect first child process
171
- children[0].send({ command: "disconnectSelf" });
279
+ // Further Setup with Act and Verify
280
+ await connectAndWaitForAttendees(
281
+ children,
282
+ numClients - 1,
283
+ childConnectTimeoutMs,
284
+ allConnectedTimeoutMs,
285
+ childErrorPromise,
286
+ );
287
+ });
172
288
 
173
- // Verify - wait for all 'attendeeDisconnected' events
174
- await Promise.race([Promise.all(waitForDisconnected), childErrorPromise]);
175
- });
289
+ it(`announces 'attendeeDisconnected' when remote client disconnects [${numClients} clients]`, async () => {
290
+ // Setup
291
+ const { children, childErrorPromise } = await forkChildProcesses(
292
+ numClients,
293
+ afterCleanUp,
294
+ );
295
+
296
+ const connectResult = await connectAndWaitForAttendees(
297
+ children,
298
+ numClients - 1,
299
+ childConnectTimeoutMs,
300
+ allConnectedTimeoutMs,
301
+ childErrorPromise,
302
+ );
303
+
304
+ const childDisconnectTimeoutMs = 10_000;
305
+
306
+ const waitForDisconnected = children.map(async (child, index) =>
307
+ index === 0
308
+ ? Promise.resolve()
309
+ : timeoutPromise(
310
+ (resolve) => {
311
+ child.on("message", (msg: MessageFromChild) => {
312
+ if (
313
+ msg.event === "attendeeDisconnected" &&
314
+ msg.attendeeId === connectResult.containerCreatorAttendeeId
315
+ ) {
316
+ console.log(`Child[${index}] saw creator disconnect`);
317
+ resolve();
318
+ }
319
+ });
320
+ },
321
+ {
322
+ durationMs: childDisconnectTimeoutMs,
323
+ errorMsg: `Attendee[${index}] Disconnected Timeout`,
324
+ },
325
+ ),
326
+ );
327
+
328
+ // Act - disconnect first child process
329
+ children[0].send({ command: "disconnectSelf" });
330
+
331
+ // Verify - wait for all 'attendeeDisconnected' events
332
+ await Promise.race([Promise.all(waitForDisconnected), childErrorPromise]);
333
+ });
334
+ }
176
335
  });