feat: validation error type (#13)

* feat: add validation error type

* feat: move subscribe to top-level

* fix: wrong import
This commit is contained in:
Daniel Rochetti 2023-10-07 16:12:47 -07:00 committed by GitHub
parent 25a763602f
commit f240e622d3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 69 additions and 46 deletions

View File

@ -112,6 +112,50 @@ export async function run<Input, Output>(
return await responseHandler(response);
}
/**
* Subscribes to updates for a specific request in the queue.
*
* @param id - The ID or URL of the function web endpoint.
* @param options - Options to configure how the request is run and how updates are received.
* @returns A promise that resolves to the result of the request once it's completed.
*/
export async function subscribe<Input, Output>(
id: string,
options: RunOptions<Input> & QueueSubscribeOptions = {}
): Promise<Output> {
const { request_id: requestId } = await queue.submit(id, options);
if (options.onEnqueue) {
options.onEnqueue(requestId);
}
return new Promise<Output>((resolve, reject) => {
let timeoutId: ReturnType<typeof setTimeout>;
const pollInterval = options.pollInterval ?? 1000;
const poll = async () => {
try {
const requestStatus = await queue.status(id, requestId);
if (options.onQueueUpdate) {
options.onQueueUpdate(requestStatus);
}
if (requestStatus.status === 'COMPLETED') {
clearTimeout(timeoutId);
try {
const result = await queue.result<Output>(id, requestId);
resolve(result);
} catch (error) {
reject(error);
}
return;
}
timeoutId = setTimeout(poll, pollInterval);
} catch (error) {
clearTimeout(timeoutId);
reject(error);
}
};
poll().catch(reject);
});
}
/**
* Options for subscribing to the request queue.
*/
@ -168,11 +212,7 @@ interface Queue {
result<Output>(id: string, requestId: string): Promise<Output>;
/**
* Subscribes to updates for a specific request in the queue.
*
* @param id - The ID or URL of the function web endpoint.
* @param options - Options to configure how the request is run and how updates are received.
* @returns A promise that resolves to the result of the request once it's completed.
* @deprecated Use `fal.subscribe` instead.
*/
subscribe<Input, Output>(
id: string,
@ -204,40 +244,5 @@ export const queue: Queue = {
path: `/fal/queue/requests/${requestId}/response`,
});
},
async subscribe<Input, Output>(
id: string,
options: RunOptions<Input> & QueueSubscribeOptions = {}
): Promise<Output> {
const { request_id: requestId } = await queue.submit(id, options);
if (options.onEnqueue) {
options.onEnqueue(requestId);
}
return new Promise<Output>((resolve, reject) => {
let timeoutId: ReturnType<typeof setTimeout>;
const pollInterval = options.pollInterval ?? 1000;
const poll = async () => {
try {
const requestStatus = await queue.status(id, requestId);
if (options.onQueueUpdate) {
options.onQueueUpdate(requestStatus);
}
if (requestStatus.status === 'COMPLETED') {
clearTimeout(timeoutId);
try {
const result = await queue.result<Output>(id, requestId);
resolve(result);
} catch (error) {
reject(error);
}
return;
}
timeoutId = setTimeout(poll, pollInterval);
} catch (error) {
clearTimeout(timeoutId);
reject(error);
}
};
poll().catch(reject);
});
},
subscribe,
};

View File

@ -1,6 +1,7 @@
export { config, getConfig } from './config';
export { queue, run } from './function';
export { queue, run, subscribe } from './function';
export { withMiddleware } from './middleware';
export { ApiError, ValidationError } from './response';
export type { RequestMiddleware } from './middleware';
export type { ResponseHandler } from './response';
export type { QueueStatus } from './types';

View File

@ -1,14 +1,17 @@
import { ValidationErrorInfo } from './types';
export type ResponseHandler<Output> = (response: Response) => Promise<Output>;
type ApiErrorArgs = {
message: string;
status: number;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
body?: any;
};
export class ApiError extends Error {
export class ApiError<Body> extends Error {
public readonly status: number;
public readonly body?: any;
public readonly body: Body;
constructor({ message, status, body }: ApiErrorArgs) {
super(message);
this.name = 'ApiError';
@ -17,6 +20,13 @@ export class ApiError extends Error {
}
}
export class ValidationError extends ApiError<ValidationErrorInfo[]> {
constructor(args: ApiErrorArgs) {
super(args);
this.name = 'ValidationError';
}
}
export async function defaultResponseHandler<Output>(
response: Response
): Promise<Output> {
@ -25,13 +35,14 @@ export async function defaultResponseHandler<Output>(
if (!response.ok) {
if (contentType?.includes('application/json')) {
const body = await response.json();
throw new ApiError({
const ErrorType = status === 422 ? ValidationError : ApiError;
throw new ErrorType({
message: body.message || statusText,
status,
body,
});
}
throw new Error(`HTTP ${status}: ${statusText}`);
throw new ApiError({ message: `HTTP ${status}: ${statusText}`, status });
}
if (contentType?.includes('application/json')) {
return response.json() as Promise<Output>;

View File

@ -33,3 +33,9 @@ export type QueueStatus =
export function isQueueStatus(obj: any): obj is QueueStatus {
return obj && obj.status && obj.response_url;
}
export type ValidationErrorInfo = {
msg: string;
loc: Array<string | number>;
type: string;
};