@mml-io/3d-web-user-networking
Advanced tools
Comparing version
@@ -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"; |
1667
build/index.js
@@ -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
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
181315
250.81%15
25%1850
128.96%3
200%13
116.67%1
Infinity%+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
- Removed
- Removed