Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .storybook/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export * from './telemetryDecorator';
export * from './channelSnapshotDecorator';
export * from './standingsSnapshot';
export * from './trackStateSnapshot';
export * from './DynamicTelemetrySelector';
export * from './mockDashboardBridge';
27 changes: 27 additions & 0 deletions .storybook/trackStateSnapshot.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import type { TrackStateSnapshot } from '@irdashies/types';

export const trackStateStorySnapshot = {
focusCarIdx: null,
carIdxLapDistPct: [],
carIdxOnPitRoad: [],
carIdxTrackSurface: [],
carIdxClassPosition: [],
carLeftRight: 0,
isOnTrack: true,
playerCarInPitStall: false,
playerTrackSurface: 3,
onPitRoad: false,
isInGarage: false,
isGarageVisible: false,
isReplayPlaying: false,
sessionTime: 10,
sessionState: 4,
speed: 0,
displayUnits: 1,
pitSpeedLimiterToggle: false,
pitstopActive: false,
engineWarnings: 0,
lapDistPct: 0,
sessionNum: 0,
version: 1,
} satisfies TrackStateSnapshot;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
7 changes: 4 additions & 3 deletions docs/IMPLEMENTATION_PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
| **Phase 2a remaining items** | R1+R2 LANDED, R3 PENDING | `feat/phase-2a-integration` for R1+R2 | R1 (reference-lap fetch dedup) + R2 (post-debounce write log) landed 2026-05-19. R3 (Empty Dashboard substrate baseline test) is a test run, not code work — pending. |
| **Phase 2b — Architectural cleanup (remaining)** | NOT STARTED | — | A1, A4, A5, A6, A7 completion, A9. Lower urgency now Standings memory issue is resolved |
| **Phase 3 — Channel-based bridge** | LANDED; MEMORY GATE OPEN | PRs #646, #649–#652, #656, #658 | Typed rate-aware channels, per-window subscriptions, deterministic replay validation, Fuel processor/renderer migration, conditional legacy telemetry, and performance instrumentation are on `main`. The Fuel-only A/B removed legacy deliveries and reduced app-wide renderer wake-ups by 42.4%; both baseline and candidate still failed the memory-slope gate. |
| **Phase 4 — Main-process processors** | IN PROGRESS | PRs #659–#667 | The planned derived processors and complete Session Bar migration are on `main`. Four final slices migrate direct telemetry consumers, then remove the legacy renderer firehose; the first slice is Input and Tachometer via `driver-controls.snapshot`. |
| **Phase 4 — Main-process processors** | IN PROGRESS | PRs #659–#669 | The planned derived processors, complete Session Bar migration, and driver controls are on `main`. PR #669 moves positional, pit-state, and warning consumers to a demand-driven `track-state.snapshot`; low-frequency/debug consumers and final legacy deletion remain. |
| **Phase 5 — Worker-thread SDK loop** | NOT STARTED | — | |
| **Phase 6 — Native optimisations** | DEFERRED | — | Only if Phase 4 profiling demands |

Expand Down Expand Up @@ -278,8 +278,8 @@ Today every renderer wakes 25 times/sec regardless of what's mounted. A weather
- [x] Shared race/session timing projection — `session-timing.snapshot`, demand-driven at 5 Hz
- [x] Auxiliary items (weather, fuel/units, brake bias, incidents, lap results, player position, and top speed) — `session-bar.snapshot`
- [ ] Direct telemetry migration and legacy removal
1. [ ] Input and Tachometer — full-precision `driver-controls.snapshot`; PR #668 in review
2. [ ] Positional, pit-state, and warning consumers — Pitlane Helper, maps, Battle/Relative helpers, Blind Spot, Rejoin, Faster/Slow Car
1. [x] Input and Tachometer — full-precision `driver-controls.snapshot`; merged as PR #668
2. [ ] Positional, pit-state, and warning consumers — demand-driven `track-state.snapshot`, channel-only Pitlane Helper, maps, Battle, Blind Spot, Rejoin, Faster Cars From Behind, and Slow Car Ahead; PR #669 in review
3. [ ] Remaining low-frequency consumers plus an explicit development-only path for Telemetry Inspector
4. [ ] Delete the legacy `'telemetry'` IPC/store/provider infrastructure, run the complete replay suite, and re-profile

Expand Down Expand Up @@ -472,6 +472,7 @@ LLM agents: read this file at the start of any session that touches the architec

