room-kit 1.0.0 → 1.0.2
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/README.md +25 -10
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +83 -16
- package/dist/client.js.map +1 -1
- package/dist/server.d.ts +4 -4
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +143 -14
- package/dist/server.js.map +1 -1
- package/dist/types.d.ts +24 -4
- package/dist/types.d.ts.map +1 -1
- package/example/public/app.ts +184 -187
- package/example/server.ts +173 -146
- package/jsr.json +1 -1
- package/package.json +1 -1
- package/src/client.ts +105 -16
- package/src/server.ts +189 -25
- package/src/types.ts +28 -4
- package/test/room.spec.ts +241 -78
package/src/server.ts
CHANGED
|
@@ -31,6 +31,132 @@ const SERVER_EVENT = "room-kit:server-event";
|
|
|
31
31
|
const PRESENCE_EVENT = "room-kit:presence";
|
|
32
32
|
const PRESENCE_QUERY_EVENT = "room-kit:presence-query";
|
|
33
33
|
|
|
34
|
+
function assertNonEmptyString(value: unknown, label: string): asserts value is string {
|
|
35
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
36
|
+
throw new ClientSafeError(`${label} must be a non-empty string`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function assertObject(value: unknown, label: string): asserts value is Record<string, unknown> {
|
|
41
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
42
|
+
throw new ClientSafeError(`${label} must be an object`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function assertArray(value: unknown, label: string): asserts value is unknown[] {
|
|
47
|
+
if (!Array.isArray(value)) {
|
|
48
|
+
throw new ClientSafeError(`${label} must be an array`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Validates and narrows a join frame from the wire. */
|
|
53
|
+
function validateJoinFrame(raw: unknown): {
|
|
54
|
+
roomType: string;
|
|
55
|
+
payload: Record<string, unknown>;
|
|
56
|
+
} {
|
|
57
|
+
assertObject(raw, "Join frame");
|
|
58
|
+
assertNonEmptyString(raw.roomType, "roomType");
|
|
59
|
+
assertObject(raw.payload, "payload");
|
|
60
|
+
return { roomType: raw.roomType, payload: raw.payload as Record<string, unknown> };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Validates and narrows a leave frame from the wire. */
|
|
64
|
+
function validateLeaveFrame(raw: unknown): { roomType: string; roomId: string } {
|
|
65
|
+
assertObject(raw, "Leave frame");
|
|
66
|
+
assertNonEmptyString(raw.roomType, "roomType");
|
|
67
|
+
assertNonEmptyString(raw.roomId, "roomId");
|
|
68
|
+
return { roomType: raw.roomType, roomId: raw.roomId };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Validates and narrows an RPC frame from the wire. */
|
|
72
|
+
function validateRpcFrame(raw: unknown): {
|
|
73
|
+
roomType: string;
|
|
74
|
+
roomId: string;
|
|
75
|
+
name: string;
|
|
76
|
+
args: unknown[];
|
|
77
|
+
} {
|
|
78
|
+
assertObject(raw, "RPC frame");
|
|
79
|
+
assertNonEmptyString(raw.roomType, "roomType");
|
|
80
|
+
assertNonEmptyString(raw.roomId, "roomId");
|
|
81
|
+
assertNonEmptyString(raw.name, "name");
|
|
82
|
+
assertArray(raw.args, "args");
|
|
83
|
+
return { roomType: raw.roomType, roomId: raw.roomId, name: raw.name, args: raw.args };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Validates and narrows a client-event frame from the wire. */
|
|
87
|
+
function validateClientEventFrame(raw: unknown): {
|
|
88
|
+
roomType: string;
|
|
89
|
+
roomId: string;
|
|
90
|
+
name: string;
|
|
91
|
+
payload: unknown;
|
|
92
|
+
} {
|
|
93
|
+
assertObject(raw, "Client-event frame");
|
|
94
|
+
assertNonEmptyString(raw.roomType, "roomType");
|
|
95
|
+
assertNonEmptyString(raw.roomId, "roomId");
|
|
96
|
+
assertNonEmptyString(raw.name, "name");
|
|
97
|
+
return { roomType: raw.roomType, roomId: raw.roomId, name: raw.name, payload: raw.payload };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Validates and narrows a presence-query frame from the wire. */
|
|
101
|
+
function validatePresenceQueryFrame(raw: unknown): {
|
|
102
|
+
roomType: string;
|
|
103
|
+
roomId: string;
|
|
104
|
+
kind: "count" | "list";
|
|
105
|
+
offset?: number;
|
|
106
|
+
limit?: number;
|
|
107
|
+
} {
|
|
108
|
+
assertObject(raw, "Presence-query frame");
|
|
109
|
+
assertNonEmptyString(raw.roomType, "roomType");
|
|
110
|
+
assertNonEmptyString(raw.roomId, "roomId");
|
|
111
|
+
if (raw.kind !== "count" && raw.kind !== "list") {
|
|
112
|
+
throw new ClientSafeError("Presence query kind must be 'count' or 'list'");
|
|
113
|
+
}
|
|
114
|
+
const result: {
|
|
115
|
+
roomType: string;
|
|
116
|
+
roomId: string;
|
|
117
|
+
kind: "count" | "list";
|
|
118
|
+
offset?: number;
|
|
119
|
+
limit?: number;
|
|
120
|
+
} = { roomType: raw.roomType, roomId: raw.roomId, kind: raw.kind };
|
|
121
|
+
if (raw.offset !== undefined) {
|
|
122
|
+
if (typeof raw.offset !== "number" || !Number.isFinite(raw.offset)) {
|
|
123
|
+
throw new ClientSafeError("offset must be a finite number");
|
|
124
|
+
}
|
|
125
|
+
result.offset = raw.offset;
|
|
126
|
+
}
|
|
127
|
+
if (raw.limit !== undefined) {
|
|
128
|
+
if (typeof raw.limit !== "number" || !Number.isFinite(raw.limit)) {
|
|
129
|
+
throw new ClientSafeError("limit must be a finite number");
|
|
130
|
+
}
|
|
131
|
+
result.limit = raw.limit;
|
|
132
|
+
}
|
|
133
|
+
return result;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const DANGEROUS_PROPERTY_NAMES = new Set([
|
|
137
|
+
"__proto__",
|
|
138
|
+
"constructor",
|
|
139
|
+
"prototype",
|
|
140
|
+
"toString",
|
|
141
|
+
"valueOf",
|
|
142
|
+
"hasOwnProperty",
|
|
143
|
+
"isPrototypeOf",
|
|
144
|
+
"propertyIsEnumerable",
|
|
145
|
+
"toLocaleString",
|
|
146
|
+
]);
|
|
147
|
+
|
|
148
|
+
function assertSafeHandlerName(name: string): void {
|
|
149
|
+
if (DANGEROUS_PROPERTY_NAMES.has(name)) {
|
|
150
|
+
throw new ClientSafeError(`Disallowed handler name '${name}'`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Maximum number of rooms that can exist within a single namespace. */
|
|
155
|
+
const MAX_ROOMS_PER_NAMESPACE = 10_000;
|
|
156
|
+
|
|
157
|
+
/** Maximum number of socket connections tracked per room. */
|
|
158
|
+
const MAX_MEMBERS_PER_ROOM = 10_000;
|
|
159
|
+
|
|
34
160
|
type StoredMember<TRoom extends RoomDefinition<any>> = {
|
|
35
161
|
socketId: string;
|
|
36
162
|
memberId: string;
|
|
@@ -59,13 +185,13 @@ const namespaceStates = new WeakMap<object, NamespaceState>();
|
|
|
59
185
|
/**
|
|
60
186
|
* Attaches room runtime handlers to a connected server socket.
|
|
61
187
|
*
|
|
62
|
-
* Returns a
|
|
63
|
-
*
|
|
188
|
+
* Returns a handle that unregisters all listeners for the bound socket via
|
|
189
|
+
* `cleanup()`, and exposes room introspection helpers.
|
|
64
190
|
*
|
|
65
191
|
* @example
|
|
66
192
|
* ```ts
|
|
67
193
|
* io.on("connection", (socket) => {
|
|
68
|
-
* const
|
|
194
|
+
* const handle = serveRoomType(socket, chatRoomType, {
|
|
69
195
|
* onAuth: async () => ({ userId: socket.id }),
|
|
70
196
|
* admit: async (join, ctx) => ({
|
|
71
197
|
* roomId: join.roomId,
|
|
@@ -76,7 +202,7 @@ const namespaceStates = new WeakMap<object, NamespaceState>();
|
|
|
76
202
|
* });
|
|
77
203
|
*
|
|
78
204
|
* // Later if needed:
|
|
79
|
-
* //
|
|
205
|
+
* // handle.cleanup();
|
|
80
206
|
* });
|
|
81
207
|
* ```
|
|
82
208
|
*/
|
|
@@ -90,20 +216,27 @@ export function serveRoomType<TRoom extends RoomDefinition<any>, TAuth = unknown
|
|
|
90
216
|
const authCache = new WeakMap<ServerSocketLike, AuthCacheEntry<TAuth>>();
|
|
91
217
|
|
|
92
218
|
const onJoin = async (
|
|
93
|
-
|
|
94
|
-
roomType: string;
|
|
95
|
-
payload: JoinRequest<TRoom>;
|
|
96
|
-
},
|
|
219
|
+
raw: unknown,
|
|
97
220
|
ack?: (result: { ok: true; value: { roomId: string; memberId: string; roomProfile: any; presence: PresenceValueFor<TRoom> } } | { ok: false; error: string }) => void,
|
|
98
221
|
) => {
|
|
99
222
|
try {
|
|
223
|
+
|
|
224
|
+
if (ack !== undefined && typeof ack !== "function") {
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
const frame = validateJoinFrame(raw);
|
|
100
228
|
assertMatchingRoomName(_room, frame.roomType);
|
|
101
229
|
const requestedRoomId = extractRoomId(frame.payload);
|
|
102
230
|
const roomCollection = getOrCreateRoomCollection(namespaceState, frame.roomType);
|
|
231
|
+
|
|
232
|
+
if (!roomCollection.has(requestedRoomId) && roomCollection.size >= MAX_ROOMS_PER_NAMESPACE) {
|
|
233
|
+
throw new ClientSafeError("Maximum room limit reached for this namespace");
|
|
234
|
+
}
|
|
235
|
+
|
|
103
236
|
const existingRoomState = roomCollection.get(requestedRoomId);
|
|
104
|
-
const initialState = (existingRoomState?.serverState ??
|
|
105
|
-
await Promise.resolve(handlers.initState?.(frame.payload) ?? {})) as ServerStateFor<TRoom>;
|
|
106
237
|
const auth = await resolveSocketAuth(socket, handlers, authCache, true);
|
|
238
|
+
const initialState = (existingRoomState?.serverState ??
|
|
239
|
+
await Promise.resolve(handlers.initState?.(frame.payload as JoinRequest<TRoom>) ?? {})) as ServerStateFor<TRoom>;
|
|
107
240
|
const provisional = createContext<TRoom, TAuth>(socket, namespaceState, {
|
|
108
241
|
adapter,
|
|
109
242
|
name: frame.roomType,
|
|
@@ -114,7 +247,7 @@ export function serveRoomType<TRoom extends RoomDefinition<any>, TAuth = unknown
|
|
|
114
247
|
roomProfile: undefined,
|
|
115
248
|
serverState: initialState,
|
|
116
249
|
});
|
|
117
|
-
const admission = await handlers.admit(frame.payload
|
|
250
|
+
const admission = await handlers.admit(frame.payload as JoinRequest<TRoom>, provisional);
|
|
118
251
|
assertMatchingRoomIds(
|
|
119
252
|
requestedRoomId,
|
|
120
253
|
admission.roomId,
|
|
@@ -128,6 +261,11 @@ export function serveRoomType<TRoom extends RoomDefinition<any>, TAuth = unknown
|
|
|
128
261
|
membersBySocketId: new Map(),
|
|
129
262
|
socketIdsByMemberId: new Map(),
|
|
130
263
|
};
|
|
264
|
+
|
|
265
|
+
if (!roomState.membersBySocketId.has(socket.id) && roomState.membersBySocketId.size >= MAX_MEMBERS_PER_ROOM) {
|
|
266
|
+
throw new ClientSafeError("Room is full");
|
|
267
|
+
}
|
|
268
|
+
|
|
131
269
|
roomState.roomProfile = roomState.roomProfile ?? admission.roomProfile;
|
|
132
270
|
roomState.serverState = roomState.serverState ?? provisional.serverState;
|
|
133
271
|
roomState.membersBySocketId.set(socket.id, {
|
|
@@ -168,10 +306,14 @@ export function serveRoomType<TRoom extends RoomDefinition<any>, TAuth = unknown
|
|
|
168
306
|
};
|
|
169
307
|
|
|
170
308
|
const onLeave = async (
|
|
171
|
-
|
|
309
|
+
raw: unknown,
|
|
172
310
|
ack?: (result: { ok: true; value: void } | { ok: false; error: string }) => void,
|
|
173
311
|
) => {
|
|
174
312
|
try {
|
|
313
|
+
if (ack !== undefined && typeof ack !== "function") {
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
const payload = validateLeaveFrame(raw);
|
|
175
317
|
assertMatchingRoomName(_room, payload.roomType);
|
|
176
318
|
const stored = getStoredMembership(namespaceState, payload.roomType, payload.roomId, socket.id);
|
|
177
319
|
if (!stored) {
|
|
@@ -202,11 +344,16 @@ export function serveRoomType<TRoom extends RoomDefinition<any>, TAuth = unknown
|
|
|
202
344
|
};
|
|
203
345
|
|
|
204
346
|
const onRpc = async (
|
|
205
|
-
|
|
347
|
+
raw: unknown,
|
|
206
348
|
ack?: (result: { ok: true; value: unknown } | { ok: false; error: string }) => void,
|
|
207
349
|
) => {
|
|
208
350
|
try {
|
|
351
|
+
if (ack !== undefined && typeof ack !== "function") {
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
const frame = validateRpcFrame(raw);
|
|
209
355
|
assertMatchingRoomName(_room, frame.roomType);
|
|
356
|
+
assertSafeHandlerName(frame.name);
|
|
210
357
|
if (!handlers.rpc || !Object.hasOwn(handlers.rpc, frame.name)) {
|
|
211
358
|
throw new ClientSafeError(`Unknown RPC '${frame.name}'`);
|
|
212
359
|
}
|
|
@@ -232,6 +379,9 @@ export function serveRoomType<TRoom extends RoomDefinition<any>, TAuth = unknown
|
|
|
232
379
|
serverState: getRoomState(namespaceState, frame.roomType, frame.roomId).serverState as ServerStateFor<TRoom>,
|
|
233
380
|
});
|
|
234
381
|
|
|
382
|
+
if (frame.args.length > 64) {
|
|
383
|
+
throw new ClientSafeError("Too many RPC arguments");
|
|
384
|
+
}
|
|
235
385
|
const result = await handler(...frame.args, ctx);
|
|
236
386
|
ack?.({ ok: true, value: result });
|
|
237
387
|
} catch (error) {
|
|
@@ -240,11 +390,16 @@ export function serveRoomType<TRoom extends RoomDefinition<any>, TAuth = unknown
|
|
|
240
390
|
};
|
|
241
391
|
|
|
242
392
|
const onClientEvent = async (
|
|
243
|
-
|
|
393
|
+
raw: unknown,
|
|
244
394
|
ack?: (result: { ok: true; value: void } | { ok: false; error: string }) => void,
|
|
245
395
|
) => {
|
|
246
396
|
try {
|
|
397
|
+
if (ack !== undefined && typeof ack !== "function") {
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
const frame = validateClientEventFrame(raw);
|
|
247
401
|
assertMatchingRoomName(_room, frame.roomType);
|
|
402
|
+
assertSafeHandlerName(frame.name);
|
|
248
403
|
const stored = getStoredMembership(namespaceState, frame.roomType, frame.roomId, socket.id);
|
|
249
404
|
if (!stored) {
|
|
250
405
|
throw new ClientSafeError("Socket is not joined to that room");
|
|
@@ -284,16 +439,14 @@ export function serveRoomType<TRoom extends RoomDefinition<any>, TAuth = unknown
|
|
|
284
439
|
};
|
|
285
440
|
|
|
286
441
|
const onPresenceQuery = async (
|
|
287
|
-
|
|
288
|
-
roomType: string;
|
|
289
|
-
roomId: string;
|
|
290
|
-
kind: "count" | "list";
|
|
291
|
-
offset?: number;
|
|
292
|
-
limit?: number;
|
|
293
|
-
},
|
|
442
|
+
raw: unknown,
|
|
294
443
|
ack?: (result: { ok: true; value: number | PresencePageFor<TRoom> } | { ok: false; error: string }) => void,
|
|
295
444
|
) => {
|
|
296
445
|
try {
|
|
446
|
+
if (ack !== undefined && typeof ack !== "function") {
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
const frame = validatePresenceQueryFrame(raw);
|
|
297
450
|
assertMatchingRoomName(_room, frame.roomType);
|
|
298
451
|
const stored = getStoredMembership(namespaceState, frame.roomType, frame.roomId, socket.id);
|
|
299
452
|
if (!stored) {
|
|
@@ -414,7 +567,7 @@ export function serveRoomType<TRoom extends RoomDefinition<any>, TAuth = unknown
|
|
|
414
567
|
.catch(() => undefined);
|
|
415
568
|
}
|
|
416
569
|
|
|
417
|
-
const
|
|
570
|
+
const cleanup = () => {
|
|
418
571
|
socket.off(JOIN_EVENT, onJoin);
|
|
419
572
|
socket.off(LEAVE_EVENT, onLeave);
|
|
420
573
|
socket.off(RPC_EVENT, onRpc);
|
|
@@ -423,13 +576,14 @@ export function serveRoomType<TRoom extends RoomDefinition<any>, TAuth = unknown
|
|
|
423
576
|
socket.off("disconnect", onDisconnect);
|
|
424
577
|
};
|
|
425
578
|
|
|
426
|
-
return
|
|
579
|
+
return {
|
|
580
|
+
cleanup,
|
|
427
581
|
rooms: () => listRoomSnapshots<TRoom>(namespaceState, _room.name),
|
|
428
582
|
room: (roomId: string) => getRoomSnapshot<TRoom>(namespaceState, _room.name, roomId),
|
|
429
583
|
members: (roomId: string, query?: PresenceListQuery) =>
|
|
430
584
|
getPresenceMembersPage<TRoom>(namespaceState, _room.name, roomId, query),
|
|
431
585
|
count: (roomId: string) => getPresenceCount(namespaceState, _room.name, roomId),
|
|
432
|
-
}
|
|
586
|
+
};
|
|
433
587
|
}
|
|
434
588
|
|
|
435
589
|
function createContext<TRoom extends RoomDefinition<any>, TAuth = unknown>(
|
|
@@ -499,6 +653,11 @@ async function resolveSocketAuth<TAuth>(
|
|
|
499
653
|
if (entry.value === undefined) {
|
|
500
654
|
const pending = Promise.resolve(handlers.onAuth?.(socket) as TAuth)
|
|
501
655
|
.then((auth) => {
|
|
656
|
+
if (auth === false) {
|
|
657
|
+
authCache.delete(socket);
|
|
658
|
+
throw new ClientSafeError("Unauthorized");
|
|
659
|
+
}
|
|
660
|
+
|
|
502
661
|
const current = authCache.get(socket) ?? {};
|
|
503
662
|
current.value = auth;
|
|
504
663
|
current.pending = undefined;
|
|
@@ -1026,7 +1185,12 @@ function extractRoomId(payload: unknown): string {
|
|
|
1026
1185
|
throw new ClientSafeError("Join request must include a string roomId");
|
|
1027
1186
|
}
|
|
1028
1187
|
|
|
1029
|
-
|
|
1188
|
+
const roomId = (payload as { roomId: string }).roomId;
|
|
1189
|
+
if (roomId.length === 0 || roomId.length > 256) {
|
|
1190
|
+
throw new ClientSafeError("roomId must be between 1 and 256 characters");
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
return roomId;
|
|
1030
1194
|
}
|
|
1031
1195
|
|
|
1032
1196
|
function toErrorMessage(error: unknown): string {
|
package/src/types.ts
CHANGED
|
@@ -218,6 +218,13 @@ export type EventListener<TRoom extends RoomDefinition<any>, TName extends keyof
|
|
|
218
218
|
meta: EventMetaFor<TRoom>,
|
|
219
219
|
) => void;
|
|
220
220
|
|
|
221
|
+
/**
|
|
222
|
+
* Event listener map accepted by `JoinedRoom.listen`.
|
|
223
|
+
*/
|
|
224
|
+
export type RoomEventListenerMap<TRoom extends RoomDefinition<any>> = Partial<{
|
|
225
|
+
[K in keyof RoomEvents<TRoom>]: EventListener<TRoom, K>;
|
|
226
|
+
}>;
|
|
227
|
+
|
|
221
228
|
/**
|
|
222
229
|
* Server-side snapshot entry for a connected socket membership.
|
|
223
230
|
*/
|
|
@@ -271,16 +278,31 @@ export type JoinedRoom<TRoom extends RoomDefinition<any>> = {
|
|
|
271
278
|
readonly rpc: RpcClientApi<TRoom>;
|
|
272
279
|
readonly emit: EventEmitApi<TRoom>;
|
|
273
280
|
readonly on: EventListenApi<TRoom>;
|
|
281
|
+
listen(options: RoomListenApi<TRoom>): () => void;
|
|
274
282
|
leave(): Promise<void>;
|
|
275
283
|
} & PresenceClientApi<TRoom>;
|
|
276
284
|
|
|
285
|
+
/**
|
|
286
|
+
* Batched listener registration accepted by `JoinedRoom.listen`.
|
|
287
|
+
*/
|
|
288
|
+
export type RoomListenApi<TRoom extends RoomDefinition<any>> = {
|
|
289
|
+
readonly events?: RoomEventListenerMap<TRoom>;
|
|
290
|
+
} & ([PresenceFor<TRoom>] extends [never]
|
|
291
|
+
? {}
|
|
292
|
+
: {
|
|
293
|
+
readonly presence?: {
|
|
294
|
+
onChange: (presence: PresenceFor<TRoom>) => void;
|
|
295
|
+
};
|
|
296
|
+
});
|
|
297
|
+
|
|
277
298
|
/**
|
|
278
299
|
* Handle returned by `serveRoomType`.
|
|
279
300
|
*
|
|
280
|
-
* The handle
|
|
301
|
+
* The handle unregisters listeners for the bound socket via `cleanup()`.
|
|
281
302
|
* It also exposes read-only introspection helpers for tests and diagnostics.
|
|
282
303
|
*/
|
|
283
|
-
export type RoomServerHandle<TRoom extends RoomDefinition<any>> =
|
|
304
|
+
export type RoomServerHandle<TRoom extends RoomDefinition<any>> = {
|
|
305
|
+
cleanup(): void;
|
|
284
306
|
rooms(): Array<RoomSnapshot<TRoom>>;
|
|
285
307
|
room(roomId: string): RoomSnapshot<TRoom> | undefined;
|
|
286
308
|
members(roomId: string, query?: PresenceListQuery): PresencePageFor<TRoom> | undefined;
|
|
@@ -439,14 +461,16 @@ type RoomServerHandlersCommon<TRoom extends RoomDefinition<any>, TAuth> = {
|
|
|
439
461
|
*
|
|
440
462
|
* `onAuth` is required when `TAuth` is explicitly typed to a non-`unknown`
|
|
441
463
|
* shape, and optional otherwise.
|
|
464
|
+
*
|
|
465
|
+
* Returning `false` rejects the socket before any room state is initialized.
|
|
442
466
|
*/
|
|
443
467
|
export type RoomServerHandlers<TRoom extends RoomDefinition<any>, TAuth = unknown> =
|
|
444
468
|
IsUnknown<TAuth> extends true
|
|
445
469
|
? RoomServerHandlersCommon<TRoom, TAuth> & {
|
|
446
|
-
onAuth?(socket: ServerSocketLike): Promise<TAuth> | TAuth;
|
|
470
|
+
onAuth?(socket: ServerSocketLike): Promise<TAuth | false> | TAuth | false;
|
|
447
471
|
}
|
|
448
472
|
: RoomServerHandlersCommon<TRoom, TAuth> & {
|
|
449
|
-
onAuth(socket: ServerSocketLike): Promise<TAuth> | TAuth;
|
|
473
|
+
onAuth(socket: ServerSocketLike): Promise<TAuth | false> | TAuth | false;
|
|
450
474
|
};
|
|
451
475
|
|
|
452
476
|
/**
|