mirror of
https://github.com/FoxxMD/multi-scrobbler.git
synced 2026-08-20 14:13:20 +00:00
feat: Implement multiple stateful errors/warnings on component
This commit is contained in:
parent
08a4686885
commit
eb43fa04fd
10 changed files with 56 additions and 41 deletions
|
|
@ -245,7 +245,7 @@ export default abstract class AbstractComponent extends AbstractInitializable {
|
|||
this.setStatus('Retention cleanup finished');
|
||||
} catch (e) {
|
||||
const retentionErr = new Error('Failed to do retention cleanup', {cause: e});
|
||||
this.warning = retentionErr;
|
||||
this.warnings.push(retentionErr);
|
||||
this.logger.warn(retentionErr);
|
||||
this.setStatus('Retention cleanup failed');
|
||||
}
|
||||
|
|
@ -545,7 +545,7 @@ export default abstract class AbstractComponent extends AbstractInitializable {
|
|||
|
||||
public abstract getRunningState(): ComponentState
|
||||
|
||||
public getApiData(): Omit<ComponentCommonApiJson, 'type' | 'countLive' | 'players'> & Pick<ComponentCommonApi, 'state' | 'error' | 'warning'> {
|
||||
public getApiData(): Omit<ComponentCommonApiJson, 'type' | 'countLive' | 'players'> & Pick<ComponentCommonApi, 'state' | 'errors' | 'warnings'> {
|
||||
let state: ComponentState;
|
||||
if(!this.initializedOnce || this.initializing) {
|
||||
state = COMPONENT_STATE.INITIALIZING;
|
||||
|
|
@ -565,8 +565,8 @@ export default abstract class AbstractComponent extends AbstractInitializable {
|
|||
createdAt: this.dbComponent.createdAt?.toISOString(),
|
||||
lastReadyAt: this.lastReadyAt?.toISOString(),
|
||||
lastActiveAt: this.lastActiveAt?.toISOString(),
|
||||
error: this.error !== undefined && this.error instanceof Error ? serializeError(this.error) : this.error,
|
||||
warning: this.warning !== undefined && this.warning instanceof Error ? serializeError(this.warning) : this.warning,
|
||||
errors: this.errors.map(x => x instanceof Error ? serializeError(x) : x),
|
||||
warnings: this.warnings.map(x => x instanceof Error ? serializeError(x) : x),
|
||||
...this.additionalApiData()
|
||||
}
|
||||
}
|
||||
|
|
@ -582,11 +582,19 @@ export default abstract class AbstractComponent extends AbstractInitializable {
|
|||
}
|
||||
|
||||
protected emitComponentUpdate = <T extends Partial<ReturnType<typeof this.getApiData>>>(payload: T) => {
|
||||
if('error' in payload && payload.error instanceof Error) {
|
||||
payload.error = serializeError(payload.error);
|
||||
if('errors' in payload) {
|
||||
if(payload.errors.length > 0) {
|
||||
payload.errors = payload.errors.map(x => x instanceof Error ? serializeError(x) : x);
|
||||
} else {
|
||||
payload.errors = [];
|
||||
}
|
||||
}
|
||||
if('warning' in payload && payload.warning instanceof Error) {
|
||||
payload.warning = serializeError(payload.warning);
|
||||
if('warnings' in payload) {
|
||||
if(payload.warnings.length > 0) {
|
||||
payload.warnings = payload.warnings.map(x => x instanceof Error ? serializeError(x) : x);
|
||||
} else {
|
||||
payload.warnings = [];
|
||||
}
|
||||
}
|
||||
this.emitEvent('componentUpdate', payload);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,8 +19,8 @@ export default abstract class AbstractInitializable {
|
|||
databaseOK?: boolean | null;
|
||||
connectionOK?: boolean | null;
|
||||
cacheOK?: boolean | null;
|
||||
error?: Error;
|
||||
warning?: Error;
|
||||
errors?: Error[] = [];
|
||||
warnings?: Error[] = [];
|
||||
|
||||
protected initializedOnce: boolean = false;
|
||||
initializing: boolean = false;
|
||||
|
|
@ -88,15 +88,15 @@ export default abstract class AbstractInitializable {
|
|||
} catch (e) {
|
||||
throw new PostInitError('Error occurred during post-initialization hook', {cause: e});
|
||||
}
|
||||
this.error = undefined;
|
||||
this.warning = undefined;
|
||||
this.errors = [];
|
||||
this.warnings = [];
|
||||
return true;
|
||||
} catch(e) {
|
||||
if(notify) {
|
||||
await this.notify({identifier: this.getIdentifier(), title: notifyTitle, message: truncateStringToLength(500)(messageWithCausesTruncatedDefault(e)), priority: 'error'});
|
||||
}
|
||||
const initError = new Error('Initialization failed', {cause: e});
|
||||
this.error = initError;
|
||||
this.errors = [initError];
|
||||
throw initError;
|
||||
} finally {
|
||||
this.initializing = false;
|
||||
|
|
|
|||
|
|
@ -249,12 +249,12 @@ export default abstract class AbstractScrobbleClient extends AbstractComponent i
|
|||
'Heartbeat',
|
||||
(): Promise<any> => {
|
||||
return this.heartbeatTask().then(() => null).catch((err) => {
|
||||
this.error = err;
|
||||
this.errors.push(err);
|
||||
this.logger.error(err);
|
||||
});
|
||||
},
|
||||
(err: Error) => {
|
||||
this.error = err;
|
||||
this.errors.push(err);
|
||||
this.logger.error(err);
|
||||
}
|
||||
), {id: 'heartbeat'}));
|
||||
|
|
@ -278,14 +278,14 @@ export default abstract class AbstractScrobbleClient extends AbstractComponent i
|
|||
(): Promise<any> => {
|
||||
if(this.isReady()) {
|
||||
return this.processDeadLetterQueue().then(() => null).catch((e) => {
|
||||
this.warning = e;
|
||||
this.warnings = e;
|
||||
this.logger.error(e);
|
||||
})
|
||||
}
|
||||
return new Promise((resolve, reject) => resolve);
|
||||
},
|
||||
(err: Error) => {
|
||||
this.warning = err;
|
||||
this.warnings.push(err);
|
||||
this.logger.error(err);
|
||||
}
|
||||
), {id: 'dead'}));
|
||||
|
|
@ -547,8 +547,8 @@ export default abstract class AbstractScrobbleClient extends AbstractComponent i
|
|||
}, (err: Error) => {
|
||||
const npErr = new Error('Unexpected error while processing Now Playing queue', {cause: err});
|
||||
this.npLogger.error(npErr);
|
||||
this.warning = npErr;
|
||||
this.emitComponentUpdate<Partial<ComponentClientApiJson>>({warning: npErr});
|
||||
this.warnings.push(npErr);
|
||||
this.emitComponentUpdate<Partial<ComponentClientApiJson>>({warnings: this.warnings});
|
||||
});
|
||||
|
||||
// even though we are processing every 5 seconds the interval that Now Playing is updated at, and that the queue is cleared on,
|
||||
|
|
@ -856,8 +856,8 @@ export default abstract class AbstractScrobbleClient extends AbstractComponent i
|
|||
}
|
||||
} catch (e) {
|
||||
const preloadErr = new SimpleError('Could not preload scrobbles', {cause: e, shortStack: true});
|
||||
this.warning = preloadErr;
|
||||
this.emitComponentUpdate<Partial<ComponentClientApiJson>>({warning: preloadErr});
|
||||
this.warnings.push(preloadErr);
|
||||
this.emitComponentUpdate<Partial<ComponentClientApiJson>>({warnings: this.warnings});
|
||||
this.logger.warn(preloadErr);
|
||||
}
|
||||
}
|
||||
|
|
@ -1044,7 +1044,8 @@ export default abstract class AbstractScrobbleClient extends AbstractComponent i
|
|||
const err = new Error('Scrobble processing stopped with error', { cause: e });
|
||||
this.logger.warn(err);
|
||||
componentUpdate.status = 'Processing stopped with error';
|
||||
componentUpdate.warning = err;
|
||||
this.warnings.push(err);
|
||||
componentUpdate.warnings = this.warnings;
|
||||
}
|
||||
this.emitComponentUpdate<Partial<ComponentClientApiJson>>(componentUpdate);
|
||||
}).finally(() => {
|
||||
|
|
@ -1169,10 +1170,10 @@ export default abstract class AbstractScrobbleClient extends AbstractComponent i
|
|||
if(nextQueued !== undefined) {
|
||||
while (nextQueued !== undefined) {
|
||||
await this.processQueueCurrentScrobble(nextQueued, signal);
|
||||
if(this.error !== undefined) {
|
||||
if(this.errors.length > 0) {
|
||||
// we made it through a scrobble without any issues so clear any issue we may have previously had
|
||||
this.error = undefined;
|
||||
this.emitComponentUpdate<Partial<ComponentClientApiJson>>({error: null});
|
||||
this.errors = [];
|
||||
this.emitComponentUpdate<Partial<ComponentClientApiJson>>({errors: []});
|
||||
}
|
||||
nextQueued = await this.playRepo.getQueueNext(CLIENT_INGRESS_QUEUE)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,9 +27,7 @@ export const setupDeezerRoutes = (app: Express, logger: Logger, scrobbleSources:
|
|||
// @ts-expect-error TS(2339): Property 'deezerSource' does not exist on type 'Se... Remove this comment to see the full error message
|
||||
const entity = scrobbleSources.getByName(req.session.deezerSource as string) as DeezerSource;
|
||||
for(let i = 0; i < 3; i++) {
|
||||
if(entity.error !== undefined) {
|
||||
return res.send('Error with deezer credentials storage');
|
||||
} else if(entity.config.data.accessToken !== undefined) {
|
||||
if(entity.config.data.accessToken !== undefined) {
|
||||
// start polling
|
||||
await entity.doAuthentication();
|
||||
entity.poll()
|
||||
|
|
|
|||
|
|
@ -155,13 +155,13 @@ export default abstract class AbstractSource extends AbstractComponent implement
|
|||
'Heartbeat',
|
||||
(): Promise<any> => {
|
||||
return this.heartbeatTask().then(() => null).catch((err) => {
|
||||
this.error = err;
|
||||
this.errors.push(err);
|
||||
this.logger.error(err);
|
||||
});
|
||||
},
|
||||
(err: Error) => {
|
||||
this.logger.error(err);
|
||||
this.error = err;
|
||||
this.errors.push(err);
|
||||
}
|
||||
), {id: 'heartbeat'}));
|
||||
} else {
|
||||
|
|
@ -556,8 +556,8 @@ export default abstract class AbstractSource extends AbstractComponent implement
|
|||
const err = new Error('Cannot start polling because Source is not ready', {cause: e});
|
||||
this.logger.error(err);
|
||||
this.setStatus('Polling Error');
|
||||
this.emitComponentUpdate<Partial<ComponentSourceApiJson>>({error: err});
|
||||
this.error = err;
|
||||
this.errors.push(err);
|
||||
this.emitComponentUpdate<Partial<ComponentSourceApiJson>>({errors: this.errors});
|
||||
if(notify) {
|
||||
await this.notify( {title: `Polling Error`, message: `Cannot start polling because Source is not ready: ${truncateStringToLength(500)(messageWithCausesTruncatedDefault(e))}`, priority: 'error'});
|
||||
}
|
||||
|
|
@ -609,8 +609,8 @@ export default abstract class AbstractSource extends AbstractComponent implement
|
|||
const err = new Error('Polling stopped with error', { cause: e });
|
||||
this.logger.warn(err);
|
||||
componentUpdate.status = 'Polling stopped with error';
|
||||
componentUpdate.warning = err;
|
||||
this.warning = err;
|
||||
this.warnings.push(err);
|
||||
componentUpdate.warnings = this.warnings;
|
||||
}
|
||||
this.emitComponentUpdate<Partial<ComponentSourceApiJson>>(componentUpdate);
|
||||
}).finally(() => {
|
||||
|
|
|
|||
|
|
@ -242,7 +242,7 @@ export default class DeezerSource extends AbstractSource {
|
|||
return true;
|
||||
} else {
|
||||
this.logger.warn('Callback contained an error! User may have denied access?')
|
||||
this.error = error;
|
||||
this.errors = error;
|
||||
this.logger.error(error);
|
||||
return error;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,6 +34,12 @@ class TestComponent extends AbstractComponent {
|
|||
protected getIdentifier(): string {
|
||||
return 'test';
|
||||
}
|
||||
public async start(opts?: { forceInit?: boolean; }): Promise<boolean> {
|
||||
return true;
|
||||
}
|
||||
public async stop(opts?: { reason?: string | Error; }): Promise<void> {
|
||||
return;
|
||||
}
|
||||
constructor(config?: Omit<AbstractComponentConfig, 'id'>) {
|
||||
super({id: `test-${Date.now()}`, ...(config ?? {})});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -165,8 +165,8 @@ export const ComponentDetailedDesktop = (props: {data?: ComponentCommonApiJson,
|
|||
const {
|
||||
data,
|
||||
data: {
|
||||
warning,
|
||||
error
|
||||
warnings = [],
|
||||
errors = []
|
||||
} = {}
|
||||
} = props;
|
||||
const isSource = isComponentSourceApiJson(data)
|
||||
|
|
@ -207,8 +207,8 @@ export const ComponentDetailedDesktop = (props: {data?: ComponentCommonApiJson,
|
|||
<Flex justifyContent="flex-end" rowGap="6" flexDirection="row-reverse" wrap="wrap">
|
||||
<Box marginEnd="auto"><MSComponentStats {...props}/></Box>
|
||||
</Flex>
|
||||
{error !== undefined && error !== null ? <ErrorAlert error={error}/> : undefined}
|
||||
{warning !== undefined && warning !== null ? <ErrorAlert error={warning} status="warning"/> : undefined}
|
||||
{errors.length > 0 ? <>{errors.map(x => <ErrorAlert error={x}/>)}</> : undefined }
|
||||
{warnings.length > 0 ? <>{warnings.map(x => <ErrorAlert error={x} status="warning"/>)}</> : undefined }
|
||||
<MSErrorBoundary>{props.live ? <PlayersContainerFetchable nowPlaying={isSource ? undefined : true} data={data}/> : <PlayersContainer nowPlaying={isSource ? undefined : true} data={data} live={props.live}/>}</MSErrorBoundary>
|
||||
<MSErrorBoundary><ListContainerFilterable render="virtDynamic" componentType={data.mode} componentId={data.id}/></MSErrorBoundary>
|
||||
</Flex>
|
||||
|
|
|
|||
|
|
@ -80,8 +80,8 @@ export type ComponentCommonApi = {
|
|||
/** More specific, live activity state like "sleeping", "hydrating historical scrobbles", "processing dead scrobbles", etc... */
|
||||
status?: string
|
||||
players: Record<string, SourcePlayerJson>
|
||||
error?: ErrorIsh
|
||||
warning?: ErrorIsh
|
||||
errors?: ErrorIsh[]
|
||||
warnings?: ErrorIsh[]
|
||||
monitoringStatus?: MonitoringStatus
|
||||
} & Omit<ComponentMinimalSelect, 'type'>
|
||||
|
||||
|
|
|
|||
|
|
@ -808,6 +808,8 @@ export const NO_DEVICE = 'NoDevice';export const NO_USER = 'SingleUser';
|
|||
export const SINGLE_USER_PLATFORM_ID: PlayPlatformId = [NO_DEVICE, NO_USER];
|
||||
export const SINGLE_USER_PLATFORM_ID_STR = `${NO_DEVICE}-${NO_USER}`;
|
||||
|
||||
export type ComponentAuthType = 'none' | 'interactive' | 'unattended';
|
||||
|
||||
export type EmittedMSEvent<T = Record<string, any>, K = Record<string, any>,Y = ClientType | SourceType> = {
|
||||
type: Y
|
||||
name: string
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue