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