fit-file-parser 6.1.1 → 6.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,53 @@
1
1
  # Change Log
2
2
 
3
+ ## 6.2.0 - 2026-10-10
4
+
5
+ ### Added
6
+
7
+ - Add `parseFilesAsync()` to return every complete FIT file in concatenated
8
+ input as a separate result, with independent decoder state and file order.
9
+ - Decode documented session and lap swimming summaries, segment and gear-shift
10
+ counts, lap bounds, metabolic calories, and grade-adjusted speed.
11
+ - Retain every battery sample in `batteries` and `messages.battery`, including
12
+ timestamp and capacity percentage when present.
13
+ - Add `--all-files` to the external corpus checker to validate appended files.
14
+
15
+ ### Fixed
16
+
17
+ - Preserve exact bytes for developer fields without usable names in opt-in
18
+ `unmapped_messages`, including fields whose descriptions have been cached.
19
+ - Recover unsigned-byte record temperatures from 0 through 126 with normal
20
+ unit conversion, while retaining their original bytes in opt-in output.
21
+ - Reject records and definitions extending beyond the declared data section
22
+ when using the complete-file `parseFilesAsync()` API.
23
+
24
+ ### Compatibility
25
+
26
+ - Existing `parseAsync()` and callback APIs retain their single-file output
27
+ and recovery behavior. Newly supported fields and battery collections are
28
+ additive.
29
+ - `parseFilesAsync()` returns a one-element array for ordinary FIT input and
30
+ rejects incomplete files or trailing non-FIT data, even with `force: true`.
31
+ CRC validation follows the caller's `force` option for every file.
32
+ - Unsigned temperature bytes from 127 through 255 and other undocumented or
33
+ incompatible fields remain undecoded and available through opt-in retention.
34
+
35
+ ## 6.1.2 - 2026-09-22
36
+
37
+ ### Changed
38
+
39
+ - Keep the manufacturer, Garmin product, sport, sub-sport, and course-point
40
+ lookup maps in one authoritative internal module shared by the full decoder
41
+ profile and the lightweight `fit-file-parser/profile` entry point.
42
+ - Prevent lookup-only consumers from loading the full decoder profile and its
43
+ message definitions.
44
+ - Enforce the lookup entry point's dependency graph and a 25 KB minified bundle
45
+ ceiling in the profile checks run by CI.
46
+
47
+ ### Compatibility
48
+
49
+ - Profile mappings, parsed output, and the public TypeScript API are unchanged.
50
+
3
51
  ## 6.1.1 - 2026-09-21
4
52
 
5
53
  ### Added
package/PROFILE.md CHANGED
@@ -1,11 +1,18 @@
1
1
  # FIT Profile Maintenance
2
2
 
3
- The parser uses one static interoperability table in `src/profile.ts` for
4
- normal installs, builds, tests, and releases. It contains message and field
5
- identifiers, names, wire types, array behavior, scales, offsets, units, enum
6
- labels, and product identifiers. `npm run profile:check` pins full-table
7
- fingerprints and the resulting message, field, type, enum-value, and product
8
- counts so accidental drift fails CI.
3
+ The parser uses one static interoperability profile for normal installs,
4
+ builds, tests, and releases. Message definitions and general enum tables live
5
+ in `src/profile.ts`; the manufacturer, Garmin product, sport, sub-sport, and
6
+ course-point tables live in `src/profile-lookup-data.ts`. The full profile
7
+ composes those exact objects, so lookup-only consumers and the decoder share
8
+ one authoritative value for every mapping. `npm run profile:check` pins
9
+ full-table fingerprints and the resulting message, field, type, enum-value,
10
+ and product counts so accidental drift fails CI.
11
+
12
+ `npm run profile:budget` also bundles the lookup entry point in isolation. It
13
+ fails if anything beyond the lookup helpers and their authoritative data enters
14
+ that graph, or if the minified bundle exceeds 25,000 bytes. Review any budget
15
+ change together with the corresponding profile growth.
9
16
 
