This commit is contained in:
2025-12-11 13:50:38 +01:00
parent e4c1be628d
commit c7e5de40d6
212 changed files with 2965 additions and 763 deletions

View File

@@ -1,4 +1,4 @@
import type { HostedSessionConfig } from '../../domain/entities/HostedSessionConfig';
import type { HostedSessionConfig } from '../../domain/types/HostedSessionConfig';
export interface SessionDTO {
sessionId: string;

View File

@@ -1,4 +1,4 @@
import { HostedSessionConfig } from '../../domain/entities/HostedSessionConfig';
import type { HostedSessionConfig } from '../../domain/types/HostedSessionConfig';
import { StepId } from '../../domain/value-objects/StepId';
import type { AutomationEngineValidationResultDTO } from '../dto/AutomationEngineValidationResultDTO';

View File

@@ -2,6 +2,7 @@ import { OverlaySyncPort, OverlayAction, ActionAck } from '../ports/OverlaySyncP
import { AutomationEventPublisherPort, AutomationEvent } from '../ports/AutomationEventPublisherPort';
import { IAutomationLifecycleEmitter, LifecycleCallback } from '../../infrastructure/adapters/IAutomationLifecycleEmitter';
import { LoggerPort } from '../ports/LoggerPort';
import type { IAsyncApplicationService } from '@gridpilot/shared/application';
type ConstructorArgs = {
lifecycleEmitter: IAutomationLifecycleEmitter
@@ -13,7 +14,9 @@ type ConstructorArgs = {
defaultTimeoutMs?: number
}
export class OverlaySyncService implements OverlaySyncPort {
export class OverlaySyncService
implements OverlaySyncPort, IAsyncApplicationService<OverlayAction, ActionAck>
{
private lifecycleEmitter: IAutomationLifecycleEmitter
private publisher: AutomationEventPublisherPort
private logger: LoggerPort
@@ -32,6 +35,10 @@ export class OverlaySyncService implements OverlaySyncPort {
this.defaultTimeoutMs = args.defaultTimeoutMs ?? 5000
}
async execute(action: OverlayAction): Promise<ActionAck> {
return this.startAction(action)
}
async startAction(action: OverlayAction): Promise<ActionAck> {
const timeoutMs = action.timeoutMs ?? this.defaultTimeoutMs
const seenEvents: AutomationEvent[] = []

View File

@@ -1,11 +1,13 @@
import type { AsyncUseCase } from '@gridpilot/shared/application';
import { AutomationSession } from '../../domain/entities/AutomationSession';
import { HostedSessionConfig } from '../../domain/entities/HostedSessionConfig';
import type { HostedSessionConfig } from '../../domain/types/HostedSessionConfig';
import { AutomationEnginePort } from '../ports/AutomationEnginePort';
import type { IBrowserAutomation } from '../ports/ScreenAutomationPort';
import { SessionRepositoryPort } from '../ports/SessionRepositoryPort';
import type { SessionDTO } from '../dto/SessionDTO';
export class StartAutomationSessionUseCase {
export class StartAutomationSessionUseCase
implements AsyncUseCase<HostedSessionConfig, SessionDTO> {
constructor(
private readonly automationEngine: AutomationEnginePort,
private readonly browserAutomation: IBrowserAutomation,

View File

@@ -1,10 +1,11 @@
import { randomUUID } from 'crypto';
import type { IEntity } from '@gridpilot/shared/domain';
import { StepId } from '../value-objects/StepId';
import { SessionState } from '../value-objects/SessionState';
import { HostedSessionConfig } from './HostedSessionConfig';
import type { HostedSessionConfig } from '../types/HostedSessionConfig';
import { AutomationDomainError } from '../errors/AutomationDomainError';
export class AutomationSession {
export class AutomationSession implements IEntity<string> {
private readonly _id: string;
private _currentStep: StepId;
private _state: SessionState;

View File

@@ -1,5 +1,11 @@
import { StepId } from '../value-objects/StepId';
import type { StepId } from '../value-objects/StepId';
/**
* Domain Type: StepExecution
*
* Represents execution metadata for a single automation step.
* This is a pure data shape (DTO-like), not an entity or value object.
*/
export interface StepExecution {
stepId: StepId;
startedAt: Date;

View File

@@ -1,8 +1,19 @@
export class AutomationDomainError extends Error {
readonly name: string = 'AutomationDomainError';
import type { IDomainError } from '@gridpilot/shared/errors';
constructor(message: string) {
/**
* Domain Error: AutomationDomainError
*
* Implements the shared IDomainError contract for automation domain failures.
*/
export class AutomationDomainError extends Error implements IDomainError {
readonly name = 'AutomationDomainError';
readonly type = 'domain' as const;
readonly context = 'automation';
readonly kind: string;
constructor(message: string, kind: string = 'validation') {
super(message);
this.kind = kind;
Object.setPrototypeOf(this, new.target.prototype);
}
}

View File

@@ -1,3 +1,4 @@
import type { IDomainValidationService } from '@gridpilot/shared/domain';
import { Result } from '../../../shared/result/Result';
/**
@@ -24,6 +25,12 @@ export interface PageStateValidationResult {
unexpectedSelectors?: string[];
}
export interface PageStateValidationInput {
actualState: (selector: string) => boolean;
validation: PageStateValidation;
realMode?: boolean;
}
/**
* Domain service for validating page state during wizard navigation.
*
@@ -32,7 +39,18 @@ export interface PageStateValidationResult {
* Clean Architecture: This is pure domain logic with no infrastructure dependencies.
* It validates state based on selector presence/absence without knowing HOW to check them.
*/
export class PageStateValidator {
export class PageStateValidator
implements
IDomainValidationService<PageStateValidationInput, PageStateValidationResult, Error>
{
validate(input: PageStateValidationInput): Result<PageStateValidationResult, Error> {
const { actualState, validation, realMode } = input;
if (typeof realMode === 'boolean') {
return this.validateStateEnhanced(actualState, validation, realMode);
}
return this.validateState(actualState, validation);
}
/**
* Validate that the page state matches expected conditions.
*

View File

@@ -1,11 +1,21 @@
import { StepId } from '../value-objects/StepId';
import { SessionState } from '../value-objects/SessionState';
import type { IDomainValidationService } from '@gridpilot/shared/domain';
import { Result } from '../../../shared/result/Result';
export interface ValidationResult {
isValid: boolean;
error?: string;
}
export interface StepTransitionValidationInput {
currentStep: StepId;
nextStep: StepId;
state: SessionState;
}
export interface StepTransitionValidationResult extends ValidationResult {}
const STEP_DESCRIPTIONS: Record<number, string> = {
1: 'Navigate to Hosted Racing page',
2: 'Click Create a Race',
@@ -26,7 +36,23 @@ const STEP_DESCRIPTIONS: Record<number, string> = {
17: 'Track Conditions (STOP - Manual Submit Required)',
};
export class StepTransitionValidator {
export class StepTransitionValidator
implements
IDomainValidationService<StepTransitionValidationInput, StepTransitionValidationResult, Error>
{
validate(input: StepTransitionValidationInput): Result<StepTransitionValidationResult, Error> {
try {
const { currentStep, nextStep, state } = input;
const result = StepTransitionValidator.canTransition(currentStep, nextStep, state);
return Result.ok(result);
} catch (error) {
return Result.err(
error instanceof Error
? error
: new Error(`Step transition validation failed: ${String(error)}`),
);
}
}
static canTransition(
currentStep: StepId,
nextStep: StepId,

View File

@@ -1,3 +1,9 @@
/**
* Domain Type: HostedSessionConfig
*
* Pure configuration shape for an iRacing hosted session.
* This is a DTO-like domain type, not a value object or entity.
*/
export interface HostedSessionConfig {
sessionName: string;
trackId: string;

View File

@@ -0,0 +1,88 @@
/**
* Domain Types: ScreenRegion, Point, ElementLocation, LoginDetectionResult
*
* These are pure data shapes and helpers used across automation.
*/
export interface ScreenRegion {
x: number;
y: number;
width: number;
height: number;
}
/**
* Represents a point on the screen with x,y coordinates.
*/
export interface Point {
x: number;
y: number;
}
/**
* Represents the location of a detected UI element on screen.
* Contains the center point, bounding box, and confidence score.
*/
export interface ElementLocation {
center: Point;
bounds: ScreenRegion;
confidence: number;
}
/**
* Result of login state detection via screen recognition.
*/
export interface LoginDetectionResult {
isLoggedIn: boolean;
confidence: number;
detectedIndicators: string[];
error?: string;
}
/**
* Create a ScreenRegion from coordinates.
*/
export function createScreenRegion(x: number, y: number, width: number, height: number): ScreenRegion {
return { x, y, width, height };
}
/**
* Create a Point from coordinates.
*/
export function createPoint(x: number, y: number): Point {
return { x, y };
}
/**
* Calculate the center point of a ScreenRegion.
*/
export function getRegionCenter(region: ScreenRegion): Point {
return {
x: region.x + Math.floor(region.width / 2),
y: region.y + Math.floor(region.height / 2),
};
}
/**
* Check if a point is within a screen region.
*/
export function isPointInRegion(point: Point, region: ScreenRegion): boolean {
return (
point.x >= region.x &&
point.x <= region.x + region.width &&
point.y >= region.y &&
point.y <= region.y + region.height
);
}
/**
* Check if two screen regions overlap.
*/
export function regionsOverlap(a: ScreenRegion, b: ScreenRegion): boolean {
return !(
a.x + a.width < b.x ||
b.x + b.width < a.x ||
a.y + a.height < b.y ||
b.y + b.height < a.y
);
}

View File

@@ -1,6 +1,12 @@
import { AuthenticationState } from './AuthenticationState';
import type { IValueObject } from '@gridpilot/shared/domain';
export class BrowserAuthenticationState {
export interface BrowserAuthenticationStateProps {
cookiesValid: boolean;
pageAuthenticated: boolean;
}
export class BrowserAuthenticationState implements IValueObject<BrowserAuthenticationStateProps> {
private readonly cookiesValid: boolean;
private readonly pageAuthenticated: boolean;
@@ -36,4 +42,17 @@ export class BrowserAuthenticationState {
getPageAuthenticationStatus(): boolean {
return this.pageAuthenticated;
}
get props(): BrowserAuthenticationStateProps {
return {
cookiesValid: this.cookiesValid,
pageAuthenticated: this.pageAuthenticated,
};
}
equals(other: IValueObject<BrowserAuthenticationStateProps>): boolean {
const a = this.props;
const b = other.props;
return a.cookiesValid === b.cookiesValid && a.pageAuthenticated === b.pageAuthenticated;
}
}

View File

@@ -1,4 +1,10 @@
export class CheckoutPrice {
import type { IValueObject } from '@gridpilot/shared/domain';
export interface CheckoutPriceProps {
amountUsd: number;
}
export class CheckoutPrice implements IValueObject<CheckoutPriceProps> {
private constructor(private readonly amountUsd: number) {
if (amountUsd < 0) {
throw new Error('Price cannot be negative');
@@ -54,4 +60,14 @@ export class CheckoutPrice {
isZero(): boolean {
return this.amountUsd < 0.001;
}
get props(): CheckoutPriceProps {
return {
amountUsd: this.amountUsd,
};
}
equals(other: IValueObject<CheckoutPriceProps>): boolean {
return this.props.amountUsd === other.props.amountUsd;
}
}

View File

@@ -4,7 +4,14 @@
* Represents the lifetime of an authentication session with expiry tracking.
* Handles validation of session expiry dates with a configurable buffer window.
*/
export class SessionLifetime {
import type { IValueObject } from '@gridpilot/shared/domain';
export interface SessionLifetimeProps {
expiry: Date | null;
bufferMinutes: number;
}
export class SessionLifetime implements IValueObject<SessionLifetimeProps> {
private readonly expiry: Date | null;
private readonly bufferMinutes: number;
@@ -78,8 +85,23 @@ export class SessionLifetime {
if (this.expiry === null) {
return Infinity;
}
const remaining = this.expiry.getTime() - Date.now();
return Math.max(0, remaining);
}
get props(): SessionLifetimeProps {
return {
expiry: this.expiry,
bufferMinutes: this.bufferMinutes,
};
}
equals(other: IValueObject<SessionLifetimeProps>): boolean {
const a = this.props;
const b = other.props;
const aExpiry = a.expiry?.getTime() ?? null;
const bExpiry = b.expiry?.getTime() ?? null;
return aExpiry === bExpiry && a.bufferMinutes === b.bufferMinutes;
}
}

View File

@@ -1,3 +1,5 @@
import type { IValueObject } from '@gridpilot/shared/domain';
export type SessionStateValue =
| 'PENDING'
| 'IN_PROGRESS'
@@ -30,7 +32,11 @@ const VALID_TRANSITIONS: Record<SessionStateValue, SessionStateValue[]> = {
CANCELLED: [],
};
export class SessionState {
export interface SessionStateProps {
value: SessionStateValue;
}
export class SessionState implements IValueObject<SessionStateProps> {
private readonly _value: SessionStateValue;
private constructor(value: SessionStateValue) {
@@ -93,4 +99,12 @@ export class SessionState {
this._value === 'CANCELLED'
);
}
get props(): SessionStateProps {
return { value: this._value };
}
equals(other: IValueObject<SessionStateProps>): boolean {
return this.props.value === other.props.value;
}
}

View File

@@ -1,4 +1,10 @@
export class StepId {
import type { IValueObject } from '@gridpilot/shared/domain';
export interface StepIdProps {
value: number;
}
export class StepId implements IValueObject<StepIdProps> {
private readonly _value: number;
private constructor(value: number) {
@@ -37,4 +43,12 @@ export class StepId {
}
return StepId.create(this._value + 1);
}
get props(): StepIdProps {
return { value: this._value };
}
equals(other: IValueObject<StepIdProps>): boolean {
return this.props.value === other.props.value;
}
}

View File

@@ -10,9 +10,9 @@ export * from './domain/value-objects/ScreenRegion';
export * from './domain/value-objects/SessionLifetime';
export * from './domain/value-objects/SessionState';
export * from './domain/entities/HostedSessionConfig';
export * from './domain/entities/StepExecution';
export * from './domain/entities/AutomationSession';
export * from './domain/types/HostedSessionConfig';
export * from './domain/entities/StepExecution';
export * from './domain/services/PageStateValidator';
export * from './domain/services/StepTransitionValidator';

View File

@@ -1,5 +1,5 @@
import type { AutomationEnginePort } from '../../../../application/ports/AutomationEnginePort';
import { HostedSessionConfig } from '../../../../domain/entities/HostedSessionConfig';
import type { HostedSessionConfig } from '../../../../domain/types/HostedSessionConfig';
import { StepId } from '../../../../domain/value-objects/StepId';
import type { IBrowserAutomation } from '../../../../application/ports/ScreenAutomationPort';
import type { SessionRepositoryPort } from '../../../../application/ports/SessionRepositoryPort';

View File

@@ -1,5 +1,5 @@
import type { AutomationEnginePort } from '../../../../application/ports/AutomationEnginePort';
import { HostedSessionConfig } from '../../../../domain/entities/HostedSessionConfig';
import type { HostedSessionConfig } from '../../../../domain/types/HostedSessionConfig';
import { StepId } from '../../../../domain/value-objects/StepId';
import type { IBrowserAutomation } from '../../../../application/ports/ScreenAutomationPort';
import type { SessionRepositoryPort } from '../../../../application/ports/SessionRepositoryPort';