## 6. Activity log

- **2026-08-09** — PR #668 merged. Opened PR #669 for the second final Phase 4 slice: a reusable-buffer, demand-driven 25 Hz `track-state.snapshot` for positional, pit-state, and warning data; migrated Pitlane Helper, Track Map, Flat Track Map, Battle, Blind Spot, Rejoin, Faster Cars From Behind, and Slow Car Ahead off the legacy renderer firehose; decoupled session and pit-lane providers from raw telemetry; added processor/runtime tests, Storybook snapshots, and a thirteenth curated replay probe — `feat/positional-status-channels` — in review

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Correct the future activity-log date.

This entry is dated 2026-08-09, but the current date is 2026-08-08. If this entry documents the current PR opening, use 2026-08-08. Otherwise, add the entry after August 9, 2026.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/IMPLEMENTATION_PLAN.md` at line 475, Correct the date on the PR `#669`
activity-log entry to 2026-08-08 if it documents the current PR opening;
otherwise, move the entry after the existing August 9, 2026 activity.

- **2026-08-09** — PR #667 merged. Opened PR #668 for the first of four final Phase 4 slices: Input and Tachometer move to a full-precision, demand-driven `driver-controls.snapshot`; positional/warning consumers, low-frequency/debug consumers, and final legacy deletion follow as separate reviewable PRs — `feat/driver-controls-channel` — in review
- **2026-08-09** — PR #666 merged. Opened PR #667 for the complete Session Bar migration: shared race/session timing plus auxiliary weather, fuel, incident, lap-result, position, and top-speed data now use demand-driven snapshots wired for live/tape and mock sources — `feat/session-bar-channel` — merged as PR #667

Expand Down
7 changes: 7 additions & 0 deletions src/app/bridge/iracingSdk/iracingSdkBridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { RadioRuntime } from '../../processors/radioRuntime';
import { SessionTimingRuntime } from '../../processors/sessionTimingRuntime';
import { SessionBarRuntime } from '../../processors/sessionBarRuntime';
import { DriverControlsRuntime } from '../../processors/driverControlsRuntime';
import { TrackStateRuntime } from '../../processors/trackStateRuntime';

