@colyseus/core 0.17.42 → 0.18.0

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.
Files changed (90) hide show
  1. package/build/MatchMaker.cjs +19 -6
  2. package/build/MatchMaker.cjs.map +2 -2
  3. package/build/MatchMaker.d.ts +10 -0
  4. package/build/MatchMaker.mjs +18 -6
  5. package/build/MatchMaker.mjs.map +2 -2
  6. package/build/Protocol.cjs +102 -37
  7. package/build/Protocol.cjs.map +2 -2
  8. package/build/Protocol.d.ts +33 -2
  9. package/build/Protocol.mjs +102 -37
  10. package/build/Protocol.mjs.map +2 -2
  11. package/build/Room.cjs +296 -19
  12. package/build/Room.cjs.map +3 -3
  13. package/build/Room.d.ts +186 -3
  14. package/build/Room.mjs +303 -21
  15. package/build/Room.mjs.map +3 -3
  16. package/build/RoomPlugin.cjs +252 -0
  17. package/build/RoomPlugin.cjs.map +7 -0
  18. package/build/RoomPlugin.d.ts +271 -0
  19. package/build/RoomPlugin.mjs +220 -0
  20. package/build/RoomPlugin.mjs.map +7 -0
  21. package/build/Server.cjs +40 -7
  22. package/build/Server.cjs.map +2 -2
  23. package/build/Server.d.ts +25 -0
  24. package/build/Server.mjs +41 -8
  25. package/build/Server.mjs.map +2 -2
  26. package/build/Transport.cjs +38 -2
  27. package/build/Transport.cjs.map +2 -2
  28. package/build/Transport.d.ts +40 -4
  29. package/build/Transport.mjs +38 -2
  30. package/build/Transport.mjs.map +2 -2
  31. package/build/index.cjs +11 -2
  32. package/build/index.cjs.map +2 -2
  33. package/build/index.d.ts +2 -1
  34. package/build/index.mjs +12 -2
  35. package/build/index.mjs.map +2 -2
  36. package/build/input/InputBuffer.cjs +113 -0
  37. package/build/input/InputBuffer.cjs.map +7 -0
  38. package/build/input/InputBuffer.d.ts +136 -0
  39. package/build/input/InputBuffer.mjs +86 -0
  40. package/build/input/InputBuffer.mjs.map +7 -0
  41. package/build/internal.cjs +61 -0
  42. package/build/internal.cjs.map +7 -0
  43. package/build/internal.d.ts +9 -0
  44. package/build/internal.mjs +29 -0
  45. package/build/internal.mjs.map +7 -0
  46. package/build/matchmaker/LocalDriver/LocalDriver.cjs +13 -0
  47. package/build/matchmaker/LocalDriver/LocalDriver.cjs.map +2 -2
  48. package/build/matchmaker/LocalDriver/LocalDriver.d.ts +1 -0
  49. package/build/matchmaker/LocalDriver/LocalDriver.mjs +13 -0
  50. package/build/matchmaker/LocalDriver/LocalDriver.mjs.map +2 -2
  51. package/build/matchmaker/driver.cjs.map +1 -1
  52. package/build/matchmaker/driver.d.ts +12 -0
  53. package/build/matchmaker/driver.mjs.map +1 -1
  54. package/build/presence/LocalPresence.d.ts +1 -1
  55. package/build/rooms/LobbyRoom.cjs +8 -10
  56. package/build/rooms/LobbyRoom.cjs.map +2 -2
  57. package/build/rooms/LobbyRoom.d.ts +4 -3
  58. package/build/rooms/LobbyRoom.mjs +8 -10
  59. package/build/rooms/LobbyRoom.mjs.map +2 -2
  60. package/build/rooms/RelayRoom.cjs +12 -16
  61. package/build/rooms/RelayRoom.cjs.map +2 -2
  62. package/build/rooms/RelayRoom.d.ts +32 -11
  63. package/build/rooms/RelayRoom.mjs +10 -16
  64. package/build/rooms/RelayRoom.mjs.map +2 -2
  65. package/build/router/index.cjs +65 -4
  66. package/build/router/index.cjs.map +2 -2
  67. package/build/router/index.d.ts +30 -6
  68. package/build/router/index.mjs +66 -6
  69. package/build/router/index.mjs.map +3 -3
  70. package/build/utils/UserSessionIndex.cjs +162 -0
  71. package/build/utils/UserSessionIndex.cjs.map +7 -0
  72. package/build/utils/UserSessionIndex.d.ts +166 -0
  73. package/build/utils/UserSessionIndex.mjs +130 -0
  74. package/build/utils/UserSessionIndex.mjs.map +7 -0
  75. package/package.json +19 -14
  76. package/src/MatchMaker.ts +40 -6
  77. package/src/Protocol.ts +130 -59
  78. package/src/Room.ts +475 -22
  79. package/src/RoomPlugin.ts +563 -0
  80. package/src/Server.ts +72 -11
  81. package/src/Transport.ts +76 -8
  82. package/src/index.ts +10 -1
  83. package/src/input/InputBuffer.ts +192 -0
  84. package/src/internal.ts +46 -0
  85. package/src/matchmaker/LocalDriver/LocalDriver.ts +10 -0
  86. package/src/matchmaker/driver.ts +13 -0
  87. package/src/rooms/LobbyRoom.ts +12 -8
  88. package/src/rooms/RelayRoom.ts +9 -15
  89. package/src/router/index.ts +112 -11
  90. package/src/utils/UserSessionIndex.ts +311 -0
