UNPKG

ng-commander

Version:

Command pattern for Angular applications

234 lines (229 loc) 9.38 kB
import * as i0 from '@angular/core'; import { computed, Injectable } from '@angular/core'; import { BehaviorSubject, Subject, switchMap, catchError, finalize, EMPTY } from 'rxjs'; var CommandEventType; (function (CommandEventType) { CommandEventType["START"] = "start"; CommandEventType["SUCCESS"] = "success"; CommandEventType["FAIL"] = "fail"; CommandEventType["RESTART"] = "restart"; CommandEventType["DEAD"] = "dead"; })(CommandEventType || (CommandEventType = {})); var CommanderState; (function (CommanderState) { CommanderState[CommanderState["IDLE"] = 0] = "IDLE"; CommanderState[CommanderState["EXECUTING"] = 1] = "EXECUTING"; CommanderState[CommanderState["DONE"] = 2] = "DONE"; CommanderState[CommanderState["ERROR"] = 3] = "ERROR"; })(CommanderState || (CommanderState = {})); var CommandsType; (function (CommandsType) { CommandsType[CommandsType["WAITING"] = 0] = "WAITING"; CommandsType[CommandsType["DONE"] = 1] = "DONE"; CommandsType[CommandsType["ERROR"] = 2] = "ERROR"; CommandsType[CommandsType["DEAD"] = 3] = "DEAD"; })(CommandsType || (CommandsType = {})); class Commander { configuration = { error: { maxNumberOfRetries: 3, }, }; commandsSubject = new BehaviorSubject([]); commandsDoneSubject = new BehaviorSubject([]); commandsInErrorSubject = new BehaviorSubject([]); commandsDeadSubject = new BehaviorSubject([]); stateSubject = new BehaviorSubject(CommanderState.IDLE); processingCommand = new Subject(); destroy$ = new Subject(); commands$ = this.commandsSubject.asObservable(); commandsDone$ = this.commandsDoneSubject.asObservable(); commandsInError$ = this.commandsInErrorSubject.asObservable(); commandsDead$ = this.commandsDeadSubject.asObservable(); state$ = this.stateSubject.asObservable(); // Signal properties for Angular 18+ compatibility commandsSignal = computed(() => this.commandsSubject.value, ...(ngDevMode ? [{ debugName: "commandsSignal" }] : [])); commandsDoneSignal = computed(() => this.commandsDoneSubject.value, ...(ngDevMode ? [{ debugName: "commandsDoneSignal" }] : [])); commandsInErrorSignal = computed(() => this.commandsInErrorSubject.value, ...(ngDevMode ? [{ debugName: "commandsInErrorSignal" }] : [])); commandsDeadSignal = computed(() => this.commandsDeadSubject.value, ...(ngDevMode ? [{ debugName: "commandsDeadSignal" }] : [])); stateSignal = computed(() => this.stateSubject.value, ...(ngDevMode ? [{ debugName: "stateSignal" }] : [])); constructor() { } ngOnDestroy() { this.destroy$.next(); this.destroy$.complete(); } init(configuration) { this.configuration = configuration; this.startCommander(); } startCommander() { this.processingCommand .pipe(switchMap(({ command }) => { // Update state to executing before starting execution this.stateSubject.next(CommanderState.EXECUTING); return command.execute().pipe( // Handle successful execution switchMap((result) => { return this.handleSuccessfulCommand(command, result); }), // Handle errors with retry logic catchError((error) => { return this.handleFailedCommand(command, error); }), // Finalize to ensure cleanup finalize(() => { this.executeNextCommand(); })); }), // Complete the stream when destroy$ emits finalize(() => { this.stateSubject.complete(); })) .subscribe({ error: (error) => { console.error('Commander error:', error); this.stateSubject.next(CommanderState.ERROR); }, }); } handleSuccessfulCommand(command, result) { const event = { type: CommandEventType.SUCCESS, timestamp: new Date(), command: command, }; if (!command.events) command.events = []; command.events.push(event); const currentErrors = this.commandsInErrorSubject.value; this.commandsInErrorSubject.next(currentErrors.filter(c => c !== command)); const currentDone = this.commandsDoneSubject.value; this.commandsDoneSubject.next([...currentDone, command]); // Update state to DONE only if there are no more commands this.stateSubject.next(this.commandsSubject.value.length > 0 ? CommanderState.EXECUTING : CommanderState.DONE); return EMPTY; } handleFailedCommand(command, error) { const event = { type: CommandEventType.FAIL, timestamp: new Date(), command: command, }; if (!command.events) command.events = []; command.events.push(event); const currentErrors = this.commandsInErrorSubject.value; this.commandsInErrorSubject.next([...currentErrors, command]); // Check if we should mark as dead this.trashDeadCommands(); return EMPTY; } stop() { this.stateSubject.complete(); } addCommand(command) { const currentCommands = this.commandsSubject.value; this.commandsSubject.next([...currentCommands, command]); // Start processing if this is the first command if (currentCommands.length === 0 && this.stateSubject.value === CommanderState.IDLE) { this.executeNextCommand(); } } replayCommandsInError() { const commandsInError = this.commandsInErrorSubject.value; this.commandsInErrorSubject.next([]); if (commandsInError.length > 0) { this.commandsSubject.next(commandsInError); // Only start execution if not already executing if (this.stateSubject.value === CommanderState.IDLE) { this.executeNextCommand(); } } } trashDeadCommands() { const inError = this.commandsInErrorSubject.value; // Check for commands that have exceeded max retries const deadCommands = []; const remainingCommands = []; inError.forEach((command) => { const events = command.events; // Count failure events (excluding restarts if they exist) const failCount = events?.filter((e) => e.type === CommandEventType.FAIL).length || 0; if (events && failCount > this.configuration.error.maxNumberOfRetries) { // Mark as dead const event = { type: CommandEventType.DEAD, timestamp: new Date(), command: command, }; if (!command.events) command.events = []; command.events.push(event); deadCommands.push(command); } else { remainingCommands.push(command); } }); if (deadCommands.length > 0) { this.commandsDeadSubject.next([ ...this.commandsDeadSubject.value, ...deadCommands, ]); } if (remainingCommands.length !== inError.length) { this.commandsInErrorSubject.next(remainingCommands); } } flushDeadCommands() { this.commandsDeadSubject.next([]); } getCommands(type) { switch (type) { case CommandsType.WAITING: return this.commandsSubject.value; case CommandsType.DONE: return this.commandsDoneSubject.value; case CommandsType.ERROR: return this.commandsInErrorSubject.value; case CommandsType.DEAD: return this.commandsDeadSubject.value; } } getState() { return this.stateSubject.value; } executeNextCommand() { const currentCommands = this.commandsSubject.value; if (currentCommands.length > 0) { const [command, ...rest] = currentCommands; this.commandsSubject.next(rest); // Update state to EXECUTING if (this.stateSubject.value !== CommanderState.EXECUTING) { this.stateSubject.next(CommanderState.EXECUTING); } // Process the command this.processingCommand.next({ command, result: null }); } else { // No more commands, set to IDLE this.stateSubject.next(CommanderState.IDLE); } } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: Commander, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: Commander, providedIn: 'root' }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: Commander, decorators: [{ type: Injectable, args: [{ providedIn: 'root', }] }], ctorParameters: () => [] }); /** * Generated bundle index. Do not edit. */ export { Commander }; //# sourceMappingURL=ng-commander.mjs.map