Skip to content

Commit cc4dbe3

Browse files
authored
feat: move positional status to channels (#669)
1 parent fa37ec4 commit cc4dbe3

73 files changed

Lines changed: 2252 additions & 948 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.storybook/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
export * from './telemetryDecorator';
22
export * from './channelSnapshotDecorator';
33
export * from './standingsSnapshot';
4+
export * from './trackStateSnapshot';
45
export * from './DynamicTelemetrySelector';
56
export * from './mockDashboardBridge';

.storybook/trackStateSnapshot.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import type { TrackStateSnapshot } from '@irdashies/types';
2+
3+
export const trackStateStorySnapshot = {
4+
focusCarIdx: null,
5+
carIdxLapDistPct: [],
6+
carIdxOnPitRoad: [],
7+
carIdxTrackSurface: [],
8+
carIdxClassPosition: [],
9+
carLeftRight: 0,
10+
isOnTrack: true,
11+
playerCarInPitStall: false,
12+
playerTrackSurface: 3,
13+
onPitRoad: false,
14+
isInGarage: false,
15+
isGarageVisible: false,
16+
isReplayPlaying: false,
17+
sessionTime: 10,
18+
sessionState: 4,
19+
speed: 0,
20+
displayUnits: 1,
21+
pitSpeedLimiterToggle: false,
22+
pitstopActive: false,
23+
engineWarnings: 0,
24+
lapDistPct: 0,
25+
sessionNum: 0,
26+
version: 1,
27+
} satisfies TrackStateSnapshot;

docs/IMPLEMENTATION_PLAN.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
| **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. |
3131
| **Phase 2b — Architectural cleanup (remaining)** | NOT STARTED || A1, A4, A5, A6, A7 completion, A9. Lower urgency now Standings memory issue is resolved |
3232
| **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. |
33-
| **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`. |
33+
| **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. |
3434
| **Phase 5 — Worker-thread SDK loop** | NOT STARTED || |
3535
| **Phase 6 — Native optimisations** | DEFERRED || Only if Phase 4 profiling demands |
3636

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

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

473473
## 6. Activity log
474474

475+
- **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
475476
- **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
476477
- **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
477478

src/app/bridge/iracingSdk/iracingSdkBridge.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { RadioRuntime } from '../../processors/radioRuntime';
2020
import { SessionTimingRuntime } from '../../processors/sessionTimingRuntime';
2121
import { SessionBarRuntime } from '../../processors/sessionBarRuntime';
2222
import { DriverControlsRuntime } from '../../processors/driverControlsRuntime';
23+
import { TrackStateRuntime } from '../../processors/trackStateRuntime';
2324

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

231235
let shouldStop = false;
232236
let lastRunningState: boolean | undefined = undefined;
@@ -334,6 +338,7 @@ export async function publishIRacingSDKEvents(
334338
sessionTimingRuntime?.onFrame(telemetry);
335339
sessionBarRuntime?.onFrame(telemetry);
336340
driverControlsRuntime?.onFrame(telemetry);
341+
trackStateRuntime?.onFrame(telemetry);
337342
if (
338343
perfTelemetryDeliveryEnabled &&
339344
overlayManager.hasLegacyStreamSubscribers('telemetry')
@@ -371,6 +376,7 @@ export async function publishIRacingSDKEvents(
371376
sessionTimingRuntime?.onSession(session);
372377
sessionBarRuntime?.onSession(session);
373378
driverControlsRuntime?.onSession(session);
379+
trackStateRuntime?.onSession(session);
374380
overlayManager.publishMessage('sessionData', session);
375381
sessionCallbacks.forEach((callback) => callback(session));
376382
perfMetrics.markEnd('sessionPublish');
@@ -437,6 +443,7 @@ export async function publishIRacingSDKEvents(
437443
sessionTimingRuntime?.dispose();
438444
sessionBarRuntime?.dispose();
439445
driverControlsRuntime?.dispose();
446+
trackStateRuntime?.dispose();
440447
referenceLapRuntime?.dispose();
441448
perfMetrics.stopReporting();
442449
},

src/app/bridge/iracingSdk/mock-data/mockSdkBridge.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { SessionTimingRuntime } from '../../../processors/sessionTimingRuntime';
1313
import { LapTimesRuntime } from '../../../processors/lapTimesRuntime';
1414
import { SessionBarRuntime } from '../../../processors/sessionBarRuntime';
1515
import { DriverControlsRuntime } from '../../../processors/driverControlsRuntime';
16+
import { TrackStateRuntime } from '../../../processors/trackStateRuntime';
1617

1718
export async function publishIRacingSDKEvents(
1819
overlayManager: OverlayManager,
@@ -68,6 +69,9 @@ export async function publishIRacingSDKEvents(
6869
const driverControlsRuntime = channelBus
6970
? new DriverControlsRuntime(channelBus, lifecycle, perfMetrics)
7071
: undefined;
72+
const trackStateRuntime = channelBus
73+
? new TrackStateRuntime(channelBus, lifecycle, perfMetrics)
74+
: undefined;
7175

7276
bridge.onSessionData((session) => {
7377
carSpeedsRuntime?.onSession(session);
@@ -78,6 +82,7 @@ export async function publishIRacingSDKEvents(
7882
sessionTimingRuntime?.onSession(session);
7983
sessionBarRuntime?.onSession(session);
8084
driverControlsRuntime?.onSession(session);
85+
trackStateRuntime?.onSession(session);
8186
overlayManager.publishMessage('sessionData', session);
8287
});
8388

@@ -93,6 +98,7 @@ export async function publishIRacingSDKEvents(
9398
sessionTimingRuntime?.onFrame(telemetry);
9499
sessionBarRuntime?.onFrame(telemetry);
95100
driverControlsRuntime?.onFrame(telemetry);
101+
trackStateRuntime?.onFrame(telemetry);
96102
perfMetrics.markStart('broadcast');
97103
overlayManager.publishMessage('telemetry', telemetry);
98104
perfMetrics.markEnd('broadcast');
@@ -117,6 +123,7 @@ export async function publishIRacingSDKEvents(
117123
sessionTimingRuntime?.dispose();
118124
sessionBarRuntime?.dispose();
119125
driverControlsRuntime?.dispose();
126+
trackStateRuntime?.dispose();
120127
referenceLapRuntime?.dispose();
121128
perfMetrics.stopReporting();
122129
originalStop();
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import { describe, expect, it } from 'vitest';
2+
import type { Telemetry } from '@irdashies/types';
3+
import { TrackStateProcessor } from './TrackStateProcessor';
4+
5+
const frame = (values: Record<string, unknown[]>): Telemetry =>
6+
Object.fromEntries(
7+
Object.entries(values).map(([key, current]) => [key, { value: current }])
8+
) as unknown as Telemetry;
9+
10+
describe('TrackStateProcessor', () => {
11+
it('projects positional, pit, and warning state', () => {
12+
const processor = new TrackStateProcessor();
13+
processor.onFrame(
14+
frame({
15+
CamCarIdx: [2],
16+
CarIdxLapDistPct: [0.1004, 0.2005, 0.30049],
17+
CarIdxOnPitRoad: [0, 1, 0],
18+
CarIdxTrackSurface: [3, 2, 3],
19+
CarIdxClassPosition: [1, 2, 3],
20+
CarLeftRight: [2],
21+
IsOnTrack: [true],
22+
OnPitRoad: [true],
23+
Speed: [41.5],
24+
EngineWarnings: [16],
25+
SessionNum: [1],
26+
})
27+
);
28+
29+
expect(processor.snapshot()).toMatchObject({
30+
focusCarIdx: 2,
31+
carIdxLapDistPct: [0.1, 0.201, 0.3],
32+
carIdxOnPitRoad: [false, true, false],
33+
carIdxTrackSurface: [3, 2, 3],
34+
carIdxClassPosition: [1, 2, 3],
35+
carLeftRight: 2,
36+
isOnTrack: true,
37+
onPitRoad: true,
38+
speed: 41.5,
39+
engineWarnings: 16,
40+
sessionNum: 1,
41+
version: 1,
42+
});
43+
});
44+
45+
it('reuses buffers, publishes only changes, and resets on disconnect', () => {
46+
const processor = new TrackStateProcessor();
47+
const telemetry = frame({ CarIdxLapDistPct: [0.25], IsOnTrack: [true] });
48+
processor.onFrame(telemetry);
49+
const positions = processor.snapshot().carIdxLapDistPct;
50+
processor.onFrame(telemetry);
51+
expect(processor.snapshot().version).toBe(1);
52+
expect(processor.snapshot().carIdxLapDistPct).toBe(positions);
53+
54+
processor.onLifecycle({ type: 'disconnect' });
55+
expect(processor.snapshot()).toMatchObject({
56+
carIdxLapDistPct: [],
57+
isOnTrack: false,
58+
sessionNum: null,
59+
version: 2,
60+
});
61+
});
62+
63+
it('ignores sub-millipercent position movement', () => {
64+
const processor = new TrackStateProcessor();
65+
processor.onFrame(frame({ CarIdxLapDistPct: [0.12341] }));
66+
processor.onFrame(frame({ CarIdxLapDistPct: [0.12344] }));
67+
68+
expect(processor.snapshot()).toMatchObject({
69+
carIdxLapDistPct: [0.123],
70+
version: 1,
71+
});
72+
});
73+
});

0 commit comments

Comments
 (0)