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

Feat: Handling emission errors #254

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 17 additions & 18 deletions src/action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@ import { init, last } from "ramda";
import { z } from "zod";
import { ActionNoAckDef, ActionWithAckDef } from "./actions-factory";
import { EmissionMap } from "./emission";
import { OutputValidationError, InputValidationError } from "./errors";
import {
OutputValidationError,
InputValidationError,
parseWrapped,
} from "./errors";
import { ActionContext, ClientContext, Handler } from "./handler";
import { Namespaces, rootNS } from "./namespace";

Expand Down Expand Up @@ -77,37 +81,32 @@ export class Action<
/** @throws InputValidationError */
#parseInput(params: unknown[]) {
const payload = this.#outputSchema ? init(params) : params;
try {
return this.#inputSchema.parse(payload);
} catch (e) {
throw e instanceof z.ZodError ? new InputValidationError(e) : e;
}
return parseWrapped(this.#inputSchema, payload, InputValidationError);
}

/** @throws InputValidationError */
#parseAckCb(params: unknown[]) {
if (!this.#outputSchema) {
return undefined;
}
try {
return z
.function(this.#outputSchema, z.void())
.parse(last(params), { path: [Math.max(0, params.length - 1)] });
} catch (e) {
throw e instanceof z.ZodError ? new InputValidationError(e) : e;
}
return parseWrapped(
z.function(this.#outputSchema, z.void()),
last(params),
InputValidationError,
{ path: [Math.max(0, params.length - 1)] },
);
}

/** @throws OutputValidationError */
#parseOutput(output: z.input<NonNullable<OUT>> | void) {
if (!this.#outputSchema) {
return;
}
try {
return this.#outputSchema.parse(output) as z.output<NonNullable<OUT>>;
} catch (e) {
throw e instanceof z.ZodError ? new OutputValidationError(e) : e;
}
return parseWrapped(
this.#outputSchema,
output,
OutputValidationError,
) as z.output<NonNullable<OUT>>;
}

public override async execute({
Expand Down
21 changes: 15 additions & 6 deletions src/emission.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import assert from "node:assert/strict";
import type { RemoteSocket, Server, Socket } from "socket.io";
import { z } from "zod";
import {
InputValidationError,
OutputValidationError,
parseWrapped,
} from "./errors";
import {
RemoteClient,
SomeRemoteSocket,
Expand Down Expand Up @@ -67,22 +72,26 @@ export function makeEmitter({
}: {
subject: Socket | SomeRemoteSocket | Socket["broadcast"] | Server;
} & EmitterConfig<EmissionMap>) {
const getSchemas = (event: string) => {
const { schema, ack } = emission[event];
return { schema, ack: ack && ("id" in subject ? ack : ack.array()) };
};
/**
* @throws z.ZodError on validation
* @throws Error on ack timeout
* @throws OutputValidationError on validating emission
* @throws InputValidationError on validating acknowledgment
* @throws Error on ack timeout or unsupported event
* */
return async (event: string, ...args: unknown[]) => {
const isSocket = "id" in subject;
assert(event in emission, new Error(`Unsupported event ${event}`));
const { schema, ack } = emission[event];
const payload = schema.parse(args);
const { schema, ack } = getSchemas(event);
const payload = parseWrapped(schema, args, OutputValidationError);
if (!ack) {
return subject.emit(String(event), ...payload) || true;
}
const response = await subject
.timeout(timeout)
.emitWithAck(String(event), ...payload);
return (isSocket ? ack : ack.array()).parse(response);
return parseWrapped(ack, response, InputValidationError);
};
}

Expand Down
19 changes: 16 additions & 3 deletions src/errors.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ZodError } from "zod";
import { z } from "zod";
import { getMessageFromError } from "./common-helpers";

/** @desc An error related to the input and output schemas declaration */
Expand All @@ -10,7 +10,7 @@ export class IOSchemaError extends Error {
export class InputValidationError extends IOSchemaError {
public override name = "InputValidationError";

constructor(public readonly originalError: ZodError) {
constructor(public readonly originalError: z.ZodError) {
super(getMessageFromError(originalError));
}
}
Expand All @@ -19,7 +19,20 @@ export class InputValidationError extends IOSchemaError {
export class OutputValidationError extends IOSchemaError {
public override name = "OutputValidationError";

constructor(public readonly originalError: ZodError) {
constructor(public readonly originalError: z.ZodError) {
super(getMessageFromError(originalError));
}
}

export const parseWrapped = <T extends z.ZodTypeAny>(
schema: T,
data: unknown,
Class: { new (error: z.ZodError): IOSchemaError },
params?: Partial<z.ParseParams>,
): z.output<T> => {
try {
return schema.parse(data, params);
} catch (error) {
throw error instanceof z.ZodError ? new Class(error) : error;
}
};