@hotlap.ai/telemetry 1.0.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,349 @@
1
+ // src/normalize.ts
2
+ var NORMALIZE_THRESHOLDS = {
3
+ MIN_SAMPLES: 10,
4
+ MIN_COMPLETION_PCT: 0.9,
5
+ MAX_AVG_SPEED_KMH: 220,
6
+ LLT_UPDATE_GRACE_SEC: 2
7
+ };
8
+ function parseTrackLengthKm(weekend) {
9
+ const raw = weekend?.TrackLength ?? weekend?.TrackLengthOfficial;
10
+ if (typeof raw !== "string")
11
+ return null;
12
+ const m = raw.match(/([\d.]+)\s*km/i);
13
+ if (!m)
14
+ return null;
15
+ const km = parseFloat(m[1]);
16
+ return Number.isFinite(km) && km > 0 ? km : null;
17
+ }
18
+ function num(v) {
19
+ return typeof v === "number" && Number.isFinite(v) ? v : null;
20
+ }
21
+ function str(v) {
22
+ return typeof v === "string" && v.length > 0 ? v : null;
23
+ }
24
+ function normalizeIbt(source) {
25
+ const n = source.recordCount;
26
+ const lapArr = source.getAllTyped("Lap");
27
+ const timeArr = source.getAllTyped("SessionTime");
28
+ const distArr = source.getAllTyped("LapDist");
29
+ const pctArr = source.getAllTyped("LapDistPct");
30
+ const lltArr = source.getAllTyped("LapLastLapTime");
31
+ const sessArr = source.getAllTyped("SessionNum");
32
+ const incArr = source.getAllTyped("PlayerCarMyIncidentCount");
33
+ if (!lapArr || !timeArr || n === 0)
34
+ return [];
35
+ const yaml = source.getSessionInfo() ?? {};
36
+ const weekend = yaml.WeekendInfo;
37
+ const trackLengthKm = parseTrackLengthKm(weekend);
38
+ const boundaries = sectorBoundaries(yaml);
39
+ const meta = driverMeta(yaml);
40
+ const sessions = new Map;
41
+ for (let i = 0;i < n; i++) {
42
+ const lap = lapArr[i];
43
+ const sess = sessArr ? sessArr[i] : 0;
44
+ if (!Number.isFinite(lap))
45
+ continue;
46
+ let lapsOfSession = sessions.get(sess);
47
+ if (!lapsOfSession)
48
+ sessions.set(sess, lapsOfSession = new Map);
49
+ let acc = lapsOfSession.get(lap);
50
+ if (!acc)
51
+ lapsOfSession.set(lap, acc = { lapNumber: lap, indices: [] });
52
+ acc.indices.push(i);
53
+ }
54
+ const out = [];
55
+ for (const [sessionNum, lapsOfSession] of [...sessions.entries()].sort((a, b) => a[0] - b[0])) {
56
+ const sessionEntry = yaml.SessionInfo?.Sessions?.find((s) => num(s.SessionNum) === sessionNum);
57
+ const laps = [];
58
+ const lapNumbers = [...lapsOfSession.keys()].sort((a, b) => a - b);
59
+ for (const lapNumber of lapNumbers) {
60
+ const acc = lapsOfSession.get(lapNumber);
61
+ const next = lapsOfSession.get(lapNumber + 1);
62
+ laps.push(normalizeLap(acc, next, {
63
+ timeArr,
64
+ distArr,
65
+ pctArr,
66
+ lltArr,
67
+ incArr,
68
+ trackLengthKm,
69
+ boundaries
70
+ }));
71
+ }
72
+ out.push({
73
+ sessionNum,
74
+ sessionType: str(sessionEntry?.SessionType),
75
+ sessionName: str(sessionEntry?.SessionName),
76
+ eventType: str(weekend?.EventType),
77
+ trackId: num(weekend?.TrackID),
78
+ trackName: str(weekend?.TrackDisplayName) ?? str(weekend?.TrackName),
79
+ trackConfigName: str(weekend?.TrackConfigName),
80
+ trackLengthKm,
81
+ seriesId: num(weekend?.SeriesID),
82
+ seasonId: num(weekend?.SeasonID),
83
+ sessionId: num(weekend?.SessionID),
84
+ subSessionId: num(weekend?.SubSessionID),
85
+ driverCarIdx: meta.driverCarIdx,
86
+ driverName: meta.driverName,
87
+ carId: meta.carId,
88
+ carScreenName: meta.carScreenName,
89
+ carPath: meta.carPath,
90
+ tickRate: source.tickRate,
91
+ laps
92
+ });
93
+ }
94
+ return out;
95
+ }
96
+ function driverMeta(yaml) {
97
+ const driverCarIdx = num(yaml.DriverInfo?.DriverCarIdx) ?? 0;
98
+ const driver = yaml.DriverInfo?.Drivers?.find((d) => num(d.CarIdx) === driverCarIdx);
99
+ return {
100
+ driverCarIdx,
101
+ driverName: str(driver?.UserName),
102
+ carId: num(driver?.CarID),
103
+ carScreenName: str(driver?.CarScreenName),
104
+ carPath: str(driver?.CarPath)
105
+ };
106
+ }
107
+ function sectorBoundaries(yaml) {
108
+ const sectors = yaml.SplitTimeInfo?.Sectors;
109
+ if (!Array.isArray(sectors) || sectors.length === 0)
110
+ return [];
111
+ const pcts = sectors.map((s) => num(s.SectorStartPct)).filter((p) => p != null && p >= 0 && p < 1).sort((a, b) => a - b);
112
+ if (pcts.length === 0)
113
+ return [];
114
+ if (pcts[0] !== 0)
115
+ pcts.unshift(0);
116
+ return pcts;
117
+ }
118
+ function normalizeLap(acc, next, ctx) {
119
+ const { timeArr, distArr, pctArr, lltArr, incArr, trackLengthKm, boundaries } = ctx;
120
+ const idx = acc.indices;
121
+ const flags = [];
122
+ const isOutLap = acc.lapNumber === 0;
123
+ if (isOutLap)
124
+ flags.push("out-lap");
125
+ let minT = Infinity, maxT = -Infinity, minD = Infinity, maxD = -Infinity;
126
+ for (const i of idx) {
127
+ const t = timeArr[i];
128
+ if (t < minT)
129
+ minT = t;
130
+ if (t > maxT)
131
+ maxT = t;
132
+ if (distArr) {
133
+ const d = distArr[i];
134
+ if (Number.isFinite(d)) {
135
+ if (d < minD)
136
+ minD = d;
137
+ if (d > maxD)
138
+ maxD = d;
139
+ }
140
+ }
141
+ }
142
+ const sampleSpanSec = maxT - minT;
143
+ const distanceKm = distArr && maxD > minD ? (maxD - minD) / 1000 : 0;
144
+ const completionPct = trackLengthKm && trackLengthKm > 0 ? distanceKm / trackLengthKm : null;
145
+ if (idx.length < NORMALIZE_THRESHOLDS.MIN_SAMPLES)
146
+ flags.push("too-few-samples");
147
+ if (completionPct != null && completionPct < NORMALIZE_THRESHOLDS.MIN_COMPLETION_PCT) {
148
+ flags.push("low-completion");
149
+ }
150
+ let lapTimeSec = null;
151
+ let timingSource = null;
152
+ if (lltArr && next && next.indices.length > 0 && !isOutLap) {
153
+ const lltDuringLap = lastFinite(lltArr, idx);
154
+ let updated = null;
155
+ for (const i of next.indices) {
156
+ const v = lltArr[i];
157
+ if (v > 0 && (lltDuringLap == null || Math.abs(v - lltDuringLap) > 0.000000001))
158
+ updated = v;
159
+ }
160
+ if (updated != null) {
161
+ lapTimeSec = updated;
162
+ timingSource = "lap-last-lap-time";
163
+ }
164
+ }
165
+ let completed = timingSource === "lap-last-lap-time";
166
+ if (lapTimeSec == null && !isOutLap) {
167
+ flags.push("no-lap-last-time");
168
+ const nextSpanSec = next && next.indices.length > 0 ? timeArr[next.indices[next.indices.length - 1]] - timeArr[next.indices[0]] : null;
169
+ const updateMissed = lltArr != null && nextSpanSec != null && nextSpanSec < NORMALIZE_THRESHOLDS.LLT_UPDATE_GRACE_SEC;
170
+ if (!lltArr || updateMissed) {
171
+ const start = crossingStart(acc, ctx);
172
+ const finish = crossingFinish(acc, next, ctx);
173
+ if (start != null && finish != null && finish > start) {
174
+ lapTimeSec = finish - start;
175
+ timingSource = "crossing-interpolation";
176
+ completed = coverageComplete(acc, ctx, completionPct);
177
+ }
178
+ }
179
+ }
180
+ const effectiveTime = lapTimeSec ?? (sampleSpanSec > 0 ? sampleSpanSec : null);
181
+ if (effectiveTime && distanceKm > 0) {
182
+ const avgKmh = distanceKm / (effectiveTime / 3600);
183
+ if (avgKmh > NORMALIZE_THRESHOLDS.MAX_AVG_SPEED_KMH)
184
+ flags.push("unrealistic-speed");
185
+ }
186
+ let incidents = 0;
187
+ if (incArr && idx.length >= 2) {
188
+ const first = firstFinite(incArr, idx);
189
+ const last = lastFinite(incArr, idx);
190
+ if (first != null && last != null)
191
+ incidents = Math.max(0, last - first);
192
+ }
193
+ if (incidents > 0)
194
+ flags.push("incident-delta");
195
+ const sectorTimes = computeSectors(acc, next, ctx);
196
+ if (sectorTimes.some((s) => s.timeSec == null) && sectorTimes.length > 0) {
197
+ flags.push("no-sector-data");
198
+ }
199
+ let sectorResidualSec = null;
200
+ if (lapTimeSec != null && sectorTimes.length > 0 && sectorTimes.every((s) => s.timeSec != null)) {
201
+ const sum = sectorTimes.reduce((a, s) => a + (s.timeSec ?? 0), 0);
202
+ sectorResidualSec = lapTimeSec - sum;
203
+ }
204
+ return {
205
+ lapNumber: acc.lapNumber,
206
+ completed,
207
+ lapTimeSec,
208
+ timingSource,
209
+ sampleSpanSec,
210
+ distanceKm,
211
+ completionPct,
212
+ sectorTimes,
213
+ sectorResidualSec,
214
+ incidents,
215
+ flags,
216
+ sampleCount: idx.length,
217
+ firstSampleIndex: idx[0],
218
+ lastSampleIndex: idx[idx.length - 1]
219
+ };
220
+ }
221
+ function computeSectors(acc, next, ctx) {
222
+ const { boundaries, timeArr } = ctx;
223
+ if (boundaries.length === 0 || acc.lapNumber === 0)
224
+ return [];
225
+ const pctAt = (i) => pctAtIndex(i, ctx);
226
+ const crossing = (targetPct) => {
227
+ let prevI = null;
228
+ for (const i of acc.indices) {
229
+ const p = pctAt(i);
230
+ if (p == null)
231
+ continue;
232
+ if (prevI != null) {
233
+ const p0 = pctAt(prevI);
234
+ if (p0 <= targetPct && p >= targetPct && p >= p0) {
235
+ const t0 = timeArr[prevI];
236
+ const t1 = timeArr[i];
237
+ if (p === p0)
238
+ return t0;
239
+ return t0 + (targetPct - p0) / (p - p0) * (t1 - t0);
240
+ }
241
+ }
242
+ prevI = i;
243
+ }
244
+ return null;
245
+ };
246
+ const startT = crossingStart(acc, ctx) ?? timeArr[acc.indices[0]];
247
+ const times = [];
248
+ for (let b = 1;b < boundaries.length; b++) {
249
+ times.push(crossing(boundaries[b]));
250
+ }
251
+ times.push(crossingFinish(acc, next, ctx));
252
+ const sectors = [];
253
+ let prevT = startT;
254
+ for (let k = 0;k < times.length; k++) {
255
+ const t = times[k];
256
+ sectors.push({
257
+ sectorNum: k + 1,
258
+ timeSec: t != null && prevT != null ? t - prevT : null
259
+ });
260
+ if (t != null)
261
+ prevT = t;
262
+ }
263
+ return sectors;
264
+ }
265
+ function crossingStart(acc, ctx) {
266
+ const { timeArr } = ctx;
267
+ const firstI = acc.indices[0];
268
+ if (firstI === 0)
269
+ return null;
270
+ const prevI = firstI - 1;
271
+ const p0 = pctAtIndex(prevI, ctx);
272
+ const p1 = pctAtIndex(firstI, ctx);
273
+ if (p0 == null || p1 == null)
274
+ return null;
275
+ const t0 = timeArr[prevI];
276
+ const t1 = timeArr[firstI];
277
+ if (p0 > p1) {
278
+ const step = 1 - p0 + p1;
279
+ if (step <= 0)
280
+ return (t0 + t1) / 2;
281
+ return t0 + (1 - p0) / step * (t1 - t0);
282
+ }
283
+ return null;
284
+ }
285
+ function coverageComplete(acc, ctx, completionPct) {
286
+ if (completionPct != null) {
287
+ return completionPct >= NORMALIZE_THRESHOLDS.MIN_COMPLETION_PCT;
288
+ }
289
+ let minP = Infinity;
290
+ let maxP = -Infinity;
291
+ for (const i of acc.indices) {
292
+ const p = pctAtIndex(i, ctx);
293
+ if (p == null)
294
+ continue;
295
+ if (p < minP)
296
+ minP = p;
297
+ if (p > maxP)
298
+ maxP = p;
299
+ }
300
+ return maxP > minP && maxP - minP >= NORMALIZE_THRESHOLDS.MIN_COMPLETION_PCT;
301
+ }
302
+ function crossingFinish(acc, next, ctx) {
303
+ if (!next || next.indices.length === 0)
304
+ return null;
305
+ const { timeArr } = ctx;
306
+ const lastI = acc.indices[acc.indices.length - 1];
307
+ const firstNext = next.indices[0];
308
+ const p0 = pctAtIndex(lastI, ctx);
309
+ const p1 = pctAtIndex(firstNext, ctx);
310
+ if (p0 == null || p1 == null)
311
+ return null;
312
+ const t0 = timeArr[lastI];
313
+ const t1 = timeArr[firstNext];
314
+ const step = 1 - p0 + p1;
315
+ if (step <= 0)
316
+ return (t0 + t1) / 2;
317
+ return t0 + (1 - p0) / step * (t1 - t0);
318
+ }
319
+ function pctAtIndex(i, ctx) {
320
+ if (ctx.pctArr) {
321
+ const p = ctx.pctArr[i];
322
+ return Number.isFinite(p) && p >= 0 ? p : null;
323
+ }
324
+ if (ctx.distArr && ctx.trackLengthKm && ctx.trackLengthKm > 0) {
325
+ const d = ctx.distArr[i];
326
+ return Number.isFinite(d) ? d / (ctx.trackLengthKm * 1000) : null;
327
+ }
328
+ return null;
329
+ }
330
+ function firstFinite(arr, indices) {
331
+ for (const i of indices) {
332
+ const v = arr[i];
333
+ if (Number.isFinite(v))
334
+ return v;
335
+ }
336
+ return null;
337
+ }
338
+ function lastFinite(arr, indices) {
339
+ for (let k = indices.length - 1;k >= 0; k--) {
340
+ const v = arr[indices[k]];
341
+ if (Number.isFinite(v))
342
+ return v;
343
+ }
344
+ return null;
345
+ }
346
+ export {
347
+ normalizeIbt,
348
+ NORMALIZE_THRESHOLDS
349
+ };
@@ -0,0 +1,20 @@
1
+ import {
2
+ cleanYamlFast,
3
+ decodeSessionYaml,
4
+ parseSessionYaml,
5
+ parseTrackLengthKm,
6
+ readSessionInfoRange,
7
+ resolveSessionMetadata,
8
+ sessionMetadataFromYamlBytes,
9
+ sessionTypeForSessionNum
10
+ } from "./ibt-kagktz7r.js";
11
+ export {
12
+ sessionTypeForSessionNum,
13
+ sessionMetadataFromYamlBytes,
14
+ resolveSessionMetadata,
15
+ readSessionInfoRange,
16
+ parseTrackLengthKm,
17
+ parseSessionYaml,
18
+ decodeSessionYaml,
19
+ cleanYamlFast
20
+ };
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Canonical telemetry normalization — the single source of truth for what a
3
+ * lap IS across HotLap.ai (UI, Edge Functions, CLI).
4
+ *
5
+ * Implements RFC-0001 (hotlap.ai repo, docs/rfcs/RFC-0001-canonical-telemetry-parser.md):
6
+ * - C2: official lap time from the sim's LapLastLapTime channel, with
7
+ * crossing-interpolation fallback. Sample striding is never an input.
8
+ * - C3: a lap is `completed` only when the sim scored it.
9
+ * - C4: sectors computed at full resolution, numbered 1..N (iRacing S1..SN),
10
+ * no silent rescaling — the residual is reported instead.
11
+ * - C5: session metadata resolved via the SessionNum channel against the
12
+ * YAML Sessions list; multi-session files split per session.
13
+ * - C7: classify, don't drop — every observed lap appears with flags.
14
+ *
15
+ * RUNTIME NEUTRALITY: this module must not import Bun/Deno/Node APIs.
16
+ * It consumes an already-parsed IBT source (see IbtSource).
17
+ */
18
+ /** Structural subset of the IBT class this module needs (keeps it testable). */
19
+ export interface IbtSource {
20
+ readonly tickRate: number;
21
+ readonly recordCount: number;
22
+ getAllTyped(key: string): Float32Array | Float64Array | Int32Array | Uint32Array | null;
23
+ getAll(key: string): unknown[] | null;
24
+ getSessionInfo<T = unknown>(key?: string): T | null;
25
+ }
26
+ export type LapFlag = 'out-lap' | 'no-lap-last-time' | 'low-completion' | 'incident-delta' | 'unrealistic-speed' | 'too-few-samples' | 'no-sector-data';
27
+ export type TimingSource = 'lap-last-lap-time' | 'crossing-interpolation' | 'sample-span';
28
+ export interface NormalizedSector {
29
+ /** 1-based, matches iRacing S1/S2/S3 display convention. */
30
+ sectorNum: number;
31
+ timeSec: number | null;
32
+ }
33
+ export interface NormalizedLap {
34
+ lapNumber: number;
35
+ completed: boolean;
36
+ /** Official lap time (null when the lap was never completed/scored and no fallback exists). */
37
+ lapTimeSec: number | null;
38
+ timingSource: TimingSource | null;
39
+ /** Diagnostic: max−min SessionTime within the lap. Never the official time. */
40
+ sampleSpanSec: number;
41
+ distanceKm: number;
42
+ completionPct: number | null;
43
+ sectorTimes: NormalizedSector[];
44
+ /** lapTimeSec − Σ sectors; ≈0 when timing and sectors agree. Null without both. */
45
+ sectorResidualSec: number | null;
46
+ incidents: number;
47
+ flags: LapFlag[];
48
+ sampleCount: number;
49
+ /** First/last record indices of this lap in the file (for consumers extracting points). */
50
+ firstSampleIndex: number;
51
+ lastSampleIndex: number;
52
+ }
53
+ export interface NormalizedSession {
54
+ sessionNum: number;
55
+ /** Sessions[sessionNum].SessionType — full string, e.g. "Offline Testing", "Lone Qualify". */
56
+ sessionType: string | null;
57
+ sessionName: string | null;
58
+ /** WeekendInfo.EventType — event-level context, distinct from sessionType. */
59
+ eventType: string | null;
60
+ trackId: number | null;
61
+ trackName: string | null;
62
+ trackConfigName: string | null;
63
+ trackLengthKm: number | null;
64
+ seriesId: number | null;
65
+ seasonId: number | null;
66
+ sessionId: number | null;
67
+ subSessionId: number | null;
68
+ driverCarIdx: number;
69
+ driverName: string | null;
70
+ carId: number | null;
71
+ carScreenName: string | null;
72
+ carPath: string | null;
73
+ tickRate: number;
74
+ laps: NormalizedLap[];
75
+ }
76
+ export declare const NORMALIZE_THRESHOLDS: {
77
+ readonly MIN_SAMPLES: 10;
78
+ readonly MIN_COMPLETION_PCT: 0.9;
79
+ readonly MAX_AVG_SPEED_KMH: 220;
80
+ /**
81
+ * LapLastLapTime updates shortly after the line crossing. If the recording
82
+ * contains fewer than this many seconds of lap N+1, a missing update means
83
+ * "recording ended before the sim published the time", not "unscored lap".
84
+ */
85
+ readonly LLT_UPDATE_GRACE_SEC: 2;
86
+ };
87
+ /**
88
+ * Normalize a parsed .ibt file into one NormalizedSession per SessionNum
89
+ * observed in the channel data (RFC C5).
90
+ */
91
+ export declare function normalizeIbt(source: IbtSource): NormalizedSession[];
92
+ //# sourceMappingURL=normalize.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"normalize.d.ts","sourceRoot":"","sources":["../src/normalize.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAMH,gFAAgF;AAChF,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,YAAY,GAAG,YAAY,GAAG,UAAU,GAAG,WAAW,GAAG,IAAI,CAAC;IACxF,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,EAAE,GAAG,IAAI,CAAC;IACtC,cAAc,CAAC,CAAC,GAAG,OAAO,EAAE,GAAG,CAAC,EAAE,MAAM,GAAG,CAAC,GAAG,IAAI,CAAC;CACrD;AAMD,MAAM,MAAM,OAAO,GACf,SAAS,GACT,kBAAkB,GAClB,gBAAgB,GAChB,gBAAgB,GAChB,mBAAmB,GACnB,iBAAiB,GACjB,gBAAgB,CAAC;AAErB,MAAM,MAAM,YAAY,GACpB,mBAAmB,GACnB,wBAAwB,GACxB,aAAa,CAAC;AAElB,MAAM,WAAW,gBAAgB;IAC/B,4DAA4D;IAC5D,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;CACxB;AAED,MAAM,WAAW,aAAa;IAC5B,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,OAAO,CAAC;IACnB,+FAA+F;IAC/F,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,YAAY,EAAE,YAAY,GAAG,IAAI,CAAC;IAClC,+EAA+E;IAC/E,aAAa,EAAE,MAAM,CAAC;IACtB,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,WAAW,EAAE,gBAAgB,EAAE,CAAC;IAChC,mFAAmF;IACnF,iBAAiB,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,OAAO,EAAE,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,2FAA2F;IAC3F,gBAAgB,EAAE,MAAM,CAAC;IACzB,eAAe,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,iBAAiB;IAChC,UAAU,EAAE,MAAM,CAAC;IACnB,8FAA8F;IAC9F,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,8EAA8E;IAC9E,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,aAAa,EAAE,CAAC;CACvB;AAMD,eAAO,MAAM,oBAAoB;;;;IAI/B;;;;OAIG;;CAEK,CAAC;AAuCX;;;GAGG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,SAAS,GAAG,iBAAiB,EAAE,CA2EnE"}
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Fast single-pass YAML cleaning function.
3
+ * Combines multiple string operations to reduce intermediate allocations.
4
+ */
5
+ export declare function cleanYamlFast(yaml: string): string;
6
+ /**
7
+ * Read the session-info byte range from the .ibt fixed header (first 48+
8
+ * bytes are sufficient). For consumers that range-download instead of
9
+ * loading the whole file.
10
+ */
11
+ export declare function readSessionInfoRange(headerBytes: ArrayBuffer | Uint8Array): {
12
+ sessionInfoLen: number;
13
+ sessionInfoOffset: number;
14
+ };
15
+ /**
16
+ * Canonical text decode (RFC C5/D5): UTF-8 first; if replacement characters
17
+ * appear, fall back to windows-1252 (older iRacing builds and non-ASCII
18
+ * driver/team names). A plain lossy UTF-8 decode corrupts those names —
19
+ * that was divergence D5.
20
+ */
21
+ export declare function decodeSessionYaml(bytes: Uint8Array): string;
22
+ export interface SessionYamlDoc {
23
+ WeekendInfo?: Record<string, unknown>;
24
+ DriverInfo?: {
25
+ DriverCarIdx?: number;
26
+ Drivers?: Array<Record<string, unknown>>;
27
+ [key: string]: unknown;
28
+ };
29
+ SplitTimeInfo?: {
30
+ Sectors?: Array<{
31
+ SectorNum?: number;
32
+ SectorStartPct?: number;
33
+ }>;
34
+ };
35
+ SessionInfo?: {
36
+ Sessions?: Array<Record<string, unknown>>;
37
+ };
38
+ [key: string]: unknown;
39
+ }
40
+ /** Clean + parse the raw YAML text with a real YAML parser (never regex). */
41
+ export declare function parseSessionYaml(raw: string): SessionYamlDoc | null;
42
+ export interface ResolvedSessionEntry {
43
+ sessionNum: number | null;
44
+ /** Full string, e.g. "Offline Testing", "Lone Qualify" — never truncated. */
45
+ sessionType: string | null;
46
+ sessionName: string | null;
47
+ raw: Record<string, unknown>;
48
+ }
49
+ export interface SessionMetadata {
50
+ trackId: number | null;
51
+ trackName: string | null;
52
+ trackConfigName: string | null;
53
+ trackLengthKm: number | null;
54
+ trackNumTurns: number | null;
55
+ eventType: string | null;
56
+ category: string | null;
57
+ seriesId: number | null;
58
+ seasonId: number | null;
59
+ sessionId: number | null;
60
+ subSessionId: number | null;
61
+ raceWeek: number | null;
62
+ official: boolean | null;
63
+ driverCarIdx: number;
64
+ driverName: string | null;
65
+ driverUserId: number | null;
66
+ carId: number | null;
67
+ carScreenName: string | null;
68
+ carPath: string | null;
69
+ carClassId: number | null;
70
+ carNumber: string | null;
71
+ teamName: string | null;
72
+ sessions: ResolvedSessionEntry[];
73
+ /** Raw parsed doc for specialized consumers (results positions, sectors…). */
74
+ doc: SessionYamlDoc;
75
+ }
76
+ export declare function parseTrackLengthKm(value: unknown): number | null;
77
+ export declare function resolveSessionMetadata(doc: SessionYamlDoc): SessionMetadata;
78
+ /**
79
+ * Session-type resolution per RFC C5: the type of the session the telemetry
80
+ * was recorded in is Sessions[n].SessionType where n comes from the
81
+ * SessionNum CHANNEL — never "the first SessionType in the document" (that
82
+ * was divergence D4) and never WeekendInfo.EventType (kept separately).
83
+ */
84
+ export declare function sessionTypeForSessionNum(meta: Pick<SessionMetadata, 'sessions'>, sessionNum: number): string | null;
85
+ /** Convenience: raw YAML byte range → resolved metadata (or null on parse failure). */
86
+ export declare function sessionMetadataFromYamlBytes(bytes: Uint8Array): SessionMetadata | null;
87
+ //# sourceMappingURL=session-yaml.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-yaml.d.ts","sourceRoot":"","sources":["../src/session-yaml.ts"],"names":[],"mappings":"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"}
@@ -2,10 +2,11 @@
2
2
  // @bun
