edfcore 0.2.33 → 0.2.35

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
@@ -6,6 +6,38 @@ alone does not tell you whether you were affected.
6
6
  edfcore is pre-1.0. Patch releases have carried behaviour changes where the old behaviour was a
7
7
  defect; those are called out below.
8
8
 
9
+ ## 0.2.35
10
+
11
+ - **Fixed** `byteSource` building a source over an argument that is not bytes, so the caller's
12
+ mistake was reported as a defect in the file. `new Uint8Array(x)` accepts almost anything: a
13
+ string, a plain object and `null` all yield an empty array, and a `number[]` yields one of the
14
+ wrong length. The source was constructed happily and the failure surfaced later as
15
+ `[SOURCE_TOO_SMALL] the header is 0 bytes` — the file blamed for the argument, which is the one
16
+ confusion this package works hardest to avoid. It now refuses at construction and says what it
17
+ wanted. `Int8Array` is refused by name: one byte per element, so it passes every length check
18
+ and then decodes to fabricated sample values.
19
+
20
+ ## 0.2.34
21
+
22
+ - **Added** the golden-value harness this README has withheld a numerical-interop claim for since
23
+ 0.1.0. edfcore now **reproduces pyEDFlib's float64 physical values bit for bit** on EDF and
24
+ 24-bit BDF, across symmetric and asymmetric declared ranges. `scripts/golden/generate.py` writes
25
+ the fixtures with pyEDFlib's own writer, reads them back with pyEDFlib, and records every sample
26
+ as its exact IEEE-754 bit pattern; the test compares with `Object.is`, so one ULP is a failure.
27
+ Nothing under `tests/corpus/golden/` was produced by edfcore, and the goldens are committed so
28
+ CI never needs Python.
29
+
30
+ Every previous test of the pinned scaling expression re-derived that same expression inside the
31
+ test, which proved edfcore agrees with itself and nothing more. This is the first evidence that
32
+ the choice to keep EDFlib's numerically worse form was worth making: substituting the textbook
33
+ `physicalMinimum + (digital - digitalMinimum) * gain` fails on 140 of 256 samples of the
34
+ symmetric fixture — `-492.15686274509807` where pyEDFlib says `-492.156862745098` — and a test
35
+ asserts that the goldens can tell the two apart, so the parity is a real constraint rather than
36
+ a coincidence of the fixtures.
37
+
38
+ Still not claimed, because no test produces it yet: parity with MNE, and validation across the
39
+ public corpora.
40
+
9
41
  ## 0.2.33
10
42
 
11
43
  - **Fixed** annotations being dropped with no diagnostic naming them. `TIMEKEEPING_TAL_NONCONFORMANT`
package/README.md CHANGED
@@ -291,10 +291,17 @@ the reason to trust the reader.
291
291
  **0.1 — reads everything, lies about nothing.** All six variants, the primitive/I/O/convenience
292
292
  layers, `inspectEdf`, `edfcore/validate`, six source adapters, the full diagnostic vocabulary.
293
293
 
294
- **Before 1.0.** Validation against public corpora (sleep-edfx, CHB-MIT, the BioSemi and
295
- edfplus.info test files) and a golden-value harness cross-checking physical values against
296
- pyEDFlib and MNE. Until that harness exists this README makes no numerical-interop claim, and it
297
- will not make one that a test did not produce.
294
+ **Numerical interop, as of 0.2.34.** edfcore reproduces **pyEDFlib's float64 physical values bit
295
+ for bit** on EDF and 24-bit BDF, across symmetric and asymmetric ranges. That is a test, not a
296
+ belief: `scripts/golden/generate.py` writes the fixtures with pyEDFlib's own writer, reads them
297
+ back with pyEDFlib, and records every sample as its exact IEEE-754 bit pattern;
298
+ `tests/corpus/golden-values.test.ts` compares with `Object.is`, so one ULP is a failure. Nothing in
299
+ `tests/corpus/golden/` was produced by edfcore. Substituting the numerically better textbook
300
+ expression fails it on 140 of 256 samples — which is why the EDFlib form is pinned.
301
+
302
+ **Still open before 1.0.** The same harness against MNE, and validation across public corpora
303
+ (sleep-edfx, CHB-MIT, the BioSemi and edfplus.info test files). Those claims are not made here
304
+ until a test produces them.
298
305
 
299
306
  **Shipped since 0.1.6.** Min/max envelope decimation (`readEnvelope`,
300
307
  `readEnvelopeAtResolution`). BioSemi Status-byte helpers (`readTriggers`). Streaming iteration
