Skip to content

Commit

Permalink
Typed Errors (draft)
Browse files Browse the repository at this point in the history
  • Loading branch information
penx committed Oct 24, 2023
1 parent 638fa0c commit 89b787b
Show file tree
Hide file tree
Showing 9 changed files with 94 additions and 13 deletions.
5 changes: 3 additions & 2 deletions src/batch/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import RetrieveJobsFilters from '../types/list-job-filters';
import { BatchFeatureDiscovery } from '../types/batch-feature-discovery';
import { ISO639_1_Language } from '../types/language-code';
import { ReadStream } from 'fs';
import { SpeechmaticsInternalError } from '../utils/errors';

export class BatchTranscription {
private config: ConnectionConfigFull;
Expand Down Expand Up @@ -114,7 +115,7 @@ export class BatchTranscription {
format = 'json-v2',
}: TranscribeConfig): Promise<RetrieveTranscriptResponse | string> {
if (this.config.apiKey === undefined)
throw new Error('Error: apiKey is undefined');
throw new SpeechmaticsInternalError('Error: apiKey is undefined');

const fileOrFetchConfig = 'fetch' in input ? input.fetch : input;

Expand Down Expand Up @@ -169,7 +170,7 @@ export class BatchTranscription {
summarization_config,
}: CreateJobConfig): Promise<CreateJobResponse> {
if (this.config.apiKey === undefined)
throw new Error('Error: apiKey is undefined');
throw new SpeechmaticsInternalError('Error: apiKey is undefined');

const config: JobConfig = {
type: 'transcription',
Expand Down
7 changes: 6 additions & 1 deletion src/management-platform/auth.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { SpeechmaticsInternalError } from '../utils/errors';
import { request } from '../utils/request';

export default async function getShortLivedToken(
Expand All @@ -23,7 +24,11 @@ export default async function getShortLivedToken(
undefined,
'application/json',
).catch((err) => {
throw new Error(`Error fetching short lived token: ${err}`);
throw new SpeechmaticsInternalError(
'Error fetching short lived token',
undefined,
err,
);
});
return jsonResponse.key_value;
}
5 changes: 4 additions & 1 deletion src/realtime/browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
} from '../utils/request';
import { ISocketWrapper } from '../types';
import { ModelError, RealtimeMessage } from '../types';
import { SpeechmaticsInternalError } from '../utils/errors';

/**
* Wraps the socket api to be more useful in async/await kind of scenarios
Expand All @@ -23,7 +24,9 @@ export class WebSocketWrapper implements ISocketWrapper {

constructor() {
if (typeof window === 'undefined')
throw new Error('window is undefined - are you running in a browser?');
throw new SpeechmaticsInternalError(
'window is undefined - are you running in a browser?',
);
}

async connect(
Expand Down
3 changes: 2 additions & 1 deletion src/realtime/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import * as webWrapper from '../realtime/browser';
import * as chromeWrapper from '../realtime/extension';

import { EventMap } from '../types/event-map';
import { SpeechmaticsInternalError } from '../utils/errors';

/**
* A class that represents a single realtime session. It's responsible for handling the connection and the messages.
Expand All @@ -40,7 +41,7 @@ export class RealtimeSession {
} else if (typeof process !== 'undefined') {
socketImplementation = new nodeWrapper.NodeWebSocketWrapper();
} else {
throw new Error('Unsupported environment');
throw new SpeechmaticsInternalError('Unsupported environment');
}

this.rtSocketHandler = new RealtimeSocketHandler(
Expand Down
3 changes: 2 additions & 1 deletion src/realtime/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
} from '../utils/request';
import { ISocketWrapper } from '../types';
import { ModelError, RealtimeMessage } from '../types';
import { SpeechmaticsInternalError } from '../utils/errors';

/**
* Wraps the socket api to be more useful in async/await kind of scenarios
Expand All @@ -23,7 +24,7 @@ export class WebSocketWrapper implements ISocketWrapper {

constructor() {
if (typeof chrome.runtime === 'undefined')
throw new Error(
throw new SpeechmaticsInternalError(
'chrome is undefined - are you running in a background script?',
);
}
Expand Down
16 changes: 15 additions & 1 deletion src/realtime/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
ISocketWrapper,
SessionConfig,
} from '../types';
import { SpeechmaticsInternalError } from '../utils/errors';

export const defaultLanguage = 'en';

Expand All @@ -29,6 +30,13 @@ const defaultAudioFormat = {
type: 'file',
} as const;

const expectUndefined = (u: undefined) => {
if (u) {
// If our TypeScript types are correct, we should never hit this
throw new SpeechmaticsInternalError('Unexpected type');
}
};

export class RealtimeSocketHandler {
private socketWrap: ISocketWrapper;

Expand Down Expand Up @@ -156,8 +164,14 @@ export class RealtimeSocketHandler {
this.sub?.onInfo?.(data as Info);
break;

case MessagesEnum.StartRecognition:
case MessagesEnum.AddAudio:
case MessagesEnum.EndOfStream:
case MessagesEnum.SetRecognitionConfig:
// TODO: is this correct?
throw new SpeechmaticsInternalError('Unexpected message');
default:
throw new Error('Unexpected message');
expectUndefined(data.message);
}
};

Expand Down
5 changes: 4 additions & 1 deletion src/realtime/node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
SM_SDK_PARAM_NAME,
getSmSDKVersion,
} from '../utils/request';
import { SpeechmaticsInternalError } from '../utils/errors';

/**
* Wraps the socket api to be more useful in async/await kind of scenarios
Expand All @@ -24,7 +25,9 @@ export class NodeWebSocketWrapper implements ISocketWrapper {

constructor() {
if (typeof process === 'undefined')
throw new Error('process is undefined - are you running in node?');
throw new SpeechmaticsInternalError(
'process is undefined - are you running in node?',
);
}

async connect(
Expand Down
54 changes: 54 additions & 0 deletions src/utils/errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import z from 'zod';

import { ErrorResponse, ErrorResponseErrorEnum } from '../types';

const ErrorResponseSchema: z.ZodType<ErrorResponse> = z.object({
code: z.number(),
detail: z.optional(z.string()),
error: z.nativeEnum(ErrorResponseErrorEnum),
});

export class SpeechmaticsResponseError extends Error {
response: ErrorResponse;

constructor(errorResponse: unknown) {
const parse = ErrorResponseSchema.safeParse(errorResponse);
if (parse.success) {
super(parse.data.error);
this.response = parse.data;
} else {
throw new SpeechmaticsInternalError('Unexpected response');
}
}
}

export const InternalErrorEnum = {
ProcessUndefined: 'process is undefined - are you running in node?',
WindowUndefined: 'window is undefined - are you running in a browser?',
ApiKeyUndefined: 'Error: apiKey is undefined',
FetchSLT: 'Error fetching short lived token',
UnsuportedEnvironment: 'Unsupported environment',
UnexpectedMessage: 'Unexpected message',
UnexpectedResponse: 'Unexpected response',
TypeError: 'Unexpected type',
} as const;

export type InternalErrorEnum =
typeof InternalErrorEnum[keyof typeof InternalErrorEnum];

export class SpeechmaticsInternalError extends Error {
error: InternalErrorEnum;
detail?: string;
cause?: unknown;

constructor(error: InternalErrorEnum, detail?: string, cause?: unknown) {
super();
this.error = error;
this.detail = detail;
this.cause = cause;
}
}

export type SpeechmaticsError =
| SpeechmaticsInternalError
| SpeechmaticsResponseError;
9 changes: 4 additions & 5 deletions src/utils/request.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { SpeechmaticsResponseError } from './errors';

export type HttpMethod = 'GET' | 'PUT' | 'POST' | 'DELETE';

export type QueryParams<K extends string = string> = Partial<
Expand Down Expand Up @@ -32,11 +34,8 @@ export async function request<T, K extends string = string>(
const response = await fetch(fullUrl, requestOptions);

if (!response.ok) {
throw new Error(
`SMjs error: ${response.statusText} ${
response.status
} ${await response.text()}`,
);
const responseJson = await response.json();
throw new SpeechmaticsResponseError(responseJson);
}

const isPlain = contentType === 'text/plain';
Expand Down

0 comments on commit 89b787b

Please sign in to comment.