3
3
  import {
4
4
  createIRSDK
5
- } from "./live.js";
6
- import"./index-k5bgtn4c.js";
7
- import"./binary-reader.js";
8
- import"./constants.js";
5
+ } from "./index-t4qz2011.js";
6
+ import"./index-hxw291vy.js";
7
+ import"./index-c5jf6rjt.js";
8
+ import"./index-wb6ebd0j.js";
9
+ import"./index-ftpvnye9.js";
9
10
 
10
11
  // src/telemetry-server.ts
11
12
  var TELEMETRY_PORT = 32100;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@hotlap.ai/telemetry",
3
- "version": "1.0.0",
4
- "description": "High-performance iRacing SDK for Bun - telemetry parsing and live data",
3
+ "version": "1.2.0",
4
+ "description": "High-performance iRacing telemetry library for Bun - IBT file parsing and live data streaming",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
7
7
  "module": "./dist/index.js",
@@ -17,6 +17,21 @@
17
17
  "import": "./dist/ibt.js",
18
18
  "default": "./dist/ibt.js"
19
19
  },
20
+ "./core": {
21
+ "types": "./dist/ibt.d.ts",
22
+ "import": "./dist/neutral/ibt.js",
23
+ "default": "./dist/neutral/ibt.js"
24
+ },
25
+ "./normalize": {
26
+ "types": "./dist/normalize.d.ts",
27
+ "import": "./dist/neutral/normalize.js",
28
+ "default": "./dist/neutral/normalize.js"
29
+ },
30
+ "./session": {
31
+ "types": "./dist/session-yaml.d.ts",
32
+ "import": "./dist/neutral/session-yaml.js",
33
+ "default": "./dist/neutral/session-yaml.js"
34
+ },
20
35
  "./live": {
21
36
  "types": "./dist/live.d.ts",
22
37
  "import": "./dist/live.js",
@@ -39,7 +54,8 @@
39
54
  ],
