feat(companion): implement hosted session automation POC with TDD approach
This commit is contained in:
@@ -0,0 +1,172 @@
|
||||
import { StepId } from '../../../packages/domain/value-objects/StepId';
|
||||
import { HostedSessionConfig } from '../../../packages/domain/entities/HostedSessionConfig';
|
||||
import { IBrowserAutomation } from '../../../packages/application/ports/IBrowserAutomation';
|
||||
|
||||
interface MockConfig {
|
||||
simulateFailures?: boolean;
|
||||
failureRate?: number;
|
||||
}
|
||||
|
||||
interface StepExecutionResult {
|
||||
success: boolean;
|
||||
stepId: number;
|
||||
wasModalStep?: boolean;
|
||||
shouldStop?: boolean;
|
||||
executionTime: number;
|
||||
metrics: {
|
||||
totalDelay: number;
|
||||
operationCount: number;
|
||||
};
|
||||
}
|
||||
|
||||
interface NavigationResult {
|
||||
success: boolean;
|
||||
url: string;
|
||||
simulatedDelay: number;
|
||||
}
|
||||
|
||||
interface FormFillResult {
|
||||
success: boolean;
|
||||
fieldName: string;
|
||||
value: string;
|
||||
simulatedDelay: number;
|
||||
}
|
||||
|
||||
interface ClickResult {
|
||||
success: boolean;
|
||||
selector: string;
|
||||
simulatedDelay: number;
|
||||
}
|
||||
|
||||
interface WaitResult {
|
||||
success: boolean;
|
||||
selector: string;
|
||||
simulatedDelay: number;
|
||||
}
|
||||
|
||||
interface ModalResult {
|
||||
success: boolean;
|
||||
stepId: number;
|
||||
action: string;
|
||||
simulatedDelay: number;
|
||||
}
|
||||
|
||||
export class MockBrowserAutomationAdapter implements IBrowserAutomation {
|
||||
private config: MockConfig;
|
||||
|
||||
constructor(config: MockConfig = {}) {
|
||||
this.config = {
|
||||
simulateFailures: config.simulateFailures ?? false,
|
||||
failureRate: config.failureRate ?? 0.1,
|
||||
};
|
||||
}
|
||||
|
||||
async navigateToPage(url: string): Promise<NavigationResult> {
|
||||
const delay = this.randomDelay(200, 800);
|
||||
await this.sleep(delay);
|
||||
return {
|
||||
success: true,
|
||||
url,
|
||||
simulatedDelay: delay,
|
||||
};
|
||||
}
|
||||
|
||||
async fillFormField(fieldName: string, value: string): Promise<FormFillResult> {
|
||||
const delay = this.randomDelay(100, 500);
|
||||
await this.sleep(delay);
|
||||
return {
|
||||
success: true,
|
||||
fieldName,
|
||||
value,
|
||||
simulatedDelay: delay,
|
||||
};
|
||||
}
|
||||
|
||||
async clickElement(selector: string): Promise<ClickResult> {
|
||||
const delay = this.randomDelay(50, 300);
|
||||
await this.sleep(delay);
|
||||
return {
|
||||
success: true,
|
||||
selector,
|
||||
simulatedDelay: delay,
|
||||
};
|
||||
}
|
||||
|
||||
async waitForElement(selector: string, maxWaitMs: number = 5000): Promise<WaitResult> {
|
||||
const delay = this.randomDelay(100, 1000);
|
||||
|
||||
await this.sleep(delay);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
selector,
|
||||
simulatedDelay: delay,
|
||||
};
|
||||
}
|
||||
|
||||
async handleModal(stepId: StepId, action: string): Promise<ModalResult> {
|
||||
if (!stepId.isModalStep()) {
|
||||
throw new Error(`Step ${stepId.value} is not a modal step`);
|
||||
}
|
||||
|
||||
const delay = this.randomDelay(200, 600);
|
||||
await this.sleep(delay);
|
||||
return {
|
||||
success: true,
|
||||
stepId: stepId.value,
|
||||
action,
|
||||
simulatedDelay: delay,
|
||||
};
|
||||
}
|
||||
|
||||
async executeStep(stepId: StepId, config: HostedSessionConfig): Promise<StepExecutionResult> {
|
||||
if (this.shouldSimulateFailure()) {
|
||||
throw new Error(`Simulated failure at step ${stepId.value}`);
|
||||
}
|
||||
|
||||
const startTime = Date.now();
|
||||
let totalDelay = 0;
|
||||
let operationCount = 0;
|
||||
|
||||
const navigationDelay = this.randomDelay(200, 500);
|
||||
await this.sleep(navigationDelay);
|
||||
totalDelay += navigationDelay;
|
||||
operationCount++;
|
||||
|
||||
if (stepId.isModalStep()) {
|
||||
const modalDelay = this.randomDelay(200, 400);
|
||||
await this.sleep(modalDelay);
|
||||
totalDelay += modalDelay;
|
||||
operationCount++;
|
||||
}
|
||||
|
||||
const executionTime = Date.now() - startTime;
|
||||
|
||||
return {
|
||||
success: true,
|
||||
stepId: stepId.value,
|
||||
wasModalStep: stepId.isModalStep(),
|
||||
shouldStop: stepId.isFinalStep(),
|
||||
executionTime,
|
||||
metrics: {
|
||||
totalDelay,
|
||||
operationCount,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private randomDelay(min: number, max: number): number {
|
||||
return Math.floor(Math.random() * (max - min + 1)) + min;
|
||||
}
|
||||
|
||||
private async sleep(ms: number): Promise<void> {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
private shouldSimulateFailure(): boolean {
|
||||
if (!this.config.simulateFailures) {
|
||||
return false;
|
||||
}
|
||||
return Math.random() < (this.config.failureRate || 0.1);
|
||||
}
|
||||
}
|
||||
36
src/infrastructure/repositories/InMemorySessionRepository.ts
Normal file
36
src/infrastructure/repositories/InMemorySessionRepository.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { AutomationSession } from '../../packages/domain/entities/AutomationSession';
|
||||
import { SessionStateValue } from '../../packages/domain/value-objects/SessionState';
|
||||
import { ISessionRepository } from '../../packages/application/ports/ISessionRepository';
|
||||
|
||||
export class InMemorySessionRepository implements ISessionRepository {
|
||||
private sessions: Map<string, AutomationSession> = new Map();
|
||||
|
||||
async save(session: AutomationSession): Promise<void> {
|
||||
this.sessions.set(session.id, session);
|
||||
}
|
||||
|
||||
async findById(id: string): Promise<AutomationSession | null> {
|
||||
return this.sessions.get(id) || null;
|
||||
}
|
||||
|
||||
async update(session: AutomationSession): Promise<void> {
|
||||
if (!this.sessions.has(session.id)) {
|
||||
throw new Error('Session not found');
|
||||
}
|
||||
this.sessions.set(session.id, session);
|
||||
}
|
||||
|
||||
async delete(id: string): Promise<void> {
|
||||
this.sessions.delete(id);
|
||||
}
|
||||
|
||||
async findAll(): Promise<AutomationSession[]> {
|
||||
return Array.from(this.sessions.values());
|
||||
}
|
||||
|
||||
async findByState(state: SessionStateValue): Promise<AutomationSession[]> {
|
||||
return Array.from(this.sessions.values()).filter(
|
||||
session => session.state.value === state
|
||||
);
|
||||
}
|
||||
}
|
||||
12
src/packages/application/ports/IAutomationEngine.ts
Normal file
12
src/packages/application/ports/IAutomationEngine.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { HostedSessionConfig } from '../../domain/entities/HostedSessionConfig';
|
||||
import { StepId } from '../../domain/value-objects/StepId';
|
||||
|
||||
export interface ValidationResult {
|
||||
isValid: boolean;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface IAutomationEngine {
|
||||
validateConfiguration(config: HostedSessionConfig): Promise<ValidationResult>;
|
||||
executeStep(stepId: StepId, config: HostedSessionConfig): Promise<void>;
|
||||
}
|
||||
9
src/packages/application/ports/IBrowserAutomation.ts
Normal file
9
src/packages/application/ports/IBrowserAutomation.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { StepId } from '../../domain/value-objects/StepId';
|
||||
|
||||
export interface IBrowserAutomation {
|
||||
navigateToPage(url: string): Promise<any>;
|
||||
fillFormField(fieldName: string, value: string): Promise<any>;
|
||||
clickElement(selector: string): Promise<any>;
|
||||
waitForElement(selector: string, maxWaitMs?: number): Promise<any>;
|
||||
handleModal(stepId: StepId, action: string): Promise<any>;
|
||||
}
|
||||
11
src/packages/application/ports/ISessionRepository.ts
Normal file
11
src/packages/application/ports/ISessionRepository.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { AutomationSession } from '../../domain/entities/AutomationSession';
|
||||
import { SessionStateValue } from '../../domain/value-objects/SessionState';
|
||||
|
||||
export interface ISessionRepository {
|
||||
save(session: AutomationSession): Promise<void>;
|
||||
findById(id: string): Promise<AutomationSession | null>;
|
||||
update(session: AutomationSession): Promise<void>;
|
||||
delete(id: string): Promise<void>;
|
||||
findAll(): Promise<AutomationSession[]>;
|
||||
findByState(state: SessionStateValue): Promise<AutomationSession[]>;
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import { AutomationSession } from '../../domain/entities/AutomationSession';
|
||||
import { HostedSessionConfig } from '../../domain/entities/HostedSessionConfig';
|
||||
import { IAutomationEngine } from '../ports/IAutomationEngine';
|
||||
import { IBrowserAutomation } from '../ports/IBrowserAutomation';
|
||||
import { ISessionRepository } from '../ports/ISessionRepository';
|
||||
|
||||
export interface SessionDTO {
|
||||
sessionId: string;
|
||||
state: string;
|
||||
currentStep: number;
|
||||
config: HostedSessionConfig;
|
||||
startedAt?: Date;
|
||||
completedAt?: Date;
|
||||
errorMessage?: string;
|
||||
}
|
||||
|
||||
export class StartAutomationSessionUseCase {
|
||||
constructor(
|
||||
private readonly automationEngine: IAutomationEngine,
|
||||
private readonly browserAutomation: IBrowserAutomation,
|
||||
private readonly sessionRepository: ISessionRepository
|
||||
) {}
|
||||
|
||||
async execute(config: HostedSessionConfig): Promise<SessionDTO> {
|
||||
const session = AutomationSession.create(config);
|
||||
|
||||
const validationResult = await this.automationEngine.validateConfiguration(config);
|
||||
if (!validationResult.isValid) {
|
||||
throw new Error(validationResult.error);
|
||||
}
|
||||
|
||||
await this.sessionRepository.save(session);
|
||||
|
||||
return {
|
||||
sessionId: session.id,
|
||||
state: session.state.value,
|
||||
currentStep: session.currentStep.value,
|
||||
config: session.config,
|
||||
startedAt: session.startedAt,
|
||||
completedAt: session.completedAt,
|
||||
errorMessage: session.errorMessage,
|
||||
};
|
||||
}
|
||||
}
|
||||
143
src/packages/domain/entities/AutomationSession.ts
Normal file
143
src/packages/domain/entities/AutomationSession.ts
Normal file
@@ -0,0 +1,143 @@
|
||||
import { randomUUID } from 'crypto';
|
||||
import { StepId } from '../value-objects/StepId';
|
||||
import { SessionState } from '../value-objects/SessionState';
|
||||
import { HostedSessionConfig } from './HostedSessionConfig';
|
||||
|
||||
export class AutomationSession {
|
||||
private readonly _id: string;
|
||||
private _currentStep: StepId;
|
||||
private _state: SessionState;
|
||||
private readonly _config: HostedSessionConfig;
|
||||
private _startedAt?: Date;
|
||||
private _completedAt?: Date;
|
||||
private _errorMessage?: string;
|
||||
|
||||
private constructor(
|
||||
id: string,
|
||||
currentStep: StepId,
|
||||
state: SessionState,
|
||||
config: HostedSessionConfig
|
||||
) {
|
||||
this._id = id;
|
||||
this._currentStep = currentStep;
|
||||
this._state = state;
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
static create(config: HostedSessionConfig): AutomationSession {
|
||||
if (!config.sessionName || config.sessionName.trim() === '') {
|
||||
throw new Error('Session name cannot be empty');
|
||||
}
|
||||
if (!config.trackId || config.trackId.trim() === '') {
|
||||
throw new Error('Track ID is required');
|
||||
}
|
||||
if (!config.carIds || config.carIds.length === 0) {
|
||||
throw new Error('At least one car must be selected');
|
||||
}
|
||||
|
||||
return new AutomationSession(
|
||||
randomUUID(),
|
||||
StepId.create(1),
|
||||
SessionState.create('PENDING'),
|
||||
config
|
||||
);
|
||||
}
|
||||
|
||||
get id(): string {
|
||||
return this._id;
|
||||
}
|
||||
|
||||
get currentStep(): StepId {
|
||||
return this._currentStep;
|
||||
}
|
||||
|
||||
get state(): SessionState {
|
||||
return this._state;
|
||||
}
|
||||
|
||||
get config(): HostedSessionConfig {
|
||||
return this._config;
|
||||
}
|
||||
|
||||
get startedAt(): Date | undefined {
|
||||
return this._startedAt;
|
||||
}
|
||||
|
||||
get completedAt(): Date | undefined {
|
||||
return this._completedAt;
|
||||
}
|
||||
|
||||
get errorMessage(): string | undefined {
|
||||
return this._errorMessage;
|
||||
}
|
||||
|
||||
start(): void {
|
||||
if (!this._state.isPending()) {
|
||||
throw new Error('Cannot start session that is not pending');
|
||||
}
|
||||
this._state = SessionState.create('IN_PROGRESS');
|
||||
this._startedAt = new Date();
|
||||
}
|
||||
|
||||
transitionToStep(targetStep: StepId): void {
|
||||
if (!this._state.isInProgress()) {
|
||||
throw new Error('Cannot transition steps when session is not in progress');
|
||||
}
|
||||
|
||||
if (this._currentStep.equals(targetStep)) {
|
||||
throw new Error('Already at this step');
|
||||
}
|
||||
|
||||
if (targetStep.value < this._currentStep.value) {
|
||||
throw new Error('Cannot move backward - steps must progress forward only');
|
||||
}
|
||||
|
||||
if (targetStep.value !== this._currentStep.value + 1) {
|
||||
throw new Error('Cannot skip steps - must transition sequentially');
|
||||
}
|
||||
|
||||
this._currentStep = targetStep;
|
||||
|
||||
if (this._currentStep.isFinalStep()) {
|
||||
this._state = SessionState.create('STOPPED_AT_STEP_18');
|
||||
this._completedAt = new Date();
|
||||
}
|
||||
}
|
||||
|
||||
pause(): void {
|
||||
if (!this._state.isInProgress()) {
|
||||
throw new Error('Cannot pause session that is not in progress');
|
||||
}
|
||||
this._state = SessionState.create('PAUSED');
|
||||
}
|
||||
|
||||
resume(): void {
|
||||
if (this._state.value !== 'PAUSED') {
|
||||
throw new Error('Cannot resume session that is not paused');
|
||||
}
|
||||
this._state = SessionState.create('IN_PROGRESS');
|
||||
}
|
||||
|
||||
fail(errorMessage: string): void {
|
||||
if (this._state.isTerminal()) {
|
||||
throw new Error('Cannot fail terminal session');
|
||||
}
|
||||
this._state = SessionState.create('FAILED');
|
||||
this._errorMessage = errorMessage;
|
||||
this._completedAt = new Date();
|
||||
}
|
||||
|
||||
isAtModalStep(): boolean {
|
||||
return this._currentStep.isModalStep();
|
||||
}
|
||||
|
||||
getElapsedTime(): number {
|
||||
if (!this._startedAt) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const endTime = this._completedAt || new Date();
|
||||
const elapsed = endTime.getTime() - this._startedAt.getTime();
|
||||
return elapsed > 0 ? elapsed : 1;
|
||||
}
|
||||
}
|
||||
5
src/packages/domain/entities/HostedSessionConfig.ts
Normal file
5
src/packages/domain/entities/HostedSessionConfig.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export interface HostedSessionConfig {
|
||||
sessionName: string;
|
||||
trackId: string;
|
||||
carIds: string[];
|
||||
}
|
||||
9
src/packages/domain/entities/StepExecution.ts
Normal file
9
src/packages/domain/entities/StepExecution.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { StepId } from '../value-objects/StepId';
|
||||
|
||||
export interface StepExecution {
|
||||
stepId: StepId;
|
||||
startedAt: Date;
|
||||
completedAt?: Date;
|
||||
success: boolean;
|
||||
error?: string;
|
||||
}
|
||||
81
src/packages/domain/services/StepTransitionValidator.ts
Normal file
81
src/packages/domain/services/StepTransitionValidator.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import { StepId } from '../value-objects/StepId';
|
||||
import { SessionState } from '../value-objects/SessionState';
|
||||
|
||||
export interface ValidationResult {
|
||||
isValid: boolean;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
const STEP_DESCRIPTIONS: Record<number, string> = {
|
||||
1: 'Navigate to Hosted Racing page',
|
||||
2: 'Click Create a Race',
|
||||
3: 'Fill Race Information',
|
||||
4: 'Configure Server Details',
|
||||
5: 'Set Admins',
|
||||
6: 'Add Admin (Modal)',
|
||||
7: 'Set Time Limits',
|
||||
8: 'Set Cars',
|
||||
9: 'Add a Car (Modal)',
|
||||
10: 'Set Car Classes',
|
||||
11: 'Set Track',
|
||||
12: 'Add a Track (Modal)',
|
||||
13: 'Configure Track Options',
|
||||
14: 'Set Time of Day',
|
||||
15: 'Configure Weather',
|
||||
16: 'Set Race Options',
|
||||
17: 'Configure Team Driving',
|
||||
18: 'Track Conditions (STOP - Manual Submit Required)',
|
||||
};
|
||||
|
||||
export class StepTransitionValidator {
|
||||
static canTransition(
|
||||
currentStep: StepId,
|
||||
nextStep: StepId,
|
||||
state: SessionState
|
||||
): ValidationResult {
|
||||
if (!state.isInProgress()) {
|
||||
return {
|
||||
isValid: false,
|
||||
error: 'Session must be in progress to transition steps',
|
||||
};
|
||||
}
|
||||
|
||||
if (currentStep.equals(nextStep)) {
|
||||
return {
|
||||
isValid: false,
|
||||
error: 'Already at this step',
|
||||
};
|
||||
}
|
||||
|
||||
if (nextStep.value < currentStep.value) {
|
||||
return {
|
||||
isValid: false,
|
||||
error: 'Cannot move backward - steps must progress forward only',
|
||||
};
|
||||
}
|
||||
|
||||
if (nextStep.value !== currentStep.value + 1) {
|
||||
return {
|
||||
isValid: false,
|
||||
error: 'Cannot skip steps - must progress sequentially',
|
||||
};
|
||||
}
|
||||
|
||||
return { isValid: true };
|
||||
}
|
||||
|
||||
static validateModalStepTransition(
|
||||
currentStep: StepId,
|
||||
nextStep: StepId
|
||||
): ValidationResult {
|
||||
return { isValid: true };
|
||||
}
|
||||
|
||||
static shouldStopAtStep18(nextStep: StepId): boolean {
|
||||
return nextStep.isFinalStep();
|
||||
}
|
||||
|
||||
static getStepDescription(step: StepId): string {
|
||||
return STEP_DESCRIPTIONS[step.value] || `Step ${step.value}`;
|
||||
}
|
||||
}
|
||||
81
src/packages/domain/value-objects/SessionState.ts
Normal file
81
src/packages/domain/value-objects/SessionState.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
export type SessionStateValue =
|
||||
| 'PENDING'
|
||||
| 'IN_PROGRESS'
|
||||
| 'PAUSED'
|
||||
| 'COMPLETED'
|
||||
| 'FAILED'
|
||||
| 'STOPPED_AT_STEP_18';
|
||||
|
||||
const VALID_STATES: SessionStateValue[] = [
|
||||
'PENDING',
|
||||
'IN_PROGRESS',
|
||||
'PAUSED',
|
||||
'COMPLETED',
|
||||
'FAILED',
|
||||
'STOPPED_AT_STEP_18',
|
||||
];
|
||||
|
||||
const VALID_TRANSITIONS: Record<SessionStateValue, SessionStateValue[]> = {
|
||||
PENDING: ['IN_PROGRESS', 'FAILED'],
|
||||
IN_PROGRESS: ['PAUSED', 'COMPLETED', 'FAILED', 'STOPPED_AT_STEP_18'],
|
||||
PAUSED: ['IN_PROGRESS', 'FAILED'],
|
||||
COMPLETED: [],
|
||||
FAILED: [],
|
||||
STOPPED_AT_STEP_18: [],
|
||||
};
|
||||
|
||||
export class SessionState {
|
||||
private readonly _value: SessionStateValue;
|
||||
|
||||
private constructor(value: SessionStateValue) {
|
||||
this._value = value;
|
||||
}
|
||||
|
||||
static create(value: SessionStateValue): SessionState {
|
||||
if (!VALID_STATES.includes(value)) {
|
||||
throw new Error('Invalid session state');
|
||||
}
|
||||
return new SessionState(value);
|
||||
}
|
||||
|
||||
get value(): SessionStateValue {
|
||||
return this._value;
|
||||
}
|
||||
|
||||
equals(other: SessionState): boolean {
|
||||
return this._value === other._value;
|
||||
}
|
||||
|
||||
isPending(): boolean {
|
||||
return this._value === 'PENDING';
|
||||
}
|
||||
|
||||
isInProgress(): boolean {
|
||||
return this._value === 'IN_PROGRESS';
|
||||
}
|
||||
|
||||
isCompleted(): boolean {
|
||||
return this._value === 'COMPLETED';
|
||||
}
|
||||
|
||||
isFailed(): boolean {
|
||||
return this._value === 'FAILED';
|
||||
}
|
||||
|
||||
isStoppedAtStep18(): boolean {
|
||||
return this._value === 'STOPPED_AT_STEP_18';
|
||||
}
|
||||
|
||||
canTransitionTo(targetState: SessionState): boolean {
|
||||
const allowedTransitions = VALID_TRANSITIONS[this._value];
|
||||
return allowedTransitions.includes(targetState._value);
|
||||
}
|
||||
|
||||
isTerminal(): boolean {
|
||||
return (
|
||||
this._value === 'COMPLETED' ||
|
||||
this._value === 'FAILED' ||
|
||||
this._value === 'STOPPED_AT_STEP_18'
|
||||
);
|
||||
}
|
||||
}
|
||||
40
src/packages/domain/value-objects/StepId.ts
Normal file
40
src/packages/domain/value-objects/StepId.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
export class StepId {
|
||||
private readonly _value: number;
|
||||
|
||||
private constructor(value: number) {
|
||||
this._value = value;
|
||||
}
|
||||
|
||||
static create(value: number): StepId {
|
||||
if (!Number.isInteger(value)) {
|
||||
throw new Error('StepId must be an integer');
|
||||
}
|
||||
if (value < 1 || value > 18) {
|
||||
throw new Error('StepId must be between 1 and 18');
|
||||
}
|
||||
return new StepId(value);
|
||||
}
|
||||
|
||||
get value(): number {
|
||||
return this._value;
|
||||
}
|
||||
|
||||
equals(other: StepId): boolean {
|
||||
return this._value === other._value;
|
||||
}
|
||||
|
||||
isModalStep(): boolean {
|
||||
return this._value === 6 || this._value === 9 || this._value === 12;
|
||||
}
|
||||
|
||||
isFinalStep(): boolean {
|
||||
return this._value === 18;
|
||||
}
|
||||
|
||||
next(): StepId {
|
||||
if (this.isFinalStep()) {
|
||||
throw new Error('Cannot advance beyond final step');
|
||||
}
|
||||
return StepId.create(this._value + 1);
|
||||
}
|
||||
}
|
||||
62
src/packages/shared/result/Result.ts
Normal file
62
src/packages/shared/result/Result.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
export class Result<T, E = Error> {
|
||||
private constructor(
|
||||
private readonly _value?: T,
|
||||
private readonly _error?: E,
|
||||
private readonly _isSuccess: boolean = true
|
||||
) {}
|
||||
|
||||
static ok<T, E = Error>(value: T): Result<T, E> {
|
||||
return new Result<T, E>(value, undefined, true);
|
||||
}
|
||||
|
||||
static err<T, E = Error>(error: E): Result<T, E> {
|
||||
return new Result<T, E>(undefined, error, false);
|
||||
}
|
||||
|
||||
isOk(): boolean {
|
||||
return this._isSuccess;
|
||||
}
|
||||
|
||||
isErr(): boolean {
|
||||
return !this._isSuccess;
|
||||
}
|
||||
|
||||
unwrap(): T {
|
||||
if (!this._isSuccess) {
|
||||
throw new Error('Called unwrap on an error result');
|
||||
}
|
||||
return this._value!;
|
||||
}
|
||||
|
||||
unwrapOr(defaultValue: T): T {
|
||||
return this._isSuccess ? this._value! : defaultValue;
|
||||
}
|
||||
|
||||
unwrapErr(): E {
|
||||
if (this._isSuccess) {
|
||||
throw new Error('Called unwrapErr on a success result');
|
||||
}
|
||||
return this._error!;
|
||||
}
|
||||
|
||||
map<U>(fn: (value: T) => U): Result<U, E> {
|
||||
if (this._isSuccess) {
|
||||
return Result.ok(fn(this._value!));
|
||||
}
|
||||
return Result.err(this._error!);
|
||||
}
|
||||
|
||||
mapErr<F>(fn: (error: E) => F): Result<T, F> {
|
||||
if (!this._isSuccess) {
|
||||
return Result.err(fn(this._error!));
|
||||
}
|
||||
return Result.ok(this._value!);
|
||||
}
|
||||
|
||||
andThen<U>(fn: (value: T) => Result<U, E>): Result<U, E> {
|
||||
if (this._isSuccess) {
|
||||
return fn(this._value!);
|
||||
}
|
||||
return Result.err(this._error!);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user