Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Client metadata #19

Merged
merged 12 commits into from
Feb 9, 2024
4 changes: 4 additions & 0 deletions example/actions/chat.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import { z } from "zod";
import { Metadata } from "../config";
import { actionsFactory } from "../factories";

export const onChat = actionsFactory.build({
input: z.tuple([z.string()]),
handler: async ({ input: [message], client, all, logger }) => {
try {
await all.broadcast("chat", message, { from: client.id });
client.setData<Metadata>({
msgCount: (client.getData<Metadata>().msgCount || 0) + 1,
});
} catch (error) {
logger.error("Failed to broadcast", error);
}
Expand Down
6 changes: 6 additions & 0 deletions example/config.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import { z } from "zod";
import { createConfig } from "../src";

/** @desc Client metadata */
export interface Metadata {
/** @desc Number of messages sent using the chat event */
msgCount: number;
}

export const config = createConfig({
timeout: 2000,
logger: console,
Expand Down
18 changes: 15 additions & 3 deletions src/action.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ describe("Action", () => {
const getRoomsMock = vi.fn();
const getAllRoomsMock = vi.fn();
const getAllClientsMock = vi.fn();
const getDataMock = vi.fn();
const setDataMock = vi.fn();

test("should handle simple action", async () => {
await simpleAction.execute({
Expand All @@ -48,10 +50,12 @@ describe("Action", () => {
getRooms: getAllRoomsMock,
},
client: {
id: "ID",
emit: emitMock,
getRooms: getRoomsMock,
isConnected: isConnectedMock,
id: "ID",
getData: getDataMock,
setData: setDataMock,
},
});
expect(loggerMock.error).not.toHaveBeenCalled();
Expand All @@ -65,10 +69,12 @@ describe("Action", () => {
getRooms: getAllRoomsMock,
},
client: {
id: "ID",
emit: emitMock,
isConnected: isConnectedMock,
getRooms: getRoomsMock,
id: "ID",
getData: getDataMock,
setData: setDataMock,
},
});
});
Expand All @@ -86,10 +92,12 @@ describe("Action", () => {
getRooms: getAllRoomsMock,
},
client: {
id: "ID",
emit: emitMock,
getRooms: getRoomsMock,
isConnected: isConnectedMock,
id: "ID",
getData: getDataMock,
setData: setDataMock,
},
});
expect(loggerMock.error).not.toHaveBeenCalled();
Expand All @@ -99,6 +107,8 @@ describe("Action", () => {
getRooms: getRoomsMock,
isConnected: isConnectedMock,
emit: emitMock,
getData: getDataMock,
setData: setDataMock,
},
all: {
broadcast: broadcastMock,
Expand Down Expand Up @@ -128,6 +138,8 @@ describe("Action", () => {
getRooms: getRoomsMock,
isConnected: isConnectedMock,
emit: emitMock,
getData: getDataMock,
setData: setDataMock,
},
});
expect(loggerMock.error).toHaveBeenCalled();
Expand Down
19 changes: 17 additions & 2 deletions src/action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,19 @@ export interface Client<E extends EmissionMap> {
id: Socket["id"];
/** @desc Returns the list of the rooms the client in */
getRooms: () => string[];
/** @desc Sends a new event to the client (this is not acknowledgement) */
/**
* @desc Sends a new event to the client (this is not acknowledgement)
* @throws z.ZodError on validation
* @throws Error on ack timeout
* */
emit: Emitter<E>;
/** @desc Returns the client metadata according to the specified type or empty object */
getData: <D extends object>() => Readonly<Partial<D>>;
/**
* @desc Sets the client metadata according to the schema specified in config
RobinTail marked this conversation as resolved.
Show resolved Hide resolved
* @throws z.ZodError on validation
* */
setData: <D extends object>(value: D) => void;
}

export interface HandlingFeatures<E extends EmissionMap> {
Expand All @@ -23,7 +34,11 @@ export interface HandlingFeatures<E extends EmissionMap> {
client: Client<E>;
/** @desc The global scope */
all: {
/** @desc Emits to everyone */
/**
* @desc Emits to everyone
* @throws z.ZodError on validation
* @throws Error on ack timeout
* */
broadcast: Broadcaster<E>;
/** @desc Returns the list of available rooms */
getRooms: () => string[];
Expand Down
42 changes: 31 additions & 11 deletions src/attach.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,9 @@ describe("Attach", () => {

// on connection:
await ioMock.on.mock.lastCall![1](socketMock);
expect(loggerMock.debug).toHaveBeenLastCalledWith(
"Client connected",
"ID",
);
expect(loggerMock.debug).toHaveBeenLastCalledWith("Client connected", {
id: "ID",
});
expect(socketMock.onAny).toHaveBeenLastCalledWith(expect.any(Function));
expect(socketMock.on).toHaveBeenCalledWith("test", expect.any(Function));
expect(socketMock.on).toHaveBeenLastCalledWith(
Expand All @@ -69,14 +68,13 @@ describe("Attach", () => {

// on disconnect:
socketMock.on.mock.lastCall![1]();
expect(loggerMock.debug).toHaveBeenLastCalledWith(
"Client disconnected",
"ID",
);
expect(loggerMock.debug).toHaveBeenLastCalledWith("Client disconnected", {
id: "ID",
});

// on any event:
socketMock.onAny.mock.lastCall![0]("test");
expect(loggerMock.debug).toHaveBeenLastCalledWith("test from ID");
expect(loggerMock.debug).toHaveBeenLastCalledWith("test from ID", {});

// on the listened event:
const call = socketMock.on.mock.calls.find(([evt]) => evt === "test");
Expand All @@ -92,6 +90,8 @@ describe("Attach", () => {
isConnected: expect.any(Function),
getRooms: expect.any(Function),
emit: expect.any(Function),
getData: expect.any(Function),
setData: expect.any(Function),
},
all: {
broadcast: expect.any(Function),
Expand Down Expand Up @@ -122,9 +122,29 @@ describe("Attach", () => {
await expect(
actionsMock.test.execute.mock.lastCall[0].all.getClients(),
).resolves.toEqual([
{ id: "ID", rooms: ["room1", "room2"] },
{ id: "other", rooms: ["room3"] },
{
id: "ID",
rooms: ["room1", "room2"],
getData: expect.any(Function),
},
{
id: "other",
rooms: ["room3"],
getData: expect.any(Function),
},
]);

// client.setData:
actionsMock.test.execute.mock.lastCall[0].client.setData({
name: "user",
});

// client.getData:
expect(
actionsMock.test.execute.mock.lastCall[0].client.getData(),
).toEqual({
name: "user",
});
});
});
});
14 changes: 8 additions & 6 deletions src/attach.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,12 @@ export const attachSockets = <E extends EmissionMap>({
actions,
target,
config,
onConnection = ({ client }) =>
config.logger.debug("Client connected", client.id),
onDisconnect = ({ client }) =>
config.logger.debug("Client disconnected", client.id),
onAnyEvent = ({ input: [event], client }) =>
config.logger.debug(`${event} from ${client.id}`),
onConnection = ({ client: { id, getData } }) =>
config.logger.debug("Client connected", { ...getData(), id }),
onDisconnect = ({ client: { id, getData } }) =>
config.logger.debug("Client disconnected", { ...getData(), id }),
onAnyEvent = ({ input: [event], client: { id, getData } }) =>
config.logger.debug(`${event} from ${id}`, getData()),
}: {
/**
* @desc The Socket.IO server
Expand Down Expand Up @@ -59,6 +59,8 @@ export const attachSockets = <E extends EmissionMap>({
id: socket.id,
isConnected: () => socket.connected,
getRooms: () => Array.from(socket.rooms),
getData: () => socket.data || {},
setData: (value) => (socket.data = value),
},
all: {
broadcast,
Expand Down
6 changes: 6 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,14 @@ import { EmissionMap } from "./emission";
import { AbstractLogger } from "./logger";

export interface Config<E extends EmissionMap> {
/**
* @desc The instance of a logger
* @example console
* */
logger: AbstractLogger;
/** @desc The acknowledgment awaiting timeout */
timeout: number;
/** @desc The events that the server can emit */
emission: E;
}

Expand Down
12 changes: 10 additions & 2 deletions src/emission.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,8 +103,16 @@ describe("Emission", () => {
}
}
await expect(getClients()).resolves.toEqual([
{ id: "ID", rooms: ["room1", "room2"] },
{ id: "other", rooms: ["room3"] },
{
id: "ID",
rooms: ["room1", "room2"],
getData: expect.any(Function),
},
{
id: "other",
rooms: ["room3"],
getData: expect.any(Function),
},
]);
},
);
Expand Down
5 changes: 5 additions & 0 deletions src/emission.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ export type Broadcaster<E extends EmissionMap> = <K extends keyof E>(
) => Promise<z.output<TuplesOrTrue<E[K]["ack"]>>>;

export type RoomService<E extends EmissionMap> = (rooms: string | string[]) => {
/**
* @desc Emits an event to everyone in the specified room(s)
* @throws z.ZodError on validation
* @throws Error on ack timeout
* */
broadcast: Broadcaster<E>;
join: () => void | Promise<void>;
leave: () => void | Promise<void>;
Expand Down
32 changes: 32 additions & 0 deletions src/remote-client.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { RemoteSocket } from "socket.io";
import { describe, expect, test } from "vitest";
import { getRemoteClients } from "./remote-client";

describe("RemoteClient", () => {
describe("getRemoteClients()", () => {
const socketsMock = [
{ id: "ONE", rooms: new Set(["room1"]), data: { name: "TEST" } },
{ id: "TWO", rooms: new Set(["room2"]) },
];

test("should map RemoteSockets to RemoteClients", () => {
const clients = getRemoteClients(socketsMock as RemoteSocket<any, any>[]);
expect(clients).toEqual([
{
id: "ONE",
rooms: ["room1"],
getData: expect.any(Function),
},
{
id: "TWO",
rooms: ["room2"],
getData: expect.any(Function),
},
]);

// getData:
expect(clients[0].getData()).toEqual({ name: "TEST" });
expect(clients[1].getData()).toEqual({});
});
});
});
11 changes: 7 additions & 4 deletions src/remote-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@ import { RemoteSocket } from "socket.io";
export interface RemoteClient {
id: string;
rooms: string[];
getData: <D extends object>() => Readonly<Partial<D>>;
}

export const getRemoteClients = (
sockets: RemoteSocket<{}, unknown>[],
): RemoteClient[] =>
sockets.map(({ id, rooms }) => ({ id, rooms: Array.from(rooms) }));
export const getRemoteClients = (sockets: RemoteSocket<{}, unknown>[]) =>
sockets.map<RemoteClient>(({ id, rooms, data }) => ({
id: id,
rooms: Array.from(rooms),
getData: <D extends object>() => (data as D) || {},
}));
Loading