40
55
  "scripts": {
41
56
  "build": "bun run build:js && bun run build:types",
42
- "build:js": "bun build ./src/index.ts ./src/ibt.ts ./src/live.ts ./src/constants.ts ./src/cli.ts ./src/telemetry-server.ts ./src/types.ts ./src/binary-reader.ts --outdir ./dist --target bun --splitting",
57
+ "build:js": "bun build ./src/index.ts ./src/ibt.ts ./src/live.ts ./src/constants.ts ./src/cli.ts ./src/telemetry-server.ts ./src/types.ts ./src/binary-reader.ts --outdir ./dist --target bun --splitting && bun run build:neutral",
58
+ "build:neutral": "bun build ./src/ibt.ts ./src/normalize.ts ./src/session-yaml.ts --outdir ./dist/neutral --target browser --splitting",
43
59
  "build:types": "tsc --emitDeclarationOnly --declaration --declarationMap --outDir ./dist",
44
60
  "build:server": "bun build ./src/telemetry-server.ts --compile --outfile ./dist/hotlap-telemetry",
45
61
  "clean": "rm -rf dist",
@@ -48,6 +64,8 @@
48
64
  "server": "bun run src/telemetry-server.ts",
49
65
  "typecheck": "tsc --noEmit",
50
66
  "test": "bun test",
67
+ "fixtures:pull": "bun run scripts/fixtures-pull.ts",
68
+ "gate:deno": "deno run scripts/deno-gate.ts",
51
69
  "bench": "bun run bench/parse-ibt.bench.ts",
52
70
  "prepublishOnly": "bun run clean && bun run build"
53
71
  },