@colyseus/core 0.18.5 → 0.18.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/Server.cjs +52 -18
- package/build/Server.cjs.map +2 -2
- package/build/Server.d.ts +19 -1
- package/build/Server.mjs +51 -19
- package/build/Server.mjs.map +2 -2
- package/build/index.cjs +4 -0
- package/build/index.cjs.map +2 -2
- package/build/index.d.ts +1 -1
- package/build/index.mjs +3 -1
- package/build/index.mjs.map +2 -2
- package/build/router/index.cjs +47 -1
- package/build/router/index.cjs.map +3 -3
- package/build/router/index.d.ts +6 -2
- package/build/router/index.mjs +47 -1
- package/build/router/index.mjs.map +2 -2
- package/package.json +5 -5
- package/src/Server.ts +68 -24
- package/src/index.ts +1 -1
- package/src/router/index.ts +72 -3
package/build/index.cjs.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/index.ts"],
|
|
4
|
-
"sourcesContent": ["import { ClockTimer as Clock, Delayed } from '@colyseus/timer';\n\n// Shared types - re-export from @colyseus/shared-types for convenience\nexport {\n Protocol,\n ProtocolModifier,\n ErrorCode,\n CloseCode,\n ResponseStatus,\n type InferState,\n type ExtractRoomMessages,\n type ExtractRoomClientMessages,\n type MessageContext,\n type Rejection,\n type Resolution,\n type ExtractRejectReason,\n type ExtractResponseType,\n} from '@colyseus/shared-types';\n\n// Core classes\nexport { Server, defineRoom, defineServer, registerRoomDefinitions, unregisterRoomDefinitions, type RoomDefinitions, type ServerOptions, type SDKTypes } from './Server.ts';\nexport { Room, RoomInternalState, validate, type RoomOptions, type DefineInputOptions, type SimulationCallback, type FixedTimestepCallback, type StepContext, type MessageHandlerWithFormat, type Messages, type ExtractRoomState, type ExtractRoomMetadata, type ExtractRoomClient } from './Room.ts';\nexport { InputBufferImpl, compileSanitizer } from './input/InputBuffer.ts';\nexport { type InputAccessor, type InputAPI, type NormalizedInputOptions, type ConsumeOptions, type IdleInput, type IdleContext, type SanitizeInput, type NumericFieldsOf } from './input/types.ts';\nexport { Rewind, RewindView, type RewindOptions, type RewindMode } from './Rewind.ts';\nexport {\n RoomPlugin, definePlugins, attachToTestRoom,\n type RoomPluginOrder, type PluginDependencies, type RoomPluginClass,\n} from './RoomPlugin.ts';\nexport { getMessageBytes } from './Protocol.ts';\nexport { RegisteredHandler } from './matchmaker/RegisteredHandler.ts';\nexport { ServerError } from './errors/ServerError.ts';\n\nexport {\n type RoomException,\n type RoomMethodName,\n OnCreateException,\n OnAuthException,\n OnJoinException,\n OnLeaveException,\n OnDisposeException,\n OnMessageException,\n TimestepException,\n SimulationIntervalException,\n TimedEventException,\n} from './errors/RoomExceptions.ts';\n\n// MatchMaker\nimport * as matchMaker from './MatchMaker.ts';\nexport { matchMaker };\nexport { updateLobby, subscribeLobby } from './matchmaker/Lobby.ts';\nexport { createNodeMatchmakingMiddleware } from './router/node.ts';\n\n// Driver\nexport * from './matchmaker/LocalDriver/LocalDriver.ts';\nexport { initializeRoomCache } from './matchmaker/driver.ts';\n\n// Transport\nexport { type Client, type ClientPrivate, type AuthContext, ClientState, ClientArray, Transport, type ISendOptions, type BeforeUpgradeHandler, runBeforeUpgrade, createAuthContext, connectClientToRoom, enqueueClientRaw } from './Transport.ts';\n\n// Presence\nexport { type Presence } from './presence/Presence.ts';\nexport { LocalPresence } from './presence/LocalPresence.ts';\n\n// Serializers\nexport { type Serializer } from './serializer/Serializer.ts';\nexport { SchemaSerializer } from './serializer/SchemaSerializer.ts';\n\n// Utilities\nexport { Clock, Delayed };\nexport { generateId, Deferred, spliceOne, getBearerToken, dynamicImport } from './utils/Utils.ts';\nexport { isDevMode, setDevMode } from './utils/DevMode.ts';\n\n// IPC\nexport { subscribeIPC, requestFromIPC } from './IPC.ts';\n\n// Debug\nexport {\n debugMatchMaking,\n debugMessage,\n debugPatch,\n debugError,\n debugConnection,\n debugDriver,\n debugPresence,\n debugAndPrintError,\n} from './Debug.ts';\n\n// Default rooms\nexport { LobbyRoom } from './rooms/LobbyRoom.ts';\nexport { RelayRoom } from './rooms/RelayRoom.ts';\nexport { QueueRoom, type QueueOptions, type QueueMatchGroup, type QueueMatchTeam, type QueueClientData } from './rooms/QueueRoom.ts';\n\n// Router / Endpoints\nexport {\n createEndpoint,\n createInternalContext,\n createMiddleware,\n createRouter,\n basicAuth,\n type BasicAuthOptions,\n toNodeHandler,\n dualModeEndpoints,\n type Router,\n type RouterConfig,\n type Endpoint,\n type EndpointHandler,\n type EndpointOptions,\n type EndpointContext,\n type StrictEndpoint,\n type ExpressMiddleware,\n type NodeHandler,\n type DualModeHelpers,\n} from './router/index.ts';\n\n// Abstract logging support\nexport { logger } from './Logger.ts';\n"],
|
|
5
|
-
"mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA,4BAAAA;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,mBAA6C;AAG7C,0BAcO;AAGP,
|
|
4
|
+
"sourcesContent": ["import { ClockTimer as Clock, Delayed } from '@colyseus/timer';\n\n// Shared types - re-export from @colyseus/shared-types for convenience\nexport {\n Protocol,\n ProtocolModifier,\n ErrorCode,\n CloseCode,\n ResponseStatus,\n type InferState,\n type ExtractRoomMessages,\n type ExtractRoomClientMessages,\n type MessageContext,\n type Rejection,\n type Resolution,\n type ExtractRejectReason,\n type ExtractResponseType,\n} from '@colyseus/shared-types';\n\n// Core classes\nexport { Server, defineRoom, defineServer, registerRoomDefinitions, unregisterRoomDefinitions, applySimulatedLatency, parseLatencyEnv, type RoomDefinitions, type ServerOptions, type SDKTypes } from './Server.ts';\nexport { Room, RoomInternalState, validate, type RoomOptions, type DefineInputOptions, type SimulationCallback, type FixedTimestepCallback, type StepContext, type MessageHandlerWithFormat, type Messages, type ExtractRoomState, type ExtractRoomMetadata, type ExtractRoomClient } from './Room.ts';\nexport { InputBufferImpl, compileSanitizer } from './input/InputBuffer.ts';\nexport { type InputAccessor, type InputAPI, type NormalizedInputOptions, type ConsumeOptions, type IdleInput, type IdleContext, type SanitizeInput, type NumericFieldsOf } from './input/types.ts';\nexport { Rewind, RewindView, type RewindOptions, type RewindMode } from './Rewind.ts';\nexport {\n RoomPlugin, definePlugins, attachToTestRoom,\n type RoomPluginOrder, type PluginDependencies, type RoomPluginClass,\n} from './RoomPlugin.ts';\nexport { getMessageBytes } from './Protocol.ts';\nexport { RegisteredHandler } from './matchmaker/RegisteredHandler.ts';\nexport { ServerError } from './errors/ServerError.ts';\n\nexport {\n type RoomException,\n type RoomMethodName,\n OnCreateException,\n OnAuthException,\n OnJoinException,\n OnLeaveException,\n OnDisposeException,\n OnMessageException,\n TimestepException,\n SimulationIntervalException,\n TimedEventException,\n} from './errors/RoomExceptions.ts';\n\n// MatchMaker\nimport * as matchMaker from './MatchMaker.ts';\nexport { matchMaker };\nexport { updateLobby, subscribeLobby } from './matchmaker/Lobby.ts';\nexport { createNodeMatchmakingMiddleware } from './router/node.ts';\n\n// Driver\nexport * from './matchmaker/LocalDriver/LocalDriver.ts';\nexport { initializeRoomCache } from './matchmaker/driver.ts';\n\n// Transport\nexport { type Client, type ClientPrivate, type AuthContext, ClientState, ClientArray, Transport, type ISendOptions, type BeforeUpgradeHandler, runBeforeUpgrade, createAuthContext, connectClientToRoom, enqueueClientRaw } from './Transport.ts';\n\n// Presence\nexport { type Presence } from './presence/Presence.ts';\nexport { LocalPresence } from './presence/LocalPresence.ts';\n\n// Serializers\nexport { type Serializer } from './serializer/Serializer.ts';\nexport { SchemaSerializer } from './serializer/SchemaSerializer.ts';\n\n// Utilities\nexport { Clock, Delayed };\nexport { generateId, Deferred, spliceOne, getBearerToken, dynamicImport } from './utils/Utils.ts';\nexport { isDevMode, setDevMode } from './utils/DevMode.ts';\n\n// IPC\nexport { subscribeIPC, requestFromIPC } from './IPC.ts';\n\n// Debug\nexport {\n debugMatchMaking,\n debugMessage,\n debugPatch,\n debugError,\n debugConnection,\n debugDriver,\n debugPresence,\n debugAndPrintError,\n} from './Debug.ts';\n\n// Default rooms\nexport { LobbyRoom } from './rooms/LobbyRoom.ts';\nexport { RelayRoom } from './rooms/RelayRoom.ts';\nexport { QueueRoom, type QueueOptions, type QueueMatchGroup, type QueueMatchTeam, type QueueClientData } from './rooms/QueueRoom.ts';\n\n// Router / Endpoints\nexport {\n createEndpoint,\n createInternalContext,\n createMiddleware,\n createRouter,\n basicAuth,\n type BasicAuthOptions,\n toNodeHandler,\n dualModeEndpoints,\n type Router,\n type RouterConfig,\n type Endpoint,\n type EndpointHandler,\n type EndpointOptions,\n type EndpointContext,\n type StrictEndpoint,\n type ExpressMiddleware,\n type NodeHandler,\n type DualModeHelpers,\n} from './router/index.ts';\n\n// Abstract logging support\nexport { logger } from './Logger.ts';\n"],
|
|
5
|
+
"mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA,4BAAAA;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,mBAA6C;AAG7C,0BAcO;AAGP,oBAAsM;AACtM,kBAA2R;AAC3R,yBAAkD;AAClD,mBAAgL;AAChL,oBAAwE;AACxE,wBAGO;AACP,sBAAgC;AAChC,+BAAkC;AAClC,yBAA4B;AAE5B,4BAYO;AAGP,iBAA4B;AAE5B,mBAA4C;AAC5C,kBAAgD;AAGhD,0BAAc,qDAtDd;AAuDA,oBAAoC;AAGpC,uBAAiO;AAGjO,sBAA8B;AAC9B,2BAA8B;AAG9B,wBAAgC;AAChC,8BAAiC;AAIjC,mBAA+E;AAC/E,qBAAsC;AAGtC,iBAA6C;AAG7C,mBASO;AAGP,uBAA0B;AAC1B,uBAA0B;AAC1B,uBAA8G;AAG9G,oBAmBO;AAGP,oBAAuB;",
|
|
6
6
|
"names": ["Clock"]
|
|
7
7
|
}
|
package/build/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ClockTimer as Clock, Delayed } from '@colyseus/timer';
|
|
2
2
|
export { Protocol, ProtocolModifier, ErrorCode, CloseCode, ResponseStatus, type InferState, type ExtractRoomMessages, type ExtractRoomClientMessages, type MessageContext, type Rejection, type Resolution, type ExtractRejectReason, type ExtractResponseType, } from '@colyseus/shared-types';
|
|
3
|
-
export { Server, defineRoom, defineServer, registerRoomDefinitions, unregisterRoomDefinitions, type RoomDefinitions, type ServerOptions, type SDKTypes } from './Server.ts';
|
|
3
|
+
export { Server, defineRoom, defineServer, registerRoomDefinitions, unregisterRoomDefinitions, applySimulatedLatency, parseLatencyEnv, type RoomDefinitions, type ServerOptions, type SDKTypes } from './Server.ts';
|
|
4
4
|
export { Room, RoomInternalState, validate, type RoomOptions, type DefineInputOptions, type SimulationCallback, type FixedTimestepCallback, type StepContext, type MessageHandlerWithFormat, type Messages, type ExtractRoomState, type ExtractRoomMetadata, type ExtractRoomClient } from './Room.ts';
|
|
5
5
|
export { InputBufferImpl, compileSanitizer } from './input/InputBuffer.ts';
|
|
6
6
|
export { type InputAccessor, type InputAPI, type NormalizedInputOptions, type ConsumeOptions, type IdleInput, type IdleContext, type SanitizeInput, type NumericFieldsOf } from './input/types.ts';
|
package/build/index.mjs
CHANGED
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
CloseCode,
|
|
8
8
|
ResponseStatus
|
|
9
9
|
} from "@colyseus/shared-types";
|
|
10
|
-
import { Server, defineRoom, defineServer, registerRoomDefinitions, unregisterRoomDefinitions } from "./Server.mjs";
|
|
10
|
+
import { Server, defineRoom, defineServer, registerRoomDefinitions, unregisterRoomDefinitions, applySimulatedLatency, parseLatencyEnv } from "./Server.mjs";
|
|
11
11
|
import { Room, RoomInternalState, validate } from "./Room.mjs";
|
|
12
12
|
import { InputBufferImpl, compileSanitizer } from "./input/InputBuffer.mjs";
|
|
13
13
|
import {} from "./input/types.mjs";
|
|
@@ -102,6 +102,7 @@ export {
|
|
|
102
102
|
TimedEventException,
|
|
103
103
|
TimestepException,
|
|
104
104
|
Transport,
|
|
105
|
+
applySimulatedLatency,
|
|
105
106
|
attachToTestRoom,
|
|
106
107
|
basicAuth,
|
|
107
108
|
compileSanitizer,
|
|
@@ -133,6 +134,7 @@ export {
|
|
|
133
134
|
isDevMode,
|
|
134
135
|
logger,
|
|
135
136
|
matchMaker,
|
|
137
|
+
parseLatencyEnv,
|
|
136
138
|
registerRoomDefinitions,
|
|
137
139
|
requestFromIPC,
|
|
138
140
|
runBeforeUpgrade,
|
package/build/index.mjs.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/index.ts"],
|
|
4
|
-
"sourcesContent": ["import { ClockTimer as Clock, Delayed } from '@colyseus/timer';\n\n// Shared types - re-export from @colyseus/shared-types for convenience\nexport {\n Protocol,\n ProtocolModifier,\n ErrorCode,\n CloseCode,\n ResponseStatus,\n type InferState,\n type ExtractRoomMessages,\n type ExtractRoomClientMessages,\n type MessageContext,\n type Rejection,\n type Resolution,\n type ExtractRejectReason,\n type ExtractResponseType,\n} from '@colyseus/shared-types';\n\n// Core classes\nexport { Server, defineRoom, defineServer, registerRoomDefinitions, unregisterRoomDefinitions, type RoomDefinitions, type ServerOptions, type SDKTypes } from './Server.ts';\nexport { Room, RoomInternalState, validate, type RoomOptions, type DefineInputOptions, type SimulationCallback, type FixedTimestepCallback, type StepContext, type MessageHandlerWithFormat, type Messages, type ExtractRoomState, type ExtractRoomMetadata, type ExtractRoomClient } from './Room.ts';\nexport { InputBufferImpl, compileSanitizer } from './input/InputBuffer.ts';\nexport { type InputAccessor, type InputAPI, type NormalizedInputOptions, type ConsumeOptions, type IdleInput, type IdleContext, type SanitizeInput, type NumericFieldsOf } from './input/types.ts';\nexport { Rewind, RewindView, type RewindOptions, type RewindMode } from './Rewind.ts';\nexport {\n RoomPlugin, definePlugins, attachToTestRoom,\n type RoomPluginOrder, type PluginDependencies, type RoomPluginClass,\n} from './RoomPlugin.ts';\nexport { getMessageBytes } from './Protocol.ts';\nexport { RegisteredHandler } from './matchmaker/RegisteredHandler.ts';\nexport { ServerError } from './errors/ServerError.ts';\n\nexport {\n type RoomException,\n type RoomMethodName,\n OnCreateException,\n OnAuthException,\n OnJoinException,\n OnLeaveException,\n OnDisposeException,\n OnMessageException,\n TimestepException,\n SimulationIntervalException,\n TimedEventException,\n} from './errors/RoomExceptions.ts';\n\n// MatchMaker\nimport * as matchMaker from './MatchMaker.ts';\nexport { matchMaker };\nexport { updateLobby, subscribeLobby } from './matchmaker/Lobby.ts';\nexport { createNodeMatchmakingMiddleware } from './router/node.ts';\n\n// Driver\nexport * from './matchmaker/LocalDriver/LocalDriver.ts';\nexport { initializeRoomCache } from './matchmaker/driver.ts';\n\n// Transport\nexport { type Client, type ClientPrivate, type AuthContext, ClientState, ClientArray, Transport, type ISendOptions, type BeforeUpgradeHandler, runBeforeUpgrade, createAuthContext, connectClientToRoom, enqueueClientRaw } from './Transport.ts';\n\n// Presence\nexport { type Presence } from './presence/Presence.ts';\nexport { LocalPresence } from './presence/LocalPresence.ts';\n\n// Serializers\nexport { type Serializer } from './serializer/Serializer.ts';\nexport { SchemaSerializer } from './serializer/SchemaSerializer.ts';\n\n// Utilities\nexport { Clock, Delayed };\nexport { generateId, Deferred, spliceOne, getBearerToken, dynamicImport } from './utils/Utils.ts';\nexport { isDevMode, setDevMode } from './utils/DevMode.ts';\n\n// IPC\nexport { subscribeIPC, requestFromIPC } from './IPC.ts';\n\n// Debug\nexport {\n debugMatchMaking,\n debugMessage,\n debugPatch,\n debugError,\n debugConnection,\n debugDriver,\n debugPresence,\n debugAndPrintError,\n} from './Debug.ts';\n\n// Default rooms\nexport { LobbyRoom } from './rooms/LobbyRoom.ts';\nexport { RelayRoom } from './rooms/RelayRoom.ts';\nexport { QueueRoom, type QueueOptions, type QueueMatchGroup, type QueueMatchTeam, type QueueClientData } from './rooms/QueueRoom.ts';\n\n// Router / Endpoints\nexport {\n createEndpoint,\n createInternalContext,\n createMiddleware,\n createRouter,\n basicAuth,\n type BasicAuthOptions,\n toNodeHandler,\n dualModeEndpoints,\n type Router,\n type RouterConfig,\n type Endpoint,\n type EndpointHandler,\n type EndpointOptions,\n type EndpointContext,\n type StrictEndpoint,\n type ExpressMiddleware,\n type NodeHandler,\n type DualModeHelpers,\n} from './router/index.ts';\n\n// Abstract logging support\nexport { logger } from './Logger.ts';\n"],
|
|
5
|
-
"mappings": ";AAAA,SAAS,cAAc,OAAO,eAAe;AAG7C;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OASK;AAGP,SAAS,QAAQ,YAAY,cAAc,yBAAyB,
|
|
4
|
+
"sourcesContent": ["import { ClockTimer as Clock, Delayed } from '@colyseus/timer';\n\n// Shared types - re-export from @colyseus/shared-types for convenience\nexport {\n Protocol,\n ProtocolModifier,\n ErrorCode,\n CloseCode,\n ResponseStatus,\n type InferState,\n type ExtractRoomMessages,\n type ExtractRoomClientMessages,\n type MessageContext,\n type Rejection,\n type Resolution,\n type ExtractRejectReason,\n type ExtractResponseType,\n} from '@colyseus/shared-types';\n\n// Core classes\nexport { Server, defineRoom, defineServer, registerRoomDefinitions, unregisterRoomDefinitions, applySimulatedLatency, parseLatencyEnv, type RoomDefinitions, type ServerOptions, type SDKTypes } from './Server.ts';\nexport { Room, RoomInternalState, validate, type RoomOptions, type DefineInputOptions, type SimulationCallback, type FixedTimestepCallback, type StepContext, type MessageHandlerWithFormat, type Messages, type ExtractRoomState, type ExtractRoomMetadata, type ExtractRoomClient } from './Room.ts';\nexport { InputBufferImpl, compileSanitizer } from './input/InputBuffer.ts';\nexport { type InputAccessor, type InputAPI, type NormalizedInputOptions, type ConsumeOptions, type IdleInput, type IdleContext, type SanitizeInput, type NumericFieldsOf } from './input/types.ts';\nexport { Rewind, RewindView, type RewindOptions, type RewindMode } from './Rewind.ts';\nexport {\n RoomPlugin, definePlugins, attachToTestRoom,\n type RoomPluginOrder, type PluginDependencies, type RoomPluginClass,\n} from './RoomPlugin.ts';\nexport { getMessageBytes } from './Protocol.ts';\nexport { RegisteredHandler } from './matchmaker/RegisteredHandler.ts';\nexport { ServerError } from './errors/ServerError.ts';\n\nexport {\n type RoomException,\n type RoomMethodName,\n OnCreateException,\n OnAuthException,\n OnJoinException,\n OnLeaveException,\n OnDisposeException,\n OnMessageException,\n TimestepException,\n SimulationIntervalException,\n TimedEventException,\n} from './errors/RoomExceptions.ts';\n\n// MatchMaker\nimport * as matchMaker from './MatchMaker.ts';\nexport { matchMaker };\nexport { updateLobby, subscribeLobby } from './matchmaker/Lobby.ts';\nexport { createNodeMatchmakingMiddleware } from './router/node.ts';\n\n// Driver\nexport * from './matchmaker/LocalDriver/LocalDriver.ts';\nexport { initializeRoomCache } from './matchmaker/driver.ts';\n\n// Transport\nexport { type Client, type ClientPrivate, type AuthContext, ClientState, ClientArray, Transport, type ISendOptions, type BeforeUpgradeHandler, runBeforeUpgrade, createAuthContext, connectClientToRoom, enqueueClientRaw } from './Transport.ts';\n\n// Presence\nexport { type Presence } from './presence/Presence.ts';\nexport { LocalPresence } from './presence/LocalPresence.ts';\n\n// Serializers\nexport { type Serializer } from './serializer/Serializer.ts';\nexport { SchemaSerializer } from './serializer/SchemaSerializer.ts';\n\n// Utilities\nexport { Clock, Delayed };\nexport { generateId, Deferred, spliceOne, getBearerToken, dynamicImport } from './utils/Utils.ts';\nexport { isDevMode, setDevMode } from './utils/DevMode.ts';\n\n// IPC\nexport { subscribeIPC, requestFromIPC } from './IPC.ts';\n\n// Debug\nexport {\n debugMatchMaking,\n debugMessage,\n debugPatch,\n debugError,\n debugConnection,\n debugDriver,\n debugPresence,\n debugAndPrintError,\n} from './Debug.ts';\n\n// Default rooms\nexport { LobbyRoom } from './rooms/LobbyRoom.ts';\nexport { RelayRoom } from './rooms/RelayRoom.ts';\nexport { QueueRoom, type QueueOptions, type QueueMatchGroup, type QueueMatchTeam, type QueueClientData } from './rooms/QueueRoom.ts';\n\n// Router / Endpoints\nexport {\n createEndpoint,\n createInternalContext,\n createMiddleware,\n createRouter,\n basicAuth,\n type BasicAuthOptions,\n toNodeHandler,\n dualModeEndpoints,\n type Router,\n type RouterConfig,\n type Endpoint,\n type EndpointHandler,\n type EndpointOptions,\n type EndpointContext,\n type StrictEndpoint,\n type ExpressMiddleware,\n type NodeHandler,\n type DualModeHelpers,\n} from './router/index.ts';\n\n// Abstract logging support\nexport { logger } from './Logger.ts';\n"],
|
|
5
|
+
"mappings": ";AAAA,SAAS,cAAc,OAAO,eAAe;AAG7C;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OASK;AAGP,SAAS,QAAQ,YAAY,cAAc,yBAAyB,2BAA2B,uBAAuB,uBAAgF;AACtM,SAAS,MAAM,mBAAmB,gBAAyP;AAC3R,SAAS,iBAAiB,wBAAwB;AAClD,eAAgL;AAChL,SAAS,QAAQ,kBAAuD;AACxE;AAAA,EACE;AAAA,EAAY;AAAA,EAAe;AAAA,OAEtB;AACP,SAAS,uBAAuB;AAChC,SAAS,yBAAyB;AAClC,SAAS,mBAAmB;AAE5B;AAAA,EAGE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAGP,YAAY,gBAAgB;AAE5B,SAAS,aAAa,sBAAsB;AAC5C,SAAS,uCAAuC;AAGhD,cAAc;AACd,SAAS,2BAA2B;AAGpC,SAA4D,aAAa,aAAa,WAAyD,kBAAkB,mBAAmB,qBAAqB,wBAAwB;AAGjO,eAA8B;AAC9B,SAAS,qBAAqB;AAG9B,eAAgC;AAChC,SAAS,wBAAwB;AAIjC,SAAS,YAAY,UAAU,WAAW,gBAAgB,qBAAqB;AAC/E,SAAS,WAAW,kBAAkB;AAGtC,SAAS,cAAc,sBAAsB;AAG7C;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAGP,SAAS,iBAAiB;AAC1B,SAAS,iBAAiB;AAC1B,SAAS,iBAAqG;AAG9G;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EAEA;AAAA,EACA;AAAA,OAWK;AAGP,SAAS,cAAc;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/build/router/index.cjs
CHANGED
|
@@ -41,6 +41,8 @@ __export(router_exports, {
|
|
|
41
41
|
toNodeHandler: () => import_node.toNodeHandler
|
|
42
42
|
});
|
|
43
43
|
module.exports = __toCommonJS(router_exports);
|
|
44
|
+
var import_promises = __toESM(require("node:fs/promises"), 1);
|
|
45
|
+
var import_node_path = __toESM(require("node:path"), 1);
|
|
44
46
|
var import_node_crypto = require("node:crypto");
|
|
45
47
|
var import_better_call = require("@colyseus/better-call");
|
|
46
48
|
var import_node = require("@colyseus/better-call/node");
|
|
@@ -179,7 +181,8 @@ function dualModeEndpoints(endpoints, opts) {
|
|
|
179
181
|
) : endpoints;
|
|
180
182
|
const specificRouter = createRouter(specificEndpoints);
|
|
181
183
|
const specificHandler = (0, import_node.toNodeHandler)(specificRouter.handler);
|
|
182
|
-
const
|
|
184
|
+
const buildMiddleware = opts.buildMiddleware ?? panelMiddleware(opts.prefix ?? "", opts.staticDir);
|
|
185
|
+
const middleware = buildMiddleware({
|
|
183
186
|
specificRouter,
|
|
184
187
|
specificHandler,
|
|
185
188
|
fullRouter,
|
|
@@ -187,6 +190,49 @@ function dualModeEndpoints(endpoints, opts) {
|
|
|
187
190
|
});
|
|
188
191
|
return Object.assign(middleware, endpoints);
|
|
189
192
|
}
|
|
193
|
+
function panelMiddleware(prefix, staticDir) {
|
|
194
|
+
const staticRoot = staticDir && import_node_path.default.resolve(staticDir);
|
|
195
|
+
return ({ specificRouter, specificHandler, fullHandler }) => (req, res, next) => {
|
|
196
|
+
const r = req;
|
|
197
|
+
const raw = r.url ?? "";
|
|
198
|
+
const q = raw.indexOf("?");
|
|
199
|
+
const url = q < 0 ? raw : raw.slice(0, q);
|
|
200
|
+
const query = q < 0 ? "" : raw.slice(q);
|
|
201
|
+
const dispatchPath = r.baseUrl ? prefix + url : url;
|
|
202
|
+
if (dispatchPath === prefix || dispatchPath === `${prefix}/`) {
|
|
203
|
+
const original = (r.originalUrl ?? raw).split("?")[0];
|
|
204
|
+
if (r.method === "GET" && !original.endsWith("/")) {
|
|
205
|
+
res.writeHead(302, { location: `${original}/${query}` });
|
|
206
|
+
res.end();
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
const dispatch = (handler) => {
|
|
211
|
+
const wrapped = Object.create(req, {
|
|
212
|
+
url: { value: dispatchPath + query, enumerable: true, configurable: true },
|
|
213
|
+
baseUrl: { value: "", enumerable: true, configurable: true },
|
|
214
|
+
originalUrl: { value: dispatchPath + query, enumerable: true, configurable: true }
|
|
215
|
+
});
|
|
216
|
+
handler(wrapped, res).catch(next);
|
|
217
|
+
};
|
|
218
|
+
const route = specificRouter.findRoute(r.method ?? "GET", dispatchPath);
|
|
219
|
+
if (route && (!route.data?.path?.endsWith("/") || dispatchPath.endsWith("/"))) {
|
|
220
|
+
return dispatch(specificHandler);
|
|
221
|
+
}
|
|
222
|
+
if (r.method !== "GET" || !staticRoot || !dispatchPath.startsWith(prefix)) {
|
|
223
|
+
return next();
|
|
224
|
+
}
|
|
225
|
+
const rel = dispatchPath.slice(prefix.length).replace(/^\/+/, "");
|
|
226
|
+
if (!rel || rel.includes("..")) {
|
|
227
|
+
return next();
|
|
228
|
+
}
|
|
229
|
+
const filePath = import_node_path.default.resolve(staticRoot, rel);
|
|
230
|
+
if (!filePath.startsWith(staticRoot + import_node_path.default.sep)) {
|
|
231
|
+
return next();
|
|
232
|
+
}
|
|
233
|
+
import_promises.default.stat(filePath).then((stat) => stat.isFile() ? dispatch(fullHandler) : next()).catch(() => next());
|
|
234
|
+
};
|
|
235
|
+
}
|
|
190
236
|
// Annotate the CommonJS export names for ESM import in node:
|
|
191
237
|
0 && (module.exports = {
|
|
192
238
|
basicAuth,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/router/index.ts"],
|
|
4
|
-
"sourcesContent": ["import type express from \"express\";\nimport type { IncomingMessage, ServerResponse } from \"http\";\nimport { createHash, timingSafeEqual } from \"node:crypto\";\nimport { type Endpoint, type Router, type RouterConfig, createRouter as createBetterCallRouter, createEndpoint, createMiddleware, APIError } from \"@colyseus/better-call\";\nimport { toNodeHandler, getRequest, setResponse } from \"@colyseus/better-call/node\";\nimport { Transport } from \"../Transport.ts\";\nimport { controller } from \"../matchmaker/controller.ts\";\nimport pkg from \"../../package.json\" with { type: \"json\" };\n\nexport {\n createEndpoint,\n createMiddleware,\n createInternalContext,\n\n // Re-export every type reachable from an inferred type below \u2014 consumers\n // depend on @colyseus/core, not @colyseus/better-call, and cannot name it\n // under pnpm's isolated node_modules (TS2742/TS2883).\n type Router,\n type RouterConfig,\n type Endpoint,\n type EndpointHandler,\n type EndpointOptions,\n type EndpointContext,\n type StrictEndpoint,\n type StandardSchemaV1,\n type MiddlewareOptions,\n type MiddlewareInputContext,\n type CookieOptions,\n type CookiePrefixOptions,\n type Status,\n} from \"@colyseus/better-call\";\n\nexport { toNodeHandler };\n\nexport function bindRouterToTransport(transport: Transport, router: Router, useExpress: boolean) {\n // add default \"/__healthcheck\" endpoint\n router.addEndpoint(createEndpoint(\"/__healthcheck\", { method: \"GET\" }, async (ctx) => {\n return new Response(\"OK\", { status: 200 });\n }));\n\n const server = transport.server;\n\n // check if the server is bound to an express app\n const expressApp: express.Application = (useExpress)\n ? transport.getExpressApp() as express.Application\n // fallback searching for express app in server listeners\n : server?.listeners('request').find((listener: Function) => listener.name === \"app\" && listener['mountpath'] === '/') as express.Application;\n\n // add default \"/\" route, if not provided.\n const hasRootRoute = (\n // check if express app has a root route\n (expressApp && hasExpressRootRoute(expressApp)) ||\n\n // check if router has a root route\n Object.values(router.endpoints).some(endpoint => endpoint.path === \"/\")\n );\n\n if (!hasRootRoute) {\n router.addEndpoint(createEndpoint(\"/\", { method: \"GET\" }, async (ctx) => {\n return new Response(`Colyseus ${pkg.version}`, { status: 200 });\n }));\n }\n\n // use custom bindRouter method if provided\n if (!server && transport.bindRouter) {\n transport.bindRouter(router);\n return;\n }\n\n // which route handler to use\n // (router + fallback to express, or just router)\n let next: any;\n\n if (expressApp) {\n server.removeListener('request', expressApp);\n\n next = async (req: IncomingMessage, res: ServerResponse) => {\n // check if the route is defined in the router\n // if so, use the router handler, otherwise fallback to express\n if (router.findRoute(req.method, req.url.split('?')[0]) !== undefined) {\n const protocol = req.headers[\"x-forwarded-proto\"] || ((req.socket as any).encrypted ? \"https\" : \"http\");\n const base = `${protocol}://${req.headers[\":authority\"] || req.headers.host}`;\n const response = await router.handler(getRequest({ base, request: req }));\n return setResponse(res, response);\n\n } else {\n return expressApp['handle'](req, res);\n }\n };\n\n } else {\n next = toNodeHandler(router.handler);\n }\n\n // handle cors headers for all requests by default\n server.prependListener('request', (req: IncomingMessage, res: ServerResponse) => {\n const corsHeaders = {\n ...controller.DEFAULT_CORS_HEADERS,\n ...controller.getCorsHeaders(new Headers(req.headers as any)),\n };\n\n if (req.method === \"OPTIONS\") {\n res.writeHead(204, corsHeaders);\n res.end();\n return;\n }\n\n Object.entries(corsHeaders).forEach(([key, value]) => {\n res.setHeader(key, value);\n });\n\n next(req, res);\n });\n}\n\n/**\n * Whether the express app already handles the root route, in which case\n * Colyseus must not register its own default \"/\" endpoint over it.\n */\nfunction hasExpressRootRoute(expressApp: express.Application) {\n return expressRouterStack(expressApp).some((layer: any) =>\n layer.match('/') && !['query', 'expressInit'].includes(layer.name));\n}\n\n/**\n * The app's router stack, or an empty stack if it has no routes yet.\n *\n * express v5 exposes the router as `app.router`; v4 exposes it as `app._router`\n * and only creates it once the first route/middleware is registered.\n */\nfunction expressRouterStack(expressApp: express.Application): any[] {\n const app = expressApp as any;\n\n if (app?._router?.stack) {\n return app._router.stack;\n }\n\n try {\n return app?.router?.stack ?? [];\n } catch (e) {\n return []; // express v4 throws on `app.router` \u2014 no routes registered\n }\n}\n\nexport function createRouter<\n E extends Record<string, Endpoint>,\n Config extends RouterConfig\n>(endpoints: E, config: Config = {} as Config) {\n const onError = config?.onError;\n return createBetterCallRouter({ ...endpoints }, {\n // better-call's /api/reference page dumps the full API surface\n // unauthenticated \u2014 opt back in by passing `openapi` explicitly.\n openapi: { disabled: true },\n ...config,\n // Otherwise a malformed body is a 500 plus a stack trace on stderr: log\n // noise any anonymous client can trigger at will. Matched on the message\n // because `onError` receives no request context to test against.\n onError: async (error: unknown) => (error instanceof SyntaxError && error.message.includes('JSON'))\n ? Response.json({ error: 'malformed request body' }, { status: 400 })\n : await onError?.(error),\n });\n}\n\nexport interface BasicAuthOptions {\n /** username \u2192 password. The common static case. */\n users?: Record<string, string>;\n /** Custom validator (e.g. DB-backed). Takes precedence over `users`. */\n validate?: (username: string, password: string) => boolean | Promise<boolean>;\n /** Realm shown in the browser prompt. Default 'Restricted'. */\n realm?: string;\n}\n\n/**\n * HTTP Basic Auth middleware. Drop into any endpoint's `use:` slot to gate\n * it behind a browser credentials prompt:\n *\n * playground({ use: [basicAuth({ users: { admin: 's3cret' } })] })\n */\nexport function basicAuth(opts: BasicAuthOptions) {\n const { users, validate } = opts;\n if (!users && !validate) {\n throw new Error('[basicAuth] provide `users` or `validate`');\n }\n // Realm is interpolated into a header \u2014 strip `\"` so it can't break out.\n const challenge = `Basic realm=\"${(opts.realm ?? 'Restricted').replace(/\"/g, '')}\", charset=\"UTF-8\"`;\n\n return createMiddleware(async (ctx) => {\n const creds = parseBasicHeader(ctx.getHeader('authorization'));\n const ok = !!creds && (validate\n ? await validate(creds.username, creds.password)\n : staticCheck(users!, creds.username, creds.password));\n if (!ok) {\n throw new APIError(401, { message: 'authentication required' }, { 'WWW-Authenticate': challenge });\n }\n });\n}\n\nfunction parseBasicHeader(header: string | null | undefined) {\n if (!header) { return null; }\n const sep = header.indexOf(' ');\n if (sep < 0 || header.slice(0, sep).toLowerCase() !== 'basic') { return null; }\n let decoded: string;\n try { decoded = Buffer.from(header.slice(sep + 1), 'base64').toString('utf8'); } catch { return null; }\n const colon = decoded.indexOf(':');\n if (colon < 0) { return null; }\n return { username: decoded.slice(0, colon), password: decoded.slice(colon + 1) };\n}\n\nfunction staticCheck(users: Record<string, string>, username: string, password: string): boolean {\n const expected = Object.prototype.hasOwnProperty.call(users, username) ? users[username] : undefined;\n // Compare even for an unknown user so reject timing doesn't reveal which\n // usernames exist.\n return safeEqual(password, expected ?? '\\0') && expected !== undefined;\n}\n\n// Hash both sides first: equalizes length (timingSafeEqual throws on a\n// length mismatch, which would itself leak the secret's length).\nfunction safeEqual(a: string, b: string): boolean {\n return timingSafeEqual(\n createHash('sha256').update(a).digest(),\n createHash('sha256').update(b).digest(),\n );\n}\n\n// ---------------------------------------------------------------------------\n// dualModeEndpoints \u2014 shared express-compat layer for @colyseus/admin,\n// @colyseus/monitor, @colyseus/playground. Builds the two local routers\n// (specific = no catch-all, full = everything) and the matching node\n// handlers, then packages the express middleware so the return value works\n// both as `{...spread}` into createRouter AND as `app.use(\"/\", x)` middleware.\n// ---------------------------------------------------------------------------\n\nexport type ExpressMiddleware = (\n req: IncomingMessage,\n res: ServerResponse,\n next: (err?: any) => void,\n) => void;\n\nexport type NodeHandler = (req: IncomingMessage, res: ServerResponse) => Promise<void>;\n\nexport interface DualModeHelpers {\n specificRouter: Router;\n specificHandler: NodeHandler;\n fullRouter: Router;\n fullHandler: NodeHandler;\n}\n\nexport function dualModeEndpoints<E extends Record<string, Endpoint>>(\n endpoints: E,\n opts: {\n /** Key in `endpoints` whose path is a catch-all. Excluded from `specificRouter` so it doesn't eat fall-through decisions. */\n catchAllKey?: keyof E;\n /** Build the express middleware given the pre-built routers + node handlers. */\n buildMiddleware: (helpers: DualModeHelpers) => ExpressMiddleware;\n },\n): ExpressMiddleware & E {\n const fullRouter = createRouter(endpoints);\n const fullHandler = toNodeHandler(fullRouter.handler) as NodeHandler;\n\n const specificEndpoints = opts.catchAllKey\n ? Object.fromEntries(\n Object.entries(endpoints).filter(([k]) => k !== opts.catchAllKey),\n ) as Partial<E>\n : endpoints;\n const specificRouter = createRouter(specificEndpoints as E);\n const specificHandler = toNodeHandler(specificRouter.handler) as NodeHandler;\n\n const middleware = opts.buildMiddleware({\n specificRouter, specificHandler, fullRouter, fullHandler,\n });\n return Object.assign(middleware, endpoints) as ExpressMiddleware & E;\n}\n"],
|
|
5
|
-
"mappings": "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
|
|
6
|
-
"names": ["import_better_call", "pkg", "createBetterCallRouter"]
|
|
4
|
+
"sourcesContent": ["import type express from \"express\";\nimport type { IncomingMessage, ServerResponse } from \"http\";\nimport fs from \"node:fs/promises\";\nimport path from \"node:path\";\nimport { createHash, timingSafeEqual } from \"node:crypto\";\nimport { type Endpoint, type Router, type RouterConfig, createRouter as createBetterCallRouter, createEndpoint, createMiddleware, APIError } from \"@colyseus/better-call\";\nimport { toNodeHandler, getRequest, setResponse } from \"@colyseus/better-call/node\";\nimport { Transport } from \"../Transport.ts\";\nimport { controller } from \"../matchmaker/controller.ts\";\nimport pkg from \"../../package.json\" with { type: \"json\" };\n\nexport {\n createEndpoint,\n createMiddleware,\n createInternalContext,\n\n // Re-export every type reachable from an inferred type below \u2014 consumers\n // depend on @colyseus/core, not @colyseus/better-call, and cannot name it\n // under pnpm's isolated node_modules (TS2742/TS2883).\n type Router,\n type RouterConfig,\n type Endpoint,\n type EndpointHandler,\n type EndpointOptions,\n type EndpointContext,\n type StrictEndpoint,\n type StandardSchemaV1,\n type MiddlewareOptions,\n type MiddlewareInputContext,\n type CookieOptions,\n type CookiePrefixOptions,\n type Status,\n} from \"@colyseus/better-call\";\n\nexport { toNodeHandler };\n\nexport function bindRouterToTransport(transport: Transport, router: Router, useExpress: boolean) {\n // add default \"/__healthcheck\" endpoint\n router.addEndpoint(createEndpoint(\"/__healthcheck\", { method: \"GET\" }, async (ctx) => {\n return new Response(\"OK\", { status: 200 });\n }));\n\n const server = transport.server;\n\n // check if the server is bound to an express app\n const expressApp: express.Application = (useExpress)\n ? transport.getExpressApp() as express.Application\n // fallback searching for express app in server listeners\n : server?.listeners('request').find((listener: Function) => listener.name === \"app\" && listener['mountpath'] === '/') as express.Application;\n\n // add default \"/\" route, if not provided.\n const hasRootRoute = (\n // check if express app has a root route\n (expressApp && hasExpressRootRoute(expressApp)) ||\n\n // check if router has a root route\n Object.values(router.endpoints).some(endpoint => endpoint.path === \"/\")\n );\n\n if (!hasRootRoute) {\n router.addEndpoint(createEndpoint(\"/\", { method: \"GET\" }, async (ctx) => {\n return new Response(`Colyseus ${pkg.version}`, { status: 200 });\n }));\n }\n\n // use custom bindRouter method if provided\n if (!server && transport.bindRouter) {\n transport.bindRouter(router);\n return;\n }\n\n // which route handler to use\n // (router + fallback to express, or just router)\n let next: any;\n\n if (expressApp) {\n server.removeListener('request', expressApp);\n\n next = async (req: IncomingMessage, res: ServerResponse) => {\n // check if the route is defined in the router\n // if so, use the router handler, otherwise fallback to express\n if (router.findRoute(req.method, req.url.split('?')[0]) !== undefined) {\n const protocol = req.headers[\"x-forwarded-proto\"] || ((req.socket as any).encrypted ? \"https\" : \"http\");\n const base = `${protocol}://${req.headers[\":authority\"] || req.headers.host}`;\n const response = await router.handler(getRequest({ base, request: req }));\n return setResponse(res, response);\n\n } else {\n return expressApp['handle'](req, res);\n }\n };\n\n } else {\n next = toNodeHandler(router.handler);\n }\n\n // handle cors headers for all requests by default\n server.prependListener('request', (req: IncomingMessage, res: ServerResponse) => {\n const corsHeaders = {\n ...controller.DEFAULT_CORS_HEADERS,\n ...controller.getCorsHeaders(new Headers(req.headers as any)),\n };\n\n if (req.method === \"OPTIONS\") {\n res.writeHead(204, corsHeaders);\n res.end();\n return;\n }\n\n Object.entries(corsHeaders).forEach(([key, value]) => {\n res.setHeader(key, value);\n });\n\n next(req, res);\n });\n}\n\n/**\n * Whether the express app already handles the root route, in which case\n * Colyseus must not register its own default \"/\" endpoint over it.\n */\nfunction hasExpressRootRoute(expressApp: express.Application) {\n return expressRouterStack(expressApp).some((layer: any) =>\n layer.match('/') && !['query', 'expressInit'].includes(layer.name));\n}\n\n/**\n * The app's router stack, or an empty stack if it has no routes yet.\n *\n * express v5 exposes the router as `app.router`; v4 exposes it as `app._router`\n * and only creates it once the first route/middleware is registered.\n */\nfunction expressRouterStack(expressApp: express.Application): any[] {\n const app = expressApp as any;\n\n if (app?._router?.stack) {\n return app._router.stack;\n }\n\n try {\n return app?.router?.stack ?? [];\n } catch (e) {\n return []; // express v4 throws on `app.router` \u2014 no routes registered\n }\n}\n\nexport function createRouter<\n E extends Record<string, Endpoint>,\n Config extends RouterConfig\n>(endpoints: E, config: Config = {} as Config) {\n const onError = config?.onError;\n return createBetterCallRouter({ ...endpoints }, {\n // better-call's /api/reference page dumps the full API surface\n // unauthenticated \u2014 opt back in by passing `openapi` explicitly.\n openapi: { disabled: true },\n ...config,\n // Otherwise a malformed body is a 500 plus a stack trace on stderr: log\n // noise any anonymous client can trigger at will. Matched on the message\n // because `onError` receives no request context to test against.\n onError: async (error: unknown) => (error instanceof SyntaxError && error.message.includes('JSON'))\n ? Response.json({ error: 'malformed request body' }, { status: 400 })\n : await onError?.(error),\n });\n}\n\nexport interface BasicAuthOptions {\n /** username \u2192 password. The common static case. */\n users?: Record<string, string>;\n /** Custom validator (e.g. DB-backed). Takes precedence over `users`. */\n validate?: (username: string, password: string) => boolean | Promise<boolean>;\n /** Realm shown in the browser prompt. Default 'Restricted'. */\n realm?: string;\n}\n\n/**\n * HTTP Basic Auth middleware. Drop into any endpoint's `use:` slot to gate\n * it behind a browser credentials prompt:\n *\n * playground({ use: [basicAuth({ users: { admin: 's3cret' } })] })\n */\nexport function basicAuth(opts: BasicAuthOptions) {\n const { users, validate } = opts;\n if (!users && !validate) {\n throw new Error('[basicAuth] provide `users` or `validate`');\n }\n // Realm is interpolated into a header \u2014 strip `\"` so it can't break out.\n const challenge = `Basic realm=\"${(opts.realm ?? 'Restricted').replace(/\"/g, '')}\", charset=\"UTF-8\"`;\n\n return createMiddleware(async (ctx) => {\n const creds = parseBasicHeader(ctx.getHeader('authorization'));\n const ok = !!creds && (validate\n ? await validate(creds.username, creds.password)\n : staticCheck(users!, creds.username, creds.password));\n if (!ok) {\n throw new APIError(401, { message: 'authentication required' }, { 'WWW-Authenticate': challenge });\n }\n });\n}\n\nfunction parseBasicHeader(header: string | null | undefined) {\n if (!header) { return null; }\n const sep = header.indexOf(' ');\n if (sep < 0 || header.slice(0, sep).toLowerCase() !== 'basic') { return null; }\n let decoded: string;\n try { decoded = Buffer.from(header.slice(sep + 1), 'base64').toString('utf8'); } catch { return null; }\n const colon = decoded.indexOf(':');\n if (colon < 0) { return null; }\n return { username: decoded.slice(0, colon), password: decoded.slice(colon + 1) };\n}\n\nfunction staticCheck(users: Record<string, string>, username: string, password: string): boolean {\n const expected = Object.prototype.hasOwnProperty.call(users, username) ? users[username] : undefined;\n // Compare even for an unknown user so reject timing doesn't reveal which\n // usernames exist.\n return safeEqual(password, expected ?? '\\0') && expected !== undefined;\n}\n\n// Hash both sides first: equalizes length (timingSafeEqual throws on a\n// length mismatch, which would itself leak the secret's length).\nfunction safeEqual(a: string, b: string): boolean {\n return timingSafeEqual(\n createHash('sha256').update(a).digest(),\n createHash('sha256').update(b).digest(),\n );\n}\n\n// ---------------------------------------------------------------------------\n// dualModeEndpoints \u2014 shared express-compat layer for @colyseus/admin,\n// @colyseus/monitor, @colyseus/playground. Builds the two local routers\n// (specific = no catch-all, full = everything) and the matching node\n// handlers, then packages the express middleware so the return value works\n// both as `{...spread}` into createRouter AND as `app.use(\"/\", x)` middleware.\n// ---------------------------------------------------------------------------\n\nexport type ExpressMiddleware = (\n req: IncomingMessage,\n res: ServerResponse,\n next: (err?: any) => void,\n) => void;\n\nexport type NodeHandler = (req: IncomingMessage, res: ServerResponse) => Promise<void>;\n\nexport interface DualModeHelpers {\n specificRouter: Router;\n specificHandler: NodeHandler;\n fullRouter: Router;\n fullHandler: NodeHandler;\n}\n\nexport function dualModeEndpoints<E extends Record<string, Endpoint>>(\n endpoints: E,\n opts: {\n /** Key in `endpoints` whose path is a catch-all. Excluded from `specificRouter` so it doesn't eat fall-through decisions. */\n catchAllKey?: keyof E;\n /** Endpoint-path prefix baked into `endpoints` (`''`, `'/monitor'`). Used by the default middleware to rebase path mounts. */\n prefix?: string;\n /** SPA dist dir \u2014 the default middleware dispatches catch-all requests only for files that exist here. */\n staticDir?: string;\n /** Build a custom express middleware given the pre-built routers + node handlers. Omit for the default SPA-panel middleware. */\n buildMiddleware?: (helpers: DualModeHelpers) => ExpressMiddleware;\n },\n): ExpressMiddleware & E {\n const fullRouter = createRouter(endpoints);\n const fullHandler = toNodeHandler(fullRouter.handler) as NodeHandler;\n\n const specificEndpoints = opts.catchAllKey\n ? Object.fromEntries(\n Object.entries(endpoints).filter(([k]) => k !== opts.catchAllKey),\n ) as Partial<E>\n : endpoints;\n const specificRouter = createRouter(specificEndpoints as E);\n const specificHandler = toNodeHandler(specificRouter.handler) as NodeHandler;\n\n const buildMiddleware = opts.buildMiddleware ?? panelMiddleware(opts.prefix ?? '', opts.staticDir);\n const middleware = buildMiddleware({\n specificRouter, specificHandler, fullRouter, fullHandler,\n });\n return Object.assign(middleware, endpoints) as ExpressMiddleware & E;\n}\n\n/**\n * Default express middleware for a single-prefix SPA panel (monitor,\n * playground). Express and the endpoint map use different coordinates: a path\n * mount strips its prefix into `req.baseUrl`, while the endpoints keep the\n * configured `prefix` baked into their paths. This rebases every request onto\n * the endpoint namespace, so the panel works at any mount path \u2014 `prefix` only\n * matters in router mode and at root mounts.\n */\nfunction panelMiddleware(prefix: string, staticDir?: string) {\n const staticRoot = staticDir && path.resolve(staticDir);\n return ({ specificRouter, specificHandler, fullHandler }: DualModeHelpers): ExpressMiddleware =>\n (req, res, next) => {\n const r = req as IncomingMessage & { baseUrl?: string; originalUrl?: string };\n const raw = r.url ?? '';\n const q = raw.indexOf('?');\n const url = q < 0 ? raw : raw.slice(0, q);\n const query = q < 0 ? '' : raw.slice(q);\n\n // Path mounts arrive stripped (`req.url` is mount-relative); root and\n // pathless mounts arrive unstripped and already match the endpoints.\n const dispatchPath = r.baseUrl ? prefix + url : url;\n\n // Canonicalize the index: the SPA references its assets relatively, so\n // they only resolve from the trailing-slash URL. The browser's address\n // is originalUrl \u2014 302 (not 301: browsers cache 301s across remounts).\n if (dispatchPath === prefix || dispatchPath === `${prefix}/`) {\n const original = (r.originalUrl ?? raw).split('?')[0]!;\n if (r.method === 'GET' && !original.endsWith('/')) {\n res.writeHead(302, { location: `${original}/${query}` });\n res.end();\n return;\n }\n }\n\n const dispatch = (handler: NodeHandler) => {\n // better-call's getRequest resolves the path as baseUrl + url\n const wrapped = Object.create(req, {\n url: { value: dispatchPath + query, enumerable: true, configurable: true },\n baseUrl: { value: '', enumerable: true, configurable: true },\n originalUrl: { value: dispatchPath + query, enumerable: true, configurable: true },\n });\n handler(wrapped as any, res).catch(next);\n };\n\n const route = specificRouter.findRoute(r.method ?? 'GET', dispatchPath);\n // rou3 normalizes trailing slashes in findRoute but processRequest\n // exact-matches \u2014 only trust the hit when the slashes agree.\n if (route && (!route.data?.path?.endsWith('/') || dispatchPath.endsWith('/'))) {\n return dispatch(specificHandler);\n }\n\n // Asset request \u2014 only delegate when the file exists on disk, so the\n // catch-all's SPA fallback can't mask sibling express routes.\n if (r.method !== 'GET' || !staticRoot || !dispatchPath.startsWith(prefix)) { return next(); }\n const rel = dispatchPath.slice(prefix.length).replace(/^\\/+/, '');\n if (!rel || rel.includes('..')) { return next(); }\n const filePath = path.resolve(staticRoot, rel);\n if (!filePath.startsWith(staticRoot + path.sep)) { return next(); }\n fs.stat(filePath).then((stat) => stat.isFile() ? dispatch(fullHandler) : next()).catch(() => next());\n };\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["import_better_call", "pkg", "createBetterCallRouter", "path", "fs"]
|
|
7
7
|
}
|
package/build/router/index.d.ts
CHANGED
|
@@ -183,6 +183,10 @@ export interface DualModeHelpers {
|
|
|
183
183
|
export declare function dualModeEndpoints<E extends Record<string, Endpoint>>(endpoints: E, opts: {
|
|
184
184
|
/** Key in `endpoints` whose path is a catch-all. Excluded from `specificRouter` so it doesn't eat fall-through decisions. */
|
|
185
185
|
catchAllKey?: keyof E;
|
|
186
|
-
/**
|
|
187
|
-
|
|
186
|
+
/** Endpoint-path prefix baked into `endpoints` (`''`, `'/monitor'`). Used by the default middleware to rebase path mounts. */
|
|
187
|
+
prefix?: string;
|
|
188
|
+
/** SPA dist dir — the default middleware dispatches catch-all requests only for files that exist here. */
|
|
189
|
+
staticDir?: string;
|
|
190
|
+
/** Build a custom express middleware given the pre-built routers + node handlers. Omit for the default SPA-panel middleware. */
|
|
191
|
+
buildMiddleware?: (helpers: DualModeHelpers) => ExpressMiddleware;
|
|
188
192
|
}): ExpressMiddleware & E;
|
package/build/router/index.mjs
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
// packages/core/src/router/index.ts
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
2
4
|
import { createHash, timingSafeEqual } from "node:crypto";
|
|
3
5
|
import { createRouter as createBetterCallRouter, createEndpoint, createMiddleware, APIError } from "@colyseus/better-call";
|
|
4
6
|
import { toNodeHandler, getRequest, setResponse } from "@colyseus/better-call/node";
|
|
@@ -141,7 +143,8 @@ function dualModeEndpoints(endpoints, opts) {
|
|
|
141
143
|
) : endpoints;
|
|
142
144
|
const specificRouter = createRouter(specificEndpoints);
|
|
143
145
|
const specificHandler = toNodeHandler(specificRouter.handler);
|
|
144
|
-
const
|
|
146
|
+
const buildMiddleware = opts.buildMiddleware ?? panelMiddleware(opts.prefix ?? "", opts.staticDir);
|
|
147
|
+
const middleware = buildMiddleware({
|
|
145
148
|
specificRouter,
|
|
146
149
|
specificHandler,
|
|
147
150
|
fullRouter,
|
|
@@ -149,6 +152,49 @@ function dualModeEndpoints(endpoints, opts) {
|
|
|
149
152
|
});
|
|
150
153
|
return Object.assign(middleware, endpoints);
|
|
151
154
|
}
|
|
155
|
+
function panelMiddleware(prefix, staticDir) {
|
|
156
|
+
const staticRoot = staticDir && path.resolve(staticDir);
|
|
157
|
+
return ({ specificRouter, specificHandler, fullHandler }) => (req, res, next) => {
|
|
158
|
+
const r = req;
|
|
159
|
+
const raw = r.url ?? "";
|
|
160
|
+
const q = raw.indexOf("?");
|
|
161
|
+
const url = q < 0 ? raw : raw.slice(0, q);
|
|
162
|
+
const query = q < 0 ? "" : raw.slice(q);
|
|
163
|
+
const dispatchPath = r.baseUrl ? prefix + url : url;
|
|
164
|
+
if (dispatchPath === prefix || dispatchPath === `${prefix}/`) {
|
|
165
|
+
const original = (r.originalUrl ?? raw).split("?")[0];
|
|
166
|
+
if (r.method === "GET" && !original.endsWith("/")) {
|
|
167
|
+
res.writeHead(302, { location: `${original}/${query}` });
|
|
168
|
+
res.end();
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
const dispatch = (handler) => {
|
|
173
|
+
const wrapped = Object.create(req, {
|
|
174
|
+
url: { value: dispatchPath + query, enumerable: true, configurable: true },
|
|
175
|
+
baseUrl: { value: "", enumerable: true, configurable: true },
|
|
176
|
+
originalUrl: { value: dispatchPath + query, enumerable: true, configurable: true }
|
|
177
|
+
});
|
|
178
|
+
handler(wrapped, res).catch(next);
|
|
179
|
+
};
|
|
180
|
+
const route = specificRouter.findRoute(r.method ?? "GET", dispatchPath);
|
|
181
|
+
if (route && (!route.data?.path?.endsWith("/") || dispatchPath.endsWith("/"))) {
|
|
182
|
+
return dispatch(specificHandler);
|
|
183
|
+
}
|
|
184
|
+
if (r.method !== "GET" || !staticRoot || !dispatchPath.startsWith(prefix)) {
|
|
185
|
+
return next();
|
|
186
|
+
}
|
|
187
|
+
const rel = dispatchPath.slice(prefix.length).replace(/^\/+/, "");
|
|
188
|
+
if (!rel || rel.includes("..")) {
|
|
189
|
+
return next();
|
|
190
|
+
}
|
|
191
|
+
const filePath = path.resolve(staticRoot, rel);
|
|
192
|
+
if (!filePath.startsWith(staticRoot + path.sep)) {
|
|
193
|
+
return next();
|
|
194
|
+
}
|
|
195
|
+
fs.stat(filePath).then((stat) => stat.isFile() ? dispatch(fullHandler) : next()).catch(() => next());
|
|
196
|
+
};
|
|
197
|
+
}
|
|
152
198
|
export {
|
|
153
199
|
basicAuth,
|
|
154
200
|
bindRouterToTransport,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/router/index.ts"],
|
|
4
|
-
"sourcesContent": ["import type express from \"express\";\nimport type { IncomingMessage, ServerResponse } from \"http\";\nimport { createHash, timingSafeEqual } from \"node:crypto\";\nimport { type Endpoint, type Router, type RouterConfig, createRouter as createBetterCallRouter, createEndpoint, createMiddleware, APIError } from \"@colyseus/better-call\";\nimport { toNodeHandler, getRequest, setResponse } from \"@colyseus/better-call/node\";\nimport { Transport } from \"../Transport.ts\";\nimport { controller } from \"../matchmaker/controller.ts\";\nimport pkg from \"../../package.json\" with { type: \"json\" };\n\nexport {\n createEndpoint,\n createMiddleware,\n createInternalContext,\n\n // Re-export every type reachable from an inferred type below \u2014 consumers\n // depend on @colyseus/core, not @colyseus/better-call, and cannot name it\n // under pnpm's isolated node_modules (TS2742/TS2883).\n type Router,\n type RouterConfig,\n type Endpoint,\n type EndpointHandler,\n type EndpointOptions,\n type EndpointContext,\n type StrictEndpoint,\n type StandardSchemaV1,\n type MiddlewareOptions,\n type MiddlewareInputContext,\n type CookieOptions,\n type CookiePrefixOptions,\n type Status,\n} from \"@colyseus/better-call\";\n\nexport { toNodeHandler };\n\nexport function bindRouterToTransport(transport: Transport, router: Router, useExpress: boolean) {\n // add default \"/__healthcheck\" endpoint\n router.addEndpoint(createEndpoint(\"/__healthcheck\", { method: \"GET\" }, async (ctx) => {\n return new Response(\"OK\", { status: 200 });\n }));\n\n const server = transport.server;\n\n // check if the server is bound to an express app\n const expressApp: express.Application = (useExpress)\n ? transport.getExpressApp() as express.Application\n // fallback searching for express app in server listeners\n : server?.listeners('request').find((listener: Function) => listener.name === \"app\" && listener['mountpath'] === '/') as express.Application;\n\n // add default \"/\" route, if not provided.\n const hasRootRoute = (\n // check if express app has a root route\n (expressApp && hasExpressRootRoute(expressApp)) ||\n\n // check if router has a root route\n Object.values(router.endpoints).some(endpoint => endpoint.path === \"/\")\n );\n\n if (!hasRootRoute) {\n router.addEndpoint(createEndpoint(\"/\", { method: \"GET\" }, async (ctx) => {\n return new Response(`Colyseus ${pkg.version}`, { status: 200 });\n }));\n }\n\n // use custom bindRouter method if provided\n if (!server && transport.bindRouter) {\n transport.bindRouter(router);\n return;\n }\n\n // which route handler to use\n // (router + fallback to express, or just router)\n let next: any;\n\n if (expressApp) {\n server.removeListener('request', expressApp);\n\n next = async (req: IncomingMessage, res: ServerResponse) => {\n // check if the route is defined in the router\n // if so, use the router handler, otherwise fallback to express\n if (router.findRoute(req.method, req.url.split('?')[0]) !== undefined) {\n const protocol = req.headers[\"x-forwarded-proto\"] || ((req.socket as any).encrypted ? \"https\" : \"http\");\n const base = `${protocol}://${req.headers[\":authority\"] || req.headers.host}`;\n const response = await router.handler(getRequest({ base, request: req }));\n return setResponse(res, response);\n\n } else {\n return expressApp['handle'](req, res);\n }\n };\n\n } else {\n next = toNodeHandler(router.handler);\n }\n\n // handle cors headers for all requests by default\n server.prependListener('request', (req: IncomingMessage, res: ServerResponse) => {\n const corsHeaders = {\n ...controller.DEFAULT_CORS_HEADERS,\n ...controller.getCorsHeaders(new Headers(req.headers as any)),\n };\n\n if (req.method === \"OPTIONS\") {\n res.writeHead(204, corsHeaders);\n res.end();\n return;\n }\n\n Object.entries(corsHeaders).forEach(([key, value]) => {\n res.setHeader(key, value);\n });\n\n next(req, res);\n });\n}\n\n/**\n * Whether the express app already handles the root route, in which case\n * Colyseus must not register its own default \"/\" endpoint over it.\n */\nfunction hasExpressRootRoute(expressApp: express.Application) {\n return expressRouterStack(expressApp).some((layer: any) =>\n layer.match('/') && !['query', 'expressInit'].includes(layer.name));\n}\n\n/**\n * The app's router stack, or an empty stack if it has no routes yet.\n *\n * express v5 exposes the router as `app.router`; v4 exposes it as `app._router`\n * and only creates it once the first route/middleware is registered.\n */\nfunction expressRouterStack(expressApp: express.Application): any[] {\n const app = expressApp as any;\n\n if (app?._router?.stack) {\n return app._router.stack;\n }\n\n try {\n return app?.router?.stack ?? [];\n } catch (e) {\n return []; // express v4 throws on `app.router` \u2014 no routes registered\n }\n}\n\nexport function createRouter<\n E extends Record<string, Endpoint>,\n Config extends RouterConfig\n>(endpoints: E, config: Config = {} as Config) {\n const onError = config?.onError;\n return createBetterCallRouter({ ...endpoints }, {\n // better-call's /api/reference page dumps the full API surface\n // unauthenticated \u2014 opt back in by passing `openapi` explicitly.\n openapi: { disabled: true },\n ...config,\n // Otherwise a malformed body is a 500 plus a stack trace on stderr: log\n // noise any anonymous client can trigger at will. Matched on the message\n // because `onError` receives no request context to test against.\n onError: async (error: unknown) => (error instanceof SyntaxError && error.message.includes('JSON'))\n ? Response.json({ error: 'malformed request body' }, { status: 400 })\n : await onError?.(error),\n });\n}\n\nexport interface BasicAuthOptions {\n /** username \u2192 password. The common static case. */\n users?: Record<string, string>;\n /** Custom validator (e.g. DB-backed). Takes precedence over `users`. */\n validate?: (username: string, password: string) => boolean | Promise<boolean>;\n /** Realm shown in the browser prompt. Default 'Restricted'. */\n realm?: string;\n}\n\n/**\n * HTTP Basic Auth middleware. Drop into any endpoint's `use:` slot to gate\n * it behind a browser credentials prompt:\n *\n * playground({ use: [basicAuth({ users: { admin: 's3cret' } })] })\n */\nexport function basicAuth(opts: BasicAuthOptions) {\n const { users, validate } = opts;\n if (!users && !validate) {\n throw new Error('[basicAuth] provide `users` or `validate`');\n }\n // Realm is interpolated into a header \u2014 strip `\"` so it can't break out.\n const challenge = `Basic realm=\"${(opts.realm ?? 'Restricted').replace(/\"/g, '')}\", charset=\"UTF-8\"`;\n\n return createMiddleware(async (ctx) => {\n const creds = parseBasicHeader(ctx.getHeader('authorization'));\n const ok = !!creds && (validate\n ? await validate(creds.username, creds.password)\n : staticCheck(users!, creds.username, creds.password));\n if (!ok) {\n throw new APIError(401, { message: 'authentication required' }, { 'WWW-Authenticate': challenge });\n }\n });\n}\n\nfunction parseBasicHeader(header: string | null | undefined) {\n if (!header) { return null; }\n const sep = header.indexOf(' ');\n if (sep < 0 || header.slice(0, sep).toLowerCase() !== 'basic') { return null; }\n let decoded: string;\n try { decoded = Buffer.from(header.slice(sep + 1), 'base64').toString('utf8'); } catch { return null; }\n const colon = decoded.indexOf(':');\n if (colon < 0) { return null; }\n return { username: decoded.slice(0, colon), password: decoded.slice(colon + 1) };\n}\n\nfunction staticCheck(users: Record<string, string>, username: string, password: string): boolean {\n const expected = Object.prototype.hasOwnProperty.call(users, username) ? users[username] : undefined;\n // Compare even for an unknown user so reject timing doesn't reveal which\n // usernames exist.\n return safeEqual(password, expected ?? '\\0') && expected !== undefined;\n}\n\n// Hash both sides first: equalizes length (timingSafeEqual throws on a\n// length mismatch, which would itself leak the secret's length).\nfunction safeEqual(a: string, b: string): boolean {\n return timingSafeEqual(\n createHash('sha256').update(a).digest(),\n createHash('sha256').update(b).digest(),\n );\n}\n\n// ---------------------------------------------------------------------------\n// dualModeEndpoints \u2014 shared express-compat layer for @colyseus/admin,\n// @colyseus/monitor, @colyseus/playground. Builds the two local routers\n// (specific = no catch-all, full = everything) and the matching node\n// handlers, then packages the express middleware so the return value works\n// both as `{...spread}` into createRouter AND as `app.use(\"/\", x)` middleware.\n// ---------------------------------------------------------------------------\n\nexport type ExpressMiddleware = (\n req: IncomingMessage,\n res: ServerResponse,\n next: (err?: any) => void,\n) => void;\n\nexport type NodeHandler = (req: IncomingMessage, res: ServerResponse) => Promise<void>;\n\nexport interface DualModeHelpers {\n specificRouter: Router;\n specificHandler: NodeHandler;\n fullRouter: Router;\n fullHandler: NodeHandler;\n}\n\nexport function dualModeEndpoints<E extends Record<string, Endpoint>>(\n endpoints: E,\n opts: {\n /** Key in `endpoints` whose path is a catch-all. Excluded from `specificRouter` so it doesn't eat fall-through decisions. */\n catchAllKey?: keyof E;\n /** Build the express middleware given the pre-built routers + node handlers. */\n buildMiddleware: (helpers: DualModeHelpers) => ExpressMiddleware;\n },\n): ExpressMiddleware & E {\n const fullRouter = createRouter(endpoints);\n const fullHandler = toNodeHandler(fullRouter.handler) as NodeHandler;\n\n const specificEndpoints = opts.catchAllKey\n ? Object.fromEntries(\n Object.entries(endpoints).filter(([k]) => k !== opts.catchAllKey),\n ) as Partial<E>\n : endpoints;\n const specificRouter = createRouter(specificEndpoints as E);\n const specificHandler = toNodeHandler(specificRouter.handler) as NodeHandler;\n\n const middleware = opts.buildMiddleware({\n specificRouter, specificHandler, fullRouter, fullHandler,\n });\n return Object.assign(middleware, endpoints) as ExpressMiddleware & E;\n}\n"],
|
|
5
|
-
"mappings": "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
|
|
4
|
+
"sourcesContent": ["import type express from \"express\";\nimport type { IncomingMessage, ServerResponse } from \"http\";\nimport fs from \"node:fs/promises\";\nimport path from \"node:path\";\nimport { createHash, timingSafeEqual } from \"node:crypto\";\nimport { type Endpoint, type Router, type RouterConfig, createRouter as createBetterCallRouter, createEndpoint, createMiddleware, APIError } from \"@colyseus/better-call\";\nimport { toNodeHandler, getRequest, setResponse } from \"@colyseus/better-call/node\";\nimport { Transport } from \"../Transport.ts\";\nimport { controller } from \"../matchmaker/controller.ts\";\nimport pkg from \"../../package.json\" with { type: \"json\" };\n\nexport {\n createEndpoint,\n createMiddleware,\n createInternalContext,\n\n // Re-export every type reachable from an inferred type below \u2014 consumers\n // depend on @colyseus/core, not @colyseus/better-call, and cannot name it\n // under pnpm's isolated node_modules (TS2742/TS2883).\n type Router,\n type RouterConfig,\n type Endpoint,\n type EndpointHandler,\n type EndpointOptions,\n type EndpointContext,\n type StrictEndpoint,\n type StandardSchemaV1,\n type MiddlewareOptions,\n type MiddlewareInputContext,\n type CookieOptions,\n type CookiePrefixOptions,\n type Status,\n} from \"@colyseus/better-call\";\n\nexport { toNodeHandler };\n\nexport function bindRouterToTransport(transport: Transport, router: Router, useExpress: boolean) {\n // add default \"/__healthcheck\" endpoint\n router.addEndpoint(createEndpoint(\"/__healthcheck\", { method: \"GET\" }, async (ctx) => {\n return new Response(\"OK\", { status: 200 });\n }));\n\n const server = transport.server;\n\n // check if the server is bound to an express app\n const expressApp: express.Application = (useExpress)\n ? transport.getExpressApp() as express.Application\n // fallback searching for express app in server listeners\n : server?.listeners('request').find((listener: Function) => listener.name === \"app\" && listener['mountpath'] === '/') as express.Application;\n\n // add default \"/\" route, if not provided.\n const hasRootRoute = (\n // check if express app has a root route\n (expressApp && hasExpressRootRoute(expressApp)) ||\n\n // check if router has a root route\n Object.values(router.endpoints).some(endpoint => endpoint.path === \"/\")\n );\n\n if (!hasRootRoute) {\n router.addEndpoint(createEndpoint(\"/\", { method: \"GET\" }, async (ctx) => {\n return new Response(`Colyseus ${pkg.version}`, { status: 200 });\n }));\n }\n\n // use custom bindRouter method if provided\n if (!server && transport.bindRouter) {\n transport.bindRouter(router);\n return;\n }\n\n // which route handler to use\n // (router + fallback to express, or just router)\n let next: any;\n\n if (expressApp) {\n server.removeListener('request', expressApp);\n\n next = async (req: IncomingMessage, res: ServerResponse) => {\n // check if the route is defined in the router\n // if so, use the router handler, otherwise fallback to express\n if (router.findRoute(req.method, req.url.split('?')[0]) !== undefined) {\n const protocol = req.headers[\"x-forwarded-proto\"] || ((req.socket as any).encrypted ? \"https\" : \"http\");\n const base = `${protocol}://${req.headers[\":authority\"] || req.headers.host}`;\n const response = await router.handler(getRequest({ base, request: req }));\n return setResponse(res, response);\n\n } else {\n return expressApp['handle'](req, res);\n }\n };\n\n } else {\n next = toNodeHandler(router.handler);\n }\n\n // handle cors headers for all requests by default\n server.prependListener('request', (req: IncomingMessage, res: ServerResponse) => {\n const corsHeaders = {\n ...controller.DEFAULT_CORS_HEADERS,\n ...controller.getCorsHeaders(new Headers(req.headers as any)),\n };\n\n if (req.method === \"OPTIONS\") {\n res.writeHead(204, corsHeaders);\n res.end();\n return;\n }\n\n Object.entries(corsHeaders).forEach(([key, value]) => {\n res.setHeader(key, value);\n });\n\n next(req, res);\n });\n}\n\n/**\n * Whether the express app already handles the root route, in which case\n * Colyseus must not register its own default \"/\" endpoint over it.\n */\nfunction hasExpressRootRoute(expressApp: express.Application) {\n return expressRouterStack(expressApp).some((layer: any) =>\n layer.match('/') && !['query', 'expressInit'].includes(layer.name));\n}\n\n/**\n * The app's router stack, or an empty stack if it has no routes yet.\n *\n * express v5 exposes the router as `app.router`; v4 exposes it as `app._router`\n * and only creates it once the first route/middleware is registered.\n */\nfunction expressRouterStack(expressApp: express.Application): any[] {\n const app = expressApp as any;\n\n if (app?._router?.stack) {\n return app._router.stack;\n }\n\n try {\n return app?.router?.stack ?? [];\n } catch (e) {\n return []; // express v4 throws on `app.router` \u2014 no routes registered\n }\n}\n\nexport function createRouter<\n E extends Record<string, Endpoint>,\n Config extends RouterConfig\n>(endpoints: E, config: Config = {} as Config) {\n const onError = config?.onError;\n return createBetterCallRouter({ ...endpoints }, {\n // better-call's /api/reference page dumps the full API surface\n // unauthenticated \u2014 opt back in by passing `openapi` explicitly.\n openapi: { disabled: true },\n ...config,\n // Otherwise a malformed body is a 500 plus a stack trace on stderr: log\n // noise any anonymous client can trigger at will. Matched on the message\n // because `onError` receives no request context to test against.\n onError: async (error: unknown) => (error instanceof SyntaxError && error.message.includes('JSON'))\n ? Response.json({ error: 'malformed request body' }, { status: 400 })\n : await onError?.(error),\n });\n}\n\nexport interface BasicAuthOptions {\n /** username \u2192 password. The common static case. */\n users?: Record<string, string>;\n /** Custom validator (e.g. DB-backed). Takes precedence over `users`. */\n validate?: (username: string, password: string) => boolean | Promise<boolean>;\n /** Realm shown in the browser prompt. Default 'Restricted'. */\n realm?: string;\n}\n\n/**\n * HTTP Basic Auth middleware. Drop into any endpoint's `use:` slot to gate\n * it behind a browser credentials prompt:\n *\n * playground({ use: [basicAuth({ users: { admin: 's3cret' } })] })\n */\nexport function basicAuth(opts: BasicAuthOptions) {\n const { users, validate } = opts;\n if (!users && !validate) {\n throw new Error('[basicAuth] provide `users` or `validate`');\n }\n // Realm is interpolated into a header \u2014 strip `\"` so it can't break out.\n const challenge = `Basic realm=\"${(opts.realm ?? 'Restricted').replace(/\"/g, '')}\", charset=\"UTF-8\"`;\n\n return createMiddleware(async (ctx) => {\n const creds = parseBasicHeader(ctx.getHeader('authorization'));\n const ok = !!creds && (validate\n ? await validate(creds.username, creds.password)\n : staticCheck(users!, creds.username, creds.password));\n if (!ok) {\n throw new APIError(401, { message: 'authentication required' }, { 'WWW-Authenticate': challenge });\n }\n });\n}\n\nfunction parseBasicHeader(header: string | null | undefined) {\n if (!header) { return null; }\n const sep = header.indexOf(' ');\n if (sep < 0 || header.slice(0, sep).toLowerCase() !== 'basic') { return null; }\n let decoded: string;\n try { decoded = Buffer.from(header.slice(sep + 1), 'base64').toString('utf8'); } catch { return null; }\n const colon = decoded.indexOf(':');\n if (colon < 0) { return null; }\n return { username: decoded.slice(0, colon), password: decoded.slice(colon + 1) };\n}\n\nfunction staticCheck(users: Record<string, string>, username: string, password: string): boolean {\n const expected = Object.prototype.hasOwnProperty.call(users, username) ? users[username] : undefined;\n // Compare even for an unknown user so reject timing doesn't reveal which\n // usernames exist.\n return safeEqual(password, expected ?? '\\0') && expected !== undefined;\n}\n\n// Hash both sides first: equalizes length (timingSafeEqual throws on a\n// length mismatch, which would itself leak the secret's length).\nfunction safeEqual(a: string, b: string): boolean {\n return timingSafeEqual(\n createHash('sha256').update(a).digest(),\n createHash('sha256').update(b).digest(),\n );\n}\n\n// ---------------------------------------------------------------------------\n// dualModeEndpoints \u2014 shared express-compat layer for @colyseus/admin,\n// @colyseus/monitor, @colyseus/playground. Builds the two local routers\n// (specific = no catch-all, full = everything) and the matching node\n// handlers, then packages the express middleware so the return value works\n// both as `{...spread}` into createRouter AND as `app.use(\"/\", x)` middleware.\n// ---------------------------------------------------------------------------\n\nexport type ExpressMiddleware = (\n req: IncomingMessage,\n res: ServerResponse,\n next: (err?: any) => void,\n) => void;\n\nexport type NodeHandler = (req: IncomingMessage, res: ServerResponse) => Promise<void>;\n\nexport interface DualModeHelpers {\n specificRouter: Router;\n specificHandler: NodeHandler;\n fullRouter: Router;\n fullHandler: NodeHandler;\n}\n\nexport function dualModeEndpoints<E extends Record<string, Endpoint>>(\n endpoints: E,\n opts: {\n /** Key in `endpoints` whose path is a catch-all. Excluded from `specificRouter` so it doesn't eat fall-through decisions. */\n catchAllKey?: keyof E;\n /** Endpoint-path prefix baked into `endpoints` (`''`, `'/monitor'`). Used by the default middleware to rebase path mounts. */\n prefix?: string;\n /** SPA dist dir \u2014 the default middleware dispatches catch-all requests only for files that exist here. */\n staticDir?: string;\n /** Build a custom express middleware given the pre-built routers + node handlers. Omit for the default SPA-panel middleware. */\n buildMiddleware?: (helpers: DualModeHelpers) => ExpressMiddleware;\n },\n): ExpressMiddleware & E {\n const fullRouter = createRouter(endpoints);\n const fullHandler = toNodeHandler(fullRouter.handler) as NodeHandler;\n\n const specificEndpoints = opts.catchAllKey\n ? Object.fromEntries(\n Object.entries(endpoints).filter(([k]) => k !== opts.catchAllKey),\n ) as Partial<E>\n : endpoints;\n const specificRouter = createRouter(specificEndpoints as E);\n const specificHandler = toNodeHandler(specificRouter.handler) as NodeHandler;\n\n const buildMiddleware = opts.buildMiddleware ?? panelMiddleware(opts.prefix ?? '', opts.staticDir);\n const middleware = buildMiddleware({\n specificRouter, specificHandler, fullRouter, fullHandler,\n });\n return Object.assign(middleware, endpoints) as ExpressMiddleware & E;\n}\n\n/**\n * Default express middleware for a single-prefix SPA panel (monitor,\n * playground). Express and the endpoint map use different coordinates: a path\n * mount strips its prefix into `req.baseUrl`, while the endpoints keep the\n * configured `prefix` baked into their paths. This rebases every request onto\n * the endpoint namespace, so the panel works at any mount path \u2014 `prefix` only\n * matters in router mode and at root mounts.\n */\nfunction panelMiddleware(prefix: string, staticDir?: string) {\n const staticRoot = staticDir && path.resolve(staticDir);\n return ({ specificRouter, specificHandler, fullHandler }: DualModeHelpers): ExpressMiddleware =>\n (req, res, next) => {\n const r = req as IncomingMessage & { baseUrl?: string; originalUrl?: string };\n const raw = r.url ?? '';\n const q = raw.indexOf('?');\n const url = q < 0 ? raw : raw.slice(0, q);\n const query = q < 0 ? '' : raw.slice(q);\n\n // Path mounts arrive stripped (`req.url` is mount-relative); root and\n // pathless mounts arrive unstripped and already match the endpoints.\n const dispatchPath = r.baseUrl ? prefix + url : url;\n\n // Canonicalize the index: the SPA references its assets relatively, so\n // they only resolve from the trailing-slash URL. The browser's address\n // is originalUrl \u2014 302 (not 301: browsers cache 301s across remounts).\n if (dispatchPath === prefix || dispatchPath === `${prefix}/`) {\n const original = (r.originalUrl ?? raw).split('?')[0]!;\n if (r.method === 'GET' && !original.endsWith('/')) {\n res.writeHead(302, { location: `${original}/${query}` });\n res.end();\n return;\n }\n }\n\n const dispatch = (handler: NodeHandler) => {\n // better-call's getRequest resolves the path as baseUrl + url\n const wrapped = Object.create(req, {\n url: { value: dispatchPath + query, enumerable: true, configurable: true },\n baseUrl: { value: '', enumerable: true, configurable: true },\n originalUrl: { value: dispatchPath + query, enumerable: true, configurable: true },\n });\n handler(wrapped as any, res).catch(next);\n };\n\n const route = specificRouter.findRoute(r.method ?? 'GET', dispatchPath);\n // rou3 normalizes trailing slashes in findRoute but processRequest\n // exact-matches \u2014 only trust the hit when the slashes agree.\n if (route && (!route.data?.path?.endsWith('/') || dispatchPath.endsWith('/'))) {\n return dispatch(specificHandler);\n }\n\n // Asset request \u2014 only delegate when the file exists on disk, so the\n // catch-all's SPA fallback can't mask sibling express routes.\n if (r.method !== 'GET' || !staticRoot || !dispatchPath.startsWith(prefix)) { return next(); }\n const rel = dispatchPath.slice(prefix.length).replace(/^\\/+/, '');\n if (!rel || rel.includes('..')) { return next(); }\n const filePath = path.resolve(staticRoot, rel);\n if (!filePath.startsWith(staticRoot + path.sep)) { return next(); }\n fs.stat(filePath).then((stat) => stat.isFile() ? dispatch(fullHandler) : next()).catch(() => next());\n };\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": ["createEndpoint", "createMiddleware"]
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@colyseus/core",
|
|
3
|
-
"version": "0.18.
|
|
3
|
+
"version": "0.18.7",
|
|
4
4
|
"description": "Multiplayer Framework for Node.js.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"input": "./src/index.ts",
|
|
@@ -52,8 +52,8 @@
|
|
|
52
52
|
"msgpackr": "^2.0.1",
|
|
53
53
|
"nanoid": "^3.3.11",
|
|
54
54
|
"@colyseus/better-call": "^1.3.1",
|
|
55
|
-
"@colyseus/
|
|
56
|
-
"@colyseus/
|
|
55
|
+
"@colyseus/greeting-banner": "^4.0.1",
|
|
56
|
+
"@colyseus/shared-types": "^0.18.1"
|
|
57
57
|
},
|
|
58
58
|
"devDependencies": {
|
|
59
59
|
"@colyseus/schema": "^5.0.8",
|
|
@@ -68,9 +68,9 @@
|
|
|
68
68
|
"@pm2/io": "^6.1.0",
|
|
69
69
|
"express": "^4.16.0 || ^5.0.0",
|
|
70
70
|
"zod": "^4.1.12",
|
|
71
|
-
"@colyseus/ws-transport": "^0.18.1",
|
|
72
71
|
"@colyseus/auth": "^0.18.1",
|
|
73
|
-
"@colyseus/better-call": "^1.3.1"
|
|
72
|
+
"@colyseus/better-call": "^1.3.1",
|
|
73
|
+
"@colyseus/ws-transport": "^0.18.1"
|
|
74
74
|
},
|
|
75
75
|
"peerDependenciesMeta": {
|
|
76
76
|
"@colyseus/auth": {
|
package/src/Server.ts
CHANGED
|
@@ -11,7 +11,7 @@ import { Deferred, registerGracefulShutdown, dynamicImport, type Type } from './
|
|
|
11
11
|
|
|
12
12
|
import type { Presence } from "./presence/Presence.ts";
|
|
13
13
|
|
|
14
|
-
import { setTransport, Transport } from './Transport.ts';
|
|
14
|
+
import { getTransport, setTransport, Transport } from './Transport.ts';
|
|
15
15
|
import { logger, setLogger } from './Logger.ts';
|
|
16
16
|
import { setDevMode, isDevMode } from './utils/DevMode.ts';
|
|
17
17
|
import { type Router, bindRouterToTransport, createRouter } from './router/index.ts';
|
|
@@ -125,8 +125,6 @@ export class Server<
|
|
|
125
125
|
|
|
126
126
|
protected _onTransportReady = new Deferred<Transport>();
|
|
127
127
|
|
|
128
|
-
private _originalRoomOnMessage: typeof Room.prototype['_onMessage'] | null = null;
|
|
129
|
-
|
|
130
128
|
// Implicit default for callers that omit explicit Server reference — e.g.
|
|
131
129
|
// `playground()` reads `Server.current.router.endpoints` at request time.
|
|
132
130
|
// Last-construction wins; multi-server setups should reference instances
|
|
@@ -384,7 +382,7 @@ export class Server<
|
|
|
384
382
|
// this is going to lock all rooms and wait for them to be disposed
|
|
385
383
|
await matchMaker.gracefullyShutdown();
|
|
386
384
|
|
|
387
|
-
this.transport
|
|
385
|
+
this.transport?.shutdown();
|
|
388
386
|
this.presence?.shutdown();
|
|
389
387
|
await this.driver?.shutdown();
|
|
390
388
|
|
|
@@ -403,29 +401,19 @@ export class Server<
|
|
|
403
401
|
|
|
404
402
|
/**
|
|
405
403
|
* Add simulated latency between client and server.
|
|
404
|
+
*
|
|
405
|
+
* May be called at any time — before or after `listen()`. Also available
|
|
406
|
+
* via the `COLYSEUS_LATENCY` environment variable (round-trip ms), which
|
|
407
|
+
* overrides calls made before the server boots.
|
|
408
|
+
*
|
|
406
409
|
* @param milliseconds round trip latency in milliseconds.
|
|
407
410
|
*/
|
|
408
411
|
public simulateLatency(milliseconds: number) {
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
const halfwayMS = (milliseconds / 2);
|
|
416
|
-
this.transport.simulateLatency(halfwayMS);
|
|
417
|
-
|
|
418
|
-
if (this._originalRoomOnMessage == null) {
|
|
419
|
-
this._originalRoomOnMessage = Room.prototype['_onMessage'];
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
const originalOnMessage = this._originalRoomOnMessage;
|
|
423
|
-
|
|
424
|
-
Room.prototype['_onMessage'] = milliseconds <= Number.EPSILON ? originalOnMessage : function (this: Room, client, buffer) {
|
|
425
|
-
// uWebSockets.js: duplicate buffer because it is cleared at native layer before the timeout.
|
|
426
|
-
const cachedBuffer = Buffer.from(buffer);
|
|
427
|
-
setTimeout(() => originalOnMessage.call(this, client, cachedBuffer), halfwayMS);
|
|
428
|
-
};
|
|
412
|
+
// transport may still be resolving (constructor's attach() imports the default one)
|
|
413
|
+
this._onTransportReady.then(
|
|
414
|
+
(transport) => applySimulatedLatency(transport, milliseconds),
|
|
415
|
+
() => {/* missing-transport error already surfaces via attach()/listen() */},
|
|
416
|
+
);
|
|
429
417
|
}
|
|
430
418
|
|
|
431
419
|
/**
|
|
@@ -447,6 +435,11 @@ export class Server<
|
|
|
447
435
|
private async _bootServices(): Promise<void> {
|
|
448
436
|
const { beforeListen, database, express } = this.options;
|
|
449
437
|
|
|
438
|
+
// boot runs after module evaluation, so the env var wins over
|
|
439
|
+
// simulateLatency() calls made at the top level of user code
|
|
440
|
+
const envLatency = parseLatencyEnv();
|
|
441
|
+
if (envLatency !== undefined) { this.simulateLatency(envLatency); }
|
|
442
|
+
|
|
450
443
|
if (beforeListen) { await beforeListen(); }
|
|
451
444
|
if (database) { await database.boot(); }
|
|
452
445
|
|
|
@@ -505,6 +498,55 @@ export class Server<
|
|
|
505
498
|
() => Promise.resolve()
|
|
506
499
|
}
|
|
507
500
|
|
|
501
|
+
let _originalRoomOnMessage: typeof Room.prototype['_onMessage'] | null = null;
|
|
502
|
+
|
|
503
|
+
/**
|
|
504
|
+
* Applies both halves of the round-trip latency simulation: delays the
|
|
505
|
+
* transport's outgoing messages and the Room's incoming message handling.
|
|
506
|
+
*
|
|
507
|
+
* Prefer `server.simulateLatency()` — this is the shared core behind it, the
|
|
508
|
+
* dev-mode `defineServer()` object and the `COLYSEUS_LATENCY` env var, where
|
|
509
|
+
* no `Server` instance (or transport, yet) may exist.
|
|
510
|
+
*/
|
|
511
|
+
export function applySimulatedLatency(transport: Transport | undefined, milliseconds: number) {
|
|
512
|
+
if (milliseconds > 0) {
|
|
513
|
+
logger.warn(`📶️❗ Colyseus latency simulation enabled → ${milliseconds}ms latency for round trip.`);
|
|
514
|
+
} else {
|
|
515
|
+
logger.warn(`📶️❗ Colyseus latency simulation disabled.`);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
const halfwayMS = (milliseconds / 2);
|
|
519
|
+
transport?.simulateLatency(halfwayMS);
|
|
520
|
+
|
|
521
|
+
if (_originalRoomOnMessage == null) {
|
|
522
|
+
_originalRoomOnMessage = Room.prototype['_onMessage'];
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
const originalOnMessage = _originalRoomOnMessage;
|
|
526
|
+
|
|
527
|
+
Room.prototype['_onMessage'] = milliseconds <= Number.EPSILON ? originalOnMessage : function (this: Room, client, buffer) {
|
|
528
|
+
// uWebSockets.js: duplicate buffer because it is cleared at native layer before the timeout.
|
|
529
|
+
const cachedBuffer = Buffer.from(buffer);
|
|
530
|
+
setTimeout(() => originalOnMessage.call(this, client, cachedBuffer), halfwayMS);
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
/**
|
|
535
|
+
* Reads the `COLYSEUS_LATENCY` environment variable (round-trip milliseconds).
|
|
536
|
+
* Returns `undefined` when absent or empty; a non-numeric value warns and is ignored.
|
|
537
|
+
*/
|
|
538
|
+
export function parseLatencyEnv(): number | undefined {
|
|
539
|
+
const value = process.env.COLYSEUS_LATENCY;
|
|
540
|
+
if (value === undefined || value === '') { return undefined; }
|
|
541
|
+
|
|
542
|
+
const milliseconds = Number(value);
|
|
543
|
+
if (isNaN(milliseconds)) {
|
|
544
|
+
logger.warn(`📶️❗ Ignoring COLYSEUS_LATENCY: expected a number of milliseconds, got ${JSON.stringify(value)}.`);
|
|
545
|
+
return undefined;
|
|
546
|
+
}
|
|
547
|
+
return milliseconds;
|
|
548
|
+
}
|
|
549
|
+
|
|
508
550
|
export type RoomDefinitions = Record<string, RegisteredHandler | Type<Room>>;
|
|
509
551
|
|
|
510
552
|
function isRegisteredHandler(value: RegisteredHandler | Type<Room>): value is RegisteredHandler {
|
|
@@ -562,6 +604,8 @@ export function defineServer<
|
|
|
562
604
|
options: serverOptions,
|
|
563
605
|
router: routes,
|
|
564
606
|
'~rooms': rooms,
|
|
607
|
+
// the vite plugin registers the transport before importing user code
|
|
608
|
+
simulateLatency: (milliseconds: number) => applySimulatedLatency(getTransport(), milliseconds),
|
|
565
609
|
} as unknown as Server<T, R>;
|
|
566
610
|
}
|
|
567
611
|
|