10
17
  ## Unknown and newer fields
11
18
 
@@ -24,12 +31,30 @@ complete raw representation of selected messages can continue to use
24
31
  `includeRawMessages`; `unmapped_messages` stores only the fields the maintained
25
32
  table cannot decode.
26
33
 
34
+ The unsigned-byte record-temperature interoperability recovery also retains
35
+ the original byte here, even when its unambiguous value is decoded.
36
+
37
+ ## Focused interoperability additions
38
+
39
+ Session fields 79, 80, 106, 107, 108, and 211; lap fields 27–30, 73, 90, 96,
40
+ 97, 155, and 161; and battery message 104 fields 2 and 253 were checked against
41
+ independently observed wire definitions in the external FIT corpus and the
42
+ redistributable interoperability definitions for
43
+ [session](https://github.com/muktihari/fit/blob/126e1a11233f87b7f139254d991bc3d2d952b62c/profile/mesgdef/session_gen.go),
44
+ [lap](https://github.com/muktihari/fit/blob/126e1a11233f87b7f139254d991bc3d2d952b62c/profile/mesgdef/lap_gen.go),
45
+ and [battery](https://github.com/muktihari/fit/blob/126e1a11233f87b7f139254d991bc3d2d952b62c/profile/mesgdef/battery_gen.go).
46
+ The focused mappings expose swimming summaries, segment and shift counts,
47
+ lap bounds, metabolic calories, grade-adjusted speed, and battery samples.
48
+ Synthetic regression tests pin identifiers, wire types, scales, units, invalid
49
+ sentinels, and collection behavior. Other battery fields remain unmapped.
50
+
27
51
  ## Updating the table
28
52
 
29
53
  Every mapping change requires focused regression coverage. Do not guess
30
54
  adjacent identifiers or add a mapping solely to make `unmapped_messages`
31
- disappear. Update `src/profile.ts`, the relevant tests, and the public types,
32
- then verify the complete compatibility contract:
55
+ disappear. Update the owning table in `src/profile.ts` or
56
+ `src/profile-lookup-data.ts`, the relevant tests, and the public types, then
57
+ verify the complete compatibility contract:
33
58
 
34
59
  ```sh
35
60
  npm run codegen
@@ -37,7 +62,13 @@ npm run profile:check
37
62
  npm run check
38
63
  npm run compatibility:check -- /path/to/reference-package /path/to/fit-corpus
39
64
  npm run corpus:check -- ../FIT-test-files --allow-force-recovery
65
+ npm run corpus:check -- ../FIT-test-files --allow-force-recovery --all-files
40
66
  ```
41
67
 
42
68
  `src/fit_types.ts` is generated from the maintained table and must not be
43
69
  edited manually.
70
+
71
+ `--all-files` validates every appended FIT file through `parseFilesAsync()` and
72
+ reports both parsed FIT file counts and concatenated input counts. Recovery
73
+ counts apply to whole inputs; unmapped summaries count each parsed FIT file
74
+ separately in this mode.
package/README.md CHANGED
@@ -11,6 +11,7 @@ including developer-defined data.
11
11
  ## Features
12
12
 
13
13
  - Parse Node.js `Buffer` and standard `ArrayBuffer` inputs.
14
+ - Parse concatenated FIT files as separate results with `parseFilesAsync()`.
14
15
  - Choose flat lists, nested activity data, or both output shapes.
15
16
  - Convert speed, length, temperature, and pressure fields to preferred units.
16
17
  - Decode developer fields while preserving record alignment when descriptions
@@ -33,7 +34,7 @@ npm install fit-file-parser
33
34
 
34
35
  ## Profile coverage
35
36
 
36
- The static profile contains 126 messages, 1,444 fields, 200 types, and 4,403
37
+ The static profile contains 127 messages, 1,462 fields, 200 types, and 4,406
37
38
  mapped values. Pass `includeUnmappedMessages: true` to retain fields outside
38
39
  that maintained surface by number and exact bytes; applications that need all
39
40
  bytes for recognized messages can also opt into `raw_messages`.
@@ -62,6 +63,27 @@ console.log({
62
63
  })
63
64
  ```
64
65
 
66
+ ### Concatenated FIT files
67
+
68
+ Some exports append several complete FIT files into one input. Use
69
+ `parseFilesAsync()` to return a `ParsedFit[]` in file order:
70
+
71
+ ```javascript
72
+ const files = await new FitParser({ force: false }).parseFilesAsync(content)
73
+ for (const file of files) {
74
+ console.log(file.sessions?.length ?? 0)
75
+ }
76
+ ```
77
+
78
+ Each result has independent message definitions, developer descriptions,
79
+ timestamps, and activity collections. Results are never merged. A single FIT
80
+ file returns a one-element array. Empty input, incomplete files (including
81
+ missing file CRC bytes), and trailing non-FIT bytes reject the entire call,
82
+ even with `force: true`. Records and definitions extending beyond the declared
83
+ data section are also rejected. CRC validation follows the `force` option for every
84
+ file. Failures reject with an `Error`. The existing `parseAsync()` and callback
85
+ APIs continue to parse the first FIT file only.
86
+
65
87
  ### Callback API
66
88
 
67
89
  ```javascript
@@ -86,8 +108,9 @@ readFile('./activity.fit', (readError, content) => {
86
108
  })
87
109
  ```
88
110
 
89
- Parser errors are strings. `parseAsync()` rejects with the same value that the
90
- callback API receives as its first argument.
111
+ The callback reports validation errors as strings. `parseAsync()` rejects with
112
+ those values; decoder failures can reject with an `Error`. `parseFilesAsync()`
113
+ consistently rejects with an `Error`.
91
114
 
92
115
  ## Parser options
93
116
 
@@ -148,8 +171,8 @@ message numbers.
148
171
 
149
172
  Set `includeUnmappedMessages: true` to return fields that are absent from the
150
173
  maintained profile or belong to an unknown global message in
151
- `unmapped_messages`. Only unmapped fields are retained, so normal known fields
152
- are not duplicated:
174
+ `unmapped_messages`. Fields without usable developer names are also retained.
175
+ Normal known fields are not duplicated:
153
176
 
154
177
  ```javascript
155
178
  const data = await new FitParser({ includeUnmappedMessages: true })
@@ -169,6 +192,20 @@ Each entry uses the same wire-level field representation as `raw_messages`.
169
192
  This opt-in ensures a newer FIT field cannot disappear merely because it
170
193
  does not yet have a reviewed semantic mapping.
171
194
 
195
+ For record temperature encoded as one unsigned byte, values from 0 through
196
+ 126 can be decoded as Celsius before the selected temperature conversion.
197
+ Values from 127 through 255 remain undecoded. This narrow interoperability
198
+ recovery preserves the original unsigned byte in `unmapped_messages` when
199
+ enabled, including values that also receive a decoded temperature.
200
+
201
+ ### Battery samples
202
+
203
+ `batteries` retains every battery message in file order, including its
204
+ timestamp and capacity percentage when present. These samples are also
205
+ available in `messages.battery`. Undocumented battery fields remain available
206
+ through `includeUnmappedMessages`; the parser does not infer their device
207
+ association or calculate battery consumption.
208
+
172
209
  ### Lossless selected messages
173
210
 
174
211
  Set `includeRawMessages` when a consumer needs native FIT numeric codes rather
@@ -240,8 +277,9 @@ unchanged.
240
277
  ## Profile-backed output
241
278
 
242
279
  Recognized message names, field names, enum values, wire types, scales,
243
- offsets, arrays, and units come from the community-maintained table in
244
- `src/profile.ts`. Its maintenance and verification contract is documented in
280
+ offsets, arrays, and units come from the community-maintained profile in
281
+ `src/profile.ts` and `src/profile-lookup-data.ts`. Its maintenance and
282
+ verification contract is documented in
245
283
  [`PROFILE.md`](./PROFILE.md).
246
284
 
247
285
  Public names use `snake_case` while preserving established alphanumeric tokens
@@ -443,9 +481,10 @@ a reference package, compares their complete default outputs, and reports only
443
481
  aggregate counts. Files rejected by both strict parsers are retried in force
444
482
  mode.
445
483
 
446
- Edit `src/profile.ts` only through reviewed profile changes with focused
447
- regression coverage and a documented source. Do not edit `src/fit_types.ts`
448
- manually; run `npm run codegen` after changing the maintained profile.
484
+ Edit `src/profile.ts` and `src/profile-lookup-data.ts` only through reviewed
485
+ profile changes with focused regression coverage and a documented source. Do
486
+ not edit `src/fit_types.ts` manually; run `npm run codegen` after changing the
487
+ maintained profile.
449
488
 
450
489
  Repository-specific automation guidance is tracked in
451
490
  [`.agent/README.md`](./.agent/README.md). More examples are available in the
package/dist/binary.js CHANGED
@@ -305,6 +305,7 @@ function applyOptions(data, field, options, fields) {
305
305
  case 'enhanced_speed':
306
306
  case 'vertical_speed':
307
307
  case 'avg_speed':
308
+ case 'avg_grade_adjusted_speed':
308
309
  case 'max_speed':
309
310
  case 'speed1s':
310
311
  case 'ball_speed':
@@ -465,7 +466,12 @@ export function readRecord(blob, messageTypes, developerFields, startIndex, opti
465
466
  const fieldSize = blob[fDefIndex + 1];
466
467
  const wireType = FIT.types.fit_base_type[baseType];
467
468
  const { field, type, baseType: profileBaseType, array, scale, offset, units, } = message.getAttributes(blob[fDefIndex]);
468
- const profileCompatible = areProfileBaseTypesCompatible(profileBaseType, wireType);
469
+ // Narrow producer interoperability case: only the shared, non-sentinel
470
+ // temperature range is decoded below. All original bytes remain unmapped.
471
+ const unsignedTemperature = mTypeDef.globalMessageNumber === 20
472
+ && fieldNumber === 13 && fieldSize === 1
473
+ && profileBaseType === 'sint8' && wireType === 'uint8';
474
+ const profileCompatible = unsignedTemperature || areProfileBaseTypesCompatible(profileBaseType, wireType);
469
475
  const fDef = {
470
476
  type: profileCompatible ? type : wireType,
471
477
  rawType: wireType,
@@ -539,7 +545,11 @@ export function readRecord(blob, messageTypes, developerFields, startIndex, opti
539
545
  rawData[i] = InvalidFieldData;
540
546
  continue;
541
547
  }
542
- if ((rawFields || (unmappedFields && !isOutputFieldName(fDef.name)))
548
+ const unsignedTemperature = messageType.globalMessageNumber === 20
549
+ && fDef.fDefNo === 13 && fDef.size === 1
550
+ && fDef.type === 'sint8' && fDef.rawType === 'uint8';
551
+ const preserveNative = !isOutputFieldName(fDef.name) || unsignedTemperature;
552
+ if ((rawFields || (unmappedFields && preserveNative))
543
553
  && readDataFromIndex + fDef.size <= dataEnd) {
544
554
  const rawField = {
545
555
  fieldDefinitionNumber: fDef.fDefNo,
@@ -547,11 +557,13 @@ export function readRecord(blob, messageTypes, developerFields, startIndex, opti
547
557
  rawValue: Array.from(blob.subarray(readDataFromIndex, readDataFromIndex + fDef.size)),
548
558
  };
549
559
  rawFields === null || rawFields === void 0 ? void 0 : rawFields.push(rawField);
550
- if (!isOutputFieldName(fDef.name)) {
560
+ if (preserveNative) {
551
561
  unmappedFields === null || unmappedFields === void 0 ? void 0 : unmappedFields.push(rawField);
552
562
  }
553
563
  }
554
- const data = readData(blob, dataView, fDef, readDataFromIndex);
564
+ const data = unsignedTemperature && blob[readDataFromIndex] >= 127
565
+ ? InvalidFieldData
566
+ : readData(blob, dataView, fDef, readDataFromIndex);
555
567
  if (data !== InvalidFieldData
556
568
  && !isInvalidValue(data, fDef.type)
557
569
  && !isInvalidBaseTypeValue(data, fDef.baseTypeNo)) {
@@ -573,20 +585,21 @@ export function readRecord(blob, messageTypes, developerFields, startIndex, opti
573
585
  for (let i = 0; i < developerFieldDefs.length; i++) {
574
586
  const developerFieldDef = developerFieldDefs[i];
575
587
  const rawDataIndex = messageType.fieldDefs.length + i;
576
- let rawDeveloperField;
577
- if ((rawDeveloperFields
578
- || (unmappedDeveloperFields && developerFieldDef.resolvedFieldDef === undefined))
588
+ // Resolve before deciding whether raw retention is needed. A resolved
589
+ // description can still have no usable output name, including after cache use.
590
+ const fDef = resolveDeveloperFieldDefinition(developerFieldDef, messageType.littleEndian, developerFields, options);
591
+ const preserveDeveloper = !fDef || !isOutputFieldName(fDef.name);
592
+ if ((rawDeveloperFields || (unmappedDeveloperFields && preserveDeveloper))
579
593
  && readDataFromIndex + developerFieldDef.size <= dataEnd) {
580
- rawDeveloperField = {
594
+ const rawDeveloperField = {
581
595
  developerDataIndex: developerFieldDef.developerDataIndex,
582
596
  fieldDefinitionNumber: developerFieldDef.fieldDefinitionNumber,
583
597
  rawValue: Array.from(blob.subarray(readDataFromIndex, readDataFromIndex + developerFieldDef.size)),
584
598
  };
585
599
  rawDeveloperFields === null || rawDeveloperFields === void 0 ? void 0 : rawDeveloperFields.push(rawDeveloperField);
586
- }
587
- const fDef = resolveDeveloperFieldDefinition(developerFieldDef, messageType.littleEndian, developerFields, options);
588
- if (!fDef && rawDeveloperField) {
589
- unmappedDeveloperFields === null || unmappedDeveloperFields === void 0 ? void 0 : unmappedDeveloperFields.push(rawDeveloperField);
600
+ if (preserveDeveloper) {
601
+ unmappedDeveloperFields === null || unmappedDeveloperFields === void 0 ? void 0 : unmappedDeveloperFields.push(rawDeveloperField);
602
+ }
590
603
  }
591
604
  if (fDef) {
592
605
  const data = readData(blob, dataView, fDef, readDataFromIndex);
@@ -311,6 +311,7 @@ function applyOptions(data, field, options, fields) {
311
311
  case 'enhanced_speed':
312
312
  case 'vertical_speed':
313
313
  case 'avg_speed':
314
+ case 'avg_grade_adjusted_speed':
314
315
  case 'max_speed':
315
316
  case 'speed1s':
316
317
  case 'ball_speed':
@@ -471,7 +472,12 @@ function readRecord(blob, messageTypes, developerFields, startIndex, options, st
471
472
  const fieldSize = blob[fDefIndex + 1];
472
473
  const wireType = fit_js_1.FIT.types.fit_base_type[baseType];
473
474
  const { field, type, baseType: profileBaseType, array, scale, offset, units, } = message.getAttributes(blob[fDefIndex]);
474
- const profileCompatible = areProfileBaseTypesCompatible(profileBaseType, wireType);
475
+ // Narrow producer interoperability case: only the shared, non-sentinel
476
+ // temperature range is decoded below. All original bytes remain unmapped.
477
+ const unsignedTemperature = mTypeDef.globalMessageNumber === 20
478
+ && fieldNumber === 13 && fieldSize === 1
479
+ && profileBaseType === 'sint8' && wireType === 'uint8';
480
+ const profileCompatible = unsignedTemperature || areProfileBaseTypesCompatible(profileBaseType, wireType);
475
481
  const fDef = {
476
482
  type: profileCompatible ? type : wireType,
477
483
  rawType: wireType,
@@ -545,7 +551,11 @@ function readRecord(blob, messageTypes, developerFields, startIndex, options, st
545
551
  rawData[i] = InvalidFieldData;
546
552
  continue;
547
553
  }
548
- if ((rawFields || (unmappedFields && !isOutputFieldName(fDef.name)))
554
+ const unsignedTemperature = messageType.globalMessageNumber === 20
555
+ && fDef.fDefNo === 13 && fDef.size === 1
556
+ && fDef.type === 'sint8' && fDef.rawType === 'uint8';
557
+ const preserveNative = !isOutputFieldName(fDef.name) || unsignedTemperature;
558
+ if ((rawFields || (unmappedFields && preserveNative))
549
559
  && readDataFromIndex + fDef.size <= dataEnd) {
550
560
  const rawField = {
551
561
  fieldDefinitionNumber: fDef.fDefNo,
@@ -553,11 +563,13 @@ function readRecord(blob, messageTypes, developerFields, startIndex, options, st
553
563
  rawValue: Array.from(blob.subarray(readDataFromIndex, readDataFromIndex + fDef.size)),
554
564
  };
555
565
  rawFields === null || rawFields === void 0 ? void 0 : rawFields.push(rawField);
556
- if (!isOutputFieldName(fDef.name)) {
566
+ if (preserveNative) {
557
567
  unmappedFields === null || unmappedFields === void 0 ? void 0 : unmappedFields.push(rawField);
558
568
  }
559
569
  }
560
- const data = readData(blob, dataView, fDef, readDataFromIndex);
570
+ const data = unsignedTemperature && blob[readDataFromIndex] >= 127
571
+ ? InvalidFieldData
572
+ : readData(blob, dataView, fDef, readDataFromIndex);
561
573
  if (data !== InvalidFieldData
562
574
  && !isInvalidValue(data, fDef.type)
563
575
  && !isInvalidBaseTypeValue(data, fDef.baseTypeNo)) {
@@ -579,20 +591,21 @@ function readRecord(blob, messageTypes, developerFields, startIndex, options, st
579
591
  for (let i = 0; i < developerFieldDefs.length; i++) {
580
592
  const developerFieldDef = developerFieldDefs[i];
581
593
  const rawDataIndex = messageType.fieldDefs.length + i;
582
- let rawDeveloperField;
583
- if ((rawDeveloperFields
584
- || (unmappedDeveloperFields && developerFieldDef.resolvedFieldDef === undefined))
594
+ // Resolve before deciding whether raw retention is needed. A resolved
595
+ // description can still have no usable output name, including after cache use.
596
+ const fDef = resolveDeveloperFieldDefinition(developerFieldDef, messageType.littleEndian, developerFields, options);
597
+ const preserveDeveloper = !fDef || !isOutputFieldName(fDef.name);
598
+ if ((rawDeveloperFields || (unmappedDeveloperFields && preserveDeveloper))
585
599
  && readDataFromIndex + developerFieldDef.size <= dataEnd) {
586
- rawDeveloperField = {
600
+ const rawDeveloperField = {
587
601
  developerDataIndex: developerFieldDef.developerDataIndex,
588
602
  fieldDefinitionNumber: developerFieldDef.fieldDefinitionNumber,
589
603
  rawValue: Array.from(blob.subarray(readDataFromIndex, readDataFromIndex + developerFieldDef.size)),
590
604
  };
591
605
  rawDeveloperFields === null || rawDeveloperFields === void 0 ? void 0 : rawDeveloperFields.push(rawDeveloperField);
592
- }
593
- const fDef = resolveDeveloperFieldDefinition(developerFieldDef, messageType.littleEndian, developerFields, options);
594
- if (!fDef && rawDeveloperField) {
595
- unmappedDeveloperFields === null || unmappedDeveloperFields === void 0 ? void 0 : unmappedDeveloperFields.push(rawDeveloperField);
606
+ if (preserveDeveloper) {
607
+ unmappedDeveloperFields === null || unmappedDeveloperFields === void 0 ? void 0 : unmappedDeveloperFields.push(rawDeveloperField);
608
+ }
596
609
  }
597
610
  if (fDef) {
598
611
  const data = readData(blob, dataView, fDef, readDataFromIndex);
@@ -28,5 +28,13 @@ export default class FitParser {
28
28
  private readonly options;
29
29
  constructor(options?: FitParserOptions);
30
30
  parseAsync(content: ArrayBuffer | Buffer<ArrayBuffer>): Promise<ParsedFit>;
31
+ /**
32
+ * Parses every complete FIT file in an input, preserving file order and
33
+ * independent decoder state. Rejects incomplete files and trailing bytes;
34
+ * each file uses this parser's options, including its CRC policy.
35
+ * Rejects with an Error when any file cannot be parsed.
36
+ */
37
+ parseFilesAsync(content: ArrayBuffer | Buffer<ArrayBuffer>): Promise<ParsedFit[]>;
31
38
  parse(content: ArrayBuffer | Buffer<ArrayBuffer>, callback: FitParserCallback): void;
39
+ private parseFile;
32
40
  }
@@ -1,4 +1,13 @@
1
1
  "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
2
11
  Object.defineProperty(exports, "__esModule", { value: true });
3
12
  exports.readFitUnsignedField = exports.readFitStringField = exports.readFitMessages = exports.getFitBaseTypeId = exports.fitTimestampToUnixMilliseconds = exports.FitMessageReaderError = exports.getFitSubSportName = exports.getFitSubSportId = exports.getFitSportName = exports.getFitSportId = exports.getFitManufacturerName = exports.getFitGarminProductName = exports.getFitGarminProductDisplayName = exports.getFitCoursePointId = exports.FitEncoder = exports.FitBaseType = void 0;
4
13
  const binary_js_1 = require("./binary.js");
@@ -51,7 +60,71 @@ class FitParser {
51
60
  });
52
61
  });
53
62
  }
63
+ /**
64
+ * Parses every complete FIT file in an input, preserving file order and
65
+ * independent decoder state. Rejects incomplete files and trailing bytes;
66
+ * each file uses this parser's options, including its CRC policy.
67
+ * Rejects with an Error when any file cannot be parsed.
68
+ */
69
+ parseFilesAsync(content) {
70
+ return __awaiter(this, void 0, void 0, function* () {
71
+ const blob = content instanceof ArrayBuffer
72
+ ? new Uint8Array(content)
73
+ : new Uint8Array(content.buffer, content.byteOffset, content.byteLength);
74
+ const results = [];
75
+ let start = 0;
76
+ do {
77
+ const remaining = blob.length - start;
78
+ if (remaining < 12) {
79
+ throw new Error('File to small to be a FIT file');
80
+ }
81
+ const headerLength = blob[start];
82
+ if (headerLength !== 12 && headerLength !== 14) {
83
+ throw new Error('Incorrect header size');
84
+ }
85
+ if (remaining < headerLength) {
86
+ throw new Error('File to small to be a FIT file');
87
+ }
88
+ if (blob[start + 8] !== 0x2E
89
+ || blob[start + 9] !== 0x46
90
+ || blob[start + 10] !== 0x49
91
+ || blob[start + 11] !== 0x54) {
92
+ throw new Error('Missing \'.FIT\' in header');
93
+ }
94
+ const dataLength = new DataView(blob.buffer, blob.byteOffset + start, remaining)
95
+ .getUint32(4, true);
96
+ const crcStart = start + headerLength + dataLength;
97
+ if (crcStart > blob.length) {
98
+ throw new Error('File data exceeds input length');
99
+ }
100
+ if (crcStart + 2 > blob.length) {
101
+ throw new Error('File CRC missing');
102
+ }
103
+ const end = crcStart + 2;
104
+ try {
105
+ results.push(yield new Promise((resolve, reject) => {
106
+ this.parseFile(blob.slice(start, end).buffer, (error, data) => {
107
+ if (error) {
108
+ reject(error);
109
+ }
110
+ else if (data) {
111
+ resolve(data);
112
+ }
113
+ }, true);
114
+ }));
115
+ }
116
+ catch (error) {
117
+ throw error instanceof Error ? error : new Error(String(error));
118
+ }
119
+ start = end;
120
+ } while (start < blob.length);
121
+ return results;
122
+ });
123
+ }
54
124
  parse(content, callback) {
125
+ this.parseFile(content, callback);
126
+ }
127
+ parseFile(content, callback, validateRecordBounds = false) {
55
128
  var _a, _b, _c;
56
129
  const blob = content instanceof ArrayBuffer
57
130
  ? new Uint8Array(content)
@@ -111,6 +184,7 @@ class FitParser {
111
184
  protocolVersion,
112
185
  };
113
186
  const messages = {};
187
+ const batteries = [];
114
188
  let sessions = [];
115
189
  let laps = [];
116
190
  const records = [];
@@ -160,6 +234,12 @@ class FitParser {
160
234
  let pausedTime = 0;
161
235
  while (loopIndex < crcStart) {
162
236
  const { globalMessageNumber, littleEndian, message, messageType, nextIndex, compressedTimestamp, rawFields: recordRawFields, rawDeveloperFields: recordRawDeveloperFields, unmappedFields: recordUnmappedFields, unmappedDeveloperFields: recordUnmappedDeveloperFields, } = (0, binary_js_1.readRecord)(blob, messageTypes, developerFields, loopIndex, this.options, startDate, pausedTime, dataView, decoderState, crcStart);
237
+ // Complete-file parsing also validates bounds without requiring raw
238
+ // retention. The single-file API keeps its existing recovery behavior.
239
+ if (validateRecordBounds
240
+ && (!Number.isInteger(nextIndex) || nextIndex <= loopIndex || nextIndex > crcStart)) {
241
+ throw new Error('Invalid FIT record bounds');
242
+ }
163
243
  loopIndex = nextIndex;
164
244
  if (globalMessageNumber !== undefined) {
165
245
  const messageIndex = (_a = messageCountsByGlobalNumber.get(globalMessageNumber)) !== null && _a !== void 0 ? _a : 0;
@@ -210,6 +290,9 @@ class FitParser {
210
290
  ((_b = messages[messageType]) !== null && _b !== void 0 ? _b : (messages[messageType] = [])).push(message);
211
291
  }
212
292
  switch (messageType) {
293
+ case 'battery':
294
+ batteries.push(message);
295
+ break;
213
296
  case 'lap':
214
297
  laps.push(message);
215
298
  break;
@@ -354,6 +437,7 @@ class FitParser {
354
437
  fitObj.time_in_zone = time_in_zone;
355
438
  fitObj.activity_metrics = activity_metrics;
356
439
  fitObj.user_metrics = user_metrics;
440
+ fitObj.batteries = batteries;
357
441
  fitObj.messages = messages;
358
442
  if (rawDeveloperFields) {
359
443
  fitObj.raw_developer_fields = rawDeveloperFields;