You're Invited:Meet the Socket Team at BlackHat and DEF CON in Las Vegas, Aug 4-6.RSVP
Socket
Book a DemoInstallSign in
Socket

@mml-io/3d-web-user-networking

Package Overview
Dependencies
Maintainers
2
Versions
211
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@mml-io/3d-web-user-networking - npm Package Compare versions

Comparing version

to
0.0.0-experimental-dfa8273-20250802

build/DeltaNetComponentMapping.d.ts

10

build/index.d.ts

@@ -1,5 +0,9 @@

export * from "./UserNetworkingCodec";
export * from "./UserNetworkingServer";
export * from "./UserNetworkingClient";
export * from "./ReconnectingWebSocket";
export * from "./messages";
export * from "./UserData";
export * from "./types";
export * from "./UserNetworkingMessages";
export * from "./legacy/LegacyUserNetworkingMessages";
export * from "./DeltaNetComponentMapping";
export * from "./UserNetworkingLogger";
export { DeltaNetV01ServerErrors, deltaNetProtocolSubProtocol_v0_1, } from "@mml-io/delta-net-protocol";

@@ -1,3 +0,270 @@

// src/UserNetworkingCodec.ts
var UserNetworkingCodec = class {
// src/UserNetworkingServer.ts
import { encodeError, DeltaNetV01ServerErrors } from "@mml-io/delta-net-protocol";
import {
DeltaNetServer,
DeltaNetServerError as DeltaNetServerError2
} from "@mml-io/delta-net-server";
// src/DeltaNetComponentMapping.ts
import { BufferReader, BufferWriter } from "@mml-io/delta-net-protocol";
var COMPONENT_POSITION_X = 1;
var COMPONENT_POSITION_Y = 2;
var COMPONENT_POSITION_Z = 3;
var COMPONENT_ROTATION_Y = 4;
var COMPONENT_ROTATION_W = 5;
var COMPONENT_STATE = 6;
var STATE_INTERNAL_CONNECTION_ID = 0;
var STATE_CHARACTER_DESCRIPTION = 1;
var STATE_USERNAME = 2;
var STATE_COLORS = 3;
var rotationMultiplier = 360;
var positionMultiplier = 100;
var textDecoder = new TextDecoder();
var DeltaNetComponentMapping = class _DeltaNetComponentMapping {
/**
* Convert UserNetworkingClientUpdate to deltanet components
*/
static toComponents(update) {
const components = /* @__PURE__ */ new Map();
components.set(
COMPONENT_POSITION_X,
BigInt(Math.round(update.position.x * positionMultiplier))
);
components.set(
COMPONENT_POSITION_Y,
BigInt(Math.round(update.position.y * positionMultiplier))
);
components.set(
COMPONENT_POSITION_Z,
BigInt(Math.round(update.position.z * positionMultiplier))
);
components.set(
COMPONENT_ROTATION_Y,
BigInt(Math.round(update.rotation.quaternionY * rotationMultiplier))
);
components.set(
COMPONENT_ROTATION_W,
BigInt(Math.round(update.rotation.quaternionW * rotationMultiplier))
);
components.set(COMPONENT_STATE, BigInt(update.state));
return components;
}
/**
* Convert deltanet components back to UserNetworkingClientUpdate
*/
static fromComponents(components) {
const positionX = Number(components.get(COMPONENT_POSITION_X) || BigInt(0)) / positionMultiplier;
const positionY = Number(components.get(COMPONENT_POSITION_Y) || BigInt(0)) / positionMultiplier;
const positionZ = Number(components.get(COMPONENT_POSITION_Z) || BigInt(0)) / positionMultiplier;
const rotationY = Number(components.get(COMPONENT_ROTATION_Y) || BigInt(0)) / rotationMultiplier;
const rotationW = Number(components.get(COMPONENT_ROTATION_W) || BigInt(0)) / rotationMultiplier;
const state = Number(components.get(COMPONENT_STATE) || BigInt(0));
return {
position: { x: positionX, y: positionY, z: positionZ },
rotation: { quaternionY: rotationY, quaternionW: rotationW },
state
};
}
/**
* Encode character description and username to binary states
*/
static toStates(userIdentity) {
const states = /* @__PURE__ */ new Map();
const textEncoder = new TextEncoder();
if (userIdentity.username) {
states.set(STATE_USERNAME, textEncoder.encode(userIdentity.username));
}
if (userIdentity.characterDescription) {
states.set(
STATE_CHARACTER_DESCRIPTION,
textEncoder.encode(JSON.stringify(userIdentity.characterDescription))
);
}
if (userIdentity.colors) {
states.set(STATE_COLORS, _DeltaNetComponentMapping.encodeColors(userIdentity.colors));
}
return states;
}
/**
* Encode username to binary state
*/
static toUsernameState(username) {
const states = /* @__PURE__ */ new Map();
const textEncoder = new TextEncoder();
states.set(STATE_USERNAME, textEncoder.encode(username));
return states;
}
/**
* Encode character description to binary state
*/
static toCharacterDescriptionState(characterDescription) {
const states = /* @__PURE__ */ new Map();
const textEncoder = new TextEncoder();
states.set(
STATE_CHARACTER_DESCRIPTION,
textEncoder.encode(JSON.stringify(characterDescription))
);
return states;
}
/**
* Encode colors to binary state
*/
static toColorsState(colors) {
const states = /* @__PURE__ */ new Map();
states.set(STATE_COLORS, _DeltaNetComponentMapping.encodeColors(colors));
return states;
}
/**
* Encode single state value
*/
static toSingleState(stateId, value) {
const states = /* @__PURE__ */ new Map();
const textEncoder = new TextEncoder();
switch (stateId) {
case STATE_USERNAME:
if (typeof value === "string") {
states.set(stateId, textEncoder.encode(value));
}
break;
case STATE_CHARACTER_DESCRIPTION:
if (typeof value === "object" && value !== null) {
states.set(stateId, textEncoder.encode(JSON.stringify(value)));
}
break;
case STATE_COLORS:
if (Array.isArray(value)) {
states.set(stateId, _DeltaNetComponentMapping.encodeColors(value));
}
break;
}
return states;
}
static encodeColors(colors) {
const bufferWriter = new BufferWriter(3 * colors.length + 1);
bufferWriter.writeUVarint(colors.length);
for (const color of colors) {
bufferWriter.writeUVarint(color[0]);
bufferWriter.writeUVarint(color[1]);
bufferWriter.writeUVarint(color[2]);
}
return bufferWriter.getBuffer();
}
static decodeColors(colors, logger) {
if (colors.byteLength === 0) {
return [];
}
try {
const bufferReader = new BufferReader(colors);
const colorsArray = [];
const count = bufferReader.readUVarint();
for (let i = 0; i < count; i++) {
colorsArray.push([
bufferReader.readUVarint(),
bufferReader.readUVarint(),
bufferReader.readUVarint()
]);
}
return colorsArray;
} catch (e) {
logger.error("Error decoding colors", colors, e);
return [];
}
}
static fromUserStates(states, logger) {
const usernameBytes = states.get(STATE_USERNAME);
const username = usernameBytes ? _DeltaNetComponentMapping.usernameFromBytes(usernameBytes) : null;
const characterDescBytes = states.get(STATE_CHARACTER_DESCRIPTION);
const characterDescription = characterDescBytes ? _DeltaNetComponentMapping.characterDescriptionFromBytes(characterDescBytes) : null;
const colorsBytes = states.get(STATE_COLORS);
const colorsArray = colorsBytes ? _DeltaNetComponentMapping.decodeColors(colorsBytes, logger) : [];
return { username, characterDescription, colors: colorsArray };
}
static userIdFromBytes(bytes) {
if (bytes.length === 0) {
return null;
}
const reader = new BufferReader(bytes);
return reader.readUVarint(false);
}
static usernameFromBytes(bytes) {
if (bytes.length === 0) {
return null;
}
return textDecoder.decode(bytes);
}
static characterDescriptionFromBytes(bytes) {
if (bytes.length === 0) {
return null;
}
return JSON.parse(textDecoder.decode(bytes));
}
/**
* Decode binary states back to username and character description
*/
static fromStates(states, logger) {
const userIdBytes = states.get(STATE_INTERNAL_CONNECTION_ID);
let userId = null;
if (userIdBytes) {
const reader = new BufferReader(userIdBytes);
userId = reader.readUVarint(false);
}
const userStates = _DeltaNetComponentMapping.fromUserStates(states, logger);
return { userId, ...userStates };
}
};
// src/UserNetworkingMessages.ts
import { DeltaNetServerError } from "@mml-io/delta-net-server";
var UserNetworkingServerError = class extends DeltaNetServerError {
};
var SERVER_BROADCAST_MESSAGE_TYPE = 1;
var FROM_CLIENT_CHAT_MESSAGE_TYPE = 2;
var FROM_SERVER_CHAT_MESSAGE_TYPE = 3;
function parseClientChatMessage(contents) {
try {
const parsed = JSON.parse(contents);
if (typeof parsed === "object" && parsed !== null && "message" in parsed && typeof parsed.message === "string") {
return {
message: parsed.message
};
} else {
throw new Error("Invalid chat message");
}
} catch (error) {
return new Error(`Invalid chat message: ${error}`);
}
}
function parseServerChatMessage(contents) {
try {
const parsed = JSON.parse(contents);
if (typeof parsed === "object" && parsed !== null && "fromUserId" in parsed && typeof parsed.fromUserId === "number" && "message" in parsed && typeof parsed.message === "string") {
return {
fromUserId: parsed.fromUserId,
message: parsed.message
};
} else {
throw new Error("Invalid server chat message");
}
} catch (error) {
return new Error(`Invalid server chat message: ${error}`);
}
}
function parseServerBroadcastMessage(contents) {
try {
const parsed = JSON.parse(contents);
if (typeof parsed === "object" && parsed !== null && "broadcastType" in parsed && typeof parsed.broadcastType === "string" && "payload" in parsed && typeof parsed.payload === "object") {
return {
broadcastType: parsed.broadcastType,
payload: parsed.payload
};
} else {
throw new Error("Invalid server broadcast message");
}
} catch (error) {
return new Error(`Invalid server broadcast message: ${error}`);
}
}
// src/legacy/LegacyUserNetworkingCodec.ts
var LegacyUserNetworkingCodec = class {
static encodeUpdate(update) {

@@ -30,77 +297,64 @@ const buffer = new ArrayBuffer(19);

// src/messages.ts
var CONNECTED_MESSAGE_TYPE = "connected";
var DISCONNECTED_MESSAGE_TYPE = "disconnected";
var IDENTITY_MESSAGE_TYPE = "identity";
var PING_MESSAGE_TYPE = "ping";
var PONG_MESSAGE_TYPE = "pong";
// src/legacy/LegacyUserNetworkingMessages.ts
var LEGACY_USER_NETWORKING_DISCONNECTED_MESSAGE_TYPE = "disconnected";
var LEGACY_USER_NETWORKING_IDENTITY_MESSAGE_TYPE = "identity";
var LEGACY_USER_NETWORKING_USER_AUTHENTICATE_MESSAGE_TYPE = "user_auth";
var LEGACY_USER_NETWORKING_USER_PROFILE_MESSAGE_TYPE = "user_profile";
var LEGACY_USER_NETWORKING_USER_UPDATE_MESSAGE_TYPE = "user_update";
var LEGACY_USER_NETWORKING_SERVER_BROADCAST_MESSAGE_TYPE = "broadcast";
var LEGACY_USER_NETWORKING_SERVER_ERROR_MESSAGE_TYPE = "error";
var LEGACY_USER_NETWORKING_PING_MESSAGE_TYPE = "ping";
var LEGACY_USER_NETWORKING_PONG_MESSAGE_TYPE = "pong";
var LEGACY_USER_NETWORKING_CONNECTION_LIMIT_REACHED_ERROR_TYPE = "CONNECTION_LIMIT_REACHED";
var LEGACY_USER_NETWORKING_AUTHENTICATION_FAILED_ERROR_TYPE = "AUTHENTICATION_FAILED";
var LEGACY_USER_NETWORKING_SERVER_SHUTDOWN_ERROR_TYPE = "SERVER_SHUTDOWN";
var LEGACY_USER_NETWORKING_UNKNOWN_ERROR = "UNKNOWN_ERROR";
// src/user-networking-settings.ts
var pingPongRate = 1500;
var heartBeatRate = 15e3;
var packetsUpdateRate = 1 / 30 * 1e3;
// src/UserNetworkingServer.ts
// src/legacy/LegacyAdapter.ts
function toArrayBuffer(buffer) {
const arrayBuffer = new ArrayBuffer(buffer.length);
const view = new Uint8Array(arrayBuffer);
for (let i = 0; i < buffer.length; ++i) {
view[i] = buffer[i];
}
return arrayBuffer;
}
var WebSocketOpenStatus = 1;
var UserNetworkingServer = class {
constructor() {
this.clients = /* @__PURE__ */ new Map();
this.clientLastPong = /* @__PURE__ */ new Map();
setInterval(this.sendUpdates.bind(this), packetsUpdateRate);
setInterval(this.pingClients.bind(this), pingPongRate);
setInterval(this.heartBeat.bind(this), heartBeatRate);
var LegacyAdapter = class {
constructor(userNetworkingServer, deltaNetServer, logger) {
this.userNetworkingServer = userNetworkingServer;
this.deltaNetServer = deltaNetServer;
this.logger = logger;
}
heartBeat() {
const now = Date.now();
this.clientLastPong.forEach((clientLastPong, id) => {
if (now - clientLastPong > heartBeatRate) {
this.clients.delete(id);
this.clientLastPong.delete(id);
const disconnectMessage = JSON.stringify({
id,
type: DISCONNECTED_MESSAGE_TYPE
});
for (const { socket: otherSocket } of this.clients.values()) {
if (otherSocket.readyState === WebSocketOpenStatus) {
otherSocket.send(disconnectMessage);
}
}
}
});
}
pingClients() {
this.clients.forEach((client) => {
allClientsById = /* @__PURE__ */ new Map();
legacyAuthenticatedClientsById = /* @__PURE__ */ new Map();
broadcastMessage(broadcastType, broadcastPayload) {
if (broadcastType !== SERVER_BROADCAST_MESSAGE_TYPE) {
return;
}
const parsedPayload = parseServerBroadcastMessage(broadcastPayload);
if (parsedPayload instanceof Error) {
this.logger.error("Error parsing server broadcast message", parsedPayload);
return;
}
const { broadcastType: broadcastTypeString, payload } = parsedPayload;
const message = {
type: "broadcast",
broadcastType: broadcastTypeString,
payload
};
const messageString = JSON.stringify(message);
for (const [, client] of this.legacyAuthenticatedClientsById) {
if (client.socket.readyState === WebSocketOpenStatus) {
client.socket.send(JSON.stringify({ type: "ping" }));
client.socket.send(messageString);
}
});
}
}
getId() {
let id = 1;
while (this.clients.has(id))
id++;
return id;
}
connectClient(socket) {
const id = this.getId();
console.log(`Client ID: ${id} joined`);
const connectMessage = JSON.stringify({
addWebSocket(socket) {
const id = this.userNetworkingServer.getLegacyClientId();
const client = {
id,
type: CONNECTED_MESSAGE_TYPE
});
for (const { socket: otherSocket } of this.clients.values()) {
if (otherSocket.readyState === WebSocketOpenStatus) {
otherSocket.send(connectMessage);
}
}
const identityMessage = JSON.stringify({
id,
type: IDENTITY_MESSAGE_TYPE
});
socket.send(identityMessage);
for (const { update } of this.clients.values()) {
socket.send(UserNetworkingCodec.encodeUpdate(update));
}
this.clients.set(id, {
lastPong: Date.now(),
socket,
authenticatedUser: null,
update: {

@@ -112,42 +366,303 @@ id,

}
});
socket.on("message", (message, _isBinary) => {
if (message instanceof Buffer) {
const arrayBuffer = new Uint8Array(message).buffer;
const update = UserNetworkingCodec.decodeUpdate(arrayBuffer);
update.id = id;
if (this.clients.get(id) !== void 0) {
this.clients.get(id).update = update;
}
} else {
try {
const data = JSON.parse(message);
if (data.type === "pong") {
this.clientLastPong.set(id, Date.now());
};
this.allClientsById.set(id, client);
socket.addEventListener("message", (message) => {
try {
if (message.data instanceof ArrayBuffer || message.data instanceof Buffer) {
if (client.authenticatedUser) {
const arrayBuffer = message.data instanceof ArrayBuffer ? message.data : toArrayBuffer(message.data);
const update = LegacyUserNetworkingCodec.decodeUpdate(arrayBuffer);
update.id = id;
const index = this.deltaNetServer.dangerouslyGetConnectionsToComponentIndex().get(id);
client.update = update;
if (index !== void 0) {
this.deltaNetServer.setComponentValue(
COMPONENT_POSITION_X,
index,
BigInt(Math.round(update.position.x * positionMultiplier))
);
this.deltaNetServer.setComponentValue(
COMPONENT_POSITION_Y,
index,
BigInt(Math.round(update.position.y * positionMultiplier))
);
this.deltaNetServer.setComponentValue(
COMPONENT_POSITION_Z,
index,
BigInt(Math.round(update.position.z * positionMultiplier))
);
this.deltaNetServer.setComponentValue(
COMPONENT_ROTATION_Y,
index,
BigInt(Math.round(update.rotation.quaternionY * rotationMultiplier))
);
this.deltaNetServer.setComponentValue(
COMPONENT_ROTATION_W,
index,
BigInt(Math.round(update.rotation.quaternionW * rotationMultiplier))
);
this.deltaNetServer.setComponentValue(
COMPONENT_STATE,
index,
BigInt(Math.round(update.state))
);
}
}
} catch (e) {
console.error("Error parsing JSON message", message, e);
} else {
let parsed;
try {
parsed = JSON.parse(message.data);
} catch (e) {
this.logger.error("Error parsing JSON message", message, e);
return;
}
if (!client.authenticatedUser) {
if (parsed.type === LEGACY_USER_NETWORKING_USER_AUTHENTICATE_MESSAGE_TYPE) {
this.handleUserAuth(client, parsed).then((authResult) => {
if (client.socket.readyState !== WebSocketOpenStatus) {
return;
}
if (!authResult) {
this.logger.error(`Client-id ${client.id} user_auth failed`, authResult);
const serverError = JSON.stringify({
type: LEGACY_USER_NETWORKING_SERVER_ERROR_MESSAGE_TYPE,
errorType: LEGACY_USER_NETWORKING_AUTHENTICATION_FAILED_ERROR_TYPE,
message: "Authentication failed"
});
socket.send(serverError);
socket.close();
} else {
if (!this.userNetworkingServer.hasCapacityForLegacyClient()) {
const serverError = JSON.stringify({
type: LEGACY_USER_NETWORKING_SERVER_ERROR_MESSAGE_TYPE,
errorType: LEGACY_USER_NETWORKING_CONNECTION_LIMIT_REACHED_ERROR_TYPE,
message: "Connection limit reached"
});
socket.send(serverError);
socket.close();
return;
}
const userData = authResult;
this.deltaNetServer.dangerouslyAddNewJoinerCallback((index) => {
if (client.socket.readyState !== WebSocketOpenStatus) {
return null;
}
client.authenticatedUser = userData;
this.deltaNetServer.setComponentValue(COMPONENT_POSITION_X, index, BigInt(0));
this.deltaNetServer.setComponentValue(COMPONENT_POSITION_Y, index, BigInt(0));
this.deltaNetServer.setComponentValue(COMPONENT_POSITION_Z, index, BigInt(0));
this.deltaNetServer.setComponentValue(COMPONENT_ROTATION_Y, index, BigInt(0));
this.deltaNetServer.setComponentValue(COMPONENT_ROTATION_W, index, BigInt(0));
this.deltaNetServer.setComponentValue(COMPONENT_STATE, index, BigInt(0));
const asUserData = {
...userData,
colors: []
};
return {
id: client.id,
afterAddCallback: () => {
this.userNetworkingServer.setAuthenticatedLegacyClientConnection(
client.id,
client.socket,
asUserData
);
this.userNetworkingServer.updateUserCharacter(client.id, asUserData);
}
};
});
const userProfileMessage = JSON.stringify({
id: client.id,
type: LEGACY_USER_NETWORKING_USER_PROFILE_MESSAGE_TYPE,
username: userData.username,
characterDescription: userData.characterDescription
});
client.socket.send(userProfileMessage);
const identityMessage = JSON.stringify({
id: client.id,
type: LEGACY_USER_NETWORKING_IDENTITY_MESSAGE_TYPE
});
client.socket.send(identityMessage);
const allUsers = this.deltaNetServer.dangerouslyGetConnectionsToComponentIndex();
for (const [connectionId, componentIndex] of allUsers) {
if (connectionId === client.id) {
continue;
}
const x = this.deltaNetServer.getComponentValue(COMPONENT_POSITION_X, componentIndex) ?? 0 / positionMultiplier;
const y = this.deltaNetServer.getComponentValue(COMPONENT_POSITION_Y, componentIndex) ?? 0 / positionMultiplier;
const z = this.deltaNetServer.getComponentValue(COMPONENT_POSITION_Z, componentIndex) ?? 0 / positionMultiplier;
const quaternionY = this.deltaNetServer.getComponentValue(COMPONENT_ROTATION_Y, componentIndex) ?? 0 / rotationMultiplier;
const quaternionW = this.deltaNetServer.getComponentValue(COMPONENT_ROTATION_W, componentIndex) ?? 0 / rotationMultiplier;
const state = this.deltaNetServer.getComponentValue(COMPONENT_STATE, componentIndex) ?? 0;
const update = LegacyUserNetworkingCodec.encodeUpdate({
id: connectionId,
position: { x, y, z },
rotation: { quaternionY, quaternionW },
state
});
client.socket.send(
JSON.stringify({
id: connectionId,
type: LEGACY_USER_NETWORKING_USER_PROFILE_MESSAGE_TYPE,
username: this.userNetworkingServer.getUsername(connectionId),
characterDescription: this.userNetworkingServer.getCharacterDescription(connectionId)
})
);
client.socket.send(update);
}
this.legacyAuthenticatedClientsById.set(id, client);
}
});
} else {
this.logger.error(`Unhandled message pre-auth: ${JSON.stringify(parsed)}`);
socket.close();
}
} else {
switch (parsed.type) {
case LEGACY_USER_NETWORKING_PONG_MESSAGE_TYPE:
client.lastPong = Date.now();
break;
case LEGACY_USER_NETWORKING_USER_UPDATE_MESSAGE_TYPE:
this.handleUserUpdate(id, parsed);
break;
default:
this.logger.error(`Unhandled message: ${JSON.stringify(parsed)}`);
}
}
}
} catch (e) {
this.logger.error("Error handling message", message, e);
socket.send(
JSON.stringify({
type: LEGACY_USER_NETWORKING_SERVER_ERROR_MESSAGE_TYPE,
errorType: LEGACY_USER_NETWORKING_UNKNOWN_ERROR,
message: "Error handling message"
})
);
socket.close();
}
});
socket.on("close", () => {
console.log("Client disconnected", id);
this.clients.delete(id);
socket.addEventListener("close", () => {
this.handleDisconnectedClient(client);
});
}
handleDisconnectedClient(client) {
if (!this.allClientsById.has(client.id)) {
return;
}
this.allClientsById.delete(client.id);
if (client.authenticatedUser !== null) {
this.userNetworkingServer.onLegacyClientDisconnect(client.id);
this.legacyAuthenticatedClientsById.delete(client.id);
this.deltaNetServer.clearInternalConnectionId(client.id);
}
}
async handleUserAuth(client, credentials) {
const userData = this.userNetworkingServer.onLegacyClientConnect(
client.id,
credentials.sessionToken,
credentials.userIdentity
);
let resolvedUserData;
if (userData instanceof Promise) {
resolvedUserData = await userData;
} else {
resolvedUserData = userData;
}
if (resolvedUserData instanceof Error) {
this.logger.error(`Client-id ${client.id} user_auth failed`, resolvedUserData);
return false;
} else if (resolvedUserData === true) {
this.logger.error(`Client-id ${client.id} user_auth failed`, resolvedUserData);
resolvedUserData = credentials.userIdentity;
} else {
resolvedUserData = resolvedUserData;
}
if (resolvedUserData === null) {
this.logger.error(`Client-id ${client.id} user_auth unauthorized and ignored`);
return false;
}
return resolvedUserData;
}
updateUserCharacter(clientId, userData) {
this.internalUpdateUser(clientId, userData);
}
internalUpdateUser(clientId, userData) {
const client = this.legacyAuthenticatedClientsById.get(clientId);
client.authenticatedUser = userData;
this.legacyAuthenticatedClientsById.set(clientId, client);
this.userNetworkingServer.updateUserCharacter(client.id, { ...userData, colors: [] });
}
async handleUserUpdate(clientId, message) {
const client = this.legacyAuthenticatedClientsById.get(clientId);
if (!client) {
this.logger.error(`Client-id ${clientId} user_update ignored, client not found`);
return;
}
const authorizedUserData = message.userIdentity;
let resolvedAuthorizedUserData;
if (authorizedUserData instanceof Promise) {
resolvedAuthorizedUserData = await authorizedUserData;
} else {
resolvedAuthorizedUserData = authorizedUserData;
}
if (!resolvedAuthorizedUserData) {
this.logger.warn(`Client-id ${clientId} user_update unauthorized and ignored`);
return;
}
this.internalUpdateUser(clientId, resolvedAuthorizedUserData);
}
sendUpdates(removedIds, addedIds, updateUserProfilesInTick) {
for (const id of removedIds) {
const disconnectMessage = JSON.stringify({
id,
type: DISCONNECTED_MESSAGE_TYPE
type: LEGACY_USER_NETWORKING_DISCONNECTED_MESSAGE_TYPE
});
for (const [clientId, { socket: otherSocket }] of this.clients) {
if (otherSocket.readyState === WebSocketOpenStatus) {
otherSocket.send(disconnectMessage);
for (const [, otherClient] of this.legacyAuthenticatedClientsById) {
if (otherClient.socket.readyState === WebSocketOpenStatus) {
otherClient.socket.send(disconnectMessage);
}
}
});
}
sendUpdates() {
for (const [clientId, client] of this.clients) {
const update = client.update;
const encodedUpdate = UserNetworkingCodec.encodeUpdate(update);
for (const [otherClientId, otherClient] of this.clients) {
if (otherClientId !== clientId && otherClient.socket.readyState === WebSocketOpenStatus) {
}
for (const id of addedIds) {
const identityMessage = JSON.stringify({
id,
type: LEGACY_USER_NETWORKING_USER_PROFILE_MESSAGE_TYPE,
username: this.userNetworkingServer.getUsername(id),
characterDescription: this.userNetworkingServer.getCharacterDescription(id)
});
for (const [, otherClient] of this.legacyAuthenticatedClientsById) {
if (otherClient.socket.readyState === WebSocketOpenStatus) {
otherClient.socket.send(identityMessage);
}
}
}
for (const id of updateUserProfilesInTick) {
const identityMessage = JSON.stringify({
id,
type: LEGACY_USER_NETWORKING_USER_PROFILE_MESSAGE_TYPE,
username: this.userNetworkingServer.getUsername(id),
characterDescription: this.userNetworkingServer.getCharacterDescription(id)
});
for (const [, otherClient] of this.legacyAuthenticatedClientsById) {
if (otherClient.socket.readyState === WebSocketOpenStatus) {
otherClient.socket.send(identityMessage);
}
}
}
const allUsers = this.deltaNetServer.dangerouslyGetConnectionsToComponentIndex();
for (const [connectionId, componentIndex] of allUsers) {
const x = this.deltaNetServer.getComponentValue(COMPONENT_POSITION_X, componentIndex) ?? 0 / positionMultiplier;
const y = this.deltaNetServer.getComponentValue(COMPONENT_POSITION_Y, componentIndex) ?? 0 / positionMultiplier;
const z = this.deltaNetServer.getComponentValue(COMPONENT_POSITION_Z, componentIndex) ?? 0 / positionMultiplier;
const quaternionY = this.deltaNetServer.getComponentValue(COMPONENT_ROTATION_Y, componentIndex) ?? 0 / rotationMultiplier;
const quaternionW = this.deltaNetServer.getComponentValue(COMPONENT_ROTATION_W, componentIndex) ?? 0 / rotationMultiplier;
const state = this.deltaNetServer.getComponentValue(COMPONENT_STATE, componentIndex) ?? 0;
const encodedUpdate = LegacyUserNetworkingCodec.encodeUpdate({
id: connectionId,
position: { x, y, z },
rotation: { quaternionY, quaternionW },
state
});
for (const [otherClientId, otherClient] of this.legacyAuthenticatedClientsById) {
if (otherClientId !== connectionId && otherClient.socket.readyState === WebSocketOpenStatus) {
otherClient.socket.send(encodedUpdate);

@@ -158,204 +673,826 @@ }

}
dispose(clientCloseError) {
const stringifiedError = clientCloseError ? JSON.stringify(clientCloseError) : void 0;
for (const [, client] of this.legacyAuthenticatedClientsById) {
if (stringifiedError) {
client.socket.send(stringifiedError);
}
client.socket.close();
}
}
};
// src/ReconnectingWebSocket.ts
var WebsocketStatus = /* @__PURE__ */ ((WebsocketStatus3) => {
WebsocketStatus3[WebsocketStatus3["Connecting"] = 0] = "Connecting";
WebsocketStatus3[WebsocketStatus3["Connected"] = 1] = "Connected";
WebsocketStatus3[WebsocketStatus3["Reconnecting"] = 2] = "Reconnecting";
WebsocketStatus3[WebsocketStatus3["Disconnected"] = 3] = "Disconnected";
return WebsocketStatus3;
})(WebsocketStatus || {});
var startingBackoffTimeMilliseconds = 100;
var maximumBackoffTimeMilliseconds = 1e4;
var maximumWebsocketConnectionTimeout = 5e3;
var ReconnectingWebSocket = class {
constructor(url, websocketFactory, statusUpdateCallback) {
this.url = url;
this.websocketFactory = websocketFactory;
this.statusUpdateCallback = statusUpdateCallback;
this.websocket = null;
this.status = null;
this.receivedMessageSinceOpen = false;
this.backoffTime = startingBackoffTimeMilliseconds;
this.stopped = false;
this.setStatus(0 /* Connecting */);
this.startWebSocketConnectionAttempt();
// src/UserNetworkingLogger.ts
var UserNetworkingConsoleLogger = class {
trace(...args) {
console.trace(...args);
}
setStatus(status) {
if (this.status !== status) {
this.status = status;
this.statusUpdateCallback(status);
}
debug(...args) {
console.debug(...args);
}
sendUpdate(update) {
if (!this.websocket) {
console.error("Not connected to the server");
return;
}
const encodedUpdate = UserNetworkingCodec.encodeUpdate(update);
this.send(encodedUpdate);
info(...args) {
console.info(...args);
}
async startWebSocketConnectionAttempt() {
if (this.stopped) {
return;
}
while (true) {
if (this.stopped) {
warn(...args) {
console.warn(...args);
}
error(...args) {
console.error(...args);
}
};
// src/UserNetworkingServer.ts
var UserNetworkingServer = class {
constructor(options, logger = new UserNetworkingConsoleLogger()) {
this.options = options;
this.logger = logger;
this.deltaNetServer = new DeltaNetServer({
serverConnectionIdStateId: 0,
onJoiner: (joiner) => {
return this.handleJoiner(joiner);
},
onLeave: (leave) => {
this.handleLeave(leave);
},
onComponentsUpdate: (update) => {
return;
},
onStatesUpdate: (update) => {
return this.handleStatesUpdate(update);
},
onCustomMessage: (customMessage) => {
this.handleCustomMessage(customMessage);
}
try {
await this.createWebsocketWithTimeout(maximumWebsocketConnectionTimeout);
break;
} catch (e) {
this.setStatus(2 /* Reconnecting */);
await this.waitBackoffTime();
});
if (this.options.legacyAdapterEnabled) {
this.legacyAdapter = new LegacyAdapter(this, this.deltaNetServer, this.logger);
}
this.tickInterval = setInterval(() => {
const { removedIds, addedIds } = this.deltaNetServer.tick();
if (this.legacyAdapter) {
this.legacyAdapter.sendUpdates(removedIds, addedIds, this.updatedUserProfilesInTick);
this.updatedUserProfilesInTick.clear();
}
}
}, 50);
}
async waitBackoffTime() {
console.warn(`Websocket connection to '${this.url}' failed: retrying in ${this.backoffTime}ms`);
await new Promise((resolve) => setTimeout(resolve, this.backoffTime));
this.backoffTime = Math.min(
// Introduce a small amount of randomness to prevent clients from retrying in lockstep
this.backoffTime * (1.5 + Math.random() * 0.5),
maximumBackoffTimeMilliseconds
deltaNetServer;
authenticatedClientsById = /* @__PURE__ */ new Map();
tickInterval;
legacyAdapter = null;
updatedUserProfilesInTick = /* @__PURE__ */ new Set();
getCharacterDescription(connectionId) {
var _a;
const client = this.authenticatedClientsById.get(connectionId);
return ((_a = client == null ? void 0 : client.authenticatedUser) == null ? void 0 : _a.characterDescription) ?? { mmlCharacterUrl: "" };
}
getUsername(connectionId) {
var _a, _b;
const client = this.authenticatedClientsById.get(connectionId);
this.logger.info("getUsername", connectionId, (_a = client == null ? void 0 : client.authenticatedUser) == null ? void 0 : _a.username);
return ((_b = client == null ? void 0 : client.authenticatedUser) == null ? void 0 : _b.username) ?? "";
}
getLegacyClientId() {
return this.deltaNetServer.getNextConnectionId();
}
hasCapacityForLegacyClient() {
return true;
}
onLegacyClientConnect(id, sessionToken, userIdentity) {
return this.options.onClientConnect(id, sessionToken, {
username: (userIdentity == null ? void 0 : userIdentity.username) ?? null,
characterDescription: (userIdentity == null ? void 0 : userIdentity.characterDescription) ?? null,
colors: null
});
}
setAuthenticatedLegacyClientConnection(clientId, webSocket, userData) {
this.logger.info("setAuthenticatedLegacyClientConnection", clientId, userData);
const authenticatedClient = {
id: clientId,
socket: webSocket,
lastPong: Date.now(),
authenticatedUser: userData,
deltaNetConnection: null
};
this.authenticatedClientsById.set(clientId, authenticatedClient);
}
onLegacyClientDisconnect(id) {
this.options.onClientDisconnect(id);
}
handleStatesUpdate(update) {
const deltaNetConnection = update.deltaNetV01Connection;
const clientId = deltaNetConnection.internalConnectionId;
const updatedStates = update.states;
const updatedStatesMap = new Map(updatedStates);
const updatedUserData = DeltaNetComponentMapping.fromUserStates(
updatedStatesMap,
this.logger
);
const existingClient = this.authenticatedClientsById.get(clientId);
if (!existingClient) {
return new DeltaNetServerError2(
DeltaNetV01ServerErrors.USER_AUTHENTICATION_FAILED_ERROR_TYPE,
"User not authenticated - no client found",
false
);
}
const existingUserData = existingClient.authenticatedUser ?? {};
const userData = {
...existingUserData,
...updatedUserData
};
const res = this.options.onClientUserIdentityUpdate(clientId, userData);
if (res instanceof Promise) {
return res.then((res2) => {
if (!this.authenticatedClientsById.get(clientId)) {
return new DeltaNetServerError2(
DeltaNetV01ServerErrors.USER_AUTHENTICATION_FAILED_ERROR_TYPE,
"User not authenticated - client disconnected",
false
);
}
if (res2 instanceof DeltaNetServerError2) {
return res2;
}
if (res2 instanceof Error) {
return new DeltaNetServerError2(
DeltaNetV01ServerErrors.USER_AUTHENTICATION_FAILED_ERROR_TYPE,
"User identity update failed",
false
);
}
if (res2 === null) {
return new DeltaNetServerError2(
DeltaNetV01ServerErrors.USER_AUTHENTICATION_FAILED_ERROR_TYPE,
"User identity update failed",
false
);
}
if (res2 === false) {
return new DeltaNetServerError2(
DeltaNetV01ServerErrors.USER_AUTHENTICATION_FAILED_ERROR_TYPE,
"User identity update failed",
false
);
}
if (!res2 || typeof res2 !== "object") {
return new DeltaNetServerError2(
DeltaNetV01ServerErrors.USER_AUTHENTICATION_FAILED_ERROR_TYPE,
"User identity update failed",
false
);
}
this.updatedUserProfilesInTick.add(clientId);
existingClient.authenticatedUser = {
...existingClient.authenticatedUser,
...res2
};
return {
success: true,
stateOverrides: Array.from(DeltaNetComponentMapping.toStates(res2).entries())
};
});
}
if (res instanceof DeltaNetServerError2) {
return res;
}
if (res instanceof Error) {
return new DeltaNetServerError2(
DeltaNetV01ServerErrors.USER_AUTHENTICATION_FAILED_ERROR_TYPE,
"User identity update failed",
false
);
}
if (res === null) {
return new DeltaNetServerError2(
DeltaNetV01ServerErrors.USER_AUTHENTICATION_FAILED_ERROR_TYPE,
"User identity update failed",
false
);
}
if (res === false) {
return new DeltaNetServerError2(
DeltaNetV01ServerErrors.USER_AUTHENTICATION_FAILED_ERROR_TYPE,
"User identity update failed",
false
);
}
if (!res || typeof res !== "object") {
return new DeltaNetServerError2(
DeltaNetV01ServerErrors.USER_AUTHENTICATION_FAILED_ERROR_TYPE,
"User identity update failed",
false
);
}
this.updatedUserProfilesInTick.add(clientId);
existingClient.authenticatedUser = {
...existingClient.authenticatedUser,
...res
};
return {
success: true,
stateOverrides: Array.from(DeltaNetComponentMapping.toStates(res).entries())
};
}
send(message) {
if (!this.websocket) {
console.error("Not connected to the server");
return;
handleJoiner(joiner) {
const deltaNetConnection = joiner.deltaNetV01Connection;
const webSocket = deltaNetConnection.webSocket;
const states = joiner.states;
const clientId = joiner.internalConnectionId;
const statesMap = new Map(states);
const userData = DeltaNetComponentMapping.fromUserStates(statesMap, this.logger);
return this.handleDeltaNetAuthentication(
clientId,
webSocket,
deltaNetConnection,
joiner.token,
userData
).then((authResult) => {
var _a;
if (!authResult.success) {
this.logger.warn(`Authentication failed for client ID: ${clientId}`, authResult.error);
return new DeltaNetServerError2(
DeltaNetV01ServerErrors.USER_AUTHENTICATION_FAILED_ERROR_TYPE,
((_a = authResult.error) == null ? void 0 : _a.message) || "Authentication failed",
false
);
} else {
return {
success: true,
stateOverrides: authResult.stateOverrides
};
}
}).catch((error) => {
this.logger.error(`Authentication error for client ID: ${clientId}:`, error);
return new DeltaNetServerError2(
DeltaNetV01ServerErrors.USER_AUTHENTICATION_FAILED_ERROR_TYPE,
"Authentication error",
false
);
});
}
handleLeave(leave) {
const deltaNetConnection = leave.deltaNetV01Connection;
const clientId = deltaNetConnection.internalConnectionId;
if (clientId !== void 0) {
const client = this.authenticatedClientsById.get(clientId);
if (client) {
this.options.onClientDisconnect(clientId);
this.authenticatedClientsById.delete(clientId);
}
}
if (message instanceof Uint8Array) {
this.websocket.send(message);
}
handleCustomMessage(customMessage) {
const deltaNetConnection = customMessage.deltaNetV01Connection;
const clientId = deltaNetConnection.internalConnectionId;
const client = this.authenticatedClientsById.get(clientId);
if (client && client.authenticatedUser) {
if (customMessage.customType === FROM_CLIENT_CHAT_MESSAGE_TYPE) {
const chatMessage = parseClientChatMessage(customMessage.contents);
if (chatMessage instanceof Error) {
this.logger.error(`Invalid chat message from client ${clientId}:`, chatMessage);
} else {
const serverChatMessage = {
fromUserId: clientId,
message: chatMessage.message
};
this.deltaNetServer.broadcastCustomMessage(
FROM_SERVER_CHAT_MESSAGE_TYPE,
JSON.stringify(serverChatMessage)
);
}
}
} else {
this.websocket.send(JSON.stringify(message));
this.logger.warn(`Custom message from unauthenticated client ${clientId} - ignoring`);
}
}
createWebsocketWithTimeout(timeout) {
return new Promise((resolve, reject) => {
const timeoutId = setTimeout(() => {
reject(new Error("websocket connection timed out"));
}, timeout);
const websocket = this.websocketFactory(this.url);
websocket.binaryType = "arraybuffer";
websocket.addEventListener("open", () => {
clearTimeout(timeoutId);
this.receivedMessageSinceOpen = false;
this.websocket = websocket;
this.setStatus(1 /* Connected */);
websocket.addEventListener("message", (event) => {
if (websocket !== this.websocket) {
console.log("Ignoring websocket message event because it is no longer current");
websocket.close();
return;
}
if (!this.receivedMessageSinceOpen) {
this.receivedMessageSinceOpen = true;
}
this.handleIncomingWebsocketMessage(event);
});
const onWebsocketClose = async () => {
if (websocket !== this.websocket) {
console.log("Ignoring websocket close event because it is no longer current");
return;
}
this.websocket = null;
if (this.stopped) {
this.setStatus(3 /* Disconnected */);
return;
}
if (!this.receivedMessageSinceOpen) {
await this.waitBackoffTime();
}
this.setStatus(2 /* Reconnecting */);
this.startWebSocketConnectionAttempt();
};
websocket.addEventListener("close", (e) => {
if (websocket !== this.websocket) {
console.warn("Ignoring websocket close event because it is no longer current");
return;
}
console.log("NetworkedDOMWebsocket close", e);
onWebsocketClose();
});
websocket.addEventListener("error", (e) => {
if (websocket !== this.websocket) {
console.log("Ignoring websocket error event because it is no longer current");
return;
}
console.error("NetworkedDOMWebsocket error", e);
onWebsocketClose();
});
resolve(websocket);
});
websocket.addEventListener("error", (e) => {
clearTimeout(timeoutId);
reject(e);
});
async handleDeltaNetAuthentication(clientId, webSocket, deltaNetConnection, sessionToken, userIdentity) {
try {
let onClientConnectReturn = deltaNetConnection.isObserver ? null : await this.options.onClientConnect(clientId, sessionToken, userIdentity);
if (!deltaNetConnection.isObserver && !onClientConnectReturn) {
this.logger.warn(`Authentication failed for client ${clientId} - no user data returned`);
return { success: false };
}
if (onClientConnectReturn instanceof Error) {
return { success: false, error: onClientConnectReturn };
}
if (onClientConnectReturn === true) {
onClientConnectReturn = userIdentity;
}
const authenticatedUser = onClientConnectReturn;
const authenticatedClient = {
id: clientId,
socket: webSocket,
lastPong: Date.now(),
authenticatedUser,
deltaNetConnection
};
this.authenticatedClientsById.set(clientId, authenticatedClient);
let stateOverrides = [];
if (onClientConnectReturn) {
const officialStates = DeltaNetComponentMapping.toStates(onClientConnectReturn);
stateOverrides = Array.from(officialStates.entries());
}
return {
success: true,
stateOverrides
};
} catch (error) {
this.logger.error("Authentication error:", error);
return { success: false };
}
}
connectClient(socket) {
if (socket.protocol === "") {
if (this.legacyAdapter) {
this.legacyAdapter.addWebSocket(socket);
return;
} else {
socket.close(1e3, "Legacy client detected (no subprotocol) - not supported");
return;
}
}
this.deltaNetServer.addWebSocket(socket);
socket.addEventListener("close", () => {
this.deltaNetServer.removeWebSocket(socket);
});
}
stop() {
this.stopped = true;
if (this.websocket !== null) {
this.websocket.close();
this.websocket = null;
broadcastMessage(broadcastType, broadcastPayload) {
this.deltaNetServer.broadcastCustomMessage(broadcastType, broadcastPayload);
if (this.legacyAdapter) {
this.legacyAdapter.broadcastMessage(broadcastType, broadcastPayload);
}
}
updateUserCharacter(clientId, userData) {
this.logger.info("updateUserCharacter", clientId, userData);
this.internalUpdateUser(clientId, userData);
}
updateUserUsername(clientId, username) {
const client = this.authenticatedClientsById.get(clientId);
if (!client || !client.authenticatedUser) return;
client.authenticatedUser = {
...client.authenticatedUser,
username
};
this.updatedUserProfilesInTick.add(clientId);
const states = DeltaNetComponentMapping.toUsernameState(username);
const asArray = Array.from(states.entries());
this.deltaNetServer.overrideUserStates(client.deltaNetConnection, clientId, asArray);
}
updateUserCharacterDescription(clientId, characterDescription) {
const client = this.authenticatedClientsById.get(clientId);
if (!client || !client.authenticatedUser) return;
client.authenticatedUser = {
...client.authenticatedUser,
characterDescription
};
this.updatedUserProfilesInTick.add(clientId);
const states = DeltaNetComponentMapping.toCharacterDescriptionState(characterDescription);
const asArray = Array.from(states.entries());
this.deltaNetServer.overrideUserStates(client.deltaNetConnection, clientId, asArray);
}
updateUserColors(clientId, colors) {
const client = this.authenticatedClientsById.get(clientId);
if (!client || !client.authenticatedUser) return;
client.authenticatedUser = {
...client.authenticatedUser,
colors
};
this.updatedUserProfilesInTick.add(clientId);
const states = DeltaNetComponentMapping.toColorsState(colors);
const asArray = Array.from(states.entries());
this.deltaNetServer.overrideUserStates(client.deltaNetConnection, clientId, asArray);
}
updateUserStates(clientId, updates) {
const client = this.authenticatedClientsById.get(clientId);
if (!client || !client.authenticatedUser) return;
const states = /* @__PURE__ */ new Map();
let hasUpdates = false;
let updatedUserData = client.authenticatedUser;
this.updatedUserProfilesInTick.add(clientId);
if (updates.username !== null) {
updatedUserData = {
...updatedUserData,
username: updates.username
};
const usernameStates = DeltaNetComponentMapping.toUsernameState(updates.username);
for (const [stateId, stateValue] of usernameStates) {
states.set(stateId, stateValue);
}
hasUpdates = true;
}
if (updates.characterDescription !== null) {
updatedUserData = {
...updatedUserData,
characterDescription: updates.characterDescription
};
const characterDescStates = DeltaNetComponentMapping.toCharacterDescriptionState(
updates.characterDescription
);
for (const [stateId, stateValue] of characterDescStates) {
states.set(stateId, stateValue);
}
hasUpdates = true;
}
if (updates.colors !== null) {
updatedUserData = {
...updatedUserData,
colors: updates.colors
};
const colorsStates = DeltaNetComponentMapping.toColorsState(updates.colors);
for (const [stateId, stateValue] of colorsStates) {
states.set(stateId, stateValue);
}
hasUpdates = true;
}
if (hasUpdates) {
client.authenticatedUser = updatedUserData;
const asArray = Array.from(states.entries());
this.deltaNetServer.overrideUserStates(client.deltaNetConnection, clientId, asArray);
}
}
internalUpdateUser(clientId, userData) {
const client = this.authenticatedClientsById.get(clientId);
if (!client) {
throw new Error(`internalUpdateUser - client not found for clientId ${clientId}`);
}
this.logger.info("internalUpdateUser", clientId, userData);
this.updatedUserProfilesInTick.add(clientId);
client.authenticatedUser = {
...client.authenticatedUser,
...userData
};
const states = DeltaNetComponentMapping.toStates(userData);
const asArray = Array.from(states.entries());
this.deltaNetServer.overrideUserStates(client.deltaNetConnection, clientId, asArray);
}
dispose(clientCloseError) {
if (this.tickInterval) {
clearInterval(this.tickInterval);
}
let errorMessage = null;
if (clientCloseError) {
errorMessage = encodeError({
type: "error",
errorType: clientCloseError.errorType,
message: clientCloseError.message,
retryable: clientCloseError.retryable
}).getBuffer();
}
for (const [, client] of this.authenticatedClientsById) {
if (errorMessage) {
client.socket.send(errorMessage);
}
client.socket.close();
}
this.authenticatedClientsById.clear();
}
};
// src/UserNetworkingClient.ts
var UserNetworkingClient = class extends ReconnectingWebSocket {
constructor(url, websocketFactory, statusUpdateCallback, setIdentityCallback, clientUpdate) {
super(url, websocketFactory, statusUpdateCallback);
this.setIdentityCallback = setIdentityCallback;
this.clientUpdate = clientUpdate;
import {
DeltaNetClientState,
DeltaNetClientWebsocket,
DeltaNetClientWebsocketStatus
} from "@mml-io/delta-net-web";
// src/types.ts
var WebsocketStatus = /* @__PURE__ */ ((WebsocketStatus2) => {
WebsocketStatus2[WebsocketStatus2["Connecting"] = 0] = "Connecting";
WebsocketStatus2[WebsocketStatus2["Connected"] = 1] = "Connected";
WebsocketStatus2[WebsocketStatus2["Reconnecting"] = 2] = "Reconnecting";
WebsocketStatus2[WebsocketStatus2["Disconnected"] = 3] = "Disconnected";
return WebsocketStatus2;
})(WebsocketStatus || {});
// src/UserNetworkingClient.ts
var UserNetworkingClient = class {
constructor(config, initialUserState, initialUpdate, logger = new UserNetworkingConsoleLogger()) {
this.config = config;
this.logger = logger;
this.pendingUpdate = initialUpdate ?? {
position: { x: 0, y: 0, z: 0 },
rotation: { quaternionY: 0, quaternionW: 1 },
state: 0
};
this.userState = initialUserState ?? {
username: null,
characterDescription: null,
colors: null
};
this.deltaNetState = new DeltaNetClientState();
this.deltaNetClient = new DeltaNetClientWebsocket(
config.url,
(url) => {
const ws = config.websocketFactory(url);
return ws;
},
config.sessionToken,
{
ignoreData: false,
onInitialCheckout: (initialCheckout) => {
const { addedStableIds } = this.deltaNetState.handleInitialCheckout(initialCheckout);
const networkUpdate = this.processNetworkUpdate([], addedStableIds, []);
this.config.onUpdate(networkUpdate);
if (this.userIndex !== null) {
const userIds = this.deltaNetState.getStableIds();
if (this.userIndex < userIds.length) {
const stableId = userIds[this.userIndex];
const userId = this.stableIdToUserId.get(stableId);
if (!userId) {
throw new Error(`No userId found for stableId ${stableId}`);
}
this.userId = userId;
this.isAuthenticated = true;
this.config.assignedIdentity(this.userId);
} else {
this.logger.error(
`Invalid userIndex ${this.userIndex}, userIds length: ${userIds.length}`
);
}
}
},
onTick: (tick) => {
const { stateUpdates, removedStableIds, addedStableIds } = this.deltaNetState.handleTick(tick);
const networkUpdate = this.processNetworkUpdate(
removedStableIds,
addedStableIds,
stateUpdates
);
this.config.onUpdate(networkUpdate);
},
onUserIndex: (userIndex) => {
this.userIndex = userIndex.userIndex;
this.deltaNetState.setLocalIndex(userIndex.userIndex);
},
onError: (errorType, errorMessage, retryable) => {
this.logger.error(
"DeltaNet error:",
errorType,
"errorMessage:",
errorMessage,
"retryable:",
retryable
);
this.config.onServerError({
message: errorMessage,
errorType
});
},
onWarning: (warning) => {
this.logger.warn("DeltaNet warning:", warning);
},
onServerCustom: (customType, contents) => {
var _a, _b;
(_b = (_a = this.config).onCustomMessage) == null ? void 0 : _b.call(_a, customType, contents);
}
},
void 0,
// timeCallback is optional
(status) => {
let mappedStatus;
switch (status) {
case DeltaNetClientWebsocketStatus.Connected:
mappedStatus = 1 /* Connected */;
break;
case DeltaNetClientWebsocketStatus.ConnectionOpen:
this.sendInitialAuthentication();
mappedStatus = 1 /* Connected */;
break;
case DeltaNetClientWebsocketStatus.Disconnected:
mappedStatus = 3 /* Disconnected */;
this.reset();
break;
case DeltaNetClientWebsocketStatus.Reconnecting:
mappedStatus = 2 /* Reconnecting */;
this.reset();
break;
default:
mappedStatus = 3 /* Disconnected */;
}
this.config.statusUpdateCallback(mappedStatus);
}
);
}
sendUpdate(update) {
const encodedUpdate = UserNetworkingCodec.encodeUpdate(update);
this.send(encodedUpdate);
deltaNetClient;
deltaNetState;
userId = null;
userIndex = null;
userState = {
username: null,
characterDescription: null,
colors: null
};
stableIdToUserId = /* @__PURE__ */ new Map();
userProfiles = /* @__PURE__ */ new Map();
isAuthenticated = false;
pendingUpdate;
reset() {
this.deltaNetState.reset();
this.userProfiles.clear();
this.stableIdToUserId.clear();
this.isAuthenticated = false;
this.userId = null;
this.userIndex = null;
}
handleIncomingWebsocketMessage(message) {
if (typeof message.data === "string") {
const parsed = JSON.parse(message.data);
switch (parsed.type) {
case IDENTITY_MESSAGE_TYPE:
console.log(`Assigned ID: ${parsed.id}`);
this.setIdentityCallback(parsed.id);
sendInitialAuthentication() {
const components = DeltaNetComponentMapping.toComponents(this.pendingUpdate);
const states = DeltaNetComponentMapping.toStates(this.userState);
this.deltaNetClient.setUserComponents(components, states);
}
processNetworkUpdate(removedStableIds, addedStableIdsArray, stateUpdates) {
const addedUserIds = /* @__PURE__ */ new Map();
const removedUserIds = /* @__PURE__ */ new Set();
for (const stableId of removedStableIds) {
const userId = this.stableIdToUserId.get(stableId);
if (userId) {
removedUserIds.add(userId);
this.userProfiles.delete(userId);
this.stableIdToUserId.delete(stableId);
} else {
throw new Error(`No userId found for stableId ${stableId}`);
}
}
for (const stableId of addedStableIdsArray) {
const stableUserData = this.deltaNetState.byStableId.get(stableId);
if (!stableUserData) {
throw new Error(`No stableUserData found for stableId ${stableId}`);
}
const userIdState = stableUserData.states.get(STATE_INTERNAL_CONNECTION_ID);
if (!userIdState) {
throw new Error(`No userIdState found for stableId ${stableId}`);
}
const userId = DeltaNetComponentMapping.userIdFromBytes(userIdState);
if (!userId) {
throw new Error(`Failed to extract userId from bytes for stableId ${stableId}`);
}
this.stableIdToUserId.set(stableId, userId);
const newProfile = DeltaNetComponentMapping.fromStates(stableUserData.states, this.logger);
this.userProfiles.set(userId, newProfile);
const clientUpdate = DeltaNetComponentMapping.fromComponents(stableUserData.components);
addedUserIds.set(userId, {
userState: newProfile,
components: clientUpdate
});
}
const updatedUsers = /* @__PURE__ */ new Map();
for (const [stableUserId, userInfo] of this.deltaNetState.byStableId) {
const userId = this.stableIdToUserId.get(stableUserId);
if (!userId) {
throw new Error(`No userId found for stableUserId ${stableUserId}`);
}
if (!addedUserIds.has(userId)) {
if (userInfo.components.size > 0) {
const clientUpdate = DeltaNetComponentMapping.fromComponents(userInfo.components);
updatedUsers.set(userId, {
components: clientUpdate
});
}
}
}
for (const update of stateUpdates) {
const stableUserId = update.stableId;
const userId = this.stableIdToUserId.get(stableUserId);
if (!userId) {
throw new Error(`No userId found for stableUserId ${stableUserId}`);
}
if (addedUserIds.has(userId)) {
continue;
}
const profile = this.userProfiles.get(userId);
if (!profile) {
this.logger.warn(`No profile found for user ${userId}, skipping update`);
continue;
}
const existingUpdate = updatedUsers.get(userId);
let existingUserStateUpdate = existingUpdate.userState;
if (!existingUserStateUpdate) {
existingUserStateUpdate = {};
existingUpdate.userState = existingUserStateUpdate;
}
switch (update.stateId) {
case STATE_INTERNAL_CONNECTION_ID:
this.logger.error(
"STATE_INTERNAL_CONNECTION_ID is not expected to change in state updates"
);
break;
case CONNECTED_MESSAGE_TYPE:
console.log(`Client ID: ${parsed.id} joined`);
case STATE_USERNAME:
const username = DeltaNetComponentMapping.usernameFromBytes(update.state);
if (username) {
profile.username = username;
existingUserStateUpdate.username = username;
}
break;
case DISCONNECTED_MESSAGE_TYPE:
console.log(`Client ID: ${parsed.id} left`);
this.clientUpdate(parsed.id, null);
case STATE_CHARACTER_DESCRIPTION:
const characterDescription = DeltaNetComponentMapping.characterDescriptionFromBytes(
update.state
);
profile.characterDescription = characterDescription;
existingUserStateUpdate.characterDescription = characterDescription;
break;
case PING_MESSAGE_TYPE: {
this.send({ type: "pong" });
case STATE_COLORS:
const colors = DeltaNetComponentMapping.decodeColors(update.state, this.logger);
profile.colors = colors;
existingUserStateUpdate.colors = colors;
break;
}
default:
console.warn("unknown message type received", parsed);
this.logger.warn(`Unknown state ID: ${update.stateId}`);
}
} else if (message.data instanceof ArrayBuffer) {
const userNetworkingClientUpdate = UserNetworkingCodec.decodeUpdate(message.data);
this.clientUpdate(userNetworkingClientUpdate.id, userNetworkingClientUpdate);
} else {
console.error("Unhandled message type", message.data);
}
return {
removedUserIds,
addedUserIds,
updatedUsers
};
}
sendUpdate(update) {
if (!this.isAuthenticated || this.userId === null) {
this.pendingUpdate = update;
return;
}
const components = DeltaNetComponentMapping.toComponents(update);
this.deltaNetClient.setUserComponents(components, /* @__PURE__ */ new Map());
}
sendCustomMessage(customType, contents) {
if (!this.isAuthenticated || this.userId === null) {
this.logger.warn("Cannot send custom message before authentication");
return;
}
this.deltaNetClient.sendCustomMessage(customType, contents);
}
updateUsername(username) {
if (!this.isAuthenticated || this.userId === null) {
return;
}
this.userState.username = username;
const states = DeltaNetComponentMapping.toUsernameState(username);
this.deltaNetClient.setUserComponents(/* @__PURE__ */ new Map(), states);
}
updateCharacterDescription(characterDescription) {
if (!this.isAuthenticated || this.userId === null) {
return;
}
this.userState.characterDescription = characterDescription;
const states = DeltaNetComponentMapping.toCharacterDescriptionState(characterDescription);
this.deltaNetClient.setUserComponents(/* @__PURE__ */ new Map(), states);
}
updateColors(colors) {
if (!this.isAuthenticated || this.userId === null) {
return;
}
this.userState.colors = colors;
const states = DeltaNetComponentMapping.toColorsState(colors);
this.deltaNetClient.setUserComponents(/* @__PURE__ */ new Map(), states);
}
stop() {
this.deltaNetClient.stop();
this.reset();
}
};
// src/index.ts
import {
DeltaNetV01ServerErrors as DeltaNetV01ServerErrors2,
deltaNetProtocolSubProtocol_v0_1
} from "@mml-io/delta-net-protocol";
export {
CONNECTED_MESSAGE_TYPE,
DISCONNECTED_MESSAGE_TYPE,
IDENTITY_MESSAGE_TYPE,
PING_MESSAGE_TYPE,
PONG_MESSAGE_TYPE,
ReconnectingWebSocket,
COMPONENT_POSITION_X,
COMPONENT_POSITION_Y,
COMPONENT_POSITION_Z,
COMPONENT_ROTATION_W,
COMPONENT_ROTATION_Y,
COMPONENT_STATE,
DeltaNetComponentMapping,
DeltaNetV01ServerErrors2 as DeltaNetV01ServerErrors,
FROM_CLIENT_CHAT_MESSAGE_TYPE,
FROM_SERVER_CHAT_MESSAGE_TYPE,
LEGACY_USER_NETWORKING_AUTHENTICATION_FAILED_ERROR_TYPE,
LEGACY_USER_NETWORKING_CONNECTION_LIMIT_REACHED_ERROR_TYPE,
LEGACY_USER_NETWORKING_DISCONNECTED_MESSAGE_TYPE,
LEGACY_USER_NETWORKING_IDENTITY_MESSAGE_TYPE,
LEGACY_USER_NETWORKING_PING_MESSAGE_TYPE,
LEGACY_USER_NETWORKING_PONG_MESSAGE_TYPE,
LEGACY_USER_NETWORKING_SERVER_BROADCAST_MESSAGE_TYPE,
LEGACY_USER_NETWORKING_SERVER_ERROR_MESSAGE_TYPE,
LEGACY_USER_NETWORKING_SERVER_SHUTDOWN_ERROR_TYPE,
LEGACY_USER_NETWORKING_UNKNOWN_ERROR,
LEGACY_USER_NETWORKING_USER_AUTHENTICATE_MESSAGE_TYPE,
LEGACY_USER_NETWORKING_USER_PROFILE_MESSAGE_TYPE,
LEGACY_USER_NETWORKING_USER_UPDATE_MESSAGE_TYPE,
SERVER_BROADCAST_MESSAGE_TYPE,
STATE_CHARACTER_DESCRIPTION,
STATE_COLORS,
STATE_INTERNAL_CONNECTION_ID,
STATE_USERNAME,
UserNetworkingClient,
UserNetworkingCodec,
UserNetworkingConsoleLogger,
UserNetworkingServer,
WebsocketStatus
UserNetworkingServerError,
WebsocketStatus,
deltaNetProtocolSubProtocol_v0_1,
parseClientChatMessage,
parseServerBroadcastMessage,
parseServerChatMessage,
positionMultiplier,
rotationMultiplier
};
//# sourceMappingURL=index.js.map

@@ -1,9 +0,53 @@

import { ReconnectingWebSocket, WebsocketFactory, WebsocketStatus } from "./ReconnectingWebSocket";
import { UserNetworkingClientUpdate } from "./UserNetworkingCodec";
export declare class UserNetworkingClient extends ReconnectingWebSocket {
private setIdentityCallback;
private clientUpdate;
constructor(url: string, websocketFactory: WebsocketFactory, statusUpdateCallback: (status: WebsocketStatus) => void, setIdentityCallback: (id: number) => void, clientUpdate: (id: number, update: null | UserNetworkingClientUpdate) => void);
import { UserNetworkingClientUpdate, WebsocketFactory, WebsocketStatus } from "./types";
import { UserData } from "./UserData";
import { UserNetworkingLogger } from "./UserNetworkingLogger";
import { CharacterDescription } from "./UserNetworkingMessages";
export type UserNetworkingClientConfig = {
url: string;
sessionToken: string;
websocketFactory: WebsocketFactory;
statusUpdateCallback: (status: WebsocketStatus) => void;
assignedIdentity: (clientId: number) => void;
onServerError: (error: {
message: string;
errorType: string;
}) => void;
onCustomMessage?: (customType: number, contents: string) => void;
onUpdate(update: NetworkUpdate): void;
};
export type AddedUser = {
userState: UserData;
components: UserNetworkingClientUpdate;
};
export type UpdatedUser = {
userState?: Partial<UserData>;
components: UserNetworkingClientUpdate;
};
export type NetworkUpdate = {
removedUserIds: Set<number>;
addedUserIds: Map<number, AddedUser>;
updatedUsers: Map<number, UpdatedUser>;
};
export declare class UserNetworkingClient {
private config;
private logger;
private deltaNetClient;
private deltaNetState;
private userId;
private userIndex;
private userState;
private stableIdToUserId;
private userProfiles;
private isAuthenticated;
private pendingUpdate;
constructor(config: UserNetworkingClientConfig, initialUserState?: UserData, initialUpdate?: UserNetworkingClientUpdate, logger?: UserNetworkingLogger);
private reset;
private sendInitialAuthentication;
private processNetworkUpdate;
sendUpdate(update: UserNetworkingClientUpdate): void;
protected handleIncomingWebsocketMessage(message: MessageEvent): void;
sendCustomMessage(customType: number, contents: string): void;
updateUsername(username: string): void;
updateCharacterDescription(characterDescription: CharacterDescription): void;
updateColors(colors: Array<[number, number, number]>): void;
stop(): void;
}

@@ -1,16 +0,49 @@

import WebSocket from "ws";
import { UserNetworkingClientUpdate } from "./UserNetworkingCodec";
export type Client = {
import { DeltaNetV01Connection } from "@mml-io/delta-net-server";
import { LegacyUserIdentity, LegacyCharacterDescription } from "./legacy/LegacyUserNetworkingMessages";
import { UserData } from "./UserData";
import { UserNetworkingLogger } from "./UserNetworkingLogger";
import { UserNetworkingServerError, CharacterDescription } from "./UserNetworkingMessages";
export type UserNetworkingServerClient = {
socket: WebSocket;
update: UserNetworkingClientUpdate;
id: number;
lastPong: number;
authenticatedUser: UserData | null;
deltaNetConnection: DeltaNetV01Connection | null;
};
export type UserNetworkingServerOptions = {
legacyAdapterEnabled?: boolean;
onClientConnect: (clientId: number, sessionToken: string, userIdentity?: UserData) => Promise<UserData | true | Error> | UserData | true | Error;
onClientUserIdentityUpdate: (clientId: number, userIdentity: UserData) => Promise<UserData | null | false | true | Error> | UserData | null | false | true | Error;
onClientDisconnect: (clientId: number) => void;
};
export declare class UserNetworkingServer {
private clients;
private clientLastPong;
constructor();
heartBeat(): void;
pingClients(): void;
getId(): number;
private options;
private logger;
private deltaNetServer;
private authenticatedClientsById;
private tickInterval;
private legacyAdapter;
private updatedUserProfilesInTick;
constructor(options: UserNetworkingServerOptions, logger?: UserNetworkingLogger);
getCharacterDescription(connectionId: number): LegacyCharacterDescription;
getUsername(connectionId: number): string;
getLegacyClientId(): number;
hasCapacityForLegacyClient(): boolean;
onLegacyClientConnect(id: number, sessionToken: string, userIdentity: LegacyUserIdentity | undefined): Promise<UserData | true | Error> | UserData | true | Error;
setAuthenticatedLegacyClientConnection(clientId: number, webSocket: WebSocket, userData: UserData): void;
onLegacyClientDisconnect(id: number): void;
private handleStatesUpdate;
private handleJoiner;
private handleLeave;
private handleCustomMessage;
private handleDeltaNetAuthentication;
connectClient(socket: WebSocket): void;
sendUpdates(): void;
broadcastMessage(broadcastType: number, broadcastPayload: string): void;
updateUserCharacter(clientId: number, userData: UserData): void;
updateUserUsername(clientId: number, username: string): void;
updateUserCharacterDescription(clientId: number, characterDescription: CharacterDescription): void;
updateUserColors(clientId: number, colors: Array<[number, number, number]>): void;
updateUserStates(clientId: number, updates: UserData): void;
private internalUpdateUser;
dispose(clientCloseError?: UserNetworkingServerError): void;
}
{
"name": "@mml-io/3d-web-user-networking",
"version": "0.0.0-experimental-dd4ec30-20240307",
"version": "0.0.0-experimental-dfa8273-20250802",
"publishConfig": {

@@ -14,2 +14,3 @@ "access": "public"

"scripts": {
"depcheck": "depcheck --quiet",
"build": "tsx ./build.ts --build",

@@ -20,16 +21,25 @@ "iterate": "tsx ./build.ts --watch",

"lint-fix": "eslint \"./{src,test}/**/*.{js,jsx,ts,tsx}\" --fix",
"test": "jest"
"test": "cross-env NODE_OPTIONS=--experimental-vm-modules jest"
},
"dependencies": {
"ws": "^8.16.0"
"@mml-io/delta-net-protocol": "0.0.0-experimental-dfa8273-20250802",
"@mml-io/delta-net-server": "0.0.0-experimental-dfa8273-20250802",
"@mml-io/delta-net-web": "0.0.0-experimental-dfa8273-20250802"
},
"devDependencies": {
"@types/express": "^4.17.21",
"@types/express-ws": "^3.0.4",
"@types/node": "^20.11.13",
"@types/ws": "^8.5.10",
"express": "4.18.2",
"express-ws": "5.0.2"
"@jest/globals": "29.7.0",
"@types/express": "^5.0.0",
"@types/express-ws": "^3.0.5",
"@types/jest": "^29.5.12",
"@types/node": "^22.13.1",
"@types/ws": "^8.5.14",
"cross-env": "^7.0.3",
"express": "4.21.2",
"express-ws": "5.0.2",
"jest": "^29.7.0",
"jest-junit": "16.0.0",
"ts-jest": "^29.1.2",
"ws": "^8.18.0"
},
"gitHead": "2e160ecac0ab168195d69bdf5aeb15d82aa757f1"
"gitHead": "5d9e85ad540813a7c56328ff2ac227e6520d80e2"
}

Sorry, the diff of this file is not supported yet

SocketSocket SOC 2 Logo

Product

About

Packages

Stay in touch

Get open source security insights delivered straight into your inbox.

  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc

U.S. Patent No. 12,346,443 & 12,314,394. Other pending.