@@ -109,5 +109,5 @@ export declare const SIGNAL_FIELD_BLOCK_OFFSETS: {
109
109
  readonly reserved: 224;
110
110
  };
111
111
  /** Published package version. Kept in sync with package.json by a test. */
112
- export declare const VERSION = "0.2.33";
112
+ export declare const VERSION = "0.2.35";
113
113
  //# sourceMappingURL=constants.d.ts.map
package/dist/constants.js CHANGED
@@ -79,5 +79,5 @@ export const SIGNAL_FIELD_BLOCK_OFFSETS = {
79
79
  reserved: 224,
80
80
  };
81
81
  /** Published package version. Kept in sync with package.json by a test. */
82
- export const VERSION = '0.2.33';
82
+ export const VERSION = '0.2.35';
83
83
  //# sourceMappingURL=constants.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"bytes.d.ts","sourceRoot":"","sources":["../../src/io/bytes.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAe,MAAM,aAAa,CAAC;AAG3D,wBAAgB,UAAU,CAAC,KAAK,EAAE,WAAW,GAAG,UAAU,GAAG,UAAU,CAgBtE"}
1
+ {"version":3,"file":"bytes.d.ts","sourceRoot":"","sources":["../../src/io/bytes.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,KAAK,EAAE,UAAU,EAAe,MAAM,aAAa,CAAC;AAe3D,wBAAgB,UAAU,CAAC,KAAK,EAAE,WAAW,GAAG,UAAU,GAAG,UAAU,CAsCtE"}
package/dist/io/bytes.js CHANGED
@@ -6,11 +6,44 @@
6
6
  * owned by the caller" rule exists to stop an adapter handing out a view into state it retains,
7
7
  * and this adapter retains nothing the caller does not already hold.
8
8
  */
9
- import { assertExactRead, assertReadRange, throwIfAborted } from './source.js';
9
+ import { EdfSourceError } from '../errors.js';
10
+ import { assertExactRead, assertReadRange, isByteArray, throwIfAborted } from './source.js';
11
+ /**
12
+ * Describes what arrived, for an argument that is not bytes. Never prints the value: it could be
13
+ * anything, including something large.
14
+ */
15
+ function describe(value) {
16
+ if (value === null)
17
+ return 'null';
18
+ if (value === undefined)
19
+ return 'undefined';
20
+ if (ArrayBuffer.isView(value))
21
+ return Object.prototype.toString.call(value).slice(8, -1);
22
+ if (Array.isArray(value))
23
+ return 'a plain Array';
24
+ return typeof value === 'object' ? 'a plain object' : `a ${typeof value}`;
25
+ }
10
26
  export function byteSource(bytes) {
11
- // `ArrayBuffer.isView` rather than `instanceof Uint8Array`: `instanceof` is false for a view
12
- // that crossed a realm boundary (a worker, an iframe), and this is a public entry point.
13
- const view = ArrayBuffer.isView(bytes) ? bytes : new Uint8Array(bytes);
27
+ // Refused at CONSTRUCTION, because the alternative is worse than an error. `new Uint8Array(x)`
28
+ // accepts almost anything: a string, a plain object and `null` all yield an empty array, and a
29
+ // `number[]` yields one of the wrong length. The source was then built happily and the failure
30
+ // surfaced later as `[SOURCE_TOO_SMALL] the header is 0 bytes` — blaming the FILE for a mistake
31
+ // in the caller's argument, which is the one confusion this package works hardest to avoid.
32
+ //
33
+ // `Int8Array` is rejected too, and deliberately: it has one byte per element so it passes any
34
+ // length check, and its already-signed elements are sign-extended a second time during decode
35
+ // (see `assertExactRead`). Fabricated microvolts, with no error anywhere.
36
+ const isBuffer = bytes instanceof ArrayBuffer ||
37
+ Object.prototype.toString.call(bytes) === '[object SharedArrayBuffer]';
38
+ if (!isBuffer && !isByteArray(bytes)) {
39
+ throw new EdfSourceError(`byteSource() needs an ArrayBuffer or a Uint8Array, received ${describe(bytes)}. ` +
40
+ 'Next: pass `new Uint8Array(await blob.arrayBuffer())`, `await readFile(path)`, or the ' +
41
+ 'ArrayBuffer itself. An Int8Array is not accepted — it has one byte per element, so it ' +
42
+ 'would pass every length check and then decode to fabricated sample values.', { offset: 0, requestedLength: 0 });
43
+ }
44
+ // `isByteArray` rather than `instanceof Uint8Array`: `instanceof` is false for a view that
45
+ // crossed a realm boundary (a worker, an iframe), and this is a public entry point.
46
+ const view = isByteArray(bytes) ? bytes : new Uint8Array(bytes);
14
47
  const byteLength = view.byteLength;
15
48
  return {
16
49
  byteLength,
@@ -1 +1 @@
1
- {"version":3,"file":"bytes.js","sourceRoot":"","sources":["../../src/io/bytes.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,EAAE,eAAe,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAE/E,MAAM,UAAU,UAAU,CAAC,KAA+B;IACxD,6FAA6F;IAC7F,yFAAyF;IACzF,MAAM,IAAI,GAAe,WAAW,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,UAAU,CAAC,KAAK,CAAC,CAAC;IACnF,MAAM,UAAU,GAAG,IAAI,CAAC,UAAU,CAAC;IAEnC,OAAO;QACL,UAAU;QACV,KAAK,CAAC,IAAI,CAAC,MAAc,EAAE,MAAc,EAAE,OAAqB;YAC9D,cAAc,CAAC,OAAO,CAAC,CAAC;YACxB,eAAe,CAAC,MAAM,EAAE,MAAM,EAAE,UAAU,CAAC,CAAC;YAC5C,iFAAiF;YACjF,+EAA+E;YAC/E,OAAO,eAAe,CAAC,IAAI,CAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;QACjF,CAAC;KACF,CAAC;AACJ,CAAC"}
1
+ {"version":3,"file":"bytes.js","sourceRoot":"","sources":["../../src/io/bytes.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAE9C,OAAO,EAAE,eAAe,EAAE,eAAe,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAE5F;;;GAGG;AACH,SAAS,QAAQ,CAAC,KAAc;IAC9B,IAAI,KAAK,KAAK,IAAI;QAAE,OAAO,MAAM,CAAC;IAClC,IAAI,KAAK,KAAK,SAAS;QAAE,OAAO,WAAW,CAAC;IAC5C,IAAI,WAAW,CAAC,MAAM,CAAC,KAAK,CAAC;QAAE,OAAO,MAAM,CAAC,SAAS,CAAC,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;IACzF,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;QAAE,OAAO,eAAe,CAAC;IACjD,OAAO,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,gBAAgB,CAAC,CAAC,CAAC,KAAK,OAAO,KAAK,EAAE,CAAC;AAC5E,CAAC;AAED,MAAM,UAAU,UAAU,CAAC,KAA+B;IACxD,+FAA+F;IAC/F,+FAA+F;IAC/F,+FAA+F;IAC/F,gGAAgG;IAChG,4FAA4F;IAC5F,EAAE;IACF,8FAA8F;IAC9F,8FAA8F;IAC9F,0EAA0E;IAC1E,MAAM,QAAQ,GACZ,KAAK,YAAY,WAAW;QAC5B,MAAM,CAAC,SAAS,CAAC,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,4BAA4B,CAAC;IACzE,IAAI,CAAC,QAAQ,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC,EAAE,CAAC;QACrC,MAAM,IAAI,cAAc,CACtB,+DAA+D,QAAQ,CAAC,KAAK,CAAC,IAAI;YAChF,wFAAwF;YACxF,wFAAwF;YACxF,4EAA4E,EAC9E,EAAE,MAAM,EAAE,CAAC,EAAE,eAAe,EAAE,CAAC,EAAE,CAClC,CAAC;IACJ,CAAC;IAED,2FAA2F;IAC3F,oFAAoF;IACpF,MAAM,IAAI,GAAe,WAAW,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,UAAU,CAAC,KAAoB,CAAC,CAAC;IAC3F,MAAM,UAAU,GAAG,IAAI,CAAC,UAAU,CAAC;IAEnC,OAAO;QACL,UAAU;QACV,KAAK,CAAC,IAAI,CAAC,MAAc,EAAE,MAAc,EAAE,OAAqB;YAC9D,cAAc,CAAC,OAAO,CAAC,CAAC;YACxB,eAAe,CAAC,MAAM,EAAE,MAAM,EAAE,UAAU,CAAC,CAAC;YAC5C,iFAAiF;YACjF,+EAA+E;YAC/E,OAAO,eAAe,CAAC,IAAI,CAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;QACjF,CAAC;KACF,CAAC;AACJ,CAAC"}
@@ -11,6 +11,14 @@
11
11
  * cause for the wrong thing.
12
12
  */
13
13
  import type { AbortSignalLike, ReadOptions } from '../types.js';
14
+ /**
15
+ * Whether a value is a real one-byte-per-element view: `Uint8Array`, `Uint8ClampedArray`, or
16
+ * Node's `Buffer` (a `Uint8Array` subclass, so it inherits the tag).
17
+ *
18
+ * Shared with `byteSource`, which has to reject the same set at construction time rather than
19
+ * building a source over something that is not bytes.
20
+ */
21
+ export declare function isByteArray(value: unknown): value is Uint8Array;
14
22
  /**
15
23
  * Enforces the exact-length contract and returns the value unchanged so it can wrap a read
16
24
  * expression directly.
@@ -1 +1 @@
1
- {"version":3,"file":"source.d.ts","sourceRoot":"","sources":["../../src/io/source.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAGH,OAAO,KAAK,EAAE,eAAe,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAgChE;;;GAGG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,UAAU,CAYhG;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,IAAI,CAuBxF;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,OAAO,CAAC,EAAE,WAAW,GAAG,IAAI,CAE1D;AAED;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,CAAC,EAAE,eAAe,GAAG,IAAI,CAKnE;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,mBAAmB,CACjC,KAAK,EAAE,MAAM,GAAG,SAAS,EACzB,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,MAAM,GACf,MAAM,CAQR"}
1
+ {"version":3,"file":"source.d.ts","sourceRoot":"","sources":["../../src/io/source.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAGH,OAAO,KAAK,EAAE,eAAe,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AA0BhE;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,UAAU,CAE/D;AAMD;;;GAGG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,UAAU,CAYhG;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,IAAI,CAuBxF;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,OAAO,CAAC,EAAE,WAAW,GAAG,IAAI,CAE1D;AAED;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,CAAC,EAAE,eAAe,GAAG,IAAI,CAKnE;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,mBAAmB,CACjC,KAAK,EAAE,MAAM,GAAG,SAAS,EACzB,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,MAAM,GACf,MAAM,CAQR"}
package/dist/io/source.js CHANGED
@@ -34,12 +34,18 @@ import { EdfSourceError } from '../errors.js';
34
34
  * tag — and `Uint8ClampedArray`, and rejects `Int8Array`, every wider view, and `DataView`.
35
35
  */
36
36
  const BYTE_ARRAY_TAGS = new Set(['[object Uint8Array]', '[object Uint8ClampedArray]']);
37
+ /**
38
+ * Whether a value is a real one-byte-per-element view: `Uint8Array`, `Uint8ClampedArray`, or
39
+ * Node's `Buffer` (a `Uint8Array` subclass, so it inherits the tag).
40
+ *
41
+ * Shared with `byteSource`, which has to reject the same set at construction time rather than
42
+ * building a source over something that is not bytes.
43
+ */
44
+ export function isByteArray(value) {
45
+ return ArrayBuffer.isView(value) && BYTE_ARRAY_TAGS.has(Object.prototype.toString.call(value));
46
+ }
37
47
  function receivedLengthOf(received) {
38
- if (!ArrayBuffer.isView(received))
39
- return undefined;
40
- if (!BYTE_ARRAY_TAGS.has(Object.prototype.toString.call(received)))
41
- return undefined;
42
- return received.byteLength;
48
+ return isByteArray(received) ? received.byteLength : undefined;
43
49
  }
44
50
  /**
45
51
  * Enforces the exact-length contract and returns the value unchanged so it can wrap a read
@@ -1 +1 @@
1
- {"version":3,"file":"source.js","sourceRoot":"","sources":["../../src/io/source.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"source.js","sourceRoot":"","sources":["../../src/io/source.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "edfcore",
3
- "version": "0.2.33",
3
+ "version": "0.2.35",
4
4
  "description": "Modern, typed, zero-dependency reader for EDF, EDF+, BDF and BDF+ biosignal files. Works in browsers and Node with true random access.",
5
5
  "keywords": [
6
6
  "edf",
package/src/constants.ts CHANGED
@@ -93,4 +93,4 @@ export const SIGNAL_FIELD_BLOCK_OFFSETS = {
93
93
  } as const;
94
94
 
95
95
  /** Published package version. Kept in sync with package.json by a test. */
96
- export const VERSION = '0.2.33';
96
+ export const VERSION = '0.2.35';
package/src/io/bytes.ts CHANGED
@@ -7,13 +7,48 @@
7
7
  * and this adapter retains nothing the caller does not already hold.
8
8
  */
9
9
 
10
+ import { EdfSourceError } from '../errors.js';
10
11
  import type { ByteSource, ReadOptions } from '../types.js';
11
- import { assertExactRead, assertReadRange, throwIfAborted } from './source.js';
12
+ import { assertExactRead, assertReadRange, isByteArray, throwIfAborted } from './source.js';
13
+
14
+ /**
15
+ * Describes what arrived, for an argument that is not bytes. Never prints the value: it could be
16
+ * anything, including something large.
17
+ */
18
+ function describe(value: unknown): string {
19
+ if (value === null) return 'null';
20
+ if (value === undefined) return 'undefined';
21
+ if (ArrayBuffer.isView(value)) return Object.prototype.toString.call(value).slice(8, -1);
22
+ if (Array.isArray(value)) return 'a plain Array';
23
+ return typeof value === 'object' ? 'a plain object' : `a ${typeof value}`;
24
+ }
12
25
 
13
26
  export function byteSource(bytes: ArrayBuffer | Uint8Array): ByteSource {
14
- // `ArrayBuffer.isView` rather than `instanceof Uint8Array`: `instanceof` is false for a view
15
- // that crossed a realm boundary (a worker, an iframe), and this is a public entry point.
16
- const view: Uint8Array = ArrayBuffer.isView(bytes) ? bytes : new Uint8Array(bytes);
27
+ // Refused at CONSTRUCTION, because the alternative is worse than an error. `new Uint8Array(x)`
28
+ // accepts almost anything: a string, a plain object and `null` all yield an empty array, and a
29
+ // `number[]` yields one of the wrong length. The source was then built happily and the failure
30
+ // surfaced later as `[SOURCE_TOO_SMALL] the header is 0 bytes` — blaming the FILE for a mistake
31
+ // in the caller's argument, which is the one confusion this package works hardest to avoid.
32
+ //
33
+ // `Int8Array` is rejected too, and deliberately: it has one byte per element so it passes any
34
+ // length check, and its already-signed elements are sign-extended a second time during decode
35
+ // (see `assertExactRead`). Fabricated microvolts, with no error anywhere.
36
+ const isBuffer =
37
+ bytes instanceof ArrayBuffer ||
38
+ Object.prototype.toString.call(bytes) === '[object SharedArrayBuffer]';
39
+ if (!isBuffer && !isByteArray(bytes)) {
40
+ throw new EdfSourceError(
41
+ `byteSource() needs an ArrayBuffer or a Uint8Array, received ${describe(bytes)}. ` +
42
+ 'Next: pass `new Uint8Array(await blob.arrayBuffer())`, `await readFile(path)`, or the ' +
43
+ 'ArrayBuffer itself. An Int8Array is not accepted — it has one byte per element, so it ' +
44
+ 'would pass every length check and then decode to fabricated sample values.',
45
+ { offset: 0, requestedLength: 0 },
46
+ );
47
+ }
48
+
49
+ // `isByteArray` rather than `instanceof Uint8Array`: `instanceof` is false for a view that
50
+ // crossed a realm boundary (a worker, an iframe), and this is a public entry point.
51
+ const view: Uint8Array = isByteArray(bytes) ? bytes : new Uint8Array(bytes as ArrayBuffer);
17
52
  const byteLength = view.byteLength;
18
53
 
19
54
  return {
package/src/io/source.ts CHANGED
@@ -38,10 +38,19 @@ import type { AbortSignalLike, ReadOptions } from '../types.js';
38
38
  */
39
39
  const BYTE_ARRAY_TAGS = new Set(['[object Uint8Array]', '[object Uint8ClampedArray]']);
40
40
 
41
+ /**
42
+ * Whether a value is a real one-byte-per-element view: `Uint8Array`, `Uint8ClampedArray`, or
43
+ * Node's `Buffer` (a `Uint8Array` subclass, so it inherits the tag).
44
+ *
45
+ * Shared with `byteSource`, which has to reject the same set at construction time rather than
46
+ * building a source over something that is not bytes.
47
+ */
48
+ export function isByteArray(value: unknown): value is Uint8Array {
49
+ return ArrayBuffer.isView(value) && BYTE_ARRAY_TAGS.has(Object.prototype.toString.call(value));
50
+ }
51
+
41
52
  function receivedLengthOf(received: unknown): number | undefined {
42
- if (!ArrayBuffer.isView(received)) return undefined;
43
- if (!BYTE_ARRAY_TAGS.has(Object.prototype.toString.call(received))) return undefined;
44
- return received.byteLength;
53
+ return isByteArray(received) ? received.byteLength : undefined;
45
54
  }
46
55
 
47
56
  /**