API Index — Namespaces, Packages, Reports, Indices
Room
in package
A first-class WebSocket room — cluster-wide membership, presence, fan-out + handler registration. Built on the existing v0.2.40 Store + pub/sub fabric:
- Membership lives in the shared
ws_room_membersStore table (visible to every worker on every node).- Push / presence events flow over
ws:room:{name}pub/sub channel. - One PSUBSCRIBE per worker covers EVERY room (no per-room subscriber proliferation).
- Push / presence events flow over
Construct via WSRouter::room('chat.42') — don't new directly
(instances need WSRouter::init() to have wired the PSUBSCRIBE). Room names
must match /^[A-Za-z0-9_.-]+$/ (no : — it would collide ws_room_members
keys and the ws:room:* channel; #247).
Usage:
$room = WSRouter::room('chat.42');
$room->join('alice'); // SADD-equivalent + presence broadcast
$room->push(['from' => 'alice', 'msg' => 'hi']); // fans out across cluster
$room->size(); // cluster-wide member count
$room->members(); // cluster-wide roster
$room->onMessage(function (array $msg) { ... }); // user-side handler
$room->onPresence(function (array $event) { ... }); // join/leave events
$room->leave('alice'); // SREM-equivalent + presence broadcast
Federation: a publish from server-A reaches every server's PSUBSCRIBE. Each worker pushes to its local members (those in WSRouter's per-worker fd map). No double-delivery: workers without local members of this room skip the push entirely.
Table of Contents
Properties
Methods
- __construct() : mixed
- isMember() : bool
- True if
$clientIdis a current member (cluster-wide check). - join() : void
- Add a client to this room. Idempotent — re-joining is cheap.
- leave() : void
- Remove a client from this room. Idempotent. Broadcasts the leave event so peers update their local caches.
- members() : array<int, string>
- Cluster-wide roster — list of client ids currently joined.
-
membersPaged()
: array{cursor: string, members: list
} - Paginated roster — returns one SSCAN batch + an opaque next-cursor
(WS-2). Use for very large rooms where
members()would be too heavy. Cursor'0'starts a fresh walk; the returned cursor'0'signals end-of-scan. - name() : string
- onMessage() : void
- Register a handler for messages broadcast to this room.
- onPresence() : void
- Register a handler for join/leave events on this room. The handler
is called with the full event object:
{type, client_id, ts}. - push() : int
- Broadcast a message to every member of this room across the cluster. Returns the receivers count Redis reported (1 per subscribed worker × every server with a pattern subscriber).
- size() : int
- Cluster-wide member count (WS-2 hardened).
- checkRoomRateLimit() : bool
- Per-room rate limit check. True = allowed, false = drop.
- compositeKey() : string
- Composite key shape used in the ws_room_members Store table.
- publish() : int
- readForbidden() : bool
- #234 — true when a roomAuthorizer is wired AND it denies a 'read' on this room for the current session principal. Roster reads (members/size/ isMember) consult this so an authorizer can block cross-tenant enumeration.
Properties
$name
private
string
$name
$serverId
private
string
$serverId
Methods
__construct()
public
__construct(string $name, string $serverId) : mixed
Parameters
- $name : string
- $serverId : string
isMember()
True if $clientId is a current member (cluster-wide check).
public
isMember(string $clientId) : bool
Parameters
- $clientId : string
Return values
booljoin()
Add a client to this room. Idempotent — re-joining is cheap.
public
join(string $clientId) : void
Broadcasts a presence event to every server's room subscriber so each worker can refresh its local-membership cache.
Parameters
- $clientId : string
leave()
Remove a client from this room. Idempotent. Broadcasts the leave event so peers update their local caches.
public
leave(string $clientId) : void
Parameters
- $clientId : string
members()
Cluster-wide roster — list of client ids currently joined.
public
members() : array<int, string>
Redis / Tiered backend: drains the per-room SET via SSCAN. For very
large rooms (>10k members), use membersPaged() instead to avoid
loading the full roster into memory.
Table backend: iterates the metadata table.
Return values
array<int, string>membersPaged()
Paginated roster — returns one SSCAN batch + an opaque next-cursor
(WS-2). Use for very large rooms where members() would be too
heavy. Cursor '0' starts a fresh walk; the returned cursor '0'
signals end-of-scan.
public
membersPaged([string $cursor = '0' ][, int $count = 100 ]) : array{cursor: string, members: list}
Only useful on Redis / Tiered backends — on Table backend the iterate path returns the full roster in one batch (cursor '0').
Parameters
- $cursor : string = '0'
- $count : int = 100
Return values
array{cursor: string, members: listname()
public
name() : string
Return values
stringonMessage()
Register a handler for messages broadcast to this room.
public
onMessage(callable(array<string, mixed> $msg, string $room): void $handler) : void
Multiple handlers per room are allowed; all fire in order.
Parameters
- $handler : callable(array<string, mixed> $msg, string $room): void
onPresence()
Register a handler for join/leave events on this room. The handler
is called with the full event object: {type, client_id, ts}.
public
onPresence(callable(array<string, mixed> $event, string $room): void $handler) : void
Parameters
- $handler : callable(array<string, mixed> $event, string $room): void
push()
Broadcast a message to every member of this room across the cluster. Returns the receivers count Redis reported (1 per subscribed worker × every server with a pattern subscriber).
public
push(array<string, mixed>|string $payload[, string|null $fromClientId = null ]) : int
Payload is JSON-encoded when array; sent as-is when string.
Parameters
- $payload : array<string, mixed>|string
- $fromClientId : string|null = null
Return values
intsize()
Cluster-wide member count (WS-2 hardened).
public
size() : int
Redis / Tiered backend: O(1) SCARD on the per-room SET maintained by join/leave. Sub-millisecond regardless of cluster size.
Table backend: O(total members across ALL rooms) — iterates the ws_room_members metadata table and filters by room. Fine for single-node deployments where the iterate is in-process.
Return values
intcheckRoomRateLimit()
Per-room rate limit check. True = allowed, false = drop.
private
checkRoomRateLimit() : bool
Sliding window via floor(time() / window) bucket. WS-7: the counter NAME is STABLE per room (no per-window suffix) — the window roll is handled by WSRouter::rateLimitAllow() resetting the SAME counter on boundary crossing, instead of allocating a brand-new named Atomic every window (which leaked one dead Atomic per elapsed window on the default Atomic Counter backend). Disabled when WSRouter::$roomRateLimitN === 0.
Return values
boolcompositeKey()
Composite key shape used in the ws_room_members Store table.
private
static compositeKey(string $room, string $clientId) : string
#247 — length-prefix the room half so the key is an unambiguous function
of the (room, client) pair even if a client id contains a :. The room
name is already charset-validated (no :) at the WSRouter::room()
chokepoint; prefixing its byte length here removes any residual ambiguity
at the room↔client boundary (5:chat:42:alice can only decompose one way).
Parameters
- $room : string
- $clientId : string
Return values
stringpublish()
private
publish(array<string, mixed> $envelope) : int
Parameters
- $envelope : array<string, mixed>
Return values
intreadForbidden()
#234 — true when a roomAuthorizer is wired AND it denies a 'read' on this room for the current session principal. Roster reads (members/size/ isMember) consult this so an authorizer can block cross-tenant enumeration.
private
readForbidden() : bool
BC: with no authorizer the short-circuit returns false (reads unguarded) WITHOUT touching the session.