ng-commander
Version:
Command pattern for Angular applications
234 lines (229 loc) • 9.38 kB
JavaScript
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