Skip to content

Commit 73518ae

Browse files
authored
Feat: Room service (#15)
* Room service draft. * Ref: extracting common params. * Ref: emission makers accept whole config. * Issue 952: exposing HandlingFeatures. * Ren: rooms -> withRooms. * Feat: getRooms(). * getRooms() test. * Tests for makeRoomService().
1 parent e7e13d5 commit 73518ae

7 files changed

+146
-69
lines changed

src/action.spec.ts

+12
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ describe("Action", () => {
3131
const emitMock = vi.fn();
3232
const broadcastMock = vi.fn();
3333
const isConnectedMock = vi.fn();
34+
const withRoomsMock = vi.fn();
35+
const getRoomsMock = vi.fn();
3436

3537
test("should handle simple action", async () => {
3638
await simpleAction.execute({
@@ -39,12 +41,16 @@ describe("Action", () => {
3941
params: ["some"],
4042
emit: emitMock,
4143
broadcast: broadcastMock,
44+
withRooms: withRoomsMock,
45+
getRooms: getRoomsMock,
4246
isConnected: isConnectedMock,
4347
socketId: "ID",
4448
});
4549
expect(loggerMock.error).not.toHaveBeenCalled();
4650
expect(simpleHandler).toHaveBeenLastCalledWith({
4751
broadcast: broadcastMock,
52+
withRooms: withRoomsMock,
53+
getRooms: getRoomsMock,
4854
emit: emitMock,
4955
input: ["some"],
5056
isConnected: isConnectedMock,
@@ -61,12 +67,16 @@ describe("Action", () => {
6167
params: ["some", ackMock],
6268
emit: emitMock,
6369
broadcast: broadcastMock,
70+
withRooms: withRoomsMock,
71+
getRooms: getRoomsMock,
6472
isConnected: isConnectedMock,
6573
socketId: "ID",
6674
});
6775
expect(loggerMock.error).not.toHaveBeenCalled();
6876
expect(ackHandler).toHaveBeenLastCalledWith({
6977
broadcast: broadcastMock,
78+
withRooms: withRoomsMock,
79+
getRooms: getRoomsMock,
7080
emit: emitMock,
7181
input: ["some"],
7282
isConnected: isConnectedMock,
@@ -83,6 +93,8 @@ describe("Action", () => {
8393
params: [], // too short
8494
emit: emitMock,
8595
broadcast: broadcastMock,
96+
withRooms: withRoomsMock,
97+
getRooms: getRoomsMock,
8698
isConnected: isConnectedMock,
8799
socketId: "ID",
88100
});

src/action.ts

+16-22
Original file line numberDiff line numberDiff line change
@@ -2,32 +2,36 @@ import { init, last } from "ramda";
22
import type { Socket } from "socket.io";
33
import { z } from "zod";
44
import { AckActionDef, SimpleActionDef } from "./actions-factory";
5-
import { Broadcaster, EmissionMap, Emitter } from "./emission";
5+
import { Broadcaster, EmissionMap, Emitter, RoomService } from "./emission";
66
import { AbstractLogger } from "./logger";
77

88
export interface SocketFeatures {
99
isConnected: () => boolean;
1010
socketId: Socket["id"];
11+
getRooms: () => string[];
12+
}
13+
14+
export interface HandlingFeatures<E extends EmissionMap> {
15+
logger: AbstractLogger;
16+
emit: Emitter<E>;
17+
broadcast: Broadcaster<E>;
18+
withRooms: RoomService<E>;
1119
}
1220

1321
export type Handler<IN, OUT, E extends EmissionMap> = (
1422
params: {
1523
input: IN;
16-
logger: AbstractLogger;
17-
emit: Emitter<E>;
18-
broadcast: Broadcaster<E>;
19-
} & SocketFeatures,
24+
} & SocketFeatures &
25+
HandlingFeatures<E>,
2026
) => Promise<OUT>;
2127

2228
export abstract class AbstractAction {
2329
public abstract execute(
2430
params: {
2531
event: string;
2632
params: unknown[];
27-
logger: AbstractLogger;
28-
emit: Emitter<EmissionMap>;
29-
broadcast: Broadcaster<EmissionMap>;
30-
} & SocketFeatures,
33+
} & SocketFeatures &
34+
HandlingFeatures<EmissionMap>,
3135
): Promise<void>;
3236
}
3337

@@ -80,30 +84,20 @@ export class Action<
8084
event,
8185
params,
8286
logger,
83-
emit,
84-
broadcast,
8587
...rest
8688
}: {
8789
event: string;
8890
params: unknown[];
89-
logger: AbstractLogger;
90-
emit: Emitter<EmissionMap>;
91-
broadcast: Broadcaster<EmissionMap>;
92-
} & SocketFeatures): Promise<void> {
91+
} & SocketFeatures &
92+
HandlingFeatures<EmissionMap>): Promise<void> {
9393
try {
9494
const input = this.#parseInput(params);
9595
logger.debug(
9696
`parsed input (${this.#outputSchema ? "excl." : "no"} ack)`,
9797
input,
9898
);
9999
const ack = this.#parseAckCb(params);
100-
const output = await this.#handler({
101-
input,
102-
logger,
103-
emit,
104-
broadcast,
105-
...rest,
106-
});
100+
const output = await this.#handler({ input, logger, ...rest });
107101
const response = this.#parseOutput(output);
108102
if (ack && response) {
109103
logger.debug("parsed output", response);

src/attach.spec.ts

+9
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ describe("Attach", () => {
99
const socketMock = {
1010
id: "ID",
1111
connected: false,
12+
rooms: new Set(["room1", "room2"]),
1213
on: vi.fn(),
1314
onAny: vi.fn(),
1415
};
@@ -66,6 +67,8 @@ describe("Attach", () => {
6667
await call[1]([123, 456]);
6768
expect(actionsMock.test.execute).toHaveBeenLastCalledWith({
6869
broadcast: expect.any(Function),
70+
withRooms: expect.any(Function),
71+
getRooms: expect.any(Function),
6972
emit: expect.any(Function),
7073
event: "test",
7174
isConnected: expect.any(Function),
@@ -74,6 +77,12 @@ describe("Attach", () => {
7477
socketId: "ID",
7578
});
7679

80+
// getRooms:
81+
expect(actionsMock.test.execute.mock.lastCall[0].getRooms()).toEqual([
82+
"room1",
83+
"room2",
84+
]);
85+
7786
// isConnected:
7887
expect(
7988
actionsMock.test.execute.mock.lastCall[0].isConnected(),

src/attach.ts

+27-18
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,25 @@
11
import http from "node:http";
22
import type { Server } from "socket.io";
3-
import { ActionMap, Handler, SocketFeatures } from "./action";
3+
import { ActionMap, Handler, HandlingFeatures, SocketFeatures } from "./action";
44
import { SocketsConfig } from "./config";
5-
import { EmissionMap, makeBroadcaster, makeEmitter } from "./emission";
5+
import {
6+
EmissionMap,
7+
makeBroadcaster,
8+
makeEmitter,
9+
makeRoomService,
10+
} from "./emission";
611

712
export const attachSockets = <E extends EmissionMap>({
813
io,
914
actions,
1015
target,
11-
config: { emission, timeout, logger },
12-
onConnection = ({ socketId }) => logger.debug("User connected", socketId),
13-
onDisconnect = ({ socketId }) => logger.debug("User disconnected", socketId),
16+
config,
17+
onConnection = ({ socketId }) =>
18+
config.logger.debug("User connected", socketId),
19+
onDisconnect = ({ socketId }) =>
20+
config.logger.debug("User disconnected", socketId),
1421
onAnyEvent = ({ input: [event], socketId }) =>
15-
logger.debug(`${event} from ${socketId}`),
22+
config.logger.debug(`${event} from ${socketId}`),
1623
}: {
1724
/**
1825
* @desc The Socket.IO server
@@ -36,26 +43,28 @@ export const attachSockets = <E extends EmissionMap>({
3643
onDisconnect?: Handler<[], void, E>;
3744
onAnyEvent?: Handler<[string], void, E>;
3845
}): Server => {
39-
logger.info("ZOD-SOCKETS", target.address());
46+
config.logger.info("ZOD-SOCKETS", target.address());
4047
io.on("connection", async (socket) => {
41-
const commons: SocketFeatures = {
48+
const emit = makeEmitter({ socket, config });
49+
const broadcast = makeBroadcaster({ socket, config });
50+
const withRooms = makeRoomService({ socket, config });
51+
const commons: SocketFeatures & HandlingFeatures<E> = {
4252
socketId: socket.id,
4353
isConnected: () => socket.connected,
54+
getRooms: () => Array.from(socket.rooms),
55+
logger: config.logger,
56+
emit,
57+
broadcast,
58+
withRooms,
4459
};
45-
const emit = makeEmitter({ emission, socket, logger, timeout });
46-
const broadcast = makeBroadcaster({ emission, socket, logger, timeout });
47-
await onConnection({ input: [], logger, emit, broadcast, ...commons });
48-
socket.onAny((event) =>
49-
onAnyEvent({ input: [event], logger, emit, broadcast, ...commons }),
50-
);
60+
await onConnection({ input: [], ...commons });
61+
socket.onAny((event) => onAnyEvent({ input: [event], ...commons }));
5162
for (const [event, action] of Object.entries(actions)) {
5263
socket.on(event, async (...params) =>
53-
action.execute({ event, params, logger, emit, broadcast, ...commons }),
64+
action.execute({ event, params, ...commons }),
5465
);
5566
}
56-
socket.on("disconnect", () =>
57-
onDisconnect({ input: [], logger, emit, broadcast, ...commons }),
58-
);
67+
socket.on("disconnect", () => onDisconnect({ input: [], ...commons }));
5968
});
6069
return io.attach(target);
6170
};

src/emission.spec.ts

+48-9
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Socket } from "socket.io";
22
import { MockedFunction, describe, expect, test, vi } from "vitest";
33
import { z } from "zod";
4-
import { makeBroadcaster, makeEmitter } from "./emission";
4+
import { makeBroadcaster, makeEmitter, makeRoomService } from "./emission";
55
import { AbstractLogger } from "./logger";
66

77
describe("Emission", () => {
@@ -15,29 +15,39 @@ describe("Emission", () => {
1515
};
1616

1717
const socketMock: Record<
18-
"emit" | "timeout" | "emitWithAck",
18+
"emit" | "timeout" | "emitWithAck" | "join" | "leave",
1919
MockedFunction<any>
20-
> & { id: string; broadcast: typeof broadcastMock } = {
20+
> & {
21+
id: string;
22+
broadcast: typeof broadcastMock;
23+
to: (rooms: string | string[]) => typeof broadcastMock;
24+
} = {
2125
id: "ID",
2226
emit: vi.fn(),
2327
timeout: vi.fn(() => socketMock),
2428
emitWithAck: vi.fn(),
2529
broadcast: broadcastMock,
30+
to: vi.fn(() => broadcastMock),
31+
join: vi.fn(),
32+
leave: vi.fn(),
2633
};
2734
const loggerMock = { debug: vi.fn() };
35+
const config = {
36+
logger: loggerMock as unknown as AbstractLogger,
37+
timeout: 100,
38+
emission: {
39+
one: { schema: z.tuple([z.string()]) },
40+
two: { schema: z.tuple([z.number()]), ack: z.tuple([z.string()]) },
41+
},
42+
};
2843

2944
describe.each([
3045
{ maker: makeEmitter, target: socketMock, ack: ["test"] },
3146
{ maker: makeBroadcaster, target: broadcastMock, ack: [["test"]] },
3247
])("$maker.name", ({ maker, target, ack }) => {
3348
const emitter = maker({
3449
socket: socketMock as unknown as Socket,
35-
logger: loggerMock as unknown as AbstractLogger,
36-
timeout: 100,
37-
emission: {
38-
one: { schema: z.tuple([z.string()]) },
39-
two: { schema: z.tuple([z.number()]), ack: z.tuple([z.string()]) },
40-
},
50+
config,
4151
});
4252

4353
test("should create an emitter", () => {
@@ -60,4 +70,33 @@ describe("Emission", () => {
6070
expect(await emitter("two", 123)).toEqual(ack);
6171
});
6272
});
73+
74+
describe("makeRoomService", () => {
75+
test.each(["room1", ["room2", "room3"]])(
76+
"should provide methods in rooms context %#",
77+
async (rooms) => {
78+
const withRooms = makeRoomService({
79+
socket: socketMock as unknown as Socket,
80+
config,
81+
});
82+
expect(typeof withRooms).toBe("function");
83+
const { broadcast, leave, join } = withRooms(rooms);
84+
expect(socketMock.to).toHaveBeenLastCalledWith(rooms);
85+
for (const method of [broadcast, leave, join]) {
86+
expect(typeof method).toBe("function");
87+
}
88+
join();
89+
expect(socketMock.join).toHaveBeenLastCalledWith(rooms);
90+
if (typeof rooms === "string") {
91+
leave();
92+
expect(socketMock.leave).toHaveBeenLastCalledWith(rooms);
93+
} else {
94+
await leave();
95+
for (const room of rooms) {
96+
expect(socketMock.leave).toHaveBeenCalledWith(room);
97+
}
98+
}
99+
},
100+
);
101+
});
63102
});

0 commit comments

Comments
 (0)