Skip to content

Commit e69c9ae

Browse files
authored
feat: move session bar data to channels (#667)
1 parent e9b7d99 commit e69c9ae

55 files changed

Lines changed: 2651 additions & 202 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.

docs/IMPLEMENTATION_PLAN.md

Lines changed: 6 additions & 2 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#666 | Fuel, lap times, car speeds, reference laps, relative gaps, sector timing, Standings core state, and live positions are on `main`. Radio migration is in PR #666; session-bar migration and legacy telemetry removal or development-only restriction remain. |
33+
| **Phase 4 — Main-process processors** | IN PROGRESS | PRs #659#667 | Fuel, lap times, car speeds, reference laps, relative gaps, sector timing, Standings core state, live positions, and radio are on `main`. The complete Session Bar migration is in PR #667; legacy telemetry removal or development-only restriction remains. |
3434
| **Phase 5 — Worker-thread SDK loop** | NOT STARTED || |
3535
| **Phase 6 — Native optimisations** | DEFERRED || Only if Phase 4 profiling demands |
3636

@@ -273,8 +273,10 @@ Today every renderer wakes 25 times/sec regardless of what's mounted. A weather
273273
- [x] SectorTimingProcessor — PR #663
274274
- [x] StandingsProcessor — PR #664
275275
- [x] Standings live-position projection — PR #665
276-
- [ ] Radio transmit state — `radio.snapshot`, event-driven and demand-activated; PR #666 in review
276+
- [x] Radio transmit state — `radio.snapshot`, event-driven and demand-activated; PR #666
277277
- [ ] Session-bar telemetry migration
278+
- [ ] Shared race/session timing projection — `session-timing.snapshot`, demand-driven at 5 Hz; PR #667 in review
279+
- [ ] Auxiliary items (weather, fuel/units, brake bias, incidents, lap results, player position, and top speed) — `session-bar.snapshot`; PR #667 in review
278280
- [ ] Legacy `'telemetry'` channel removed or dev-only
279281

280282
### Phase 5 — Worker-thread SDK loop
@@ -466,6 +468,8 @@ LLM agents: read this file at the start of any session that touches the architec
466468

467469
## 6. Activity log
468470

471+
- **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. Legacy telemetry restriction remains the next Phase 4 step — `feat/session-bar-channel` — in review
472+
469473
- **2026-08-09** — PR #665 merged. Opened PR #666 for the next explicit Phase 4 slice: move bursty `RadioTransmitCarIdx` state to a demand-activated, event-driven `radio.snapshot` channel while retaining renderer-configured icon persistence. Session-bar migration follows; legacy telemetry restriction/removal remains the Phase 4 exit step — `feat/radio-channel` — in review
470474

471475
- **2026-08-08** — Standings PR #664 merged. Opened PR #665 to move live in-class position calculation from renderer telemetry hooks into the demand-driven Standings processor with reusable projection buffers and conditional renderer subscriptions. Radio and session-bar telemetry remain after this slice — `feat/standings-live-positions` — in review

src/app/bridge/iracingSdk/iracingSdkBridge.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ import { RelativeGapRuntime } from '../../processors/relativeGapRuntime';
1717
import { SectorTimingRuntime } from '../../processors/sectorTimingRuntime';
1818
import { StandingsRuntime } from '../../processors/standingsRuntime';
1919
import { RadioRuntime } from '../../processors/radioRuntime';
20+
import { SessionTimingRuntime } from '../../processors/sessionTimingRuntime';
21+
import { SessionBarRuntime } from '../../processors/sessionBarRuntime';
2022

2123
// Keys consumed by the renderer. Anything outside this set is dropped before
2224
// the telemetry object crosses the IPC boundary — reducing structured-clone
@@ -208,6 +210,19 @@ export async function publishIRacingSDKEvents(
208210
lifecycle && channelBus
209211
? new RadioRuntime(channelBus, lifecycle, perfMetrics, isTapeReplay)
210212
: undefined;
213+
const sessionTimingRuntime =
214+
lifecycle && channelBus && lapTimesRuntime
215+
? new SessionTimingRuntime(
216+
channelBus,
217+
lifecycle,
218+
perfMetrics,
219+
lapTimesRuntime,
220+
isTapeReplay
221+
)
222+
: undefined;
223+
const sessionBarRuntime = channelBus
224+
? new SessionBarRuntime(channelBus, lifecycle, perfMetrics, isTapeReplay)
225+
: undefined;
211226

212227
let shouldStop = false;
213228
let lastRunningState: boolean | undefined = undefined;
@@ -312,6 +327,8 @@ export async function publishIRacingSDKEvents(
312327
sectorTimingRuntime?.onFrame(telemetry);
313328
standingsRuntime?.onFrame(telemetry);
314329
radioRuntime?.onFrame(telemetry);
330+
sessionTimingRuntime?.onFrame(telemetry);
331+
sessionBarRuntime?.onFrame(telemetry);
315332
if (
316333
perfTelemetryDeliveryEnabled &&
317334
overlayManager.hasLegacyStreamSubscribers('telemetry')
@@ -346,6 +363,8 @@ export async function publishIRacingSDKEvents(
346363
relativeGapRuntime?.onSession(session);
347364
sectorTimingRuntime?.onSession(session);
348365
standingsRuntime?.onSession(session);
366+
sessionTimingRuntime?.onSession(session);
367+
sessionBarRuntime?.onSession(session);
349368
overlayManager.publishMessage('sessionData', session);
350369
sessionCallbacks.forEach((callback) => callback(session));
351370
perfMetrics.markEnd('sessionPublish');
@@ -409,6 +428,8 @@ export async function publishIRacingSDKEvents(
409428
sectorTimingRuntime?.dispose();
410429
standingsRuntime?.dispose();
411430
radioRuntime?.dispose();
431+
sessionTimingRuntime?.dispose();
432+
sessionBarRuntime?.dispose();
412433
referenceLapRuntime?.dispose();
413434
perfMetrics.stopReporting();
414435
},

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

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,53 @@ describe('mockSdkBridge processor channels', () => {
8888
);
8989
});
9090

91+
it('publishes session timing without a lifecycle in demo mode', async () => {
92+
const bus = new ChannelBus();
93+
const publish = vi.spyOn(bus, 'publish');
94+
bus.subscribe(
95+
{
96+
id: 3,
97+
isDestroyed: () => false,
98+
isVisible: () => true,
99+
send: vi.fn(),
100+
},
101+
'session-timing.snapshot'
102+
);
103+
const bridge = await publishIRacingSDKEvents(
104+
{ publishMessage: vi.fn() } as never,
105+
undefined,
106+
bus
107+
);
108+
try {
109+
callbacks.session?.({
110+
DriverInfo: { DriverCarIdx: 0, Drivers: [{ CarIdx: 0 }] },
111+
SessionInfo: {
112+
Sessions: [{ SessionNum: 1, SessionType: 'Race', SessionLaps: 20 }],
113+
},
114+
} as unknown as Session);
115+
callbacks.telemetry?.({
116+
SessionTime: { value: [120] },
117+
SessionNum: { value: [1] },
118+
SessionState: { value: [4] },
119+
SessionTimeTotal: { value: [2400] },
120+
SessionTimeRemain: { value: [2280] },
121+
CamCarIdx: { value: [0] },
122+
CarIdxLap: { value: [2] },
123+
CarIdxPosition: { value: [1] },
124+
CarIdxLapDistPct: { value: [0.25] },
125+
CarIdxBestLapTime: { value: [60] },
126+
CarIdxLastLapTime: { value: [60] },
127+
} as unknown as Telemetry);
128+
129+
expect(publish).toHaveBeenCalledWith(
130+
'session-timing.snapshot',
131+
expect.objectContaining({ currentLap: 2, sessionType: 'Race' })
132+
);
133+
} finally {
134+
bridge.stop();
135+
}
136+
});
137+
91138
it('feeds mock data through the relative-gap runtime', async () => {
92139
const bus = new ChannelBus();
93140
const publish = vi.spyOn(bus, 'publish');

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

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ import { RelativeGapRuntime } from '../../../processors/relativeGapRuntime';
99
import { SectorTimingRuntime } from '../../../processors/sectorTimingRuntime';
1010
import { StandingsRuntime } from '../../../processors/standingsRuntime';
1111
import { RadioRuntime } from '../../../processors/radioRuntime';
12+
import { SessionTimingRuntime } from '../../../processors/sessionTimingRuntime';
13+
import { LapTimesRuntime } from '../../../processors/lapTimesRuntime';
14+
import { SessionBarRuntime } from '../../../processors/sessionBarRuntime';
1215

1316
export async function publishIRacingSDKEvents(
1417
overlayManager: OverlayManager,
@@ -19,6 +22,9 @@ export async function publishIRacingSDKEvents(
1922
perfMetrics.startReporting();
2023

2124
const bridge = generateMockData();
25+
const lapTimesRuntime = channelBus
26+
? new LapTimesRuntime(channelBus, lifecycle, perfMetrics)
27+
: undefined;
2228
const carSpeedsRuntime = channelBus
2329
? new CarSpeedsRuntime(channelBus, lifecycle, perfMetrics)
2430
: undefined;
@@ -46,24 +52,41 @@ export async function publishIRacingSDKEvents(
4652
const radioRuntime = channelBus
4753
? new RadioRuntime(channelBus, lifecycle, perfMetrics)
4854
: undefined;
55+
const sessionTimingRuntime =
56+
channelBus && lapTimesRuntime
57+
? new SessionTimingRuntime(
58+
channelBus,
59+
lifecycle,
60+
perfMetrics,
61+
lapTimesRuntime
62+
)
63+
: undefined;
64+
const sessionBarRuntime = channelBus
65+
? new SessionBarRuntime(channelBus, lifecycle, perfMetrics)
66+
: undefined;
4967

5068
bridge.onSessionData((session) => {
5169
carSpeedsRuntime?.onSession(session);
5270
referenceLapRuntime?.onSession(session);
5371
relativeGapRuntime?.onSession(session);
5472
sectorTimingRuntime?.onSession(session);
5573
standingsRuntime?.onSession(session);
74+
sessionTimingRuntime?.onSession(session);
75+
sessionBarRuntime?.onSession(session);
5676
overlayManager.publishMessage('sessionData', session);
5777
});
5878

5979
bridge.onTelemetry((telemetry) => {
6080
perfMetrics.markStart('processTelemetry');
81+
lapTimesRuntime?.onFrame(telemetry);
6182
carSpeedsRuntime?.onFrame(telemetry);
6283
referenceLapRuntime?.onFrame(telemetry);
6384
relativeGapRuntime?.onFrame(telemetry);
6485
sectorTimingRuntime?.onFrame(telemetry);
6586
standingsRuntime?.onFrame(telemetry);
6687
radioRuntime?.onFrame(telemetry);
88+
sessionTimingRuntime?.onFrame(telemetry);
89+
sessionBarRuntime?.onFrame(telemetry);
6790
perfMetrics.markStart('broadcast');
6891
overlayManager.publishMessage('telemetry', telemetry);
6992
perfMetrics.markEnd('broadcast');
@@ -80,10 +103,13 @@ export async function publishIRacingSDKEvents(
80103
...bridge,
81104
stop: () => {
82105
carSpeedsRuntime?.dispose();
106+
lapTimesRuntime?.dispose();
83107
relativeGapRuntime?.dispose();
84108
sectorTimingRuntime?.dispose();
85109
standingsRuntime?.dispose();
86110
radioRuntime?.dispose();
111+
sessionTimingRuntime?.dispose();
112+
sessionBarRuntime?.dispose();
87113
referenceLapRuntime?.dispose();
88114
perfMetrics.stopReporting();
89115
originalStop();
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import { describe, expect, it } from 'vitest';
2+
import type { Session, Telemetry } from '@irdashies/types';
3+
import { SessionBarProcessor } from './SessionBarProcessor';
4+
5+
describe('SessionBarProcessor', () => {
6+
it('returns snapshots detached from reusable processor buffers', () => {
7+
const processor = new SessionBarProcessor();
8+
processor.init({
9+
DriverInfo: {
10+
DriverCarIdx: 0,
11+
Drivers: [{ CarIdx: 0, CarID: 67, CarClassID: 1 }],
12+
},
13+
} as unknown as Session);
14+
processor.onFrame({
15+
SessionTime: { value: [1] },
16+
SessionNum: { value: [1] },
17+
CarIdxPosition: { value: [1] },
18+
} as unknown as Telemetry);
19+
const published = processor.snapshot();
20+
processor.onFrame({
21+
SessionTime: { value: [1.2] },
22+
SessionNum: { value: [1] },
23+
CarIdxPosition: { value: [2] },
24+
} as unknown as Telemetry);
25+
expect(published.competitorPositions).toEqual([1]);
26+
expect(processor.snapshot().competitorPositions).toEqual([2]);
27+
});
28+
29+
it('projects the auxiliary session bar state and resets with lifecycle', () => {
30+
const processor = new SessionBarProcessor();
31+
processor.init({
32+
WeekendInfo: {
33+
TrackDisplayName: 'Okayama',
34+
WeekendOptions: { IncidentLimit: 17 },
35+
},
36+
DriverInfo: {
37+
DriverCarIdx: 0,
38+
Drivers: [{ CarIdx: 0, CarID: 67, CarClassID: 1 }],
39+
},
40+
SessionInfo: {
41+
Sessions: [{ SessionNum: 1, SessionName: 'Race', SessionType: 'Race' }],
42+
},
43+
} as unknown as Session);
44+
processor.onFrame({
45+
SessionTime: { value: [1] },
46+
SessionNum: { value: [1] },
47+
DisplayUnits: { value: [1] },
48+
FuelLevel: { value: [30] },
49+
PlayerCarTeamIncidentCount: { value: [2] },
50+
CarIdxPosition: { value: [1] },
51+
CarIdxClassPosition: { value: [1] },
52+
CarIdxBestLapTime: { value: [90] },
53+
Lap: { value: [2] },
54+
Speed: { value: [50] },
55+
} as unknown as Telemetry);
56+
expect(processor.snapshot()).toMatchObject({
57+
sessionName: 'Race',
58+
trackDisplayName: 'Okayama',
59+
fuelLevel: 30,
60+
incidents: 2,
61+
playerClassPosition: 1,
62+
playerClassSize: 1,
63+
sessionBestLap: 90,
64+
});
65+
processor.onFrame({
66+
SessionTime: { value: [1.05] },
67+
SessionNum: { value: [1] },
68+
Lap: { value: [2] },
69+
Speed: { value: [70] },
70+
} as unknown as Telemetry);
71+
processor.onFrame({
72+
SessionTime: { value: [1.2] },
73+
SessionNum: { value: [1] },
74+
Lap: { value: [3] },
75+
Speed: { value: [40] },
76+
} as unknown as Telemetry);
77+
expect(processor.snapshot()).toMatchObject({
78+
lastLapTopSpeed: 70,
79+
sessionBestTopSpeed: 70,
80+
});
81+
processor.onLifecycle({ type: 'disconnect' });
82+
expect(processor.snapshot()).toMatchObject({
83+
sessionNum: null,
84+
incidents: 0,
85+
});
86+
});
87+
});

0 commit comments

Comments
 (0)