diff --git a/examples/tests/src/emit.test.ts b/examples/tests/src/emit.test.ts new file mode 100644 index 0000000..b1a50b7 --- /dev/null +++ b/examples/tests/src/emit.test.ts @@ -0,0 +1,28 @@ +import { beforeAll, expect, mock, test } from "bun:test" +import { faker } from "@faker-js/faker" +import { Type } from "@sinclair/typebox" +import { Client } from "@wsx/client" +import { Wsx } from "@wsx/server" + +const onEmit = mock((message: string) => void message) + +async function initialize() { + const wsx = new Wsx() + .route("/some", ({ body }) => onEmit(body), { body: Type.String() }) + .listen(0) + const { port } = wsx.server! + return await Client(`ws://localhost:${port}`) +} + +let routes: Awaited>["routes"] +beforeAll(async () => { + const client = await initialize() + routes = client.routes +}) + +test("valid", async () => { + const message = faker.lorem.sentence() + routes.some.emit(message) + await Bun.sleep(10) + expect(onEmit).toBeCalledWith(message) +}) diff --git a/examples/tests/src/rpc.test.ts b/examples/tests/src/rpc.test.ts new file mode 100644 index 0000000..7e35760 --- /dev/null +++ b/examples/tests/src/rpc.test.ts @@ -0,0 +1,29 @@ +import { beforeAll, expect, mock, test } from "bun:test" +import { faker } from "@faker-js/faker" +import { Type } from "@sinclair/typebox" +import { Client } from "@wsx/client" +import { Wsx } from "@wsx/server" + +const onRpc = mock((message: string) => message) + +async function initialize() { + const wsx = new Wsx() + .route("/some", ({ body }) => onRpc(body), { body: Type.String() }) + .listen(0) + const { port } = wsx.server! + return await Client(`ws://localhost:${port}`) +} + +let routes: Awaited>["routes"] +beforeAll(async () => { + const client = await initialize() + routes = client.routes +}) + +test("valid", async () => { + const message = faker.lorem.sentence() + const response = await routes.some.call(message) + expect(response.status).toBe("success") + expect(response.body).toBe(message) + expect(onRpc).toBeCalledWith(message) +}) diff --git a/examples/tests/src/validation.test.ts b/examples/tests/src/validation.test.ts index 728ee66..2d7491e 100644 --- a/examples/tests/src/validation.test.ts +++ b/examples/tests/src/validation.test.ts @@ -1,26 +1,44 @@ -import { expect, test } from "bun:test" +import { beforeAll, expect, test } from "bun:test" import { faker } from "@faker-js/faker" -import { Type } from "@sinclair/typebox" +import { type Static, Type } from "@sinclair/typebox" import { Client } from "@wsx/client" import { Wsx } from "@wsx/server" -const wsx = new Wsx() - .route("/valid", ({ body }) => body, { - body: Type.Object({ message: Type.String() }), - response: Type.Object({ message: Type.String() }), - }) - .listen(0) +type ValidBody = Static +const ValidBody = Type.Object({ message: Type.String() }) + +async function initialize() { + const wsx = new Wsx() + .route("/validate", ({ body }) => body, { + body: ValidBody, + response: Type.Object({ message: Type.String() }), + }) + .listen(0) -const { port } = wsx.server! + const { port } = wsx.server! -const { routes } = await Client(`ws://localhost:${port}`) + return await Client(`ws://localhost:${port}`) +} + +let routes: Awaited>["routes"] +beforeAll(async () => { + const client = await initialize() + routes = client.routes +}) test("valid", async () => { const message = faker.lorem.sentence() - const response = await routes.valid.call({ + const response = await routes.validate.call({ message, }) - console.log(response) - expect(response.data).toBeObject() - expect(response.data!.message).toBe(message) + expect(response.status).toBe("success") + expect((response.body as ValidBody).message).toBe(message) +}) + +test("invalid", async () => { + const response = await routes.validate.call({ + message: 5 as any, + }) + expect(response.status).toBe("fail") + expect((response as { message: string }).message).toBe("Validation failed") }) diff --git a/libs/client/src/index.ts b/libs/client/src/index.ts index a843456..18c347c 100644 --- a/libs/client/src/index.ts +++ b/libs/client/src/index.ts @@ -2,14 +2,20 @@ import type { Wsx } from "@wsx/server" import type { ClientConfig, ClientType } from "./types" export type { ClientType, ClientConfig, ClientWs } from "./types" -import { Proto, type RPCHandler, isPromise, subprotocol } from "@wsx/shared" +import { + Proto, + type RPCHandler, + type RpcResponse, + isPromise, + subprotocol, +} from "@wsx/shared" type Method = (typeof methods)[number] const methods = ["call", "emit", "listen", "unlisten"] as const const locals = ["localhost", "127.0.0.1", "0.0.0.0"] -type Resolve = (response: unknown) => void +type Resolve = (response: RpcResponse) => void class Store { /** @@ -108,13 +114,23 @@ export const Client = < const action = JSON.parse(data) as Proto.GenericAction const [actionType] = action if (Proto.isRpcResponse(action)) { - const [, id, body] = action + const [, id] = action const resolve = store.resolvers.get(id) if (!resolve) { console.error("No resolver for call", id) return } - resolve(body) + + if (actionType === Proto.actionTypes.rpc.response.success) { + const [, , body] = action + resolve({ status: "success", body }) + } else if (actionType === Proto.actionTypes.rpc.response.fail) { + const [, , message, body] = action + resolve({ status: "fail", body, message }) + } else if (actionType === Proto.actionTypes.rpc.response.error) { + const [, , message, body, code] = action + resolve({ status: "error", body, message, code }) + } return } diff --git a/libs/server/src/index.ts b/libs/server/src/index.ts index dc13ed3..d45da09 100644 --- a/libs/server/src/index.ts +++ b/libs/server/src/index.ts @@ -80,7 +80,7 @@ export class WsxHandler implements WebSocketHandler { const route = this.wsx.router.get(path) if (!route) { - console.debug("Route not found", { path }) + // console.debug("Route not found", { path }) if (isRpcRequest) { ws[sendSymbol]([ Proto.actionTypes.rpc.response.fail, @@ -96,7 +96,7 @@ export class WsxHandler implements WebSocketHandler { if (bodySchema) { const validationResult = await validate(bodySchema, body) if (!validationResult.success) { - console.debug("Validation failed", validationResult.issues) + // console.debug("Validation failed", validationResult.issues) if (isRpcRequest) { ws[sendSymbol]([ Proto.actionTypes.rpc.response.fail, diff --git a/libs/shared/src/index.ts b/libs/shared/src/index.ts index b3b718a..6b443c0 100644 --- a/libs/shared/src/index.ts +++ b/libs/shared/src/index.ts @@ -29,12 +29,19 @@ export type RPCOptions = { export type RpcResponse = | { - data: Response - error?: undefined + status: "success" + body: Response } | { - data?: undefined - error: unknown + status: "fail" + message: string + body: unknown + } + | { + status: "error" + message: string + code?: number + body?: unknown } export function isObject(x: unknown): x is object {