@mml-io/3d-web-user-networking
Advanced tools
Comparing version 0.0.0-experimental-8634a66-20240605 to 0.0.0-experimental-87201ea-20250131
@@ -36,9 +36,15 @@ // src/UserNetworkingCodec.ts | ||
// src/UserNetworkingMessages.ts | ||
var DISCONNECTED_MESSAGE_TYPE = "disconnected"; | ||
var IDENTITY_MESSAGE_TYPE = "identity"; | ||
var USER_AUTHENTICATE_MESSAGE_TYPE = "user_auth"; | ||
var USER_PROFILE_MESSAGE_TYPE = "user_profile"; | ||
var USER_UPDATE_MESSAGE_TYPE = "user_update"; | ||
var PING_MESSAGE_TYPE = "ping"; | ||
var PONG_MESSAGE_TYPE = "pong"; | ||
var USER_NETWORKING_DISCONNECTED_MESSAGE_TYPE = "disconnected"; | ||
var USER_NETWORKING_IDENTITY_MESSAGE_TYPE = "identity"; | ||
var USER_NETWORKING_USER_AUTHENTICATE_MESSAGE_TYPE = "user_auth"; | ||
var USER_NETWORKING_USER_PROFILE_MESSAGE_TYPE = "user_profile"; | ||
var USER_NETWORKING_USER_UPDATE_MESSAGE_TYPE = "user_update"; | ||
var USER_NETWORKING_SERVER_BROADCAST_MESSAGE_TYPE = "broadcast"; | ||
var USER_NETWORKING_SERVER_ERROR_MESSAGE_TYPE = "error"; | ||
var USER_NETWORKING_PING_MESSAGE_TYPE = "ping"; | ||
var USER_NETWORKING_PONG_MESSAGE_TYPE = "pong"; | ||
var USER_NETWORKING_CONNECTION_LIMIT_REACHED_ERROR_TYPE = "CONNECTION_LIMIT_REACHED"; | ||
var USER_NETWORKING_AUTHENTICATION_FAILED_ERROR_TYPE = "AUTHENTICATION_FAILED"; | ||
var USER_NETWORKING_SERVER_SHUTDOWN_ERROR_TYPE = "SERVER_SHUTDOWN"; | ||
var USER_NETWORKING_UNKNOWN_ERROR = "UNKNOWN_ERROR"; | ||
@@ -52,5 +58,5 @@ // src/UserNetworkingServer.ts | ||
this.authenticatedClientsById = /* @__PURE__ */ new Map(); | ||
setInterval(this.sendUpdates.bind(this), packetsUpdateRate); | ||
setInterval(this.pingClients.bind(this), pingPongRate); | ||
setInterval(this.heartBeat.bind(this), heartBeatRate); | ||
this.sendUpdatesIntervalTimer = setInterval(this.sendUpdates.bind(this), packetsUpdateRate); | ||
this.pingClientsIntervalTimer = setInterval(this.pingClients.bind(this), pingPongRate); | ||
this.heartbeatIntervalTimer = setInterval(this.heartBeat.bind(this), heartBeatRate); | ||
} | ||
@@ -67,5 +73,7 @@ heartBeat() { | ||
pingClients() { | ||
const message = { type: "ping" }; | ||
const messageString = JSON.stringify(message); | ||
this.authenticatedClientsById.forEach((client) => { | ||
if (client.socket.readyState === WebSocketOpenStatus) { | ||
client.socket.send(JSON.stringify({ type: "ping" })); | ||
client.socket.send(messageString); | ||
} | ||
@@ -81,2 +89,15 @@ }); | ||
} | ||
broadcastMessage(broadcastType, broadcastPayload) { | ||
const message = { | ||
type: "broadcast", | ||
broadcastType, | ||
payload: broadcastPayload | ||
}; | ||
const messageString = JSON.stringify(message); | ||
for (const [, client] of this.authenticatedClientsById) { | ||
if (client.socket.readyState === WebSocketOpenStatus) { | ||
client.socket.send(messageString); | ||
} | ||
} | ||
} | ||
connectClient(socket) { | ||
@@ -113,7 +134,58 @@ const id = this.getId(); | ||
if (!client.authenticatedUser) { | ||
if (parsed.type === USER_AUTHENTICATE_MESSAGE_TYPE) { | ||
if (parsed.type === USER_NETWORKING_USER_AUTHENTICATE_MESSAGE_TYPE) { | ||
this.handleUserAuth(client, parsed).then((authResult) => { | ||
var _a, _b; | ||
if (client.socket.readyState !== WebSocketOpenStatus) { | ||
return; | ||
} | ||
if (!authResult) { | ||
const serverError = JSON.stringify({ | ||
type: USER_NETWORKING_SERVER_ERROR_MESSAGE_TYPE, | ||
errorType: USER_NETWORKING_AUTHENTICATION_FAILED_ERROR_TYPE, | ||
message: "Authentication failed" | ||
}); | ||
socket.send(serverError); | ||
socket.close(); | ||
} else { | ||
if (this.options.connectionLimit !== void 0 && this.authenticatedClientsById.size >= this.options.connectionLimit) { | ||
const serverError = JSON.stringify({ | ||
type: USER_NETWORKING_SERVER_ERROR_MESSAGE_TYPE, | ||
errorType: USER_NETWORKING_CONNECTION_LIMIT_REACHED_ERROR_TYPE, | ||
message: "Connection limit reached" | ||
}); | ||
socket.send(serverError); | ||
socket.close(); | ||
return; | ||
} | ||
const userData = authResult; | ||
client.authenticatedUser = userData; | ||
const userProfileMessage = JSON.stringify({ | ||
id: client.id, | ||
type: USER_NETWORKING_USER_PROFILE_MESSAGE_TYPE, | ||
username: userData.username, | ||
characterDescription: userData.characterDescription | ||
}); | ||
client.socket.send(userProfileMessage); | ||
const identityMessage = JSON.stringify({ | ||
id: client.id, | ||
type: USER_NETWORKING_IDENTITY_MESSAGE_TYPE | ||
}); | ||
client.socket.send(identityMessage); | ||
const userUpdateMessage = UserNetworkingCodec.encodeUpdate(client.update); | ||
for (const [, otherClient] of this.authenticatedClientsById) { | ||
if (otherClient.socket.readyState !== WebSocketOpenStatus || otherClient === client) { | ||
continue; | ||
} | ||
client.socket.send( | ||
JSON.stringify({ | ||
id: otherClient.update.id, | ||
type: USER_NETWORKING_USER_PROFILE_MESSAGE_TYPE, | ||
username: (_a = otherClient.authenticatedUser) == null ? void 0 : _a.username, | ||
characterDescription: (_b = otherClient.authenticatedUser) == null ? void 0 : _b.characterDescription | ||
}) | ||
); | ||
client.socket.send(UserNetworkingCodec.encodeUpdate(otherClient.update)); | ||
otherClient.socket.send(userProfileMessage); | ||
otherClient.socket.send(userUpdateMessage); | ||
} | ||
this.authenticatedClientsById.set(id, client); | ||
@@ -128,6 +200,6 @@ } | ||
switch (parsed.type) { | ||
case PONG_MESSAGE_TYPE: | ||
case USER_NETWORKING_PONG_MESSAGE_TYPE: | ||
client.lastPong = Date.now(); | ||
break; | ||
case USER_UPDATE_MESSAGE_TYPE: | ||
case USER_NETWORKING_USER_UPDATE_MESSAGE_TYPE: | ||
this.handleUserUpdate(id, parsed); | ||
@@ -156,3 +228,3 @@ break; | ||
id: client.id, | ||
type: DISCONNECTED_MESSAGE_TYPE | ||
type: USER_NETWORKING_DISCONNECTED_MESSAGE_TYPE | ||
}); | ||
@@ -167,3 +239,2 @@ for (const [, otherClient] of this.authenticatedClientsById) { | ||
async handleUserAuth(client, credentials) { | ||
var _a, _b; | ||
const userData = this.options.onClientConnect( | ||
@@ -185,34 +256,3 @@ client.id, | ||
console.log("Client authenticated", client.id, resolvedUserData); | ||
client.authenticatedUser = resolvedUserData; | ||
const identityMessage = JSON.stringify({ | ||
id: client.id, | ||
type: IDENTITY_MESSAGE_TYPE | ||
}); | ||
const userProfileMessage = JSON.stringify({ | ||
id: client.id, | ||
type: USER_PROFILE_MESSAGE_TYPE, | ||
username: resolvedUserData.username, | ||
characterDescription: resolvedUserData.characterDescription | ||
}); | ||
client.socket.send(userProfileMessage); | ||
client.socket.send(identityMessage); | ||
const userUpdateMessage = UserNetworkingCodec.encodeUpdate(client.update); | ||
for (const [, otherClient] of this.authenticatedClientsById) { | ||
if (otherClient.socket.readyState !== WebSocketOpenStatus || otherClient === client) { | ||
continue; | ||
} | ||
client.socket.send( | ||
JSON.stringify({ | ||
id: otherClient.update.id, | ||
type: USER_PROFILE_MESSAGE_TYPE, | ||
username: (_a = otherClient.authenticatedUser) == null ? void 0 : _a.username, | ||
characterDescription: (_b = otherClient.authenticatedUser) == null ? void 0 : _b.characterDescription | ||
}) | ||
); | ||
client.socket.send(UserNetworkingCodec.encodeUpdate(otherClient.update)); | ||
otherClient.socket.send(userProfileMessage); | ||
otherClient.socket.send(userUpdateMessage); | ||
} | ||
console.log("Client authenticated", client.id); | ||
return true; | ||
return resolvedUserData; | ||
} | ||
@@ -228,3 +268,3 @@ updateUserCharacter(clientId, userData) { | ||
id: clientId, | ||
type: USER_PROFILE_MESSAGE_TYPE, | ||
type: USER_NETWORKING_USER_PROFILE_MESSAGE_TYPE, | ||
username: userData.username, | ||
@@ -272,2 +312,14 @@ characterDescription: userData.characterDescription | ||
} | ||
dispose(clientCloseError) { | ||
clearInterval(this.sendUpdatesIntervalTimer); | ||
clearInterval(this.pingClientsIntervalTimer); | ||
clearInterval(this.heartbeatIntervalTimer); | ||
const stringifiedError = clientCloseError ? JSON.stringify(clientCloseError) : void 0; | ||
for (const [, client] of this.authenticatedClientsById) { | ||
if (stringifiedError) { | ||
client.socket.send(stringifiedError); | ||
} | ||
client.socket.close(); | ||
} | ||
} | ||
}; | ||
@@ -394,3 +446,3 @@ | ||
} | ||
console.log("NetworkedDOMWebsocket close", e); | ||
console.log("ReconnectingWebSocket close", e); | ||
onWebsocketClose(); | ||
@@ -403,3 +455,3 @@ }); | ||
} | ||
console.error("NetworkedDOMWebsocket error", e); | ||
console.error("ReconnectingWebSocket error", e); | ||
onWebsocketClose(); | ||
@@ -430,3 +482,3 @@ }); | ||
this.sendMessage({ | ||
type: USER_AUTHENTICATE_MESSAGE_TYPE, | ||
type: USER_NETWORKING_USER_AUTHENTICATE_MESSAGE_TYPE, | ||
sessionToken: config.sessionToken | ||
@@ -450,18 +502,33 @@ }); | ||
switch (parsed.type) { | ||
case DISCONNECTED_MESSAGE_TYPE: | ||
case USER_NETWORKING_SERVER_ERROR_MESSAGE_TYPE: | ||
console.error(`Server error: ${parsed.message}. errorType: ${parsed.errorType}`); | ||
this.config.onServerError(parsed); | ||
break; | ||
case USER_NETWORKING_DISCONNECTED_MESSAGE_TYPE: | ||
console.log(`Client ID: ${parsed.id} left`); | ||
this.config.clientUpdate(parsed.id, null); | ||
break; | ||
case IDENTITY_MESSAGE_TYPE: | ||
case USER_NETWORKING_IDENTITY_MESSAGE_TYPE: | ||
console.log(`Client ID: ${parsed.id} assigned to self`); | ||
this.config.assignedIdentity(parsed.id); | ||
break; | ||
case USER_PROFILE_MESSAGE_TYPE: | ||
case USER_NETWORKING_USER_PROFILE_MESSAGE_TYPE: | ||
console.log(`Client ID: ${parsed.id} updated profile`); | ||
this.config.clientProfileUpdated(parsed.id, parsed.username, parsed.characterDescription); | ||
break; | ||
case PING_MESSAGE_TYPE: { | ||
case USER_NETWORKING_PING_MESSAGE_TYPE: { | ||
this.sendMessage({ type: "pong" }); | ||
break; | ||
} | ||
case USER_NETWORKING_SERVER_BROADCAST_MESSAGE_TYPE: { | ||
if (this.config.onServerBroadcast) { | ||
this.config.onServerBroadcast({ | ||
broadcastType: parsed.broadcastType, | ||
payload: parsed.payload | ||
}); | ||
} else { | ||
console.warn("Unhandled broadcast", parsed); | ||
} | ||
break; | ||
} | ||
default: | ||
@@ -479,10 +546,16 @@ console.error("Unhandled message", parsed); | ||
export { | ||
DISCONNECTED_MESSAGE_TYPE, | ||
IDENTITY_MESSAGE_TYPE, | ||
PING_MESSAGE_TYPE, | ||
PONG_MESSAGE_TYPE, | ||
ReconnectingWebSocket, | ||
USER_AUTHENTICATE_MESSAGE_TYPE, | ||
USER_PROFILE_MESSAGE_TYPE, | ||
USER_UPDATE_MESSAGE_TYPE, | ||
USER_NETWORKING_AUTHENTICATION_FAILED_ERROR_TYPE, | ||
USER_NETWORKING_CONNECTION_LIMIT_REACHED_ERROR_TYPE, | ||
USER_NETWORKING_DISCONNECTED_MESSAGE_TYPE, | ||
USER_NETWORKING_IDENTITY_MESSAGE_TYPE, | ||
USER_NETWORKING_PING_MESSAGE_TYPE, | ||
USER_NETWORKING_PONG_MESSAGE_TYPE, | ||
USER_NETWORKING_SERVER_BROADCAST_MESSAGE_TYPE, | ||
USER_NETWORKING_SERVER_ERROR_MESSAGE_TYPE, | ||
USER_NETWORKING_SERVER_SHUTDOWN_ERROR_TYPE, | ||
USER_NETWORKING_UNKNOWN_ERROR, | ||
USER_NETWORKING_USER_AUTHENTICATE_MESSAGE_TYPE, | ||
USER_NETWORKING_USER_PROFILE_MESSAGE_TYPE, | ||
USER_NETWORKING_USER_UPDATE_MESSAGE_TYPE, | ||
UserNetworkingClient, | ||
@@ -489,0 +562,0 @@ UserNetworkingCodec, |
import { ReconnectingWebSocket, WebsocketFactory, WebsocketStatus } from "./ReconnectingWebSocket"; | ||
import { UserNetworkingClientUpdate } from "./UserNetworkingCodec"; | ||
import { CharacterDescription, FromClientMessage } from "./UserNetworkingMessages"; | ||
import { CharacterDescription, FromUserNetworkingClientMessage, UserNetworkingServerErrorType } from "./UserNetworkingMessages"; | ||
export type UserNetworkingClientConfig = { | ||
@@ -12,2 +12,10 @@ url: string; | ||
clientProfileUpdated: (id: number, username: string, characterDescription: CharacterDescription) => void; | ||
onServerError: (error: { | ||
message: string; | ||
errorType: UserNetworkingServerErrorType; | ||
}) => void; | ||
onServerBroadcast?: (broadcast: { | ||
broadcastType: string; | ||
payload: any; | ||
}) => void; | ||
}; | ||
@@ -18,4 +26,4 @@ export declare class UserNetworkingClient extends ReconnectingWebSocket { | ||
sendUpdate(update: UserNetworkingClientUpdate): void; | ||
sendMessage(message: FromClientMessage): void; | ||
sendMessage(message: FromUserNetworkingClientMessage): void; | ||
protected handleIncomingWebsocketMessage(message: MessageEvent): void; | ||
} |
@@ -1,25 +0,29 @@ | ||
export declare const DISCONNECTED_MESSAGE_TYPE = "disconnected"; | ||
export declare const IDENTITY_MESSAGE_TYPE = "identity"; | ||
export declare const USER_AUTHENTICATE_MESSAGE_TYPE = "user_auth"; | ||
export declare const USER_PROFILE_MESSAGE_TYPE = "user_profile"; | ||
export declare const USER_UPDATE_MESSAGE_TYPE = "user_update"; | ||
export declare const PING_MESSAGE_TYPE = "ping"; | ||
export declare const PONG_MESSAGE_TYPE = "pong"; | ||
export type IdentityMessage = { | ||
type: typeof IDENTITY_MESSAGE_TYPE; | ||
export declare const USER_NETWORKING_DISCONNECTED_MESSAGE_TYPE = "disconnected"; | ||
export declare const USER_NETWORKING_IDENTITY_MESSAGE_TYPE = "identity"; | ||
export declare const USER_NETWORKING_USER_AUTHENTICATE_MESSAGE_TYPE = "user_auth"; | ||
export declare const USER_NETWORKING_USER_PROFILE_MESSAGE_TYPE = "user_profile"; | ||
export declare const USER_NETWORKING_USER_UPDATE_MESSAGE_TYPE = "user_update"; | ||
export declare const USER_NETWORKING_SERVER_BROADCAST_MESSAGE_TYPE = "broadcast"; | ||
export declare const USER_NETWORKING_SERVER_ERROR_MESSAGE_TYPE = "error"; | ||
export declare const USER_NETWORKING_PING_MESSAGE_TYPE = "ping"; | ||
export declare const USER_NETWORKING_PONG_MESSAGE_TYPE = "pong"; | ||
export type UserNetworkingIdentityMessage = { | ||
type: typeof USER_NETWORKING_IDENTITY_MESSAGE_TYPE; | ||
id: number; | ||
}; | ||
export type CharacterDescription = { | ||
meshFileUrl?: string; | ||
mmlCharacterUrl?: string; | ||
mmlCharacterString?: string; | ||
} & ({ | ||
meshFileUrl: string; | ||
mmlCharacterString?: null; | ||
mmlCharacterUrl?: null; | ||
} | { | ||
meshFileUrl?: null; | ||
mmlCharacterString: string; | ||
mmlCharacterUrl?: null; | ||
} | { | ||
meshFileUrl?: null; | ||
mmlCharacterString?: null; | ||
mmlCharacterUrl: string; | ||
} | { | ||
mmlCharacterString: string; | ||
}); | ||
export type UserProfileMessage = { | ||
type: typeof USER_PROFILE_MESSAGE_TYPE; | ||
}; | ||
export type UserNetworkingProfileMessage = { | ||
type: typeof USER_NETWORKING_USER_PROFILE_MESSAGE_TYPE; | ||
id: number; | ||
@@ -29,13 +33,28 @@ username: string; | ||
}; | ||
export type DisconnectedMessage = { | ||
type: typeof DISCONNECTED_MESSAGE_TYPE; | ||
export type UserNetworkingDisconnectedMessage = { | ||
type: typeof USER_NETWORKING_DISCONNECTED_MESSAGE_TYPE; | ||
id: number; | ||
}; | ||
export type FromServerPingMessage = { | ||
type: typeof PING_MESSAGE_TYPE; | ||
export declare const USER_NETWORKING_CONNECTION_LIMIT_REACHED_ERROR_TYPE = "CONNECTION_LIMIT_REACHED"; | ||
export declare const USER_NETWORKING_AUTHENTICATION_FAILED_ERROR_TYPE = "AUTHENTICATION_FAILED"; | ||
export declare const USER_NETWORKING_SERVER_SHUTDOWN_ERROR_TYPE = "SERVER_SHUTDOWN"; | ||
export declare const USER_NETWORKING_UNKNOWN_ERROR = "UNKNOWN_ERROR"; | ||
export type UserNetworkingServerErrorType = typeof USER_NETWORKING_CONNECTION_LIMIT_REACHED_ERROR_TYPE | typeof USER_NETWORKING_AUTHENTICATION_FAILED_ERROR_TYPE | typeof USER_NETWORKING_SERVER_SHUTDOWN_ERROR_TYPE | typeof USER_NETWORKING_UNKNOWN_ERROR; | ||
export type UserNetworkingServerError = { | ||
type: typeof USER_NETWORKING_SERVER_ERROR_MESSAGE_TYPE; | ||
errorType: UserNetworkingServerErrorType; | ||
message: string; | ||
}; | ||
export type FromServerMessage = IdentityMessage | UserProfileMessage | DisconnectedMessage | FromServerPingMessage; | ||
export type FromClientPongMessage = { | ||
type: typeof PONG_MESSAGE_TYPE; | ||
export type UserNetworkingServerBroadcast = { | ||
type: typeof USER_NETWORKING_SERVER_BROADCAST_MESSAGE_TYPE; | ||
broadcastType: string; | ||
payload: any; | ||
}; | ||
export type UserNetworkingServerPingMessage = { | ||
type: typeof USER_NETWORKING_PING_MESSAGE_TYPE; | ||
}; | ||
export type FromUserNetworkingServerMessage = UserNetworkingIdentityMessage | UserNetworkingProfileMessage | UserNetworkingDisconnectedMessage | UserNetworkingServerPingMessage | UserNetworkingServerBroadcast | UserNetworkingServerError; | ||
export type UserNetworkingClientPongMessage = { | ||
type: typeof USER_NETWORKING_PONG_MESSAGE_TYPE; | ||
}; | ||
export type UserIdentity = { | ||
@@ -45,11 +64,11 @@ characterDescription: CharacterDescription | null; | ||
}; | ||
export type UserAuthenticateMessage = { | ||
type: typeof USER_AUTHENTICATE_MESSAGE_TYPE; | ||
export type UserNetworkingAuthenticateMessage = { | ||
type: typeof USER_NETWORKING_USER_AUTHENTICATE_MESSAGE_TYPE; | ||
sessionToken: string; | ||
userIdentity?: UserIdentity; | ||
}; | ||
export type UserUpdateMessage = { | ||
type: typeof USER_UPDATE_MESSAGE_TYPE; | ||
export type UserNetworkingUserUpdateMessage = { | ||
type: typeof USER_NETWORKING_USER_UPDATE_MESSAGE_TYPE; | ||
userIdentity: UserIdentity; | ||
}; | ||
export type FromClientMessage = FromClientPongMessage | UserAuthenticateMessage | UserUpdateMessage; | ||
export type FromUserNetworkingClientMessage = UserNetworkingClientPongMessage | UserNetworkingAuthenticateMessage | UserNetworkingUserUpdateMessage; |
import WebSocket from "ws"; | ||
import { UserData } from "./UserData"; | ||
import { UserNetworkingClientUpdate } from "./UserNetworkingCodec"; | ||
import { UserIdentity } from "./UserNetworkingMessages"; | ||
export type Client = { | ||
import { UserIdentity, UserNetworkingServerError } from "./UserNetworkingMessages"; | ||
export type UserNetworkingServerClient = { | ||
socket: WebSocket; | ||
@@ -13,2 +13,3 @@ id: number; | ||
export type UserNetworkingServerOptions = { | ||
connectionLimit?: number; | ||
onClientConnect: (clientId: number, sessionToken: string, userIdentity?: UserIdentity) => Promise<UserData | null> | UserData | null; | ||
@@ -22,2 +23,5 @@ onClientUserIdentityUpdate: (clientId: number, userIdentity: UserIdentity) => Promise<UserData | null> | UserData | null; | ||
private authenticatedClientsById; | ||
private sendUpdatesIntervalTimer; | ||
private pingClientsIntervalTimer; | ||
private heartbeatIntervalTimer; | ||
constructor(options: UserNetworkingServerOptions); | ||
@@ -27,2 +31,3 @@ private heartBeat; | ||
private getId; | ||
broadcastMessage(broadcastType: string, broadcastPayload: any): void; | ||
connectClient(socket: WebSocket): void; | ||
@@ -35,2 +40,3 @@ private handleDisconnectedClient; | ||
private sendUpdates; | ||
dispose(clientCloseError?: UserNetworkingServerError): void; | ||
} |
{ | ||
"name": "@mml-io/3d-web-user-networking", | ||
"version": "0.0.0-experimental-8634a66-20240605", | ||
"version": "0.0.0-experimental-87201ea-20250131", | ||
"publishConfig": { | ||
@@ -22,3 +22,3 @@ "access": "public" | ||
"dependencies": { | ||
"ws": "^8.16.0" | ||
"ws": "^8.18.0" | ||
}, | ||
@@ -28,3 +28,3 @@ "devDependencies": { | ||
"@types/express-ws": "^3.0.4", | ||
"@types/node": "^20.12.7", | ||
"@types/node": "^20.14.10", | ||
"@types/ws": "^8.5.10", | ||
@@ -34,3 +34,3 @@ "express": "4.19.2", | ||
}, | ||
"gitHead": "4ed3528284b3c6b08e63c141dab721007d74841d" | ||
"gitHead": "77933100fcfb7df926bc8ea86b21563f9a35d773" | ||
} |
Sorry, the diff of this file is not supported yet
70965
12
740
Updatedws@^8.18.0