@@ -0,0 +1,252 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // packages/core/src/RoomPlugin.ts
21
+ var RoomPlugin_exports = {};
22
+ __export(RoomPlugin_exports, {
23
+ DEFAULT_PLUGIN_ORDER: () => DEFAULT_PLUGIN_ORDER,
24
+ PLUGIN_LIFECYCLE_KEYS: () => PLUGIN_LIFECYCLE_KEYS,
25
+ RoomPlugin: () => RoomPlugin,
26
+ attachToTestRoom: () => attachToTestRoom,
27
+ computePluginLayout: () => computePluginLayout,
28
+ definePlugins: () => definePlugins,
29
+ installPluginHookWrappers: () => installPluginHookWrappers,
30
+ setupRoomPlugins: () => setupRoomPlugins
31
+ });
32
+ module.exports = __toCommonJS(RoomPlugin_exports);
33
+ var RoomPlugin = class {
34
+ };
35
+ function definePlugins(plugins) {
36
+ if (!Array.isArray(plugins)) {
37
+ return plugins;
38
+ }
39
+ const out = {};
40
+ for (const p of plugins) {
41
+ const key = p["pluginName"];
42
+ if (typeof key !== "string" || key.length === 0) {
43
+ throw new Error(
44
+ `[Room] plugin ${p.constructor.name} is missing a 'pluginName' field. Declare \`readonly pluginName = '<key>' as const\` on the class, or register it via the keyed-record form \`definePlugins({ <key>: <plugin> })\`.`
45
+ );
46
+ }
47
+ if (out[key]) {
48
+ throw new Error(
49
+ `[Room] two plugins resolve to pluginName "${key}". Configure one of them via constructor argument, or use the keyed-record form to disambiguate.`
50
+ );
51
+ }
52
+ out[key] = p;
53
+ }
54
+ return out;
55
+ }
56
+ var PLUGIN_LIFECYCLE_KEYS = ["onCreate", "onAuth", "onJoin", "onLeave", "onDispose"];
57
+ function attachToTestRoom(plugin, roomStub = {}) {
58
+ plugin.room = roomStub;
59
+ return roomStub;
60
+ }
61
+ var DEP_KEY_PREFIX = "__dep:";
62
+ var DEFAULT_PLUGIN_ORDER = {
63
+ onCreate: "before",
64
+ onAuth: "before",
65
+ onJoin: "before",
66
+ onLeave: "after",
67
+ onDispose: "after"
68
+ };
69
+ function computePluginLayout(plugins) {
70
+ const resolved = resolveDependencies(plugins);
71
+ const hooks = {};
72
+ let anyHook = false;
73
+ for (const hook of PLUGIN_LIFECYCLE_KEYS) {
74
+ const before = [];
75
+ const after = [];
76
+ for (const { key, plugin } of resolved.entries) {
77
+ if (typeof plugin[hook] !== "function") {
78
+ continue;
79
+ }
80
+ const order = plugin["order"]?.[hook] ?? DEFAULT_PLUGIN_ORDER[hook];
81
+ (order === "before" ? before : after).push(key);
82
+ anyHook = true;
83
+ }
84
+ hooks[hook] = { before, after };
85
+ }
86
+ const messageOwners = /* @__PURE__ */ new Map();
87
+ for (const { key, plugin } of resolved.entries) {
88
+ const messages = plugin["messages"];
89
+ if (!messages) {
90
+ continue;
91
+ }
92
+ for (const messageKey of Object.keys(messages)) {
93
+ const prior = messageOwners.get(messageKey);
94
+ if (prior !== void 0) {
95
+ throw new Error(
96
+ `[Room] message key "${messageKey}" declared by multiple plugins: "${prior}" and "${key}". Resolve by giving one of them a different key, or override on the room's own \`messages\`.`
97
+ );
98
+ }
99
+ messageOwners.set(messageKey, key);
100
+ }
101
+ }
102
+ if (!anyHook && messageOwners.size === 0 && resolved.autoDeps.length === 0) {
103
+ return null;
104
+ }
105
+ return { hooks, messageOwners, autoDeps: resolved.autoDeps };
106
+ }
107
+ function resolveDependencies(plugins) {
108
+ const entries = [];
109
+ for (const [key, plugin] of Object.entries(plugins)) {
110
+ entries.push({ key, plugin });
111
+ }
112
+ const present = /* @__PURE__ */ new Set();
113
+ for (const { plugin } of entries) {
114
+ present.add(plugin.constructor);
115
+ }
116
+ const autoDeps = [];
117
+ const visiting = /* @__PURE__ */ new Set();
118
+ const path = [];
119
+ function walk(depCtor) {
120
+ if (present.has(depCtor)) {
121
+ return;
122
+ }
123
+ if (visiting.has(depCtor)) {
124
+ const cycle = [...path, depCtor.name].join(" \u2192 ");
125
+ throw new Error(`[Room] plugin dependency cycle: ${cycle}`);
126
+ }
127
+ visiting.add(depCtor);
128
+ path.push(depCtor.name);
129
+ const deeper = depCtor.dependencies;
130
+ if (Array.isArray(deeper)) {
131
+ for (const inner of deeper) {
132
+ walk(inner);
133
+ }
134
+ }
135
+ let instance;
136
+ try {
137
+ instance = new depCtor();
138
+ } catch (err) {
139
+ throw new Error(
140
+ `[Room] auto-included plugin "${depCtor.name}" must be constructible with no arguments. If it needs options, register it explicitly in definePlugins({...}). (cause: ${err?.message ?? err})`
141
+ );
142
+ }
143
+ const key = DEP_KEY_PREFIX + depCtor.name;
144
+ entries.push({ key, plugin: instance });
145
+ autoDeps.push({ key, ctor: depCtor });
146
+ present.add(depCtor);
147
+ visiting.delete(depCtor);
148
+ path.pop();
149
+ }
150
+ const userEntriesSnapshot = entries.slice();
151
+ for (const { plugin } of userEntriesSnapshot) {
152
+ const deps = plugin.constructor.dependencies;
153
+ if (!Array.isArray(deps)) {
154
+ continue;
155
+ }
156
+ for (const depCtor of deps) {
157
+ walk(depCtor);
158
+ }
159
+ }
160
+ return { entries, autoDeps };
161
+ }
162
+ function installPluginHookWrappers(ctor, layout) {
163
+ if (layout === null) {
164
+ return;
165
+ }
166
+ const proto = ctor.prototype;
167
+ for (const hook of PLUGIN_LIFECYCLE_KEYS) {
168
+ const { before, after } = layout.hooks[hook];
169
+ if (before.length === 0 && after.length === 0) {
170
+ continue;
171
+ }
172
+ const original = proto[hook];
173
+ proto[hook] = async function(...args) {
174
+ const lookup = (k) => k.startsWith(DEP_KEY_PREFIX) ? this._autoPlugins[k] : this.plugins[k];
175
+ for (const k of before) {
176
+ const p = lookup(k);
177
+ await p[hook].call(p, ...args);
178
+ }
179
+ let result;
180
+ if (original) {
181
+ result = await original.apply(this, args);
182
+ }
183
+ for (const k of after) {
184
+ const p = lookup(k);
185
+ await p[hook].call(p, ...args);
186
+ }
187
+ return result;
188
+ };
189
+ }
190
+ }
191
+ function setupRoomPlugins(room) {
192
+ const plugins = room.plugins;
193
+ const layout = resolveOrComputeLayout(room.constructor, plugins);
194
+ attachRoomReference(room, plugins);
195
+ if (layout && layout.autoDeps.length > 0) {
196
+ room._autoPlugins = instantiateAutoDeps(room, layout);
197
+ }
198
+ if (layout && layout.messageOwners.size > 0) {
199
+ mergePluginMessages(room, layout);
200
+ }
201
+ Object.freeze(plugins);
202
+ if (room._autoPlugins) {
203
+ Object.freeze(room._autoPlugins);
204
+ }
205
+ }
206
+ function resolveOrComputeLayout(ctor, plugins) {
207
+ if (Object.prototype.hasOwnProperty.call(ctor, "__pluginLayout")) {
208
+ return ctor.__pluginLayout;
209
+ }
210
+ const layout = computePluginLayout(plugins);
211
+ installPluginHookWrappers(ctor, layout);
212
+ ctor.__pluginLayout = layout;
213
+ return layout;
214
+ }
215
+ function attachRoomReference(room, plugins) {
216
+ for (const plugin of Object.values(plugins)) {
217
+ plugin.room = room;
218
+ }
219
+ }
220
+ function instantiateAutoDeps(room, layout) {
221
+ const auto = {};
222
+ for (const { key, ctor: depCtor } of layout.autoDeps) {
223
+ const instance = new depCtor();
224
+ instance.room = room;
225
+ auto[key] = instance;
226
+ }
227
+ return auto;
228
+ }
229
+ function mergePluginMessages(room, layout) {
230
+ const plugins = room.plugins;
231
+ for (const [messageKey, pluginKey] of layout.messageOwners) {
232
+ if (room.messages?.[messageKey]) {
233
+ continue;
234
+ }
235
+ const source = pluginKey.startsWith(DEP_KEY_PREFIX) ? room._autoPlugins[pluginKey] : plugins[pluginKey];
236
+ const handler = source["messages"]?.[messageKey];
237
+ if (handler !== void 0) {
238
+ (room.messages ??= {})[messageKey] = handler;
239
+ }
240
+ }
241
+ }
242
+ // Annotate the CommonJS export names for ESM import in node:
243
+ 0 && (module.exports = {
244
+ DEFAULT_PLUGIN_ORDER,
245
+ PLUGIN_LIFECYCLE_KEYS,
246
+ RoomPlugin,
247
+ attachToTestRoom,
248
+ computePluginLayout,
249
+ definePlugins,
250
+ installPluginHookWrappers,
251
+ setupRoomPlugins
252
+ });
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 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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",
6
+ "names": []
7
+ }
@@ -0,0 +1,271 @@
1
+ /**
2
+ * Room plugins.
3
+ *
4
+ * A plugin is a composable extension to a Room — it can declare message
5
+ * handlers, attach lifecycle hooks, and expose public methods that the
6
+ * room can call (`this.plugins.<key>.someMethod()`). The framework
7
+ * instantiates one plugin per room, sets `this.room` after the room is
8
+ * constructed, then merges the plugin's `messages` with the room's own
9
+ * and wires its lifecycle hooks alongside the room's.
10
+ *
11
+ * Example: a tiny chat plugin contributing a single message handler:
12
+ *
13
+ * class ChatPlugin extends RoomPlugin {
14
+ * private history: string[] = [];
15
+ * constructor(private opts: { historyLimit: number }) { super(); }
16
+ *
17
+ * messages = {
18
+ * chat: (client: Client, msg: { text: string }) => {
19
+ * this.history.push(msg.text);
20
+ * if (this.history.length > this.opts.historyLimit) this.history.shift();
21
+ * this.room.broadcast('chat', { from: client.userId, text: msg.text });
22
+ * },
23
+ * };
24
+ *
25
+ * // Public — callable as `this.plugins.chat.getHistory()` from the room
26
+ * getHistory(): readonly string[] { return this.history; }
27
+ * }
28
+ *
29
+ * Use via `definePlugins`:
30
+ *
31
+ * class MyRoom extends Room<{ state: MyState }> {
32
+ * plugins = definePlugins({
33
+ * chat: new ChatPlugin({ historyLimit: 100 }),
34
+ * });
35
+ * }
36
+ */
37
+ import type { Room } from './Room.ts';
38
+ import type { AuthContext, Client } from './Transport.ts';
39
+ import type { Messages } from '@colyseus/shared-types';
40
+ /**
41
+ * Ordering hint for a plugin's lifecycle hook relative to the room's own
42
+ * hook. Sensible per-hook defaults are applied when omitted (see
43
+ * `Room.__init` for the exact ordering policy).
44
+ *
45
+ * onCreate / onAuth / onJoin → plugins run BEFORE room (guards + setup)
46
+ * onLeave / onDispose → plugins run AFTER room (capture final state)
47
+ */
48
+ export interface RoomPluginOrder {
49
+ onCreate?: 'before' | 'after';
50
+ onAuth?: 'before' | 'after';
51
+ onJoin?: 'before' | 'after';
52
+ onLeave?: 'before' | 'after';
53
+ onDispose?: 'before' | 'after';
54
+ }
55
+ /**
56
+ * Base class for room plugins. Subclass to define a plugin; the framework
57
+ * sets `this.room` after the host room is fully constructed.
58
+ *
59
+ * Don't access `this.room` from the plugin's constructor — it hasn't been
60
+ * wired yet. Everything room-dependent goes in `onCreate` / `onJoin` /
61
+ * etc. or in public methods that are called from the room post-init.
62
+ *
63
+ * @typeParam This - The Room subclass this plugin is attached to. Narrow
64
+ * for schema-driven plugins that need a specific state shape, e.g.
65
+ * `class PhysicsPlugin extends RoomPlugin<Room<{ state: PhysicsContract }>>`.
66
+ */
67
+ export declare abstract class RoomPlugin<This extends Room = Room> {
68
+ /**
69
+ * Live room reference. Wired by the framework at __init, AFTER the
70
+ * room's own construction — accessing it from the plugin's
71
+ * constructor throws.
72
+ */
73
+ protected readonly room: This;
74
+ /**
75
+ * Canonical key for the plugin when registered via `definePlugins([...])`.
76
+ * Declared on the subclass with `as const` so the literal type flows
77
+ * into `room.plugins.<key>`:
78
+ *
79
+ * class ChatPlugin extends RoomPlugin {
80
+ * readonly pluginName = 'chat' as const;
81
+ * }
82
+ *
83
+ * Optional under the keyed-record form (`definePlugins({ chat: ... })`),
84
+ * required under the array form. Must stay `public` so `extends` /
85
+ * `keyof` can see the literal — those are blind to protected from
86
+ * outside the class. End-user autocomplete hides it via `Omit` in
87
+ * `definePlugins`'s return type.
88
+ *
89
+ * For multi-instance use, accept the name at construction:
90
+ *
91
+ * readonly pluginName: string;
92
+ * constructor(name = 'chat') { super(); this.pluginName = name; }
93
+ */
94
+ readonly pluginName?: string;
95
+ /**
96
+ * Declarative message handlers — merged into the room's `messages` at
97
+ * __init. Conflict against the room's own key: room wins. Conflict
98
+ * between two plugins: throws at __init.
99
+ */
100
+ protected messages?: Messages<This>;
101
+ /** Optional per-hook ordering vs the room's own hook. */
102
+ protected order?: RoomPluginOrder;
103
+ protected onCreate?(options: any): void | Promise<void>;
104
+ protected onAuth?(client: Client, options: any, context: AuthContext): void | Promise<void>;
105
+ protected onJoin?(client: Client, options?: any): void | Promise<void>;
106
+ protected onLeave?(client: Client, code?: number): void | Promise<void>;
107
+ protected onDispose?(): void | Promise<void>;
108
+ }
109
+ /**
110
+ * Plugin-class constructor type used by the `dependencies` static
111
+ * declaration. Constrained to zero-arg constructors because the
112
+ * framework auto-instantiates missing dependencies with no
113
+ * configuration — plugins that need options can't be auto-included
114
+ * and must be registered explicitly in `definePlugins({...})`.
115
+ */
116
+ export type RoomPluginClass = new () => RoomPlugin<any>;
117
+ /**
118
+ * Static `dependencies` declaration. List other plugin classes this
119
+ * plugin needs alongside it; the framework auto-instantiates any
120
+ * missing ones at room construction time. Transitive deps are
121
+ * resolved recursively. Cycles throw at class-init.
122
+ *
123
+ * Example:
124
+ * class UniqueSessionPlugin extends RoomPlugin {
125
+ * static dependencies: PluginDependencies = [TrackUserSessionsPlugin];
126
+ * }
127
+ */
128
+ export type PluginDependencies = ReadonlyArray<RoomPluginClass>;
129
+ /**
130
+ * Define a Room's plugin record. The framework wires plugins at
131
+ * `__init` — first construct of the class computes the layout
132
+ * (cached on the constructor) and installs hook wrappers on the
133
+ * prototype. The `const T` modifier preserves literal types so
134
+ * `this.plugins.<key>.method()` autocompletes against each plugin's
135
+ * specific subclass.
136
+ */
137
+ type ExtractPluginName<P> = P extends {
138
+ pluginName: infer K;
139
+ } ? (K extends string ? K : never) : never;
140
+ /** Hide `pluginName` from end-user autocomplete on `this.plugins.<key>`. */
141
+ type PluginPublicSurface<P> = Omit<P, 'pluginName'>;
142
+ type PluginsArrayToRecord<T extends readonly RoomPlugin<any>[]> = {
143
+ [P in T[number] as ExtractPluginName<P>]: PluginPublicSurface<P>;
144
+ };
145
+ /**
146
+ * Array form (recommended). Each plugin declares its own canonical
147
+ * key via `readonly pluginName = '...' as const`; the framework
148
+ * turns the array into a typed record so `plugins.<pluginName>`
149
+ * autocompletes the right instance type.
150
+ *
151
+ * class GameRoom extends Room {
152
+ * plugins = definePlugins([
153
+ * new ChatPlugin(), // pluginName: 'chat'
154
+ * new UniqueSessionPlugin({ max: 1 }), // pluginName: 'uniqueSession'
155
+ * ]);
156
+ * }
157
+ * this.plugins.chat.send('hi');
158
+ *
159
+ * Throws at runtime if any plugin is missing `pluginName` or if two
160
+ * plugins resolve to the same key.
161
+ */
162
+ export declare function definePlugins<const T extends readonly RoomPlugin<any>[]>(plugins: T): PluginsArrayToRecord<T>;
163
+ /**
164
+ * Record form (legacy / multi-instance escape hatch). The caller
165
+ * chooses the key per-room. Useful when registering two instances of
166
+ * the same plugin class without configuring `pluginName` per
167
+ * instance — e.g. `{ adminChat: new ChatPlugin(), playerChat: new ChatPlugin() }`.
168
+ */
169
+ export declare function definePlugins<const T extends Record<string, RoomPlugin<any>>>(plugins: T): {
170
+ [K in keyof T]: PluginPublicSurface<T[K]>;
171
+ };
172
+ /**
173
+ * Lifecycle hook keys recognized by the framework — used internally to
174
+ * separate "framework-recognized methods" from "user-defined public
175
+ * methods" when wiring a plugin into a room. Exported for the test
176
+ * harness; downstream code should not need it.
177
+ */
178
+ export declare const PLUGIN_LIFECYCLE_KEYS: readonly ["onCreate", "onAuth", "onJoin", "onLeave", "onDispose"];
179
+ export type PluginLifecycleKey = (typeof PLUGIN_LIFECYCLE_KEYS)[number];
180
+ /**
181
+ * Test helper — attach a stub or fake room to a plugin so its methods
182
+ * and lifecycle hooks can be exercised in isolation without spinning up
183
+ * a real Colyseus server.
184
+ *
185
+ * const plugin = new ChatPlugin({ historyLimit: 5 });
186
+ * const room = attachToTestRoom(plugin, { broadcast: sinon.spy() });
187
+ * await plugin.messages!.chat!.call(plugin, { userId: 'u1' }, { text: 'hi' });
188
+ *
189
+ * The second arg is shallow-merged onto the stub so tests only declare
190
+ * the room properties they actually exercise. Returns the room stub for
191
+ * post-call assertions.
192
+ */
193
+ export declare function attachToTestRoom<This extends Room, R extends Partial<This>>(plugin: RoomPlugin<This>, roomStub?: R): R;
194
+ /**
195
+ * Precomputed plugin layout for a Room subclass — populated on first
196
+ * construct, cached on the constructor. Hook wrappers are installed
197
+ * on the prototype in the same pass (see `installPluginHookWrappers`).
198
+ *
199
+ * @internal
200
+ */
201
+ export interface PluginLayout {
202
+ /** Per-hook participation: which plugin keys run before/after the room's own hook. */
203
+ hooks: Record<PluginLifecycleKey, {
204
+ before: string[];
205
+ after: string[];
206
+ }>;
207
+ /** Message key → plugin key. Conflict detection ran when this was built. */
208
+ messageOwners: Map<string, string>;
209
+ /** Sentinel-keyed plugin classes to instantiate per room. */
210
+ autoDeps: Array<{
211
+ key: string;
212
+ ctor: RoomPluginClass;
213
+ }>;
214
+ }
215
+ /**
216
+ * Default before/after policy for lifecycle hooks vs the room's own
217
+ * hook. Plugins can override per-hook via the `order` field on the
218
+ * plugin instance.
219
+ *
220
+ * onCreate / onAuth / onJoin → plugins run BEFORE room (guards + setup)
221
+ * onLeave / onDispose → plugins run AFTER room (capture final state)
222
+ *
223
+ * @internal
224
+ */
225
+ export declare const DEFAULT_PLUGIN_ORDER: Record<PluginLifecycleKey, 'before' | 'after'>;
226
+ /**
227
+ * Walk the room's plugin instances and produce the lifecycle + message
228
+ * layout for the Room class. Called once per Room subclass — on the
229
+ * first construct — and the result is cached on the constructor. Throws
230
+ * on duplicate message keys (named both plugin keys) so the failure is
231
+ * visible at class-init rather than at first message dispatch.
232
+ *
233
+ * Returns `null` when no plugins participate in any hook AND none
234
+ * declare a message — distinguishes "computed, nothing to do" from
235
+ * "not computed yet" in the `__pluginLayout` cache.
236
+ *
237
+ * @internal
238
+ */
239
+ export declare function computePluginLayout(plugins: Record<string, RoomPlugin>): PluginLayout | null;
240
+ /**
241
+ * Install one wrapper per participating hook on the Room subclass's
242
+ * prototype. The wrapper closes over plugin KEYS (resolved to refs at
243
+ * call time) and invokes the captured original room hook between the
244
+ * before/after plugin runs.
245
+ *
246
+ * @internal
247
+ */
248
+ export declare function installPluginHookWrappers(ctor: {
249
+ prototype: any;
250
+ }, layout: PluginLayout | null): void;
251
+ /** Structural shape used by `setupRoomPlugins` — avoids a Room import
252
+ * so the dependency graph stays one-way (Room → RoomPlugin). */
253
+ interface RoomPluginHost {
254
+ plugins?: Record<string, RoomPlugin<any>>;
255
+ _autoPlugins?: Record<string, RoomPlugin<any>>;
256
+ messages?: Record<string, Function> | any;
257
+ constructor: {
258
+ __pluginLayout?: PluginLayout | null;
259
+ prototype: any;
260
+ };
261
+ }
262
+ /**
263
+ * Wire a Room instance's plugins. Once-per-class layout (hook
264
+ * participation, message owners, dep resolution) is cached on the
265
+ * constructor; per-instance work attaches `room` refs, instantiates
266
+ * auto-deps, and merges plugin messages.
267
+ *
268
+ * @internal
269
+ */
270
+ export declare function setupRoomPlugins(room: RoomPluginHost): void;
271
+ export {};