From 249c4c404dca1b809430fd649a394aa7d631350c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0t=C4=9Bp=C3=A1n=20Gran=C3=A1t?= Date: Thu, 14 Mar 2024 12:54:19 +0100 Subject: [PATCH] feat: errors handling (#3320) --- package.json | 2 +- packages/core/src/Controller/Cache/Cache.ts | 48 ++++--- packages/core/src/Controller/Controller.ts | 15 +-- packages/core/src/Controller/Events/Events.ts | 4 + .../core/src/Controller/Plugins/Plugins.ts | 52 ++++--- .../core/src/__test/errors.detector.test.ts | 64 +++++++++ .../core/src/__test/errors.record.test.ts | 127 ++++++++++++++++++ .../core/src/__test/errors.storage.test.ts | 117 ++++++++++++++++ packages/core/src/helpers.ts | 31 ++++- packages/core/src/types/errors.ts | 40 ++++++ packages/core/src/types/events.ts | 7 + packages/core/src/types/index.ts | 1 + 12 files changed, 457 insertions(+), 51 deletions(-) create mode 100644 packages/core/src/__test/errors.detector.test.ts create mode 100644 packages/core/src/__test/errors.record.test.ts create mode 100644 packages/core/src/__test/errors.storage.test.ts create mode 100644 packages/core/src/types/errors.ts diff --git a/package.json b/package.json index a282a96e7c..481b006f09 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "private": true, "scripts": { "build": "turbo run build --cache-dir='.turbo'", - "test": "turbo run test --cache-dir='.turbo' && npm run testRoot", + "test": "turbo run test --cache-dir='.turbo' -- --runInBand && npm run testRoot -- --runInBand", "testRoot": "jest", "eslint": "eslint --ext .ts --ext .tsx --max-warnings 0 .", "develop": "turbo run develop --parallel --include-dependencies --cache-dir='.turbo'", diff --git a/packages/core/src/Controller/Cache/Cache.ts b/packages/core/src/Controller/Cache/Cache.ts index 930ec5190b..8f6557fda4 100644 --- a/packages/core/src/Controller/Cache/Cache.ts +++ b/packages/core/src/Controller/Cache/Cache.ts @@ -1,19 +1,19 @@ import { CacheDescriptor, CacheDescriptorInternal, - CacheDescriptorWithKey, NsFallback, TranslationsFlat, TranslationValue, TreeTranslationsData, BackendGetRecordInternal, + RecordFetchError, } from '../../types'; -import { getFallbackArray, unique } from '../../helpers'; -import { EventEmitterInstance } from '../Events/EventEmitter'; +import { getFallbackArray, isPromise, unique } from '../../helpers'; import { TolgeeStaticData } from '../State/initState'; import { ValueObserverInstance } from '../ValueObserver'; import { decodeCacheKey, encodeCacheKey, flattenTranslations } from './helpers'; +import { EventsInstance } from '../Events/Events'; type CacheAsyncRequests = Map< string, @@ -28,7 +28,7 @@ type CacheRecord = { type StateCache = Map; export function Cache( - onCacheChange: EventEmitterInstance, + events: EventsInstance, backendGetRecord: BackendGetRecordInternal, backendGetDevRecord: BackendGetRecordInternal, withDefaultNs: (descriptor: CacheDescriptor) => CacheDescriptorInternal, @@ -51,48 +51,60 @@ export function Cache( data: flattenTranslations(data), version: recordVersion, }); - onCacheChange.emit(descriptor); + events.onCacheChange.emit(descriptor); } /** * Fetches production data */ function fetchProd(keyObject: CacheDescriptorInternal) { - let dataPromise = undefined as + let dataOrPromise = undefined as | Promise | undefined; - if (!dataPromise) { + if (!dataOrPromise) { const staticDataValue = staticData[encodeCacheKey(keyObject)]; if (typeof staticDataValue === 'function') { - dataPromise = staticDataValue(); + dataOrPromise = staticDataValue(); } } - if (!dataPromise) { - dataPromise = backendGetRecord(keyObject); + if (!dataOrPromise) { + dataOrPromise = backendGetRecord(keyObject); } - return dataPromise; + if (isPromise(dataOrPromise)) { + return dataOrPromise?.catch((e) => { + const error = new RecordFetchError(keyObject, e); + events.onError.emit(error); + // eslint-disable-next-line no-console + console.error(error); + throw error; + }); + } else { + return dataOrPromise; + } } function fetchData(keyObject: CacheDescriptorInternal, isDev: boolean) { - let dataPromise = undefined as + let dataOrPromise = undefined as | Promise | undefined; if (isDev) { - dataPromise = backendGetDevRecord(keyObject)?.catch(() => { + dataOrPromise = backendGetDevRecord(keyObject)?.catch((e) => { + const error = new RecordFetchError(keyObject, e, true); + events.onError.emit(error); // eslint-disable-next-line no-console - console.warn(`Tolgee: Failed to fetch data from dev backend`); + console.warn(error); // fallback to prod fetch if dev fails return fetchProd(keyObject); }); } - if (!dataPromise) { - dataPromise = fetchProd(keyObject); + if (!dataOrPromise) { + dataOrPromise = fetchProd(keyObject); } - return dataPromise; + return dataOrPromise; } const self = Object.freeze({ @@ -175,7 +187,7 @@ export function Cache( ) { const record = cache.get(encodeCacheKey(descriptor))?.data; record?.set(key, value); - onCacheChange.emit({ ...descriptor, key }); + events.onCacheChange.emit({ ...descriptor, key }); }, isFetching(ns?: NsFallback) { diff --git a/packages/core/src/Controller/Controller.ts b/packages/core/src/Controller/Controller.ts index e9589d2890..4722696fff 100644 --- a/packages/core/src/Controller/Controller.ts +++ b/packages/core/src/Controller/Controller.ts @@ -6,7 +6,6 @@ import { TFnType, NsType, KeyAndNamespacesInternal, - TranslationDescriptor, } from '../types'; import { Cache } from './Cache/Cache'; import { getFallbackArray } from '../helpers'; @@ -47,11 +46,11 @@ export function Controller({ options }: StateServiceProps) { getTranslationNs, getTranslation, changeTranslation, - onPermanentChange + events ); const cache = Cache( - events.onCacheChange, + events, pluginService.getBackendRecord, pluginService.getBackendDevRecord, state.withDefaultNs, @@ -110,13 +109,6 @@ export function Controller({ options }: StateServiceProps) { }; } - function onPermanentChange(props: TranslationDescriptor) { - events.onPermanentChange.emit({ - key: props.key, - namespace: props.namespace, - }); - } - function init(options: Partial) { state.init(options); cache.addStaticData(state.getInitialOptions().staticData); @@ -182,6 +174,7 @@ export function Controller({ options }: StateServiceProps) { return; } const languageOrPromise = pluginService.getInitialLanguage(); + return valueOrPromise(languageOrPromise, (lang) => { const language = (lang as string | undefined) || @@ -231,7 +224,7 @@ export function Controller({ options }: StateServiceProps) { // there might be parallel language change // we only want to apply latest state.setLanguage(language); - pluginService.setStoredLanguage(language); + await pluginService.setStoredLanguage(language); } }, diff --git a/packages/core/src/Controller/Events/Events.ts b/packages/core/src/Controller/Events/Events.ts index ee3f72327f..26f3bc4ac9 100644 --- a/packages/core/src/Controller/Events/Events.ts +++ b/packages/core/src/Controller/Events/Events.ts @@ -2,6 +2,7 @@ import { EventEmitter } from './EventEmitter'; import { EventEmitterSelective } from './EventEmitterSelective'; import { CacheDescriptorWithKey, + TolgeeError, TolgeeOn, TranslationDescriptor, } from '../../types'; @@ -26,6 +27,7 @@ export function Events( onCacheChange: EventEmitter(isActive), onUpdate: EventEmitterSelective(isActive, getFallbackNs, getDefaultNs), onPermanentChange: EventEmitter(isActive), + onError: EventEmitter(isActive), setEmitterActive(active: boolean) { emitterActive = active; }, @@ -49,6 +51,8 @@ export function Events( return self.onUpdate.listen(handler as any); case 'permanentChange': return self.onPermanentChange.listen(handler as any); + case 'error': + return self.onError.listen(handler as any); } }) as TolgeeOn, }); diff --git a/packages/core/src/Controller/Plugins/Plugins.ts b/packages/core/src/Controller/Plugins/Plugins.ts index 2bd878998d..952133e769 100644 --- a/packages/core/src/Controller/Plugins/Plugins.ts +++ b/packages/core/src/Controller/Plugins/Plugins.ts @@ -1,4 +1,8 @@ -import { getErrorMessage, isPromise, valueOrPromise } from '../../helpers'; +import { + getErrorMessage, + valueOrPromise, + handleRegularOrAsyncErr, +} from '../../helpers'; import { BackendDevMiddleware, BackendMiddleware, @@ -22,8 +26,10 @@ import { FormatErrorHandler, FindPositionsInterface, BackendGetRecordInternal, - TranslationDescriptor, + LanguageStorageError, + LanguageDetectorError, } from '../../types'; +import { EventsInstance } from '../Events/Events'; import { DEFAULT_FORMAT_ERROR } from '../State/initState'; export function Plugins( @@ -34,7 +40,7 @@ export function Plugins( getTranslationNs: (props: KeyAndNamespacesInternal) => string[], getTranslation: (props: KeyAndNamespacesInternal) => string | undefined, changeTranslation: ChangeTranslationInterface, - onPermanentChange: (props: TranslationDescriptor) => void + events: EventsInstance ) { const plugins = { ui: undefined as UiMiddleware | undefined, @@ -125,6 +131,14 @@ export function Plugins( instances.languageDetector = detector; } + function storageLoadLanguage() { + return handleRegularOrAsyncErr( + events.onError, + (e) => new LanguageStorageError('Tolgee: Failed to load language', e), + () => instances.languageStorage?.getLanguage(getCommonProps()) + ); + } + function detectLanguage() { if (!instances.languageDetector) { return undefined; @@ -132,10 +146,15 @@ export function Plugins( const availableLanguages = getAvailableLanguages()!; - return instances.languageDetector.getLanguage({ - availableLanguages, - ...getCommonProps(), - }); + return handleRegularOrAsyncErr( + events.onError, + (e) => new LanguageDetectorError('Tolgee: Failed to detect language', e), + () => + instances.languageDetector?.getLanguage({ + availableLanguages, + ...getCommonProps(), + }) + ); } function addBackend(backend: BackendMiddleware | undefined) { @@ -177,7 +196,7 @@ export function Plugins( highlight: self.highlight, changeTranslation, findPositions, - onPermanentChange, + onPermanentChange: (data) => events.onPermanentChange.emit(data), }); instances.observer?.run({ @@ -199,9 +218,7 @@ export function Plugins( getInitialLanguage() { const availableLanguages = getAvailableLanguages(); - const languageOrPromise = instances.languageStorage?.getLanguage( - getCommonProps() - ); + const languageOrPromise = storageLoadLanguage(); return valueOrPromise(languageOrPromise, (language) => { if ( @@ -215,7 +232,11 @@ export function Plugins( }, setStoredLanguage(language: string) { - instances.languageStorage?.setLanguage(language, getCommonProps()); + return handleRegularOrAsyncErr( + events.onError, + (e) => new LanguageStorageError('Tolgee: Failed to store language', e), + () => instances.languageStorage?.setLanguage(language, getCommonProps()) + ); }, getDevBackend() { @@ -229,13 +250,6 @@ export function Plugins( namespace, ...getCommonProps(), }); - if (isPromise(data)) { - return data?.catch((e) => { - // eslint-disable-next-line no-console - console.error(e); - return {}; - }); - } if (data !== undefined) { return data; } diff --git a/packages/core/src/__test/errors.detector.test.ts b/packages/core/src/__test/errors.detector.test.ts new file mode 100644 index 0000000000..223f5f178e --- /dev/null +++ b/packages/core/src/__test/errors.detector.test.ts @@ -0,0 +1,64 @@ +/* eslint-disable no-console */ +import { TolgeeCore } from '../TolgeeCore'; +import { LanguageDetectorError, TolgeePlugin } from '../types'; + +const failingDetector = (async: boolean): TolgeePlugin => { + return (tolgee, tools) => { + tools.setLanguageDetector({ + async getLanguage() { + if (async) { + return Promise.reject(new Error('failed to fetch')); + } else { + throw new Error('failed to fetch'); + } + }, + }); + return tolgee; + }; +}; + +describe('language detector errors', () => { + beforeEach(() => { + console.warn = jest.fn(); + console.error = jest.fn(); + }); + + it('emitts error when detector fails async', async () => { + const errorHandler = jest.fn(); + const tolgee = TolgeeCore() + .use(failingDetector(true)) + .init({ availableLanguages: ['en', 'cs'], defaultLanguage: 'en' }); + tolgee.on('error', errorHandler); + await expect(() => tolgee.run()).rejects.toThrow(LanguageDetectorError); + const firstArgument = errorHandler.mock.calls[0][0] + .value as LanguageDetectorError; + expect(firstArgument).toBeInstanceOf(LanguageDetectorError); + expect(firstArgument).toBeInstanceOf(Error); + expect(firstArgument).toHaveProperty('name', 'LanguageDetectorError'); + expect(firstArgument).toHaveProperty( + 'message', + 'Tolgee: Failed to detect language' + ); + expect(console.warn).toBeCalledTimes(0); + expect(console.error).toBeCalledTimes(1); + }); + + it('emitts error when detector fails sync', async () => { + const errorHandler = jest.fn(); + const tolgee = TolgeeCore() + .use(failingDetector(false)) + .init({ availableLanguages: ['en', 'cs'], defaultLanguage: 'en' }); + tolgee.on('error', errorHandler); + await expect(() => tolgee.run()).rejects.toThrow(LanguageDetectorError); + const firstArgument = errorHandler.mock.calls[0][0] + .value as LanguageDetectorError; + expect(firstArgument).toBeInstanceOf(LanguageDetectorError); + expect(firstArgument).toHaveProperty('name', 'LanguageDetectorError'); + expect(firstArgument).toHaveProperty( + 'message', + 'Tolgee: Failed to detect language' + ); + expect(console.warn).toBeCalledTimes(0); + expect(console.error).toBeCalledTimes(1); + }); +}); diff --git a/packages/core/src/__test/errors.record.test.ts b/packages/core/src/__test/errors.record.test.ts new file mode 100644 index 0000000000..b6ac8a2e63 --- /dev/null +++ b/packages/core/src/__test/errors.record.test.ts @@ -0,0 +1,127 @@ +/* eslint-disable no-console */ +import { TolgeeCore } from '../TolgeeCore'; +import { RecordFetchError, TolgeePlugin } from '../types'; + +const failingBackend = (): TolgeePlugin => { + return (tolgee, tools) => { + tools.addBackend({ + async getRecord() { + return Promise.reject(new Error('failed to fetch')); + }, + }); + return tolgee; + }; +}; + +const failingDevBackend = (): TolgeePlugin => { + return (tolgee, tools) => { + tools.setDevBackend({ + async getRecord() { + return Promise.reject(new Error('failed to fetch')); + }, + }); + return tolgee; + }; +}; + +describe('translation records', () => { + beforeEach(() => { + console.warn = jest.fn(); + console.error = jest.fn(); + }); + + it('emitts error when fails to fetch dev record', async () => { + const errorHandler = jest.fn(); + const tolgee = TolgeeCore() + .use(failingDevBackend()) + .init({ language: 'en', apiKey: 'test', apiUrl: 'test' }); + tolgee.on('error', errorHandler); + await tolgee.run(); + const firstArgument = errorHandler.mock.calls[0][0] + .value as RecordFetchError; + expect(firstArgument).toBeInstanceOf(RecordFetchError); + expect(firstArgument).toBeInstanceOf(Error); + expect(firstArgument).toHaveProperty('language', 'en'); + expect(firstArgument).toHaveProperty('namespace', ''); + expect(firstArgument).toHaveProperty('isDev', true); + expect(firstArgument).toHaveProperty('name', 'RecordFetchError'); + expect(firstArgument).toHaveProperty( + 'message', + 'Tolgee: Failed to fetch record for "en"' + ); + expect(console.warn).toBeCalledTimes(1); + expect(console.error).toBeCalledTimes(0); + }); + + it('emitts error when fails to fetch record', async () => { + const errorHandler = jest.fn(); + const tolgee = TolgeeCore().use(failingBackend()).init({ language: 'en' }); + tolgee.on('error', errorHandler); + await expect(() => tolgee.run()).rejects.toThrow(RecordFetchError); + const firstArgument = errorHandler.mock.calls[0][0] + .value as RecordFetchError; + expect(firstArgument).toBeInstanceOf(RecordFetchError); + expect(firstArgument).toHaveProperty('language', 'en'); + expect(firstArgument).toHaveProperty('namespace', ''); + expect(firstArgument).toHaveProperty('isDev', false); + expect(firstArgument).toHaveProperty('name', 'RecordFetchError'); + expect(firstArgument).toHaveProperty( + 'message', + 'Tolgee: Failed to fetch record for "en"' + ); + expect(console.warn).toBeCalledTimes(0); + expect(console.error).toBeCalledTimes(1); + }); + + it('emitts error when fails to fetch promise in static data', async () => { + const errorHandler = jest.fn(); + const tolgee = TolgeeCore() + .use(failingDevBackend()) + .init({ + language: 'en', + staticData: { en: () => Promise.reject(new Error('No data')) }, + }); + tolgee.on('error', errorHandler); + await expect(() => tolgee.run()).rejects.toThrow(RecordFetchError); + + const firstArgument = errorHandler.mock.calls[0][0] + .value as RecordFetchError; + expect(firstArgument).toBeInstanceOf(RecordFetchError); + expect(firstArgument).toHaveProperty('language', 'en'); + expect(firstArgument).toHaveProperty('namespace', ''); + expect(firstArgument).toHaveProperty('isDev', false); + expect(firstArgument).toHaveProperty('name', 'RecordFetchError'); + expect(firstArgument).toHaveProperty( + 'message', + 'Tolgee: Failed to fetch record for "en"' + ); + expect(console.warn).toBeCalledTimes(0); + expect(console.error).toBeCalledTimes(1); + }); + + it('emitts error when it fails', async () => { + const errorHandler = jest.fn(); + const tolgee = TolgeeCore() + .use(failingDevBackend()) + .init({ + language: 'en', + staticData: { en: () => Promise.reject(new Error('No data')) }, + }); + tolgee.on('error', errorHandler); + await expect(() => tolgee.run()).rejects.toThrow(RecordFetchError); + + const firstArgument = errorHandler.mock.calls[0][0] + .value as RecordFetchError; + expect(firstArgument).toBeInstanceOf(RecordFetchError); + expect(firstArgument).toHaveProperty('language', 'en'); + expect(firstArgument).toHaveProperty('namespace', ''); + expect(firstArgument).toHaveProperty('isDev', false); + expect(firstArgument).toHaveProperty('name', 'RecordFetchError'); + expect(firstArgument).toHaveProperty( + 'message', + 'Tolgee: Failed to fetch record for "en"' + ); + expect(console.warn).toBeCalledTimes(0); + expect(console.error).toBeCalledTimes(1); + }); +}); diff --git a/packages/core/src/__test/errors.storage.test.ts b/packages/core/src/__test/errors.storage.test.ts new file mode 100644 index 0000000000..16301b5973 --- /dev/null +++ b/packages/core/src/__test/errors.storage.test.ts @@ -0,0 +1,117 @@ +/* eslint-disable no-console */ +import { TolgeeCore } from '../TolgeeCore'; +import { LanguageStorageError, TolgeePlugin } from '../types'; + +const failingStorage = (async: boolean): TolgeePlugin => { + return (tolgee, tools) => { + tools.setLanguageStorage({ + async getLanguage() { + if (async) { + return Promise.reject(new Error('failed to fetch')); + } else { + throw new Error('failed to fetch'); + } + }, + async setLanguage() { + if (async) { + return Promise.reject(new Error('failed to fetch')); + } else { + throw new Error('failed to fetch'); + } + }, + }); + return tolgee; + }; +}; + +describe('language storage errors', () => { + beforeEach(() => { + console.warn = jest.fn(); + console.error = jest.fn(); + }); + + it('emitts error when language is loaded async', async () => { + const errorHandler = jest.fn(); + const tolgee = TolgeeCore() + .use(failingStorage(true)) + .init({ availableLanguages: ['en', 'cs'], defaultLanguage: 'en' }); + tolgee.on('error', errorHandler); + await expect(() => tolgee.run()).rejects.toThrow(LanguageStorageError); + const firstArgument = errorHandler.mock.calls[0][0] + .value as LanguageStorageError; + + expect(firstArgument).toHaveProperty( + 'message', + 'Tolgee: Failed to load language' + ); + expect(firstArgument).toBeInstanceOf(Error); + expect(firstArgument).toBeInstanceOf(LanguageStorageError); + expect(firstArgument).toHaveProperty('name', 'LanguageStorageError'); + expect(console.warn).toBeCalledTimes(0); + expect(console.error).toBeCalledTimes(1); + }); + + it('emitts error when language is loaded sync', async () => { + const errorHandler = jest.fn(); + const tolgee = TolgeeCore() + .use(failingStorage(false)) + .init({ availableLanguages: ['en', 'cs'], defaultLanguage: 'en' }); + tolgee.on('error', errorHandler); + await expect(() => tolgee.run()).rejects.toThrow(LanguageStorageError); + const firstArgument = errorHandler.mock.calls[0][0] + .value as LanguageStorageError; + + expect(firstArgument).toHaveProperty( + 'message', + 'Tolgee: Failed to load language' + ); + expect(firstArgument).toBeInstanceOf(LanguageStorageError); + expect(firstArgument).toHaveProperty('name', 'LanguageStorageError'); + expect(console.warn).toBeCalledTimes(0); + expect(console.error).toBeCalledTimes(1); + }); + + it('emitts error when language is saved async', async () => { + const errorHandler = jest.fn(); + const tolgee = TolgeeCore() + .use(failingStorage(true)) + .init({ availableLanguages: ['en', 'cs'], language: 'en' }); + tolgee.on('error', errorHandler); + await tolgee.run(); + await expect(() => tolgee.changeLanguage('cs')).rejects.toThrow( + LanguageStorageError + ); + const firstArgument = errorHandler.mock.calls[0][0] + .value as LanguageStorageError; + expect(firstArgument).toHaveProperty( + 'message', + 'Tolgee: Failed to store language' + ); + expect(firstArgument).toBeInstanceOf(LanguageStorageError); + expect(firstArgument).toHaveProperty('name', 'LanguageStorageError'); + expect(console.warn).toBeCalledTimes(0); + expect(console.error).toBeCalledTimes(1); + }); + + it('emitts error when language is saved sync', async () => { + const errorHandler = jest.fn(); + const tolgee = TolgeeCore() + .use(failingStorage(false)) + .init({ availableLanguages: ['en', 'cs'], language: 'en' }); + tolgee.on('error', errorHandler); + await tolgee.run(); + await expect(() => tolgee.changeLanguage('cs')).rejects.toThrow( + LanguageStorageError + ); + const firstArgument = errorHandler.mock.calls[0][0] + .value as LanguageStorageError; + expect(firstArgument).toHaveProperty( + 'message', + 'Tolgee: Failed to store language' + ); + expect(firstArgument).toBeInstanceOf(LanguageStorageError); + expect(firstArgument).toHaveProperty('name', 'LanguageStorageError'); + expect(console.warn).toBeCalledTimes(0); + expect(console.error).toBeCalledTimes(1); + }); +}); diff --git a/packages/core/src/helpers.ts b/packages/core/src/helpers.ts index 3dfdc68952..cc15097fd0 100644 --- a/packages/core/src/helpers.ts +++ b/packages/core/src/helpers.ts @@ -3,10 +3,14 @@ import { FallbackLanguageObject, FallbackLanguageOption, FetchFn, + TolgeeError, } from './types'; +import { EventEmitterInstance } from './Controller/Events/EventEmitter'; -export function isPromise(value: any) { - return Boolean(value && typeof value.then === 'function'); +export function isPromise(value: unknown): value is Promise { + return Boolean( + value && typeof (value as unknown as Promise).then === 'function' + ); } export function valueOrPromise( @@ -20,6 +24,29 @@ export function valueOrPromise( } } +export function handleRegularOrAsyncErr( + onError: EventEmitterInstance, + createError: (e: any) => TolgeeError, + callback: () => Promise | T +): Promise | T { + function handle(e: any): never { + const error = createError(e); + onError.emit(error); + // eslint-disable-next-line no-console + console.error(error); + throw error; + } + try { + const result = callback(); + if (isPromise(result)) { + return result.catch(handle); + } + return result; + } catch (e) { + handle(e); + } +} + export function missingOptionError(option: string | string[]) { const options = (Array.isArray(option) ? option : [option]).map( (val) => `'${val}'` diff --git a/packages/core/src/types/errors.ts b/packages/core/src/types/errors.ts new file mode 100644 index 0000000000..84b1574ec3 --- /dev/null +++ b/packages/core/src/types/errors.ts @@ -0,0 +1,40 @@ +import { CacheDescriptorInternal } from './cache'; + +export class RecordFetchError extends Error { + public name = 'RecordFetchError' as const; + public language: string; + public namespace: string | undefined; + constructor( + descriptor: CacheDescriptorInternal, + public cause: any, + public isDev: boolean = false + ) { + const { language, namespace } = descriptor; + super( + `Tolgee: Failed to fetch record for "${language}"${ + namespace && ` and "${namespace}"` + }` + ); + this.language = language; + this.namespace = namespace; + } +} + +export class LanguageDetectorError extends Error { + public name = 'LanguageDetectorError' as const; + constructor(message: string, public cause: any) { + super(message); + } +} + +export class LanguageStorageError extends Error { + public name = 'LanguageStorageError' as const; + constructor(message: string, public cause: any) { + super(message); + } +} + +export type TolgeeError = + | RecordFetchError + | LanguageDetectorError + | LanguageStorageError; diff --git a/packages/core/src/types/events.ts b/packages/core/src/types/events.ts index 4679ffa089..dabe0a6379 100644 --- a/packages/core/src/types/events.ts +++ b/packages/core/src/types/events.ts @@ -1,5 +1,6 @@ import type { NsFallback } from './general'; import type { CacheDescriptorWithKey } from './cache'; +import { TolgeeError } from './errors'; export type Subscription = { unsubscribe: () => void; @@ -39,6 +40,7 @@ export interface EventType { running: boolean; cache: CacheDescriptorWithKey; update: void; + error: TolgeeError; permanentChange: CacheDescriptorWithKey; } @@ -84,6 +86,11 @@ export type TolgeeOn = { */ (event: 'cache', handler: Listener): Subscription; + /** + * Emitted on errors + */ + (event: 'error', handler: Listener): Subscription; + /** * Translation was changed or created via dev tools */ diff --git a/packages/core/src/types/index.ts b/packages/core/src/types/index.ts index a42ea737d1..86ebbcbd19 100644 --- a/packages/core/src/types/index.ts +++ b/packages/core/src/types/index.ts @@ -2,6 +2,7 @@ export * from './general'; export * from './events'; export * from './cache'; export * from './plugin'; +export * from './errors'; export type { State,