edfcore 0.1.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/LICENSE +21 -0
- package/README.md +319 -0
- package/dist/bytes/latin1.d.ts +43 -0
- package/dist/bytes/latin1.d.ts.map +1 -0
- package/dist/bytes/latin1.js +84 -0
- package/dist/bytes/latin1.js.map +1 -0
- package/dist/bytes/numbers.d.ts +45 -0
- package/dist/bytes/numbers.d.ts.map +1 -0
- package/dist/bytes/numbers.js +87 -0
- package/dist/bytes/numbers.js.map +1 -0
- package/dist/bytes/view.d.ts +27 -0
- package/dist/bytes/view.d.ts.map +1 -0
- package/dist/bytes/view.js +53 -0
- package/dist/bytes/view.js.map +1 -0
- package/dist/constants.d.ts +113 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +83 -0
- package/dist/constants.js.map +1 -0
- package/dist/decode/digital.d.ts +49 -0
- package/dist/decode/digital.d.ts.map +1 -0
- package/dist/decode/digital.js +180 -0
- package/dist/decode/digital.js.map +1 -0
- package/dist/decode/physical.d.ts +37 -0
- package/dist/decode/physical.d.ts.map +1 -0
- package/dist/decode/physical.js +178 -0
- package/dist/decode/physical.js.map +1 -0
- package/dist/diagnostics/codes.d.ts +38 -0
- package/dist/diagnostics/codes.d.ts.map +1 -0
- package/dist/diagnostics/codes.js +134 -0
- package/dist/diagnostics/codes.js.map +1 -0
- package/dist/diagnostics/collector.d.ts +76 -0
- package/dist/diagnostics/collector.d.ts.map +1 -0
- package/dist/diagnostics/collector.js +95 -0
- package/dist/diagnostics/collector.js.map +1 -0
- package/dist/diagnostics/format.d.ts +21 -0
- package/dist/diagnostics/format.d.ts.map +1 -0
- package/dist/diagnostics/format.js +125 -0
- package/dist/diagnostics/format.js.map +1 -0
- package/dist/errors.d.ts +143 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +135 -0
- package/dist/errors.js.map +1 -0
- package/dist/header/dates.d.ts +101 -0
- package/dist/header/dates.d.ts.map +1 -0
- package/dist/header/dates.js +378 -0
- package/dist/header/dates.js.map +1 -0
- package/dist/header/fields.d.ts +105 -0
- package/dist/header/fields.d.ts.map +1 -0
- package/dist/header/fields.js +363 -0
- package/dist/header/fields.js.map +1 -0
- package/dist/header/identification.d.ts +47 -0
- package/dist/header/identification.d.ts.map +1 -0
- package/dist/header/identification.js +156 -0
- package/dist/header/identification.js.map +1 -0
- package/dist/header/lookup.d.ts +33 -0
- package/dist/header/lookup.d.ts.map +1 -0
- package/dist/header/lookup.js +67 -0
- package/dist/header/lookup.js.map +1 -0
- package/dist/header/parse.d.ts +32 -0
- package/dist/header/parse.d.ts.map +1 -0
- package/dist/header/parse.js +319 -0
- package/dist/header/parse.js.map +1 -0
- package/dist/header/scale.d.ts +68 -0
- package/dist/header/scale.d.ts.map +1 -0
- package/dist/header/scale.js +154 -0
- package/dist/header/scale.js.map +1 -0
- package/dist/header/signals.d.ts +64 -0
- package/dist/header/signals.d.ts.map +1 -0
- package/dist/header/signals.js +374 -0
- package/dist/header/signals.js.map +1 -0
- package/dist/header/variant.d.ts +67 -0
- package/dist/header/variant.d.ts.map +1 -0
- package/dist/header/variant.js +180 -0
- package/dist/header/variant.js.map +1 -0
- package/dist/index.d.ts +46 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +59 -0
- package/dist/index.js.map +1 -0
- package/dist/inspect.d.ts +31 -0
- package/dist/inspect.d.ts.map +1 -0
- package/dist/inspect.js +165 -0
- package/dist/inspect.js.map +1 -0
- package/dist/io/blob.d.ts +14 -0
- package/dist/io/blob.d.ts.map +1 -0
- package/dist/io/blob.js +29 -0
- package/dist/io/blob.js.map +1 -0
- package/dist/io/bytes.d.ts +11 -0
- package/dist/io/bytes.d.ts.map +1 -0
- package/dist/io/bytes.js +26 -0
- package/dist/io/bytes.js.map +1 -0
- package/dist/io/cached.d.ts +19 -0
- package/dist/io/cached.d.ts.map +1 -0
- package/dist/io/cached.js +129 -0
- package/dist/io/cached.js.map +1 -0
- package/dist/io/http.d.ts +20 -0
- package/dist/io/http.d.ts.map +1 -0
- package/dist/io/http.js +225 -0
- package/dist/io/http.js.map +1 -0
- package/dist/io/read.d.ts +42 -0
- package/dist/io/read.d.ts.map +1 -0
- package/dist/io/read.js +119 -0
- package/dist/io/read.js.map +1 -0
- package/dist/io/source.d.ts +33 -0
- package/dist/io/source.d.ts.map +1 -0
- package/dist/io/source.js +72 -0
- package/dist/io/source.js.map +1 -0
- package/dist/node.d.ts +70 -0
- package/dist/node.d.ts.map +1 -0
- package/dist/node.js +109 -0
- package/dist/node.js.map +1 -0
- package/dist/record-index.d.ts +58 -0
- package/dist/record-index.d.ts.map +1 -0
- package/dist/record-index.js +290 -0
- package/dist/record-index.js.map +1 -0
- package/dist/recording.d.ts +57 -0
- package/dist/recording.d.ts.map +1 -0
- package/dist/recording.js +178 -0
- package/dist/recording.js.map +1 -0
- package/dist/tal/annotations.d.ts +36 -0
- package/dist/tal/annotations.d.ts.map +1 -0
- package/dist/tal/annotations.js +437 -0
- package/dist/tal/annotations.js.map +1 -0
- package/dist/tal/grammar.d.ts +111 -0
- package/dist/tal/grammar.d.ts.map +1 -0
- package/dist/tal/grammar.js +324 -0
- package/dist/tal/grammar.js.map +1 -0
- package/dist/tal/ticks.d.ts +69 -0
- package/dist/tal/ticks.d.ts.map +1 -0
- package/dist/tal/ticks.js +137 -0
- package/dist/tal/ticks.js.map +1 -0
- package/dist/time/segments.d.ts +33 -0
- package/dist/time/segments.d.ts.map +1 -0
- package/dist/time/segments.js +87 -0
- package/dist/time/segments.js.map +1 -0
- package/dist/time/timeline.d.ts +62 -0
- package/dist/time/timeline.d.ts.map +1 -0
- package/dist/time/timeline.js +227 -0
- package/dist/time/timeline.js.map +1 -0
- package/dist/time/window.d.ts +54 -0
- package/dist/time/window.d.ts.map +1 -0
- package/dist/time/window.js +232 -0
- package/dist/time/window.js.map +1 -0
- package/dist/types.d.ts +409 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +16 -0
- package/dist/types.js.map +1 -0
- package/dist/validate.d.ts +53 -0
- package/dist/validate.d.ts.map +1 -0
- package/dist/validate.js +483 -0
- package/dist/validate.js.map +1 -0
- package/package.json +77 -0
- package/src/bytes/latin1.ts +87 -0
- package/src/bytes/numbers.ts +115 -0
- package/src/bytes/view.ts +59 -0
- package/src/constants.ts +96 -0
- package/src/decode/digital.ts +258 -0
- package/src/decode/physical.ts +235 -0
- package/src/diagnostics/codes.ts +209 -0
- package/src/diagnostics/collector.ts +129 -0
- package/src/diagnostics/format.ts +143 -0
- package/src/errors.ts +208 -0
- package/src/header/dates.ts +453 -0
- package/src/header/fields.ts +455 -0
- package/src/header/identification.ts +196 -0
- package/src/header/lookup.ts +81 -0
- package/src/header/parse.ts +398 -0
- package/src/header/scale.ts +268 -0
- package/src/header/signals.ts +550 -0
- package/src/header/variant.ts +239 -0
- package/src/index.ts +151 -0
- package/src/inspect.ts +181 -0
- package/src/io/blob.ts +31 -0
- package/src/io/bytes.ts +29 -0
- package/src/io/cached.ts +136 -0
- package/src/io/http.ts +299 -0
- package/src/io/read.ts +152 -0
- package/src/io/source.ts +87 -0
- package/src/node.ts +138 -0
- package/src/record-index.ts +389 -0
- package/src/recording.ts +234 -0
- package/src/tal/annotations.ts +574 -0
- package/src/tal/grammar.ts +540 -0
- package/src/tal/ticks.ts +172 -0
- package/src/time/segments.ts +109 -0
- package/src/time/timeline.ts +290 -0
- package/src/time/window.ts +304 -0
- package/src/types.ts +490 -0
- package/src/validate.ts +610 -0
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The diagnostic vocabulary.
|
|
3
|
+
*
|
|
4
|
+
* Layer 1. Imports nothing, so every other module may depend on it.
|
|
5
|
+
*
|
|
6
|
+
* A diagnostic is a *value*, never a log line. Each code has exactly one severity and one
|
|
7
|
+
* disposition, both fixed here:
|
|
8
|
+
*
|
|
9
|
+
* - `fatal` — edfcore cannot proceed without inventing something. Always throws, even when
|
|
10
|
+
* `strict` is false.
|
|
11
|
+
* - `deferred` — the header parses, but one signal cannot be scaled. `signal.scale` becomes
|
|
12
|
+
* `undefined`, `decodeDigital` keeps working, and `toPhysical` throws.
|
|
13
|
+
* - `warning` — the file is impolite but readable, and what we return is true.
|
|
14
|
+
* - `info` — the file is correct; the note exists because the situation surprises people.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
export type EdfSeverity = 'error' | 'warning' | 'info';
|
|
18
|
+
|
|
19
|
+
/** How a code behaves when it fires. See the module comment. */
|
|
20
|
+
export type EdfDiagnosticDisposition = 'fatal' | 'deferred' | 'warning' | 'info';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Every code edfcore itself can emit, grouped by disposition.
|
|
24
|
+
*
|
|
25
|
+
* This union and the `DISPOSITIONS` map below are checked against each other by the compiler:
|
|
26
|
+
* the map is typed as a total `Record` over this union, so a code added to one and not the
|
|
27
|
+
* other is a build error rather than a silent gap.
|
|
28
|
+
*/
|
|
29
|
+
export type EdfKnownDiagnosticCode =
|
|
30
|
+
// Always fatal
|
|
31
|
+
| 'NOT_AN_EDF_FILE'
|
|
32
|
+
| 'SOURCE_TOO_SMALL'
|
|
33
|
+
| 'SIGNAL_COUNT_INVALID'
|
|
34
|
+
| 'NUMERIC_FIELD_INVALID'
|
|
35
|
+
| 'COMMA_DECIMAL_SEPARATOR'
|
|
36
|
+
| 'RECORD_SIZE_ZERO'
|
|
37
|
+
| 'EDFPLUS_WITHOUT_ANNOTATION_SIGNAL'
|
|
38
|
+
| 'TIMELINE_NOT_MONOTONIC'
|
|
39
|
+
// Deferred-fatal: header parses, scaling does not
|
|
40
|
+
| 'DEGENERATE_DIGITAL_RANGE'
|
|
41
|
+
| 'DEGENERATE_PHYSICAL_RANGE'
|
|
42
|
+
| 'INVERTED_DIGITAL_RANGE'
|
|
43
|
+
| 'LOG_TRANSFORMED_CHANNEL'
|
|
44
|
+
| 'SCALE_UNAVAILABLE'
|
|
45
|
+
// Warnings
|
|
46
|
+
| 'HEADER_SIZE_MISMATCH'
|
|
47
|
+
| 'RECORD_COUNT_RECOVERED'
|
|
48
|
+
| 'TRUNCATED_FILE'
|
|
49
|
+
| 'PARTIAL_FINAL_RECORD'
|
|
50
|
+
| 'TRAILING_BYTES'
|
|
51
|
+
| 'RECORD_SIZE_ABOVE_RECOMMENDED'
|
|
52
|
+
| 'NONSTANDARD_RESERVED_FIELD'
|
|
53
|
+
| 'NON_ASCII_HEADER_FIELD'
|
|
54
|
+
| 'NUMERIC_FIELD_NOT_LEFT_JUSTIFIED'
|
|
55
|
+
| 'DATE_FIELDS_DISAGREE'
|
|
56
|
+
| 'DATE_UNPARSEABLE'
|
|
57
|
+
| 'PATIENT_ID_NONCONFORMANT'
|
|
58
|
+
| 'RECORDING_ID_NONCONFORMANT'
|
|
59
|
+
| 'DUPLICATE_SIGNAL_LABEL'
|
|
60
|
+
| 'DIGITAL_RANGE_EXCEEDS_FORMAT'
|
|
61
|
+
| 'ZERO_SAMPLES_PER_RECORD'
|
|
62
|
+
| 'ZERO_RECORD_DURATION'
|
|
63
|
+
| 'ANNOTATION_SIGNAL_HEADER_NONCONFORMANT'
|
|
64
|
+
| 'MISSING_EDFPLUS_MARKER'
|
|
65
|
+
| 'TIMEKEEPING_TAL_MISSING'
|
|
66
|
+
| 'TIMEKEEPING_TAL_NONCONFORMANT'
|
|
67
|
+
| 'START_OFFSET_OUT_OF_RANGE'
|
|
68
|
+
| 'TAL_MALFORMED'
|
|
69
|
+
| 'TAL_TRUNCATED_AT_REGION_END'
|
|
70
|
+
| 'TAL_REGION_NOT_NUL_TERMINATED'
|
|
71
|
+
| 'ANNOTATION_TEXT_NOT_UTF8'
|
|
72
|
+
| 'RECORD_ONSET_SPACING_VIOLATION'
|
|
73
|
+
| 'DISCONTINUITY_IN_CONTINUOUS_FILE'
|
|
74
|
+
// Info
|
|
75
|
+
| 'DATE_CLIPPED_TO_1985_2084'
|
|
76
|
+
| 'INVERTED_PHYSICAL_RANGE'
|
|
77
|
+
| 'NEGATIVE_ANNOTATION_ONSET';
|
|
78
|
+
|
|
79
|
+
const DISPOSITIONS: Readonly<Record<EdfKnownDiagnosticCode, EdfDiagnosticDisposition>> = {
|
|
80
|
+
// ---- Always fatal --------------------------------------------------------
|
|
81
|
+
/** No recognisable EDF or BDF version block at offset 0. */
|
|
82
|
+
NOT_AN_EDF_FILE: 'fatal',
|
|
83
|
+
/** Fewer than 256 bytes, or fewer than 256*(ns+1). */
|
|
84
|
+
SOURCE_TOO_SMALL: 'fatal',
|
|
85
|
+
/** ns outside 1..9999, blank, or non-numeric. Every later offset is a multiple of it. */
|
|
86
|
+
SIGNAL_COUNT_INVALID: 'fatal',
|
|
87
|
+
/** A field the file geometry depends on failed its grammar end-to-end. */
|
|
88
|
+
NUMERIC_FIELD_INVALID: 'fatal',
|
|
89
|
+
/** '0,5' and '1,024' are indistinguishable; guessing turns 1024 into 1.024. */
|
|
90
|
+
COMMA_DECIMAL_SEPARATOR: 'fatal',
|
|
91
|
+
/** Every signal declares 0 samples per record, so records have no size to step by. */
|
|
92
|
+
RECORD_SIZE_ZERO: 'fatal',
|
|
93
|
+
/** EDF+ marker present but no annotation signal: no per-record timing exists, so any
|
|
94
|
+
* time we reported would be invented. */
|
|
95
|
+
EDFPLUS_WITHOUT_ANNOTATION_SIGNAL: 'fatal',
|
|
96
|
+
/** Record onsets went backwards. Every time-based answer would be wrong. */
|
|
97
|
+
TIMELINE_NOT_MONOTONIC: 'fatal',
|
|
98
|
+
|
|
99
|
+
// ---- Deferred-fatal: header parses, scaling does not -----------------------
|
|
100
|
+
/** digitalMinimum === digitalMaximum. Division by zero. EDF+ spec item 5. */
|
|
101
|
+
DEGENERATE_DIGITAL_RANGE: 'deferred',
|
|
102
|
+
/** physicalMinimum === physicalMaximum. Every sample would map to one value. */
|
|
103
|
+
DEGENERATE_PHYSICAL_RANGE: 'deferred',
|
|
104
|
+
/** digitalMinimum > digitalMaximum. Violates EDF+ item 5; we will not guess the intent. */
|
|
105
|
+
INVERTED_DIGITAL_RANGE: 'deferred',
|
|
106
|
+
/** Physical dimension is exactly 'Filtered': values are log-compressed (edffloat.html),
|
|
107
|
+
* so the linear formula would be wrong by orders of magnitude. Refused, not applied. */
|
|
108
|
+
LOG_TRANSFORMED_CHANNEL: 'deferred',
|
|
109
|
+
/** `toPhysical` was called on a signal with no scale and the specific cause was not
|
|
110
|
+
* re-derivable from the signal alone. Naming the wrong cause would be worse. */
|
|
111
|
+
SCALE_UNAVAILABLE: 'deferred',
|
|
112
|
+
|
|
113
|
+
// ---- Warnings: the file stays readable and what we return is true ----------
|
|
114
|
+
/** Header byte-count field disagrees with 256*(ns+1). The computed value always wins. */
|
|
115
|
+
HEADER_SIZE_MISMATCH: 'warning',
|
|
116
|
+
/** recordCount was -1 (writer never closed the file); recovered from the source length. */
|
|
117
|
+
RECORD_COUNT_RECOVERED: 'warning',
|
|
118
|
+
/** The file is shorter than its declared record count implies. */
|
|
119
|
+
TRUNCATED_FILE: 'warning',
|
|
120
|
+
/** A final partial record exists. Only whole records are exposed; nothing is zero-padded. */
|
|
121
|
+
PARTIAL_FINAL_RECORD: 'warning',
|
|
122
|
+
/** Bytes exist beyond the last whole data record. */
|
|
123
|
+
TRAILING_BYTES: 'warning',
|
|
124
|
+
/** Record exceeds the 61440-byte recommendation. */
|
|
125
|
+
RECORD_SIZE_ABOVE_RECOMMENDED: 'warning',
|
|
126
|
+
/** Reserved field is neither blank nor a recognised EDF+/BDF+ marker. */
|
|
127
|
+
NONSTANDARD_RESERVED_FIELD: 'warning',
|
|
128
|
+
/** A header field carries bytes outside printable ASCII. Decoded as Latin-1. */
|
|
129
|
+
NON_ASCII_HEADER_FIELD: 'warning',
|
|
130
|
+
/** A numeric field is right-justified; the spec requires left-justified. */
|
|
131
|
+
NUMERIC_FIELD_NOT_LEFT_JUSTIFIED: 'warning',
|
|
132
|
+
/** Header startdate and the recording-id Startdate disagree. Both are exposed; we pick no winner. */
|
|
133
|
+
DATE_FIELDS_DISAGREE: 'warning',
|
|
134
|
+
/** The date could not be parsed at all. */
|
|
135
|
+
DATE_UNPARSEABLE: 'warning',
|
|
136
|
+
/** Patient identification does not follow the EDF+ subfield grammar. */
|
|
137
|
+
PATIENT_ID_NONCONFORMANT: 'warning',
|
|
138
|
+
/** Recording identification does not follow the EDF+ subfield grammar. */
|
|
139
|
+
RECORDING_ID_NONCONFORMANT: 'warning',
|
|
140
|
+
/** Two signals share a label. `getSignal` by label will throw for these. */
|
|
141
|
+
DUPLICATE_SIGNAL_LABEL: 'warning',
|
|
142
|
+
/** Declared digital range exceeds what the sample width can represent. */
|
|
143
|
+
DIGITAL_RANGE_EXCEEDS_FORMAT: 'warning',
|
|
144
|
+
/** A signal declares 0 samples per record. */
|
|
145
|
+
ZERO_SAMPLES_PER_RECORD: 'warning',
|
|
146
|
+
/** Record duration is 0. Legal in EDF+, but sample rates become undefined. */
|
|
147
|
+
ZERO_RECORD_DURATION: 'warning',
|
|
148
|
+
/** An annotation signal's header fields deviate from the EDF+ requirements. */
|
|
149
|
+
ANNOTATION_SIGNAL_HEADER_NONCONFORMANT: 'warning',
|
|
150
|
+
/** An 'EDF Annotations' signal exists without an EDF+ marker in the reserved field.
|
|
151
|
+
* Annotations are parsed anyway; the channel is never exposed as an ordinary signal. */
|
|
152
|
+
MISSING_EDFPLUS_MARKER: 'warning',
|
|
153
|
+
/** A data record has no timekeeping TAL. */
|
|
154
|
+
TIMEKEEPING_TAL_MISSING: 'warning',
|
|
155
|
+
/** The timekeeping TAL exists but deviates (e.g. the widespread `+t 0x14 0x00` shorthand). */
|
|
156
|
+
TIMEKEEPING_TAL_NONCONFORMANT: 'warning',
|
|
157
|
+
/** Record 0's sub-second start offset fell outside [0, 1). */
|
|
158
|
+
START_OFFSET_OUT_OF_RANGE: 'warning',
|
|
159
|
+
/** A TAL did not match the grammar. That TAL is skipped; the file is kept. */
|
|
160
|
+
TAL_MALFORMED: 'warning',
|
|
161
|
+
/** A TAL ran past the end of its annotation region and was discarded. */
|
|
162
|
+
TAL_TRUNCATED_AT_REGION_END: 'warning',
|
|
163
|
+
/** The annotation region tail was not NUL padding. */
|
|
164
|
+
TAL_REGION_NOT_NUL_TERMINATED: 'warning',
|
|
165
|
+
/** Annotation text was not valid UTF-8; decoded as Latin-1 instead. */
|
|
166
|
+
ANNOTATION_TEXT_NOT_UTF8: 'warning',
|
|
167
|
+
/** Consecutive record onsets are not spaced by the record duration. */
|
|
168
|
+
RECORD_ONSET_SPACING_VIOLATION: 'warning',
|
|
169
|
+
/** A file marked EDF+C contains a real discontinuity. */
|
|
170
|
+
DISCONTINUITY_IN_CONTINUOUS_FILE: 'warning',
|
|
171
|
+
|
|
172
|
+
// ---- Info: correct, and deliberately not a warning ------------------------
|
|
173
|
+
/** A two-digit year was resolved through the spec's own 1985..2084 rule. Every conforming
|
|
174
|
+
* EDF file has a two-digit year, so this is the normal path, not a defect — it is
|
|
175
|
+
* reported only because the resolved century is worth being able to see. */
|
|
176
|
+
DATE_CLIPPED_TO_1985_2084: 'info',
|
|
177
|
+
/** physicalMinimum > physicalMaximum encodes a negative amplifier gain. Spec-sanctioned
|
|
178
|
+
* (EDF FAQ Q6). Never "fixed" — swapping them is a silent polarity flip. */
|
|
179
|
+
INVERTED_PHYSICAL_RANGE: 'info',
|
|
180
|
+
/** A negative annotation onset is normal for pre-stimulus events. */
|
|
181
|
+
NEGATIVE_ANNOTATION_ONSET: 'info',
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Open union: known codes autocomplete, and a `default` branch stays mandatory so adding a
|
|
186
|
+
* code in a minor release cannot break a consumer's exhaustive switch.
|
|
187
|
+
*/
|
|
188
|
+
export type EdfDiagnosticCode = EdfKnownDiagnosticCode | (string & {});
|
|
189
|
+
|
|
190
|
+
export const DIAGNOSTIC_DISPOSITIONS: Readonly<
|
|
191
|
+
Record<EdfKnownDiagnosticCode, EdfDiagnosticDisposition>
|
|
192
|
+
> = DISPOSITIONS;
|
|
193
|
+
|
|
194
|
+
/** Unknown codes are treated as warnings — an unrecognised note must never escalate. */
|
|
195
|
+
export function dispositionOf(code: EdfDiagnosticCode): EdfDiagnosticDisposition {
|
|
196
|
+
return DISPOSITIONS[code as EdfKnownDiagnosticCode] ?? 'warning';
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export function severityOf(code: EdfDiagnosticCode): EdfSeverity {
|
|
200
|
+
const disposition = dispositionOf(code);
|
|
201
|
+
if (disposition === 'fatal' || disposition === 'deferred') return 'error';
|
|
202
|
+
if (disposition === 'warning') return 'warning';
|
|
203
|
+
return 'info';
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** Fires regardless of `strict`: proceeding would require inventing data. */
|
|
207
|
+
export function isAlwaysFatal(code: EdfDiagnosticCode): boolean {
|
|
208
|
+
return dispositionOf(code) === 'fatal';
|
|
209
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The strict-vs-collect decision.
|
|
3
|
+
*
|
|
4
|
+
* Layer 1. Every diagnostic edfcore emits is built here, which is what makes two invariants
|
|
5
|
+
* structural rather than conventional:
|
|
6
|
+
*
|
|
7
|
+
* - `strict: true` throws `EdfFormatError` carrying the first would-be diagnostic, so under
|
|
8
|
+
* strict every `diagnostics` array is empty by construction;
|
|
9
|
+
* - a code whose disposition is `fatal` throws whether or not `strict` is set, because
|
|
10
|
+
* proceeding would require inventing data.
|
|
11
|
+
*
|
|
12
|
+
* The caller never passes a severity — it is derived from the code by `severityOf`, so one code
|
|
13
|
+
* cannot acquire two severities in two call sites.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { EdfFormatError, type EdfFormatErrorInit } from '../errors.js';
|
|
17
|
+
import type { EdfDiagnostic, ParseOptions } from '../types.js';
|
|
18
|
+
import { type EdfDiagnosticCode, isAlwaysFatal, severityOf } from './codes.js';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Everything a diagnostic may carry except its severity. Optional here, required-or-undefined
|
|
22
|
+
* on `EdfDiagnostic`: `createDiagnostic` normalises between the two, which is what lets the
|
|
23
|
+
* result type stay exhaustive under `exactOptionalPropertyTypes`.
|
|
24
|
+
*/
|
|
25
|
+
export interface DiagnosticInit {
|
|
26
|
+
readonly code: EdfDiagnosticCode;
|
|
27
|
+
/** Must name the field, the raw bytes as written, the rule, and an actionable next step. */
|
|
28
|
+
readonly message: string;
|
|
29
|
+
readonly field?: string;
|
|
30
|
+
readonly byteOffset?: number;
|
|
31
|
+
readonly byteLength?: number;
|
|
32
|
+
readonly rawBytes?: Uint8Array;
|
|
33
|
+
readonly raw?: string;
|
|
34
|
+
readonly expected?: string;
|
|
35
|
+
readonly actual?: string;
|
|
36
|
+
readonly signalIndex?: number;
|
|
37
|
+
readonly recordIndex?: number;
|
|
38
|
+
/** A spec clause, e.g. 'EDF+ additional specification 5'. */
|
|
39
|
+
readonly specReference?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function createDiagnostic(init: DiagnosticInit): EdfDiagnostic {
|
|
43
|
+
return {
|
|
44
|
+
code: init.code,
|
|
45
|
+
severity: severityOf(init.code),
|
|
46
|
+
message: init.message,
|
|
47
|
+
field: init.field,
|
|
48
|
+
byteOffset: init.byteOffset,
|
|
49
|
+
byteLength: init.byteLength,
|
|
50
|
+
// Copied, not aliased: a diagnostic outlives the read that produced it, and the caller's
|
|
51
|
+
// view is typically a subarray of a buffer an I/O adapter is free to reuse.
|
|
52
|
+
rawBytes: init.rawBytes === undefined ? undefined : init.rawBytes.slice(),
|
|
53
|
+
raw: init.raw,
|
|
54
|
+
expected: init.expected,
|
|
55
|
+
actual: init.actual,
|
|
56
|
+
signalIndex: init.signalIndex,
|
|
57
|
+
recordIndex: init.recordIndex,
|
|
58
|
+
specReference: init.specReference,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* The one way to turn a diagnostic into the error that carries it. `EdfFormatError` re-derives
|
|
64
|
+
* `field`/`byteOffset`/`signalIndex`/`recordIndex` from the diagnostic, so they are not repeated.
|
|
65
|
+
*/
|
|
66
|
+
export function toFormatError(diagnostic: EdfDiagnostic, cause?: unknown): EdfFormatError {
|
|
67
|
+
const init: EdfFormatErrorInit =
|
|
68
|
+
cause === undefined
|
|
69
|
+
? { code: diagnostic.code, diagnostic }
|
|
70
|
+
: { code: diagnostic.code, diagnostic, cause };
|
|
71
|
+
return new EdfFormatError(`[${diagnostic.code}] ${diagnostic.message}`, init);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* `toFormatError(createDiagnostic(init))`, for the paths that throw without a sink — an always
|
|
76
|
+
* fatal code detected before one exists, or where the type system needs the `throw` to be
|
|
77
|
+
* visible at the call site.
|
|
78
|
+
*/
|
|
79
|
+
export function fatalError(init: DiagnosticInit, cause?: unknown): EdfFormatError {
|
|
80
|
+
return toFormatError(createDiagnostic(init), cause);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export class DiagnosticSink {
|
|
84
|
+
/**
|
|
85
|
+
* Readable so a caller can skip building a message it is about to throw away. Never so a
|
|
86
|
+
* caller can re-implement the decision in `report`.
|
|
87
|
+
*/
|
|
88
|
+
readonly strict: boolean;
|
|
89
|
+
|
|
90
|
+
#collected: EdfDiagnostic[] = [];
|
|
91
|
+
|
|
92
|
+
constructor(options?: ParseOptions) {
|
|
93
|
+
this.strict = options?.strict === true;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
get size(): number {
|
|
97
|
+
return this.#collected.length;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Records the diagnostic, or throws `EdfFormatError` when the code is always fatal, or when
|
|
102
|
+
* `strict` is set and the code describes an actual defect. There is no severity parameter
|
|
103
|
+
* by design.
|
|
104
|
+
*
|
|
105
|
+
* `info` codes are exempt from `strict`. They exist to explain something that is correct but
|
|
106
|
+
* surprising — a spec-sanctioned negative gain, a pre-stimulus onset, the mandated two-digit
|
|
107
|
+
* year rule — so throwing on one would contradict what the severity means, and would make
|
|
108
|
+
* `strict` reject conforming files. Every `info` note is still collected and readable.
|
|
109
|
+
*/
|
|
110
|
+
report(init: DiagnosticInit): void {
|
|
111
|
+
const diagnostic = createDiagnostic(init);
|
|
112
|
+
if (isAlwaysFatal(diagnostic.code) || (this.strict && diagnostic.severity !== 'info')) {
|
|
113
|
+
throw toFormatError(diagnostic);
|
|
114
|
+
}
|
|
115
|
+
this.#collected.push(diagnostic);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** A frozen copy: an array already attached to a result must not grow if reporting continues. */
|
|
119
|
+
get diagnostics(): readonly EdfDiagnostic[] {
|
|
120
|
+
return Object.freeze(this.#collected.slice());
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** `diagnostics`, then reset — for a sink reused across records. */
|
|
124
|
+
drain(): readonly EdfDiagnostic[] {
|
|
125
|
+
const collected = this.diagnostics;
|
|
126
|
+
this.#collected = [];
|
|
127
|
+
return collected;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rendering diagnostics for humans.
|
|
3
|
+
*
|
|
4
|
+
* Layer 1. Layout only: by the message contract a diagnostic's own message already names the
|
|
5
|
+
* field, the raw bytes as written, the rule and the next step, so this module adds structure —
|
|
6
|
+
* severity marker, code, location, the bytes as hex — and invents no wording.
|
|
7
|
+
*
|
|
8
|
+
* Output is deterministic and asserted as such: no locale-sensitive number or date formatting,
|
|
9
|
+
* no iteration over an unordered collection, and no ANSI escapes unless `color` is requested.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { EdfDiagnostic, EdfSeverity } from '../types.js';
|
|
13
|
+
|
|
14
|
+
export interface FormatDiagnosticsOptions {
|
|
15
|
+
readonly color?: boolean;
|
|
16
|
+
readonly maxItems?: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const INDENT = ' ';
|
|
20
|
+
|
|
21
|
+
/** A report is a summary, not a hex dump; longer runs are elided with a count. */
|
|
22
|
+
const MAX_RAW_BYTES_SHOWN = 24;
|
|
23
|
+
|
|
24
|
+
const ANSI_RESET = '\u001b[0m';
|
|
25
|
+
const ANSI_DIM = '\u001b[2m';
|
|
26
|
+
|
|
27
|
+
const SEVERITY_COLORS: Readonly<Record<EdfSeverity, string>> = {
|
|
28
|
+
error: '\u001b[31m',
|
|
29
|
+
warning: '\u001b[33m',
|
|
30
|
+
info: '\u001b[36m',
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* A multi-line report, one block per diagnostic. Returns `''` for an empty list so the result
|
|
35
|
+
* can be concatenated into a larger report without a stray blank line.
|
|
36
|
+
*/
|
|
37
|
+
export function formatDiagnostics(
|
|
38
|
+
diagnostics: readonly EdfDiagnostic[],
|
|
39
|
+
options?: FormatDiagnosticsOptions,
|
|
40
|
+
): string {
|
|
41
|
+
const color = options?.color === true;
|
|
42
|
+
const shown = resolveLimit(options?.maxItems, diagnostics.length);
|
|
43
|
+
const lines: string[] = [];
|
|
44
|
+
|
|
45
|
+
for (let i = 0; i < shown; i++) {
|
|
46
|
+
const diagnostic = diagnostics[i];
|
|
47
|
+
// i < shown <= diagnostics.length, so this only satisfies noUncheckedIndexedAccess.
|
|
48
|
+
if (diagnostic === undefined) continue;
|
|
49
|
+
appendDiagnostic(lines, diagnostic, color);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const hidden = diagnostics.length - shown;
|
|
53
|
+
if (hidden > 0) lines.push(paint(`... and ${hidden} more`, ANSI_DIM, color));
|
|
54
|
+
|
|
55
|
+
return lines.join('\n');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function resolveLimit(maxItems: number | undefined, total: number): number {
|
|
59
|
+
if (maxItems === undefined || !Number.isFinite(maxItems)) return total;
|
|
60
|
+
return Math.max(0, Math.min(total, Math.floor(maxItems)));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function appendDiagnostic(lines: string[], diagnostic: EdfDiagnostic, color: boolean): void {
|
|
64
|
+
const marker = paint(
|
|
65
|
+
`${diagnostic.severity} [${diagnostic.code}]`,
|
|
66
|
+
SEVERITY_COLORS[diagnostic.severity],
|
|
67
|
+
color,
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
const messageLines = diagnostic.message.split('\n');
|
|
71
|
+
const first = messageLines[0] ?? '';
|
|
72
|
+
lines.push(`${marker} ${first}`.trimEnd());
|
|
73
|
+
for (let i = 1; i < messageLines.length; i++) {
|
|
74
|
+
const line = messageLines[i];
|
|
75
|
+
if (line === undefined) continue;
|
|
76
|
+
lines.push(`${INDENT}${line.trim()}`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const location = locationOf(diagnostic);
|
|
80
|
+
if (location !== undefined) detail(lines, location, color);
|
|
81
|
+
if (diagnostic.raw !== undefined) detail(lines, `raw: ${quote(diagnostic.raw)}`, color);
|
|
82
|
+
if (diagnostic.rawBytes !== undefined && diagnostic.rawBytes.length > 0) {
|
|
83
|
+
detail(lines, `bytes: ${hexDump(diagnostic.rawBytes)}`, color);
|
|
84
|
+
}
|
|
85
|
+
if (diagnostic.expected !== undefined) detail(lines, `expected: ${diagnostic.expected}`, color);
|
|
86
|
+
if (diagnostic.actual !== undefined) detail(lines, `actual: ${diagnostic.actual}`, color);
|
|
87
|
+
if (diagnostic.specReference !== undefined) {
|
|
88
|
+
detail(lines, `spec: ${diagnostic.specReference}`, color);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function detail(lines: string[], text: string, color: boolean): void {
|
|
93
|
+
lines.push(`${INDENT}${paint(text, ANSI_DIM, color)}`);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function locationOf(diagnostic: EdfDiagnostic): string | undefined {
|
|
97
|
+
const parts: string[] = [];
|
|
98
|
+
if (diagnostic.byteOffset !== undefined) {
|
|
99
|
+
parts.push(
|
|
100
|
+
diagnostic.byteLength === undefined
|
|
101
|
+
? `byte offset ${diagnostic.byteOffset}`
|
|
102
|
+
: `byte offset ${diagnostic.byteOffset} (${diagnostic.byteLength} bytes)`,
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
if (diagnostic.field !== undefined) parts.push(diagnostic.field);
|
|
106
|
+
if (diagnostic.signalIndex !== undefined) parts.push(`signal ${diagnostic.signalIndex}`);
|
|
107
|
+
if (diagnostic.recordIndex !== undefined) parts.push(`record ${diagnostic.recordIndex}`);
|
|
108
|
+
return parts.length === 0 ? undefined : `at ${parts.join(', ')}`;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** `30 20 20 20 |0 |`. Built byte by byte — TextDecoder is banned outside `src/tal/`. */
|
|
112
|
+
function hexDump(bytes: Uint8Array): string {
|
|
113
|
+
const shown = bytes.subarray(0, MAX_RAW_BYTES_SHOWN);
|
|
114
|
+
const hex: string[] = [];
|
|
115
|
+
let ascii = '';
|
|
116
|
+
for (const byte of shown) {
|
|
117
|
+
hex.push(byte.toString(16).padStart(2, '0'));
|
|
118
|
+
ascii += isPrintableAscii(byte) ? String.fromCharCode(byte) : '.';
|
|
119
|
+
}
|
|
120
|
+
const elided = bytes.length - shown.length;
|
|
121
|
+
return `${hex.join(' ')} |${ascii}|${elided > 0 ? ` +${elided} more` : ''}`;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function isPrintableAscii(byte: number): boolean {
|
|
125
|
+
return byte >= 0x20 && byte <= 0x7e;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Keeps every entry on one line: control and non-ASCII characters become escapes, not bytes. */
|
|
129
|
+
function quote(value: string): string {
|
|
130
|
+
let out = '"';
|
|
131
|
+
for (const char of value) {
|
|
132
|
+
const code = char.codePointAt(0) ?? 0;
|
|
133
|
+
if (char === '"' || char === '\\') out += `\\${char}`;
|
|
134
|
+
else if (isPrintableAscii(code)) out += char;
|
|
135
|
+
else if (code <= 0xff) out += `\\x${code.toString(16).padStart(2, '0')}`;
|
|
136
|
+
else out += `\\u{${code.toString(16)}}`;
|
|
137
|
+
}
|
|
138
|
+
return `${out}"`;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function paint(text: string, color: string, enabled: boolean): string {
|
|
142
|
+
return enabled ? `${color}${text}${ANSI_RESET}` : text;
|
|
143
|
+
}
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The error hierarchy.
|
|
3
|
+
*
|
|
4
|
+
* Layer 1. One rule decides which of these you get: if edfcore cannot proceed without
|
|
5
|
+
* inventing something, it throws; if it can proceed truthfully, it records an `EdfDiagnostic`.
|
|
6
|
+
* There is no third category, and there is no `console` call anywhere in this package.
|
|
7
|
+
*
|
|
8
|
+
* `edfErrorKind` exists so consumers can discriminate without `instanceof`, which fails across
|
|
9
|
+
* realms — an iframe, a worker, or two copies of the package in one dependency tree.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { EdfDiagnostic, EdfDiagnosticCode, RecordRange } from './types.js';
|
|
13
|
+
|
|
14
|
+
export type EdfErrorKind = 'format' | 'scaling' | 'range' | 'source' | 'budget' | 'channel';
|
|
15
|
+
|
|
16
|
+
export abstract class EdfError extends Error {
|
|
17
|
+
abstract readonly edfErrorKind: EdfErrorKind;
|
|
18
|
+
|
|
19
|
+
constructor(message: string, options?: { cause?: unknown }) {
|
|
20
|
+
super(message, options as ErrorOptions | undefined);
|
|
21
|
+
this.name = new.target.name;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Every concrete error edfcore throws.
|
|
27
|
+
*
|
|
28
|
+
* This exists so `edfErrorKind` actually discriminates in TypeScript. `EdfError` alone
|
|
29
|
+
* declares only the kind, so narrowing against the abstract class leaves the extra fields
|
|
30
|
+
* (`code`, `budgetBytes`, `matchingIndices`, …) unreachable and forces a cast at every call
|
|
31
|
+
* site. Switching on `edfErrorKind` over this union reaches them without one.
|
|
32
|
+
*/
|
|
33
|
+
export type AnyEdfError =
|
|
34
|
+
| EdfFormatError
|
|
35
|
+
| EdfScalingError
|
|
36
|
+
| EdfRangeError
|
|
37
|
+
| EdfSourceError
|
|
38
|
+
| EdfBudgetError
|
|
39
|
+
| EdfAmbiguousChannelError
|
|
40
|
+
| EdfChannelNotFoundError;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Returns true for any error thrown by edfcore, across realms.
|
|
44
|
+
*
|
|
45
|
+
* Checks the `edfErrorKind` brand rather than `instanceof`, which fails whenever the error
|
|
46
|
+
* crossed a realm boundary — an iframe, a worker, or two copies of the package in one
|
|
47
|
+
* dependency tree.
|
|
48
|
+
*
|
|
49
|
+
* Note that a handful of caller-error paths deliberately throw a plain `RangeError` instead
|
|
50
|
+
* (asking for records that cannot exist, or handing an annotation channel to a sample read).
|
|
51
|
+
* Those are bugs in the calling code rather than problems with the file, and this returns
|
|
52
|
+
* false for them.
|
|
53
|
+
*/
|
|
54
|
+
export function isEdfError(value: unknown): value is AnyEdfError {
|
|
55
|
+
return (
|
|
56
|
+
typeof value === 'object' &&
|
|
57
|
+
value !== null &&
|
|
58
|
+
typeof (value as { edfErrorKind?: unknown }).edfErrorKind === 'string'
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface EdfFormatErrorInit {
|
|
63
|
+
readonly code: EdfDiagnosticCode;
|
|
64
|
+
readonly diagnostic?: EdfDiagnostic;
|
|
65
|
+
readonly field?: string;
|
|
66
|
+
readonly byteOffset?: number;
|
|
67
|
+
readonly signalIndex?: number;
|
|
68
|
+
readonly recordIndex?: number;
|
|
69
|
+
readonly cause?: unknown;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* The file is wrong. Also what `strict: true` throws, carrying the diagnostic that would
|
|
74
|
+
* otherwise have been collected.
|
|
75
|
+
*/
|
|
76
|
+
export class EdfFormatError extends EdfError {
|
|
77
|
+
readonly edfErrorKind = 'format' as const;
|
|
78
|
+
readonly code: EdfDiagnosticCode;
|
|
79
|
+
readonly diagnostic: EdfDiagnostic | undefined;
|
|
80
|
+
readonly field: string | undefined;
|
|
81
|
+
readonly byteOffset: number | undefined;
|
|
82
|
+
readonly signalIndex: number | undefined;
|
|
83
|
+
readonly recordIndex: number | undefined;
|
|
84
|
+
|
|
85
|
+
constructor(message: string, init: EdfFormatErrorInit) {
|
|
86
|
+
super(message, { cause: init.cause });
|
|
87
|
+
this.code = init.code;
|
|
88
|
+
this.diagnostic = init.diagnostic;
|
|
89
|
+
this.field = init.field ?? init.diagnostic?.field;
|
|
90
|
+
this.byteOffset = init.byteOffset ?? init.diagnostic?.byteOffset;
|
|
91
|
+
this.signalIndex = init.signalIndex ?? init.diagnostic?.signalIndex;
|
|
92
|
+
this.recordIndex = init.recordIndex ?? init.diagnostic?.recordIndex;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Physical units are unavailable for one signal, because the header's ranges do not define a
|
|
98
|
+
* scale. `decodeDigital` still works on that signal — edfcore will not invent a gain.
|
|
99
|
+
*/
|
|
100
|
+
export class EdfScalingError extends EdfError {
|
|
101
|
+
readonly edfErrorKind = 'scaling' as const;
|
|
102
|
+
readonly code: EdfDiagnosticCode;
|
|
103
|
+
readonly signalIndex: number;
|
|
104
|
+
readonly label: string;
|
|
105
|
+
|
|
106
|
+
constructor(
|
|
107
|
+
message: string,
|
|
108
|
+
init: { code: EdfDiagnosticCode; signalIndex: number; label: string; cause?: unknown },
|
|
109
|
+
) {
|
|
110
|
+
super(message, { cause: init.cause });
|
|
111
|
+
this.code = init.code;
|
|
112
|
+
this.signalIndex = init.signalIndex;
|
|
113
|
+
this.label = init.label;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Your bug, not the file's: you asked for records that do not exist. */
|
|
118
|
+
export class EdfRangeError extends EdfError {
|
|
119
|
+
readonly edfErrorKind = 'range' as const;
|
|
120
|
+
readonly requested: RecordRange;
|
|
121
|
+
readonly available: RecordRange;
|
|
122
|
+
|
|
123
|
+
constructor(
|
|
124
|
+
message: string,
|
|
125
|
+
init: { requested: RecordRange; available: RecordRange; cause?: unknown },
|
|
126
|
+
) {
|
|
127
|
+
super(message, { cause: init.cause });
|
|
128
|
+
this.requested = init.requested;
|
|
129
|
+
this.available = init.available;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** A `ByteSource` broke its contract: it returned a different number of bytes than asked. */
|
|
134
|
+
export class EdfSourceError extends EdfError {
|
|
135
|
+
readonly edfErrorKind = 'source' as const;
|
|
136
|
+
readonly offset: number;
|
|
137
|
+
readonly requestedLength: number;
|
|
138
|
+
readonly receivedLength: number | undefined;
|
|
139
|
+
|
|
140
|
+
constructor(
|
|
141
|
+
message: string,
|
|
142
|
+
init: {
|
|
143
|
+
offset: number;
|
|
144
|
+
requestedLength: number;
|
|
145
|
+
receivedLength?: number | undefined;
|
|
146
|
+
cause?: unknown;
|
|
147
|
+
},
|
|
148
|
+
) {
|
|
149
|
+
super(message, { cause: init.cause });
|
|
150
|
+
this.offset = init.offset;
|
|
151
|
+
this.requestedLength = init.requestedLength;
|
|
152
|
+
this.receivedLength = init.receivedLength;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* An allocation was refused before it happened. Float64 physical output is four times the
|
|
158
|
+
* on-disk size for EDF, so without this one honest call can take down a browser tab.
|
|
159
|
+
*/
|
|
160
|
+
export class EdfBudgetError extends EdfError {
|
|
161
|
+
readonly edfErrorKind = 'budget' as const;
|
|
162
|
+
readonly requiredBytes: number;
|
|
163
|
+
readonly budgetBytes: number;
|
|
164
|
+
readonly optionName = 'maxMaterializeBytes' as const;
|
|
165
|
+
|
|
166
|
+
constructor(
|
|
167
|
+
message: string,
|
|
168
|
+
init: { requiredBytes: number; budgetBytes: number; cause?: unknown },
|
|
169
|
+
) {
|
|
170
|
+
super(message, { cause: init.cause });
|
|
171
|
+
this.requiredBytes = init.requiredBytes;
|
|
172
|
+
this.budgetBytes = init.budgetBytes;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Two or more signals share the requested label. Real files do this: CHB-MIT ships `T8-P8`
|
|
178
|
+
* twice. Silently returning the first is how the wrong channel ends up in a paper.
|
|
179
|
+
*/
|
|
180
|
+
export class EdfAmbiguousChannelError extends EdfError {
|
|
181
|
+
readonly edfErrorKind = 'channel' as const;
|
|
182
|
+
readonly label: string;
|
|
183
|
+
readonly matchingIndices: readonly number[];
|
|
184
|
+
|
|
185
|
+
constructor(
|
|
186
|
+
message: string,
|
|
187
|
+
init: { label: string; matchingIndices: readonly number[]; cause?: unknown },
|
|
188
|
+
) {
|
|
189
|
+
super(message, { cause: init.cause });
|
|
190
|
+
this.label = init.label;
|
|
191
|
+
this.matchingIndices = init.matchingIndices;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export class EdfChannelNotFoundError extends EdfError {
|
|
196
|
+
readonly edfErrorKind = 'channel' as const;
|
|
197
|
+
readonly selector: string | number;
|
|
198
|
+
readonly availableLabels: readonly string[];
|
|
199
|
+
|
|
200
|
+
constructor(
|
|
201
|
+
message: string,
|
|
202
|
+
init: { selector: string | number; availableLabels: readonly string[]; cause?: unknown },
|
|
203
|
+
) {
|
|
204
|
+
super(message, { cause: init.cause });
|
|
205
|
+
this.selector = init.selector;
|
|
206
|
+
this.availableLabels = init.availableLabels;
|
|
207
|
+
}
|
|
208
|
+
}
|