@colyseus/core 0.18.18 → 0.18.20
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/Room.cjs +4 -4
- package/build/Room.cjs.map +2 -2
- package/build/Room.mjs +4 -4
- package/build/Room.mjs.map +2 -2
- package/build/RoomPlugin.cjs +34 -17
- package/build/RoomPlugin.cjs.map +2 -2
- package/build/RoomPlugin.mjs +34 -17
- package/build/RoomPlugin.mjs.map +2 -2
- package/build/Transport.cjs.map +2 -2
- package/build/Transport.d.ts +15 -0
- package/build/Transport.mjs.map +2 -2
- package/build/serializer/SchemaSerializer.cjs +16 -16
- package/build/serializer/SchemaSerializer.cjs.map +2 -2
- package/build/serializer/SchemaSerializer.d.ts +1 -3
- package/build/serializer/SchemaSerializer.mjs +16 -16
- package/build/serializer/SchemaSerializer.mjs.map +2 -2
- package/package.json +8 -8
- package/src/Room.ts +8 -8
- package/src/RoomPlugin.ts +46 -21
- package/src/Transport.ts +15 -0
- package/src/serializer/SchemaSerializer.ts +26 -19
package/build/RoomPlugin.cjs
CHANGED
|
@@ -170,29 +170,46 @@ function installPluginHookWrappers(ctor, layout) {
|
|
|
170
170
|
if (before.length === 0 && after.length === 0) {
|
|
171
171
|
continue;
|
|
172
172
|
}
|
|
173
|
-
|
|
174
|
-
proto[hook] = async function(...args) {
|
|
175
|
-
const lookup = (k) => k.startsWith(DEP_KEY_PREFIX) ? this._autoPlugins[k] : this.plugins[k];
|
|
176
|
-
for (const k of before) {
|
|
177
|
-
const p = lookup(k);
|
|
178
|
-
await p[hook].call(p, ...args);
|
|
179
|
-
}
|
|
180
|
-
let result;
|
|
181
|
-
if (original) {
|
|
182
|
-
result = await original.apply(this, args);
|
|
183
|
-
}
|
|
184
|
-
for (const k of after) {
|
|
185
|
-
const p = lookup(k);
|
|
186
|
-
await p[hook].call(p, ...args);
|
|
187
|
-
}
|
|
188
|
-
return result;
|
|
189
|
-
};
|
|
173
|
+
proto[hook] = composePluginHook(hook, before, after, proto[hook]);
|
|
190
174
|
}
|
|
191
175
|
}
|
|
176
|
+
function wrapOwnHooks(room, layout) {
|
|
177
|
+
for (const hook of PLUGIN_LIFECYCLE_KEYS) {
|
|
178
|
+
const { before, after } = layout.hooks[hook];
|
|
179
|
+
if (before.length === 0 && after.length === 0) {
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
if (!Object.hasOwn(room, hook)) {
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
room[hook] = composePluginHook(hook, before, after, room[hook]);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
function composePluginHook(hook, before, after, original) {
|
|
189
|
+
return async function(...args) {
|
|
190
|
+
const lookup = (k) => k.startsWith(DEP_KEY_PREFIX) ? this._autoPlugins[k] : this.plugins[k];
|
|
191
|
+
for (const k of before) {
|
|
192
|
+
const p = lookup(k);
|
|
193
|
+
await p[hook].call(p, ...args);
|
|
194
|
+
}
|
|
195
|
+
let result;
|
|
196
|
+
if (original) {
|
|
197
|
+
result = await original.apply(this, args);
|
|
198
|
+
}
|
|
199
|
+
for (const k of after) {
|
|
200
|
+
const p = lookup(k);
|
|
201
|
+
await p[hook].call(p, ...args);
|
|
202
|
+
}
|
|
203
|
+
return result;
|
|
204
|
+
};
|
|
205
|
+
}
|
|
192
206
|
function setupRoomPlugins(room) {
|
|
193
207
|
const plugins = room.plugins;
|
|
194
208
|
const layout = resolveOrComputeLayout(room.constructor, plugins);
|
|
195
209
|
attachRoomReference(room, plugins);
|
|
210
|
+
if (layout) {
|
|
211
|
+
wrapOwnHooks(room, layout);
|
|
212
|
+
}
|
|
196
213
|
if (layout && layout.autoDeps.length > 0) {
|
|
197
214
|
room._autoPlugins = instantiateAutoDeps(room, layout);
|
|
198
215
|
}
|
package/build/RoomPlugin.cjs.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/RoomPlugin.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * Room plugins.\n *\n * A plugin is a composable extension to a Room \u2014 it can declare message\n * handlers, attach lifecycle hooks, and expose public methods that the\n * room can call (`this.plugins.<key>.someMethod()`). The framework\n * instantiates one plugin per room, sets `this.room` after the room is\n * constructed, then merges the plugin's `messages` with the room's own\n * and wires its lifecycle hooks alongside the room's.\n *\n * Example: a tiny chat plugin contributing a single message handler:\n *\n * class ChatPlugin extends RoomPlugin {\n * private history: string[] = [];\n * constructor(private opts: { historyLimit: number }) { super(); }\n *\n * messages = {\n * chat: (client: Client, msg: { text: string }) => {\n * this.history.push(msg.text);\n * if (this.history.length > this.opts.historyLimit) this.history.shift();\n * this.room.broadcast('chat', { from: client.userId, text: msg.text });\n * },\n * };\n *\n * // Public \u2014 callable as `this.plugins.chat.getHistory()` from the room\n * getHistory(): readonly string[] { return this.history; }\n * }\n *\n * Use via `definePlugins`:\n *\n * class MyRoom extends Room<{ state: MyState }> {\n * plugins = definePlugins({\n * chat: new ChatPlugin({ historyLimit: 100 }),\n * });\n * }\n */\nimport type { Room } from './Room.ts';\nimport type { AuthContext, Client } from './Transport.ts';\nimport type { Messages } from '@colyseus/shared-types';\n\n/**\n * Ordering hint for a plugin's lifecycle hook relative to the room's own\n * hook. Sensible per-hook defaults are applied when omitted (see\n * `Room.__init` for the exact ordering policy).\n *\n * onCreate / onAuth / onJoin \u2192 plugins run BEFORE room (guards + setup)\n * onLeave / onDispose \u2192 plugins run AFTER room (capture final state)\n */\nexport interface RoomPluginOrder {\n onCreate?: 'before' | 'after';\n onAuth?: 'before' | 'after';\n onJoin?: 'before' | 'after';\n onLeave?: 'before' | 'after';\n onDispose?: 'before' | 'after';\n}\n\n/**\n * Base class for room plugins. Subclass to define a plugin; the framework\n * sets `this.room` after the host room is fully constructed.\n *\n * Don't access `this.room` from the plugin's constructor \u2014 it hasn't been\n * wired yet. Everything room-dependent goes in `onCreate` / `onJoin` /\n * etc. or in public methods that are called from the room post-init.\n *\n * @typeParam This - The Room subclass this plugin is attached to. Narrow\n * for schema-driven plugins that need a specific state shape, e.g.\n * `class PhysicsPlugin extends RoomPlugin<Room<{ state: PhysicsContract }>>`.\n */\nexport abstract class RoomPlugin<This extends Room = Room> {\n /**\n * Live room reference. Wired by the framework at __init, AFTER the\n * room's own construction \u2014 accessing it from the plugin's\n * constructor throws.\n */\n protected readonly room!: This;\n\n /**\n * Canonical key for the plugin when registered via `definePlugins([...])`.\n * Declared on the subclass with `as const` so the literal type flows\n * into `room.plugins.<key>`:\n *\n * class ChatPlugin extends RoomPlugin {\n * readonly pluginName = 'chat' as const;\n * }\n *\n * Optional under the keyed-record form (`definePlugins({ chat: ... })`),\n * required under the array form. Must stay `public` so `extends` /\n * `keyof` can see the literal \u2014 those are blind to protected from\n * outside the class. End-user autocomplete hides it via `Omit` in\n * `definePlugins`'s return type.\n *\n * For multi-instance use, accept the name at construction:\n *\n * readonly pluginName: string;\n * constructor(name = 'chat') { super(); this.pluginName = name; }\n */\n readonly pluginName?: string;\n\n /**\n * Declarative message handlers \u2014 merged into the room's `messages` at\n * __init. Conflict against the room's own key: room wins. Conflict\n * between two plugins: throws at __init.\n */\n protected messages?: Messages<This>;\n\n /** Optional per-hook ordering vs the room's own hook. */\n protected order?: RoomPluginOrder;\n\n // Lifecycle hooks \u2014 override any subset. `protected` so they don't\n // leak into `this.plugins.<key>.onJoin(...)` autocomplete; subclass\n // overrides must keep `protected` (TS widens to public silently\n // otherwise).\n protected onCreate?(options: any): void | Promise<void>;\n protected onAuth?(client: Client, options: any, context: AuthContext): void | Promise<void>;\n protected onJoin?(client: Client, options?: any): void | Promise<void>;\n protected onLeave?(client: Client, code?: number): void | Promise<void>;\n protected onDispose?(): void | Promise<void>;\n}\n\n/**\n * Plugin-class constructor type used by the `dependencies` static\n * declaration. Constrained to zero-arg constructors because the\n * framework auto-instantiates missing dependencies with no\n * configuration \u2014 plugins that need options can't be auto-included\n * and must be registered explicitly in `definePlugins({...})`.\n */\nexport type RoomPluginClass = new () => RoomPlugin<any>;\n\n/**\n * Static `dependencies` declaration. List other plugin classes this\n * plugin needs alongside it; the framework auto-instantiates any\n * missing ones at room construction time. Transitive deps are\n * resolved recursively. Cycles throw at class-init.\n *\n * Example:\n * class UniqueSessionPlugin extends RoomPlugin {\n * static dependencies: PluginDependencies = [TrackUserSessionsPlugin];\n * }\n */\nexport type PluginDependencies = ReadonlyArray<RoomPluginClass>;\n\n/**\n * Define a Room's plugin record. The framework wires plugins at\n * `__init` \u2014 first construct of the class computes the layout\n * (cached on the constructor) and installs hook wrappers on the\n * prototype. The `const T` modifier preserves literal types so\n * `this.plugins.<key>.method()` autocompletes against each plugin's\n * specific subclass.\n */\n// Pulls each plugin's `pluginName` literal as the record key. Subclass\n// must narrow with `as const` \u2014 plain `string` resolves to `never`\n// and the entry is dropped (callers see a TS error on\n// `plugins.<thatKey>`).\ntype ExtractPluginName<P> = P extends { pluginName: infer K }\n ? (K extends string ? K : never)\n : never;\n\n/** Hide `pluginName` from end-user autocomplete on `this.plugins.<key>`. */\ntype PluginPublicSurface<P> = Omit<P, 'pluginName'>;\n\ntype PluginsArrayToRecord<T extends readonly RoomPlugin<any>[]> = {\n [P in T[number] as ExtractPluginName<P>]: PluginPublicSurface<P>;\n};\n\n/**\n * Array form (recommended). Each plugin declares its own canonical\n * key via `readonly pluginName = '...' as const`; the framework\n * turns the array into a typed record so `plugins.<pluginName>`\n * autocompletes the right instance type.\n *\n * class GameRoom extends Room {\n * plugins = definePlugins([\n * new ChatPlugin(), // pluginName: 'chat'\n * new UniqueSessionPlugin({ max: 1 }), // pluginName: 'uniqueSession'\n * ]);\n * }\n * this.plugins.chat.send('hi');\n *\n * Throws at runtime if any plugin is missing `pluginName` or if two\n * plugins resolve to the same key.\n */\nexport function definePlugins<const T extends readonly RoomPlugin<any>[]>(\n plugins: T,\n): PluginsArrayToRecord<T>;\n\n/**\n * Record form (legacy / multi-instance escape hatch). The caller\n * chooses the key per-room. Useful when registering two instances of\n * the same plugin class without configuring `pluginName` per\n * instance \u2014 e.g. `{ adminChat: new ChatPlugin(), playerChat: new ChatPlugin() }`.\n */\nexport function definePlugins<const T extends Record<string, RoomPlugin<any>>>(\n plugins: T,\n): { [K in keyof T]: PluginPublicSurface<T[K]> };\n\nexport function definePlugins(plugins: any): any {\n if (!Array.isArray(plugins)) { return plugins; }\n const out: Record<string, RoomPlugin> = {};\n for (const p of plugins) {\n const key = p['pluginName'];\n if (typeof key !== 'string' || key.length === 0) {\n throw new Error(\n `[Room] plugin ${p.constructor.name} is missing a 'pluginName' field. ` +\n `Declare \\`readonly pluginName = '<key>' as const\\` on the class, ` +\n `or register it via the keyed-record form \\`definePlugins({ <key>: <plugin> })\\`.`,\n );\n }\n if (out[key]) {\n throw new Error(\n `[Room] two plugins resolve to pluginName \"${key}\". Configure one of ` +\n `them via constructor argument, or use the keyed-record form to disambiguate.`,\n );\n }\n out[key] = p;\n }\n return out;\n}\n\n/**\n * Lifecycle hook keys recognized by the framework \u2014 used internally to\n * separate \"framework-recognized methods\" from \"user-defined public\n * methods\" when wiring a plugin into a room. Exported for the test\n * harness; downstream code should not need it.\n */\nexport const PLUGIN_LIFECYCLE_KEYS = ['onCreate', 'onAuth', 'onJoin', 'onLeave', 'onDispose'] as const;\nexport type PluginLifecycleKey = (typeof PLUGIN_LIFECYCLE_KEYS)[number];\n\n/**\n * Test helper \u2014 attach a stub or fake room to a plugin so its methods\n * and lifecycle hooks can be exercised in isolation without spinning up\n * a real Colyseus server.\n *\n * const plugin = new ChatPlugin({ historyLimit: 5 });\n * const room = attachToTestRoom(plugin, { broadcast: sinon.spy() });\n * await plugin.messages!.chat!.call(plugin, { userId: 'u1' }, { text: 'hi' });\n *\n * The second arg is shallow-merged onto the stub so tests only declare\n * the room properties they actually exercise. Returns the room stub for\n * post-call assertions.\n */\nexport function attachToTestRoom<This extends Room, R extends Partial<This>>(\n plugin: RoomPlugin<This>,\n roomStub: R = {} as R,\n): R {\n (plugin as any).room = roomStub;\n return roomStub;\n}\n\n// ---------------------------------------------------------------------------\n// Layout machinery \u2014 used by Room.__init to set plugins up once per class.\n// Lives in this file (rather than Room.ts) so the plugin-related types and\n// helpers stay co-located. These are framework internals; callers outside\n// `@colyseus/core` should not depend on them.\n// ---------------------------------------------------------------------------\n\n/**\n * Precomputed plugin layout for a Room subclass \u2014 populated on first\n * construct, cached on the constructor. Hook wrappers are installed\n * on the prototype in the same pass (see `installPluginHookWrappers`).\n *\n * @internal\n */\nexport interface PluginLayout {\n /** Per-hook participation: which plugin keys run before/after the room's own hook. */\n hooks: Record<PluginLifecycleKey, { before: string[]; after: string[] }>;\n /** Message key \u2192 plugin key. Conflict detection ran when this was built. */\n messageOwners: Map<string, string>;\n /** Sentinel-keyed plugin classes to instantiate per room. */\n autoDeps: Array<{ key: string; ctor: RoomPluginClass }>;\n}\n\n/** Sentinel prefix for framework-instantiated deps. The colon prevents\n * collisions with any JS identifier the user could use as a key. */\nconst DEP_KEY_PREFIX = '__dep:';\n\n/**\n * Default before/after policy for lifecycle hooks vs the room's own\n * hook. Plugins can override per-hook via the `order` field on the\n * plugin instance.\n *\n * onCreate / onAuth / onJoin \u2192 plugins run BEFORE room (guards + setup)\n * onLeave / onDispose \u2192 plugins run AFTER room (capture final state)\n *\n * @internal\n */\nexport const DEFAULT_PLUGIN_ORDER: Record<PluginLifecycleKey, 'before' | 'after'> = {\n onCreate: 'before',\n onAuth: 'before',\n onJoin: 'before',\n onLeave: 'after',\n onDispose: 'after',\n};\n\n/**\n * Walk the room's plugin instances and produce the lifecycle + message\n * layout for the Room class. Called once per Room subclass \u2014 on the\n * first construct \u2014 and the result is cached on the constructor. Throws\n * on duplicate message keys (named both plugin keys) so the failure is\n * visible at class-init rather than at first message dispatch.\n *\n * Returns `null` when no plugins participate in any hook AND none\n * declare a message \u2014 distinguishes \"computed, nothing to do\" from\n * \"not computed yet\" in the `__pluginLayout` cache.\n *\n * @internal\n */\nexport function computePluginLayout(plugins: Record<string, RoomPlugin>): PluginLayout | null {\n // Resolve `static dependencies` closures over the user's record.\n // Returns an expanded list (user + auto-deps), the auto-dep\n // class table for per-room instantiation, and a unified view of\n // entries to iterate when computing hooks / message owners.\n const resolved = resolveDependencies(plugins);\n\n const hooks = {} as Record<PluginLifecycleKey, { before: string[]; after: string[] }>;\n let anyHook = false;\n\n for (const hook of PLUGIN_LIFECYCLE_KEYS) {\n const before: string[] = [];\n const after: string[] = [];\n for (const { key, plugin } of resolved.entries) {\n if (typeof plugin[hook] !== 'function') { continue; }\n // Bracket access \u2014 `order` and `messages` are protected; TS\n // skips visibility checks on indexed access.\n const order = plugin['order']?.[hook] ?? DEFAULT_PLUGIN_ORDER[hook];\n (order === 'before' ? before : after).push(key);\n anyHook = true;\n }\n hooks[hook] = { before, after };\n }\n\n const messageOwners = new Map<string, string>();\n for (const { key, plugin } of resolved.entries) {\n const messages = plugin['messages'];\n if (!messages) { continue; }\n for (const messageKey of Object.keys(messages)) {\n const prior = messageOwners.get(messageKey);\n if (prior !== undefined) {\n throw new Error(\n `[Room] message key \"${messageKey}\" declared by multiple plugins: ` +\n `\"${prior}\" and \"${key}\". Resolve by giving one of them a ` +\n `different key, or override on the room's own \\`messages\\`.`,\n );\n }\n messageOwners.set(messageKey, key);\n }\n }\n\n if (!anyHook && messageOwners.size === 0 && resolved.autoDeps.length === 0) { return null; }\n return { hooks, messageOwners, autoDeps: resolved.autoDeps };\n}\n\n/**\n * Walk every plugin's `static dependencies` recursively, instantiating\n * missing classes (zero-arg only). Throws on cycles. Auto-deps are\n * keyed `__dep:<ClassName>` in the returned entries.\n */\nfunction resolveDependencies(plugins: Record<string, RoomPlugin>): {\n entries: Array<{ key: string; plugin: RoomPlugin }>;\n autoDeps: Array<{ key: string; ctor: RoomPluginClass }>;\n} {\n const entries: Array<{ key: string; plugin: RoomPlugin }> = [];\n for (const [key, plugin] of Object.entries(plugins)) {\n entries.push({ key, plugin });\n }\n\n const present = new Set<RoomPluginClass>();\n for (const { plugin } of entries) {\n present.add(plugin.constructor as RoomPluginClass);\n }\n\n const autoDeps: Array<{ key: string; ctor: RoomPluginClass }> = [];\n const visiting = new Set<RoomPluginClass>();\n const path: string[] = [];\n\n function walk(depCtor: RoomPluginClass): void {\n if (present.has(depCtor)) { return; }\n if (visiting.has(depCtor)) {\n const cycle = [...path, depCtor.name].join(' \u2192 ');\n throw new Error(`[Room] plugin dependency cycle: ${cycle}`);\n }\n visiting.add(depCtor);\n path.push(depCtor.name);\n\n // Recurse into deeper deps first \u2192 topological order in `entries`.\n const deeper = (depCtor as any).dependencies as PluginDependencies | undefined;\n if (Array.isArray(deeper)) {\n for (const inner of deeper) { walk(inner); }\n }\n\n let instance: RoomPlugin;\n try { instance = new depCtor(); }\n catch (err: any) {\n throw new Error(\n `[Room] auto-included plugin \"${depCtor.name}\" must be ` +\n `constructible with no arguments. If it needs options, ` +\n `register it explicitly in definePlugins({...}). ` +\n `(cause: ${err?.message ?? err})`,\n );\n }\n const key = DEP_KEY_PREFIX + depCtor.name;\n entries.push({ key, plugin: instance });\n autoDeps.push({ key, ctor: depCtor });\n present.add(depCtor);\n\n visiting.delete(depCtor);\n path.pop();\n }\n\n // Snapshot before mutating \u2014 transitive deps are handled recursively\n // inside `walk()`.\n const userEntriesSnapshot = entries.slice();\n for (const { plugin } of userEntriesSnapshot) {\n const deps = (plugin.constructor as any).dependencies as PluginDependencies | undefined;\n if (!Array.isArray(deps)) { continue; }\n for (const depCtor of deps) { walk(depCtor); }\n }\n\n return { entries, autoDeps };\n}\n\n/**\n * Install one wrapper per participating hook on the Room subclass's\n * prototype. The wrapper closes over plugin KEYS (resolved to refs at\n * call time) and invokes the captured original room hook between the\n * before/after plugin runs.\n *\n * @internal\n */\nexport function installPluginHookWrappers(\n ctor: { prototype: any },\n layout: PluginLayout | null,\n): void {\n if (layout === null) { return; }\n const proto = ctor.prototype;\n for (const hook of PLUGIN_LIFECYCLE_KEYS) {\n const { before, after } = layout.hooks[hook];\n if (before.length === 0 && after.length === 0) { continue; }\n\n const original = proto[hook] as Function | undefined;\n proto[hook] = async function (\n this: { plugins?: Record<string, RoomPlugin>; _autoPlugins?: Record<string, RoomPlugin> },\n ...args: any[]\n ) {\n // User plugins live on `this.plugins`, auto-deps (sentinel-keyed)\n // on `this._autoPlugins` \u2014 kept separate so user types stay clean.\n const lookup = (k: string): RoomPlugin =>\n (k.startsWith(DEP_KEY_PREFIX) ? this._autoPlugins![k] : this.plugins![k]);\n for (const k of before) {\n const p = lookup(k);\n await (p[hook] as Function).call(p, ...args);\n }\n let result: unknown;\n if (original) { result = await original.apply(this, args); }\n for (const k of after) {\n const p = lookup(k);\n await (p[hook] as Function).call(p, ...args);\n }\n return result;\n };\n }\n}\n\n/** Structural shape used by `setupRoomPlugins` \u2014 avoids a Room import\n * so the dependency graph stays one-way (Room \u2192 RoomPlugin). */\ninterface RoomPluginHost {\n plugins?: Record<string, RoomPlugin<any>>;\n _autoPlugins?: Record<string, RoomPlugin<any>>;\n messages?: Record<string, Function> | any;\n constructor: { __pluginLayout?: PluginLayout | null; prototype: any };\n}\n\n/**\n * Wire a Room instance's plugins. Once-per-class layout (hook\n * participation, message owners, dep resolution) is cached on the\n * constructor; per-instance work attaches `room` refs, instantiates\n * auto-deps, and merges plugin messages.\n *\n * @internal\n */\nexport function setupRoomPlugins(room: RoomPluginHost): void {\n const plugins = room.plugins!;\n const layout = resolveOrComputeLayout(room.constructor, plugins);\n\n attachRoomReference(room, plugins);\n\n if (layout && layout.autoDeps.length > 0) {\n room._autoPlugins = instantiateAutoDeps(room, layout);\n }\n\n if (layout && layout.messageOwners.size > 0) {\n mergePluginMessages(room, layout);\n }\n\n Object.freeze(plugins);\n if (room._autoPlugins) { Object.freeze(room._autoPlugins); }\n}\n\n/**\n * Read cached layout for this subclass, or compute + install on\n * first construct. `hasOwnProperty` so a subclass that redeclares\n * `plugins` doesn't inherit the parent's wrapping.\n */\nfunction resolveOrComputeLayout(\n ctor: RoomPluginHost['constructor'],\n plugins: Record<string, RoomPlugin>,\n): PluginLayout | null | undefined {\n if (Object.prototype.hasOwnProperty.call(ctor, '__pluginLayout')) {\n return ctor.__pluginLayout;\n }\n const layout = computePluginLayout(plugins);\n installPluginHookWrappers(ctor, layout);\n ctor.__pluginLayout = layout;\n return layout;\n}\n\n/** Wire `plugin.room = room` on every user-explicit plugin. */\nfunction attachRoomReference(\n room: RoomPluginHost,\n plugins: Record<string, RoomPlugin>,\n): void {\n for (const plugin of Object.values(plugins)) {\n (plugin as any).room = room;\n }\n}\n\n/**\n * Build `_autoPlugins` \u2014 one fresh instance per `static dependencies`\n * entry. Kept separate from `room.plugins` so sentinel keys\n * (`__dep:<ClassName>`) don't leak into the user's typed view.\n */\nfunction instantiateAutoDeps(\n room: RoomPluginHost,\n layout: PluginLayout,\n): Record<string, RoomPlugin> {\n const auto: Record<string, RoomPlugin> = {};\n for (const { key, ctor: depCtor } of layout.autoDeps) {\n const instance = new depCtor();\n (instance as any).room = room;\n auto[key] = instance;\n }\n return auto;\n}\n\n/**\n * Copy plugin message handlers into `room.messages` (room's own key\n * wins; plugin-vs-plugin conflicts already threw at layout time).\n */\nfunction mergePluginMessages(\n room: RoomPluginHost,\n layout: PluginLayout,\n): void {\n const plugins = room.plugins!;\n for (const [messageKey, pluginKey] of layout.messageOwners) {\n if (room.messages?.[messageKey]) { continue; }\n const source = pluginKey.startsWith(DEP_KEY_PREFIX)\n ? room._autoPlugins![pluginKey]\n : plugins[pluginKey];\n const handler = source['messages']?.[messageKey];\n if (handler !== undefined) {\n (room.messages ??= {} as any)[messageKey] = handler;\n }\n }\n}\n"],
|
|
5
|
-
"mappings": ";;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAoEO,IAAe,aAAf,MAAoD;AAiD3D;AA8EO,SAAS,cAAc,SAAmB;AAC/C,MAAI,CAAC,MAAM,QAAQ,OAAO,GAAG;AAAE,WAAO;AAAA,EAAS;AAC/C,QAAM,MAAkC,CAAC;AACzC,aAAW,KAAK,SAAS;AACvB,UAAM,MAAM,EAAE,YAAY;AAC1B,QAAI,OAAO,QAAQ,YAAY,IAAI,WAAW,GAAG;AAC/C,YAAM,IAAI;AAAA,QACR,iBAAiB,EAAE,YAAY,IAAI;AAAA,MAGrC;AAAA,IACF;AACA,QAAI,IAAI,GAAG,GAAG;AACZ,YAAM,IAAI;AAAA,QACR,6CAA6C,GAAG;AAAA,MAElD;AAAA,IACF;AACA,QAAI,GAAG,IAAI;AAAA,EACb;AACA,SAAO;AACT;AAQO,IAAM,wBAAwB,CAAC,YAAY,UAAU,UAAU,WAAW,WAAW;AAgBrF,SAAS,iBACd,QACA,WAAc,CAAC,GACZ;AACH,EAAC,OAAe,OAAO;AACvB,SAAO;AACT;AA2BA,IAAM,iBAAiB;AAYhB,IAAM,uBAAuE;AAAA,EAClF,UAAW;AAAA,EACX,QAAW;AAAA,EACX,QAAW;AAAA,EACX,SAAW;AAAA,EACX,WAAW;AACb;AAeO,SAAS,oBAAoB,SAA0D;AAK5F,QAAM,WAAW,oBAAoB,OAAO;AAE5C,QAAM,QAAQ,CAAC;AACf,MAAI,UAAU;AAEd,aAAW,QAAQ,uBAAuB;AACxC,UAAM,SAAmB,CAAC;AAC1B,UAAM,QAAkB,CAAC;AACzB,eAAW,EAAE,KAAK,OAAO,KAAK,SAAS,SAAS;AAC9C,UAAI,OAAO,OAAO,IAAI,MAAM,YAAY;AAAE;AAAA,MAAU;AAGpD,YAAM,QAAQ,OAAO,OAAO,IAAI,IAAI,KAAK,qBAAqB,IAAI;AAClE,OAAC,UAAU,WAAW,SAAS,OAAO,KAAK,GAAG;AAC9C,gBAAU;AAAA,IACZ;AACA,UAAM,IAAI,IAAI,EAAE,QAAQ,MAAM;AAAA,EAChC;AAEA,QAAM,gBAAgB,oBAAI,IAAoB;AAC9C,aAAW,EAAE,KAAK,OAAO,KAAK,SAAS,SAAS;AAC9C,UAAM,WAAW,OAAO,UAAU;AAClC,QAAI,CAAC,UAAU;AAAE;AAAA,IAAU;AAC3B,eAAW,cAAc,OAAO,KAAK,QAAQ,GAAG;AAC9C,YAAM,QAAQ,cAAc,IAAI,UAAU;AAC1C,UAAI,UAAU,QAAW;AACvB,cAAM,IAAI;AAAA,UACR,uBAAuB,UAAU,oCAC7B,KAAK,UAAU,GAAG;AAAA,QAExB;AAAA,MACF;AACA,oBAAc,IAAI,YAAY,GAAG;AAAA,IACnC;AAAA,EACF;AAEA,MAAI,CAAC,WAAW,cAAc,SAAS,KAAK,SAAS,SAAS,WAAW,GAAG;AAAE,WAAO;AAAA,EAAM;AAC3F,SAAO,EAAE,OAAO,eAAe,UAAU,SAAS,SAAS;AAC7D;AAOA,SAAS,oBAAoB,SAG3B;AACA,QAAM,UAAsD,CAAC;AAC7D,aAAW,CAAC,KAAK,MAAM,KAAK,OAAO,QAAQ,OAAO,GAAG;AACnD,YAAQ,KAAK,EAAE,KAAK,OAAO,CAAC;AAAA,EAC9B;AAEA,QAAM,UAAU,oBAAI,IAAqB;AACzC,aAAW,EAAE,OAAO,KAAK,SAAS;AAChC,YAAQ,IAAI,OAAO,WAA8B;AAAA,EACnD;AAEA,QAAM,WAA0D,CAAC;AACjE,QAAM,WAAW,oBAAI,IAAqB;AAC1C,QAAM,OAAiB,CAAC;AAExB,WAAS,KAAK,SAAgC;AAC5C,QAAI,QAAQ,IAAI,OAAO,GAAG;AAAE;AAAA,IAAQ;AACpC,QAAI,SAAS,IAAI,OAAO,GAAG;AACzB,YAAM,QAAQ,CAAC,GAAG,MAAM,QAAQ,IAAI,EAAE,KAAK,UAAK;AAChD,YAAM,IAAI,MAAM,mCAAmC,KAAK,EAAE;AAAA,IAC5D;AACA,aAAS,IAAI,OAAO;AACpB,SAAK,KAAK,QAAQ,IAAI;AAGtB,UAAM,SAAU,QAAgB;AAChC,QAAI,MAAM,QAAQ,MAAM,GAAG;AACzB,iBAAW,SAAS,QAAQ;AAAE,aAAK,KAAK;AAAA,MAAG;AAAA,IAC7C;AAEA,QAAI;AACJ,QAAI;AAAE,iBAAW,IAAI,QAAQ;AAAA,IAAG,SACzB,KAAU;AACf,YAAM,IAAI;AAAA,QACR,gCAAgC,QAAQ,IAAI,2HAGjC,KAAK,WAAW,GAAG;AAAA,MAChC;AAAA,IACF;AACA,UAAM,MAAM,iBAAiB,QAAQ;AACrC,YAAQ,KAAK,EAAE,KAAK,QAAQ,SAAS,CAAC;AACtC,aAAS,KAAK,EAAE,KAAK,MAAM,QAAQ,CAAC;AACpC,YAAQ,IAAI,OAAO;AAEnB,aAAS,OAAO,OAAO;AACvB,SAAK,IAAI;AAAA,EACX;AAIA,QAAM,sBAAsB,QAAQ,MAAM;AAC1C,aAAW,EAAE,OAAO,KAAK,qBAAqB;AAC5C,UAAM,OAAQ,OAAO,YAAoB;AACzC,QAAI,CAAC,MAAM,QAAQ,IAAI,GAAG;AAAE;AAAA,IAAU;AACtC,eAAW,WAAW,MAAM;AAAE,WAAK,OAAO;AAAA,IAAG;AAAA,EAC/C;AAEA,SAAO,EAAE,SAAS,SAAS;AAC7B;AAUO,SAAS,0BACd,MACA,QACM;AACN,MAAI,WAAW,MAAM;AAAE;AAAA,EAAQ;AAC/B,QAAM,QAAQ,KAAK;AACnB,aAAW,QAAQ,uBAAuB;AACxC,UAAM,EAAE,QAAQ,MAAM,IAAI,OAAO,MAAM,IAAI;AAC3C,QAAI,OAAO,WAAW,KAAK,MAAM,WAAW,GAAG;AAAE;AAAA,IAAU;
|
|
4
|
+
"sourcesContent": ["/**\n * Room plugins.\n *\n * A plugin is a composable extension to a Room \u2014 it can declare message\n * handlers, attach lifecycle hooks, and expose public methods that the\n * room can call (`this.plugins.<key>.someMethod()`). The framework\n * instantiates one plugin per room, sets `this.room` after the room is\n * constructed, then merges the plugin's `messages` with the room's own\n * and wires its lifecycle hooks alongside the room's.\n *\n * Example: a tiny chat plugin contributing a single message handler:\n *\n * class ChatPlugin extends RoomPlugin {\n * private history: string[] = [];\n * constructor(private opts: { historyLimit: number }) { super(); }\n *\n * messages = {\n * chat: (client: Client, msg: { text: string }) => {\n * this.history.push(msg.text);\n * if (this.history.length > this.opts.historyLimit) this.history.shift();\n * this.room.broadcast('chat', { from: client.userId, text: msg.text });\n * },\n * };\n *\n * // Public \u2014 callable as `this.plugins.chat.getHistory()` from the room\n * getHistory(): readonly string[] { return this.history; }\n * }\n *\n * Use via `definePlugins`:\n *\n * class MyRoom extends Room<{ state: MyState }> {\n * plugins = definePlugins({\n * chat: new ChatPlugin({ historyLimit: 100 }),\n * });\n * }\n */\nimport type { Room } from './Room.ts';\nimport type { AuthContext, Client } from './Transport.ts';\nimport type { Messages } from '@colyseus/shared-types';\n\n/**\n * Ordering hint for a plugin's lifecycle hook relative to the room's own\n * hook. Sensible per-hook defaults are applied when omitted (see\n * `Room.__init` for the exact ordering policy).\n *\n * onCreate / onAuth / onJoin \u2192 plugins run BEFORE room (guards + setup)\n * onLeave / onDispose \u2192 plugins run AFTER room (capture final state)\n */\nexport interface RoomPluginOrder {\n onCreate?: 'before' | 'after';\n onAuth?: 'before' | 'after';\n onJoin?: 'before' | 'after';\n onLeave?: 'before' | 'after';\n onDispose?: 'before' | 'after';\n}\n\n/**\n * Base class for room plugins. Subclass to define a plugin; the framework\n * sets `this.room` after the host room is fully constructed.\n *\n * Don't access `this.room` from the plugin's constructor \u2014 it hasn't been\n * wired yet. Everything room-dependent goes in `onCreate` / `onJoin` /\n * etc. or in public methods that are called from the room post-init.\n *\n * @typeParam This - The Room subclass this plugin is attached to. Narrow\n * for schema-driven plugins that need a specific state shape, e.g.\n * `class PhysicsPlugin extends RoomPlugin<Room<{ state: PhysicsContract }>>`.\n */\nexport abstract class RoomPlugin<This extends Room = Room> {\n /**\n * Live room reference. Wired by the framework at __init, AFTER the\n * room's own construction \u2014 accessing it from the plugin's\n * constructor throws.\n */\n protected readonly room!: This;\n\n /**\n * Canonical key for the plugin when registered via `definePlugins([...])`.\n * Declared on the subclass with `as const` so the literal type flows\n * into `room.plugins.<key>`:\n *\n * class ChatPlugin extends RoomPlugin {\n * readonly pluginName = 'chat' as const;\n * }\n *\n * Optional under the keyed-record form (`definePlugins({ chat: ... })`),\n * required under the array form. Must stay `public` so `extends` /\n * `keyof` can see the literal \u2014 those are blind to protected from\n * outside the class. End-user autocomplete hides it via `Omit` in\n * `definePlugins`'s return type.\n *\n * For multi-instance use, accept the name at construction:\n *\n * readonly pluginName: string;\n * constructor(name = 'chat') { super(); this.pluginName = name; }\n */\n readonly pluginName?: string;\n\n /**\n * Declarative message handlers \u2014 merged into the room's `messages` at\n * __init. Conflict against the room's own key: room wins. Conflict\n * between two plugins: throws at __init.\n */\n protected messages?: Messages<This>;\n\n /** Optional per-hook ordering vs the room's own hook. */\n protected order?: RoomPluginOrder;\n\n // Lifecycle hooks \u2014 override any subset. `protected` so they don't\n // leak into `this.plugins.<key>.onJoin(...)` autocomplete; subclass\n // overrides must keep `protected` (TS widens to public silently\n // otherwise).\n protected onCreate?(options: any): void | Promise<void>;\n protected onAuth?(client: Client, options: any, context: AuthContext): void | Promise<void>;\n protected onJoin?(client: Client, options?: any): void | Promise<void>;\n protected onLeave?(client: Client, code?: number): void | Promise<void>;\n protected onDispose?(): void | Promise<void>;\n}\n\n/**\n * Plugin-class constructor type used by the `dependencies` static\n * declaration. Constrained to zero-arg constructors because the\n * framework auto-instantiates missing dependencies with no\n * configuration \u2014 plugins that need options can't be auto-included\n * and must be registered explicitly in `definePlugins({...})`.\n */\nexport type RoomPluginClass = new () => RoomPlugin<any>;\n\n/**\n * Static `dependencies` declaration. List other plugin classes this\n * plugin needs alongside it; the framework auto-instantiates any\n * missing ones at room construction time. Transitive deps are\n * resolved recursively. Cycles throw at class-init.\n *\n * Example:\n * class UniqueSessionPlugin extends RoomPlugin {\n * static dependencies: PluginDependencies = [TrackUserSessionsPlugin];\n * }\n */\nexport type PluginDependencies = ReadonlyArray<RoomPluginClass>;\n\n/**\n * Define a Room's plugin record. The framework wires plugins at\n * `__init` \u2014 first construct of the class computes the layout\n * (cached on the constructor) and installs hook wrappers on the\n * prototype. The `const T` modifier preserves literal types so\n * `this.plugins.<key>.method()` autocompletes against each plugin's\n * specific subclass.\n */\n// Pulls each plugin's `pluginName` literal as the record key. Subclass\n// must narrow with `as const` \u2014 plain `string` resolves to `never`\n// and the entry is dropped (callers see a TS error on\n// `plugins.<thatKey>`).\ntype ExtractPluginName<P> = P extends { pluginName: infer K }\n ? (K extends string ? K : never)\n : never;\n\n/** Hide `pluginName` from end-user autocomplete on `this.plugins.<key>`. */\ntype PluginPublicSurface<P> = Omit<P, 'pluginName'>;\n\ntype PluginsArrayToRecord<T extends readonly RoomPlugin<any>[]> = {\n [P in T[number] as ExtractPluginName<P>]: PluginPublicSurface<P>;\n};\n\n/**\n * Array form (recommended). Each plugin declares its own canonical\n * key via `readonly pluginName = '...' as const`; the framework\n * turns the array into a typed record so `plugins.<pluginName>`\n * autocompletes the right instance type.\n *\n * class GameRoom extends Room {\n * plugins = definePlugins([\n * new ChatPlugin(), // pluginName: 'chat'\n * new UniqueSessionPlugin({ max: 1 }), // pluginName: 'uniqueSession'\n * ]);\n * }\n * this.plugins.chat.send('hi');\n *\n * Throws at runtime if any plugin is missing `pluginName` or if two\n * plugins resolve to the same key.\n */\nexport function definePlugins<const T extends readonly RoomPlugin<any>[]>(\n plugins: T,\n): PluginsArrayToRecord<T>;\n\n/**\n * Record form (legacy / multi-instance escape hatch). The caller\n * chooses the key per-room. Useful when registering two instances of\n * the same plugin class without configuring `pluginName` per\n * instance \u2014 e.g. `{ adminChat: new ChatPlugin(), playerChat: new ChatPlugin() }`.\n */\nexport function definePlugins<const T extends Record<string, RoomPlugin<any>>>(\n plugins: T,\n): { [K in keyof T]: PluginPublicSurface<T[K]> };\n\nexport function definePlugins(plugins: any): any {\n if (!Array.isArray(plugins)) { return plugins; }\n const out: Record<string, RoomPlugin> = {};\n for (const p of plugins) {\n const key = p['pluginName'];\n if (typeof key !== 'string' || key.length === 0) {\n throw new Error(\n `[Room] plugin ${p.constructor.name} is missing a 'pluginName' field. ` +\n `Declare \\`readonly pluginName = '<key>' as const\\` on the class, ` +\n `or register it via the keyed-record form \\`definePlugins({ <key>: <plugin> })\\`.`,\n );\n }\n if (out[key]) {\n throw new Error(\n `[Room] two plugins resolve to pluginName \"${key}\". Configure one of ` +\n `them via constructor argument, or use the keyed-record form to disambiguate.`,\n );\n }\n out[key] = p;\n }\n return out;\n}\n\n/**\n * Lifecycle hook keys recognized by the framework \u2014 used internally to\n * separate \"framework-recognized methods\" from \"user-defined public\n * methods\" when wiring a plugin into a room. Exported for the test\n * harness; downstream code should not need it.\n */\nexport const PLUGIN_LIFECYCLE_KEYS = ['onCreate', 'onAuth', 'onJoin', 'onLeave', 'onDispose'] as const;\nexport type PluginLifecycleKey = (typeof PLUGIN_LIFECYCLE_KEYS)[number];\n\n/**\n * Test helper \u2014 attach a stub or fake room to a plugin so its methods\n * and lifecycle hooks can be exercised in isolation without spinning up\n * a real Colyseus server.\n *\n * const plugin = new ChatPlugin({ historyLimit: 5 });\n * const room = attachToTestRoom(plugin, { broadcast: sinon.spy() });\n * await plugin.messages!.chat!.call(plugin, { userId: 'u1' }, { text: 'hi' });\n *\n * The second arg is shallow-merged onto the stub so tests only declare\n * the room properties they actually exercise. Returns the room stub for\n * post-call assertions.\n */\nexport function attachToTestRoom<This extends Room, R extends Partial<This>>(\n plugin: RoomPlugin<This>,\n roomStub: R = {} as R,\n): R {\n (plugin as any).room = roomStub;\n return roomStub;\n}\n\n// ---------------------------------------------------------------------------\n// Layout machinery \u2014 used by Room.__init to set plugins up once per class.\n// Lives in this file (rather than Room.ts) so the plugin-related types and\n// helpers stay co-located. These are framework internals; callers outside\n// `@colyseus/core` should not depend on them.\n// ---------------------------------------------------------------------------\n\n/**\n * Precomputed plugin layout for a Room subclass \u2014 populated on first\n * construct, cached on the constructor. Hook wrappers are installed\n * on the prototype in the same pass (see `installPluginHookWrappers`).\n *\n * @internal\n */\nexport interface PluginLayout {\n /** Per-hook participation: which plugin keys run before/after the room's own hook. */\n hooks: Record<PluginLifecycleKey, { before: string[]; after: string[] }>;\n /** Message key \u2192 plugin key. Conflict detection ran when this was built. */\n messageOwners: Map<string, string>;\n /** Sentinel-keyed plugin classes to instantiate per room. */\n autoDeps: Array<{ key: string; ctor: RoomPluginClass }>;\n}\n\n/** Sentinel prefix for framework-instantiated deps. The colon prevents\n * collisions with any JS identifier the user could use as a key. */\nconst DEP_KEY_PREFIX = '__dep:';\n\n/**\n * Default before/after policy for lifecycle hooks vs the room's own\n * hook. Plugins can override per-hook via the `order` field on the\n * plugin instance.\n *\n * onCreate / onAuth / onJoin \u2192 plugins run BEFORE room (guards + setup)\n * onLeave / onDispose \u2192 plugins run AFTER room (capture final state)\n *\n * @internal\n */\nexport const DEFAULT_PLUGIN_ORDER: Record<PluginLifecycleKey, 'before' | 'after'> = {\n onCreate: 'before',\n onAuth: 'before',\n onJoin: 'before',\n onLeave: 'after',\n onDispose: 'after',\n};\n\n/**\n * Walk the room's plugin instances and produce the lifecycle + message\n * layout for the Room class. Called once per Room subclass \u2014 on the\n * first construct \u2014 and the result is cached on the constructor. Throws\n * on duplicate message keys (named both plugin keys) so the failure is\n * visible at class-init rather than at first message dispatch.\n *\n * Returns `null` when no plugins participate in any hook AND none\n * declare a message \u2014 distinguishes \"computed, nothing to do\" from\n * \"not computed yet\" in the `__pluginLayout` cache.\n *\n * @internal\n */\nexport function computePluginLayout(plugins: Record<string, RoomPlugin>): PluginLayout | null {\n // Resolve `static dependencies` closures over the user's record.\n // Returns an expanded list (user + auto-deps), the auto-dep\n // class table for per-room instantiation, and a unified view of\n // entries to iterate when computing hooks / message owners.\n const resolved = resolveDependencies(plugins);\n\n const hooks = {} as Record<PluginLifecycleKey, { before: string[]; after: string[] }>;\n let anyHook = false;\n\n for (const hook of PLUGIN_LIFECYCLE_KEYS) {\n const before: string[] = [];\n const after: string[] = [];\n for (const { key, plugin } of resolved.entries) {\n if (typeof plugin[hook] !== 'function') { continue; }\n // Bracket access \u2014 `order` and `messages` are protected; TS\n // skips visibility checks on indexed access.\n const order = plugin['order']?.[hook] ?? DEFAULT_PLUGIN_ORDER[hook];\n (order === 'before' ? before : after).push(key);\n anyHook = true;\n }\n hooks[hook] = { before, after };\n }\n\n const messageOwners = new Map<string, string>();\n for (const { key, plugin } of resolved.entries) {\n const messages = plugin['messages'];\n if (!messages) { continue; }\n for (const messageKey of Object.keys(messages)) {\n const prior = messageOwners.get(messageKey);\n if (prior !== undefined) {\n throw new Error(\n `[Room] message key \"${messageKey}\" declared by multiple plugins: ` +\n `\"${prior}\" and \"${key}\". Resolve by giving one of them a ` +\n `different key, or override on the room's own \\`messages\\`.`,\n );\n }\n messageOwners.set(messageKey, key);\n }\n }\n\n if (!anyHook && messageOwners.size === 0 && resolved.autoDeps.length === 0) { return null; }\n return { hooks, messageOwners, autoDeps: resolved.autoDeps };\n}\n\n/**\n * Walk every plugin's `static dependencies` recursively, instantiating\n * missing classes (zero-arg only). Throws on cycles. Auto-deps are\n * keyed `__dep:<ClassName>` in the returned entries.\n */\nfunction resolveDependencies(plugins: Record<string, RoomPlugin>): {\n entries: Array<{ key: string; plugin: RoomPlugin }>;\n autoDeps: Array<{ key: string; ctor: RoomPluginClass }>;\n} {\n const entries: Array<{ key: string; plugin: RoomPlugin }> = [];\n for (const [key, plugin] of Object.entries(plugins)) {\n entries.push({ key, plugin });\n }\n\n const present = new Set<RoomPluginClass>();\n for (const { plugin } of entries) {\n present.add(plugin.constructor as RoomPluginClass);\n }\n\n const autoDeps: Array<{ key: string; ctor: RoomPluginClass }> = [];\n const visiting = new Set<RoomPluginClass>();\n const path: string[] = [];\n\n function walk(depCtor: RoomPluginClass): void {\n if (present.has(depCtor)) { return; }\n if (visiting.has(depCtor)) {\n const cycle = [...path, depCtor.name].join(' \u2192 ');\n throw new Error(`[Room] plugin dependency cycle: ${cycle}`);\n }\n visiting.add(depCtor);\n path.push(depCtor.name);\n\n // Recurse into deeper deps first \u2192 topological order in `entries`.\n const deeper = (depCtor as any).dependencies as PluginDependencies | undefined;\n if (Array.isArray(deeper)) {\n for (const inner of deeper) { walk(inner); }\n }\n\n let instance: RoomPlugin;\n try { instance = new depCtor(); }\n catch (err: any) {\n throw new Error(\n `[Room] auto-included plugin \"${depCtor.name}\" must be ` +\n `constructible with no arguments. If it needs options, ` +\n `register it explicitly in definePlugins({...}). ` +\n `(cause: ${err?.message ?? err})`,\n );\n }\n const key = DEP_KEY_PREFIX + depCtor.name;\n entries.push({ key, plugin: instance });\n autoDeps.push({ key, ctor: depCtor });\n present.add(depCtor);\n\n visiting.delete(depCtor);\n path.pop();\n }\n\n // Snapshot before mutating \u2014 transitive deps are handled recursively\n // inside `walk()`.\n const userEntriesSnapshot = entries.slice();\n for (const { plugin } of userEntriesSnapshot) {\n const deps = (plugin.constructor as any).dependencies as PluginDependencies | undefined;\n if (!Array.isArray(deps)) { continue; }\n for (const depCtor of deps) { walk(depCtor); }\n }\n\n return { entries, autoDeps };\n}\n\n/**\n * Install one wrapper per participating hook on the Room subclass's\n * prototype. The wrapper closes over plugin KEYS (resolved to refs at\n * call time) and invokes the captured original room hook between the\n * before/after plugin runs.\n *\n * @internal\n */\nexport function installPluginHookWrappers(\n ctor: { prototype: any },\n layout: PluginLayout | null,\n): void {\n if (layout === null) { return; }\n const proto = ctor.prototype;\n for (const hook of PLUGIN_LIFECYCLE_KEYS) {\n const { before, after } = layout.hooks[hook];\n if (before.length === 0 && after.length === 0) { continue; }\n proto[hook] = composePluginHook(hook, before, after, proto[hook]);\n }\n}\n\n/**\n * Hooks declared as class fields (`onJoin = () => {}`) are own properties\n * that shadow the prototype wrapper, so wrap them per instance.\n */\nfunction wrapOwnHooks(room: RoomPluginHost, layout: PluginLayout): void {\n for (const hook of PLUGIN_LIFECYCLE_KEYS) {\n const { before, after } = layout.hooks[hook];\n if (before.length === 0 && after.length === 0) { continue; }\n if (!Object.hasOwn(room, hook)) { continue; }\n (room as any)[hook] = composePluginHook(hook, before, after, (room as any)[hook]);\n }\n}\n\n/** Run the `before` plugins, then `original`, then the `after` plugins. */\nfunction composePluginHook(\n hook: PluginLifecycleKey,\n before: string[],\n after: string[],\n original: Function | undefined,\n) {\n return async function (\n this: { plugins?: Record<string, RoomPlugin>; _autoPlugins?: Record<string, RoomPlugin> },\n ...args: any[]\n ) {\n // User plugins live on `this.plugins`, auto-deps (sentinel-keyed)\n // on `this._autoPlugins` \u2014 kept separate so user types stay clean.\n const lookup = (k: string): RoomPlugin =>\n (k.startsWith(DEP_KEY_PREFIX) ? this._autoPlugins![k] : this.plugins![k]);\n for (const k of before) {\n const p = lookup(k);\n await (p[hook] as Function).call(p, ...args);\n }\n let result: unknown;\n if (original) { result = await original.apply(this, args); }\n for (const k of after) {\n const p = lookup(k);\n await (p[hook] as Function).call(p, ...args);\n }\n return result;\n };\n}\n\n/** Structural shape used by `setupRoomPlugins` \u2014 avoids a Room import\n * so the dependency graph stays one-way (Room \u2192 RoomPlugin). */\ninterface RoomPluginHost {\n plugins?: Record<string, RoomPlugin<any>>;\n _autoPlugins?: Record<string, RoomPlugin<any>>;\n messages?: Record<string, Function> | any;\n constructor: { __pluginLayout?: PluginLayout | null; prototype: any };\n}\n\n/**\n * Wire a Room instance's plugins. Once-per-class layout (hook\n * participation, message owners, dep resolution) is cached on the\n * constructor; per-instance work attaches `room` refs, instantiates\n * auto-deps, and merges plugin messages.\n *\n * @internal\n */\nexport function setupRoomPlugins(room: RoomPluginHost): void {\n const plugins = room.plugins!;\n const layout = resolveOrComputeLayout(room.constructor, plugins);\n\n attachRoomReference(room, plugins);\n\n if (layout) {\n wrapOwnHooks(room, layout);\n }\n\n if (layout && layout.autoDeps.length > 0) {\n room._autoPlugins = instantiateAutoDeps(room, layout);\n }\n\n if (layout && layout.messageOwners.size > 0) {\n mergePluginMessages(room, layout);\n }\n\n Object.freeze(plugins);\n if (room._autoPlugins) { Object.freeze(room._autoPlugins); }\n}\n\n/**\n * Read cached layout for this subclass, or compute + install on\n * first construct. `hasOwnProperty` so a subclass that redeclares\n * `plugins` doesn't inherit the parent's wrapping.\n */\nfunction resolveOrComputeLayout(\n ctor: RoomPluginHost['constructor'],\n plugins: Record<string, RoomPlugin>,\n): PluginLayout | null | undefined {\n if (Object.prototype.hasOwnProperty.call(ctor, '__pluginLayout')) {\n return ctor.__pluginLayout;\n }\n const layout = computePluginLayout(plugins);\n installPluginHookWrappers(ctor, layout);\n ctor.__pluginLayout = layout;\n return layout;\n}\n\n/** Wire `plugin.room = room` on every user-explicit plugin. */\nfunction attachRoomReference(\n room: RoomPluginHost,\n plugins: Record<string, RoomPlugin>,\n): void {\n for (const plugin of Object.values(plugins)) {\n (plugin as any).room = room;\n }\n}\n\n/**\n * Build `_autoPlugins` \u2014 one fresh instance per `static dependencies`\n * entry. Kept separate from `room.plugins` so sentinel keys\n * (`__dep:<ClassName>`) don't leak into the user's typed view.\n */\nfunction instantiateAutoDeps(\n room: RoomPluginHost,\n layout: PluginLayout,\n): Record<string, RoomPlugin> {\n const auto: Record<string, RoomPlugin> = {};\n for (const { key, ctor: depCtor } of layout.autoDeps) {\n const instance = new depCtor();\n (instance as any).room = room;\n auto[key] = instance;\n }\n return auto;\n}\n\n/**\n * Copy plugin message handlers into `room.messages` (room's own key\n * wins; plugin-vs-plugin conflicts already threw at layout time).\n */\nfunction mergePluginMessages(\n room: RoomPluginHost,\n layout: PluginLayout,\n): void {\n const plugins = room.plugins!;\n for (const [messageKey, pluginKey] of layout.messageOwners) {\n if (room.messages?.[messageKey]) { continue; }\n const source = pluginKey.startsWith(DEP_KEY_PREFIX)\n ? room._autoPlugins![pluginKey]\n : plugins[pluginKey];\n const handler = source['messages']?.[messageKey];\n if (handler !== undefined) {\n (room.messages ??= {} as any)[messageKey] = handler;\n }\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/build/RoomPlugin.mjs
CHANGED
|
@@ -138,29 +138,46 @@ function installPluginHookWrappers(ctor, layout) {
|
|
|
138
138
|
if (before.length === 0 && after.length === 0) {
|
|
139
139
|
continue;
|
|
140
140
|
}
|
|
141
|
-
|
|
142
|
-
proto[hook] = async function(...args) {
|
|
143
|
-
const lookup = (k) => k.startsWith(DEP_KEY_PREFIX) ? this._autoPlugins[k] : this.plugins[k];
|
|
144
|
-
for (const k of before) {
|
|
145
|
-
const p = lookup(k);
|
|
146
|
-
await p[hook].call(p, ...args);
|
|
147
|
-
}
|
|
148
|
-
let result;
|
|
149
|
-
if (original) {
|
|
150
|
-
result = await original.apply(this, args);
|
|
151
|
-
}
|
|
152
|
-
for (const k of after) {
|
|
153
|
-
const p = lookup(k);
|
|
154
|
-
await p[hook].call(p, ...args);
|
|
155
|
-
}
|
|
156
|
-
return result;
|
|
157
|
-
};
|
|
141
|
+
proto[hook] = composePluginHook(hook, before, after, proto[hook]);
|
|
158
142
|
}
|
|
159
143
|
}
|
|
144
|
+
function wrapOwnHooks(room, layout) {
|
|
145
|
+
for (const hook of PLUGIN_LIFECYCLE_KEYS) {
|
|
146
|
+
const { before, after } = layout.hooks[hook];
|
|
147
|
+
if (before.length === 0 && after.length === 0) {
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
if (!Object.hasOwn(room, hook)) {
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
room[hook] = composePluginHook(hook, before, after, room[hook]);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
function composePluginHook(hook, before, after, original) {
|
|
157
|
+
return async function(...args) {
|
|
158
|
+
const lookup = (k) => k.startsWith(DEP_KEY_PREFIX) ? this._autoPlugins[k] : this.plugins[k];
|
|
159
|
+
for (const k of before) {
|
|
160
|
+
const p = lookup(k);
|
|
161
|
+
await p[hook].call(p, ...args);
|
|
162
|
+
}
|
|
163
|
+
let result;
|
|
164
|
+
if (original) {
|
|
165
|
+
result = await original.apply(this, args);
|
|
166
|
+
}
|
|
167
|
+
for (const k of after) {
|
|
168
|
+
const p = lookup(k);
|
|
169
|
+
await p[hook].call(p, ...args);
|
|
170
|
+
}
|
|
171
|
+
return result;
|
|
172
|
+
};
|
|
173
|
+
}
|
|
160
174
|
function setupRoomPlugins(room) {
|
|
161
175
|
const plugins = room.plugins;
|
|
162
176
|
const layout = resolveOrComputeLayout(room.constructor, plugins);
|
|
163
177
|
attachRoomReference(room, plugins);
|
|
178
|
+
if (layout) {
|
|
179
|
+
wrapOwnHooks(room, layout);
|
|
180
|
+
}
|
|
164
181
|
if (layout && layout.autoDeps.length > 0) {
|
|
165
182
|
room._autoPlugins = instantiateAutoDeps(room, layout);
|
|
166
183
|
}
|
package/build/RoomPlugin.mjs.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/RoomPlugin.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * Room plugins.\n *\n * A plugin is a composable extension to a Room \u2014 it can declare message\n * handlers, attach lifecycle hooks, and expose public methods that the\n * room can call (`this.plugins.<key>.someMethod()`). The framework\n * instantiates one plugin per room, sets `this.room` after the room is\n * constructed, then merges the plugin's `messages` with the room's own\n * and wires its lifecycle hooks alongside the room's.\n *\n * Example: a tiny chat plugin contributing a single message handler:\n *\n * class ChatPlugin extends RoomPlugin {\n * private history: string[] = [];\n * constructor(private opts: { historyLimit: number }) { super(); }\n *\n * messages = {\n * chat: (client: Client, msg: { text: string }) => {\n * this.history.push(msg.text);\n * if (this.history.length > this.opts.historyLimit) this.history.shift();\n * this.room.broadcast('chat', { from: client.userId, text: msg.text });\n * },\n * };\n *\n * // Public \u2014 callable as `this.plugins.chat.getHistory()` from the room\n * getHistory(): readonly string[] { return this.history; }\n * }\n *\n * Use via `definePlugins`:\n *\n * class MyRoom extends Room<{ state: MyState }> {\n * plugins = definePlugins({\n * chat: new ChatPlugin({ historyLimit: 100 }),\n * });\n * }\n */\nimport type { Room } from './Room.ts';\nimport type { AuthContext, Client } from './Transport.ts';\nimport type { Messages } from '@colyseus/shared-types';\n\n/**\n * Ordering hint for a plugin's lifecycle hook relative to the room's own\n * hook. Sensible per-hook defaults are applied when omitted (see\n * `Room.__init` for the exact ordering policy).\n *\n * onCreate / onAuth / onJoin \u2192 plugins run BEFORE room (guards + setup)\n * onLeave / onDispose \u2192 plugins run AFTER room (capture final state)\n */\nexport interface RoomPluginOrder {\n onCreate?: 'before' | 'after';\n onAuth?: 'before' | 'after';\n onJoin?: 'before' | 'after';\n onLeave?: 'before' | 'after';\n onDispose?: 'before' | 'after';\n}\n\n/**\n * Base class for room plugins. Subclass to define a plugin; the framework\n * sets `this.room` after the host room is fully constructed.\n *\n * Don't access `this.room` from the plugin's constructor \u2014 it hasn't been\n * wired yet. Everything room-dependent goes in `onCreate` / `onJoin` /\n * etc. or in public methods that are called from the room post-init.\n *\n * @typeParam This - The Room subclass this plugin is attached to. Narrow\n * for schema-driven plugins that need a specific state shape, e.g.\n * `class PhysicsPlugin extends RoomPlugin<Room<{ state: PhysicsContract }>>`.\n */\nexport abstract class RoomPlugin<This extends Room = Room> {\n /**\n * Live room reference. Wired by the framework at __init, AFTER the\n * room's own construction \u2014 accessing it from the plugin's\n * constructor throws.\n */\n protected readonly room!: This;\n\n /**\n * Canonical key for the plugin when registered via `definePlugins([...])`.\n * Declared on the subclass with `as const` so the literal type flows\n * into `room.plugins.<key>`:\n *\n * class ChatPlugin extends RoomPlugin {\n * readonly pluginName = 'chat' as const;\n * }\n *\n * Optional under the keyed-record form (`definePlugins({ chat: ... })`),\n * required under the array form. Must stay `public` so `extends` /\n * `keyof` can see the literal \u2014 those are blind to protected from\n * outside the class. End-user autocomplete hides it via `Omit` in\n * `definePlugins`'s return type.\n *\n * For multi-instance use, accept the name at construction:\n *\n * readonly pluginName: string;\n * constructor(name = 'chat') { super(); this.pluginName = name; }\n */\n readonly pluginName?: string;\n\n /**\n * Declarative message handlers \u2014 merged into the room's `messages` at\n * __init. Conflict against the room's own key: room wins. Conflict\n * between two plugins: throws at __init.\n */\n protected messages?: Messages<This>;\n\n /** Optional per-hook ordering vs the room's own hook. */\n protected order?: RoomPluginOrder;\n\n // Lifecycle hooks \u2014 override any subset. `protected` so they don't\n // leak into `this.plugins.<key>.onJoin(...)` autocomplete; subclass\n // overrides must keep `protected` (TS widens to public silently\n // otherwise).\n protected onCreate?(options: any): void | Promise<void>;\n protected onAuth?(client: Client, options: any, context: AuthContext): void | Promise<void>;\n protected onJoin?(client: Client, options?: any): void | Promise<void>;\n protected onLeave?(client: Client, code?: number): void | Promise<void>;\n protected onDispose?(): void | Promise<void>;\n}\n\n/**\n * Plugin-class constructor type used by the `dependencies` static\n * declaration. Constrained to zero-arg constructors because the\n * framework auto-instantiates missing dependencies with no\n * configuration \u2014 plugins that need options can't be auto-included\n * and must be registered explicitly in `definePlugins({...})`.\n */\nexport type RoomPluginClass = new () => RoomPlugin<any>;\n\n/**\n * Static `dependencies` declaration. List other plugin classes this\n * plugin needs alongside it; the framework auto-instantiates any\n * missing ones at room construction time. Transitive deps are\n * resolved recursively. Cycles throw at class-init.\n *\n * Example:\n * class UniqueSessionPlugin extends RoomPlugin {\n * static dependencies: PluginDependencies = [TrackUserSessionsPlugin];\n * }\n */\nexport type PluginDependencies = ReadonlyArray<RoomPluginClass>;\n\n/**\n * Define a Room's plugin record. The framework wires plugins at\n * `__init` \u2014 first construct of the class computes the layout\n * (cached on the constructor) and installs hook wrappers on the\n * prototype. The `const T` modifier preserves literal types so\n * `this.plugins.<key>.method()` autocompletes against each plugin's\n * specific subclass.\n */\n// Pulls each plugin's `pluginName` literal as the record key. Subclass\n// must narrow with `as const` \u2014 plain `string` resolves to `never`\n// and the entry is dropped (callers see a TS error on\n// `plugins.<thatKey>`).\ntype ExtractPluginName<P> = P extends { pluginName: infer K }\n ? (K extends string ? K : never)\n : never;\n\n/** Hide `pluginName` from end-user autocomplete on `this.plugins.<key>`. */\ntype PluginPublicSurface<P> = Omit<P, 'pluginName'>;\n\ntype PluginsArrayToRecord<T extends readonly RoomPlugin<any>[]> = {\n [P in T[number] as ExtractPluginName<P>]: PluginPublicSurface<P>;\n};\n\n/**\n * Array form (recommended). Each plugin declares its own canonical\n * key via `readonly pluginName = '...' as const`; the framework\n * turns the array into a typed record so `plugins.<pluginName>`\n * autocompletes the right instance type.\n *\n * class GameRoom extends Room {\n * plugins = definePlugins([\n * new ChatPlugin(), // pluginName: 'chat'\n * new UniqueSessionPlugin({ max: 1 }), // pluginName: 'uniqueSession'\n * ]);\n * }\n * this.plugins.chat.send('hi');\n *\n * Throws at runtime if any plugin is missing `pluginName` or if two\n * plugins resolve to the same key.\n */\nexport function definePlugins<const T extends readonly RoomPlugin<any>[]>(\n plugins: T,\n): PluginsArrayToRecord<T>;\n\n/**\n * Record form (legacy / multi-instance escape hatch). The caller\n * chooses the key per-room. Useful when registering two instances of\n * the same plugin class without configuring `pluginName` per\n * instance \u2014 e.g. `{ adminChat: new ChatPlugin(), playerChat: new ChatPlugin() }`.\n */\nexport function definePlugins<const T extends Record<string, RoomPlugin<any>>>(\n plugins: T,\n): { [K in keyof T]: PluginPublicSurface<T[K]> };\n\nexport function definePlugins(plugins: any): any {\n if (!Array.isArray(plugins)) { return plugins; }\n const out: Record<string, RoomPlugin> = {};\n for (const p of plugins) {\n const key = p['pluginName'];\n if (typeof key !== 'string' || key.length === 0) {\n throw new Error(\n `[Room] plugin ${p.constructor.name} is missing a 'pluginName' field. ` +\n `Declare \\`readonly pluginName = '<key>' as const\\` on the class, ` +\n `or register it via the keyed-record form \\`definePlugins({ <key>: <plugin> })\\`.`,\n );\n }\n if (out[key]) {\n throw new Error(\n `[Room] two plugins resolve to pluginName \"${key}\". Configure one of ` +\n `them via constructor argument, or use the keyed-record form to disambiguate.`,\n );\n }\n out[key] = p;\n }\n return out;\n}\n\n/**\n * Lifecycle hook keys recognized by the framework \u2014 used internally to\n * separate \"framework-recognized methods\" from \"user-defined public\n * methods\" when wiring a plugin into a room. Exported for the test\n * harness; downstream code should not need it.\n */\nexport const PLUGIN_LIFECYCLE_KEYS = ['onCreate', 'onAuth', 'onJoin', 'onLeave', 'onDispose'] as const;\nexport type PluginLifecycleKey = (typeof PLUGIN_LIFECYCLE_KEYS)[number];\n\n/**\n * Test helper \u2014 attach a stub or fake room to a plugin so its methods\n * and lifecycle hooks can be exercised in isolation without spinning up\n * a real Colyseus server.\n *\n * const plugin = new ChatPlugin({ historyLimit: 5 });\n * const room = attachToTestRoom(plugin, { broadcast: sinon.spy() });\n * await plugin.messages!.chat!.call(plugin, { userId: 'u1' }, { text: 'hi' });\n *\n * The second arg is shallow-merged onto the stub so tests only declare\n * the room properties they actually exercise. Returns the room stub for\n * post-call assertions.\n */\nexport function attachToTestRoom<This extends Room, R extends Partial<This>>(\n plugin: RoomPlugin<This>,\n roomStub: R = {} as R,\n): R {\n (plugin as any).room = roomStub;\n return roomStub;\n}\n\n// ---------------------------------------------------------------------------\n// Layout machinery \u2014 used by Room.__init to set plugins up once per class.\n// Lives in this file (rather than Room.ts) so the plugin-related types and\n// helpers stay co-located. These are framework internals; callers outside\n// `@colyseus/core` should not depend on them.\n// ---------------------------------------------------------------------------\n\n/**\n * Precomputed plugin layout for a Room subclass \u2014 populated on first\n * construct, cached on the constructor. Hook wrappers are installed\n * on the prototype in the same pass (see `installPluginHookWrappers`).\n *\n * @internal\n */\nexport interface PluginLayout {\n /** Per-hook participation: which plugin keys run before/after the room's own hook. */\n hooks: Record<PluginLifecycleKey, { before: string[]; after: string[] }>;\n /** Message key \u2192 plugin key. Conflict detection ran when this was built. */\n messageOwners: Map<string, string>;\n /** Sentinel-keyed plugin classes to instantiate per room. */\n autoDeps: Array<{ key: string; ctor: RoomPluginClass }>;\n}\n\n/** Sentinel prefix for framework-instantiated deps. The colon prevents\n * collisions with any JS identifier the user could use as a key. */\nconst DEP_KEY_PREFIX = '__dep:';\n\n/**\n * Default before/after policy for lifecycle hooks vs the room's own\n * hook. Plugins can override per-hook via the `order` field on the\n * plugin instance.\n *\n * onCreate / onAuth / onJoin \u2192 plugins run BEFORE room (guards + setup)\n * onLeave / onDispose \u2192 plugins run AFTER room (capture final state)\n *\n * @internal\n */\nexport const DEFAULT_PLUGIN_ORDER: Record<PluginLifecycleKey, 'before' | 'after'> = {\n onCreate: 'before',\n onAuth: 'before',\n onJoin: 'before',\n onLeave: 'after',\n onDispose: 'after',\n};\n\n/**\n * Walk the room's plugin instances and produce the lifecycle + message\n * layout for the Room class. Called once per Room subclass \u2014 on the\n * first construct \u2014 and the result is cached on the constructor. Throws\n * on duplicate message keys (named both plugin keys) so the failure is\n * visible at class-init rather than at first message dispatch.\n *\n * Returns `null` when no plugins participate in any hook AND none\n * declare a message \u2014 distinguishes \"computed, nothing to do\" from\n * \"not computed yet\" in the `__pluginLayout` cache.\n *\n * @internal\n */\nexport function computePluginLayout(plugins: Record<string, RoomPlugin>): PluginLayout | null {\n // Resolve `static dependencies` closures over the user's record.\n // Returns an expanded list (user + auto-deps), the auto-dep\n // class table for per-room instantiation, and a unified view of\n // entries to iterate when computing hooks / message owners.\n const resolved = resolveDependencies(plugins);\n\n const hooks = {} as Record<PluginLifecycleKey, { before: string[]; after: string[] }>;\n let anyHook = false;\n\n for (const hook of PLUGIN_LIFECYCLE_KEYS) {\n const before: string[] = [];\n const after: string[] = [];\n for (const { key, plugin } of resolved.entries) {\n if (typeof plugin[hook] !== 'function') { continue; }\n // Bracket access \u2014 `order` and `messages` are protected; TS\n // skips visibility checks on indexed access.\n const order = plugin['order']?.[hook] ?? DEFAULT_PLUGIN_ORDER[hook];\n (order === 'before' ? before : after).push(key);\n anyHook = true;\n }\n hooks[hook] = { before, after };\n }\n\n const messageOwners = new Map<string, string>();\n for (const { key, plugin } of resolved.entries) {\n const messages = plugin['messages'];\n if (!messages) { continue; }\n for (const messageKey of Object.keys(messages)) {\n const prior = messageOwners.get(messageKey);\n if (prior !== undefined) {\n throw new Error(\n `[Room] message key \"${messageKey}\" declared by multiple plugins: ` +\n `\"${prior}\" and \"${key}\". Resolve by giving one of them a ` +\n `different key, or override on the room's own \\`messages\\`.`,\n );\n }\n messageOwners.set(messageKey, key);\n }\n }\n\n if (!anyHook && messageOwners.size === 0 && resolved.autoDeps.length === 0) { return null; }\n return { hooks, messageOwners, autoDeps: resolved.autoDeps };\n}\n\n/**\n * Walk every plugin's `static dependencies` recursively, instantiating\n * missing classes (zero-arg only). Throws on cycles. Auto-deps are\n * keyed `__dep:<ClassName>` in the returned entries.\n */\nfunction resolveDependencies(plugins: Record<string, RoomPlugin>): {\n entries: Array<{ key: string; plugin: RoomPlugin }>;\n autoDeps: Array<{ key: string; ctor: RoomPluginClass }>;\n} {\n const entries: Array<{ key: string; plugin: RoomPlugin }> = [];\n for (const [key, plugin] of Object.entries(plugins)) {\n entries.push({ key, plugin });\n }\n\n const present = new Set<RoomPluginClass>();\n for (const { plugin } of entries) {\n present.add(plugin.constructor as RoomPluginClass);\n }\n\n const autoDeps: Array<{ key: string; ctor: RoomPluginClass }> = [];\n const visiting = new Set<RoomPluginClass>();\n const path: string[] = [];\n\n function walk(depCtor: RoomPluginClass): void {\n if (present.has(depCtor)) { return; }\n if (visiting.has(depCtor)) {\n const cycle = [...path, depCtor.name].join(' \u2192 ');\n throw new Error(`[Room] plugin dependency cycle: ${cycle}`);\n }\n visiting.add(depCtor);\n path.push(depCtor.name);\n\n // Recurse into deeper deps first \u2192 topological order in `entries`.\n const deeper = (depCtor as any).dependencies as PluginDependencies | undefined;\n if (Array.isArray(deeper)) {\n for (const inner of deeper) { walk(inner); }\n }\n\n let instance: RoomPlugin;\n try { instance = new depCtor(); }\n catch (err: any) {\n throw new Error(\n `[Room] auto-included plugin \"${depCtor.name}\" must be ` +\n `constructible with no arguments. If it needs options, ` +\n `register it explicitly in definePlugins({...}). ` +\n `(cause: ${err?.message ?? err})`,\n );\n }\n const key = DEP_KEY_PREFIX + depCtor.name;\n entries.push({ key, plugin: instance });\n autoDeps.push({ key, ctor: depCtor });\n present.add(depCtor);\n\n visiting.delete(depCtor);\n path.pop();\n }\n\n // Snapshot before mutating \u2014 transitive deps are handled recursively\n // inside `walk()`.\n const userEntriesSnapshot = entries.slice();\n for (const { plugin } of userEntriesSnapshot) {\n const deps = (plugin.constructor as any).dependencies as PluginDependencies | undefined;\n if (!Array.isArray(deps)) { continue; }\n for (const depCtor of deps) { walk(depCtor); }\n }\n\n return { entries, autoDeps };\n}\n\n/**\n * Install one wrapper per participating hook on the Room subclass's\n * prototype. The wrapper closes over plugin KEYS (resolved to refs at\n * call time) and invokes the captured original room hook between the\n * before/after plugin runs.\n *\n * @internal\n */\nexport function installPluginHookWrappers(\n ctor: { prototype: any },\n layout: PluginLayout | null,\n): void {\n if (layout === null) { return; }\n const proto = ctor.prototype;\n for (const hook of PLUGIN_LIFECYCLE_KEYS) {\n const { before, after } = layout.hooks[hook];\n if (before.length === 0 && after.length === 0) { continue; }\n\n const original = proto[hook] as Function | undefined;\n proto[hook] = async function (\n this: { plugins?: Record<string, RoomPlugin>; _autoPlugins?: Record<string, RoomPlugin> },\n ...args: any[]\n ) {\n // User plugins live on `this.plugins`, auto-deps (sentinel-keyed)\n // on `this._autoPlugins` \u2014 kept separate so user types stay clean.\n const lookup = (k: string): RoomPlugin =>\n (k.startsWith(DEP_KEY_PREFIX) ? this._autoPlugins![k] : this.plugins![k]);\n for (const k of before) {\n const p = lookup(k);\n await (p[hook] as Function).call(p, ...args);\n }\n let result: unknown;\n if (original) { result = await original.apply(this, args); }\n for (const k of after) {\n const p = lookup(k);\n await (p[hook] as Function).call(p, ...args);\n }\n return result;\n };\n }\n}\n\n/** Structural shape used by `setupRoomPlugins` \u2014 avoids a Room import\n * so the dependency graph stays one-way (Room \u2192 RoomPlugin). */\ninterface RoomPluginHost {\n plugins?: Record<string, RoomPlugin<any>>;\n _autoPlugins?: Record<string, RoomPlugin<any>>;\n messages?: Record<string, Function> | any;\n constructor: { __pluginLayout?: PluginLayout | null; prototype: any };\n}\n\n/**\n * Wire a Room instance's plugins. Once-per-class layout (hook\n * participation, message owners, dep resolution) is cached on the\n * constructor; per-instance work attaches `room` refs, instantiates\n * auto-deps, and merges plugin messages.\n *\n * @internal\n */\nexport function setupRoomPlugins(room: RoomPluginHost): void {\n const plugins = room.plugins!;\n const layout = resolveOrComputeLayout(room.constructor, plugins);\n\n attachRoomReference(room, plugins);\n\n if (layout && layout.autoDeps.length > 0) {\n room._autoPlugins = instantiateAutoDeps(room, layout);\n }\n\n if (layout && layout.messageOwners.size > 0) {\n mergePluginMessages(room, layout);\n }\n\n Object.freeze(plugins);\n if (room._autoPlugins) { Object.freeze(room._autoPlugins); }\n}\n\n/**\n * Read cached layout for this subclass, or compute + install on\n * first construct. `hasOwnProperty` so a subclass that redeclares\n * `plugins` doesn't inherit the parent's wrapping.\n */\nfunction resolveOrComputeLayout(\n ctor: RoomPluginHost['constructor'],\n plugins: Record<string, RoomPlugin>,\n): PluginLayout | null | undefined {\n if (Object.prototype.hasOwnProperty.call(ctor, '__pluginLayout')) {\n return ctor.__pluginLayout;\n }\n const layout = computePluginLayout(plugins);\n installPluginHookWrappers(ctor, layout);\n ctor.__pluginLayout = layout;\n return layout;\n}\n\n/** Wire `plugin.room = room` on every user-explicit plugin. */\nfunction attachRoomReference(\n room: RoomPluginHost,\n plugins: Record<string, RoomPlugin>,\n): void {\n for (const plugin of Object.values(plugins)) {\n (plugin as any).room = room;\n }\n}\n\n/**\n * Build `_autoPlugins` \u2014 one fresh instance per `static dependencies`\n * entry. Kept separate from `room.plugins` so sentinel keys\n * (`__dep:<ClassName>`) don't leak into the user's typed view.\n */\nfunction instantiateAutoDeps(\n room: RoomPluginHost,\n layout: PluginLayout,\n): Record<string, RoomPlugin> {\n const auto: Record<string, RoomPlugin> = {};\n for (const { key, ctor: depCtor } of layout.autoDeps) {\n const instance = new depCtor();\n (instance as any).room = room;\n auto[key] = instance;\n }\n return auto;\n}\n\n/**\n * Copy plugin message handlers into `room.messages` (room's own key\n * wins; plugin-vs-plugin conflicts already threw at layout time).\n */\nfunction mergePluginMessages(\n room: RoomPluginHost,\n layout: PluginLayout,\n): void {\n const plugins = room.plugins!;\n for (const [messageKey, pluginKey] of layout.messageOwners) {\n if (room.messages?.[messageKey]) { continue; }\n const source = pluginKey.startsWith(DEP_KEY_PREFIX)\n ? room._autoPlugins![pluginKey]\n : plugins[pluginKey];\n const handler = source['messages']?.[messageKey];\n if (handler !== undefined) {\n (room.messages ??= {} as any)[messageKey] = handler;\n }\n }\n}\n"],
|
|
5
|
-
"mappings": "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
|
|
4
|
+
"sourcesContent": ["/**\n * Room plugins.\n *\n * A plugin is a composable extension to a Room \u2014 it can declare message\n * handlers, attach lifecycle hooks, and expose public methods that the\n * room can call (`this.plugins.<key>.someMethod()`). The framework\n * instantiates one plugin per room, sets `this.room` after the room is\n * constructed, then merges the plugin's `messages` with the room's own\n * and wires its lifecycle hooks alongside the room's.\n *\n * Example: a tiny chat plugin contributing a single message handler:\n *\n * class ChatPlugin extends RoomPlugin {\n * private history: string[] = [];\n * constructor(private opts: { historyLimit: number }) { super(); }\n *\n * messages = {\n * chat: (client: Client, msg: { text: string }) => {\n * this.history.push(msg.text);\n * if (this.history.length > this.opts.historyLimit) this.history.shift();\n * this.room.broadcast('chat', { from: client.userId, text: msg.text });\n * },\n * };\n *\n * // Public \u2014 callable as `this.plugins.chat.getHistory()` from the room\n * getHistory(): readonly string[] { return this.history; }\n * }\n *\n * Use via `definePlugins`:\n *\n * class MyRoom extends Room<{ state: MyState }> {\n * plugins = definePlugins({\n * chat: new ChatPlugin({ historyLimit: 100 }),\n * });\n * }\n */\nimport type { Room } from './Room.ts';\nimport type { AuthContext, Client } from './Transport.ts';\nimport type { Messages } from '@colyseus/shared-types';\n\n/**\n * Ordering hint for a plugin's lifecycle hook relative to the room's own\n * hook. Sensible per-hook defaults are applied when omitted (see\n * `Room.__init` for the exact ordering policy).\n *\n * onCreate / onAuth / onJoin \u2192 plugins run BEFORE room (guards + setup)\n * onLeave / onDispose \u2192 plugins run AFTER room (capture final state)\n */\nexport interface RoomPluginOrder {\n onCreate?: 'before' | 'after';\n onAuth?: 'before' | 'after';\n onJoin?: 'before' | 'after';\n onLeave?: 'before' | 'after';\n onDispose?: 'before' | 'after';\n}\n\n/**\n * Base class for room plugins. Subclass to define a plugin; the framework\n * sets `this.room` after the host room is fully constructed.\n *\n * Don't access `this.room` from the plugin's constructor \u2014 it hasn't been\n * wired yet. Everything room-dependent goes in `onCreate` / `onJoin` /\n * etc. or in public methods that are called from the room post-init.\n *\n * @typeParam This - The Room subclass this plugin is attached to. Narrow\n * for schema-driven plugins that need a specific state shape, e.g.\n * `class PhysicsPlugin extends RoomPlugin<Room<{ state: PhysicsContract }>>`.\n */\nexport abstract class RoomPlugin<This extends Room = Room> {\n /**\n * Live room reference. Wired by the framework at __init, AFTER the\n * room's own construction \u2014 accessing it from the plugin's\n * constructor throws.\n */\n protected readonly room!: This;\n\n /**\n * Canonical key for the plugin when registered via `definePlugins([...])`.\n * Declared on the subclass with `as const` so the literal type flows\n * into `room.plugins.<key>`:\n *\n * class ChatPlugin extends RoomPlugin {\n * readonly pluginName = 'chat' as const;\n * }\n *\n * Optional under the keyed-record form (`definePlugins({ chat: ... })`),\n * required under the array form. Must stay `public` so `extends` /\n * `keyof` can see the literal \u2014 those are blind to protected from\n * outside the class. End-user autocomplete hides it via `Omit` in\n * `definePlugins`'s return type.\n *\n * For multi-instance use, accept the name at construction:\n *\n * readonly pluginName: string;\n * constructor(name = 'chat') { super(); this.pluginName = name; }\n */\n readonly pluginName?: string;\n\n /**\n * Declarative message handlers \u2014 merged into the room's `messages` at\n * __init. Conflict against the room's own key: room wins. Conflict\n * between two plugins: throws at __init.\n */\n protected messages?: Messages<This>;\n\n /** Optional per-hook ordering vs the room's own hook. */\n protected order?: RoomPluginOrder;\n\n // Lifecycle hooks \u2014 override any subset. `protected` so they don't\n // leak into `this.plugins.<key>.onJoin(...)` autocomplete; subclass\n // overrides must keep `protected` (TS widens to public silently\n // otherwise).\n protected onCreate?(options: any): void | Promise<void>;\n protected onAuth?(client: Client, options: any, context: AuthContext): void | Promise<void>;\n protected onJoin?(client: Client, options?: any): void | Promise<void>;\n protected onLeave?(client: Client, code?: number): void | Promise<void>;\n protected onDispose?(): void | Promise<void>;\n}\n\n/**\n * Plugin-class constructor type used by the `dependencies` static\n * declaration. Constrained to zero-arg constructors because the\n * framework auto-instantiates missing dependencies with no\n * configuration \u2014 plugins that need options can't be auto-included\n * and must be registered explicitly in `definePlugins({...})`.\n */\nexport type RoomPluginClass = new () => RoomPlugin<any>;\n\n/**\n * Static `dependencies` declaration. List other plugin classes this\n * plugin needs alongside it; the framework auto-instantiates any\n * missing ones at room construction time. Transitive deps are\n * resolved recursively. Cycles throw at class-init.\n *\n * Example:\n * class UniqueSessionPlugin extends RoomPlugin {\n * static dependencies: PluginDependencies = [TrackUserSessionsPlugin];\n * }\n */\nexport type PluginDependencies = ReadonlyArray<RoomPluginClass>;\n\n/**\n * Define a Room's plugin record. The framework wires plugins at\n * `__init` \u2014 first construct of the class computes the layout\n * (cached on the constructor) and installs hook wrappers on the\n * prototype. The `const T` modifier preserves literal types so\n * `this.plugins.<key>.method()` autocompletes against each plugin's\n * specific subclass.\n */\n// Pulls each plugin's `pluginName` literal as the record key. Subclass\n// must narrow with `as const` \u2014 plain `string` resolves to `never`\n// and the entry is dropped (callers see a TS error on\n// `plugins.<thatKey>`).\ntype ExtractPluginName<P> = P extends { pluginName: infer K }\n ? (K extends string ? K : never)\n : never;\n\n/** Hide `pluginName` from end-user autocomplete on `this.plugins.<key>`. */\ntype PluginPublicSurface<P> = Omit<P, 'pluginName'>;\n\ntype PluginsArrayToRecord<T extends readonly RoomPlugin<any>[]> = {\n [P in T[number] as ExtractPluginName<P>]: PluginPublicSurface<P>;\n};\n\n/**\n * Array form (recommended). Each plugin declares its own canonical\n * key via `readonly pluginName = '...' as const`; the framework\n * turns the array into a typed record so `plugins.<pluginName>`\n * autocompletes the right instance type.\n *\n * class GameRoom extends Room {\n * plugins = definePlugins([\n * new ChatPlugin(), // pluginName: 'chat'\n * new UniqueSessionPlugin({ max: 1 }), // pluginName: 'uniqueSession'\n * ]);\n * }\n * this.plugins.chat.send('hi');\n *\n * Throws at runtime if any plugin is missing `pluginName` or if two\n * plugins resolve to the same key.\n */\nexport function definePlugins<const T extends readonly RoomPlugin<any>[]>(\n plugins: T,\n): PluginsArrayToRecord<T>;\n\n/**\n * Record form (legacy / multi-instance escape hatch). The caller\n * chooses the key per-room. Useful when registering two instances of\n * the same plugin class without configuring `pluginName` per\n * instance \u2014 e.g. `{ adminChat: new ChatPlugin(), playerChat: new ChatPlugin() }`.\n */\nexport function definePlugins<const T extends Record<string, RoomPlugin<any>>>(\n plugins: T,\n): { [K in keyof T]: PluginPublicSurface<T[K]> };\n\nexport function definePlugins(plugins: any): any {\n if (!Array.isArray(plugins)) { return plugins; }\n const out: Record<string, RoomPlugin> = {};\n for (const p of plugins) {\n const key = p['pluginName'];\n if (typeof key !== 'string' || key.length === 0) {\n throw new Error(\n `[Room] plugin ${p.constructor.name} is missing a 'pluginName' field. ` +\n `Declare \\`readonly pluginName = '<key>' as const\\` on the class, ` +\n `or register it via the keyed-record form \\`definePlugins({ <key>: <plugin> })\\`.`,\n );\n }\n if (out[key]) {\n throw new Error(\n `[Room] two plugins resolve to pluginName \"${key}\". Configure one of ` +\n `them via constructor argument, or use the keyed-record form to disambiguate.`,\n );\n }\n out[key] = p;\n }\n return out;\n}\n\n/**\n * Lifecycle hook keys recognized by the framework \u2014 used internally to\n * separate \"framework-recognized methods\" from \"user-defined public\n * methods\" when wiring a plugin into a room. Exported for the test\n * harness; downstream code should not need it.\n */\nexport const PLUGIN_LIFECYCLE_KEYS = ['onCreate', 'onAuth', 'onJoin', 'onLeave', 'onDispose'] as const;\nexport type PluginLifecycleKey = (typeof PLUGIN_LIFECYCLE_KEYS)[number];\n\n/**\n * Test helper \u2014 attach a stub or fake room to a plugin so its methods\n * and lifecycle hooks can be exercised in isolation without spinning up\n * a real Colyseus server.\n *\n * const plugin = new ChatPlugin({ historyLimit: 5 });\n * const room = attachToTestRoom(plugin, { broadcast: sinon.spy() });\n * await plugin.messages!.chat!.call(plugin, { userId: 'u1' }, { text: 'hi' });\n *\n * The second arg is shallow-merged onto the stub so tests only declare\n * the room properties they actually exercise. Returns the room stub for\n * post-call assertions.\n */\nexport function attachToTestRoom<This extends Room, R extends Partial<This>>(\n plugin: RoomPlugin<This>,\n roomStub: R = {} as R,\n): R {\n (plugin as any).room = roomStub;\n return roomStub;\n}\n\n// ---------------------------------------------------------------------------\n// Layout machinery \u2014 used by Room.__init to set plugins up once per class.\n// Lives in this file (rather than Room.ts) so the plugin-related types and\n// helpers stay co-located. These are framework internals; callers outside\n// `@colyseus/core` should not depend on them.\n// ---------------------------------------------------------------------------\n\n/**\n * Precomputed plugin layout for a Room subclass \u2014 populated on first\n * construct, cached on the constructor. Hook wrappers are installed\n * on the prototype in the same pass (see `installPluginHookWrappers`).\n *\n * @internal\n */\nexport interface PluginLayout {\n /** Per-hook participation: which plugin keys run before/after the room's own hook. */\n hooks: Record<PluginLifecycleKey, { before: string[]; after: string[] }>;\n /** Message key \u2192 plugin key. Conflict detection ran when this was built. */\n messageOwners: Map<string, string>;\n /** Sentinel-keyed plugin classes to instantiate per room. */\n autoDeps: Array<{ key: string; ctor: RoomPluginClass }>;\n}\n\n/** Sentinel prefix for framework-instantiated deps. The colon prevents\n * collisions with any JS identifier the user could use as a key. */\nconst DEP_KEY_PREFIX = '__dep:';\n\n/**\n * Default before/after policy for lifecycle hooks vs the room's own\n * hook. Plugins can override per-hook via the `order` field on the\n * plugin instance.\n *\n * onCreate / onAuth / onJoin \u2192 plugins run BEFORE room (guards + setup)\n * onLeave / onDispose \u2192 plugins run AFTER room (capture final state)\n *\n * @internal\n */\nexport const DEFAULT_PLUGIN_ORDER: Record<PluginLifecycleKey, 'before' | 'after'> = {\n onCreate: 'before',\n onAuth: 'before',\n onJoin: 'before',\n onLeave: 'after',\n onDispose: 'after',\n};\n\n/**\n * Walk the room's plugin instances and produce the lifecycle + message\n * layout for the Room class. Called once per Room subclass \u2014 on the\n * first construct \u2014 and the result is cached on the constructor. Throws\n * on duplicate message keys (named both plugin keys) so the failure is\n * visible at class-init rather than at first message dispatch.\n *\n * Returns `null` when no plugins participate in any hook AND none\n * declare a message \u2014 distinguishes \"computed, nothing to do\" from\n * \"not computed yet\" in the `__pluginLayout` cache.\n *\n * @internal\n */\nexport function computePluginLayout(plugins: Record<string, RoomPlugin>): PluginLayout | null {\n // Resolve `static dependencies` closures over the user's record.\n // Returns an expanded list (user + auto-deps), the auto-dep\n // class table for per-room instantiation, and a unified view of\n // entries to iterate when computing hooks / message owners.\n const resolved = resolveDependencies(plugins);\n\n const hooks = {} as Record<PluginLifecycleKey, { before: string[]; after: string[] }>;\n let anyHook = false;\n\n for (const hook of PLUGIN_LIFECYCLE_KEYS) {\n const before: string[] = [];\n const after: string[] = [];\n for (const { key, plugin } of resolved.entries) {\n if (typeof plugin[hook] !== 'function') { continue; }\n // Bracket access \u2014 `order` and `messages` are protected; TS\n // skips visibility checks on indexed access.\n const order = plugin['order']?.[hook] ?? DEFAULT_PLUGIN_ORDER[hook];\n (order === 'before' ? before : after).push(key);\n anyHook = true;\n }\n hooks[hook] = { before, after };\n }\n\n const messageOwners = new Map<string, string>();\n for (const { key, plugin } of resolved.entries) {\n const messages = plugin['messages'];\n if (!messages) { continue; }\n for (const messageKey of Object.keys(messages)) {\n const prior = messageOwners.get(messageKey);\n if (prior !== undefined) {\n throw new Error(\n `[Room] message key \"${messageKey}\" declared by multiple plugins: ` +\n `\"${prior}\" and \"${key}\". Resolve by giving one of them a ` +\n `different key, or override on the room's own \\`messages\\`.`,\n );\n }\n messageOwners.set(messageKey, key);\n }\n }\n\n if (!anyHook && messageOwners.size === 0 && resolved.autoDeps.length === 0) { return null; }\n return { hooks, messageOwners, autoDeps: resolved.autoDeps };\n}\n\n/**\n * Walk every plugin's `static dependencies` recursively, instantiating\n * missing classes (zero-arg only). Throws on cycles. Auto-deps are\n * keyed `__dep:<ClassName>` in the returned entries.\n */\nfunction resolveDependencies(plugins: Record<string, RoomPlugin>): {\n entries: Array<{ key: string; plugin: RoomPlugin }>;\n autoDeps: Array<{ key: string; ctor: RoomPluginClass }>;\n} {\n const entries: Array<{ key: string; plugin: RoomPlugin }> = [];\n for (const [key, plugin] of Object.entries(plugins)) {\n entries.push({ key, plugin });\n }\n\n const present = new Set<RoomPluginClass>();\n for (const { plugin } of entries) {\n present.add(plugin.constructor as RoomPluginClass);\n }\n\n const autoDeps: Array<{ key: string; ctor: RoomPluginClass }> = [];\n const visiting = new Set<RoomPluginClass>();\n const path: string[] = [];\n\n function walk(depCtor: RoomPluginClass): void {\n if (present.has(depCtor)) { return; }\n if (visiting.has(depCtor)) {\n const cycle = [...path, depCtor.name].join(' \u2192 ');\n throw new Error(`[Room] plugin dependency cycle: ${cycle}`);\n }\n visiting.add(depCtor);\n path.push(depCtor.name);\n\n // Recurse into deeper deps first \u2192 topological order in `entries`.\n const deeper = (depCtor as any).dependencies as PluginDependencies | undefined;\n if (Array.isArray(deeper)) {\n for (const inner of deeper) { walk(inner); }\n }\n\n let instance: RoomPlugin;\n try { instance = new depCtor(); }\n catch (err: any) {\n throw new Error(\n `[Room] auto-included plugin \"${depCtor.name}\" must be ` +\n `constructible with no arguments. If it needs options, ` +\n `register it explicitly in definePlugins({...}). ` +\n `(cause: ${err?.message ?? err})`,\n );\n }\n const key = DEP_KEY_PREFIX + depCtor.name;\n entries.push({ key, plugin: instance });\n autoDeps.push({ key, ctor: depCtor });\n present.add(depCtor);\n\n visiting.delete(depCtor);\n path.pop();\n }\n\n // Snapshot before mutating \u2014 transitive deps are handled recursively\n // inside `walk()`.\n const userEntriesSnapshot = entries.slice();\n for (const { plugin } of userEntriesSnapshot) {\n const deps = (plugin.constructor as any).dependencies as PluginDependencies | undefined;\n if (!Array.isArray(deps)) { continue; }\n for (const depCtor of deps) { walk(depCtor); }\n }\n\n return { entries, autoDeps };\n}\n\n/**\n * Install one wrapper per participating hook on the Room subclass's\n * prototype. The wrapper closes over plugin KEYS (resolved to refs at\n * call time) and invokes the captured original room hook between the\n * before/after plugin runs.\n *\n * @internal\n */\nexport function installPluginHookWrappers(\n ctor: { prototype: any },\n layout: PluginLayout | null,\n): void {\n if (layout === null) { return; }\n const proto = ctor.prototype;\n for (const hook of PLUGIN_LIFECYCLE_KEYS) {\n const { before, after } = layout.hooks[hook];\n if (before.length === 0 && after.length === 0) { continue; }\n proto[hook] = composePluginHook(hook, before, after, proto[hook]);\n }\n}\n\n/**\n * Hooks declared as class fields (`onJoin = () => {}`) are own properties\n * that shadow the prototype wrapper, so wrap them per instance.\n */\nfunction wrapOwnHooks(room: RoomPluginHost, layout: PluginLayout): void {\n for (const hook of PLUGIN_LIFECYCLE_KEYS) {\n const { before, after } = layout.hooks[hook];\n if (before.length === 0 && after.length === 0) { continue; }\n if (!Object.hasOwn(room, hook)) { continue; }\n (room as any)[hook] = composePluginHook(hook, before, after, (room as any)[hook]);\n }\n}\n\n/** Run the `before` plugins, then `original`, then the `after` plugins. */\nfunction composePluginHook(\n hook: PluginLifecycleKey,\n before: string[],\n after: string[],\n original: Function | undefined,\n) {\n return async function (\n this: { plugins?: Record<string, RoomPlugin>; _autoPlugins?: Record<string, RoomPlugin> },\n ...args: any[]\n ) {\n // User plugins live on `this.plugins`, auto-deps (sentinel-keyed)\n // on `this._autoPlugins` \u2014 kept separate so user types stay clean.\n const lookup = (k: string): RoomPlugin =>\n (k.startsWith(DEP_KEY_PREFIX) ? this._autoPlugins![k] : this.plugins![k]);\n for (const k of before) {\n const p = lookup(k);\n await (p[hook] as Function).call(p, ...args);\n }\n let result: unknown;\n if (original) { result = await original.apply(this, args); }\n for (const k of after) {\n const p = lookup(k);\n await (p[hook] as Function).call(p, ...args);\n }\n return result;\n };\n}\n\n/** Structural shape used by `setupRoomPlugins` \u2014 avoids a Room import\n * so the dependency graph stays one-way (Room \u2192 RoomPlugin). */\ninterface RoomPluginHost {\n plugins?: Record<string, RoomPlugin<any>>;\n _autoPlugins?: Record<string, RoomPlugin<any>>;\n messages?: Record<string, Function> | any;\n constructor: { __pluginLayout?: PluginLayout | null; prototype: any };\n}\n\n/**\n * Wire a Room instance's plugins. Once-per-class layout (hook\n * participation, message owners, dep resolution) is cached on the\n * constructor; per-instance work attaches `room` refs, instantiates\n * auto-deps, and merges plugin messages.\n *\n * @internal\n */\nexport function setupRoomPlugins(room: RoomPluginHost): void {\n const plugins = room.plugins!;\n const layout = resolveOrComputeLayout(room.constructor, plugins);\n\n attachRoomReference(room, plugins);\n\n if (layout) {\n wrapOwnHooks(room, layout);\n }\n\n if (layout && layout.autoDeps.length > 0) {\n room._autoPlugins = instantiateAutoDeps(room, layout);\n }\n\n if (layout && layout.messageOwners.size > 0) {\n mergePluginMessages(room, layout);\n }\n\n Object.freeze(plugins);\n if (room._autoPlugins) { Object.freeze(room._autoPlugins); }\n}\n\n/**\n * Read cached layout for this subclass, or compute + install on\n * first construct. `hasOwnProperty` so a subclass that redeclares\n * `plugins` doesn't inherit the parent's wrapping.\n */\nfunction resolveOrComputeLayout(\n ctor: RoomPluginHost['constructor'],\n plugins: Record<string, RoomPlugin>,\n): PluginLayout | null | undefined {\n if (Object.prototype.hasOwnProperty.call(ctor, '__pluginLayout')) {\n return ctor.__pluginLayout;\n }\n const layout = computePluginLayout(plugins);\n installPluginHookWrappers(ctor, layout);\n ctor.__pluginLayout = layout;\n return layout;\n}\n\n/** Wire `plugin.room = room` on every user-explicit plugin. */\nfunction attachRoomReference(\n room: RoomPluginHost,\n plugins: Record<string, RoomPlugin>,\n): void {\n for (const plugin of Object.values(plugins)) {\n (plugin as any).room = room;\n }\n}\n\n/**\n * Build `_autoPlugins` \u2014 one fresh instance per `static dependencies`\n * entry. Kept separate from `room.plugins` so sentinel keys\n * (`__dep:<ClassName>`) don't leak into the user's typed view.\n */\nfunction instantiateAutoDeps(\n room: RoomPluginHost,\n layout: PluginLayout,\n): Record<string, RoomPlugin> {\n const auto: Record<string, RoomPlugin> = {};\n for (const { key, ctor: depCtor } of layout.autoDeps) {\n const instance = new depCtor();\n (instance as any).room = room;\n auto[key] = instance;\n }\n return auto;\n}\n\n/**\n * Copy plugin message handlers into `room.messages` (room's own key\n * wins; plugin-vs-plugin conflicts already threw at layout time).\n */\nfunction mergePluginMessages(\n room: RoomPluginHost,\n layout: PluginLayout,\n): void {\n const plugins = room.plugins!;\n for (const [messageKey, pluginKey] of layout.messageOwners) {\n if (room.messages?.[messageKey]) { continue; }\n const source = pluginKey.startsWith(DEP_KEY_PREFIX)\n ? room._autoPlugins![pluginKey]\n : plugins[pluginKey];\n const handler = source['messages']?.[messageKey];\n if (handler !== undefined) {\n (room.messages ??= {} as any)[messageKey] = handler;\n }\n }\n}\n"],
|
|
5
|
+
"mappings": ";AAoEO,IAAe,aAAf,MAAoD;AAiD3D;AA8EO,SAAS,cAAc,SAAmB;AAC/C,MAAI,CAAC,MAAM,QAAQ,OAAO,GAAG;AAAE,WAAO;AAAA,EAAS;AAC/C,QAAM,MAAkC,CAAC;AACzC,aAAW,KAAK,SAAS;AACvB,UAAM,MAAM,EAAE,YAAY;AAC1B,QAAI,OAAO,QAAQ,YAAY,IAAI,WAAW,GAAG;AAC/C,YAAM,IAAI;AAAA,QACR,iBAAiB,EAAE,YAAY,IAAI;AAAA,MAGrC;AAAA,IACF;AACA,QAAI,IAAI,GAAG,GAAG;AACZ,YAAM,IAAI;AAAA,QACR,6CAA6C,GAAG;AAAA,MAElD;AAAA,IACF;AACA,QAAI,GAAG,IAAI;AAAA,EACb;AACA,SAAO;AACT;AAQO,IAAM,wBAAwB,CAAC,YAAY,UAAU,UAAU,WAAW,WAAW;AAgBrF,SAAS,iBACd,QACA,WAAc,CAAC,GACZ;AACH,EAAC,OAAe,OAAO;AACvB,SAAO;AACT;AA2BA,IAAM,iBAAiB;AAYhB,IAAM,uBAAuE;AAAA,EAClF,UAAW;AAAA,EACX,QAAW;AAAA,EACX,QAAW;AAAA,EACX,SAAW;AAAA,EACX,WAAW;AACb;AAeO,SAAS,oBAAoB,SAA0D;AAK5F,QAAM,WAAW,oBAAoB,OAAO;AAE5C,QAAM,QAAQ,CAAC;AACf,MAAI,UAAU;AAEd,aAAW,QAAQ,uBAAuB;AACxC,UAAM,SAAmB,CAAC;AAC1B,UAAM,QAAkB,CAAC;AACzB,eAAW,EAAE,KAAK,OAAO,KAAK,SAAS,SAAS;AAC9C,UAAI,OAAO,OAAO,IAAI,MAAM,YAAY;AAAE;AAAA,MAAU;AAGpD,YAAM,QAAQ,OAAO,OAAO,IAAI,IAAI,KAAK,qBAAqB,IAAI;AAClE,OAAC,UAAU,WAAW,SAAS,OAAO,KAAK,GAAG;AAC9C,gBAAU;AAAA,IACZ;AACA,UAAM,IAAI,IAAI,EAAE,QAAQ,MAAM;AAAA,EAChC;AAEA,QAAM,gBAAgB,oBAAI,IAAoB;AAC9C,aAAW,EAAE,KAAK,OAAO,KAAK,SAAS,SAAS;AAC9C,UAAM,WAAW,OAAO,UAAU;AAClC,QAAI,CAAC,UAAU;AAAE;AAAA,IAAU;AAC3B,eAAW,cAAc,OAAO,KAAK,QAAQ,GAAG;AAC9C,YAAM,QAAQ,cAAc,IAAI,UAAU;AAC1C,UAAI,UAAU,QAAW;AACvB,cAAM,IAAI;AAAA,UACR,uBAAuB,UAAU,oCAC7B,KAAK,UAAU,GAAG;AAAA,QAExB;AAAA,MACF;AACA,oBAAc,IAAI,YAAY,GAAG;AAAA,IACnC;AAAA,EACF;AAEA,MAAI,CAAC,WAAW,cAAc,SAAS,KAAK,SAAS,SAAS,WAAW,GAAG;AAAE,WAAO;AAAA,EAAM;AAC3F,SAAO,EAAE,OAAO,eAAe,UAAU,SAAS,SAAS;AAC7D;AAOA,SAAS,oBAAoB,SAG3B;AACA,QAAM,UAAsD,CAAC;AAC7D,aAAW,CAAC,KAAK,MAAM,KAAK,OAAO,QAAQ,OAAO,GAAG;AACnD,YAAQ,KAAK,EAAE,KAAK,OAAO,CAAC;AAAA,EAC9B;AAEA,QAAM,UAAU,oBAAI,IAAqB;AACzC,aAAW,EAAE,OAAO,KAAK,SAAS;AAChC,YAAQ,IAAI,OAAO,WAA8B;AAAA,EACnD;AAEA,QAAM,WAA0D,CAAC;AACjE,QAAM,WAAW,oBAAI,IAAqB;AAC1C,QAAM,OAAiB,CAAC;AAExB,WAAS,KAAK,SAAgC;AAC5C,QAAI,QAAQ,IAAI,OAAO,GAAG;AAAE;AAAA,IAAQ;AACpC,QAAI,SAAS,IAAI,OAAO,GAAG;AACzB,YAAM,QAAQ,CAAC,GAAG,MAAM,QAAQ,IAAI,EAAE,KAAK,UAAK;AAChD,YAAM,IAAI,MAAM,mCAAmC,KAAK,EAAE;AAAA,IAC5D;AACA,aAAS,IAAI,OAAO;AACpB,SAAK,KAAK,QAAQ,IAAI;AAGtB,UAAM,SAAU,QAAgB;AAChC,QAAI,MAAM,QAAQ,MAAM,GAAG;AACzB,iBAAW,SAAS,QAAQ;AAAE,aAAK,KAAK;AAAA,MAAG;AAAA,IAC7C;AAEA,QAAI;AACJ,QAAI;AAAE,iBAAW,IAAI,QAAQ;AAAA,IAAG,SACzB,KAAU;AACf,YAAM,IAAI;AAAA,QACR,gCAAgC,QAAQ,IAAI,2HAGjC,KAAK,WAAW,GAAG;AAAA,MAChC;AAAA,IACF;AACA,UAAM,MAAM,iBAAiB,QAAQ;AACrC,YAAQ,KAAK,EAAE,KAAK,QAAQ,SAAS,CAAC;AACtC,aAAS,KAAK,EAAE,KAAK,MAAM,QAAQ,CAAC;AACpC,YAAQ,IAAI,OAAO;AAEnB,aAAS,OAAO,OAAO;AACvB,SAAK,IAAI;AAAA,EACX;AAIA,QAAM,sBAAsB,QAAQ,MAAM;AAC1C,aAAW,EAAE,OAAO,KAAK,qBAAqB;AAC5C,UAAM,OAAQ,OAAO,YAAoB;AACzC,QAAI,CAAC,MAAM,QAAQ,IAAI,GAAG;AAAE;AAAA,IAAU;AACtC,eAAW,WAAW,MAAM;AAAE,WAAK,OAAO;AAAA,IAAG;AAAA,EAC/C;AAEA,SAAO,EAAE,SAAS,SAAS;AAC7B;AAUO,SAAS,0BACd,MACA,QACM;AACN,MAAI,WAAW,MAAM;AAAE;AAAA,EAAQ;AAC/B,QAAM,QAAQ,KAAK;AACnB,aAAW,QAAQ,uBAAuB;AACxC,UAAM,EAAE,QAAQ,MAAM,IAAI,OAAO,MAAM,IAAI;AAC3C,QAAI,OAAO,WAAW,KAAK,MAAM,WAAW,GAAG;AAAE;AAAA,IAAU;AAC3D,UAAM,IAAI,IAAI,kBAAkB,MAAM,QAAQ,OAAO,MAAM,IAAI,CAAC;AAAA,EAClE;AACF;AAMA,SAAS,aAAa,MAAsB,QAA4B;AACtE,aAAW,QAAQ,uBAAuB;AACxC,UAAM,EAAE,QAAQ,MAAM,IAAI,OAAO,MAAM,IAAI;AAC3C,QAAI,OAAO,WAAW,KAAK,MAAM,WAAW,GAAG;AAAE;AAAA,IAAU;AAC3D,QAAI,CAAC,OAAO,OAAO,MAAM,IAAI,GAAG;AAAE;AAAA,IAAU;AAC5C,IAAC,KAAa,IAAI,IAAI,kBAAkB,MAAM,QAAQ,OAAQ,KAAa,IAAI,CAAC;AAAA,EAClF;AACF;AAGA,SAAS,kBACP,MACA,QACA,OACA,UACA;AACA,SAAO,kBAEF,MACH;AAGA,UAAM,SAAS,CAAC,MACb,EAAE,WAAW,cAAc,IAAI,KAAK,aAAc,CAAC,IAAI,KAAK,QAAS,CAAC;AACzE,eAAW,KAAK,QAAQ;AACtB,YAAM,IAAI,OAAO,CAAC;AAClB,YAAO,EAAE,IAAI,EAAe,KAAK,GAAG,GAAG,IAAI;AAAA,IAC7C;AACA,QAAI;AACJ,QAAI,UAAU;AAAE,eAAS,MAAM,SAAS,MAAM,MAAM,IAAI;AAAA,IAAG;AAC3D,eAAW,KAAK,OAAO;AACrB,YAAM,IAAI,OAAO,CAAC;AAClB,YAAO,EAAE,IAAI,EAAe,KAAK,GAAG,GAAG,IAAI;AAAA,IAC7C;AACA,WAAO;AAAA,EACT;AACF;AAmBO,SAAS,iBAAiB,MAA4B;AAC3D,QAAM,UAAU,KAAK;AACrB,QAAM,SAAS,uBAAuB,KAAK,aAAa,OAAO;AAE/D,sBAAoB,MAAM,OAAO;AAEjC,MAAI,QAAQ;AACV,iBAAa,MAAM,MAAM;AAAA,EAC3B;AAEA,MAAI,UAAU,OAAO,SAAS,SAAS,GAAG;AACxC,SAAK,eAAe,oBAAoB,MAAM,MAAM;AAAA,EACtD;AAEA,MAAI,UAAU,OAAO,cAAc,OAAO,GAAG;AAC3C,wBAAoB,MAAM,MAAM;AAAA,EAClC;AAEA,SAAO,OAAO,OAAO;AACrB,MAAI,KAAK,cAAc;AAAE,WAAO,OAAO,KAAK,YAAY;AAAA,EAAG;AAC7D;AAOA,SAAS,uBACP,MACA,SACiC;AACjC,MAAI,OAAO,UAAU,eAAe,KAAK,MAAM,gBAAgB,GAAG;AAChE,WAAO,KAAK;AAAA,EACd;AACA,QAAM,SAAS,oBAAoB,OAAO;AAC1C,4BAA0B,MAAM,MAAM;AACtC,OAAK,iBAAiB;AACtB,SAAO;AACT;AAGA,SAAS,oBACP,MACA,SACM;AACN,aAAW,UAAU,OAAO,OAAO,OAAO,GAAG;AAC3C,IAAC,OAAe,OAAO;AAAA,EACzB;AACF;AAOA,SAAS,oBACP,MACA,QAC4B;AAC5B,QAAM,OAAmC,CAAC;AAC1C,aAAW,EAAE,KAAK,MAAM,QAAQ,KAAK,OAAO,UAAU;AACpD,UAAM,WAAW,IAAI,QAAQ;AAC7B,IAAC,SAAiB,OAAO;AACzB,SAAK,GAAG,IAAI;AAAA,EACd;AACA,SAAO;AACT;AAMA,SAAS,oBACP,MACA,QACM;AACN,QAAM,UAAU,KAAK;AACrB,aAAW,CAAC,YAAY,SAAS,KAAK,OAAO,eAAe;AAC1D,QAAI,KAAK,WAAW,UAAU,GAAG;AAAE;AAAA,IAAU;AAC7C,UAAM,SAAS,UAAU,WAAW,cAAc,IAC9C,KAAK,aAAc,SAAS,IAC5B,QAAQ,SAAS;AACrB,UAAM,UAAU,OAAO,UAAU,IAAI,UAAU;AAC/C,QAAI,YAAY,QAAW;AACzB,OAAC,KAAK,aAAa,CAAC,GAAU,UAAU,IAAI;AAAA,IAC9C;AAAA,EACF;AACF;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/build/Transport.cjs.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/Transport.ts"],
|
|
4
|
-
"sourcesContent": ["import * as http from 'http';\nimport * as https from 'https';\n\nimport type { Router } from '@colyseus/better-call';\n\nimport { ErrorCode } from '@colyseus/shared-types';\nimport { StateView } from '@colyseus/schema';\nimport type { InputDecoder } from '@colyseus/schema/input';\n\nimport { EventEmitter } from 'events';\nimport { debugAndPrintError } from './Debug.ts';\nimport { getBearerToken, spliceOne } from './utils/Utils.ts';\nimport { ServerError } from './errors/ServerError.ts';\n\nimport type { Room } from './Room.ts';\n\nlet _transport: Transport | undefined;\nexport function setTransport(transport: Transport) { _transport = transport; }\nexport function getTransport() { return _transport; }\n\nexport abstract class Transport {\n public protocol?: string;\n /** Self-signed cert SHA-256 hash (byte array), surfaced to clients in the\n * matchmake response so a WebTransport client can pin it via\n * `serverCertificateHashes`. Set by transports that generate their own cert\n * (h3). Undefined for transports using a CA-trusted cert. */\n public fingerprint?: number[];\n public server?: http.Server | https.Server;\n\n public abstract listen(port?: number | string, hostname?: string, backlog?: number, listeningListener?: Function): this;\n public abstract shutdown(): void;\n\n public abstract simulateLatency(milliseconds: number): void;\n\n /**\n * Returns an Express-compatible application for HTTP route handling.\n * For uWebSockets transport, this uses the uwebsockets-express module.\n * This method is called lazily only when an express callback is provided in server options.\n */\n public getExpressApp?(): Promise<import('express').Application> | import('express').Application | undefined;\n\n /**\n * Binds a router to the transport.\n * Some transports may have a custom way to bind a router to the transport.\n * (uWebSocketsTransport)\n */\n public bindRouter?(router: Router): void;\n}\n\n/**\n * Intercepts an incoming WebSocket upgrade request, before the handshake.\n *\n * Return a `Response` to answer the request instead of upgrading it. Return\n * nothing to upgrade as usual. The handler may be async, and the handshake waits\n * for it to resolve.\n *\n * `context` is the same shape `onAuth()` receives, read-only here: mutating it\n * does not carry over to `onAuth()`.\n *\n * Not supported by `H3Transport`: WebTransport has no upgrade handshake.\n *\n * @example\n * ```typescript\n * new uWebSocketsTransport({\n * beforeUpgrade: async (request, context) => {\n * if (await isBanned(context.ip)) {\n * return new Response(null, { status: 403 });\n * }\n * }\n * });\n * ```\n */\nexport type BeforeUpgradeHandler = (\n request: Request,\n context: Readonly<AuthContext>,\n) => Response | void | Promise<Response | void>;\n\n/**\n * Invokes a `beforeUpgrade` handler, resolving with the `Response` to send\n * instead of upgrading, or `undefined` to proceed with the upgrade.\n *\n * Every transport goes through here, so a handler written against one keeps\n * working on the others. Never rejects: uWebSockets.js aborts the process on an\n * upgrade handler that yields without responding, and on the other transports a\n * raw socket left behind leaks a connection.\n *\n * @internal\n */\nexport async function runBeforeUpgrade(\n handler: BeforeUpgradeHandler,\n url: string, // path, optionally including the query string\n context: AuthContext,\n): Promise<Response | undefined> {\n let request: Request;\n\n try {\n const host = context.headers.get('host') || 'localhost';\n request = new Request(`http://${host}${url}`, { headers: context.headers });\n\n } catch (e: any) {\n // a `Host` header that isn't a valid authority fails to parse as a URL\n debugAndPrintError(e);\n return new Response(null, { status: 400 });\n }\n\n try {\n // `||`, not `??`: a handler that returns nothing is typed `void`, which `??` doesn't strip\n return (await handler(request, context)) || undefined;\n\n } catch (e: any) {\n debugAndPrintError(e);\n return new Response(null, { status: 500 });\n }\n}\n\n/** Headers as the transport has them: uWebSockets.js and Node give a plain record. */\ntype RawHeaders = Headers | Record<string, string | undefined>;\n\nconst readHeader = (headers: RawHeaders, name: string) =>\n (headers instanceof Headers) ? headers.get(name) : headers[name];\n\n/**\n * Builds the context passed to `beforeUpgrade` and `onAuth`.\n *\n * Every transport goes through here, so the context is identical everywhere,\n * down to how the client address is resolved. `headers` is materialized on\n * first read: a connection nobody inspects pays nothing for the conversion.\n *\n * @internal\n */\nexport function createAuthContext(options: {\n headers: RawHeaders,\n token?: string | null,\n remoteAddress?: string,\n req?: any,\n}): AuthContext {\n const source = options.headers;\n let headers: Headers | undefined;\n\n return {\n token: options.token ?? getBearerToken(readHeader(source, 'authorization')),\n ip: resolveClientIp(source, options.remoteAddress),\n req: options.req,\n get headers() {\n return headers ??= (source instanceof Headers)\n ? source\n : new Headers(source as Record<string, string>);\n },\n };\n}\n\n/**\n * A single address, resolved the same way on every transport: `x-forwarded-for`\n * carries the whole proxy chain, and only its first entry is the client.\n */\nfunction resolveClientIp(headers: RawHeaders, remoteAddress?: string): string | undefined {\n // an empty header counts as absent\n const firstHop = (name: string) => readHeader(headers, name)?.split(',')[0].trim() || undefined;\n\n return (\n firstHop('x-real-ip') ??\n firstHop('x-forwarded-for') ??\n firstHop('x-client-ip') ??\n (remoteAddress || undefined)\n );\n}\n\nexport type AuthContext = {\n token?: string,\n /** Undefined when no proxy header carries it and the transport has no peer address. */\n ip: string | undefined;\n headers: Headers,\n /** Only set on the HTTP matchmaking request, where it is the `Request` itself. */\n req?: any;\n};\n\nexport interface ISendOptions {\n afterNextPatch?: boolean;\n}\n\nexport const ClientState = {\n JOINING: 0,\n JOINED: 1,\n RECONNECTING: 2,\n RECONNECTED: 3,\n LEAVING: 4,\n CLOSED: 5\n} as const;\nexport type ClientState = (typeof ClientState)[keyof typeof ClientState];\n\n// Helper types to extract properties from the Client type parameter\ntype ExtractClientUserData<T> = T extends { userData: infer U } ? U : T;\ntype ExtractClientAuth<T> = T extends { auth: infer A } ? A : any;\ntype ExtractClientMessages<T> = T extends { messages: infer M } ? M : any;\n\n// Helper type to make message required when the message type demands it\nexport type MessageArgs<M, Options> =\n unknown extends M ? [message?: M, options?: Options] : // Handle 'any' type (backwards compatibility)\n [M] extends [never] ? [message?: M, options?: Options] :\n [M] extends [void] ? [message?: M, options?: Options] :\n [M] extends [undefined] ? [message?: M, options?: Options] :\n undefined extends M ? [message?: M, options?: Options] :\n [message: M, options?: Options];\n\n/**\n * The client instance from the server-side is responsible for the transport layer between the server and the client.\n * It should not be confused with the Client from the client-side SDK, as they have completely different purposes!\n * You operate on client instances from `this.clients`, `Room#onJoin()`, `Room#onLeave()` and `Room#onMessage()`.\n *\n * - This is the raw WebSocket connection coming from the `ws` package. There are more methods available which aren't\n * encouraged to use along with Colyseus.\n */\nexport interface Client<T extends { userData?: any, auth?: any, messages?: Record<string | number, any> } = any> {\n '~messages': ExtractClientMessages<T>;\n\n ref: EventEmitter;\n\n /**\n * Unique id per session.\n */\n sessionId: string;\n\n /**\n * Connection state\n */\n state: ClientState;\n\n /**\n * Optional: when using `@view()` decorator in your state properties, this will be the view instance for this client.\n */\n view?: StateView;\n\n /**\n * User-defined data can be attached to the Client instance through this variable.\n * - Can be used to store custom data about the client's connection. userData is not synchronized with the client,\n * and should be used only to keep player-specific with its connection.\n */\n userData?: ExtractClientUserData<T>;\n\n /**\n * auth data provided by your `onAuth`\n */\n auth?: ExtractClientAuth<T>;\n\n /**\n * Reconnection token used to re-join the room after onLeave + allowReconnection().\n *\n * IMPORTANT:\n * This is not the full reconnection token the client provides for the server.\n * The format provided by .reconnect() from the client-side must follow: \"${roomId}:${reconnectionToken}\"\n */\n reconnectionToken: string;\n\n // TODO: move these to ClientPrivate\n raw(data: Uint8Array | Buffer, options?: ISendOptions, cb?: (err?: Error) => void): void;\n enqueueRaw(data: Uint8Array | Buffer, options?: ISendOptions): void;\n\n /**\n * Send raw bytes over the transport's UNRELIABLE channel \u2014 no delivery,\n * ordering, or duplication guarantee. Used for `@unreliable` state patches.\n *\n * Absent on transports with no datagram channel (every WebSocket transport).\n * Its presence IS the capability check \u2014 callers feature-detect rather than\n * reading a separate flag, and skip clients that can't receive.\n */\n rawUnreliable?(data: Uint8Array | Buffer, options?: ISendOptions, cb?: (err?: Error) => void): void;\n\n /**\n * Send a type of message to the client. Messages are encoded with MsgPack and can hold any\n * JSON-serializable data structure.\n *\n * @param type String or Number identifier the client SDK will use to receive this message\n * @param message Message payload. (automatically encoded with msgpack.)\n * @param options\n */\n send<K extends keyof this['~messages']>(\n type: K,\n ...args: MessageArgs<this['~messages'][K], ISendOptions>\n ): void;\n\n /**\n * Send raw bytes to this specific client.\n *\n * @param type String or Number identifier the client SDK will use to receive this message\n * @param bytes Raw byte array payload\n * @param options\n */\n sendBytes(type: string | number, bytes: Buffer | Uint8Array, options?: ISendOptions): void;\n\n /**\n * Disconnect this client from the room.\n *\n * @param code Custom close code. Default value is 1000.\n * @param data\n * @see [Leave room](https://docs.colyseus.io/room#leave-room)\n */\n leave(code?: number, data?: string): void;\n\n /**\n * @deprecated Use .leave() instead.\n */\n close(code?: number, data?: string): void;\n\n /**\n * Triggers `onError` with specified code to the client-side.\n *\n * @param code\n * @param message\n */\n error(code: number, message?: string): void;\n}\n\n/**\n * Private properties of the Client instance.\n * Only accessible internally by the framework, should not be encouraged/auto-completed for the user.\n *\n * TODO: refactor this.\n * @private\n */\nexport interface ClientPrivate {\n readyState: number; // TODO: remove readyState on version 1.0.0. Use only \"state\" instead.\n _enqueuedMessages?: any[];\n _joinedAt: number; // \"elapsedTime\" when the client joined the room.\n\n /**\n * Rate limiting via maxMessagesPerSecond: frames seen so far, and the\n * moment the count goes back to zero.\n */\n _numMessagesLastSecond?: number;\n _messageCountResetsAt?: number;\n\n /**\n * Time of the most recent inbound frame. Moves on every frame \u2014 this is\n * the one to read for \"how long has this client been quiet?\" (see\n * IdleKickPlugin). The rate-limit counter above only resets once a second.\n */\n _lastMessageTime?: number;\n\n /**\n * Per-client input Schema instance, allocated on join when the Room\n * declares `input`. Mutated in-place by {@link _inputDecoder} on each\n * incoming ROOM_INPUT_* packet.\n *\n * Typed loosely (`any`) so duplicate `@colyseus/schema` installs don't\n * trigger type-identity errors against user-defined input classes.\n */\n _input?: any;\n _inputDecoder?: InputDecoder;\n\n /**\n * Per-client buffer of cloned input snapshots, allocated on join when\n * `Room.inputOptions.bufferMaxSize > 0`. Populated on each decoded frame.\n */\n _inputBuffer?: import('./input/InputBuffer.ts').InputBufferImpl;\n\n /**\n * Cached per-client accessor returned by `room.input(sessionId)`. Built\n * once at join (when the Room called `defineInput()`), so the public API\n * call is a Map lookup + property read with no per-call allocation.\n */\n _inputAccessor?: import('./input/types.ts').InputAccessor;\n\n /**\n * Used for rate limiting ROOM_INPUT_* packets via maxInputsPerSecond,\n * independent of maxMessagesPerSecond.\n */\n _numInputsLastSecond?: number;\n _lastInputTime?: number;\n\n /**\n * `performance.now()` recorded when the most recent ROOM_INPUT_* packet\n * from this client was received. Receive-side diagnostic only \u2014 NOT on the\n * wire: the {@link ProtocolModifier.TIMED} state prefix acks the seq of the\n * last input CONSUMED into the state (the input buffer's `ackSeq`).\n *\n * `0` until the client has sent its first input.\n */\n _lastInputReceivedAt?: number;\n\n /**\n * Monotonic count of *reliable* inputs successfully received from this\n * client. Receive-time counter \u2014 it LEADS the state by inputs still\n * buffered; what the TIMED prefix acks is the CONSUMED seq (the input\n * buffer's `ackSeq`), not this. Stays at the default `0` until the client\n * sends its first reliable input.\n *\n * Only ROOM_INPUT_RELIABLE bumps this \u2014 unreliable's redundant-ring\n * pattern would double-count.\n */\n _receivedInputCount?: number;\n\n /**\n * Running baseline for the DELTA-CODED lag-comp stamp on ROOM_INPUT_RELIABLE\n * frames (the {@link ProtocolModifier.TIMED} prefix). Each frame carries only\n * the signed change from the previous stamp; this accumulates them back into\n * the absolute timeline value. Zeroed on (re)connect alongside the SDK's own\n * baseline so the first delta after a reset is absolute. `0` until allocated.\n */\n _reckonBaseline?: number;\n\n /**\n * @internal Per-client raw frames staged to ride out right AFTER this client's\n * next state patch \u2014 per-client `afterNextPatch` messages. Lazily allocated.\n * Pushed by {@link enqueueClientRaw} (the `afterNextPatch` path), flushed as\n * standalone frames after the patch by {@link Room._flushPendingClientFrames}.\n * Room-level `broadcast` `afterNextPatch` uses the Room's own queue instead, not\n * this buffer.\n */\n _pendingFrames?: Uint8Array[];\n\n /**\n * @internal Back-reference to the Room's \"clients with staged frames\" list,\n * shared by reference at join. {@link enqueueClientRaw} pushes the client here\n * on its first staged frame of a cycle, so the after-patch flush iterates just\n * those clients rather than scanning every client.\n */\n _pendingFrameClients?: Array<Client & ClientPrivate>;\n}\n\n/**\n * The framework-level send path shared by every transport's `enqueueRaw` \u2014 the\n * single source of truth so each transport implements only the wire-level `raw`.\n * Routes a raw frame by where it should go:\n *\n * - `afterNextPatch` \u2192 stage onto the per-client {@link ClientPrivate._pendingFrames}\n * buffer, sent as a standalone frame right AFTER the next state patch; a\n * no-allocation push into a reused array. The client announces itself to the\n * Room's {@link ClientPrivate._pendingFrameClients} list on its first staged\n * frame of a cycle.\n * - before JOIN \u2192 buffer in `_enqueuedMessages` until the JOIN_ROOM handshake flushes.\n * - otherwise \u2192 send now via the transport's `raw`.\n *\n * @internal\n */\nexport function enqueueClientRaw(\n client: Client & ClientPrivate,\n data: Uint8Array | Buffer,\n options?: ISendOptions,\n): void {\n if (options?.afterNextPatch) {\n let frames = client._pendingFrames;\n if (frames === undefined) { frames = client._pendingFrames = []; }\n if (frames.length === 0) { client._pendingFrameClients?.push(client); } // first frame this cycle\n frames.push(data);\n return;\n }\n if (client.state !== ClientState.JOINED) {\n // During `onJoin` / `onReconnect` the client can't register onMessage\n // handlers yet \u2014 buffer until JOIN_ROOM has been sent.\n client._enqueuedMessages?.push(data);\n return;\n }\n client.raw(data, options);\n}\n\nexport class ClientArray<C extends Client = Client> extends Array<C> {\n /**\n * Secondary index for O(1) lookup by sessionId. Kept in sync by the\n * mutating methods overridden below. Direct index assignment\n * (`arr[i] = client`) and `arr.length = 0` bypass this index \u2014 use\n * `push` / `splice` / `delete` / `pop` / `shift` / `unshift` instead.\n */\n private _byId: Map<string, C> = new Map();\n\n /** The client for `sessionId`, or `undefined` \u2014 O(1). The canonical per-session\n * lookup (mirrors `room.inputs.get(sessionId)`). */\n public get(sessionId: string): C | undefined {\n return this._byId.get(sessionId);\n }\n\n /** @deprecated Use {@link get}. */\n public getById(sessionId: string): C | undefined {\n return this._byId.get(sessionId);\n }\n\n public delete(client: C): boolean {\n const removed = spliceOne(this, this.indexOf(client));\n if (removed) this._byId.delete(client.sessionId);\n return removed;\n }\n\n public push(...items: C[]): number {\n for (let i = 0; i < items.length; i++) this._byId.set(items[i].sessionId, items[i]);\n return super.push(...items);\n }\n\n public pop(): C | undefined {\n const removed = super.pop();\n if (removed !== undefined) this._byId.delete(removed.sessionId);\n return removed;\n }\n\n public shift(): C | undefined {\n const removed = super.shift();\n if (removed !== undefined) this._byId.delete(removed.sessionId);\n return removed;\n }\n\n public unshift(...items: C[]): number {\n for (let i = 0; i < items.length; i++) this._byId.set(items[i].sessionId, items[i]);\n return super.unshift(...items);\n }\n\n public splice(start: number, deleteCount?: number, ...items: C[]): C[] {\n const removed = (deleteCount === undefined)\n ? super.splice(start)\n : super.splice(start, deleteCount, ...items);\n for (let i = 0; i < removed.length; i++) this._byId.delete(removed[i].sessionId);\n for (let i = 0; i < items.length; i++) this._byId.set(items[i].sessionId, items[i]);\n return removed;\n }\n}\n\n/**\n * Shared internal method to connect a Client into a Room.\n * Validates seat reservation and joins the client to the room.\n *\n * @remarks\n * **\u26A0\uFE0F This is an internal API and not intended for end-user use.**\n *\n * @internal\n */\nexport async function connectClientToRoom(\n room: Room | undefined,\n client: Client & ClientPrivate,\n authContext: AuthContext,\n connectionOptions: {\n reconnectionToken?: string;\n skipHandshake?: boolean;\n },\n): Promise<void> {\n if (!room || !room.hasReservedSeat(client.sessionId, connectionOptions.reconnectionToken)) {\n throw new ServerError(ErrorCode.MATCHMAKE_EXPIRED, 'seat reservation expired.');\n }\n\n await room['_onJoin'](client, authContext, connectionOptions);\n}"],
|
|
5
|
-
"mappings": ";;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,WAAsB;AACtB,YAAuB;AAIvB,0BAA0B;AAC1B,oBAA0B;AAG1B,oBAA6B;AAC7B,mBAAmC;AACnC,mBAA0C;AAC1C,yBAA4B;AAI5B,IAAI;AACG,SAAS,aAAa,WAAsB;AAAE,eAAa;AAAW;AACtE,SAAS,eAAe;AAAE,SAAO;AAAY;AAE7C,IAAe,YAAf,MAAyB;AA2BhC;AAyCA,eAAsB,iBACpB,SACA,KACA,SAC+B;AAC/B,MAAI;AAEJ,MAAI;AACF,UAAM,OAAO,QAAQ,QAAQ,IAAI,MAAM,KAAK;AAC5C,cAAU,IAAI,QAAQ,UAAU,IAAI,GAAG,GAAG,IAAI,EAAE,SAAS,QAAQ,QAAQ,CAAC;AAAA,EAE5E,SAAS,GAAQ;AAEf,yCAAmB,CAAC;AACpB,WAAO,IAAI,SAAS,MAAM,EAAE,QAAQ,IAAI,CAAC;AAAA,EAC3C;AAEA,MAAI;AAEF,WAAQ,MAAM,QAAQ,SAAS,OAAO,KAAM;AAAA,EAE9C,SAAS,GAAQ;AACf,yCAAmB,CAAC;AACpB,WAAO,IAAI,SAAS,MAAM,EAAE,QAAQ,IAAI,CAAC;AAAA,EAC3C;AACF;AAKA,IAAM,aAAa,CAAC,SAAqB,SACtC,mBAAmB,UAAW,QAAQ,IAAI,IAAI,IAAI,QAAQ,IAAI;AAW1D,SAAS,kBAAkB,SAKlB;AACd,QAAM,SAAS,QAAQ;AACvB,MAAI;AAEJ,SAAO;AAAA,IACL,OAAO,QAAQ,aAAS,6BAAe,WAAW,QAAQ,eAAe,CAAC;AAAA,IAC1E,IAAI,gBAAgB,QAAQ,QAAQ,aAAa;AAAA,IACjD,KAAK,QAAQ;AAAA,IACb,IAAI,UAAU;AACZ,aAAO,YAAa,kBAAkB,UAClC,SACA,IAAI,QAAQ,MAAgC;AAAA,IAClD;AAAA,EACF;AACF;AAMA,SAAS,gBAAgB,SAAqB,eAA4C;AAExF,QAAM,WAAW,CAAC,SAAiB,WAAW,SAAS,IAAI,GAAG,MAAM,GAAG,EAAE,CAAC,EAAE,KAAK,KAAK;AAEtF,SACE,SAAS,WAAW,KACpB,SAAS,iBAAiB,KAC1B,SAAS,aAAa,MACrB,iBAAiB;AAEtB;AAeO,IAAM,cAAc;AAAA,EACzB,SAAS;AAAA,EACT,QAAQ;AAAA,EACR,cAAc;AAAA,EACd,aAAa;AAAA,EACb,SAAS;AAAA,EACT,QAAQ;AACV;
|
|
4
|
+
"sourcesContent": ["import * as http from 'http';\nimport * as https from 'https';\n\nimport type { Router } from '@colyseus/better-call';\n\nimport { ErrorCode } from '@colyseus/shared-types';\nimport { StateView } from '@colyseus/schema';\nimport type { InputDecoder } from '@colyseus/schema/input';\n\nimport { EventEmitter } from 'events';\nimport { debugAndPrintError } from './Debug.ts';\nimport { getBearerToken, spliceOne } from './utils/Utils.ts';\nimport { ServerError } from './errors/ServerError.ts';\n\nimport type { Room } from './Room.ts';\n\nlet _transport: Transport | undefined;\nexport function setTransport(transport: Transport) { _transport = transport; }\nexport function getTransport() { return _transport; }\n\nexport abstract class Transport {\n public protocol?: string;\n /** Self-signed cert SHA-256 hash (byte array), surfaced to clients in the\n * matchmake response so a WebTransport client can pin it via\n * `serverCertificateHashes`. Set by transports that generate their own cert\n * (h3). Undefined for transports using a CA-trusted cert. */\n public fingerprint?: number[];\n public server?: http.Server | https.Server;\n\n public abstract listen(port?: number | string, hostname?: string, backlog?: number, listeningListener?: Function): this;\n public abstract shutdown(): void;\n\n public abstract simulateLatency(milliseconds: number): void;\n\n /**\n * Returns an Express-compatible application for HTTP route handling.\n * For uWebSockets transport, this uses the uwebsockets-express module.\n * This method is called lazily only when an express callback is provided in server options.\n */\n public getExpressApp?(): Promise<import('express').Application> | import('express').Application | undefined;\n\n /**\n * Binds a router to the transport.\n * Some transports may have a custom way to bind a router to the transport.\n * (uWebSocketsTransport)\n */\n public bindRouter?(router: Router): void;\n}\n\n/**\n * Intercepts an incoming WebSocket upgrade request, before the handshake.\n *\n * Return a `Response` to answer the request instead of upgrading it. Return\n * nothing to upgrade as usual. The handler may be async, and the handshake waits\n * for it to resolve.\n *\n * `context` is the same shape `onAuth()` receives, read-only here: mutating it\n * does not carry over to `onAuth()`.\n *\n * Not supported by `H3Transport`: WebTransport has no upgrade handshake.\n *\n * @example\n * ```typescript\n * new uWebSocketsTransport({\n * beforeUpgrade: async (request, context) => {\n * if (await isBanned(context.ip)) {\n * return new Response(null, { status: 403 });\n * }\n * }\n * });\n * ```\n */\nexport type BeforeUpgradeHandler = (\n request: Request,\n context: Readonly<AuthContext>,\n) => Response | void | Promise<Response | void>;\n\n/**\n * Invokes a `beforeUpgrade` handler, resolving with the `Response` to send\n * instead of upgrading, or `undefined` to proceed with the upgrade.\n *\n * Every transport goes through here, so a handler written against one keeps\n * working on the others. Never rejects: uWebSockets.js aborts the process on an\n * upgrade handler that yields without responding, and on the other transports a\n * raw socket left behind leaks a connection.\n *\n * @internal\n */\nexport async function runBeforeUpgrade(\n handler: BeforeUpgradeHandler,\n url: string, // path, optionally including the query string\n context: AuthContext,\n): Promise<Response | undefined> {\n let request: Request;\n\n try {\n const host = context.headers.get('host') || 'localhost';\n request = new Request(`http://${host}${url}`, { headers: context.headers });\n\n } catch (e: any) {\n // a `Host` header that isn't a valid authority fails to parse as a URL\n debugAndPrintError(e);\n return new Response(null, { status: 400 });\n }\n\n try {\n // `||`, not `??`: a handler that returns nothing is typed `void`, which `??` doesn't strip\n return (await handler(request, context)) || undefined;\n\n } catch (e: any) {\n debugAndPrintError(e);\n return new Response(null, { status: 500 });\n }\n}\n\n/** Headers as the transport has them: uWebSockets.js and Node give a plain record. */\ntype RawHeaders = Headers | Record<string, string | undefined>;\n\nconst readHeader = (headers: RawHeaders, name: string) =>\n (headers instanceof Headers) ? headers.get(name) : headers[name];\n\n/**\n * Builds the context passed to `beforeUpgrade` and `onAuth`.\n *\n * Every transport goes through here, so the context is identical everywhere,\n * down to how the client address is resolved. `headers` is materialized on\n * first read: a connection nobody inspects pays nothing for the conversion.\n *\n * @internal\n */\nexport function createAuthContext(options: {\n headers: RawHeaders,\n token?: string | null,\n remoteAddress?: string,\n req?: any,\n}): AuthContext {\n const source = options.headers;\n let headers: Headers | undefined;\n\n return {\n token: options.token ?? getBearerToken(readHeader(source, 'authorization')),\n ip: resolveClientIp(source, options.remoteAddress),\n req: options.req,\n get headers() {\n return headers ??= (source instanceof Headers)\n ? source\n : new Headers(source as Record<string, string>);\n },\n };\n}\n\n/**\n * A single address, resolved the same way on every transport: `x-forwarded-for`\n * carries the whole proxy chain, and only its first entry is the client.\n */\nfunction resolveClientIp(headers: RawHeaders, remoteAddress?: string): string | undefined {\n // an empty header counts as absent\n const firstHop = (name: string) => readHeader(headers, name)?.split(',')[0].trim() || undefined;\n\n return (\n firstHop('x-real-ip') ??\n firstHop('x-forwarded-for') ??\n firstHop('x-client-ip') ??\n (remoteAddress || undefined)\n );\n}\n\nexport type AuthContext = {\n token?: string,\n /** Undefined when no proxy header carries it and the transport has no peer address. */\n ip: string | undefined;\n headers: Headers,\n /** Only set on the HTTP matchmaking request, where it is the `Request` itself. */\n req?: any;\n};\n\nexport interface ISendOptions {\n afterNextPatch?: boolean;\n}\n\nexport const ClientState = {\n JOINING: 0,\n JOINED: 1,\n RECONNECTING: 2,\n RECONNECTED: 3,\n LEAVING: 4,\n CLOSED: 5\n} as const;\nexport type ClientState = (typeof ClientState)[keyof typeof ClientState];\n\n// Helper types to extract properties from the Client type parameter\ntype ExtractClientUserData<T> = T extends { userData: infer U } ? U : T;\ntype ExtractClientAuth<T> = T extends { auth: infer A } ? A : any;\ntype ExtractClientMessages<T> = T extends { messages: infer M } ? M : any;\n\n// Helper type to make message required when the message type demands it\nexport type MessageArgs<M, Options> =\n unknown extends M ? [message?: M, options?: Options] : // Handle 'any' type (backwards compatibility)\n [M] extends [never] ? [message?: M, options?: Options] :\n [M] extends [void] ? [message?: M, options?: Options] :\n [M] extends [undefined] ? [message?: M, options?: Options] :\n undefined extends M ? [message?: M, options?: Options] :\n [message: M, options?: Options];\n\n/**\n * The client instance from the server-side is responsible for the transport layer between the server and the client.\n * It should not be confused with the Client from the client-side SDK, as they have completely different purposes!\n * You operate on client instances from `this.clients`, `Room#onJoin()`, `Room#onLeave()` and `Room#onMessage()`.\n *\n * - This is the raw WebSocket connection coming from the `ws` package. There are more methods available which aren't\n * encouraged to use along with Colyseus.\n */\nexport interface Client<T extends { userData?: any, auth?: any, messages?: Record<string | number, any> } = any> {\n '~messages': ExtractClientMessages<T>;\n\n ref: EventEmitter;\n\n /**\n * Unique id per session.\n */\n sessionId: string;\n\n /**\n * Connection state\n */\n state: ClientState;\n\n /**\n * Optional: when using `@view()` decorator in your state properties, this will be the view instance for this client.\n */\n view?: StateView;\n\n /**\n * User-defined data can be attached to the Client instance through this variable.\n * - Can be used to store custom data about the client's connection. userData is not synchronized with the client,\n * and should be used only to keep player-specific with its connection.\n */\n userData?: ExtractClientUserData<T>;\n\n /**\n * auth data provided by your `onAuth`\n */\n auth?: ExtractClientAuth<T>;\n\n /**\n * Reconnection token used to re-join the room after onLeave + allowReconnection().\n *\n * IMPORTANT:\n * This is not the full reconnection token the client provides for the server.\n * The format provided by .reconnect() from the client-side must follow: \"${roomId}:${reconnectionToken}\"\n */\n reconnectionToken: string;\n\n // TODO: move these to ClientPrivate\n /**\n * Send raw bytes over the transport's reliable channel.\n *\n * The transport owns `data` from this call on: it may write it later (a\n * socket with backpressure, permessage-deflate), so callers never mutate it\n * afterwards. The same `data` may be handed to several clients.\n */\n raw(data: Uint8Array | Buffer, options?: ISendOptions, cb?: (err?: Error) => void): void;\n /**\n * {@link raw}, or held until the client finishes joining (or until after the\n * next patch, with `afterNextPatch`). Owns `data` the same way {@link raw} does.\n */\n enqueueRaw(data: Uint8Array | Buffer, options?: ISendOptions): void;\n\n /**\n * Send raw bytes over the transport's UNRELIABLE channel \u2014 no delivery,\n * ordering, or duplication guarantee. Used for `@unreliable` state patches.\n *\n * Absent on transports with no datagram channel (every WebSocket transport).\n * Its presence IS the capability check \u2014 callers feature-detect rather than\n * reading a separate flag, and skip clients that can't receive.\n *\n * Unlike {@link raw}, `data` is only valid during this call: the caller\n * reuses its bytes afterwards, so a transport that sends it later copies it\n * first.\n */\n rawUnreliable?(data: Uint8Array | Buffer, options?: ISendOptions, cb?: (err?: Error) => void): void;\n\n /**\n * Send a type of message to the client. Messages are encoded with MsgPack and can hold any\n * JSON-serializable data structure.\n *\n * @param type String or Number identifier the client SDK will use to receive this message\n * @param message Message payload. (automatically encoded with msgpack.)\n * @param options\n */\n send<K extends keyof this['~messages']>(\n type: K,\n ...args: MessageArgs<this['~messages'][K], ISendOptions>\n ): void;\n\n /**\n * Send raw bytes to this specific client.\n *\n * @param type String or Number identifier the client SDK will use to receive this message\n * @param bytes Raw byte array payload\n * @param options\n */\n sendBytes(type: string | number, bytes: Buffer | Uint8Array, options?: ISendOptions): void;\n\n /**\n * Disconnect this client from the room.\n *\n * @param code Custom close code. Default value is 1000.\n * @param data\n * @see [Leave room](https://docs.colyseus.io/room#leave-room)\n */\n leave(code?: number, data?: string): void;\n\n /**\n * @deprecated Use .leave() instead.\n */\n close(code?: number, data?: string): void;\n\n /**\n * Triggers `onError` with specified code to the client-side.\n *\n * @param code\n * @param message\n */\n error(code: number, message?: string): void;\n}\n\n/**\n * Private properties of the Client instance.\n * Only accessible internally by the framework, should not be encouraged/auto-completed for the user.\n *\n * TODO: refactor this.\n * @private\n */\nexport interface ClientPrivate {\n readyState: number; // TODO: remove readyState on version 1.0.0. Use only \"state\" instead.\n _enqueuedMessages?: any[];\n _joinedAt: number; // \"elapsedTime\" when the client joined the room.\n\n /**\n * Rate limiting via maxMessagesPerSecond: frames seen so far, and the\n * moment the count goes back to zero.\n */\n _numMessagesLastSecond?: number;\n _messageCountResetsAt?: number;\n\n /**\n * Time of the most recent inbound frame. Moves on every frame \u2014 this is\n * the one to read for \"how long has this client been quiet?\" (see\n * IdleKickPlugin). The rate-limit counter above only resets once a second.\n */\n _lastMessageTime?: number;\n\n /**\n * Per-client input Schema instance, allocated on join when the Room\n * declares `input`. Mutated in-place by {@link _inputDecoder} on each\n * incoming ROOM_INPUT_* packet.\n *\n * Typed loosely (`any`) so duplicate `@colyseus/schema` installs don't\n * trigger type-identity errors against user-defined input classes.\n */\n _input?: any;\n _inputDecoder?: InputDecoder;\n\n /**\n * Per-client buffer of cloned input snapshots, allocated on join when\n * `Room.inputOptions.bufferMaxSize > 0`. Populated on each decoded frame.\n */\n _inputBuffer?: import('./input/InputBuffer.ts').InputBufferImpl;\n\n /**\n * Cached per-client accessor returned by `room.input(sessionId)`. Built\n * once at join (when the Room called `defineInput()`), so the public API\n * call is a Map lookup + property read with no per-call allocation.\n */\n _inputAccessor?: import('./input/types.ts').InputAccessor;\n\n /**\n * Used for rate limiting ROOM_INPUT_* packets via maxInputsPerSecond,\n * independent of maxMessagesPerSecond.\n */\n _numInputsLastSecond?: number;\n _lastInputTime?: number;\n\n /**\n * `performance.now()` recorded when the most recent ROOM_INPUT_* packet\n * from this client was received. Receive-side diagnostic only \u2014 NOT on the\n * wire: the {@link ProtocolModifier.TIMED} state prefix acks the seq of the\n * last input CONSUMED into the state (the input buffer's `ackSeq`).\n *\n * `0` until the client has sent its first input.\n */\n _lastInputReceivedAt?: number;\n\n /**\n * Monotonic count of *reliable* inputs successfully received from this\n * client. Receive-time counter \u2014 it LEADS the state by inputs still\n * buffered; what the TIMED prefix acks is the CONSUMED seq (the input\n * buffer's `ackSeq`), not this. Stays at the default `0` until the client\n * sends its first reliable input.\n *\n * Only ROOM_INPUT_RELIABLE bumps this \u2014 unreliable's redundant-ring\n * pattern would double-count.\n */\n _receivedInputCount?: number;\n\n /**\n * Running baseline for the DELTA-CODED lag-comp stamp on ROOM_INPUT_RELIABLE\n * frames (the {@link ProtocolModifier.TIMED} prefix). Each frame carries only\n * the signed change from the previous stamp; this accumulates them back into\n * the absolute timeline value. Zeroed on (re)connect alongside the SDK's own\n * baseline so the first delta after a reset is absolute. `0` until allocated.\n */\n _reckonBaseline?: number;\n\n /**\n * @internal Per-client raw frames staged to ride out right AFTER this client's\n * next state patch \u2014 per-client `afterNextPatch` messages. Lazily allocated.\n * Pushed by {@link enqueueClientRaw} (the `afterNextPatch` path), flushed as\n * standalone frames after the patch by {@link Room._flushPendingClientFrames}.\n * Room-level `broadcast` `afterNextPatch` uses the Room's own queue instead, not\n * this buffer.\n */\n _pendingFrames?: Uint8Array[];\n\n /**\n * @internal Back-reference to the Room's \"clients with staged frames\" list,\n * shared by reference at join. {@link enqueueClientRaw} pushes the client here\n * on its first staged frame of a cycle, so the after-patch flush iterates just\n * those clients rather than scanning every client.\n */\n _pendingFrameClients?: Array<Client & ClientPrivate>;\n}\n\n/**\n * The framework-level send path shared by every transport's `enqueueRaw` \u2014 the\n * single source of truth so each transport implements only the wire-level `raw`.\n * Routes a raw frame by where it should go:\n *\n * - `afterNextPatch` \u2192 stage onto the per-client {@link ClientPrivate._pendingFrames}\n * buffer, sent as a standalone frame right AFTER the next state patch; a\n * no-allocation push into a reused array. The client announces itself to the\n * Room's {@link ClientPrivate._pendingFrameClients} list on its first staged\n * frame of a cycle.\n * - before JOIN \u2192 buffer in `_enqueuedMessages` until the JOIN_ROOM handshake flushes.\n * - otherwise \u2192 send now via the transport's `raw`.\n *\n * @internal\n */\nexport function enqueueClientRaw(\n client: Client & ClientPrivate,\n data: Uint8Array | Buffer,\n options?: ISendOptions,\n): void {\n if (options?.afterNextPatch) {\n let frames = client._pendingFrames;\n if (frames === undefined) { frames = client._pendingFrames = []; }\n if (frames.length === 0) { client._pendingFrameClients?.push(client); } // first frame this cycle\n frames.push(data);\n return;\n }\n if (client.state !== ClientState.JOINED) {\n // During `onJoin` / `onReconnect` the client can't register onMessage\n // handlers yet \u2014 buffer until JOIN_ROOM has been sent.\n client._enqueuedMessages?.push(data);\n return;\n }\n client.raw(data, options);\n}\n\nexport class ClientArray<C extends Client = Client> extends Array<C> {\n /**\n * Secondary index for O(1) lookup by sessionId. Kept in sync by the\n * mutating methods overridden below. Direct index assignment\n * (`arr[i] = client`) and `arr.length = 0` bypass this index \u2014 use\n * `push` / `splice` / `delete` / `pop` / `shift` / `unshift` instead.\n */\n private _byId: Map<string, C> = new Map();\n\n /** The client for `sessionId`, or `undefined` \u2014 O(1). The canonical per-session\n * lookup (mirrors `room.inputs.get(sessionId)`). */\n public get(sessionId: string): C | undefined {\n return this._byId.get(sessionId);\n }\n\n /** @deprecated Use {@link get}. */\n public getById(sessionId: string): C | undefined {\n return this._byId.get(sessionId);\n }\n\n public delete(client: C): boolean {\n const removed = spliceOne(this, this.indexOf(client));\n if (removed) this._byId.delete(client.sessionId);\n return removed;\n }\n\n public push(...items: C[]): number {\n for (let i = 0; i < items.length; i++) this._byId.set(items[i].sessionId, items[i]);\n return super.push(...items);\n }\n\n public pop(): C | undefined {\n const removed = super.pop();\n if (removed !== undefined) this._byId.delete(removed.sessionId);\n return removed;\n }\n\n public shift(): C | undefined {\n const removed = super.shift();\n if (removed !== undefined) this._byId.delete(removed.sessionId);\n return removed;\n }\n\n public unshift(...items: C[]): number {\n for (let i = 0; i < items.length; i++) this._byId.set(items[i].sessionId, items[i]);\n return super.unshift(...items);\n }\n\n public splice(start: number, deleteCount?: number, ...items: C[]): C[] {\n const removed = (deleteCount === undefined)\n ? super.splice(start)\n : super.splice(start, deleteCount, ...items);\n for (let i = 0; i < removed.length; i++) this._byId.delete(removed[i].sessionId);\n for (let i = 0; i < items.length; i++) this._byId.set(items[i].sessionId, items[i]);\n return removed;\n }\n}\n\n/**\n * Shared internal method to connect a Client into a Room.\n * Validates seat reservation and joins the client to the room.\n *\n * @remarks\n * **\u26A0\uFE0F This is an internal API and not intended for end-user use.**\n *\n * @internal\n */\nexport async function connectClientToRoom(\n room: Room | undefined,\n client: Client & ClientPrivate,\n authContext: AuthContext,\n connectionOptions: {\n reconnectionToken?: string;\n skipHandshake?: boolean;\n },\n): Promise<void> {\n if (!room || !room.hasReservedSeat(client.sessionId, connectionOptions.reconnectionToken)) {\n throw new ServerError(ErrorCode.MATCHMAKE_EXPIRED, 'seat reservation expired.');\n }\n\n await room['_onJoin'](client, authContext, connectionOptions);\n}"],
|
|
5
|
+
"mappings": ";;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,WAAsB;AACtB,YAAuB;AAIvB,0BAA0B;AAC1B,oBAA0B;AAG1B,oBAA6B;AAC7B,mBAAmC;AACnC,mBAA0C;AAC1C,yBAA4B;AAI5B,IAAI;AACG,SAAS,aAAa,WAAsB;AAAE,eAAa;AAAW;AACtE,SAAS,eAAe;AAAE,SAAO;AAAY;AAE7C,IAAe,YAAf,MAAyB;AA2BhC;AAyCA,eAAsB,iBACpB,SACA,KACA,SAC+B;AAC/B,MAAI;AAEJ,MAAI;AACF,UAAM,OAAO,QAAQ,QAAQ,IAAI,MAAM,KAAK;AAC5C,cAAU,IAAI,QAAQ,UAAU,IAAI,GAAG,GAAG,IAAI,EAAE,SAAS,QAAQ,QAAQ,CAAC;AAAA,EAE5E,SAAS,GAAQ;AAEf,yCAAmB,CAAC;AACpB,WAAO,IAAI,SAAS,MAAM,EAAE,QAAQ,IAAI,CAAC;AAAA,EAC3C;AAEA,MAAI;AAEF,WAAQ,MAAM,QAAQ,SAAS,OAAO,KAAM;AAAA,EAE9C,SAAS,GAAQ;AACf,yCAAmB,CAAC;AACpB,WAAO,IAAI,SAAS,MAAM,EAAE,QAAQ,IAAI,CAAC;AAAA,EAC3C;AACF;AAKA,IAAM,aAAa,CAAC,SAAqB,SACtC,mBAAmB,UAAW,QAAQ,IAAI,IAAI,IAAI,QAAQ,IAAI;AAW1D,SAAS,kBAAkB,SAKlB;AACd,QAAM,SAAS,QAAQ;AACvB,MAAI;AAEJ,SAAO;AAAA,IACL,OAAO,QAAQ,aAAS,6BAAe,WAAW,QAAQ,eAAe,CAAC;AAAA,IAC1E,IAAI,gBAAgB,QAAQ,QAAQ,aAAa;AAAA,IACjD,KAAK,QAAQ;AAAA,IACb,IAAI,UAAU;AACZ,aAAO,YAAa,kBAAkB,UAClC,SACA,IAAI,QAAQ,MAAgC;AAAA,IAClD;AAAA,EACF;AACF;AAMA,SAAS,gBAAgB,SAAqB,eAA4C;AAExF,QAAM,WAAW,CAAC,SAAiB,WAAW,SAAS,IAAI,GAAG,MAAM,GAAG,EAAE,CAAC,EAAE,KAAK,KAAK;AAEtF,SACE,SAAS,WAAW,KACpB,SAAS,iBAAiB,KAC1B,SAAS,aAAa,MACrB,iBAAiB;AAEtB;AAeO,IAAM,cAAc;AAAA,EACzB,SAAS;AAAA,EACT,QAAQ;AAAA,EACR,cAAc;AAAA,EACd,aAAa;AAAA,EACb,SAAS;AAAA,EACT,QAAQ;AACV;AAsQO,SAAS,iBACd,QACA,MACA,SACM;AACN,MAAI,SAAS,gBAAgB;AAC3B,QAAI,SAAS,OAAO;AACpB,QAAI,WAAW,QAAW;AAAE,eAAS,OAAO,iBAAiB,CAAC;AAAA,IAAG;AACjE,QAAI,OAAO,WAAW,GAAG;AAAE,aAAO,sBAAsB,KAAK,MAAM;AAAA,IAAG;AACtE,WAAO,KAAK,IAAI;AAChB;AAAA,EACF;AACA,MAAI,OAAO,UAAU,YAAY,QAAQ;AAGvC,WAAO,mBAAmB,KAAK,IAAI;AACnC;AAAA,EACF;AACA,SAAO,IAAI,MAAM,OAAO;AAC1B;AAEO,IAAM,cAAN,cAAqD,MAAS;AAAA,EAA9D;AAAA;AAOL;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,SAAQ,QAAwB,oBAAI,IAAI;AAAA;AAAA;AAAA;AAAA,EAIjC,IAAI,WAAkC;AAC3C,WAAO,KAAK,MAAM,IAAI,SAAS;AAAA,EACjC;AAAA;AAAA,EAGO,QAAQ,WAAkC;AAC/C,WAAO,KAAK,MAAM,IAAI,SAAS;AAAA,EACjC;AAAA,EAEO,OAAO,QAAoB;AAChC,UAAM,cAAU,wBAAU,MAAM,KAAK,QAAQ,MAAM,CAAC;AACpD,QAAI,QAAS,MAAK,MAAM,OAAO,OAAO,SAAS;AAC/C,WAAO;AAAA,EACT;AAAA,EAEO,QAAQ,OAAoB;AACjC,aAAS,IAAI,GAAG,IAAI,MAAM,QAAQ,IAAK,MAAK,MAAM,IAAI,MAAM,CAAC,EAAE,WAAW,MAAM,CAAC,CAAC;AAClF,WAAO,MAAM,KAAK,GAAG,KAAK;AAAA,EAC5B;AAAA,EAEO,MAAqB;AAC1B,UAAM,UAAU,MAAM,IAAI;AAC1B,QAAI,YAAY,OAAW,MAAK,MAAM,OAAO,QAAQ,SAAS;AAC9D,WAAO;AAAA,EACT;AAAA,EAEO,QAAuB;AAC5B,UAAM,UAAU,MAAM,MAAM;AAC5B,QAAI,YAAY,OAAW,MAAK,MAAM,OAAO,QAAQ,SAAS;AAC9D,WAAO;AAAA,EACT;AAAA,EAEO,WAAW,OAAoB;AACpC,aAAS,IAAI,GAAG,IAAI,MAAM,QAAQ,IAAK,MAAK,MAAM,IAAI,MAAM,CAAC,EAAE,WAAW,MAAM,CAAC,CAAC;AAClF,WAAO,MAAM,QAAQ,GAAG,KAAK;AAAA,EAC/B;AAAA,EAEO,OAAO,OAAe,gBAAyB,OAAiB;AACrE,UAAM,UAAW,gBAAgB,SAC7B,MAAM,OAAO,KAAK,IAClB,MAAM,OAAO,OAAO,aAAa,GAAG,KAAK;AAC7C,aAAS,IAAI,GAAG,IAAI,QAAQ,QAAQ,IAAK,MAAK,MAAM,OAAO,QAAQ,CAAC,EAAE,SAAS;AAC/E,aAAS,IAAI,GAAG,IAAI,MAAM,QAAQ,IAAK,MAAK,MAAM,IAAI,MAAM,CAAC,EAAE,WAAW,MAAM,CAAC,CAAC;AAClF,WAAO;AAAA,EACT;AACF;AAWA,eAAsB,oBACpB,MACA,QACA,aACA,mBAIe;AACf,MAAI,CAAC,QAAQ,CAAC,KAAK,gBAAgB,OAAO,WAAW,kBAAkB,iBAAiB,GAAG;AACzF,UAAM,IAAI,+BAAY,8BAAU,mBAAmB,2BAA2B;AAAA,EAChF;AAEA,QAAM,KAAK,SAAS,EAAE,QAAQ,aAAa,iBAAiB;AAC9D;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/build/Transport.d.ts
CHANGED
|
@@ -169,7 +169,18 @@ export interface Client<T extends {
|
|
|
169
169
|
* The format provided by .reconnect() from the client-side must follow: "${roomId}:${reconnectionToken}"
|
|
170
170
|
*/
|
|
171
171
|
reconnectionToken: string;
|
|
172
|
+
/**
|
|
173
|
+
* Send raw bytes over the transport's reliable channel.
|
|
174
|
+
*
|
|
175
|
+
* The transport owns `data` from this call on: it may write it later (a
|
|
176
|
+
* socket with backpressure, permessage-deflate), so callers never mutate it
|
|
177
|
+
* afterwards. The same `data` may be handed to several clients.
|
|
178
|
+
*/
|
|
172
179
|
raw(data: Uint8Array | Buffer, options?: ISendOptions, cb?: (err?: Error) => void): void;
|
|
180
|
+
/**
|
|
181
|
+
* {@link raw}, or held until the client finishes joining (or until after the
|
|
182
|
+
* next patch, with `afterNextPatch`). Owns `data` the same way {@link raw} does.
|
|
183
|
+
*/
|
|
173
184
|
enqueueRaw(data: Uint8Array | Buffer, options?: ISendOptions): void;
|
|
174
185
|
/**
|
|
175
186
|
* Send raw bytes over the transport's UNRELIABLE channel — no delivery,
|
|
@@ -178,6 +189,10 @@ export interface Client<T extends {
|
|
|
178
189
|
* Absent on transports with no datagram channel (every WebSocket transport).
|
|
179
190
|
* Its presence IS the capability check — callers feature-detect rather than
|
|
180
191
|
* reading a separate flag, and skip clients that can't receive.
|
|
192
|
+
*
|
|
193
|
+
* Unlike {@link raw}, `data` is only valid during this call: the caller
|
|
194
|
+
* reuses its bytes afterwards, so a transport that sends it later copies it
|
|
195
|
+
* first.
|
|
181
196
|
*/
|
|
182
197
|
rawUnreliable?(data: Uint8Array | Buffer, options?: ISendOptions, cb?: (err?: Error) => void): void;
|
|
183
198
|
/**
|