wip
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import type { HostedSessionConfig } from '../../domain/entities/HostedSessionConfig';
|
||||
import type { HostedSessionConfig } from '../../domain/types/HostedSessionConfig';
|
||||
|
||||
export interface SessionDTO {
|
||||
sessionId: string;
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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[] = []
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
88
packages/automation/domain/types/ScreenRegion.ts
Normal file
88
packages/automation/domain/types/ScreenRegion.ts
Normal 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
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user