// Keys consumed by the renderer. Anything outside this set is dropped before
// the telemetry object crosses the IPC boundary — reducing structured-clone
Expand Down Expand Up @@ -227,6 +228,9 @@ export async function publishIRacingSDKEvents(
const driverControlsRuntime = channelBus
? new DriverControlsRuntime(channelBus, lifecycle, perfMetrics)
: undefined;
const trackStateRuntime = channelBus
? new TrackStateRuntime(channelBus, lifecycle, perfMetrics)
: undefined;

let shouldStop = false;
let lastRunningState: boolean | undefined = undefined;
Expand Down Expand Up @@ -334,6 +338,7 @@ export async function publishIRacingSDKEvents(
sessionTimingRuntime?.onFrame(telemetry);
sessionBarRuntime?.onFrame(telemetry);
driverControlsRuntime?.onFrame(telemetry);
trackStateRuntime?.onFrame(telemetry);
if (
perfTelemetryDeliveryEnabled &&
overlayManager.hasLegacyStreamSubscribers('telemetry')
Expand Down Expand Up @@ -371,6 +376,7 @@ export async function publishIRacingSDKEvents(
sessionTimingRuntime?.onSession(session);
sessionBarRuntime?.onSession(session);
driverControlsRuntime?.onSession(session);
trackStateRuntime?.onSession(session);
overlayManager.publishMessage('sessionData', session);
sessionCallbacks.forEach((callback) => callback(session));
perfMetrics.markEnd('sessionPublish');
Expand Down Expand Up @@ -437,6 +443,7 @@ export async function publishIRacingSDKEvents(
sessionTimingRuntime?.dispose();
sessionBarRuntime?.dispose();
driverControlsRuntime?.dispose();
trackStateRuntime?.dispose();
referenceLapRuntime?.dispose();
perfMetrics.stopReporting();
},
Expand Down
7 changes: 7 additions & 0 deletions src/app/bridge/iracingSdk/mock-data/mockSdkBridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { SessionTimingRuntime } from '../../../processors/sessionTimingRuntime';
import { LapTimesRuntime } from '../../../processors/lapTimesRuntime';
import { SessionBarRuntime } from '../../../processors/sessionBarRuntime';
import { DriverControlsRuntime } from '../../../processors/driverControlsRuntime';
import { TrackStateRuntime } from '../../../processors/trackStateRuntime';

export async function publishIRacingSDKEvents(
overlayManager: OverlayManager,
Expand Down Expand Up @@ -68,6 +69,9 @@ export async function publishIRacingSDKEvents(
const driverControlsRuntime = channelBus
? new DriverControlsRuntime(channelBus, lifecycle, perfMetrics)
: undefined;
const trackStateRuntime = channelBus
? new TrackStateRuntime(channelBus, lifecycle, perfMetrics)
: undefined;

bridge.onSessionData((session) => {
carSpeedsRuntime?.onSession(session);
Expand All @@ -78,6 +82,7 @@ export async function publishIRacingSDKEvents(
sessionTimingRuntime?.onSession(session);
sessionBarRuntime?.onSession(session);
driverControlsRuntime?.onSession(session);
trackStateRuntime?.onSession(session);
overlayManager.publishMessage('sessionData', session);
});

Expand All @@ -93,6 +98,7 @@ export async function publishIRacingSDKEvents(
sessionTimingRuntime?.onFrame(telemetry);
sessionBarRuntime?.onFrame(telemetry);
driverControlsRuntime?.onFrame(telemetry);
trackStateRuntime?.onFrame(telemetry);
perfMetrics.markStart('broadcast');
overlayManager.publishMessage('telemetry', telemetry);
perfMetrics.markEnd('broadcast');
Expand All @@ -117,6 +123,7 @@ export async function publishIRacingSDKEvents(
sessionTimingRuntime?.dispose();
sessionBarRuntime?.dispose();
driverControlsRuntime?.dispose();
trackStateRuntime?.dispose();
referenceLapRuntime?.dispose();
perfMetrics.stopReporting();
originalStop();
Expand Down
73 changes: 73 additions & 0 deletions src/app/processors/TrackStateProcessor.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { describe, expect, it } from 'vitest';
import type { Telemetry } from '@irdashies/types';
import { TrackStateProcessor } from './TrackStateProcessor';

const frame = (values: Record<string, unknown[]>): Telemetry =>
Object.fromEntries(
Object.entries(values).map(([key, current]) => [key, { value: current }])
) as unknown as Telemetry;

describe('TrackStateProcessor', () => {
it('projects positional, pit, and warning state', () => {
const processor = new TrackStateProcessor();
processor.onFrame(
frame({
CamCarIdx: [2],
CarIdxLapDistPct: [0.1004, 0.2005, 0.30049],
CarIdxOnPitRoad: [0, 1, 0],
CarIdxTrackSurface: [3, 2, 3],
CarIdxClassPosition: [1, 2, 3],
CarLeftRight: [2],
IsOnTrack: [true],
OnPitRoad: [true],
Speed: [41.5],
EngineWarnings: [16],
SessionNum: [1],
})
);

expect(processor.snapshot()).toMatchObject({
focusCarIdx: 2,
carIdxLapDistPct: [0.1, 0.201, 0.3],
carIdxOnPitRoad: [false, true, false],
carIdxTrackSurface: [3, 2, 3],
carIdxClassPosition: [1, 2, 3],
carLeftRight: 2,
isOnTrack: true,
onPitRoad: true,
speed: 41.5,
engineWarnings: 16,
sessionNum: 1,
version: 1,
});
});

it('reuses buffers, publishes only changes, and resets on disconnect', () => {
const processor = new TrackStateProcessor();
const telemetry = frame({ CarIdxLapDistPct: [0.25], IsOnTrack: [true] });
processor.onFrame(telemetry);
const positions = processor.snapshot().carIdxLapDistPct;
processor.onFrame(telemetry);
expect(processor.snapshot().version).toBe(1);
expect(processor.snapshot().carIdxLapDistPct).toBe(positions);

processor.onLifecycle({ type: 'disconnect' });
expect(processor.snapshot()).toMatchObject({
carIdxLapDistPct: [],
isOnTrack: false,
sessionNum: null,
version: 2,
});
});

it('ignores sub-millipercent position movement', () => {
const processor = new TrackStateProcessor();
processor.onFrame(frame({ CarIdxLapDistPct: [0.12341] }));
processor.onFrame(frame({ CarIdxLapDistPct: [0.12344] }));

expect(processor.snapshot()).toMatchObject({
carIdxLapDistPct: [0.123],
version: 1,
});
});
});
Loading