room-kit 1.0.2 → 1.0.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/jsr.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onlycliches/room-kit",
3
- "version": "1.0.2",
3
+ "version": "1.0.5",
4
4
  "description": "Type-safe channel primitives for Socket.IO events, requests, streams, and room membership.",
5
5
  "exports": "./src/index.ts",
6
6
  "publish": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "room-kit",
3
- "version": "1.0.2",
3
+ "version": "1.0.5",
4
4
  "description": "Type-safe room membership, presence, and realtime messaging for Socket.IO.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
package/src/index.ts CHANGED
@@ -29,6 +29,7 @@ export type {
29
29
  RoomServerHandlers,
30
30
  RoomSnapshot,
31
31
  ServerAdmission,
32
+ ServerAdmissionInput,
32
33
  ServerSocketLike,
33
34
  VisibleMemberFor,
34
35
  } from "./types";
package/src/server.ts CHANGED
@@ -3,6 +3,7 @@ import type {
3
3
  EventEmitApi,
4
4
  EventMetaFor,
5
5
  JoinRequest,
6
+ MemberProfileFor,
6
7
  PresenceFor,
7
8
  PresenceListQuery,
8
9
  PresencePageFor,
@@ -10,6 +11,7 @@ import type {
10
11
  RoomMemberSnapshot,
11
12
  RoomDefinition,
12
13
  RoomEvents,
14
+ RoomProfileFor,
13
15
  RoomServerAdapter,
14
16
  RoomServerBroadcastApi,
15
17
  RoomServerHandle,
@@ -18,6 +20,7 @@ import type {
18
20
  RoomSnapshot,
19
21
  ServerSocketLike,
20
22
  ServerStateFor,
23
+ ServerAdmission,
21
24
  PresenceValueFor,
22
25
  VisibleMemberFor,
23
26
  } from "./types";
@@ -178,6 +181,7 @@ type NamespaceState = {
178
181
  type AuthCacheEntry<TAuth> = {
179
182
  pending?: Promise<TAuth>;
180
183
  value?: TAuth;
184
+ hasValue?: boolean;
181
185
  };
182
186
 
183
187
  const namespaceStates = new WeakMap<object, NamespaceState>();
@@ -247,11 +251,12 @@ export function serveRoomType<TRoom extends RoomDefinition<any>, TAuth = unknown
247
251
  roomProfile: undefined,
248
252
  serverState: initialState,
249
253
  });
250
- const admission = await handlers.admit(frame.payload as JoinRequest<TRoom>, provisional);
251
- assertMatchingRoomIds(
254
+ const admission = await resolveAdmission(
255
+ socket,
256
+ handlers,
252
257
  requestedRoomId,
253
- admission.roomId,
254
- (admission.roomProfile as { roomId: string }).roomId,
258
+ frame.payload as JoinRequest<TRoom>,
259
+ provisional,
255
260
  );
256
261
 
257
262
  const roomState: RoomState<TRoom> = roomCollection.get(admission.roomId) ?? {
@@ -650,7 +655,7 @@ async function resolveSocketAuth<TAuth>(
650
655
  return entry.pending;
651
656
  }
652
657
 
653
- if (entry.value === undefined) {
658
+ if (!entry.hasValue) {
654
659
  const pending = Promise.resolve(handlers.onAuth?.(socket) as TAuth)
655
660
  .then((auth) => {
656
661
  if (auth === false) {
@@ -660,6 +665,7 @@ async function resolveSocketAuth<TAuth>(
660
665
 
661
666
  const current = authCache.get(socket) ?? {};
662
667
  current.value = auth;
668
+ current.hasValue = true;
663
669
  current.pending = undefined;
664
670
  authCache.set(socket, current);
665
671
  return auth;
@@ -692,6 +698,26 @@ async function resolveSocketAuth<TAuth>(
692
698
  handleRejectedAuth(authCache, socket, decision);
693
699
  }
694
700
 
701
+ async function resolveAdmission<TRoom extends RoomDefinition<any>, TAuth>(
702
+ socket: ServerSocketLike,
703
+ handlers: RoomServerHandlers<TRoom, TAuth>,
704
+ joinRoomId: string,
705
+ join: JoinRequest<TRoom>,
706
+ ctx: RoomServerContext<TRoom, TAuth>,
707
+ ): Promise<ServerAdmission<TRoom>> {
708
+ const admission = handlers.admit ? await Promise.resolve(handlers.admit(join, ctx)) : {};
709
+ const roomId = admission.roomId ?? joinRoomId;
710
+ const roomProfile = admission.roomProfile ?? ({ roomId } as RoomProfileFor<TRoom>);
711
+ assertMatchingRoomIds(joinRoomId, roomId, (roomProfile as { roomId: string }).roomId);
712
+
713
+ return {
714
+ roomId,
715
+ memberId: admission.memberId ?? socket.id,
716
+ memberProfile: admission.memberProfile ?? ({} as MemberProfileFor<TRoom>),
717
+ roomProfile,
718
+ };
719
+ }
720
+
695
721
  function handleRejectedAuth<TAuth>(
696
722
  authCache: WeakMap<ServerSocketLike, AuthCacheEntry<TAuth>>,
697
723
  socket: ServerSocketLike,
package/src/types.ts CHANGED
@@ -381,7 +381,12 @@ export type EventListenApi<TRoom extends RoomDefinition<any>> = {
381
381
  };
382
382
 
383
383
  /**
384
- * Successful admission payload returned by `handlers.admit`.
384
+ * Successful admission payload used by the runtime after admission is
385
+ * normalized.
386
+ *
387
+ * The server persists these values as the membership record for the socket.
388
+ * `roomId`, `memberId`, `memberProfile`, and `roomProfile` must all be
389
+ * present in the finalized admission record.
385
390
  */
386
391
  export type ServerAdmission<TRoom extends RoomDefinition<any>> = {
387
392
  roomId: string;
@@ -390,6 +395,24 @@ export type ServerAdmission<TRoom extends RoomDefinition<any>> = {
390
395
  roomProfile: RoomProfileFor<TRoom>;
391
396
  };
392
397
 
398
+ /**
399
+ * Partial admission payload accepted from `handlers.admit`.
400
+ *
401
+ * Handlers may return only the fields they want to override. The runtime fills
402
+ * missing values with:
403
+ *
404
+ * - `roomId`: the join request room id
405
+ * - `memberId`: the connected socket id
406
+ * - `memberProfile`: an empty object
407
+ * - `roomProfile`: an object with the resolved `roomId`
408
+ *
409
+ * The runtime still validates that any supplied `roomId` matches the join
410
+ * request and that any supplied `roomProfile.roomId` matches the resolved room
411
+ * id.
412
+ *
413
+ */
414
+ export type ServerAdmissionInput<TRoom extends RoomDefinition<any>> = Partial<ServerAdmission<TRoom>>;
415
+
393
416
  /**
394
417
  * Optional decision result returned by `revalidateAuth`.
395
418
  *
@@ -407,10 +430,6 @@ export type AuthRevalidationDecision<TAuth> =
407
430
  message?: string;
408
431
  };
409
432
 
410
- type IsUnknown<T> = unknown extends T
411
- ? ([T] extends [unknown] ? true : false)
412
- : false;
413
-
414
433
  type RoomServerHandlersCommon<TRoom extends RoomDefinition<any>, TAuth> = {
415
434
  initState?(join: JoinRequest<TRoom>): Promise<ServerStateFor<TRoom>> | ServerStateFor<TRoom>;
416
435
  /**
@@ -428,7 +447,13 @@ type RoomServerHandlersCommon<TRoom extends RoomDefinition<any>, TAuth> = {
428
447
  * `{ kind: "reject", message }` to reject the operation.
429
448
  */
430
449
  revalidateAuth?(socket: ServerSocketLike, auth: TAuth): Promise<AuthRevalidationDecision<TAuth> | void> | AuthRevalidationDecision<TAuth> | void;
431
- admit(join: JoinRequest<TRoom>, ctx: RoomServerContext<TRoom, TAuth>): Promise<ServerAdmission<TRoom>> | ServerAdmission<TRoom>;
450
+ /**
451
+ * Optional join admission hook.
452
+ *
453
+ * Return only the pieces you want to override. Missing fields are filled
454
+ * by the runtime.
455
+ */
456
+ admit?(join: JoinRequest<TRoom>, ctx: RoomServerContext<TRoom, TAuth>): Promise<ServerAdmissionInput<TRoom>> | ServerAdmissionInput<TRoom>;
432
457
  /**
433
458
  * Called once when a server socket disconnects.
434
459
  *
@@ -459,19 +484,21 @@ type RoomServerHandlersCommon<TRoom extends RoomDefinition<any>, TAuth> = {
459
484
  /**
460
485
  * Server handler contract for a room type.
461
486
  *
462
- * `onAuth` is required when `TAuth` is explicitly typed to a non-`unknown`
463
- * shape, and optional otherwise.
487
+ * `onAuth` and `admit` are both optional.
464
488
  *
465
- * Returning `false` rejects the socket before any room state is initialized.
489
+ * When `onAuth` is omitted, the socket is accepted with an `undefined` auth
490
+ * value. Returning `false` from `onAuth` still rejects the socket before room
491
+ * initialization.
492
+ *
493
+ * When `admit` is omitted, the runtime derives the admission record from the
494
+ * join request and socket id. When `admit` is present, it may return a partial
495
+ * admission and the server will fill in any missing fields as described by
496
+ * `ServerAdmissionInput`.
466
497
  */
467
498
  export type RoomServerHandlers<TRoom extends RoomDefinition<any>, TAuth = unknown> =
468
- IsUnknown<TAuth> extends true
469
- ? RoomServerHandlersCommon<TRoom, TAuth> & {
470
- onAuth?(socket: ServerSocketLike): Promise<TAuth | false> | TAuth | false;
471
- }
472
- : RoomServerHandlersCommon<TRoom, TAuth> & {
473
- onAuth(socket: ServerSocketLike): Promise<TAuth | false> | TAuth | false;
474
- };
499
+ RoomServerHandlersCommon<TRoom, TAuth> & {
500
+ onAuth?(socket: ServerSocketLike): Promise<TAuth | false> | TAuth | false;
501
+ };
475
502
 
476
503
  /**
477
504
  * Context provided to server handlers (`admit`, `rpc`, `events`, join/leave).
package/test/room.spec.ts CHANGED
@@ -875,6 +875,77 @@ describe("room kit", () => {
875
875
  connection.close();
876
876
  });
877
877
 
878
+ it("uses default auth and admission when handlers are omitted", async () => {
879
+ const namespace = new MockNamespace();
880
+ const roomType = createRoomType("default-server-config", "list");
881
+ const { client, connection, stop } = createClientPair(namespace, "alice-socket", roomType, {});
882
+
883
+ const joined = await client.join({
884
+ roomId: "room-1",
885
+ roomKey: "shared-key",
886
+ userId: "alice",
887
+ userName: "Ada",
888
+ });
889
+
890
+ expect(joined.memberId).toBe("alice-socket");
891
+ expect(joined.roomProfile).toMatchObject({
892
+ roomId: "room-1",
893
+ });
894
+ expect(stop.room("room-1")).toMatchObject({
895
+ roomId: "room-1",
896
+ members: [
897
+ {
898
+ memberId: "alice-socket",
899
+ memberProfile: {},
900
+ },
901
+ ],
902
+ });
903
+
904
+ stop.cleanup();
905
+ connection.close();
906
+ });
907
+
908
+ it("fills in omitted admission fields", async () => {
909
+ const namespace = new MockNamespace();
910
+ const roomType = createRoomType("partial-admission", "list");
911
+ const handlers: RoomServerHandlers<typeof roomType> = {
912
+ admit: (join) => ({
913
+ memberProfile: {
914
+ userId: join.userId,
915
+ userName: join.userName,
916
+ },
917
+ }),
918
+ };
919
+ const { client, connection, stop } = createClientPair(namespace, "alice-socket", roomType, handlers);
920
+
921
+ const joined = await client.join({
922
+ roomId: "room-1",
923
+ roomKey: "shared-key",
924
+ userId: "alice",
925
+ userName: "Ada",
926
+ });
927
+
928
+ expect(joined.memberId).toBe("alice-socket");
929
+ expect(joined.roomProfile).toMatchObject({
930
+ roomId: "room-1",
931
+ });
932
+ expect(stop.room("room-1")).toMatchObject({
933
+ roomId: "room-1",
934
+ members: [
935
+ {
936
+ memberId: "alice-socket",
937
+ memberProfile: {
938
+ userId: "alice",
939
+ userName: "Ada",
940
+ },
941
+ },
942
+ ],
943
+ });
944
+
945
+ stop.cleanup();
946
+ connection.close();
947
+ });
948
+
878
949
  it("rejects when onAuth fails", async () => {
879
950
  const namespace = new MockNamespace();
880
951
  const roomType = createRoomType("reject-auth", "list");