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/README.md +3 -2
- package/changelog.md +32 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +15 -3
- package/dist/server.js.map +1 -1
- package/dist/types.d.ts +40 -9
- package/dist/types.d.ts.map +1 -1
- package/example/package-lock.json +1330 -0
- package/example/public/app.js +3988 -0
- package/jsr.json +1 -1
- package/package.json +1 -1
- package/src/index.ts +1 -0
- package/src/server.ts +31 -5
- package/src/types.ts +43 -16
- package/test/room.spec.ts +71 -0
package/jsr.json
CHANGED
package/package.json
CHANGED
package/src/index.ts
CHANGED
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
|
|
251
|
-
|
|
254
|
+
const admission = await resolveAdmission(
|
|
255
|
+
socket,
|
|
256
|
+
handlers,
|
|
252
257
|
requestedRoomId,
|
|
253
|
-
|
|
254
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
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`
|
|
463
|
-
* shape, and optional otherwise.
|
|
487
|
+
* `onAuth` and `admit` are both optional.
|
|
464
488
|
*
|
|
465
|
-
*
|
|
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
|
-
|
|
469
|
-
?
|
|
470
|
-
|
|
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");
|