Files
Move/docs/execution-happy-path.md
T

3.4 KiB

Execution: the happy path

This traces one full idle-triggered sweep that completes cleanly — the "happy path" where the machine is idle long enough to fire, the configured pattern runs to exhaustion, and no real user activity interrupts it.

For the module breakdown and the three seams (device / strategies / executor), see the "How it works" section of the README.

sequenceDiagram
    autonumber
    participant Entry as move.ts
    participant Keeper as runKeeper
    participant Sim as simulateActivity
    participant Strat as Strategy<br/>(e.g. line)
    participant Exec as executePath
    participant Dev as Device<br/>(nut.js)

    Note over Entry: startup (args → config)
    Entry->>Entry: parseCliArgs()
    Entry->>Entry: loadConfigFile()
    Entry->>Entry: resolveConfig(file, cli)
    Entry->>Keeper: runKeeper(config)

    Keeper->>Dev: createNutDevice()
    Note right of Dev: sets mouse.config.autoDelayMs = 0
    Keeper->>Keeper: log.info(banner)
    Keeper->>Dev: getPosition()
    Dev-->>Keeper: lastPos
    Note over Keeper: lastActivity = now

    loop every checkInterval (until idle long enough)
        Keeper->>Dev: sleep(checkInterval)
        Keeper->>Dev: getPosition()
        Dev-->>Keeper: pos
        Note over Keeper: pos == lastPos (no user movement)<br/>now - lastActivity ≥ moveInterval → fire
    end

    Keeper->>Sim: simulateActivity(config, log, dev)
    Sim->>Dev: getPosition()
    Dev-->>Sim: start
    Sim->>Dev: width()
    Dev-->>Sim: width
    Sim->>Dev: height()
    Dev-->>Sim: height
    Note over Sim: strategy = STRATEGIES[config.pattern]<br/>ctx = { start, width, height, rng }
    Sim->>Exec: executePath(strategy, ctx, dev, log, config)

    Exec->>Strat: path(ctx)
    Strat-->>Exec: Iterable<Point>

    loop for each target point (clean run)
        Exec->>Exec: resolveTarget(bounds, target) → point
        Exec->>Dev: setPosition(point)
        Exec->>Dev: sleep(stepDelay)
        Exec->>Dev: getPosition()
        Dev-->>Exec: current
        Note over Exec: |current - point| ≤ 2px → not the user, continue
    end

    Note over Exec: path exhausted, no interruption
    Exec->>Dev: setPosition(round(start))
    Note right of Exec: restore cursor to origin
    Exec-->>Sim: "completed"
    Sim-->>Keeper: (done)

    Keeper->>Dev: getPosition()
    Dev-->>Keeper: lastPos (== start; re-sync)
    Note over Keeper: lastActivity = now<br/>loop continues

Invariants this path relies on

  • createNutDevice() is the only nut.js touchpoint. It disables nut.js's 100ms auto-delay so executePath owns cadence via stepDelay.
  • The strategy is pure. path(ctx) yields ideal points from geometry alone (start / width / height / rng); it never touches the device, which is what makes every pattern unit-testable without a screen.
  • Every step re-reads the cursor and compares it against the commanded point (not the strategy's ideal, possibly fractional target) within a 2px tolerance. On the happy path each check passes, so the loop runs to exhaustion. A mismatch beyond tolerance is real user activity and returns "interrupted" without restoring — the branch this diagram omits.
  • Clean completion restores the cursor to round(start). That is why the follow-up getPosition() in runKeeper re-syncs lastPos to the origin as a no-op, and the next idle check sees no net movement (so the synthetic sweep is never mistaken for the user returning).