From 8d27143f30ca326ee9e690ff4adac4a796dc6342 Mon Sep 17 00:00:00 2001 From: Alasdair McLeay Date: Tue, 24 Oct 2023 16:37:23 +0100 Subject: [PATCH 1/7] Typed Errors (draft) --- src/batch/client.ts | 5 +-- src/management-platform/auth.ts | 7 ++++- src/realtime/browser.ts | 5 ++- src/realtime/client.ts | 3 +- src/realtime/extension.ts | 3 +- src/realtime/handlers.ts | 16 +++++++++- src/realtime/node.ts | 5 ++- src/utils/errors.ts | 54 +++++++++++++++++++++++++++++++++ src/utils/request.ts | 9 +++--- 9 files changed, 94 insertions(+), 13 deletions(-) create mode 100644 src/utils/errors.ts diff --git a/src/batch/client.ts b/src/batch/client.ts index 59cb965..9e83806 100644 --- a/src/batch/client.ts +++ b/src/batch/client.ts @@ -13,6 +13,7 @@ import { QueryParams, request, SM_APP_PARAM_NAME } from '../utils/request'; import poll from '../utils/poll'; import RetrieveJobsFilters from '../types/list-job-filters'; import { BatchFeatureDiscovery } from '../types/batch-feature-discovery'; +import { SpeechmaticsInternalError } from '../utils/errors'; export class BatchTranscription { private config: ConnectionConfigFull; @@ -109,7 +110,7 @@ export class BatchTranscription { format?: TranscriptionFormat, ): Promise { if (this.config.apiKey === undefined) - throw new Error('Error: apiKey is undefined'); + throw new SpeechmaticsInternalError('Error: apiKey is undefined'); const submitResponse = await this.createTranscriptionJob(input, jobConfig); @@ -142,7 +143,7 @@ export class BatchTranscription { }, ): Promise { if (this.config.apiKey === undefined) - throw new Error('Error: apiKey is undefined'); + throw new SpeechmaticsInternalError('Error: apiKey is undefined'); const config = { ...jobConfig, diff --git a/src/management-platform/auth.ts b/src/management-platform/auth.ts index 8ec35e3..5ba3db8 100644 --- a/src/management-platform/auth.ts +++ b/src/management-platform/auth.ts @@ -1,3 +1,4 @@ +import { SpeechmaticsInternalError } from '../utils/errors'; import { request } from '../utils/request'; export default async function getShortLivedToken( @@ -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; } diff --git a/src/realtime/browser.ts b/src/realtime/browser.ts index 6d21daf..387fb0c 100644 --- a/src/realtime/browser.ts +++ b/src/realtime/browser.ts @@ -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 @@ -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( diff --git a/src/realtime/client.ts b/src/realtime/client.ts index aab25d4..facef99 100644 --- a/src/realtime/client.ts +++ b/src/realtime/client.ts @@ -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. @@ -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( diff --git a/src/realtime/extension.ts b/src/realtime/extension.ts index 3464b61..fb1560a 100644 --- a/src/realtime/extension.ts +++ b/src/realtime/extension.ts @@ -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 @@ -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?', ); } diff --git a/src/realtime/handlers.ts b/src/realtime/handlers.ts index 65be912..db2a770 100755 --- a/src/realtime/handlers.ts +++ b/src/realtime/handlers.ts @@ -18,6 +18,7 @@ import { ISocketWrapper, SessionConfig, } from '../types'; +import { SpeechmaticsInternalError } from '../utils/errors'; export const defaultLanguage = 'en'; @@ -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; @@ -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); } }; diff --git a/src/realtime/node.ts b/src/realtime/node.ts index 977222c..58be371 100644 --- a/src/realtime/node.ts +++ b/src/realtime/node.ts @@ -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 @@ -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( diff --git a/src/utils/errors.ts b/src/utils/errors.ts new file mode 100644 index 0000000..4ee8e67 --- /dev/null +++ b/src/utils/errors.ts @@ -0,0 +1,54 @@ +import z from 'zod'; + +import { ErrorResponse, ErrorResponseErrorEnum } from '../types'; + +const ErrorResponseSchema: z.ZodType = 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; diff --git a/src/utils/request.ts b/src/utils/request.ts index 773bac2..ccbcea9 100644 --- a/src/utils/request.ts +++ b/src/utils/request.ts @@ -1,3 +1,5 @@ +import { SpeechmaticsResponseError } from './errors'; + export type HttpMethod = 'GET' | 'PUT' | 'POST' | 'DELETE'; export type QueryParams = Partial< @@ -32,11 +34,8 @@ export async function request( 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'; From fd4802307a0f51dc161da5bf107611383d369413 Mon Sep 17 00:00:00 2001 From: Alasdair McLeay Date: Wed, 25 Oct 2023 12:54:35 +0100 Subject: [PATCH 2/7] A class for each error type --- src/batch/client.ts | 10 +++-- src/management-platform/auth.ts | 8 +--- src/realtime/browser.ts | 4 +- src/realtime/client.ts | 4 +- src/realtime/extension.ts | 4 +- src/realtime/handlers.ts | 19 +++++----- src/realtime/node.ts | 4 +- src/utils/errors.ts | 65 ++++++++++++++++++++++++++------- 8 files changed, 77 insertions(+), 41 deletions(-) diff --git a/src/batch/client.ts b/src/batch/client.ts index 9e83806..073f757 100644 --- a/src/batch/client.ts +++ b/src/batch/client.ts @@ -13,7 +13,7 @@ import { QueryParams, request, SM_APP_PARAM_NAME } from '../utils/request'; import poll from '../utils/poll'; import RetrieveJobsFilters from '../types/list-job-filters'; import { BatchFeatureDiscovery } from '../types/batch-feature-discovery'; -import { SpeechmaticsInternalError } from '../utils/errors'; +import { SpeechmaticsConfigurationError } from '../utils/errors'; export class BatchTranscription { private config: ConnectionConfigFull; @@ -110,7 +110,9 @@ export class BatchTranscription { format?: TranscriptionFormat, ): Promise { if (this.config.apiKey === undefined) - throw new SpeechmaticsInternalError('Error: apiKey is undefined'); + throw new SpeechmaticsConfigurationError( + 'Missing apiKey in configuration', + ); const submitResponse = await this.createTranscriptionJob(input, jobConfig); @@ -143,7 +145,9 @@ export class BatchTranscription { }, ): Promise { if (this.config.apiKey === undefined) - throw new SpeechmaticsInternalError('Error: apiKey is undefined'); + throw new SpeechmaticsConfigurationError( + 'Missing apiKey in configuration', + ); const config = { ...jobConfig, diff --git a/src/management-platform/auth.ts b/src/management-platform/auth.ts index 5ba3db8..13c0bb1 100644 --- a/src/management-platform/auth.ts +++ b/src/management-platform/auth.ts @@ -1,4 +1,4 @@ -import { SpeechmaticsInternalError } from '../utils/errors'; +import { SpeechmaticsNetworkError } from '../utils/errors'; import { request } from '../utils/request'; export default async function getShortLivedToken( @@ -24,11 +24,7 @@ export default async function getShortLivedToken( undefined, 'application/json', ).catch((err) => { - throw new SpeechmaticsInternalError( - 'Error fetching short lived token', - undefined, - err, - ); + throw new SpeechmaticsNetworkError('Error fetching short lived token', err); }); return jsonResponse.key_value; } diff --git a/src/realtime/browser.ts b/src/realtime/browser.ts index 387fb0c..7271f24 100644 --- a/src/realtime/browser.ts +++ b/src/realtime/browser.ts @@ -6,7 +6,7 @@ import { } from '../utils/request'; import { ISocketWrapper } from '../types'; import { ModelError, RealtimeMessage } from '../types'; -import { SpeechmaticsInternalError } from '../utils/errors'; +import { SpeechmaticsUnsuportedEnvironment } from '../utils/errors'; /** * Wraps the socket api to be more useful in async/await kind of scenarios @@ -24,7 +24,7 @@ export class WebSocketWrapper implements ISocketWrapper { constructor() { if (typeof window === 'undefined') - throw new SpeechmaticsInternalError( + throw new SpeechmaticsUnsuportedEnvironment( 'window is undefined - are you running in a browser?', ); } diff --git a/src/realtime/client.ts b/src/realtime/client.ts index facef99..6015244 100644 --- a/src/realtime/client.ts +++ b/src/realtime/client.ts @@ -17,7 +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'; +import { SpeechmaticsUnsuportedEnvironment } from '../utils/errors'; /** * A class that represents a single realtime session. It's responsible for handling the connection and the messages. @@ -41,7 +41,7 @@ export class RealtimeSession { } else if (typeof process !== 'undefined') { socketImplementation = new nodeWrapper.NodeWebSocketWrapper(); } else { - throw new SpeechmaticsInternalError('Unsupported environment'); + throw new SpeechmaticsUnsuportedEnvironment(); } this.rtSocketHandler = new RealtimeSocketHandler( diff --git a/src/realtime/extension.ts b/src/realtime/extension.ts index fb1560a..7b2885f 100644 --- a/src/realtime/extension.ts +++ b/src/realtime/extension.ts @@ -6,7 +6,7 @@ import { } from '../utils/request'; import { ISocketWrapper } from '../types'; import { ModelError, RealtimeMessage } from '../types'; -import { SpeechmaticsInternalError } from '../utils/errors'; +import { SpeechmaticsUnsuportedEnvironment } from '../utils/errors'; /** * Wraps the socket api to be more useful in async/await kind of scenarios @@ -24,7 +24,7 @@ export class WebSocketWrapper implements ISocketWrapper { constructor() { if (typeof chrome.runtime === 'undefined') - throw new SpeechmaticsInternalError( + throw new SpeechmaticsUnsuportedEnvironment( 'chrome is undefined - are you running in a background script?', ); } diff --git a/src/realtime/handlers.ts b/src/realtime/handlers.ts index db2a770..5ce260e 100755 --- a/src/realtime/handlers.ts +++ b/src/realtime/handlers.ts @@ -18,7 +18,7 @@ import { ISocketWrapper, SessionConfig, } from '../types'; -import { SpeechmaticsInternalError } from '../utils/errors'; +import { SpeechmaticsUnexpectedResponse } from '../utils/errors'; export const defaultLanguage = 'en'; @@ -30,13 +30,6 @@ 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; @@ -168,10 +161,16 @@ export class RealtimeSocketHandler { case MessagesEnum.AddAudio: case MessagesEnum.EndOfStream: case MessagesEnum.SetRecognitionConfig: + case undefined: // TODO: is this correct? - throw new SpeechmaticsInternalError('Unexpected message'); + throw new SpeechmaticsUnexpectedResponse( + `Unexpected RealtimeMessage during onSocketMessage: ${data.message}`, + ); default: - expectUndefined(data.message); + data.message satisfies never; + throw new SpeechmaticsUnexpectedResponse( + `Unexpected RealtimeMessage during onSocketMessage: ${data.message}`, + ); } }; diff --git a/src/realtime/node.ts b/src/realtime/node.ts index 58be371..834d536 100644 --- a/src/realtime/node.ts +++ b/src/realtime/node.ts @@ -8,7 +8,7 @@ import { SM_SDK_PARAM_NAME, getSmSDKVersion, } from '../utils/request'; -import { SpeechmaticsInternalError } from '../utils/errors'; +import { SpeechmaticsUnsuportedEnvironment } from '../utils/errors'; /** * Wraps the socket api to be more useful in async/await kind of scenarios @@ -25,7 +25,7 @@ export class NodeWebSocketWrapper implements ISocketWrapper { constructor() { if (typeof process === 'undefined') - throw new SpeechmaticsInternalError( + throw new SpeechmaticsUnsuportedEnvironment( 'process is undefined - are you running in node?', ); } diff --git a/src/utils/errors.ts b/src/utils/errors.ts index 4ee8e67..1afa03b 100644 --- a/src/utils/errors.ts +++ b/src/utils/errors.ts @@ -8,47 +8,84 @@ const ErrorResponseSchema: z.ZodType = z.object({ error: z.nativeEnum(ErrorResponseErrorEnum), }); -export class SpeechmaticsResponseError extends Error { +type SpeechamticsErrorEnum = InternalErrorEnum | ErrorResponseErrorEnum; +interface SpeechmaticsErrorInterface extends Error { + error: SpeechamticsErrorEnum; +} + +export class SpeechmaticsResponseError + extends Error + implements SpeechmaticsErrorInterface +{ response: ErrorResponse; + error: ErrorResponseErrorEnum; - constructor(errorResponse: unknown) { + constructor(errorResponse: ErrorResponse | unknown) { const parse = ErrorResponseSchema.safeParse(errorResponse); if (parse.success) { super(parse.data.error); this.response = parse.data; + this.error = parse.data.error; } else { - throw new SpeechmaticsInternalError('Unexpected response'); + throw new SpeechmaticsUnexpectedResponse(undefined, errorResponse); } } } 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', + ConfigurationError: 'Configuration error', + NetworkError: 'Network error', UnsuportedEnvironment: 'Unsupported environment', UnexpectedMessage: 'Unexpected message', UnexpectedResponse: 'Unexpected response', - TypeError: 'Unexpected type', + TypeError: 'Type error', // TODO: this is used when code that should be unreachable is executed, which signifies there may be an issue with our types. Is there a better name? } as const; export type InternalErrorEnum = typeof InternalErrorEnum[keyof typeof InternalErrorEnum]; -export class SpeechmaticsInternalError extends Error { +class SpeechmaticsInternalError extends Error { error: InternalErrorEnum; - detail?: string; - cause?: unknown; + cause?: unknown; // e.g. a caught error or response - constructor(error: InternalErrorEnum, detail?: string, cause?: unknown) { - super(); + constructor(error: InternalErrorEnum, message?: string, cause?: unknown) { + super(message ?? error); this.error = error; - this.detail = detail; this.cause = cause; } } +export class SpeechmaticsConfigurationError extends SpeechmaticsInternalError { + constructor(message?: string) { + super(InternalErrorEnum.ConfigurationError, message); + } +} +export class SpeechmaticsNetworkError extends SpeechmaticsInternalError { + constructor(message?: string, cause?: unknown) { + super(InternalErrorEnum.NetworkError, message, cause); + } +} +export class SpeechmaticsUnsuportedEnvironment extends SpeechmaticsInternalError { + constructor(message?: string) { + super(InternalErrorEnum.UnsuportedEnvironment, message); + } +} +export class SpeechmaticsUnexpectedMessage extends SpeechmaticsInternalError { + constructor(message?: string) { + super(InternalErrorEnum.UnexpectedMessage, message); + } +} +export class SpeechmaticsUnexpectedResponse extends SpeechmaticsInternalError { + constructor(message?: string, response?: unknown) { + super(InternalErrorEnum.UnexpectedResponse, message, response); + } +} +export class SpeechmaticsTypeError extends SpeechmaticsInternalError { + constructor(message?: string) { + super(InternalErrorEnum.TypeError, message); + } +} + export type SpeechmaticsError = | SpeechmaticsInternalError | SpeechmaticsResponseError; From 937c28bc069d34a15450d5b4b16ca052493c75f9 Mon Sep 17 00:00:00 2001 From: Matt Nemitz Date: Tue, 6 Feb 2024 17:12:28 +0000 Subject: [PATCH 3/7] Export error types from root, tests pass --- src/batch/client.test.ts | 20 +++++++++++++++----- src/index.ts | 1 + src/utils/errors.ts | 6 +++--- src/utils/request.ts | 7 +++++-- tests/index.test.ts | 7 +++++-- 5 files changed, 29 insertions(+), 12 deletions(-) diff --git a/src/batch/client.test.ts b/src/batch/client.test.ts index 2b776a4..7db380c 100644 --- a/src/batch/client.test.ts +++ b/src/batch/client.test.ts @@ -36,11 +36,12 @@ describe('BatchTranscription', () => { }); it('refreshes the API key on error and retries', async () => { - const keys = (function* () { - yield 'firstKey'; - yield 'secondKey'; - })(); - const apiKey = jest.fn(async () => keys.next().value as string); + const keys: IterableIterator = ['firstKey', 'secondKey'][ + Symbol.iterator + ](); + const apiKey: () => Promise = jest.fn( + async () => keys.next().value, + ); const batch = new BatchTranscription({ apiKey }); @@ -56,4 +57,13 @@ describe('BatchTranscription', () => { expect(apiKey).toBeCalledTimes(2); expect(result.jobs).toBeInstanceOf(Array); }); + + // it('returns a descriptive error when the given API key is invalid', async () => { + // mockedRequest.mockImplementation(async () => { + // throw new Error('401 Unauthorized (mock)'); + // }); + + // const batch = new BatchTranscription({ apiKey: 'some-invalid-key' }); + // expect(batch.listJobs()).rejects; + // }); }); diff --git a/src/index.ts b/src/index.ts index dd725a4..5f659ed 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,6 +2,7 @@ export * from './client'; export * from './types'; export * from './realtime'; export * from './batch'; +export * from './utils/errors'; declare global { // This gets injected by ESBuild at compile time diff --git a/src/utils/errors.ts b/src/utils/errors.ts index 1afa03b..c13c7db 100644 --- a/src/utils/errors.ts +++ b/src/utils/errors.ts @@ -38,7 +38,7 @@ export const InternalErrorEnum = { UnsuportedEnvironment: 'Unsupported environment', UnexpectedMessage: 'Unexpected message', UnexpectedResponse: 'Unexpected response', - TypeError: 'Type error', // TODO: this is used when code that should be unreachable is executed, which signifies there may be an issue with our types. Is there a better name? + InvalidTypeError: 'Invalid type error', } as const; export type InternalErrorEnum = @@ -80,9 +80,9 @@ export class SpeechmaticsUnexpectedResponse extends SpeechmaticsInternalError { super(InternalErrorEnum.UnexpectedResponse, message, response); } } -export class SpeechmaticsTypeError extends SpeechmaticsInternalError { +export class SpeechmaticsInvalidTypeError extends SpeechmaticsInternalError { constructor(message?: string) { - super(InternalErrorEnum.TypeError, message); + super(InternalErrorEnum.InvalidTypeError, message); } } diff --git a/src/utils/request.ts b/src/utils/request.ts index ccbcea9..01a2c7e 100644 --- a/src/utils/request.ts +++ b/src/utils/request.ts @@ -1,4 +1,7 @@ -import { SpeechmaticsResponseError } from './errors'; +import { + SpeechmaticsInvalidTypeError, + SpeechmaticsResponseError, +} from './errors'; export type HttpMethod = 'GET' | 'PUT' | 'POST' | 'DELETE'; @@ -48,7 +51,7 @@ export async function request( result = (await response.json()) as T; } } catch (error) { - throw new Error(`SMjs error, can't parse json: ${error}`); + throw new SpeechmaticsInvalidTypeError(`Cannot parse error:\n\n${error}`); } return result; diff --git a/tests/index.test.ts b/tests/index.test.ts index 0e5f0c3..951fabe 100644 --- a/tests/index.test.ts +++ b/tests/index.test.ts @@ -3,7 +3,7 @@ import { AddTranscript, RetrieveTranscriptResponse, } from '../dist'; -import { Speechmatics } from '../dist'; +import { Speechmatics, SpeechmaticsResponseError } from '../dist'; import dotenv from 'dotenv'; import fs from 'fs'; import path from 'path'; @@ -55,7 +55,10 @@ describe('Testing batch capabilities', () => { jobResult = await speechmatics.batch.getJobResult(id, 'text'); return true; } catch (err) { - if (err instanceof Error && err.toString().includes('404')) { + if ( + err instanceof SpeechmaticsResponseError && + err.response.code === 404 + ) { return false; } else { throw err; From 580841bf35a35cd086420d4f208ea0a09a446df2 Mon Sep 17 00:00:00 2001 From: Matt Nemitz Date: Thu, 8 Feb 2024 14:34:39 +0000 Subject: [PATCH 4/7] Rework mocking in batch tests --- src/batch/client.test.ts | 53 ++++++++++++++++++++++++++++------------ src/realtime/handlers.ts | 3 ++- 2 files changed, 39 insertions(+), 17 deletions(-) diff --git a/src/batch/client.test.ts b/src/batch/client.test.ts index 7db380c..8796d46 100644 --- a/src/batch/client.test.ts +++ b/src/batch/client.test.ts @@ -1,12 +1,20 @@ import { BatchTranscription } from './'; -import { request } from '../utils/request'; +import { SpeechmaticsResponseError } from '../utils/errors'; -jest.mock('../utils/request'); -const mockedRequest = jest.mocked(request); +const originalFetch = global.fetch; +const mockedFetch: jest.Mock< + ReturnType, + Parameters +> = jest.fn(); +global.fetch = mockedFetch; describe('BatchTranscription', () => { afterEach(() => { - jest.clearAllMocks(); + mockedFetch.mockReset(); + }); + + afterAll(() => { + global.fetch = originalFetch; }); it('can be initialized with just an API key string', async () => { @@ -28,7 +36,9 @@ describe('BatchTranscription', () => { const apiKey = jest.fn(async () => 'asyncApiKey'); const batch = new BatchTranscription({ apiKey }); - mockedRequest.mockImplementation(async () => ({ jobs: [] })); + mockedFetch.mockImplementation( + async () => new Response(JSON.stringify({ jobs: [] }), { status: 200 }), + ); await batch.listJobs(); await batch.listJobs(); @@ -45,25 +55,36 @@ describe('BatchTranscription', () => { const batch = new BatchTranscription({ apiKey }); - mockedRequest.mockImplementation(async (apiKey: string) => { + mockedFetch.mockImplementation(async (_url, requestOpts) => { + const apiKey = JSON.parse( + JSON.stringify(requestOpts?.headers) ?? '{}', + ).Authorization?.split('Bearer ')[1]; + if (apiKey === 'firstKey') { - throw new Error('401 Unauthorized (mock)'); + return new Response('401 unauthorized (mock)', { status: 401 }); } else { - return { jobs: [] }; + return new Response(JSON.stringify({ jobs: [] }), { status: 200 }); } }); const result = await batch.listJobs(); expect(apiKey).toBeCalledTimes(2); - expect(result.jobs).toBeInstanceOf(Array); + expect(Array.isArray(result.jobs)).toBe(true); }); - // it('returns a descriptive error when the given API key is invalid', async () => { - // mockedRequest.mockImplementation(async () => { - // throw new Error('401 Unauthorized (mock)'); - // }); + it('returns a descriptive error when the given API key is invalid', async () => { + mockedFetch.mockImplementation(async () => { + return new Response( + '{"code": 401, "error": "Permission Denied", "mock": true}', + { status: 401 }, + ); + }); - // const batch = new BatchTranscription({ apiKey: 'some-invalid-key' }); - // expect(batch.listJobs()).rejects; - // }); + const batch = new BatchTranscription({ apiKey: 'some-invalid-key' }); + const listJobs = batch.listJobs(); + await expect(listJobs).rejects.toBeInstanceOf(SpeechmaticsResponseError); + await expect(listJobs).rejects.toMatchInlineSnapshot( + '[Error: Permission Denied]', + ); + }); }); diff --git a/src/realtime/handlers.ts b/src/realtime/handlers.ts index 5ce260e..4c2035b 100755 --- a/src/realtime/handlers.ts +++ b/src/realtime/handlers.ts @@ -157,12 +157,13 @@ export class RealtimeSocketHandler { this.sub?.onInfo?.(data as Info); break; + // We don't expect these messages to be sent (only received) case MessagesEnum.StartRecognition: case MessagesEnum.AddAudio: case MessagesEnum.EndOfStream: case MessagesEnum.SetRecognitionConfig: + // We also don't expect undefined case undefined: - // TODO: is this correct? throw new SpeechmaticsUnexpectedResponse( `Unexpected RealtimeMessage during onSocketMessage: ${data.message}`, ); From d6aa1f868a9071a98c77292b57036120a16a71f8 Mon Sep 17 00:00:00 2001 From: Matt Nemitz Date: Thu, 8 Feb 2024 14:52:19 +0000 Subject: [PATCH 5/7] Add error names --- src/batch/client.test.ts | 2 +- src/realtime/browser.ts | 4 ++-- src/realtime/client.ts | 4 ++-- src/realtime/extension.ts | 4 ++-- src/realtime/node.ts | 4 ++-- src/utils/errors.ts | 14 +++++++++++--- 6 files changed, 20 insertions(+), 12 deletions(-) diff --git a/src/batch/client.test.ts b/src/batch/client.test.ts index 8796d46..480f34e 100644 --- a/src/batch/client.test.ts +++ b/src/batch/client.test.ts @@ -84,7 +84,7 @@ describe('BatchTranscription', () => { const listJobs = batch.listJobs(); await expect(listJobs).rejects.toBeInstanceOf(SpeechmaticsResponseError); await expect(listJobs).rejects.toMatchInlineSnapshot( - '[Error: Permission Denied]', + '[SpeechmaticsResponseError: Permission Denied]', ); }); }); diff --git a/src/realtime/browser.ts b/src/realtime/browser.ts index 7271f24..66678f5 100644 --- a/src/realtime/browser.ts +++ b/src/realtime/browser.ts @@ -6,7 +6,7 @@ import { } from '../utils/request'; import { ISocketWrapper } from '../types'; import { ModelError, RealtimeMessage } from '../types'; -import { SpeechmaticsUnsuportedEnvironment } from '../utils/errors'; +import { SpeechmaticsUnsupportedEnvironment } from '../utils/errors'; /** * Wraps the socket api to be more useful in async/await kind of scenarios @@ -24,7 +24,7 @@ export class WebSocketWrapper implements ISocketWrapper { constructor() { if (typeof window === 'undefined') - throw new SpeechmaticsUnsuportedEnvironment( + throw new SpeechmaticsUnsupportedEnvironment( 'window is undefined - are you running in a browser?', ); } diff --git a/src/realtime/client.ts b/src/realtime/client.ts index 6015244..6e20dfc 100644 --- a/src/realtime/client.ts +++ b/src/realtime/client.ts @@ -17,7 +17,7 @@ import * as webWrapper from '../realtime/browser'; import * as chromeWrapper from '../realtime/extension'; import { EventMap } from '../types/event-map'; -import { SpeechmaticsUnsuportedEnvironment } from '../utils/errors'; +import { SpeechmaticsUnsupportedEnvironment } from '../utils/errors'; /** * A class that represents a single realtime session. It's responsible for handling the connection and the messages. @@ -41,7 +41,7 @@ export class RealtimeSession { } else if (typeof process !== 'undefined') { socketImplementation = new nodeWrapper.NodeWebSocketWrapper(); } else { - throw new SpeechmaticsUnsuportedEnvironment(); + throw new SpeechmaticsUnsupportedEnvironment(); } this.rtSocketHandler = new RealtimeSocketHandler( diff --git a/src/realtime/extension.ts b/src/realtime/extension.ts index 7b2885f..eb1734b 100644 --- a/src/realtime/extension.ts +++ b/src/realtime/extension.ts @@ -6,7 +6,7 @@ import { } from '../utils/request'; import { ISocketWrapper } from '../types'; import { ModelError, RealtimeMessage } from '../types'; -import { SpeechmaticsUnsuportedEnvironment } from '../utils/errors'; +import { SpeechmaticsUnsupportedEnvironment } from '../utils/errors'; /** * Wraps the socket api to be more useful in async/await kind of scenarios @@ -24,7 +24,7 @@ export class WebSocketWrapper implements ISocketWrapper { constructor() { if (typeof chrome.runtime === 'undefined') - throw new SpeechmaticsUnsuportedEnvironment( + throw new SpeechmaticsUnsupportedEnvironment( 'chrome is undefined - are you running in a background script?', ); } diff --git a/src/realtime/node.ts b/src/realtime/node.ts index 834d536..aeba8ba 100644 --- a/src/realtime/node.ts +++ b/src/realtime/node.ts @@ -8,7 +8,7 @@ import { SM_SDK_PARAM_NAME, getSmSDKVersion, } from '../utils/request'; -import { SpeechmaticsUnsuportedEnvironment } from '../utils/errors'; +import { SpeechmaticsUnsupportedEnvironment } from '../utils/errors'; /** * Wraps the socket api to be more useful in async/await kind of scenarios @@ -25,7 +25,7 @@ export class NodeWebSocketWrapper implements ISocketWrapper { constructor() { if (typeof process === 'undefined') - throw new SpeechmaticsUnsuportedEnvironment( + throw new SpeechmaticsUnsupportedEnvironment( 'process is undefined - are you running in node?', ); } diff --git a/src/utils/errors.ts b/src/utils/errors.ts index c13c7db..1364faf 100644 --- a/src/utils/errors.ts +++ b/src/utils/errors.ts @@ -26,6 +26,7 @@ export class SpeechmaticsResponseError super(parse.data.error); this.response = parse.data; this.error = parse.data.error; + this.name = 'SpeechmaticsResponseError'; } else { throw new SpeechmaticsUnexpectedResponse(undefined, errorResponse); } @@ -35,7 +36,7 @@ export class SpeechmaticsResponseError export const InternalErrorEnum = { ConfigurationError: 'Configuration error', NetworkError: 'Network error', - UnsuportedEnvironment: 'Unsupported environment', + UnsupportedEnvironment: 'Unsupported environment', UnexpectedMessage: 'Unexpected message', UnexpectedResponse: 'Unexpected response', InvalidTypeError: 'Invalid type error', @@ -50,6 +51,7 @@ class SpeechmaticsInternalError extends Error { constructor(error: InternalErrorEnum, message?: string, cause?: unknown) { super(message ?? error); + this.name = 'SpeechmaticsInternalError'; this.error = error; this.cause = cause; } @@ -58,31 +60,37 @@ class SpeechmaticsInternalError extends Error { export class SpeechmaticsConfigurationError extends SpeechmaticsInternalError { constructor(message?: string) { super(InternalErrorEnum.ConfigurationError, message); + this.name = 'SpeechmaticsConfigurationError'; } } export class SpeechmaticsNetworkError extends SpeechmaticsInternalError { constructor(message?: string, cause?: unknown) { super(InternalErrorEnum.NetworkError, message, cause); + this.name = 'SpeechmaticsNetworkError'; } } -export class SpeechmaticsUnsuportedEnvironment extends SpeechmaticsInternalError { +export class SpeechmaticsUnsupportedEnvironment extends SpeechmaticsInternalError { constructor(message?: string) { - super(InternalErrorEnum.UnsuportedEnvironment, message); + super(InternalErrorEnum.UnsupportedEnvironment, message); + this.name = 'SpeechmaticsUnsupp'; } } export class SpeechmaticsUnexpectedMessage extends SpeechmaticsInternalError { constructor(message?: string) { super(InternalErrorEnum.UnexpectedMessage, message); + this.name = 'SpeechmaticsUnexpectedMessage'; } } export class SpeechmaticsUnexpectedResponse extends SpeechmaticsInternalError { constructor(message?: string, response?: unknown) { super(InternalErrorEnum.UnexpectedResponse, message, response); + this.name = 'SpeechmaticsUnexpectedResponse'; } } export class SpeechmaticsInvalidTypeError extends SpeechmaticsInternalError { constructor(message?: string) { super(InternalErrorEnum.InvalidTypeError, message); + this.name = 'SpeechmaticsInvalidTypeError'; } } From f31332644f447f5e0ee3efd31bf36af5e97e6710 Mon Sep 17 00:00:00 2001 From: Matt Nemitz Date: Fri, 9 Feb 2024 11:27:17 +0000 Subject: [PATCH 6/7] Add more error tests, remove key typing from QueryParams --- src/batch/client.test.ts | 72 ++++++++++++++++++++++++++++++++-------- src/batch/client.ts | 51 ++++++++++++++++++++-------- src/utils/errors.ts | 6 ++-- src/utils/file-utils.ts | 20 ----------- src/utils/request.ts | 43 ++++++++++++++++-------- 5 files changed, 129 insertions(+), 63 deletions(-) delete mode 100644 src/utils/file-utils.ts diff --git a/src/batch/client.test.ts b/src/batch/client.test.ts index 480f34e..b222489 100644 --- a/src/batch/client.test.ts +++ b/src/batch/client.test.ts @@ -1,5 +1,9 @@ import { BatchTranscription } from './'; -import { SpeechmaticsResponseError } from '../utils/errors'; +import { + SpeechmaticsConfigurationError, + SpeechmaticsNetworkError, + SpeechmaticsResponseError, +} from '../utils/errors'; const originalFetch = global.fetch; const mockedFetch: jest.Mock< @@ -61,7 +65,10 @@ describe('BatchTranscription', () => { ).Authorization?.split('Bearer ')[1]; if (apiKey === 'firstKey') { - return new Response('401 unauthorized (mock)', { status: 401 }); + return new Response( + '{"code": 401, "error": "Permission Denied", "mock": true}', + { status: 401 }, + ); } else { return new Response(JSON.stringify({ jobs: [] }), { status: 200 }); } @@ -72,19 +79,58 @@ describe('BatchTranscription', () => { expect(Array.isArray(result.jobs)).toBe(true); }); - it('returns a descriptive error when the given API key is invalid', async () => { - mockedFetch.mockImplementation(async () => { - return new Response( - '{"code": 401, "error": "Permission Denied", "mock": true}', - { status: 401 }, + describe('Errors', () => { + it('throws a response error when the given API key is invalid', async () => { + mockedFetch.mockImplementation(async () => { + return new Response( + '{"code": 401, "error": "Permission Denied", "mock": true}', + { status: 401 }, + ); + }); + + const batch = new BatchTranscription({ apiKey: 'some-invalid-key' }); + const listJobs = batch.listJobs(); + await expect(listJobs).rejects.toBeInstanceOf(SpeechmaticsResponseError); + await expect(listJobs).rejects.toMatchInlineSnapshot( + '[SpeechmaticsResponseError: Permission Denied]', ); }); - const batch = new BatchTranscription({ apiKey: 'some-invalid-key' }); - const listJobs = batch.listJobs(); - await expect(listJobs).rejects.toBeInstanceOf(SpeechmaticsResponseError); - await expect(listJobs).rejects.toMatchInlineSnapshot( - '[SpeechmaticsResponseError: Permission Denied]', - ); + it('throws a configuration error if the apiKey is not present', async () => { + // @ts-expect-error + const batch = new BatchTranscription({}); + const listJobs = batch.listJobs(); + await expect(listJobs).rejects.toBeInstanceOf( + SpeechmaticsConfigurationError, + ); + await expect(listJobs).rejects.toMatchInlineSnapshot( + '[SpeechmaticsConfigurationError: Missing apiKey in configuration]', + ); + }); + + it('throws a network error if fetch fails', async () => { + mockedFetch.mockImplementationOnce(() => { + throw new TypeError('failed to fetch'); + }); + + const batch = new BatchTranscription({ apiKey: 'my-key' }); + const listJobs = batch.listJobs(); + await expect(listJobs).rejects.toBeInstanceOf(SpeechmaticsNetworkError); + await expect(listJobs).rejects.toMatchInlineSnapshot( + '[SpeechmaticsNetworkError: Error fetching from /v2/jobs]', + ); + }); + + it('throws an unexpected response error if an invalid response comes back', async () => { + mockedFetch.mockImplementationOnce(async () => { + return new Response('
502 bad gateway
'); + }); + + const batch = new BatchTranscription({ apiKey: 'my-key' }); + const listJobs = batch.listJobs(); + await expect(listJobs).rejects.toMatchInlineSnapshot( + '[SpeechmaticsInvalidTypeError: Failed to parse response JSON]', + ); + }); }); }); diff --git a/src/batch/client.ts b/src/batch/client.ts index 073f757..a5ff9c9 100644 --- a/src/batch/client.ts +++ b/src/batch/client.ts @@ -13,7 +13,10 @@ import { QueryParams, request, SM_APP_PARAM_NAME } from '../utils/request'; import poll from '../utils/poll'; import RetrieveJobsFilters from '../types/list-job-filters'; import { BatchFeatureDiscovery } from '../types/batch-feature-discovery'; -import { SpeechmaticsConfigurationError } from '../utils/errors'; +import { + SpeechmaticsConfigurationError, + SpeechmaticsResponseError, +} from '../utils/errors'; export class BatchTranscription { private config: ConnectionConfigFull; @@ -38,23 +41,26 @@ export class BatchTranscription { this.config = new ConnectionConfigFull(config); } - private async refreshOnFail( + private async refreshOnAuthFail( doRequest: (key: string) => Promise, ): Promise { try { return await doRequest(this.apiKey ?? (await this.refreshApiKey())); } catch (e) { - console.info('Retrying due to error:', e); - return await doRequest(await this.refreshApiKey()); + if (e instanceof SpeechmaticsResponseError && e.response.code === 401) { + return await doRequest(await this.refreshApiKey()); + } else { + throw e; + } } } - private async get( + private async get( endpoint: string, contentType?: string, - queryParams?: QueryParams, + queryParams?: QueryParams, ): Promise { - return await this.refreshOnFail((key: string) => + return await this.refreshOnAuthFail((key: string) => request( key, this.config.batchUrl, @@ -72,7 +78,7 @@ export class BatchTranscription { body: FormData | null = null, contentType?: string, ): Promise { - return await this.refreshOnFail((key: string) => + return await this.refreshOnAuthFail((key: string) => request( key, this.config.batchUrl, @@ -85,11 +91,8 @@ export class BatchTranscription { ); } - private async delete( - endpoint: string, - params?: QueryParams, - ): Promise { - return this.refreshOnFail((key: string) => + private async delete(endpoint: string, params?: QueryParams): Promise { + return this.refreshOnAuthFail((key: string) => request(key, this.config.batchUrl, endpoint, 'DELETE', null, { ...params, [SM_APP_PARAM_NAME]: this.config.appId, @@ -170,14 +173,26 @@ export class BatchTranscription { async listJobs( filters: RetrieveJobsFilters = {}, ): Promise { - return this.get('/v2/jobs', 'application/json', filters); + if (this.config.apiKey === undefined) + throw new SpeechmaticsConfigurationError( + 'Missing apiKey in configuration', + ); + return this.get('/v2/jobs', 'application/json', { ...filters }); } async getJob(id: string): Promise { + if (this.config.apiKey === undefined) + throw new SpeechmaticsConfigurationError( + 'Missing apiKey in configuration', + ); return this.get(`/v2/jobs/${id}`, 'application/json'); } async deleteJob(id: string, force = false): Promise { + if (this.config.apiKey === undefined) + throw new SpeechmaticsConfigurationError( + 'Missing apiKey in configuration', + ); const params = force ? { force } : undefined; return this.delete(`/v2/jobs/${id}`, params); } @@ -199,6 +214,10 @@ export class BatchTranscription { jobId: string, format: TranscriptionFormat = 'json-v2', ): Promise { + if (this.config.apiKey === undefined) + throw new SpeechmaticsConfigurationError( + 'Missing apiKey in configuration', + ); const params = { format: format === 'text' ? 'txt' : format }; const contentType = format === 'json-v2' ? 'application/json' : 'text/plain'; @@ -206,6 +225,10 @@ export class BatchTranscription { } async getDataFile(jobId: string) { + if (this.config.apiKey === undefined) + throw new SpeechmaticsConfigurationError( + 'Missing apiKey in configuration', + ); return this.get(`/v2/jobs/${jobId}/data`, 'application/json'); } diff --git a/src/utils/errors.ts b/src/utils/errors.ts index 1364faf..18503c5 100644 --- a/src/utils/errors.ts +++ b/src/utils/errors.ts @@ -72,7 +72,7 @@ export class SpeechmaticsNetworkError extends SpeechmaticsInternalError { export class SpeechmaticsUnsupportedEnvironment extends SpeechmaticsInternalError { constructor(message?: string) { super(InternalErrorEnum.UnsupportedEnvironment, message); - this.name = 'SpeechmaticsUnsupp'; + this.name = 'SpeechmaticsUnsupportedEnvironment'; } } export class SpeechmaticsUnexpectedMessage extends SpeechmaticsInternalError { @@ -88,8 +88,8 @@ export class SpeechmaticsUnexpectedResponse extends SpeechmaticsInternalError { } } export class SpeechmaticsInvalidTypeError extends SpeechmaticsInternalError { - constructor(message?: string) { - super(InternalErrorEnum.InvalidTypeError, message); + constructor(message?: string, cause?: unknown) { + super(InternalErrorEnum.InvalidTypeError, message, cause); this.name = 'SpeechmaticsInvalidTypeError'; } } diff --git a/src/utils/file-utils.ts b/src/utils/file-utils.ts deleted file mode 100644 index f471d1e..0000000 --- a/src/utils/file-utils.ts +++ /dev/null @@ -1,20 +0,0 @@ -export async function determineFileBuffer( - urlOrBuffer: string | Buffer, -): Promise { - if (typeof urlOrBuffer === 'string') { - return await fetchFileBuffer(urlOrBuffer); - } else if (urlOrBuffer instanceof Buffer) { - return urlOrBuffer as Buffer; - } - - return Promise.reject('SMjs error: Invalid file'); -} - -export async function fetchFileBuffer(url: string): Promise { - const response = await fetch(url); - if (response?.ok) { - return Buffer.from(await response.arrayBuffer()); - } else { - return Promise.reject(`SMjs error: ${response.statusText}`); - } -} diff --git a/src/utils/request.ts b/src/utils/request.ts index 01a2c7e..2754d0a 100644 --- a/src/utils/request.ts +++ b/src/utils/request.ts @@ -1,21 +1,22 @@ import { SpeechmaticsInvalidTypeError, + SpeechmaticsNetworkError, SpeechmaticsResponseError, } from './errors'; export type HttpMethod = 'GET' | 'PUT' | 'POST' | 'DELETE'; -export type QueryParams = Partial< - Record +export type QueryParams = Readonly< + Record >; -export async function request( +export async function request( apiKey: string, url: string, path: string, method: HttpMethod = 'POST', payload?: BodyInit | null | undefined, - params?: QueryParams, + params?: QueryParams, contentType?: string, ): Promise { const requestOptions: RequestInit = { @@ -34,7 +35,12 @@ export async function request( fullUrl = addQueryParamsToUrl(fullUrl, params); } - const response = await fetch(fullUrl, requestOptions); + let response: Response; + try { + response = await fetch(fullUrl, requestOptions); + } catch (err) { + throw new SpeechmaticsNetworkError(`Error fetching from ${path}`, err); + } if (!response.ok) { const responseJson = await response.json(); @@ -44,14 +50,25 @@ export async function request( const isPlain = contentType === 'text/plain'; let result: T; - try { - if (isPlain) { + + if (isPlain) { + try { result = (await response.text()) as T; - } else { + } catch (err) { + throw new SpeechmaticsInvalidTypeError( + 'Failed to parse response text', + err, + ); + } + } else { + try { result = (await response.json()) as T; + } catch (err) { + throw new SpeechmaticsInvalidTypeError( + 'Failed to parse response JSON', + err, + ); } - } catch (error) { - throw new SpeechmaticsInvalidTypeError(`Cannot parse error:\n\n${error}`); } return result; @@ -64,14 +81,14 @@ export function getSmSDKVersion(): string { return `js-${SDK_VERSION}`; } -export function addQueryParamsToUrl( +export function addQueryParamsToUrl( url: string, - queryParams: QueryParams, + queryParams: QueryParams, ): string { const parsedUrl = new URL(url); const params = new URLSearchParams(parsedUrl.search); Object.keys(queryParams).forEach((key) => { - const value = queryParams[key as K]; + const value = queryParams[key]; if (value !== undefined) params.append(key, `${value}`); }); parsedUrl.search = params.toString(); From 831e3e930f912c41afafddd713d8f6b6279f178d Mon Sep 17 00:00:00 2001 From: Matt Nemitz Date: Fri, 9 Feb 2024 11:33:32 +0000 Subject: [PATCH 7/7] Bump version --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 8f39e46..c21870d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "speechmatics", - "version": "4.0.0-pre.0", + "version": "4.0.0-pre.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "speechmatics", - "version": "4.0.0-pre.0", + "version": "4.0.0-pre.1", "license": "MIT", "dependencies": { "bufferutil": "^4.0.7", diff --git a/package.json b/package.json index 0d5e082..4519c19 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "speechmatics", - "version": "4.0.0-pre.0", + "version": "4.0.0-pre.1", "description": "Speechmatics Javascript Libraries", "types": "./dist/index.d.ts", "main": "./dist/index.js",