@colyseus/core 0.18.3 → 0.18.5
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/Debug.cjs +1 -0
- package/build/Debug.cjs.map +1 -1
- package/build/IPC.cjs +1 -0
- package/build/IPC.cjs.map +1 -1
- package/build/Logger.cjs +1 -0
- package/build/Logger.cjs.map +1 -1
- package/build/MatchMaker.cjs +13 -5
- package/build/MatchMaker.cjs.map +2 -2
- package/build/MatchMaker.d.ts +10 -4
- package/build/MatchMaker.mjs +12 -5
- package/build/MatchMaker.mjs.map +2 -2
- package/build/Protocol.cjs +1 -0
- package/build/Protocol.cjs.map +1 -1
- package/build/Rewind.cjs +1 -0
- package/build/Rewind.cjs.map +1 -1
- package/build/Room.cjs +113 -3
- package/build/Room.cjs.map +2 -2
- package/build/Room.d.ts +58 -1
- package/build/Room.mjs +112 -3
- package/build/Room.mjs.map +2 -2
- package/build/RoomMessages.cjs +3 -1
- package/build/RoomMessages.cjs.map +2 -2
- package/build/RoomMessages.d.ts +3 -1
- package/build/RoomMessages.mjs +2 -1
- package/build/RoomMessages.mjs.map +2 -2
- package/build/RoomPlugin.cjs +1 -0
- package/build/RoomPlugin.cjs.map +1 -1
- package/build/Server.cjs +1 -0
- package/build/Server.cjs.map +1 -1
- package/build/Stats.cjs +1 -0
- package/build/Stats.cjs.map +1 -1
- package/build/Transport.cjs +39 -0
- package/build/Transport.cjs.map +2 -2
- package/build/Transport.d.ts +66 -1
- package/build/Transport.mjs +37 -1
- package/build/Transport.mjs.map +2 -2
- package/build/errors/RoomExceptions.cjs +1 -0
- package/build/errors/RoomExceptions.cjs.map +1 -1
- package/build/errors/SeatReservationError.cjs +1 -0
- package/build/errors/SeatReservationError.cjs.map +1 -1
- package/build/errors/ServerError.cjs +1 -0
- package/build/errors/ServerError.cjs.map +1 -1
- package/build/index.cjs +5 -0
- package/build/index.cjs.map +2 -2
- package/build/index.d.ts +1 -1
- package/build/index.mjs +3 -1
- package/build/index.mjs.map +2 -2
- package/build/input/InputBuffer.cjs +1 -0
- package/build/input/InputBuffer.cjs.map +1 -1
- package/build/input/RoomInput.cjs +64 -3
- package/build/input/RoomInput.cjs.map +2 -2
- package/build/input/RoomInput.d.ts +16 -2
- package/build/input/RoomInput.mjs +63 -3
- package/build/input/RoomInput.mjs.map +2 -2
- package/build/input/types.cjs +1 -0
- package/build/input/types.cjs.map +1 -1
- package/build/internal.cjs +1 -0
- package/build/internal.cjs.map +1 -1
- package/build/matchmaker/Lobby.cjs +1 -0
- package/build/matchmaker/Lobby.cjs.map +1 -1
- package/build/matchmaker/LocalDriver/LocalDriver.cjs +1 -0
- package/build/matchmaker/LocalDriver/LocalDriver.cjs.map +1 -1
- package/build/matchmaker/LocalDriver/Query.cjs +1 -0
- package/build/matchmaker/LocalDriver/Query.cjs.map +1 -1
- package/build/matchmaker/RegisteredHandler.cjs +1 -0
- package/build/matchmaker/RegisteredHandler.cjs.map +1 -1
- package/build/matchmaker/controller.cjs +1 -0
- package/build/matchmaker/controller.cjs.map +1 -1
- package/build/matchmaker/driver.cjs +1 -0
- package/build/matchmaker/driver.cjs.map +2 -2
- package/build/matchmaker/driver.d.ts +4 -2
- package/build/matchmaker/driver.mjs.map +1 -1
- package/build/presence/LocalPresence.cjs +1 -0
- package/build/presence/LocalPresence.cjs.map +1 -1
- package/build/presence/Presence.cjs +1 -0
- package/build/presence/Presence.cjs.map +1 -1
- package/build/rooms/LobbyRoom.cjs +1 -0
- package/build/rooms/LobbyRoom.cjs.map +1 -1
- package/build/rooms/QueueRoom.cjs +1 -0
- package/build/rooms/QueueRoom.cjs.map +1 -1
- package/build/rooms/RelayRoom.cjs +1 -0
- package/build/rooms/RelayRoom.cjs.map +1 -1
- package/build/router/default_routes.cjs +4 -8
- package/build/router/default_routes.cjs.map +2 -2
- package/build/router/default_routes.mjs +4 -9
- package/build/router/default_routes.mjs.map +2 -2
- package/build/router/index.cjs +7 -1
- package/build/router/index.cjs.map +2 -2
- package/build/router/index.mjs +6 -1
- package/build/router/index.mjs.map +2 -2
- package/build/router/node.cjs +23 -2
- package/build/router/node.cjs.map +2 -2
- package/build/router/node.mjs +22 -2
- package/build/router/node.mjs.map +2 -2
- package/build/serializer/NoneSerializer.cjs +1 -0
- package/build/serializer/NoneSerializer.cjs.map +1 -1
- package/build/serializer/SchemaSerializer.cjs +98 -0
- package/build/serializer/SchemaSerializer.cjs.map +2 -2
- package/build/serializer/SchemaSerializer.d.ts +25 -0
- package/build/serializer/SchemaSerializer.mjs +98 -1
- package/build/serializer/SchemaSerializer.mjs.map +2 -2
- package/build/serializer/Serializer.cjs +1 -0
- package/build/serializer/Serializer.cjs.map +2 -2
- package/build/serializer/Serializer.d.ts +15 -0
- package/build/utils/DevMode.cjs +1 -0
- package/build/utils/DevMode.cjs.map +1 -1
- package/build/utils/Env.cjs +1 -0
- package/build/utils/Env.cjs.map +1 -1
- package/build/utils/StandardSchema.cjs +1 -0
- package/build/utils/StandardSchema.cjs.map +1 -1
- package/build/utils/UserSessionIndex.cjs +1 -0
- package/build/utils/UserSessionIndex.cjs.map +1 -1
- package/build/utils/Utils.cjs +1 -0
- package/build/utils/Utils.cjs.map +2 -2
- package/build/utils/Utils.d.ts +12 -0
- package/build/utils/Utils.mjs.map +2 -2
- package/build/utils/nanoevents.cjs +5 -1
- package/build/utils/nanoevents.cjs.map +2 -2
- package/build/utils/nanoevents.d.ts +3 -1
- package/build/utils/nanoevents.mjs +4 -1
- package/build/utils/nanoevents.mjs.map +2 -2
- package/package.json +8 -7
- package/src/MatchMaker.ts +53 -15
- package/src/Room.ts +145 -4
- package/src/RoomMessages.ts +2 -1
- package/src/Transport.ts +132 -4
- package/src/index.ts +1 -1
- package/src/input/RoomInput.ts +76 -3
- package/src/matchmaker/driver.ts +4 -2
- package/src/router/default_routes.ts +4 -9
- package/src/router/index.ts +7 -0
- package/src/router/node.ts +26 -2
- package/src/serializer/SchemaSerializer.ts +146 -1
- package/src/serializer/Serializer.ts +17 -0
- package/src/utils/Utils.ts +18 -0
- package/src/utils/nanoevents.ts +4 -1
|
@@ -2,6 +2,6 @@
|
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/input/types.ts"],
|
|
4
4
|
"sourcesContent": ["/**\n * Types for the room input subsystem: the option shapes accepted by\n * `Room.defineInput()`, the per-client {@link InputAccessor} / room-wide\n * {@link InputAPI} surfaces, and the internal normalized options. Runtime\n * lives in `InputBuffer.ts`.\n */\n\n/**\n * Names of fields on `I` whose values are `number` \u2014 used by\n * `Room.defineInput()` to constrain `seqField` to actually-numeric fields\n * on the input schema. Filters out booleans, strings, methods, etc.\n */\nexport type NumericFieldsOf<I> = {\n [K in keyof I]: I[K] extends number ? (K extends string ? K : never) : never;\n}[keyof I];\n\n/**\n * Context handed to the `idle` callback form \u2014 see {@link IdleInput}. A reused\n * per-client scratch: read it inside the callback, don't store it. It carries\n * MECHANISM only \u2014 derived judgments (e.g. liveness) stay in userland; look the\n * client up via `this.clients.get(ctx.sessionId)` when your policy needs it.\n */\nexport interface IdleContext<I> {\n /** Last decoded input (`undefined` before the client's first). */\n latest: I | undefined;\n sessionId: string;\n}\n\n/**\n * What to synthesize when the buffer is empty (declared room-wide at\n * `defineInput({ idle })`, or per-call via {@link ConsumeOptions.idle}):\n * - `true` \u2014 pure schema defaults.\n * - `Partial<I>` \u2014 defaults overlaid with these fields. A full schema instance\n * (e.g. {@link InputAccessor.latest}) also works \u2014 fields are copied BY NAME,\n * so its prototype accessors are read correctly.\n * - callback \u2014 invoked LAZILY (only on an actually-empty tick) with an\n * {@link IdleContext}, returning either of the above. The right form when the\n * overrides take work to compute (entity lookups, held-button carry-over).\n *\n * \u26A0 Do NOT build overrides by spreading a schema instance (`{ ...latest, x }`):\n * schema fields live on the prototype, so the spread copies NOTHING of them.\n * Return `latest` itself, or name the fields (`{ x, plant: !!latest?.plant }`).\n */\nexport type IdleInput<I> = true | Partial<I> | ((ctx: IdleContext<I>) => true | Partial<I>);\n\n/**\n * Options for {@link InputAccessor.drain} / {@link InputAccessor.next}.\n * `idle` overrides the room-level `defineInput({ idle })` policy for this call\n * (see {@link IdleInput}); pass `false` to suppress it (force skip behavior).\n */\nexport interface ConsumeOptions<I> {\n idle?: IdleInput<I> | false;\n}\n\n/**\n * Input sanitization, declared at `defineInput({ sanitize })` \u2014 never trust the\n * wire. Applied to each decoded frame IN PLACE, before anything reads it\n * (`latest`, the buffer, the `idle` callback's ctx):\n * - **Map form** \u2014 per-field `[min, max]` range clamps with NaN-safe semantics:\n * `NaN` (both comparisons false) lands on `min`, closing the classic\n * `Math.min(NaN, \u2026)` poisoning hole.\n * - **Callback form** \u2014 arbitrary in-place fix-up (wrap an angle, enforce a\n * cross-field rule) for anything beyond ranges.\n *\n * Sanitizers MODIFY, they never reject \u2014 a malformed value becomes a legal one\n * instead of dropping the frame. Must not touch the `seqField` (it runs before\n * dedupe). Semantic validation (\"slot must name an owned weapon\") stays in\n * your sim \u2014 this handles value domains, not game rules.\n */\nexport type SanitizeInput<I> =\n | Partial<Record<NumericFieldsOf<I>, readonly [number, number]>>\n | ((input: I) => void);\n\n/** Options for `Room.defineInput()`. The user-facing twin of the internal\n * normalized {@link NormalizedInputOptions}. */\nexport interface DefineInputOptions<I = any> {\n seqField?: NumericFieldsOf<I>;\n bufferMaxSize?: number;\n /**\n * Input sanitization \u2014 never trust the wire. Applied IN PLACE to every\n * decoded frame before anything reads it (`latest`, the buffer, the `idle`\n * ctx). Map form = per-field `[min, max]` clamps with NaN-safe semantics\n * (NaN \u2192 min \u2014 closes the `Math.min(NaN, \u2026)` poisoning hole); callback form\n * = arbitrary in-place fix-up. Sanitizers MODIFY, never reject. See\n * {@link SanitizeInput}.\n *\n * ```ts\n * sanitize: { moveF: [-1, 1], pitch: [-PITCH_LIMIT, PITCH_LIMIT], dt: [0, MAX_DT] },\n * // or: sanitize: (f) => { f.angle = wrapAngle(f.angle); },\n * ```\n */\n sanitize?: SanitizeInput<I>;\n /**\n * Room-level absence policy: when a tick has no buffered input, bare\n * `drain()` / `next()` synthesize ONE \"idle\" frame from it \u2014 the schema's\n * defaults overlaid with the policy's overrides \u2014 so the sim loop needs no\n * empty-branch. Prefer the callback form, invoked lazily (only on\n * actually-empty ticks) with an {@link IdleContext} (`latest` + `sessionId`):\n *\n * ```ts\n * idle: ({ latest, sessionId }) => {\n * const p = this.state.players.get(sessionId); // closes over the room\n * return p ? { yaw: p.yaw, plant: !!latest?.plant } : true;\n * }\n * ```\n *\n * Not declaring it keeps the skip behavior (`drain()` \u2192 `[]`). Per-call\n * `{ idle }` overrides this default; `{ idle: false }` suppresses it.\n * Synthesized frames never advance the reconcile ack or `renderTime`.\n */\n idle?: IdleInput<I>;\n /**\n * Fixed step rate in **Hz** cascaded to the client via the join handshake;\n * it predicts at dt = 1/tickRate for deterministic rollback. Defaults to\n * the `setTimestep` rate \u2014 pass this only when the prediction\n * step differs from it. NOTE: a *rate*, not an interval \u2014 `1000/30` is the\n * step in ms, a classic mistake; use {@link stepMs} for that. Superseded by\n * {@link stepMs} / {@link stepSeconds} when those are given.\n */\n tickRate?: number;\n /**\n * Fixed step as a duration in **milliseconds** \u2014 the unit-safe alternative\n * to {@link tickRate} (`stepMs: 1000/30` is unambiguous where\n * `tickRate: 1000/30` is a bug). Normalized to the canonical Hz value\n * (`1000/stepMs`). Takes precedence over `tickRate`.\n */\n stepMs?: number;\n /**\n * Fixed step as a duration in **seconds** (e.g. `1/30`). Normalized to the\n * canonical Hz value (`1/stepSeconds`). Highest precedence.\n */\n stepSeconds?: number;\n /**\n * Physics sub-steps per input tick (integer \u2265 1, default 1) \u2014 decouples the\n * PHYSICS rate from the input/network rate. One input still drives exactly\n * one fixed step (the replay invariant), but the simulation integrates\n * `subSteps` engine steps of `stepSeconds/subSteps` inside it, identically\n * on client and server \u2014 physics at `tickRate * subSteps` Hz while sending\n * `tickRate` inputs/sec. Cascaded to clients via the join handshake;\n * both sides read the derived numbers off their step context\n * (`ctx.subSteps` / `ctx.subDt`) so N and dt can't drift apart.\n * Usually declared via {@link Room.setFixedTimestep}'s `subSteps` option\n * instead \u2014 pass it here only when the room runs its own loop.\n */\n subSteps?: number;\n}\n\n/** `true` when the defineInput opts declared an `idle` policy \u2014 narrows the\n * returned {@link InputAPI} so bare `next()` types non-optional `I`.\n * @internal \u2014 exported for {@link RoomInput.define}. */\nexport type IdleDeclared<O, I> = O extends { idle: IdleInput<I> } ? true : false;\n\n/**\n * Internal: input configuration captured by `Room.defineInput()` \u2014 the\n * normalized form of {@link DefineInputOptions} (step declarations resolved to\n * Hz, sanitizer compiled). The schema constructor is stored here so the\n * runtime doesn't need to know it through the public `room.inputs` (the\n * `.get(sessionId)` accessor).\n *\n * @internal\n */\nexport interface NormalizedInputOptions {\n /**\n * Schema constructor used to allocate per-client input instances on join.\n * Captured by `defineInput()` from its `type` argument.\n *\n * Typed loosely (`new () => any`) to sidestep type-identity issues across\n * duplicate `@colyseus/schema` installs; the runtime calls\n * `instance.clone()` and friends, which match by shape.\n */\n ctor: new () => any;\n\n /**\n * Name of a monotonically-increasing numeric field on the input schema used\n * to order and dedupe incoming frames. Unset by default (opt-in) \u2014 dedupe and\n * `.at()` lookup are off unless you name a field here. When set, the framework:\n * - Drops redundant frames (`input[seqField]` \u2264 the last-seen value are\n * discarded before they enter the buffer) \u2014 the unreliable-mode\n * ring-redundancy pattern.\n * - Powers `room.inputs.get(sessionId).at(value)` lookups.\n *\n * Despite the name, \"seq\" here is broader than an integer counter \u2014 any\n * monotonic numeric field works:\n * - **Sequence counter** (`\"seq\"`, `\"tick\"`, `\"frame\"`) \u2014 typical for\n * lockstep / rollback netcode.\n * - **Timestamp** (`\"timestamp\"`, milliseconds or seconds) \u2014 useful for\n * variable-rate clients, lag compensation, hit registration (a float-seconds\n * timestamp stamped on each saved move is the common shape).\n *\n * Whichever you use, the field must increase monotonically across frames\n * for dedupe to work.\n */\n seqField?: string;\n\n /**\n * > 0 enables per-client buffering of cloned snapshots \u2014 required for\n * `room.inputs.get(sessionId).drain() / .peek() / .at()` to return populated\n * data. Oldest drops on overflow. Set to `0` to disable (`.latest` still\n * works).\n */\n bufferMaxSize: number;\n\n /**\n * Fixed step rate (Hz) advertised to clients via the join handshake; they\n * predict at dt = 1/tickRate. Set explicitly via `defineInput`, or derived\n * from `setTimestep`. Unset = not advertised.\n */\n tickRate?: number;\n\n /**\n * Physics sub-steps per input tick (integer \u2265 1) advertised to clients via\n * the join handshake \u2014 the simulation integrates `subSteps` engine steps of\n * `(1/tickRate)/subSteps` per input on BOTH sides, so physics runs at\n * `tickRate * subSteps` Hz on a `tickRate` input/network rate. Set via\n * `setFixedTimestep(..., { subSteps })` (or `defineInput`). Unset/1 = not\n * advertised (input rate == physics rate).\n */\n subSteps?: number;\n\n /**\n * Room-level absence policy: bare `drain()` / `next()` synthesize one idle\n * frame from it when a tick has no input. See {@link IdleInput}.\n */\n idle?: IdleInput<any>;\n\n /**\n * COMPILED sanitizer (see {@link SanitizeInput} / `compileSanitizer`) \u2014\n * applied in place to each decoded frame before it becomes visible to\n * `latest` / the buffer / the idle ctx.\n */\n sanitize?: (instance: any) => void;\n}\n\n/**\n * Per-client input accessor returned by `room.inputs.get(sessionId)`. Combines the\n * latest decoded instance with the (optional) snapshot ring buffer.\n *\n * - {@link latest} \u2014 the bound Schema instance, mutated in place by the\n * decoder. Cheapest read; use when only the most recent state matters.\n * - {@link consume} / `for (const inp of accessor)` / {@link drain} /\n * {@link next} / {@link take} / {@link peek} / {@link at} \u2014 populated when\n * `defineInput()` was called with `bufferMaxSize > 0` (default 32). Use for\n * rollback netcode / lockstep where every frame matters.\n *\n * **Per-entity vs shared world** \u2014 pick the consume primitive by who integrates:\n * - **Per-entity** (each body integrates itself): iterate the accessor \u2014\n * `for (const inp of this.inputs.get(sid))` (sugar for {@link consume}) \u2014 and\n * sub-integrate one per input. N inputs = N steps for that player, ack lands\n * on the newest applied. The clean default. Iterating consumes ONE AT A TIME,\n * so {@link renderTime} tracks each input (lag comp stays exact per step) and\n * `break` leaves the rest buffered. {@link drain} returns the same set as an\n * array (retainable), but reports only the newest input's {@link renderTime}.\n * - **Shared world** (one solver step advances every body together): you can't\n * replay N inputs as N world steps without over-stepping everyone else, so\n * {@link next} exactly one input per entity per tick (or {@link take} a bounded\n * few and sub-step the solver per input). The ack then === inputs actually\n * simulated; `drain()`-then-apply-latest would silently jump the ack past\n * inputs the server never simulated, snapping the client's reconciler.\n *\n * Returned for unknown sessionIds and rooms without `defineInput()` is a\n * frozen no-op accessor (latest=undefined, drain/next/take/peek=[]/undefined,\n * at=undefined, size=0, clear=no-op).\n *\n * `Idle` is `true` when the room declared `defineInput({ idle })` \u2014 it narrows\n * {@link next} to non-optional `I` (bare calls always yield a frame).\n */\nexport interface InputAccessor<I = any, Idle extends boolean = false> {\n /** Latest decoded input. `undefined` when unknown sessionId or no input declared. */\n readonly latest: I | undefined;\n\n /**\n * Find the buffered snapshot whose `[seqField]` equals `value`. The field\n * name is the Room's `defineInput()` `seqField`. Linear scan \u2014 cheap for\n * typical buffer sizes; not intended for very large rings. Returns\n * `undefined` when no match is buffered (or `seqField` isn't configured).\n *\n * Useful for tick-aligned retrieval (lockstep, rollback).\n */\n at(value: number): I | undefined;\n\n /**\n * The blessed **per-entity** loop: consume each pending input oldest \u2192 newest,\n * ONE AT A TIME. Unlike {@link drain} (which reports only the newest input's\n * stamps up front), iterating updates {@link renderTime}/{@link reckonTime} and\n * advances {@link consumedCount} per yielded input \u2014 so per-step lag comp\n * rewinds to the exact instant of the input being applied:\n *\n * ```ts\n * for (const inp of this.inputs.get(sid)) { // sugar: accessor is iterable\n * applyInput(p, inp, dt);\n * this.collide(sid, p); // renderTime === THIS input's stamp\n * if (!p.alive) break; // break leaves the rest buffered\n * }\n * ```\n *\n * When the buffer is empty and an `idle` policy is in effect (room-level, or\n * per-call `opts.idle`), yields EXACTLY ONE idle frame (not consumed: no ack\n * bump, no {@link renderTime} change, {@link wasIdle} === true) then stops. No\n * policy + empty \u2192 zero iterations. Unknown sessionId / `bufferMaxSize: 0` \u2192\n * empty. `for (const inp of accessor)` is `consume()` with no opts.\n *\n * The returned iterator is POOLED (reused per call, allocation-free). For\n * `for..of` / spread / `Array.from` this is invisible. If you drive it by hand,\n * iterate it once to completion (or call `.return()` / `break`) before the next\n * `consume()`, and read each `.next()` result before the next \u2014 don't retain or\n * compare result objects. (Nesting two `consume()` loops over the SAME channel\n * is handled safely but is meaningless \u2014 both share one cursor.)\n */\n consume(opts?: ConsumeOptions<I>): IterableIterator<I>;\n\n /** Iterate pending (and/or one idle) inputs \u2014 sugar for {@link consume}(). */\n [Symbol.iterator](): IterableIterator<I>;\n\n /**\n * Take everything buffered (oldest \u2192 newest) and clear. Snapshots are safe to retain.\n *\n * With a room-level `defineInput({ idle })` policy the result is TOTAL: an\n * empty tick yields one synthesized \"idle\" frame instead of `[]`, so the sim\n * loop needs no empty-branch (gravity still integrates, action guards\n * naturally no-op on default values). The synthesized frame is the schema's\n * DEFAULTS overlaid with the policy's overrides (see {@link IdleInput}):\n *\n * ```ts\n * // declared once at defineInput({ idle: (ctx) => ({ ... }) }); then simply:\n * for (const f of this.inputs.get(sid).drain()) {\n * stepPlayer(p, f, world); // \u22651 frame, always\n * if (f.fire) tryFire(...); // idle frame: fire=false \u2192 no-op\n * }\n * ```\n *\n * Pass `{ idle }` to override the room policy for this call, or\n * `{ idle: false }` to suppress it.\n *\n * The idle frame is NOT a consumed input: it advances neither the reconcile\n * ack (`consumedCount`) nor {@link renderTime}. It is ONE reused instance per\n * client, refilled on each synthesis \u2014 read it within the tick, don't store\n * it. No synthesis for unknown sessionIds or `bufferMaxSize: 0`.\n */\n drain(opts?: ConsumeOptions<I>): I[];\n\n /**\n * Consume the single oldest buffered input and advance the reconcile ack by\n * exactly one \u2014 the complement to {@link peek}. Returns `undefined`\n * when the buffer is empty.\n *\n * Use this in a **shared-world** loop where one solver step advances every\n * entity together: you can't apply N-inputs-per-player as N world steps without\n * over-stepping the others, so consume one input per entity per fixed step\n * instead. {@link drain} (consume all, ack jumps to newest) is correct only\n * when each entity integrates ITSELF \u2014 see the {@link InputAccessor} docs.\n *\n * `consumedCount`/{@link renderTime} stay exact: the ack reflects only the\n * inputs you actually simulated, and `renderTime` is the stamp of THIS input.\n *\n * With a room-level `defineInput({ idle })` policy (or a per-call `{ idle }`\n * override) an empty tick returns a synthesized frame instead of `undefined`\n * \u2014 same contract as {@link drain}'s. A \"held key keeps moving through a\n * packet gap\" loop:\n *\n * @example\n * ```ts\n * // declared once:\n * inputs = this.defineInput(InputSchema, {\n * idle: ({ latest }) => latest ?? true, // empty tick \u2192 last input verbatim\n * });\n * // per tick:\n * this.setFixedTimestep(() => {\n * for (const [sid, body] of bodies) {\n * applyInputToBody(body, this.inputs.get(sid).next()); // typed I \u2014 never undefined\n * }\n * world.step(); // one step for everyone\n * }, 30);\n * ```\n */\n next(): Idle extends true ? I : I | undefined;\n next(opts: { idle: IdleInput<I> }): I;\n next(opts?: ConsumeOptions<I>): I | undefined;\n\n /**\n * Consume up to `n` oldest buffered inputs (oldest \u2192 newest) and advance the\n * reconcile ack by the count actually taken. Returns fewer than `n` (or `[]`)\n * when the buffer holds fewer. Use for shared-world **sub-stepping** \u2014 apply\n * each taken input as its own solver sub-step within one tick, bounding the\n * sub-steps per tick. {@link next} is the `take(1)` shorthand.\n */\n take(n: number): I[];\n\n /** Read everything buffered without consuming. */\n peek(): I[];\n\n /** Number of snapshots currently buffered. */\n readonly size: number;\n\n /**\n * Cumulative count of this client's inputs CONSUMED so far \u2014 the reconcile ack\n * echoed back to the client (its `lastProcessed`). Advances by one per\n * {@link next}, by the count taken per {@link take}/{@link drain}, by one per\n * yield while iterating {@link consume}, and on overflow-drop; a synthesized\n * idle frame NEVER advances it. Monotonic, so it doubles as a server-side input\n * seq \u2014 gate per-input actions off it (e.g. a fire cooldown counted in inputs)\n * instead of hand-rolling a counter. `0` before the first consume / for the\n * no-op accessor.\n */\n readonly consumedCount: number;\n\n /**\n * `true` when the most recently produced frame was a synthesized idle (vs a\n * real consumed input) \u2014 the \"skip work on idle\" guard. Set per consume\n * call/yield: `true` after a {@link next}/{@link consume}/{@link drain} that\n * fell back to the `idle` policy on an empty buffer, `false` after a real\n * input. Always `false` for {@link take} (never synthesizes) and the no-op\n * accessor. Reads meaningfully right after the consume that produced the frame.\n */\n readonly wasIdle: boolean;\n\n /** Drop all buffered snapshots (also resets the dedupe tracker). */\n clear(): void;\n\n /**\n * Server-clock render timestamp (ms since room start) of the most recently\n * consumed input \u2014 the time the client was rendering the world at when it\n * issued that input. Tracks the consume primitive: {@link drain}/{@link take}\n * report the NEWEST input they consumed; {@link next} and each\n * {@link consume}/`for..of` yield report THAT one input, so per-input loops\n * rewind to the exact instant of the input simulated.\n * `0` until the first render-time-stamped input is consumed. Populated only\n * when the room rewinds a `mode:\"snapshot\"` group (which auto-enables the\n * renderTime stamp) and `bufferMaxSize > 0`. Usually you don't read this\n * directly \u2014 pass the\n * sessionId to `rewind.lastSeenBy(sessionId)` (which resolves this value, clamps\n * it, and falls back to live) for lag-compensated \"what you see is what you\n * hit\" hit registration; use this getter only for a custom rewind time.\n */\n readonly renderTime: number;\n\n /**\n * Reckon-display stamp (server-clock ms) of the most recently consumed input\n * \u2014 the client's serverNow ESTIMATE when it sampled that input, i.e. the\n * exact instant its forward-reckoned (`mode:\"reckon\"`) entities were\n * displayed at. Stamping the display instant DIRECTLY makes the rewind read\n * immune to the client's RTT-estimation error: the client displayed\n * `f(serverNow_est)` and the server reads `f(serverNow_est)` \u2014 the same\n * index into the same recorded timeline, so estimation error cancels (the\n * `maxRewindMs` clamp still bounds spoofing). Same consume semantics as\n * {@link renderTime}; consumed automatically by `rewind.lastSeenBy()` \u2014\n * rarely read directly.\n *\n * Always a usable instant: when no reckon-stamped input has been consumed\n * yet (lag-comp off, client clock unsynced, nothing consumed), resolves to\n * the room clock's current `elapsedTime` \u2014 the fallback rooms previously\n * hand-wrote as `reckonTime > 0 ? reckonTime : now`. `rewind.lastSeenBy()`\n * internally reads the raw stamp, so its midpoint reconstruction still\n * engages for unstamped clients. The no-op accessor (unknown session / no\n * `defineInput()`) returns `0`.\n */\n readonly reckonTime: number;\n}\n\n/**\n * Returned by `Room.defineInput()`. Assign it to `this.inputs`, then call\n * `room.inputs.get(sessionId)` per tick to read each client's input stream \u2014\n * the same per-session lookup verb as `room.clients.get(sessionId)`.\n *\n * The fixed-step metadata lives HERE (one per room), not on the per-client\n * {@link InputAccessor} \u2014 these values are identical for every client, so they'd\n * be pure duplication per connection. `.get()` keeps the accessor and this\n * metadata cleanly separated.\n */\nexport type InputAPI<I = any, Idle extends boolean = false> = {\n /**\n * The input accessor for `sessionId` \u2014 the per-client stream you iterate /\n * `.next()` / `.drain()`. Returns a frozen no-op accessor for unknown sessions.\n * Mirrors `room.clients.get(sessionId)`.\n */\n get(sessionId: string): InputAccessor<I, Idle>;\n /**\n * Server-advertised fixed step rate in **Hz** \u2014 from `defineInput`'s\n * `tickRate`/`stepMs`/`stepSeconds`, or derived from `setTimestep` \u2014\n * the same value cascaded to predicting clients. `undefined` when no fixed\n * step is advertised.\n */\n readonly tickRate?: number;\n /**\n * The fixed step as **seconds** (`1/tickRate`): the dt to integrate one input\n * with, bit-identical to the client's prediction dt. Pass it to your physics\n * step (`applyInput(p, cmd, level, room.inputs.stepSeconds)`) so server and\n * client share one timestep instead of each keeping a constant that can drift.\n * `undefined` when no rate is advertised.\n */\n readonly stepSeconds?: number;\n /** The fixed step as **milliseconds** (`1000/tickRate`). `undefined` when no rate. */\n readonly stepMs?: number;\n /** Physics sub-steps per input tick (\u2265 1; `1` unless declared via\n * `setFixedTimestep(..., { subSteps })` / `defineInput`). */\n readonly subSteps: number;\n /** The physics sub-step as **seconds** (`stepSeconds / subSteps`) \u2014 the engine\n * dt when sub-stepping; equals {@link stepSeconds} when `subSteps` is 1.\n * `undefined` when no rate is advertised. */\n readonly subStepSeconds?: number;\n /** The physics sub-step as **milliseconds** (`stepMs / subSteps`). `undefined`\n * when no rate is advertised. */\n readonly subStepMs?: number;\n};\n"],
|
|
5
|
-
"mappings": "
|
|
5
|
+
"mappings": ";;;;;;;;;;;;;;;;;AAAA;AAAA;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/build/internal.cjs
CHANGED
package/build/internal.cjs.map
CHANGED
|
@@ -2,6 +2,6 @@
|
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/internal.ts"],
|
|
4
4
|
"sourcesContent": ["/**\n * Framework-internal entry \u2014 symbols used by the bundled `colyseus`\n * plugins (track-user-sessions, unique-session) and the admin\n * backend, but NOT part of `@colyseus/core`'s public surface.\n *\n * Third parties should reach for `TrackUserSessionsPlugin` and its\n * `listUserSessions` static instead \u2014 the raw helpers below are\n * implementation details and can change between minor versions.\n *\n * Resolved via the `./*` wildcard in `packages/core/package.json`.\n *\n * @internal\n */\nimport * as matchMaker from './MatchMaker.ts';\nimport {\n listUserSessions,\n type ListUserSessionsOptions,\n type UserSessionInfo,\n} from './utils/UserSessionIndex.ts';\n\nexport {\n userRoomsKey,\n USER_ROOMS_KEY_PREFIX,\n trackUserSession,\n releaseUserSession,\n trackRoomJoin,\n releaseRoomLeave,\n sweepRoomDispose,\n listUserSessions,\n type UserRoomEntry,\n type UserSessionInfo,\n type ListUserSessionsOptions,\n} from './utils/UserSessionIndex.ts';\n\n/**\n * `listUserSessions` wired to the live `matchMaker` \u2014 what\n * `TrackUserSessionsPlugin.listUserSessions` and the admin backend\n * actually call. The pure `listUserSessions` is kept exported above\n * so unit tests can drive it with fake presence + findRooms.\n */\nexport function listUserSessionsLive(\n userId: string,\n options?: ListUserSessionsOptions,\n): Promise<UserSessionInfo[]> {\n return listUserSessions(matchMaker.presence, matchMaker.findRoomsByIds, userId, options);\n}\n"],
|
|
5
|
-
"mappings": "
|
|
5
|
+
"mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAaA,iBAA4B;AAC5B,8BAIO;AAEP,IAAAA,2BAYO;AAQA,SAAS,qBACd,QACA,SAC4B;AAC5B,aAAO,0CAA4B,qBAAqB,2BAAgB,QAAQ,OAAO;AACzF;",
|
|
6
6
|
"names": ["import_UserSessionIndex"]
|
|
7
7
|
}
|
|
@@ -2,6 +2,6 @@
|
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/matchmaker/Lobby.ts"],
|
|
4
4
|
"sourcesContent": ["import * as matchMaker from '../MatchMaker.ts';\n\nimport type { Room } from '../Room.ts';\nimport type { IRoomCache } from './driver.ts';\n\nconst LOBBY_CHANNEL = '$lobby';\n\n/*\n * TODO: refactor this on 1.0\n *\n * Some users might be relying on \"1\" = \"removed\" from the lobby due to this workaround: https://github.com/colyseus/colyseus/issues/617\n * Though, for consistency, we should invert as \"0\" = \"invisible\" and \"1\" = \"visible\".\n *\n * - rename \"removed\" to \"isVisible\" and swap the logic\n * - emit \"visibility-change\" with inverted value (isVisible)\n * - update \"subscribeLobby\" to check \"1\" as \"isVisible\" instead of \"removed\"\n */\n\nexport function updateLobby<T extends Room>(room: T, removed: boolean = false) {\n const listing = room['_listing'];\n\n if (listing.unlisted || !listing.roomId) {\n return;\n }\n\n if (removed) {\n matchMaker.presence.publish(LOBBY_CHANNEL, `${listing.roomId},1`);\n } else if (!listing.private) {\n matchMaker.presence.publish(LOBBY_CHANNEL, `${listing.roomId},0`);\n }\n}\n\nexport async function subscribeLobby(callback: (roomId: string, roomListing: IRoomCache) => void) {\n // Track removed roomIds to prevent race conditions where pending queries\n // complete after a room has been removed\n const removedRoomIds = new Set<string>();\n\n const cb = async (message: string) => {\n const [roomId, isRemove] = message.split(',');\n\n if (isRemove === '1') {\n // Mark as removed and process immediately\n removedRoomIds.add(roomId);\n callback(roomId, null);\n\n // Clean up after a short timeout to prevent memory leaks\n setTimeout(() => removedRoomIds.delete(roomId), 2000);\n\n } else {\n // Clear removed status - room might be coming back (e.g., visibility change)\n removedRoomIds.delete(roomId);\n\n const room = (await matchMaker.query({ roomId }))[0];\n\n // Check if room was removed while we were querying\n // See \"updating metadata should not cause race condition\" test in LobbyRoom.test.ts\n if (removedRoomIds.has(roomId)) {\n return;\n }\n\n callback(roomId, room);\n }\n };\n\n await matchMaker.presence.subscribe(LOBBY_CHANNEL, cb);\n\n return () => matchMaker.presence.unsubscribe(LOBBY_CHANNEL, cb);\n}\n"],
|
|
5
|
-
"mappings": "
|
|
5
|
+
"mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,iBAA4B;AAK5B,IAAM,gBAAgB;AAaf,SAAS,YAA4B,MAAS,UAAmB,OAAO;AAC7E,QAAM,UAAU,KAAK,UAAU;AAE/B,MAAI,QAAQ,YAAY,CAAC,QAAQ,QAAQ;AACvC;AAAA,EACF;AAEA,MAAI,SAAS;AACX,IAAW,oBAAS,QAAQ,eAAe,GAAG,QAAQ,MAAM,IAAI;AAAA,EAClE,WAAW,CAAC,QAAQ,SAAS;AAC3B,IAAW,oBAAS,QAAQ,eAAe,GAAG,QAAQ,MAAM,IAAI;AAAA,EAClE;AACF;AAEA,eAAsB,eAAe,UAA6D;AAGhG,QAAM,iBAAiB,oBAAI,IAAY;AAEvC,QAAM,KAAK,OAAO,YAAoB;AACpC,UAAM,CAAC,QAAQ,QAAQ,IAAI,QAAQ,MAAM,GAAG;AAE5C,QAAI,aAAa,KAAK;AAEpB,qBAAe,IAAI,MAAM;AACzB,eAAS,QAAQ,IAAI;AAGrB,iBAAW,MAAM,eAAe,OAAO,MAAM,GAAG,GAAI;AAAA,IAEtD,OAAO;AAEL,qBAAe,OAAO,MAAM;AAE5B,YAAM,QAAQ,MAAiB,iBAAM,EAAE,OAAO,CAAC,GAAG,CAAC;AAInD,UAAI,eAAe,IAAI,MAAM,GAAG;AAC9B;AAAA,MACF;AAEA,eAAS,QAAQ,IAAI;AAAA,IACvB;AAAA,EACF;AAEA,QAAiB,oBAAS,UAAU,eAAe,EAAE;AAErD,SAAO,MAAiB,oBAAS,YAAY,eAAe,EAAE;AAChE;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -2,6 +2,6 @@
|
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/matchmaker/LocalDriver/LocalDriver.ts"],
|
|
4
4
|
"sourcesContent": ["import { debugMatchMaking } from '../../Debug.ts';\nimport type { IRoomCache, SortOptions, MatchMakerDriver } from '../driver.ts';\nimport { Query } from './Query.ts';\n\n// re-export\nexport type { IRoomCache, SortOptions, MatchMakerDriver };\n\nexport class LocalDriver implements MatchMakerDriver {\n public rooms: IRoomCache[] = [];\n\n public has(roomId: string) {\n return this.rooms.some((room) => room.roomId === roomId);\n }\n\n public query(conditions: Partial<IRoomCache>, sortOptions?: SortOptions) {\n const query = new Query<IRoomCache>(this.rooms, conditions);\n\n if (sortOptions) {\n query.sort(sortOptions);\n }\n\n return query.filter(conditions);\n }\n\n public cleanup(processId: string) {\n const cachedRooms = this.query({ processId });\n debugMatchMaking(\"removing stale rooms by processId %s (%s rooms found)\", processId, cachedRooms.length);\n\n cachedRooms.forEach((room) => this.remove(room.roomId));\n return Promise.resolve();\n }\n\n public findOne(conditions: Partial<IRoomCache>, sortOptions?: SortOptions) {\n const query = new Query<IRoomCache>(this.rooms, conditions);\n\n if (sortOptions) {\n query.sort(sortOptions);\n }\n\n return query as unknown as Promise<IRoomCache>;\n }\n\n public async findByIds(roomIds: string[]): Promise<Map<string, IRoomCache>> {\n const result = new Map<string, IRoomCache>();\n if (roomIds.length === 0) { return result; }\n const wanted = new Set(roomIds);\n for (const room of this.rooms) {\n if (wanted.has(room.roomId)) { result.set(room.roomId, room); }\n }\n return result;\n }\n\n public update(room: IRoomCache, operations: Partial<{ $set: Partial<IRoomCache>, $inc: Partial<IRoomCache> }>) {\n if (operations.$set) {\n for (const field in operations.$set) {\n if (operations.$set.hasOwnProperty(field)) {\n room[field] = operations.$set[field];\n }\n }\n }\n\n if (operations.$inc) {\n for (const field in operations.$inc) {\n if (operations.$inc.hasOwnProperty(field)) {\n room[field] += operations.$inc[field];\n }\n }\n }\n\n return true;\n }\n\n public persist(room: IRoomCache, create: boolean = false) {\n // if (this.rooms.indexOf(room) !== -1) {\n // // already in the list\n // return true;\n // }\n\n if (!create) { return false; }\n\n // add to the list\n this.rooms.push(room);\n\n return true;\n }\n\n public remove(roomId: string) {\n const roomIndex = this.rooms.findIndex((room) => room.roomId === roomId);\n if (roomIndex !== -1) {\n this.rooms.splice(roomIndex, 1);\n return true;\n }\n return false;\n }\n\n public clear() {\n this.rooms = [];\n }\n\n public shutdown() {\n }\n}\n"],
|
|
5
|
-
"mappings": "
|
|
5
|
+
"mappings": ";;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,mBAAiC;AAEjC,mBAAsB;AAKf,IAAM,cAAN,MAA8C;AAAA,EAA9C;AACL,SAAO,QAAsB,CAAC;AAAA;AAAA,EAEvB,IAAI,QAAgB;AACzB,WAAO,KAAK,MAAM,KAAK,CAAC,SAAS,KAAK,WAAW,MAAM;AAAA,EACzD;AAAA,EAEO,MAAM,YAAiC,aAA2B;AACvE,UAAM,QAAQ,IAAI,mBAAkB,KAAK,OAAO,UAAU;AAE1D,QAAI,aAAa;AACf,YAAM,KAAK,WAAW;AAAA,IACxB;AAEA,WAAO,MAAM,OAAO,UAAU;AAAA,EAChC;AAAA,EAEO,QAAQ,WAAmB;AAChC,UAAM,cAAc,KAAK,MAAM,EAAE,UAAU,CAAC;AAC5C,uCAAiB,yDAAyD,WAAW,YAAY,MAAM;AAEvG,gBAAY,QAAQ,CAAC,SAAS,KAAK,OAAO,KAAK,MAAM,CAAC;AACtD,WAAO,QAAQ,QAAQ;AAAA,EACzB;AAAA,EAEO,QAAQ,YAAiC,aAA2B;AACzE,UAAM,QAAQ,IAAI,mBAAkB,KAAK,OAAO,UAAU;AAE1D,QAAI,aAAa;AACf,YAAM,KAAK,WAAW;AAAA,IACxB;AAEA,WAAO;AAAA,EACT;AAAA,EAEA,MAAa,UAAU,SAAqD;AAC1E,UAAM,SAAS,oBAAI,IAAwB;AAC3C,QAAI,QAAQ,WAAW,GAAG;AAAE,aAAO;AAAA,IAAQ;AAC3C,UAAM,SAAS,IAAI,IAAI,OAAO;AAC9B,eAAW,QAAQ,KAAK,OAAO;AAC7B,UAAI,OAAO,IAAI,KAAK,MAAM,GAAG;AAAE,eAAO,IAAI,KAAK,QAAQ,IAAI;AAAA,MAAG;AAAA,IAChE;AACA,WAAO;AAAA,EACT;AAAA,EAEO,OAAO,MAAkB,YAA+E;AAC7G,QAAI,WAAW,MAAM;AACnB,iBAAW,SAAS,WAAW,MAAM;AACnC,YAAI,WAAW,KAAK,eAAe,KAAK,GAAG;AACzC,eAAK,KAAK,IAAI,WAAW,KAAK,KAAK;AAAA,QACrC;AAAA,MACF;AAAA,IACF;AAEA,QAAI,WAAW,MAAM;AACnB,iBAAW,SAAS,WAAW,MAAM;AACnC,YAAI,WAAW,KAAK,eAAe,KAAK,GAAG;AACzC,eAAK,KAAK,KAAK,WAAW,KAAK,KAAK;AAAA,QACtC;AAAA,MACF;AAAA,IACF;AAEA,WAAO;AAAA,EACT;AAAA,EAEO,QAAQ,MAAkB,SAAkB,OAAO;AAMxD,QAAI,CAAC,QAAQ;AAAE,aAAO;AAAA,IAAO;AAG7B,SAAK,MAAM,KAAK,IAAI;AAEpB,WAAO;AAAA,EACT;AAAA,EAEO,OAAO,QAAgB;AAC5B,UAAM,YAAY,KAAK,MAAM,UAAU,CAAC,SAAS,KAAK,WAAW,MAAM;AACvE,QAAI,cAAc,IAAI;AACpB,WAAK,MAAM,OAAO,WAAW,CAAC;AAC9B,aAAO;AAAA,IACT;AACA,WAAO;AAAA,EACT;AAAA,EAEO,QAAQ;AACb,SAAK,QAAQ,CAAC;AAAA,EAChB;AAAA,EAEO,WAAW;AAAA,EAClB;AACF;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -2,6 +2,6 @@
|
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/matchmaker/LocalDriver/Query.ts"],
|
|
4
4
|
"sourcesContent": ["import type { IRoomCache, SortOptions } from '../driver.ts';\n\nexport class Query<T extends IRoomCache> {\n private $rooms: T[];\n private conditions: any;\n private sortOptions?: SortOptions;\n\n constructor(rooms: any[], conditions) {\n this.$rooms = rooms.slice(0);\n this.conditions = conditions;\n }\n\n public sort(options: SortOptions) {\n // Store sort options instead of sorting immediately\n // This allows filtering first, then sorting fewer items\n this.sortOptions = options;\n return this;\n }\n\n private applySort(rooms: T[]): T[] {\n if (!this.sortOptions) {\n return rooms;\n }\n\n return rooms.sort((room1: T, room2: T) => {\n for (const field in this.sortOptions) {\n const direction = this.sortOptions[field];\n if (room1.hasOwnProperty(field)) {\n /**\n * IRoomCache field\n */\n if (direction === 1 || direction === 'asc' || direction === 'ascending') {\n if (room1[field] > room2[field]) { return 1; }\n if (room1[field] < room2[field]) { return -1; }\n\n } else {\n if (room1[field] > room2[field]) { return -1; }\n if (room1[field] < room2[field]) { return 1; }\n }\n } else if (room1.metadata?.hasOwnProperty(field)) {\n /**\n * metadata field\n */\n if (direction === 1 || direction === 'asc' || direction === 'ascending') {\n if (room1.metadata[field] > room2.metadata[field]) { return 1; }\n if (room1.metadata[field] < room2.metadata[field]) { return -1; }\n } else {\n if (room1.metadata[field] > room2.metadata[field]) { return -1; }\n if (room1.metadata[field] < room2.metadata[field]) { return 1; }\n }\n }\n }\n return 0;\n });\n }\n\n private applyFilter(rooms: T[], conditions: any): T[] {\n return rooms.filter(((room) => {\n for (const field in conditions) {\n if (conditions.hasOwnProperty(field)) {\n // Check if field exists in room (IRoomCache base fields)\n if (room.hasOwnProperty(field)) {\n if (room[field] !== conditions[field]) {\n return false;\n }\n } else if (room.metadata?.hasOwnProperty(field)) {\n // Check if field exists in metadata\n if (room.metadata[field] !== conditions[field]) {\n return false;\n }\n } else {\n // Field doesn't exist in room or metadata\n return false;\n }\n }\n }\n return true;\n }));\n }\n\n public filter(conditions: any) {\n // Filter first to reduce the number of items to sort\n const filtered = this.applyFilter(this.$rooms, conditions);\n return this.applySort(filtered);\n }\n\n public then(resolve, reject) {\n // Filter first to reduce the number of items to sort\n const filtered = this.applyFilter(this.$rooms, this.conditions);\n const sorted = this.applySort(filtered);\n const result: any = sorted[0];\n return resolve(result);\n }\n}\n"],
|
|
5
|
-
"mappings": "
|
|
5
|
+
"mappings": ";;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAEO,IAAM,QAAN,MAAkC;AAAA,EAKvC,YAAY,OAAc,YAAY;AACpC,SAAK,SAAS,MAAM,MAAM,CAAC;AAC3B,SAAK,aAAa;AAAA,EACpB;AAAA,EAEO,KAAK,SAAsB;AAGhC,SAAK,cAAc;AACnB,WAAO;AAAA,EACT;AAAA,EAEQ,UAAU,OAAiB;AACjC,QAAI,CAAC,KAAK,aAAa;AACrB,aAAO;AAAA,IACT;AAEA,WAAO,MAAM,KAAK,CAAC,OAAU,UAAa;AACxC,iBAAW,SAAS,KAAK,aAAa;AACpC,cAAM,YAAY,KAAK,YAAY,KAAK;AACxC,YAAI,MAAM,eAAe,KAAK,GAAG;AAI/B,cAAI,cAAc,KAAK,cAAc,SAAS,cAAc,aAAa;AACvE,gBAAI,MAAM,KAAK,IAAI,MAAM,KAAK,GAAG;AAAE,qBAAO;AAAA,YAAG;AAC7C,gBAAI,MAAM,KAAK,IAAI,MAAM,KAAK,GAAG;AAAE,qBAAO;AAAA,YAAI;AAAA,UAEhD,OAAO;AACL,gBAAI,MAAM,KAAK,IAAI,MAAM,KAAK,GAAG;AAAE,qBAAO;AAAA,YAAI;AAC9C,gBAAI,MAAM,KAAK,IAAI,MAAM,KAAK,GAAG;AAAE,qBAAO;AAAA,YAAG;AAAA,UAC/C;AAAA,QACF,WAAW,MAAM,UAAU,eAAe,KAAK,GAAG;AAIhD,cAAI,cAAc,KAAK,cAAc,SAAS,cAAc,aAAa;AACvE,gBAAI,MAAM,SAAS,KAAK,IAAI,MAAM,SAAS,KAAK,GAAG;AAAE,qBAAO;AAAA,YAAG;AAC/D,gBAAI,MAAM,SAAS,KAAK,IAAI,MAAM,SAAS,KAAK,GAAG;AAAE,qBAAO;AAAA,YAAI;AAAA,UAClE,OAAO;AACL,gBAAI,MAAM,SAAS,KAAK,IAAI,MAAM,SAAS,KAAK,GAAG;AAAE,qBAAO;AAAA,YAAI;AAChE,gBAAI,MAAM,SAAS,KAAK,IAAI,MAAM,SAAS,KAAK,GAAG;AAAE,qBAAO;AAAA,YAAG;AAAA,UACjE;AAAA,QACF;AAAA,MACF;AACA,aAAO;AAAA,IACT,CAAC;AAAA,EACH;AAAA,EAEQ,YAAY,OAAY,YAAsB;AACpD,WAAO,MAAM,QAAQ,CAAC,SAAS;AAC7B,iBAAW,SAAS,YAAY;AAC9B,YAAI,WAAW,eAAe,KAAK,GAAG;AAEpC,cAAI,KAAK,eAAe,KAAK,GAAG;AAC9B,gBAAI,KAAK,KAAK,MAAM,WAAW,KAAK,GAAG;AACrC,qBAAO;AAAA,YACT;AAAA,UACF,WAAW,KAAK,UAAU,eAAe,KAAK,GAAG;AAE/C,gBAAI,KAAK,SAAS,KAAK,MAAM,WAAW,KAAK,GAAG;AAC9C,qBAAO;AAAA,YACT;AAAA,UACF,OAAO;AAEL,mBAAO;AAAA,UACT;AAAA,QACF;AAAA,MACF;AACA,aAAO;AAAA,IACT,EAAE;AAAA,EACJ;AAAA,EAEO,OAAO,YAAiB;AAE7B,UAAM,WAAW,KAAK,YAAY,KAAK,QAAQ,UAAU;AACzD,WAAO,KAAK,UAAU,QAAQ;AAAA,EAChC;AAAA,EAEO,KAAK,SAAS,QAAQ;AAE3B,UAAM,WAAW,KAAK,YAAY,KAAK,QAAQ,KAAK,UAAU;AAC9D,UAAM,SAAS,KAAK,UAAU,QAAQ;AACtC,UAAM,SAAc,OAAO,CAAC;AAC5B,WAAO,QAAQ,MAAM;AAAA,EACvB;AACF;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -2,6 +2,6 @@
|
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/matchmaker/RegisteredHandler.ts"],
|
|
4
4
|
"sourcesContent": ["import { EventEmitter } from 'events';\nimport { logger } from '../Logger.ts';\nimport { Room } from './../Room.ts';\nimport { updateLobby } from './Lobby.ts';\n\nimport type { IRoomCache, SortOptions, IRoomCacheFilterByKeys, IRoomCacheSortByKeys, ExtractRoomCacheMetadata } from './driver.ts';\nimport type { Client } from '../Transport.ts';\nimport type { Type } from \"../utils/Utils.ts\";\n\nexport const INVALID_OPTION_KEYS: Array<keyof IRoomCache> = [\n 'clients',\n 'locked',\n 'private',\n // 'maxClients', - maxClients can be useful as filter options\n 'metadata',\n 'name',\n 'processId',\n 'roomId',\n];\n\n/**\n * Type for filterBy that supports both onCreate options and metadata fields\n */\ntype FilterByKeys<RoomType extends Room> =\n | IRoomCacheFilterByKeys\n | (ExtractRoomCacheMetadata<RoomType> extends object\n ? keyof ExtractRoomCacheMetadata<RoomType> & string\n : never)\n\n/**\n * Type for sortBy that supports room cache fields and metadata fields\n */\ntype SortByKeys<RoomType extends Room> =\n | IRoomCacheSortByKeys\n | (ExtractRoomCacheMetadata<RoomType> extends object\n ? keyof ExtractRoomCacheMetadata<RoomType> & string\n : never);\n\nexport interface RegisteredHandlerEvents<RoomType extends Room = any> {\n create: [room: RoomType];\n lock: [room: RoomType];\n unlock: [room: RoomType];\n join: [room: RoomType, client: Client];\n leave: [room: RoomType, client: Client, willDispose: boolean];\n dispose: [room: RoomType];\n 'visibility-change': [room: RoomType, isVisible: boolean];\n 'metadata-change': [room: RoomType];\n}\n\nexport class RegisteredHandler<\n RoomType extends Room = any\n> extends EventEmitter<RegisteredHandlerEvents<RoomType>> {\n '~room': RoomType;\n\n public klass: Type<RoomType>;\n public options: any;\n\n public name: string;\n public filterOptions: Array<FilterByKeys<RoomType>> = [];\n public sortOptions?: SortOptions;\n\n public realtimeListingEnabled: boolean = false;\n\n constructor(klass: Type<RoomType>, options?: any) {\n super();\n\n this.klass = klass;\n this.options = options;\n\n if (typeof(klass) !== 'function') {\n logger.debug('You are likely not importing your room class correctly.');\n throw new Error(`class is expected but ${typeof(klass)} was provided.`);\n }\n }\n\n public enableRealtimeListing() {\n this.realtimeListingEnabled = true;\n this.on('create', (room) => updateLobby(room));\n this.on('lock', (room) => updateLobby(room));\n this.on('unlock', (room) => updateLobby(room));\n this.on('join', (room) => updateLobby(room));\n this.on('leave', (room, _, willDispose) => {\n if (!willDispose) {\n updateLobby(room);\n }\n });\n this.on('visibility-change', (room, isVisible) => updateLobby(room, isVisible));\n this.on('metadata-change', (room) => updateLobby(room));\n this.on('dispose', (room) => updateLobby(room, true));\n return this;\n }\n\n /**\n * Define which fields should be used for filtering rooms.\n * Supports both onCreate options and metadata fields using dot notation.\n *\n * @example\n * // Filter by IRoomCache fields\n * .filterBy(['maxClients'])\n *\n * @example\n * // Filter by metadata fields\n * .filterBy(['difficulty', 'metadata.region'])\n *\n * @example\n * // Mix both\n * .filterBy(['mode', 'difficulty', 'maxClients'])\n */\n public filterBy<T extends FilterByKeys<RoomType>>(\n options: T[]\n ) {\n this.filterOptions = options;\n return this;\n }\n\n /**\n * Define how rooms should be sorted when querying.\n * Supports both room cache fields and metadata fields using dot notation.\n *\n * @example\n * // Sort by number of clients (descending)\n * .sortBy({ clients: -1 })\n *\n * @example\n * // Sort by metadata field\n * .sortBy({ 'metadata.rating': -1 })\n *\n * @example\n * // Multiple sort criteria\n * .sortBy({ 'metadata.skillLevel': 1, clients: -1 })\n */\n public sortBy<T extends SortByKeys<RoomType>>(\n options: { [K in T]: SortOptions[string] }\n ): this {\n this.sortOptions = options as unknown as SortOptions;\n return this;\n }\n\n public getMetadataFromOptions(options: any) {\n const metadata = this.getFilterOptions(options);\n\n if (this.sortOptions) {\n for (const field in this.sortOptions) {\n if (field in options && !(field in metadata)) {\n metadata[field] = options[field];\n }\n }\n }\n\n return Object.keys(metadata).length > 0 ? { metadata } : {};\n }\n\n /**\n * Extract filter options from client options.\n */\n public getFilterOptions(options: any) {\n return this.filterOptions.reduce((prev, curr, i, arr) => {\n const field = String(arr[i]);\n\n // Handle regular (non-metadata) fields\n if (options.hasOwnProperty(field)) {\n if (INVALID_OPTION_KEYS.indexOf(field as any) !== -1) {\n logger.warn(`option \"${field}\" has internal usage and is going to be ignored.`);\n } else {\n prev[field] = options[field];\n }\n }\n\n return prev;\n }, {});\n }\n}\n"],
|
|
5
|
-
"mappings": "
|
|
5
|
+
"mappings": ";;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,oBAA6B;AAC7B,oBAAuB;AACvB,kBAAqB;AACrB,mBAA4B;AAMrB,IAAM,sBAA+C;AAAA,EAC1D;AAAA,EACA;AAAA,EACA;AAAA;AAAA,EAEA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AA+BO,IAAM,oBAAN,cAEG,2BAAgD;AAAA,EAYxD,YAAY,OAAuB,SAAe;AAChD,UAAM;AANR,SAAO,gBAA+C,CAAC;AAGvD,SAAO,yBAAkC;AAKvC,SAAK,QAAQ;AACb,SAAK,UAAU;AAEf,QAAI,OAAO,UAAW,YAAY;AAChC,2BAAO,MAAM,yDAAyD;AACtE,YAAM,IAAI,MAAM,yBAAyB,OAAO,KAAM,gBAAgB;AAAA,IACxE;AAAA,EACF;AAAA,EAEO,wBAAwB;AAC7B,SAAK,yBAAyB;AAC9B,SAAK,GAAG,UAAU,CAAC,aAAS,0BAAY,IAAI,CAAC;AAC7C,SAAK,GAAG,QAAQ,CAAC,aAAS,0BAAY,IAAI,CAAC;AAC3C,SAAK,GAAG,UAAU,CAAC,aAAS,0BAAY,IAAI,CAAC;AAC7C,SAAK,GAAG,QAAQ,CAAC,aAAS,0BAAY,IAAI,CAAC;AAC3C,SAAK,GAAG,SAAS,CAAC,MAAM,GAAG,gBAAgB;AACzC,UAAI,CAAC,aAAa;AAChB,sCAAY,IAAI;AAAA,MAClB;AAAA,IACF,CAAC;AACD,SAAK,GAAG,qBAAqB,CAAC,MAAM,kBAAc,0BAAY,MAAM,SAAS,CAAC;AAC9E,SAAK,GAAG,mBAAmB,CAAC,aAAS,0BAAY,IAAI,CAAC;AACtD,SAAK,GAAG,WAAW,CAAC,aAAS,0BAAY,MAAM,IAAI,CAAC;AACpD,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAkBO,SACL,SACA;AACA,SAAK,gBAAgB;AACrB,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAkBO,OACL,SACM;AACN,SAAK,cAAc;AACnB,WAAO;AAAA,EACT;AAAA,EAEO,uBAAuB,SAAc;AAC1C,UAAM,WAAW,KAAK,iBAAiB,OAAO;AAE9C,QAAI,KAAK,aAAa;AACpB,iBAAW,SAAS,KAAK,aAAa;AACpC,YAAI,SAAS,WAAW,EAAE,SAAS,WAAW;AAC5C,mBAAS,KAAK,IAAI,QAAQ,KAAK;AAAA,QACjC;AAAA,MACF;AAAA,IACF;AAEA,WAAO,OAAO,KAAK,QAAQ,EAAE,SAAS,IAAI,EAAE,SAAS,IAAI,CAAC;AAAA,EAC5D;AAAA;AAAA;AAAA;AAAA,EAKO,iBAAiB,SAAc;AACpC,WAAO,KAAK,cAAc,OAAO,CAAC,MAAM,MAAM,GAAG,QAAQ;AACvD,YAAM,QAAQ,OAAO,IAAI,CAAC,CAAC;AAG3B,UAAI,QAAQ,eAAe,KAAK,GAAG;AACjC,YAAI,oBAAoB,QAAQ,KAAY,MAAM,IAAI;AACpD,+BAAO,KAAK,WAAW,KAAK,kDAAkD;AAAA,QAChF,OAAO;AACL,eAAK,KAAK,IAAI,QAAQ,KAAK;AAAA,QAC7B;AAAA,MACF;AAEA,aAAO;AAAA,IACT,GAAG,CAAC,CAAC;AAAA,EACP;AACF;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -2,6 +2,6 @@
|
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/matchmaker/controller.ts"],
|
|
4
4
|
"sourcesContent": ["/**\n * Matchmaking controller\n * (for interoperability between different http frameworks, e.g. express, uWebSockets.js, etc)\n */\n\nimport { ErrorCode } from '@colyseus/shared-types';\nimport { ServerError } from '../errors/ServerError.ts';\nimport { debugError } from '../Debug.ts';\nimport * as matchMaker from '../MatchMaker.ts';\nimport type { AuthContext } from '../Transport.ts';\n\nexport const controller = {\n DEFAULT_CORS_HEADERS: {\n 'Access-Control-Allow-Headers': 'Origin, X-Requested-With, Content-Type, Accept, Authorization',\n 'Access-Control-Allow-Methods': 'GET,HEAD,PUT,PATCH,POST,DELETE',\n 'Access-Control-Allow-Credentials': 'true',\n 'Access-Control-Allow-Origin': '*',\n 'Access-Control-Max-Age': '2592000',\n // ...\n },\n\n exposedMethods: ['joinOrCreate', 'create', 'join', 'joinById', 'reconnect'],\n allowedRoomNameChars: /([a-zA-Z_\\-0-9]+)/gi,\n matchmakeRoute: 'matchmake',\n\n /**\n * You can manually change the default corsHeaders by overwriting the `getCorsHeaders()` method:\n * ```\n * import { matchMaker } from \"@colyseus/core\";\n * matchMaker.controller.getCorsHeaders = function(headers) {\n * if (headers.get('referer') !== \"xxx\") {\n * }\n *\n * return {\n * 'Access-Control-Allow-Origin': 'safedomain.com',\n * }\n * }\n * ```\n */\n getCorsHeaders(headers: Headers): { [header: string]: string } {\n return {\n ['Access-Control-Allow-Origin']: headers.get(\"origin\") || \"*\",\n };\n },\n\n async invokeMethod(\n method: string,\n roomName: string,\n clientOptions: matchMaker.ClientOptions = {},\n authOptions?: AuthContext,\n ) {\n if (this.exposedMethods.indexOf(method) === -1) {\n throw new ServerError(ErrorCode.MATCHMAKE_NO_HANDLER, `invalid method \"${method}\"`);\n }\n\n try {\n return await matchMaker[method](roomName, clientOptions, authOptions);\n\n } catch (e: any) {\n debugError(e);\n throw new ServerError(e.code || ErrorCode.MATCHMAKE_UNHANDLED, e.message);\n }\n }\n\n}\n\n"],
|
|
5
|
-
"mappings": "
|
|
5
|
+
"mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAKA,0BAA0B;AAC1B,yBAA4B;AAC5B,mBAA2B;AAC3B,iBAA4B;AAGrB,IAAM,aAAa;AAAA,EACxB,sBAAsB;AAAA,IACpB,gCAAgC;AAAA,IAChC,gCAAgC;AAAA,IAChC,oCAAoC;AAAA,IACpC,+BAA+B;AAAA,IAC/B,0BAA0B;AAAA;AAAA,EAE5B;AAAA,EAEA,gBAAgB,CAAC,gBAAgB,UAAU,QAAQ,YAAY,WAAW;AAAA,EAC1E,sBAAsB;AAAA,EACtB,gBAAgB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAgBhB,eAAe,SAAgD;AAC7D,WAAO;AAAA,MACL,CAAC,6BAA6B,GAAG,QAAQ,IAAI,QAAQ,KAAK;AAAA,IAC5D;AAAA,EACF;AAAA,EAEA,MAAM,aACJ,QACA,UACA,gBAA0C,CAAC,GAC3C,aACA;AACA,QAAI,KAAK,eAAe,QAAQ,MAAM,MAAM,IAAI;AAC9C,YAAM,IAAI,+BAAY,8BAAU,sBAAsB,mBAAmB,MAAM,GAAG;AAAA,IACpF;AAEA,QAAI;AACF,aAAO,MAAM,WAAW,MAAM,EAAE,UAAU,eAAe,WAAW;AAAA,IAEtE,SAAS,GAAQ;AACf,mCAAW,CAAC;AACZ,YAAM,IAAI,+BAAY,EAAE,QAAQ,8BAAU,qBAAqB,EAAE,OAAO;AAAA,IAC1E;AAAA,EACF;AAEF;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/matchmaker/driver.ts"],
|
|
4
|
-
"sourcesContent": ["import type { Room } from \"@colyseus/core\";\n\n/**\n * Sort options for room queries.\n */\nexport interface SortOptions {\n [fieldName: string]: 1 | -1 | 'asc' | 'desc' | 'ascending' | 'descending';\n}\n\n/**\n * Built-in room cache fields that can be used for sorting.\n */\nexport type IRoomCacheSortByKeys = 'clients' | 'maxClients' | 'createdAt';\n\n/**\n * Built-in room cache fields that can be used for filtering.\n */\nexport type IRoomCacheFilterByKeys = 'clients' | 'maxClients' | 'processId';\n\n/**\n * Extract metadata type from Room type\n */\nexport type ExtractRoomCacheMetadata<RoomType extends Room> = RoomType['~metadata'];\n\n/**\n * Generates a unique lock ID based on filter options.\n */\nexport function getLockId(filterOptions: any) {\n return Object.keys(filterOptions).map((key) => `${key}:${filterOptions[key]}`).join(\"-\");\n}\n\n/**\n * Initialize a room cache which contains CRUD operations for room listings.\n *\n * @internal\n * @param initialValues - Predefined room properties.\n * @returns RoomData - New room cache.\n */\nexport function initializeRoomCache(initialValues: Partial<IRoomCache> = {}): IRoomCache {\n return {\n clients: 0,\n maxClients: Infinity,\n locked: false,\n private: false,\n metadata: undefined,\n createdAt: (initialValues && initialValues.createdAt) ? new Date(initialValues.createdAt) : new Date(),\n unlisted: false,\n ...initialValues,\n } as IRoomCache;\n}\n\nexport interface IRoomCache<Metadata = any> {\n /**\n * Room name.\n */\n name: string;\n\n /**\n * Unique identifier for the room.\n */\n roomId: string;\n\n /**\n * Process id where the room is running.\n */\n processId: string;\n\n /**\n * Number of clients connected to this room.\n */\n clients: number;\n\n /**\n * Maximum number of clients allowed to join the room.\n */\n maxClients: number;\n\n /**\n * Indicates if the room is locked (i.e. join requests are rejected).\n */\n locked?: boolean;\n\n /**\n * Indicates if the room is private\n * Private rooms can't be joined via `join()` or `joinOrCreate()`.\n */\n private?: boolean;\n\n /**\n * Public address of the server.\n */\n publicAddress?: string;\n\n /**\n * Do not show this room in lobby listing.\n */\n unlisted?: boolean;\n\n /**\n * Metadata associated with the room.\n */\n metadata?: Metadata;\n\n /**\n * When the room was created.\n */\n createdAt?: Date;\n}\n\nexport interface MatchMakerDriver {\n /**\n * Check if a room exists in room cache.\n *\n * @param roomId - The room id.\n *\n * @returns Promise<boolean> | boolean - A promise or a boolean value indicating if the room exists.\n */\n has(roomId: string): Promise<boolean> | boolean;\n\n /**\n * Query rooms in room cache for given conditions.\n *\n * @param conditions - Filtering conditions.\n *\n * @returns Promise<IRoomCache[]> | IRoomCache[] - A promise or an object contaning room metadata list.\n */\n query<T extends Room = any>(\n conditions: Partial<IRoomCache & ExtractRoomCacheMetadata<T>>,\n sortOptions?: SortOptions\n ): Promise<Array<IRoomCache<ExtractRoomCacheMetadata<T>>>> | Array<IRoomCache<ExtractRoomCacheMetadata<T>>>;\n\n /**\n * Clean up rooms in room cache by process id.\n * @param processId - The process id.\n */\n cleanup?(processId: string): Promise<void>;\n\n /**\n * Query for a room in room cache for given conditions.\n *\n * @param conditions - Filtering conditions.\n *\n * @returns `IRoomCache` - An object contaning filtered room metadata.\n */\n findOne<T extends Room = any>(\n conditions: Partial<IRoomCache & ExtractRoomCacheMetadata<T>>,\n sortOptions?: SortOptions\n ): Promise<IRoomCache<ExtractRoomCacheMetadata<T>>>;\n\n /**\n * Batch-resolve room caches by roomId in a single backend round\n * trip. Missing roomIds are absent from the returned map. Used by\n * hot paths (per-join uniqueness check, by-user reverse-index\n * lookups) where K serial `findOne` calls would multiply round\n * trips needlessly.\n *\n * @param roomIds - Room ids to look up.\n *\n * @returns Map keyed by roomId of the rooms that were found.\n */\n findByIds(roomIds: string[]): Promise<Map<string, IRoomCache>>;\n\n /**\n * Remove a room from room cache.\n *\n * @param roomId - The room id.\n */\n remove(roomId: string): Promise<boolean> | boolean;\n\n /**\n * Update a room in room cache.\n *\n * @param IRoomCache - The room to update.\n * @param operations - The operations to update the room.\n */\n update(\n room: IRoomCache,\n operations: Partial<{ $set: Partial<IRoomCache>, $inc: Partial<IRoomCache> }>\n ): Promise<boolean> | boolean;\n\n /**\n * Persist a room in room cache.\n *\n * @param room - The room to persist.\n * @param create - If true, create a new record. If false (default), update existing record.\n */\n persist(room: IRoomCache, create?: boolean): Promise<boolean> | boolean;\n\n /**\n * Empty the room cache. Used for testing purposes only.\n * @internal Do not call this method yourself.\n */\n clear(): void
|
|
5
|
-
"mappings": "
|
|
4
|
+
"sourcesContent": ["import type { Room } from \"@colyseus/core\";\n\n/**\n * Sort options for room queries.\n */\nexport interface SortOptions {\n [fieldName: string]: 1 | -1 | 'asc' | 'desc' | 'ascending' | 'descending';\n}\n\n/**\n * Built-in room cache fields that can be used for sorting.\n */\nexport type IRoomCacheSortByKeys = 'clients' | 'maxClients' | 'createdAt';\n\n/**\n * Built-in room cache fields that can be used for filtering.\n */\nexport type IRoomCacheFilterByKeys = 'clients' | 'maxClients' | 'processId';\n\n/**\n * Extract metadata type from Room type\n */\nexport type ExtractRoomCacheMetadata<RoomType extends Room> = RoomType['~metadata'];\n\n/**\n * Generates a unique lock ID based on filter options.\n */\nexport function getLockId(filterOptions: any) {\n return Object.keys(filterOptions).map((key) => `${key}:${filterOptions[key]}`).join(\"-\");\n}\n\n/**\n * Initialize a room cache which contains CRUD operations for room listings.\n *\n * @internal\n * @param initialValues - Predefined room properties.\n * @returns RoomData - New room cache.\n */\nexport function initializeRoomCache(initialValues: Partial<IRoomCache> = {}): IRoomCache {\n return {\n clients: 0,\n maxClients: Infinity,\n locked: false,\n private: false,\n metadata: undefined,\n createdAt: (initialValues && initialValues.createdAt) ? new Date(initialValues.createdAt) : new Date(),\n unlisted: false,\n ...initialValues,\n } as IRoomCache;\n}\n\nexport interface IRoomCache<Metadata = any> {\n /**\n * Room name.\n */\n name: string;\n\n /**\n * Unique identifier for the room.\n */\n roomId: string;\n\n /**\n * Process id where the room is running.\n */\n processId: string;\n\n /**\n * Number of clients connected to this room.\n */\n clients: number;\n\n /**\n * Maximum number of clients allowed to join the room.\n */\n maxClients: number;\n\n /**\n * Indicates if the room is locked (i.e. join requests are rejected).\n */\n locked?: boolean;\n\n /**\n * Indicates if the room is private\n * Private rooms can't be joined via `join()` or `joinOrCreate()`.\n */\n private?: boolean;\n\n /**\n * Public address of the server.\n */\n publicAddress?: string;\n\n /**\n * Do not show this room in lobby listing.\n */\n unlisted?: boolean;\n\n /**\n * Metadata associated with the room.\n */\n metadata?: Metadata;\n\n /**\n * When the room was created.\n */\n createdAt?: Date;\n}\n\nexport interface MatchMakerDriver {\n /**\n * Check if a room exists in room cache.\n *\n * @param roomId - The room id.\n *\n * @returns Promise<boolean> | boolean - A promise or a boolean value indicating if the room exists.\n */\n has(roomId: string): Promise<boolean> | boolean;\n\n /**\n * Query rooms in room cache for given conditions.\n *\n * @param conditions - Filtering conditions.\n *\n * @returns Promise<IRoomCache[]> | IRoomCache[] - A promise or an object contaning room metadata list.\n */\n query<T extends Room = any>(\n conditions: Partial<IRoomCache & ExtractRoomCacheMetadata<T>>,\n sortOptions?: SortOptions\n ): Promise<Array<IRoomCache<ExtractRoomCacheMetadata<T>>>> | Array<IRoomCache<ExtractRoomCacheMetadata<T>>>;\n\n /**\n * Clean up rooms in room cache by process id.\n * @param processId - The process id.\n */\n cleanup?(processId: string): Promise<void>;\n\n /**\n * Query for a room in room cache for given conditions.\n *\n * @param conditions - Filtering conditions.\n *\n * @returns `IRoomCache` - An object contaning filtered room metadata.\n */\n findOne<T extends Room = any>(\n conditions: Partial<IRoomCache & ExtractRoomCacheMetadata<T>>,\n sortOptions?: SortOptions\n ): Promise<IRoomCache<ExtractRoomCacheMetadata<T>>>;\n\n /**\n * Batch-resolve room caches by roomId in a single backend round\n * trip. Missing roomIds are absent from the returned map. Used by\n * hot paths (per-join uniqueness check, by-user reverse-index\n * lookups) where K serial `findOne` calls would multiply round\n * trips needlessly.\n *\n * @param roomIds - Room ids to look up.\n *\n * @returns Map keyed by roomId of the rooms that were found.\n */\n findByIds(roomIds: string[]): Promise<Map<string, IRoomCache>>;\n\n /**\n * Remove a room from room cache.\n *\n * @param roomId - The room id.\n */\n remove(roomId: string): Promise<boolean> | boolean;\n\n /**\n * Update a room in room cache.\n *\n * @param IRoomCache - The room to update.\n * @param operations - The operations to update the room.\n */\n update(\n room: IRoomCache,\n operations: Partial<{ $set: Partial<IRoomCache>, $inc: Partial<IRoomCache> }>\n ): Promise<boolean> | boolean;\n\n /**\n * Persist a room in room cache.\n *\n * @param room - The room to persist.\n * @param create - If true, create a new record. If false (default), update existing record.\n */\n persist(room: IRoomCache, create?: boolean): Promise<boolean> | boolean;\n\n /**\n * Empty the room cache. Used for testing purposes only.\n * May be asynchronous \u2014 callers must await it.\n * @internal Do not call this method yourself.\n */\n clear(): void | Promise<void>;\n\n /**\n * Boot the room cache medium (if available).\n */\n boot?(): Promise<void>;\n\n /**\n * Dispose the connection of the room cache medium.\n * May be asynchronous \u2014 callers must await it.\n */\n shutdown(): void | Promise<void>;\n}\n"],
|
|
5
|
+
"mappings": ";;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AA2BO,SAAS,UAAU,eAAoB;AAC5C,SAAO,OAAO,KAAK,aAAa,EAAE,IAAI,CAAC,QAAQ,GAAG,GAAG,IAAI,cAAc,GAAG,CAAC,EAAE,EAAE,KAAK,GAAG;AACzF;AASO,SAAS,oBAAoB,gBAAqC,CAAC,GAAe;AACvF,SAAO;AAAA,IACL,SAAS;AAAA,IACT,YAAY;AAAA,IACZ,QAAQ;AAAA,IACR,SAAS;AAAA,IACT,UAAU;AAAA,IACV,WAAY,iBAAiB,cAAc,YAAa,IAAI,KAAK,cAAc,SAAS,IAAI,oBAAI,KAAK;AAAA,IACrG,UAAU;AAAA,IACV,GAAG;AAAA,EACL;AACF;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -143,15 +143,17 @@ export interface MatchMakerDriver {
|
|
|
143
143
|
persist(room: IRoomCache, create?: boolean): Promise<boolean> | boolean;
|
|
144
144
|
/**
|
|
145
145
|
* Empty the room cache. Used for testing purposes only.
|
|
146
|
+
* May be asynchronous — callers must await it.
|
|
146
147
|
* @internal Do not call this method yourself.
|
|
147
148
|
*/
|
|
148
|
-
clear(): void
|
|
149
|
+
clear(): void | Promise<void>;
|
|
149
150
|
/**
|
|
150
151
|
* Boot the room cache medium (if available).
|
|
151
152
|
*/
|
|
152
153
|
boot?(): Promise<void>;
|
|
153
154
|
/**
|
|
154
155
|
* Dispose the connection of the room cache medium.
|
|
156
|
+
* May be asynchronous — callers must await it.
|
|
155
157
|
*/
|
|
156
|
-
shutdown(): void
|
|
158
|
+
shutdown(): void | Promise<void>;
|
|
157
159
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/matchmaker/driver.ts"],
|
|
4
|
-
"sourcesContent": ["import type { Room } from \"@colyseus/core\";\n\n/**\n * Sort options for room queries.\n */\nexport interface SortOptions {\n [fieldName: string]: 1 | -1 | 'asc' | 'desc' | 'ascending' | 'descending';\n}\n\n/**\n * Built-in room cache fields that can be used for sorting.\n */\nexport type IRoomCacheSortByKeys = 'clients' | 'maxClients' | 'createdAt';\n\n/**\n * Built-in room cache fields that can be used for filtering.\n */\nexport type IRoomCacheFilterByKeys = 'clients' | 'maxClients' | 'processId';\n\n/**\n * Extract metadata type from Room type\n */\nexport type ExtractRoomCacheMetadata<RoomType extends Room> = RoomType['~metadata'];\n\n/**\n * Generates a unique lock ID based on filter options.\n */\nexport function getLockId(filterOptions: any) {\n return Object.keys(filterOptions).map((key) => `${key}:${filterOptions[key]}`).join(\"-\");\n}\n\n/**\n * Initialize a room cache which contains CRUD operations for room listings.\n *\n * @internal\n * @param initialValues - Predefined room properties.\n * @returns RoomData - New room cache.\n */\nexport function initializeRoomCache(initialValues: Partial<IRoomCache> = {}): IRoomCache {\n return {\n clients: 0,\n maxClients: Infinity,\n locked: false,\n private: false,\n metadata: undefined,\n createdAt: (initialValues && initialValues.createdAt) ? new Date(initialValues.createdAt) : new Date(),\n unlisted: false,\n ...initialValues,\n } as IRoomCache;\n}\n\nexport interface IRoomCache<Metadata = any> {\n /**\n * Room name.\n */\n name: string;\n\n /**\n * Unique identifier for the room.\n */\n roomId: string;\n\n /**\n * Process id where the room is running.\n */\n processId: string;\n\n /**\n * Number of clients connected to this room.\n */\n clients: number;\n\n /**\n * Maximum number of clients allowed to join the room.\n */\n maxClients: number;\n\n /**\n * Indicates if the room is locked (i.e. join requests are rejected).\n */\n locked?: boolean;\n\n /**\n * Indicates if the room is private\n * Private rooms can't be joined via `join()` or `joinOrCreate()`.\n */\n private?: boolean;\n\n /**\n * Public address of the server.\n */\n publicAddress?: string;\n\n /**\n * Do not show this room in lobby listing.\n */\n unlisted?: boolean;\n\n /**\n * Metadata associated with the room.\n */\n metadata?: Metadata;\n\n /**\n * When the room was created.\n */\n createdAt?: Date;\n}\n\nexport interface MatchMakerDriver {\n /**\n * Check if a room exists in room cache.\n *\n * @param roomId - The room id.\n *\n * @returns Promise<boolean> | boolean - A promise or a boolean value indicating if the room exists.\n */\n has(roomId: string): Promise<boolean> | boolean;\n\n /**\n * Query rooms in room cache for given conditions.\n *\n * @param conditions - Filtering conditions.\n *\n * @returns Promise<IRoomCache[]> | IRoomCache[] - A promise or an object contaning room metadata list.\n */\n query<T extends Room = any>(\n conditions: Partial<IRoomCache & ExtractRoomCacheMetadata<T>>,\n sortOptions?: SortOptions\n ): Promise<Array<IRoomCache<ExtractRoomCacheMetadata<T>>>> | Array<IRoomCache<ExtractRoomCacheMetadata<T>>>;\n\n /**\n * Clean up rooms in room cache by process id.\n * @param processId - The process id.\n */\n cleanup?(processId: string): Promise<void>;\n\n /**\n * Query for a room in room cache for given conditions.\n *\n * @param conditions - Filtering conditions.\n *\n * @returns `IRoomCache` - An object contaning filtered room metadata.\n */\n findOne<T extends Room = any>(\n conditions: Partial<IRoomCache & ExtractRoomCacheMetadata<T>>,\n sortOptions?: SortOptions\n ): Promise<IRoomCache<ExtractRoomCacheMetadata<T>>>;\n\n /**\n * Batch-resolve room caches by roomId in a single backend round\n * trip. Missing roomIds are absent from the returned map. Used by\n * hot paths (per-join uniqueness check, by-user reverse-index\n * lookups) where K serial `findOne` calls would multiply round\n * trips needlessly.\n *\n * @param roomIds - Room ids to look up.\n *\n * @returns Map keyed by roomId of the rooms that were found.\n */\n findByIds(roomIds: string[]): Promise<Map<string, IRoomCache>>;\n\n /**\n * Remove a room from room cache.\n *\n * @param roomId - The room id.\n */\n remove(roomId: string): Promise<boolean> | boolean;\n\n /**\n * Update a room in room cache.\n *\n * @param IRoomCache - The room to update.\n * @param operations - The operations to update the room.\n */\n update(\n room: IRoomCache,\n operations: Partial<{ $set: Partial<IRoomCache>, $inc: Partial<IRoomCache> }>\n ): Promise<boolean> | boolean;\n\n /**\n * Persist a room in room cache.\n *\n * @param room - The room to persist.\n * @param create - If true, create a new record. If false (default), update existing record.\n */\n persist(room: IRoomCache, create?: boolean): Promise<boolean> | boolean;\n\n /**\n * Empty the room cache. Used for testing purposes only.\n * @internal Do not call this method yourself.\n */\n clear(): void
|
|
4
|
+
"sourcesContent": ["import type { Room } from \"@colyseus/core\";\n\n/**\n * Sort options for room queries.\n */\nexport interface SortOptions {\n [fieldName: string]: 1 | -1 | 'asc' | 'desc' | 'ascending' | 'descending';\n}\n\n/**\n * Built-in room cache fields that can be used for sorting.\n */\nexport type IRoomCacheSortByKeys = 'clients' | 'maxClients' | 'createdAt';\n\n/**\n * Built-in room cache fields that can be used for filtering.\n */\nexport type IRoomCacheFilterByKeys = 'clients' | 'maxClients' | 'processId';\n\n/**\n * Extract metadata type from Room type\n */\nexport type ExtractRoomCacheMetadata<RoomType extends Room> = RoomType['~metadata'];\n\n/**\n * Generates a unique lock ID based on filter options.\n */\nexport function getLockId(filterOptions: any) {\n return Object.keys(filterOptions).map((key) => `${key}:${filterOptions[key]}`).join(\"-\");\n}\n\n/**\n * Initialize a room cache which contains CRUD operations for room listings.\n *\n * @internal\n * @param initialValues - Predefined room properties.\n * @returns RoomData - New room cache.\n */\nexport function initializeRoomCache(initialValues: Partial<IRoomCache> = {}): IRoomCache {\n return {\n clients: 0,\n maxClients: Infinity,\n locked: false,\n private: false,\n metadata: undefined,\n createdAt: (initialValues && initialValues.createdAt) ? new Date(initialValues.createdAt) : new Date(),\n unlisted: false,\n ...initialValues,\n } as IRoomCache;\n}\n\nexport interface IRoomCache<Metadata = any> {\n /**\n * Room name.\n */\n name: string;\n\n /**\n * Unique identifier for the room.\n */\n roomId: string;\n\n /**\n * Process id where the room is running.\n */\n processId: string;\n\n /**\n * Number of clients connected to this room.\n */\n clients: number;\n\n /**\n * Maximum number of clients allowed to join the room.\n */\n maxClients: number;\n\n /**\n * Indicates if the room is locked (i.e. join requests are rejected).\n */\n locked?: boolean;\n\n /**\n * Indicates if the room is private\n * Private rooms can't be joined via `join()` or `joinOrCreate()`.\n */\n private?: boolean;\n\n /**\n * Public address of the server.\n */\n publicAddress?: string;\n\n /**\n * Do not show this room in lobby listing.\n */\n unlisted?: boolean;\n\n /**\n * Metadata associated with the room.\n */\n metadata?: Metadata;\n\n /**\n * When the room was created.\n */\n createdAt?: Date;\n}\n\nexport interface MatchMakerDriver {\n /**\n * Check if a room exists in room cache.\n *\n * @param roomId - The room id.\n *\n * @returns Promise<boolean> | boolean - A promise or a boolean value indicating if the room exists.\n */\n has(roomId: string): Promise<boolean> | boolean;\n\n /**\n * Query rooms in room cache for given conditions.\n *\n * @param conditions - Filtering conditions.\n *\n * @returns Promise<IRoomCache[]> | IRoomCache[] - A promise or an object contaning room metadata list.\n */\n query<T extends Room = any>(\n conditions: Partial<IRoomCache & ExtractRoomCacheMetadata<T>>,\n sortOptions?: SortOptions\n ): Promise<Array<IRoomCache<ExtractRoomCacheMetadata<T>>>> | Array<IRoomCache<ExtractRoomCacheMetadata<T>>>;\n\n /**\n * Clean up rooms in room cache by process id.\n * @param processId - The process id.\n */\n cleanup?(processId: string): Promise<void>;\n\n /**\n * Query for a room in room cache for given conditions.\n *\n * @param conditions - Filtering conditions.\n *\n * @returns `IRoomCache` - An object contaning filtered room metadata.\n */\n findOne<T extends Room = any>(\n conditions: Partial<IRoomCache & ExtractRoomCacheMetadata<T>>,\n sortOptions?: SortOptions\n ): Promise<IRoomCache<ExtractRoomCacheMetadata<T>>>;\n\n /**\n * Batch-resolve room caches by roomId in a single backend round\n * trip. Missing roomIds are absent from the returned map. Used by\n * hot paths (per-join uniqueness check, by-user reverse-index\n * lookups) where K serial `findOne` calls would multiply round\n * trips needlessly.\n *\n * @param roomIds - Room ids to look up.\n *\n * @returns Map keyed by roomId of the rooms that were found.\n */\n findByIds(roomIds: string[]): Promise<Map<string, IRoomCache>>;\n\n /**\n * Remove a room from room cache.\n *\n * @param roomId - The room id.\n */\n remove(roomId: string): Promise<boolean> | boolean;\n\n /**\n * Update a room in room cache.\n *\n * @param IRoomCache - The room to update.\n * @param operations - The operations to update the room.\n */\n update(\n room: IRoomCache,\n operations: Partial<{ $set: Partial<IRoomCache>, $inc: Partial<IRoomCache> }>\n ): Promise<boolean> | boolean;\n\n /**\n * Persist a room in room cache.\n *\n * @param room - The room to persist.\n * @param create - If true, create a new record. If false (default), update existing record.\n */\n persist(room: IRoomCache, create?: boolean): Promise<boolean> | boolean;\n\n /**\n * Empty the room cache. Used for testing purposes only.\n * May be asynchronous \u2014 callers must await it.\n * @internal Do not call this method yourself.\n */\n clear(): void | Promise<void>;\n\n /**\n * Boot the room cache medium (if available).\n */\n boot?(): Promise<void>;\n\n /**\n * Dispose the connection of the room cache medium.\n * May be asynchronous \u2014 callers must await it.\n */\n shutdown(): void | Promise<void>;\n}\n"],
|
|
5
5
|
"mappings": ";AA2BO,SAAS,UAAU,eAAoB;AAC5C,SAAO,OAAO,KAAK,aAAa,EAAE,IAAI,CAAC,QAAQ,GAAG,GAAG,IAAI,cAAc,GAAG,CAAC,EAAE,EAAE,KAAK,GAAG;AACzF;AASO,SAAS,oBAAoB,gBAAqC,CAAC,GAAe;AACvF,SAAO;AAAA,IACL,SAAS;AAAA,IACT,YAAY;AAAA,IACZ,QAAQ;AAAA,IACR,SAAS;AAAA,IACT,UAAU;AAAA,IACV,WAAY,iBAAiB,cAAc,YAAa,IAAI,KAAK,cAAc,SAAS,IAAI,oBAAI,KAAK;AAAA,IACrG,UAAU;AAAA,IACV,GAAG;AAAA,EACL;AACF;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -2,6 +2,6 @@
|
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/presence/LocalPresence.ts"],
|
|
4
4
|
"sourcesContent": ["\nimport { EventEmitter } from 'events';\nimport { spliceOne } from '../utils/Utils.ts';\nimport type { Presence } from './Presence.ts';\n\nimport { hasDevModeCache, isDevMode, getDevModeCache, writeDevModeCache } from '../utils/DevMode.ts';\n\ntype Callback = (...args: any[]) => void;\n\nexport class LocalPresence implements Presence {\n public subscriptions: EventEmitter = new EventEmitter();\n\n public data: {[roomName: string]: string[]} = {};\n public hash: {[roomName: string]: {[key: string]: string}} = {};\n\n public keys: {[name: string]: string | number} = {};\n\n private timeouts: {[name: string]: NodeJS.Timeout} = {};\n\n constructor() {\n //\n // reload from local cache on devMode\n //\n if (\n isDevMode &&\n hasDevModeCache()\n ) {\n const cache = getDevModeCache();\n if (cache.data) { this.data = cache.data; }\n if (cache.hash) { this.hash = cache.hash; }\n if (cache.keys) { this.keys = cache.keys; }\n }\n }\n\n public subscribe(topic: string, callback: (...args: any[]) => void) {\n this.subscriptions.on(topic, callback);\n return Promise.resolve(this);\n }\n\n public unsubscribe(topic: string, callback?: Callback) {\n if (callback) {\n this.subscriptions.removeListener(topic, callback);\n\n } else {\n this.subscriptions.removeAllListeners(topic);\n }\n\n return this;\n }\n\n public publish(topic: string, data: any) {\n this.subscriptions.emit(topic, data);\n return this;\n }\n\n public async channels (pattern?: string) {\n let eventNames = this.subscriptions.eventNames() as string[];\n if (pattern) {\n //\n // This is a limited glob pattern to regexp implementation.\n // If needed, we can use a full implementation like picomatch: https://github.com/micromatch/picomatch/\n //\n const regexp = new RegExp(\n pattern.\n replaceAll(\".\", \"\\\\.\").\n replaceAll(\"$\", \"\\\\$\").\n replaceAll(\"*\", \".*\").\n replaceAll(\"?\", \".\"),\n \"i\"\n );\n eventNames = eventNames.filter((eventName) => regexp.test(eventName));\n }\n return eventNames;\n }\n\n public async exists(key: string): Promise<boolean> {\n return (\n this.keys[key] !== undefined ||\n this.data[key] !== undefined ||\n this.hash[key] !== undefined\n );\n }\n\n public set(key: string, value: string) {\n this.keys[key] = value;\n }\n\n public setex(key: string, value: string, seconds: number) {\n this.keys[key] = value;\n this.expire(key, seconds);\n }\n\n public expire(key: string, seconds: number) {\n // ensure previous timeout is clear before setting another one.\n if (this.timeouts[key]) {\n clearTimeout(this.timeouts[key]);\n }\n this.timeouts[key] = setTimeout(() => {\n delete this.keys[key];\n delete this.timeouts[key];\n }, seconds * 1000);\n }\n\n public get(key: string) {\n return this.keys[key];\n }\n\n public del(key: string) {\n delete this.keys[key];\n delete this.data[key];\n delete this.hash[key];\n }\n\n public sadd(key: string, value: any) {\n if (!this.data[key]) {\n this.data[key] = [];\n }\n\n if (this.data[key].indexOf(value) === -1) {\n this.data[key].push(value);\n }\n }\n\n public async smembers(key: string): Promise<string[]> {\n return this.data[key] || [];\n }\n\n public async sismember(key: string, field: string) {\n return this.data[key] && this.data[key].includes(field) ? 1 : 0;\n }\n\n public srem(key: string, value: any) {\n if (this.data[key]) {\n spliceOne(this.data[key], this.data[key].indexOf(value));\n }\n }\n\n public scard(key: string) {\n return (this.data[key] || []).length;\n }\n\n public async sinter(...keys: string[]) {\n const intersection: {[value: string]: number} = {};\n\n for (let i = 0, l = keys.length; i < l; i++) {\n (await this.smembers(keys[i])).forEach((member) => {\n if (!intersection[member]) {\n intersection[member] = 0;\n }\n\n intersection[member]++;\n });\n }\n\n return Object.keys(intersection).reduce((prev, curr) => {\n if (intersection[curr] > 1) {\n prev.push(curr);\n }\n return prev;\n }, []);\n }\n\n public hset(key: string, field: string, value: string) {\n if (!this.hash[key]) { this.hash[key] = {}; }\n this.hash[key][field] = value;\n return Promise.resolve(true);\n }\n\n public hincrby(key: string, field: string, incrBy: number) {\n if (!this.hash[key]) { this.hash[key] = {}; }\n let value = Number(this.hash[key][field] || '0');\n value += incrBy;\n this.hash[key][field] = value.toString();\n return Promise.resolve(value);\n }\n\n public hincrbyex(key: string, field: string, incrBy: number, expireInSeconds: number) {\n if (!this.hash[key]) { this.hash[key] = {}; }\n let value = Number(this.hash[key][field] || '0');\n value += incrBy;\n this.hash[key][field] = value.toString();\n\n //\n // FIXME: delete only hash[key][field]\n // (we can't use \"HEXPIRE\" in Redis because it's only available since Redis version 7.4.0+)\n //\n if (this.timeouts[key]) {\n clearTimeout(this.timeouts[key]);\n }\n this.timeouts[key] = setTimeout(() => {\n delete this.hash[key];\n delete this.timeouts[key];\n }, expireInSeconds * 1000);\n\n return Promise.resolve(value);\n }\n\n public async hget(key: string, field: string) {\n return (typeof(this.hash[key]) === 'object')\n ? this.hash[key][field] ?? null\n : null;\n }\n\n public async hgetall(key: string) {\n return this.hash[key] || {};\n }\n\n public hdel(key: string, field: any) {\n const success = this.hash?.[key]?.[field] !== undefined;\n if (success) {\n delete this.hash[key][field];\n }\n return Promise.resolve(success);\n }\n\n public async hlen(key: string) {\n return this.hash[key] && Object.keys(this.hash[key]).length || 0;\n }\n\n public async incr(key: string) {\n if (!this.keys[key]) {\n this.keys[key] = 0;\n }\n (this.keys[key] as number)++;\n return Promise.resolve(this.keys[key] as number);\n }\n\n public async decr(key: string) {\n if (!this.keys[key]) {\n this.keys[key] = 0;\n }\n (this.keys[key] as number)--;\n return Promise.resolve(this.keys[key] as number);\n }\n\n public llen(key: string) {\n return Promise.resolve((this.data[key] && this.data[key].length) || 0);\n }\n\n public rpush(key: string, ...values: string[]): Promise<number> {\n if (!this.data[key]) { this.data[key] = []; }\n\n let lastLength: number = 0;\n\n values.forEach(value => {\n lastLength = this.data[key].push(value);\n });\n\n return Promise.resolve(lastLength);\n }\n\n public lpush(key: string, ...values: string[]): Promise<number> {\n if (!this.data[key]) { this.data[key] = []; }\n\n let lastLength: number = 0;\n\n values.forEach(value => {\n lastLength = this.data[key].unshift(value);\n });\n\n return Promise.resolve(lastLength);\n }\n\n public lpop(key: string): Promise<string> {\n return Promise.resolve(Array.isArray(this.data[key])\n ? this.data[key].shift()\n : null);\n }\n\n public rpop(key: string): Promise<string | null> {\n return Promise.resolve(this.data[key].pop());\n }\n\n public brpop(...args: [...keys: string[], timeoutInSeconds: number]): Promise<[string, string] | null> {\n const keys = args.slice(0, -1) as string[];\n const timeoutInSeconds = args[args.length - 1] as number;\n\n const getFirstPopulated = (): [string, string] | null => {\n const keyWithValue = keys.find(key => this.data[key] && this.data[key].length > 0);\n if (keyWithValue) {\n return [keyWithValue, this.data[keyWithValue].pop()];\n } else {\n return null;\n }\n }\n\n const firstPopulated = getFirstPopulated();\n\n if (firstPopulated) {\n // return first populated key + item\n return Promise.resolve(firstPopulated);\n\n } else {\n // 8 retries per second\n const maxRetries = timeoutInSeconds * 8;\n\n let tries = 0;\n return new Promise((resolve) => {\n const interval = setInterval(() => {\n tries++;\n\n const firstPopulated = getFirstPopulated();\n if (firstPopulated) {\n clearInterval(interval);\n return resolve(firstPopulated);\n\n } else if (tries >= maxRetries) {\n clearInterval(interval);\n return resolve(null);\n }\n\n }, (timeoutInSeconds * 1000) / maxRetries);\n });\n }\n }\n\n public setMaxListeners(number: number) {\n this.subscriptions.setMaxListeners(number);\n }\n\n public shutdown() {\n if (isDevMode) {\n writeDevModeCache({\n data: this.data,\n hash: this.hash,\n keys: this.keys\n });\n }\n }\n\n}\n"],
|
|
5
|
-
"mappings": "
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": ["firstPopulated"]
|
|
7
7
|
}
|
|
@@ -2,6 +2,6 @@
|
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/presence/Presence.ts"],
|
|
4
4
|
"sourcesContent": ["import type { Room } from \"../Room.ts\";\n\n/**\n * When you need to scale your server on multiple processes and/or machines, you'd need to provide\n * the Presence option to the Server. The purpose of Presence is to allow communicating and\n * sharing data between different processes, specially during match-making.\n *\n * - Local presence - This is the default option. It's meant to be used when you're running Colyseus in\n * a single process.\n * - Redis presence - Use this option when you're running Colyseus on multiple processes and/or machines.\n *\n * @default Local presence\n */\nexport interface Presence {\n /**\n * Subscribes to the given topic. The callback will be triggered whenever a message is published on topic.\n *\n * @param topic - Topic name.\n * @param callback - Callback to trigger on subscribing.\n */\n subscribe(topic: string, callback: Function): Promise<this>;\n\n /**\n * Unsubscribe from given topic.\n *\n * @param topic - Topic name.\n * @param callback - Callback to trigger on topic unsubscribing.\n */\n unsubscribe(topic: string, callback?: Function);\n\n /**\n * Lists the currently active channels / subscriptions\n * @param pattern\n */\n channels(pattern?: string): Promise<string[]>;\n\n /**\n * Posts a message to given topic.\n *\n * @param topic - Topic name.\n * @param data - Message body/object.\n */\n publish(topic: string, data: any);\n\n /**\n * Returns if key exists.\n *\n * @param key\n */\n exists(key: string): Promise<boolean>;\n\n /**\n * Set key to hold the string value.\n *\n * @param key - Identifier.\n * @param value - Message body/object.\n */\n set(key: string, value: string);\n\n /**\n * Set key to hold the string value and set key to timeout after a given number of seconds.\n *\n * @param key - Identifier.\n * @param value - Message body/object.\n * @param seconds - Timeout value.\n */\n setex(key: string, value: string, seconds: number);\n\n /**\n * Expire the key in seconds.\n *\n * @param key - Identifier.\n * @param seconds - Seconds to expire the key.\n */\n expire(key: string, seconds: number);\n\n /**\n * Get the value of key.\n *\n * @param key - Identifier.\n */\n get(key: string);\n\n /**\n * Removes the specified key.\n *\n * @param key - Identifier of the object to removed.\n */\n del(key: string): void;\n\n /**\n * Add the specified members to the set stored at key. Specified members that are already\n * a member of this set are ignored. If key does not exist, a new set is created before\n * adding the specified members.\n *\n * @param key - Name/Identifier of the set.\n * @param value - Message body/object.\n */\n sadd(key: string, value: any);\n\n /**\n * Returns all the members of the set value stored at key.\n *\n * @param key - Name/Identifier of the set.\n */\n smembers(key: string): Promise<string[]>;\n\n /**\n * Returns if member is a member of the set stored at key.\n *\n * @param key - Name/Identifier of the set.\n * @param field - Key value within the set.\n * @returns `1` if the element is a member of the set else `0`.\n */\n sismember(key: string, field: string);\n\n /**\n * Remove the specified members from the set stored at key. Specified members that are not a\n * member of this set are ignored. If key does not exist, it is treated as an empty set\n * and this command returns 0.\n *\n * @param key - Name/Identifier of the set.\n * @param value - Key value within the set.\n */\n srem(key: string, value: any);\n\n /**\n * Returns the set cardinality (number of elements) of the set stored at key.\n *\n * @param key - Name/Identifier of the set.\n */\n scard(key: string);\n\n /**\n * Returns the members of the set resulting from the intersection of all the given sets.\n *\n * @param keys - Key values within the set.\n */\n sinter(...keys: string[]): Promise<string[]>;\n\n /**\n * Sets field in the hash stored at key to value. If key does not exist, a new key holding a hash is created.\n * If field already exists in the hash, it is overwritten.\n */\n hset(key: string, field: string, value: string): Promise<boolean>;\n\n /**\n * Increments the number stored at field in the hash stored at key by increment. If key does not exist, a new key\n * holding a hash is created. If field does not exist the value is set to 0 before the operation is performed.\n */\n hincrby(key: string, field: string, value: number): Promise<number>;\n\n /**\n * WARNING: DO NOT USE THIS METHOD. It is meant for internal use only.\n * @private\n */\n hincrbyex(key: string, field: string, value: number, expireInSeconds: number): Promise<number>;\n\n /**\n * Returns the value associated with field in the hash stored at key.\n */\n hget(key: string, field: string): Promise<string | null>;\n\n /**\n * Returns all fields and values of the hash stored at key.\n */\n hgetall(key: string): Promise<{ [key: string]: string }>;\n\n /**\n * Removes the specified fields from the hash stored at key. Specified fields that do not exist within\n * this hash are ignored. If key does not exist, it is treated as an empty hash and this command returns 0.\n */\n hdel(key: string, field: string): Promise<boolean>;\n\n /**\n * Returns the number of fields contained in the hash stored at key\n */\n hlen(key: string): Promise<number>;\n\n /**\n * Increments the number stored at key by one. If the key does not exist, it is set to 0 before performing\n * the operation. An error is returned if the key contains a value of the wrong type or\n * contains a string that can not be represented as integer. This operation is limited to 64-bit signed integers.\n */\n incr(key: string): Promise<number>;\n\n /**\n * Decrements the number stored at key by one. If the key does not exist, it is set to 0 before performing\n * the operation. An error is returned if the key contains a value of the wrong type or contains a string\n * that can not be represented as integer. This operation is limited to 64-bit signed integers.\n */\n decr(key: string): Promise<number>;\n\n /**\n * Returns the length of the list stored at key.\n */\n llen(key: string): Promise<number>;\n\n /**\n * Adds the string value to the end of the list stored at key. If key does not exist, it is created as empty list before performing the push operation.\n */\n rpush(key: string, ...values: string[]): Promise<number>;\n\n /**\n * Adds the string value to the begginning of the list stored at key. If key does not exist, it is created as empty list before performing the push operation.\n */\n lpush(key: string, ...values: string[]): Promise<number>;\n\n /**\n * Removes and returns the last element of the list stored at key.\n */\n rpop(key: string): Promise<string | null>;\n\n /**\n * Removes and returns the first element of the list stored at key.\n */\n lpop(key: string): Promise<string | null>;\n\n /**\n * Removes and returns the last element of the list stored at key. If the list is empty, the execution is halted until an element is available or the timeout is reached.\n */\n brpop(...args: [...keys: string[], timeoutInSeconds: number]): Promise<[string, string] | null>;\n\n setMaxListeners(number: number): void;\n\n shutdown(): void;\n}\n\nexport function createScopedPresence(room: Room, presence: Presence): Presence {\n // Keep a local copy of all subscriptions made through this scoped presence\n const subscriptions: Array<{ topic: string; callback: Function }> = [];\n\n // Create a copy of the presence object\n const scopedPresence = Object.create(presence) as Presence;\n\n // Override subscribe method to track subscriptions\n scopedPresence.subscribe = async function (topic: string, callback: Function): Promise<Presence> {\n subscriptions.push({ topic, callback });\n await presence.subscribe(topic, callback);\n return scopedPresence;\n };\n\n // Override unsubscribe method to remove from tracking\n scopedPresence.unsubscribe = function (topic: string, callback?: Function) {\n const index = subscriptions.findIndex(\n (sub) => sub.topic === topic && (!callback || sub.callback === callback)\n );\n if (index !== -1) {\n subscriptions.splice(index, 1);\n }\n presence.unsubscribe(topic, callback);\n };\n\n // Clean up all subscriptions when the room is disposed\n room['_events'].on('dispose', () => {\n for (const { topic, callback } of subscriptions) {\n presence.unsubscribe(topic, callback);\n }\n subscriptions.length = 0;\n });\n\n return scopedPresence;\n}"],
|
|
5
|
-
"mappings": "
|
|
5
|
+
"mappings": ";;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAoOO,SAAS,qBAAqB,MAAY,UAA8B;AAE7E,QAAM,gBAA8D,CAAC;AAGrE,QAAM,iBAAiB,OAAO,OAAO,QAAQ;AAG7C,iBAAe,YAAY,eAAgB,OAAe,UAAuC;AAC/F,kBAAc,KAAK,EAAE,OAAO,SAAS,CAAC;AACtC,UAAM,SAAS,UAAU,OAAO,QAAQ;AACxC,WAAO;AAAA,EACT;AAGA,iBAAe,cAAc,SAAU,OAAe,UAAqB;AACzE,UAAM,QAAQ,cAAc;AAAA,MAC1B,CAAC,QAAQ,IAAI,UAAU,UAAU,CAAC,YAAY,IAAI,aAAa;AAAA,IACjE;AACA,QAAI,UAAU,IAAI;AAChB,oBAAc,OAAO,OAAO,CAAC;AAAA,IAC/B;AACA,aAAS,YAAY,OAAO,QAAQ;AAAA,EACtC;AAGA,OAAK,SAAS,EAAE,GAAG,WAAW,MAAM;AAClC,eAAW,EAAE,OAAO,SAAS,KAAK,eAAe;AAC/C,eAAS,YAAY,OAAO,QAAQ;AAAA,IACtC;AACA,kBAAc,SAAS;AAAA,EACzB,CAAC;AAED,SAAO;AACT;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -2,6 +2,6 @@
|
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/rooms/LobbyRoom.ts"],
|
|
4
4
|
"sourcesContent": ["import * as matchMaker from '../MatchMaker.ts';\nimport type { IRoomCache } from '../matchmaker/LocalDriver/LocalDriver.ts';\nimport type { Client } from '../Transport.ts';\nimport { subscribeLobby } from '../matchmaker/Lobby.ts';\nimport { Room } from '../Room.ts';\n\n// TODO: use Schema state & filters on version 1.0.0\n\n// class DummyLobbyState extends Schema { // tslint:disable-line\n// @type(\"number\") public _: number;\n// }\n\n//\n// Strongly-typed client messages for LobbyRoom\n// (This is optional, but recommended for better type safety and code generation for native SDKs)\n//\ntype LobbyClient = Client<{\n messages: {\n rooms: IRoomCache[];\n '+': [roomId: string, room: IRoomCache];\n '-': string;\n }\n}>;\n\nexport interface FilterInput {\n name?: string;\n metadata?: any;\n}\n\nexport interface LobbyOptions {\n filter?: FilterInput;\n}\n\nexport type LobbyMessages = {\n filter: (client: LobbyClient, filter: FilterInput) => void;\n};\n\nexport class LobbyRoom<Metadata = any> extends Room {\n public rooms: IRoomCache<Metadata>[] = [];\n public unsubscribeLobby: () => void;\n\n public clientOptions: { [sessionId: string]: LobbyOptions } = {};\n\n declare messages: LobbyMessages;\n\n public async onCreate(options: any) {\n // prevent LobbyRoom to notify itself\n this['_listing'].unlisted = true;\n\n this.onMessage('filter', (client, filter) => {\n const clientOptions = this.clientOptions[client.sessionId];\n if (!clientOptions) { return; }\n\n clientOptions.filter = filter;\n client.send('rooms', this.filterItemsForClient(clientOptions));\n });\n\n this.unsubscribeLobby = await subscribeLobby((roomId, data) => {\n const roomIndex = this.rooms.findIndex((room) => room.roomId === roomId);\n const clients = this.clients.filter((client) => this.clientOptions[client.sessionId]);\n\n if (!data) {\n // remove room listing data\n if (roomIndex !== -1) {\n const previousData = this.rooms[roomIndex];\n\n this.rooms.splice(roomIndex, 1);\n\n clients.forEach((client) => {\n if (this.filterItemForClient(previousData, this.clientOptions[client.sessionId].filter)) {\n client.send('-', roomId);\n }\n });\n }\n\n } else if (roomIndex === -1) {\n // append room listing data\n this.rooms.push(data);\n\n clients.forEach((client) => {\n if (this.filterItemForClient(data, this.clientOptions[client.sessionId].filter)) {\n client.send('+', [roomId, data]);\n }\n });\n\n } else {\n const previousData = this.rooms[roomIndex];\n\n // replace room listing data\n this.rooms[roomIndex] = data;\n\n clients.forEach((client) => {\n const hadData = this.filterItemForClient(previousData, this.clientOptions[client.sessionId].filter);\n const hasData = this.filterItemForClient(data, this.clientOptions[client.sessionId].filter);\n\n if (hadData && !hasData) {\n client.send('-', roomId);\n\n } else if (hasData) {\n client.send('+', [roomId, data]);\n }\n });\n }\n });\n\n this.rooms = await matchMaker.query({ private: false, unlisted: false });\n }\n\n public onJoin(client: LobbyClient, options: LobbyOptions) {\n this.clientOptions[client.sessionId] = (\n !Array.isArray(options) && // Defold (Lua) sends empty objects as Array instead of object\n options\n ) || {};\n client.send('rooms', this.filterItemsForClient(this.clientOptions[client.sessionId]));\n }\n\n public onLeave(client: LobbyClient) {\n delete this.clientOptions[client.sessionId];\n }\n\n public onDispose() {\n if (this.unsubscribeLobby) {\n this.unsubscribeLobby();\n }\n }\n\n protected filterItemsForClient(options: LobbyOptions): IRoomCache<Metadata>[] {\n const filter = options.filter;\n\n return (filter)\n ? this.rooms.filter((room) => this.filterItemForClient(room, filter))\n : this.rooms;\n }\n\n protected filterItemForClient(room: IRoomCache, filter?: LobbyOptions['filter']) {\n if (!filter) {\n return true;\n }\n\n let isAllowed = true;\n\n if (filter.name !== room.name) {\n isAllowed = false;\n }\n\n if (filter.metadata) {\n for (const field in filter.metadata) {\n if (room.metadata[field] !== filter.metadata[field]) {\n isAllowed = false;\n break;\n }\n }\n }\n\n return isAllowed;\n }\n\n}\n"],
|
|
5
|
-
"mappings": "
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -2,6 +2,6 @@
|
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/rooms/QueueRoom.ts"],
|
|
4
4
|
"sourcesContent": ["import { Room } from '../Room.ts';\nimport type { Client } from '../Transport.ts';\nimport type { IRoomCache } from '../matchmaker/driver.ts';\nimport * as matchMaker from '../MatchMaker.ts';\nimport { debugMatchMaking } from '../Debug.ts';\nimport { ServerError } from '../errors/ServerError.ts';\nimport { CloseCode, ErrorCode } from '@colyseus/shared-types';\n\nexport interface QueueOptions {\n /**\n * number of players on each match\n */\n maxPlayers?: number;\n\n /**\n * name of the room to create\n */\n matchRoomName: string;\n\n /**\n * after these cycles, create a match with a bot\n */\n maxWaitingCycles?: number;\n\n /**\n * after this time, try to fit this client with a not-so-compatible group\n */\n maxWaitingCyclesForPriority?: number;\n\n /**\n * If set, teams must have the same size to be matched together\n */\n maxTeamSize?: number;\n\n /**\n * If `allowIncompleteGroups` is true, players inside an unmatched group (that\n * did not reached `maxPlayers`, and `maxWaitingCycles` has been\n * reached) will be matched together. Your room should fill the remaining\n * spots with \"bots\" on this case.\n */\n allowIncompleteGroups?: boolean;\n\n /**\n * Comparison function for matching clients to groups\n * Returns true if the client is compatible with the group\n */\n compare?: (client: QueueClientData, matchGroup: QueueMatchGroup) => boolean;\n\n /**\n *\n * When onGroupReady is set, the \"roomNameToCreate\" option is ignored.\n */\n onGroupReady?: (this: QueueRoom, group: QueueMatchGroup) => Promise<IRoomCache>;\n}\n\nexport interface QueueMatchGroup {\n averageRank: number;\n clients: Array<Client<{ userData: QueueClientData }>>,\n ready?: boolean;\n confirmed?: number;\n}\n\nexport interface QueueMatchTeam {\n averageRank: number;\n clients: Array<Client<{ userData: QueueClientData }>>,\n teamId: string | symbol;\n}\n\nexport interface QueueClientData {\n /**\n * Rank of the client\n */\n rank: number;\n\n /**\n * Timestamp of when the client entered the queue\n */\n currentCycle?: number;\n\n /**\n * Optional: if matching with a team, the team ID\n */\n teamId?: string;\n\n /**\n * Additional options passed by the client when joining the room\n */\n options?: any;\n\n /**\n * Match group the client is currently in\n */\n group?: QueueMatchGroup;\n\n /**\n * Whether the client has confirmed the connection to the room\n */\n confirmed?: boolean;\n\n /**\n * Whether the client should be prioritized in the queue\n * (e.g. for players that are waiting for a long time)\n */\n highPriority?: boolean;\n\n /**\n * The last number of clients in the queue sent to the client\n */\n lastQueueClientCount?: number;\n}\n\n//\n// Optional: strongly-typed client messages\n// (This is optional, but recommended for better type safety and code generation for native SDKs)\n//\ntype QueueClient = Client<{\n userData: QueueClientData;\n messages: {\n clients: number;\n seat: matchMaker.ISeatReservation;\n }\n}>;\n\nconst DEFAULT_TEAM = Symbol(\"$default_team\");\nconst DEFAULT_COMPARE = (client: QueueClientData, matchGroup: QueueMatchGroup) => {\n const diff = Math.abs(client.rank - matchGroup.averageRank);\n const diffRatio = (diff / matchGroup.averageRank);\n // If diff ratio is too high, create a new match group\n return (diff < 10 || diffRatio <= 2);\n}\n\nexport class QueueRoom extends Room {\n maxPlayers = 4;\n maxTeamSize: number;\n allowIncompleteGroups: boolean = false;\n\n maxWaitingCycles = 15;\n maxWaitingCyclesForPriority?: number = 10;\n\n /**\n * Evaluate groups for each client at interval\n */\n cycleTickInterval = 1000;\n\n /**\n * Groups of players per iteration\n */\n groups: QueueMatchGroup[] = [];\n highPriorityGroups: QueueMatchGroup[] = [];\n\n matchRoomName: string;\n\n protected compare = DEFAULT_COMPARE;\n protected onGroupReady = (group: QueueMatchGroup) => matchMaker.createRoom(this.matchRoomName, {});\n\n messages = {\n confirm: (client: Client, _: unknown) => {\n const queueData = client.userData;\n\n if (queueData && queueData.group && typeof (queueData.group.confirmed) === \"number\") {\n queueData.confirmed = true;\n queueData.group.confirmed++;\n client.leave(CloseCode.NORMAL_CLOSURE);\n }\n },\n }\n\n onCreate(options: QueueOptions) {\n if (typeof(options.maxWaitingCycles) === \"number\") {\n this.maxWaitingCycles = options.maxWaitingCycles;\n }\n\n if (typeof(options.maxPlayers) === \"number\") {\n this.maxPlayers = options.maxPlayers;\n }\n\n if (typeof(options.maxTeamSize) === \"number\") {\n this.maxTeamSize = options.maxTeamSize;\n }\n\n if (typeof(options.allowIncompleteGroups) !== \"undefined\") {\n this.allowIncompleteGroups = options.allowIncompleteGroups;\n }\n\n if (typeof(options.compare) === \"function\") {\n this.compare = options.compare;\n }\n\n if (typeof(options.onGroupReady) === \"function\") {\n this.onGroupReady = options.onGroupReady;\n }\n\n if (options.matchRoomName) {\n this.matchRoomName = options.matchRoomName;\n\n } else {\n throw new ServerError(ErrorCode.APPLICATION_ERROR, \"QueueRoom: 'matchRoomName' option is required.\");\n }\n\n debugMatchMaking(\"QueueRoom#onCreate() maxPlayers: %d, maxWaitingCycles: %d, maxTeamSize: %d, allowIncompleteGroups: %d, roomNameToCreate: %s\", this.maxPlayers, this.maxWaitingCycles, this.maxTeamSize, this.allowIncompleteGroups, this.matchRoomName);\n\n /**\n * Redistribute clients into groups at every interval\n */\n this.setTimestep(() => this.reassignMatchGroups(), this.cycleTickInterval);\n }\n\n onJoin(client: QueueClient, options: any, auth?: unknown) {\n this.addToQueue(client, {\n rank: options.rank,\n teamId: options.teamId,\n options,\n });\n }\n\n addToQueue(client: QueueClient, queueData: QueueClientData) {\n if (queueData.currentCycle === undefined) {\n queueData.currentCycle = 0;\n }\n client.userData = queueData;\n\n // FIXME: reassign groups upon joining [?] (without incrementing cycle count)\n client.send(\"clients\", 1);\n }\n\n createMatchGroup() {\n const group: QueueMatchGroup = { clients: [], averageRank: 0 };\n this.groups.push(group);\n return group;\n }\n\n reassignMatchGroups() {\n // Re-set all groups\n this.groups.length = 0;\n this.highPriorityGroups.length = 0;\n\n const sortedClients = (this.clients)\n .filter((client) => {\n // Filter out:\n // - clients that are not in the queue\n // - clients that are already in a \"ready\" group\n return (\n client.userData &&\n client.userData.group?.ready !== true\n );\n })\n .sort((a, b) => {\n //\n // Sort by rank ascending\n //\n return a.userData.rank - b.userData.rank;\n });\n\n //\n // The room either distribute by teams or by clients\n //\n if (typeof(this.maxTeamSize) === \"number\") {\n this.redistributeTeams(sortedClients);\n\n } else {\n this.redistributeClients(sortedClients);\n }\n\n this.evaluateHighPriorityGroups();\n this.processGroupsReady();\n }\n\n redistributeTeams(sortedClients: Client<{ userData: QueueClientData }>[]) {\n const teamsByID: { [teamId: string | symbol]: QueueMatchTeam } = {};\n\n sortedClients.forEach((client) => {\n const teamId = client.userData.teamId || DEFAULT_TEAM;\n\n // Create a new team if it doesn't exist\n if (!teamsByID[teamId]) {\n teamsByID[teamId] = { teamId: teamId, clients: [], averageRank: 0, };\n }\n\n teamsByID[teamId].averageRank += client.userData.rank;\n teamsByID[teamId].clients.push(client);\n });\n\n // Calculate average rank for each team\n let teams = Object.values(teamsByID).map((team) => {\n team.averageRank /= team.clients.length;\n return team;\n }).sort((a, b) => {\n // Sort by average rank ascending\n return a.averageRank - b.averageRank;\n });\n\n // Iterate over teams multiple times until all clients are assigned to a group\n do {\n let currentGroup: QueueMatchGroup = this.createMatchGroup();\n teams = teams.filter((team) => {\n // Remove clients from the team and add them to the current group\n const totalRank = team.averageRank * team.clients.length;\n\n // currentGroup.averageRank = (currentGroup.averageRank === undefined)\n // ? team.averageRank\n // : (currentGroup.averageRank + team.averageRank) / ;\n currentGroup = this.redistributeClients(team.clients.splice(0, this.maxTeamSize), currentGroup, totalRank);\n\n if (team.clients.length >= this.maxTeamSize) {\n // team still has enough clients to form a group\n return true;\n }\n\n // increment cycle count for all clients in the team\n team.clients.forEach((client) => client.userData.currentCycle++);\n\n return false;\n });\n } while (teams.length >= 2);\n }\n\n redistributeClients(\n sortedClients: Client<{ userData: QueueClientData }>[],\n currentGroup: QueueMatchGroup = this.createMatchGroup(),\n totalRank: number = 0,\n ) {\n for (let i = 0, l = sortedClients.length; i < l; i++) {\n const client = sortedClients[i];\n const userData = client.userData;\n const currentCycle = userData.currentCycle++;\n\n if (currentGroup.averageRank > 0) {\n if (\n !this.compare(userData, currentGroup) &&\n !userData.highPriority\n ) {\n currentGroup = this.createMatchGroup();\n totalRank = 0;\n }\n }\n\n userData.group = currentGroup;\n currentGroup.clients.push(client);\n\n totalRank += userData.rank;\n currentGroup.averageRank = totalRank / currentGroup.clients.length;\n\n // Enough players in the group, mark it as ready!\n if (currentGroup.clients.length === this.maxPlayers) {\n currentGroup.ready = true;\n currentGroup = this.createMatchGroup();\n totalRank = 0;\n continue;\n }\n\n if (currentCycle >= this.maxWaitingCycles && this.allowIncompleteGroups) {\n /**\n * Match long-waiting clients with bots\n */\n if (this.highPriorityGroups.indexOf(currentGroup) === -1) {\n this.highPriorityGroups.push(currentGroup);\n }\n\n } else if (\n this.maxWaitingCyclesForPriority !== undefined &&\n currentCycle >= this.maxWaitingCyclesForPriority\n ) {\n /**\n * Force this client to join a group, even if rank is incompatible\n */\n userData.highPriority = true;\n }\n }\n\n return currentGroup;\n }\n\n evaluateHighPriorityGroups() {\n /**\n * Evaluate groups with high priority clients\n */\n this.highPriorityGroups.forEach((group) => {\n group.ready = group.clients.every((c) => {\n // Give new clients another chance to join a group that is not \"high priority\"\n return c.userData?.currentCycle > 1;\n // return c.userData?.currentCycle >= this.maxWaitingCycles;\n });\n });\n }\n\n processGroupsReady() {\n this.groups.forEach(async (group) => {\n if (group.ready) {\n group.confirmed = 0;\n\n try {\n /**\n * Create room instance in the server.\n */\n const room = await this.onGroupReady.call(this, group);\n\n /**\n * Reserve a seat for each client in the group.\n * (If one fails, force all clients to leave, re-queueing is up to the client-side logic)\n */\n await matchMaker.reserveMultipleSeatsFor(\n room,\n group.clients.map((client) => ({\n sessionId: client.sessionId,\n options: client.userData.options,\n auth: client.auth,\n })),\n );\n\n /**\n * Send room data for new WebSocket connection!\n */\n group.clients.forEach((client, i) => {\n client.send(\"seat\", matchMaker.buildSeatReservation(room, client.sessionId));\n });\n\n } catch (e: any) {\n //\n // If creating a room, or reserving a seat failed - fail all clients\n // Whether the clients retry or not is up to the client-side logic\n //\n group.clients.forEach(client => client.leave(1011, e.message));\n }\n\n } else {\n /**\n * Notify clients within the group on how many players are in the queue\n */\n group.clients.forEach((client) => {\n //\n // avoid sending the same number of clients to the client if it hasn't changed\n //\n const queueClientCount = group.clients.length;\n if (client.userData.lastQueueClientCount !== queueClientCount) {\n client.userData.lastQueueClientCount = queueClientCount;\n client.send(\"clients\", queueClientCount);\n }\n });\n }\n });\n }\n\n}"],
|
|
5
|
-
"mappings": "
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -2,6 +2,6 @@
|
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/rooms/RelayRoom.ts"],
|
|
4
4
|
"sourcesContent": ["import { CloseCode } from '@colyseus/shared-types';\nimport { schema, t, type SchemaType } from '@colyseus/schema';\n\nimport { Room } from '../Room.ts';\nimport type { Client } from '../Transport.ts';\n\nexport const Player = schema({\n connected: t.boolean(),\n name: t.string(),\n sessionId: t.string(),\n});\nexport type Player = SchemaType<typeof Player>;\n\nexport const State = schema({\n players: t.map(Player),\n});\nexport type State = SchemaType<typeof State>;\n\n/**\n * client.joinOrCreate(\"relayroom\", {\n * maxClients: 10,\n * allowReconnectionTime: 20\n * });\n */\n\nexport class RelayRoom extends Room {\n public state = new State();\n public allowReconnectionTime: number = 0;\n\n public onCreate(options: Partial<{\n maxClients: number,\n allowReconnectionTime: number,\n metadata: any,\n }>) {\n if (options.maxClients) {\n this.maxClients = options.maxClients;\n }\n\n if (options.allowReconnectionTime) {\n this.allowReconnectionTime = Math.min(options.allowReconnectionTime, 40);\n }\n\n if (options.metadata) {\n this.setMetadata(options.metadata);\n }\n\n this.onMessage('*', (client: Client, type: string | number, message: any) => {\n this.broadcast(type, [client.sessionId, message], { except: client });\n });\n }\n\n public onJoin(client: Client, options: any = {}) {\n const player = new Player();\n\n player.connected = true;\n player.sessionId = client.sessionId;\n\n if (options.name) {\n player.name = options.name;\n }\n\n this.state.players.set(client.sessionId, player);\n }\n\n public async onLeave(client: Client, code: number) {\n if (this.allowReconnectionTime > 0) {\n const player = this.state.players.get(client.sessionId);\n player.connected = false;\n\n try {\n if (code === CloseCode.CONSENTED) {\n throw new Error('consented leave');\n }\n\n await this.allowReconnection(client, this.allowReconnectionTime);\n player.connected = true;\n\n } catch (e) {\n this.state.players.delete(client.sessionId);\n }\n }\n }\n\n}\n"],
|
|
5
|
-
"mappings": "
|
|
5
|
+
"mappings": ";;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,0BAA0B;AAC1B,oBAA2C;AAE3C,kBAAqB;AAGd,IAAM,aAAS,sBAAO;AAAA,EAC3B,WAAW,gBAAE,QAAQ;AAAA,EACrB,MAAM,gBAAE,OAAO;AAAA,EACf,WAAW,gBAAE,OAAO;AACtB,CAAC;AAGM,IAAM,YAAQ,sBAAO;AAAA,EAC1B,SAAS,gBAAE,IAAI,MAAM;AACvB,CAAC;AAUM,IAAM,YAAN,cAAwB,iBAAK;AAAA,EAA7B;AAAA;AACL,SAAO,QAAQ,IAAI,MAAM;AACzB,SAAO,wBAAgC;AAAA;AAAA,EAEhC,SAAS,SAIZ;AACF,QAAI,QAAQ,YAAY;AACtB,WAAK,aAAa,QAAQ;AAAA,IAC5B;AAEA,QAAI,QAAQ,uBAAuB;AACjC,WAAK,wBAAwB,KAAK,IAAI,QAAQ,uBAAuB,EAAE;AAAA,IACzE;AAEA,QAAI,QAAQ,UAAU;AACpB,WAAK,YAAY,QAAQ,QAAQ;AAAA,IACnC;AAEA,SAAK,UAAU,KAAK,CAAC,QAAgB,MAAuB,YAAiB;AAC3E,WAAK,UAAU,MAAM,CAAC,OAAO,WAAW,OAAO,GAAG,EAAE,QAAQ,OAAO,CAAC;AAAA,IACtE,CAAC;AAAA,EACH;AAAA,EAEO,OAAO,QAAgB,UAAe,CAAC,GAAG;AAC/C,UAAM,SAAS,IAAI,OAAO;AAE1B,WAAO,YAAY;AACnB,WAAO,YAAY,OAAO;AAE1B,QAAI,QAAQ,MAAM;AAChB,aAAO,OAAO,QAAQ;AAAA,IACxB;AAEA,SAAK,MAAM,QAAQ,IAAI,OAAO,WAAW,MAAM;AAAA,EACjD;AAAA,EAEA,MAAa,QAAQ,QAAgB,MAAc;AACjD,QAAI,KAAK,wBAAwB,GAAG;AAClC,YAAM,SAAS,KAAK,MAAM,QAAQ,IAAI,OAAO,SAAS;AACtD,aAAO,YAAY;AAEnB,UAAI;AACF,YAAI,SAAS,8BAAU,WAAW;AAChC,gBAAM,IAAI,MAAM,iBAAiB;AAAA,QACnC;AAEA,cAAM,KAAK,kBAAkB,QAAQ,KAAK,qBAAqB;AAC/D,eAAO,YAAY;AAAA,MAErB,SAAS,GAAG;AACV,aAAK,MAAM,QAAQ,OAAO,OAAO,SAAS;AAAA,MAC5C;AAAA,IACF;AAAA,EACF;AAEF;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|