edf2csv 0.5.34 → 0.5.37
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 +69 -0
- package/dist/edf/reader.d.ts.map +1 -1
- package/dist/edf/reader.js +11 -0
- package/dist/edf/reader.js.map +1 -1
- package/dist/format/csv.d.ts.map +1 -1
- package/dist/format/csv.js +51 -16
- package/dist/format/csv.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,75 @@
|
|
|
3
3
|
Notable changes to edf2csv. Versions follow [semantic versioning](https://semver.org); while the
|
|
4
4
|
major version is 0, a minor bump may contain breaking changes.
|
|
5
5
|
|
|
6
|
+
## 0.5.37
|
|
7
|
+
|
|
8
|
+
### Fixed: the page listing what `--info` cannot report was wrong about most of it
|
|
9
|
+
|
|
10
|
+
"`--info` reads the header and builds a conversion plan without touching the data records or
|
|
11
|
+
the annotation channel. It therefore surfaces every structural, calibration and output-shape
|
|
12
|
+
warning, but it can't raise the ones that only become visible while converting:
|
|
13
|
+
`ANNOTATION_DECODE_FAILED`, `NO_ANNOTATIONS`, `STALE_OUTPUT`, and the two `DISCONTINUOUS`
|
|
14
|
+
variants that come from inspecting record timestamps."
|
|
15
|
+
|
|
16
|
+
It reads the annotation channel for every EDF+ recording — the whole of it for a
|
|
17
|
+
discontinuous file, the first few records of a continuous one — and its own source comment in
|
|
18
|
+
`showInfo` says so at length. Run against the fixtures, `--info` raises
|
|
19
|
+
`ANNOTATION_DECODE_FAILED` and three of the four timestamp-derived `DISCONTINUOUS` variants.
|
|
20
|
+
The `DISCONTINUOUS` section three hundred lines down on the same page says "`--info` raises
|
|
21
|
+
`DISCONTINUOUS` too, since it has to read those record times".
|
|
22
|
+
|
|
23
|
+
The section now says what it reads and what it therefore raises, and names the three it
|
|
24
|
+
genuinely cannot: `NO_ANNOTATIONS` and `STALE_OUTPUT`, which are about files being written,
|
|
25
|
+
and the EDF+C contradiction, which is noticed while the full record-start array is built
|
|
26
|
+
rather than while the origin is found.
|
|
27
|
+
|
|
28
|
+
A test converts four fixtures with `--info --json`, collects the codes, and fails if any of
|
|
29
|
+
them appears in the page's list of what `--info` cannot raise.
|
|
30
|
+
|
|
31
|
+
## 0.5.36
|
|
32
|
+
|
|
33
|
+
### Fixed: every `toStdout` conversion left a listener on `process.stdout`
|
|
34
|
+
|
|
35
|
+
The CSV writer attaches an `'error'` listener so a stream failure surfaces as a message
|
|
36
|
+
rather than an asynchronous throw, and never took it off. Most of those streams are the
|
|
37
|
+
writer's own and go away with it. One does not: `process.stdout`. A library caller converting
|
|
38
|
+
twelve recordings with `toStdout` left twelve listeners on it and got Node's
|
|
39
|
+
`MaxListenersExceededWarning` on the eleventh — a leak warning that was, for once, describing
|
|
40
|
+
a real leak.
|
|
41
|
+
|
|
42
|
+
Taken off when the writer is finished with the stream. After it has finished ending it, not
|
|
43
|
+
before: releasing first meant an `EACCES` arriving during that final `end()` had no listener
|
|
44
|
+
left and went out as an unhandled `'error'` event, a raw stack trace, which is precisely what
|
|
45
|
+
the listener exists to prevent. Fifteen conversions now leave the count where they found it.
|
|
46
|
+
|
|
47
|
+
### Fixed: `readRecords({ chunkBytes: NaN })` failed from inside Node
|
|
48
|
+
|
|
49
|
+
`RangeError: The value of "size" is out of range` out of `Buffer.alloc`, with no mention of
|
|
50
|
+
the option that caused it — while a fractional `startRecord` two lines earlier gets a typed
|
|
51
|
+
`EdfError` naming the field. Every other option there is checked; this one reached the
|
|
52
|
+
allocator. `0`, negatives, `NaN` and `Infinity` are now an `EdfError` that says which option
|
|
53
|
+
it is about. `chunkBytes: 1` still reads a whole record, as documented.
|
|
54
|
+
|
|
55
|
+
## 0.5.35
|
|
56
|
+
|
|
57
|
+
### Fixed: four documents disagreeing with the tool or with themselves
|
|
58
|
+
|
|
59
|
+
- **`CITATION.cff` declared 0.4.19** while package.json said 0.5.34 — 107 releases behind. It
|
|
60
|
+
is the file a citation is generated from, so it is the one version number that ends up in
|
|
61
|
+
somebody else's bibliography rather than only on a page here. A test holds it to
|
|
62
|
+
package.json now.
|
|
63
|
+
- **warnings-and-errors.md said `DISCONTINUOUS` "covers five related conditions"** and listed
|
|
64
|
+
five; the code raises six. The missing one is the EDF+C file whose own records disagree
|
|
65
|
+
about its continuity, which appeared nowhere on the page despite being the condition with
|
|
66
|
+
the most careful implementation behind it — the tolerance it compares against is what
|
|
67
|
+
0.4.41 exists for.
|
|
68
|
+
- **correctness.md opened with "Eight separate claims"** over a sentence reading "covers seven
|
|
69
|
+
different things, verified seven different ways". Consecutive lines.
|
|
70
|
+
- **api.md said `require("edf2csv")` "won't work".** It works on any Node that can require an
|
|
71
|
+
ESM graph, since nothing here uses top-level `await` — verified on 22.16 and 24.4. It does
|
|
72
|
+
fail on the older Node 20 releases that predate that support, which are inside the declared
|
|
73
|
+
range, so the page now says that and points at `await import()` as the portable form.
|
|
74
|
+
|
|
6
75
|
## 0.5.34
|
|
7
76
|
|
|
8
77
|
### Fixed: `--info` over a folder left its warnings unattributable
|
package/dist/edf/reader.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"reader.d.ts","sourceRoot":"","sources":["../../src/edf/reader.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAQH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAE9C,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAExD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAWnD,oGAAoG;AACpG,eAAO,MAAM,mBAAmB,QAAkB,CAAC;AAEnD,MAAM,WAAW,WAAW;IAC1B,2EAA2E;IAC3E,gBAAgB,EAAE,MAAM,CAAC;IACzB,WAAW,EAAE,MAAM,CAAC;IACpB;;;;;OAKG;IACH,IAAI,EAAE,UAAU,CAAC;CAClB;AAED,MAAM,WAAW,kBAAkB;IACjC,sDAAsD;IACtD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,2EAA2E;IAC3E,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,qBAAa,OAAO;;IAClB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B;;;;;;;;OAQG;IACH,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC;IAClC,QAAQ,CAAC,MAAM,EAAE,SAAS,CAAC;IAC3B,sFAAsF;IACtF,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,WAAW,EAAE,UAAU,EAAE,CAAC;IAOnC,OAAO;IAoBP;;;;;;;;;;OAUG;IACG,MAAM,IAAI,OAAO,CAAC,MAAM,CAAC;IAqB/B;;;;;;;OAOG;IACG,gBAAgB,IAAI,OAAO,CAAC,OAAO,CAAC;WA4B7B,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAyDjD,+DAA+D;IAC/D,IAAI,WAAW,IAAI,SAAS,EAAE,CAE7B;IAED;;;;;;;;;;;;OAYG;IACH,IAAI,iBAAiB,IAAI,SAAS,GAAG,SAAS,CAE7C;IAED,IAAI,iBAAiB,IAAI,SAAS,EAAE,CAEnC;IAED,8EAA8E;IAC9E,IAAI,eAAe,IAAI,MAAM,CAE5B;IAED,oDAAoD;IAC7C,WAAW,CAAC,OAAO,GAAE,kBAAuB,GAAG,cAAc,CAAC,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"reader.d.ts","sourceRoot":"","sources":["../../src/edf/reader.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAQH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAE9C,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAExD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAWnD,oGAAoG;AACpG,eAAO,MAAM,mBAAmB,QAAkB,CAAC;AAEnD,MAAM,WAAW,WAAW;IAC1B,2EAA2E;IAC3E,gBAAgB,EAAE,MAAM,CAAC;IACzB,WAAW,EAAE,MAAM,CAAC;IACpB;;;;;OAKG;IACH,IAAI,EAAE,UAAU,CAAC;CAClB;AAED,MAAM,WAAW,kBAAkB;IACjC,sDAAsD;IACtD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,2EAA2E;IAC3E,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,qBAAa,OAAO;;IAClB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B;;;;;;;;OAQG;IACH,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC;IAClC,QAAQ,CAAC,MAAM,EAAE,SAAS,CAAC;IAC3B,sFAAsF;IACtF,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,WAAW,EAAE,UAAU,EAAE,CAAC;IAOnC,OAAO;IAoBP;;;;;;;;;;OAUG;IACG,MAAM,IAAI,OAAO,CAAC,MAAM,CAAC;IAqB/B;;;;;;;OAOG;IACG,gBAAgB,IAAI,OAAO,CAAC,OAAO,CAAC;WA4B7B,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAyDjD,+DAA+D;IAC/D,IAAI,WAAW,IAAI,SAAS,EAAE,CAE7B;IAED;;;;;;;;;;;;OAYG;IACH,IAAI,iBAAiB,IAAI,SAAS,GAAG,SAAS,CAE7C;IAED,IAAI,iBAAiB,IAAI,SAAS,EAAE,CAEnC;IAED,8EAA8E;IAC9E,IAAI,eAAe,IAAI,MAAM,CAE5B;IAED,oDAAoD;IAC7C,WAAW,CAAC,OAAO,GAAE,kBAAuB,GAAG,cAAc,CAAC,WAAW,CAAC;IAmFjF,gDAAgD;IAChD,QAAQ,CAAC,KAAK,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,GAAG,MAAM;IAmBlG,wDAAwD;IACxD,QAAQ,CAAC,KAAK,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,GAAG,MAAM;IAI7E,oEAAoE;IACpE,eAAe,CAAC,KAAK,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,GAAG,UAAU;IAKxF;;;;;;;;;;;OAWG;IACH;;;;;;;;;;;;;;;;;;;;;OAqBG;IACG,UAAU,IAAI,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAsBpC,eAAe,IAAI,OAAO,CAAC;QAC/B,WAAW,EAAE,UAAU,EAAE,CAAC;QAC1B,YAAY,EAAE,CAAC,MAAM,GAAG,IAAI,CAAC,EAAE,CAAC;QAChC,SAAS,EAAE,MAAM,CAAC;QAClB,kFAAkF;QAClF,oBAAoB,EAAE,MAAM,CAAC;KAC9B,CAAC;IAyCI,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;CAS7B"}
|
package/dist/edf/reader.js
CHANGED
|
@@ -217,7 +217,18 @@ export class EdfFile {
|
|
|
217
217
|
if (start >= end)
|
|
218
218
|
return;
|
|
219
219
|
const { recordBytes } = this.header;
|
|
220
|
+
/*
|
|
221
|
+
Checked rather than handed to Buffer.alloc.
|
|
222
|
+
|
|
223
|
+
`chunkBytes: NaN` came back as `RangeError: The value of "size" is out of range` from
|
|
224
|
+
inside Node, with no mention of the option that caused it — while a fractional
|
|
225
|
+
`startRecord` two lines up gets a typed EdfError naming the field. Every other option
|
|
226
|
+
here is checked; this one reached the allocator.
|
|
227
|
+
*/
|
|
220
228
|
const budget = options.chunkBytes ?? DEFAULT_CHUNK_BYTES;
|
|
229
|
+
if (!Number.isFinite(budget) || budget < 1) {
|
|
230
|
+
throw new EdfError('UNREADABLE', `chunkBytes must be a positive number of bytes, got ${String(options.chunkBytes)}.`, 'It is a ceiling on how much of the file is held at once; one record is read whatever it says.');
|
|
231
|
+
}
|
|
221
232
|
/*
|
|
222
233
|
The budget is a ceiling, not an amount to reserve.
|
|
223
234
|
|
package/dist/edf/reader.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"reader.js","sourceRoot":"","sources":["../../src/edf/reader.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Chunked reader for EDF / EDF+ files.\n *\n * Data records are read in batches sized by a byte budget rather than all at once,\n * so peak memory stays flat regardless of how long the recording is. A 4 GB file\n * and a 4 MB file use the same working set.\n */\n\nimport { open, stat } from 'node:fs/promises';\nimport type { FileHandle } from 'node:fs/promises';\n\nimport { createHash } from 'node:crypto';\n\nimport { EdfError } from './errors.js';\nimport type { Diagnostic } from './errors.js';\nimport { FIXED_HEADER_BYTES, SIGNAL_HEADER_BYTES, parseHeader, peekSignalCount } from './header.js';\nimport type { EdfHeader, EdfSignal } from './header.js';\nimport { decodeRecordAnnotations } from './annotations.js';\nimport type { Annotation } from './annotations.js';\nimport { decodeLatin1, readInt16LE } from './bytes.js';\n\n/**\n * How far `readOrigin` looks for a record that states its own start time.\n *\n * Enough that one or two unreadable timekeeping entries at the top of a file cost nothing,\n * few enough that `--info` stays a header read rather than a scan.\n */\nconst RECORDS_SEARCHED_FOR_ORIGIN = 16;\n\n/** Default read budget per batch. Large enough to amortise syscalls, small enough to stay cheap. */\nexport const DEFAULT_CHUNK_BYTES = 8 * 1024 * 1024;\n\nexport interface RecordBatch {\n /** Index of the first record in this batch, relative to the whole file. */\n firstRecordIndex: number;\n recordCount: number;\n /**\n * Raw record bytes, `recordCount * header.recordBytes` long.\n *\n * The buffer is reused between iterations. Copy anything you need to keep past\n * the current loop turn.\n */\n data: Uint8Array;\n}\n\nexport interface ReadRecordsOptions {\n /** First record to read, inclusive. Defaults to 0. */\n startRecord?: number;\n /** Last record to read, exclusive. Defaults to the file's record count. */\n endRecord?: number;\n chunkBytes?: number;\n}\n\nexport class EdfFile {\n readonly path: string;\n readonly fileSize: number;\n /**\n * Last-modified time when this file was opened, in milliseconds, for the same reason as\n * `fileSize`.\n *\n * Kept as the raw number rather than a Date because `new Date(ms).getTime()` truncates to\n * whole milliseconds: comparing that against a later `fstat`, which carries the\n * filesystem's sub-millisecond precision, reported every undisturbed conversion as one\n * whose input had changed underneath it.\n */\n readonly modifiedAtOpenMs: number;\n readonly header: EdfHeader;\n /** Records actually present in the file, which may differ from the header's claim. */\n readonly recordCount: number;\n readonly trailingBytes: number;\n readonly diagnostics: Diagnostic[];\n\n #handle: FileHandle;\n #closed = false;\n /** The last answer `changedSinceOpen` computed, so it survives the file being closed. */\n #changed: boolean | null = null;\n\n private constructor(init: {\n path: string;\n fileSize: number;\n modifiedAtOpenMs: number;\n header: EdfHeader;\n recordCount: number;\n trailingBytes: number;\n diagnostics: Diagnostic[];\n handle: FileHandle;\n }) {\n this.path = init.path;\n this.fileSize = init.fileSize;\n this.modifiedAtOpenMs = init.modifiedAtOpenMs;\n this.header = init.header;\n this.recordCount = init.recordCount;\n this.trailingBytes = init.trailingBytes;\n this.diagnostics = init.diagnostics;\n this.#handle = init.handle;\n }\n\n /**\n * SHA-256 of the bytes this conversion actually read.\n *\n * Hashed through the open descriptor, over exactly the `fileSize` bytes that were there\n * when the file was opened — the same number every record count and window in the output\n * was derived from. Re-opening the path to hash it afterwards described whatever was at\n * that name by then: a recording still being written grew from 2,000 records to 3,000\n * mid-conversion and metadata.json recorded `data_records: 2000` beside the checksum and\n * byte count of the 3,000-record file, which is provenance for bytes nobody converted.\n * Replacing the file at that path did the same thing more completely.\n */\n async sha256(): Promise<string> {\n this.#assertOpen();\n const hash = createHash('sha256');\n const buffer = Buffer.alloc(Math.min(this.fileSize, 4 * 1024 * 1024) || 1);\n for (let at = 0; at < this.fileSize; ) {\n const want = Math.min(buffer.length, this.fileSize - at);\n const { bytesRead } = await this.#handle.read(buffer, 0, want, at);\n if (bytesRead <= 0) {\n throw new EdfError(\n 'UNREADABLE',\n `Expected ${this.fileSize} bytes to checksum but the file ended at ${at}; ` +\n `it appears to have changed size while it was being read.`,\n 'Make sure the recording is not still being written to, then try again.',\n );\n }\n hash.update(buffer.subarray(0, bytesRead));\n at += bytesRead;\n }\n return hash.digest('hex');\n }\n\n /**\n * Whether the file has changed since it was opened, by size or by modification time.\n *\n * Checked through the descriptor, so it answers for the bytes that were read rather than\n * for whatever now answers to the same name. A recording still being written is the\n * ordinary cause, and the conversion is still correct for the data it saw — it is the\n * claim that the output describes the file as it now stands that stops being true.\n */\n async changedSinceOpen(): Promise<boolean> {\n /*\n A closed file remembers its last answer rather than inventing a new one.\n\n Returning false once closed asserted \"it did not change\", which is not something a\n closed descriptor can know — and `convert()` closes the file before it returns, so\n `result.file.changedSinceOpen()` denied the very change the INPUT_CHANGED diagnostic\n in the same result object had just reported. One object, two answers.\n\n `convert()` always asks before closing, so the cached answer is the true one. A caller\n who closed the file without ever asking gets an error, which is the same treatment\n every other method on a closed file gets.\n */\n if (this.#closed) {\n if (this.#changed !== null) return this.#changed;\n throw new EdfError(\n 'UNREADABLE',\n `\"${this.path}\" is closed, and whether it changed while it was open was never checked.`,\n 'Ask before closing the file. A ConvertResult carries the answer already, since ' +\n 'convert() checks it on the way out.',\n );\n }\n const now = await this.#handle.stat().catch(() => null);\n if (now === null) return this.#changed ?? false;\n this.#changed = now.size !== this.fileSize || now.mtimeMs !== this.modifiedAtOpenMs;\n return this.#changed;\n }\n\n static async open(path: string): Promise<EdfFile> {\n const info = await stat(path).catch((cause: unknown) => {\n throw new EdfError('UNREADABLE', `Cannot read \"${path}\": ${describe(cause)}`);\n });\n if (info.isDirectory()) {\n throw new EdfError('UNREADABLE', `\"${path}\" is a directory, not an EDF file.`);\n }\n if (!info.isFile()) {\n throw new EdfError('UNREADABLE', `\"${path}\" is not a regular file.`);\n }\n\n const handle = await open(path, 'r');\n try {\n const fixed = Buffer.alloc(Math.min(FIXED_HEADER_BYTES, info.size));\n if (fixed.length > 0) {\n const bytesRead = await readFully(handle, fixed, 0, fixed.length, 0);\n if (bytesRead < fixed.length) throw changedWhileReading(0, fixed.length, bytesRead);\n }\n\n // The signal count decides how much more header there is to read. Read by the header\n // parser itself, so the two cannot disagree about which files are readable: this used\n // to have its own Number(), which tolerated the NUL padding sloppy writers emit but\n // not the comma decimal separator that COMMA_DECIMAL exists to accept.\n let headerBuffer = fixed;\n if (fixed.length === FIXED_HEADER_BYTES) {\n const ns = peekSignalCount(fixed);\n if (ns !== null) {\n const total = FIXED_HEADER_BYTES + ns * SIGNAL_HEADER_BYTES;\n if (total <= info.size) {\n headerBuffer = Buffer.alloc(total);\n const bytesRead = await readFully(handle, headerBuffer, 0, total, 0);\n if (bytesRead < total) throw changedWhileReading(0, total, bytesRead);\n }\n }\n }\n\n const { header, recordCount, trailingBytes, diagnostics } = parseHeader(\n headerBuffer,\n info.size,\n );\n\n return new EdfFile({\n path,\n fileSize: info.size,\n modifiedAtOpenMs: info.mtimeMs,\n header,\n recordCount,\n trailingBytes,\n diagnostics,\n handle,\n });\n } catch (error) {\n await handle.close().catch(() => {});\n throw error;\n }\n }\n\n /** Signal channels, excluding the EDF+ annotations channel. */\n get dataSignals(): EdfSignal[] {\n return this.header.signals.filter((s) => !s.isAnnotations);\n }\n\n /**\n * The annotation channel a record's start time is read from.\n *\n * EDF+ puts the timekeeping TAL first in the first annotation channel, and this was read as\n * `annotationSignals[0]` — the first one declared, whether or not it can hold anything. A\n * writer that declares an annotation channel and gives it zero samples per record leaves a\n * slot of zero bytes, so nothing was read from it, and the timekeeping in the channel after\n * it went unread: a three-record EDF+D reported \"3 of 3 data records carry no readable\n * timekeeping annotation\" about three that were perfectly readable, and timed the file from\n * zero.\n *\n * A channel with no room carries nothing, so it is not the one the TAL is in.\n */\n get timekeepingSignal(): EdfSignal | undefined {\n return this.annotationSignals.find((signal) => signal.samplesPerRecord > 0);\n }\n\n get annotationSignals(): EdfSignal[] {\n return this.header.signals.filter((s) => s.isAnnotations);\n }\n\n /** Total recording duration in seconds, based on records actually present. */\n get durationSeconds(): number {\n return this.recordCount * this.header.recordDuration;\n }\n\n /** Read a half-open range of records in batches. */\n async *readRecords(options: ReadRecordsOptions = {}): AsyncGenerator<RecordBatch> {\n this.#assertOpen();\n\n /*\n Record bounds have to be whole records.\n\n A fractional `startRecord` was carried straight into `position = headerBytes +\n record * recordBytes`, so reading from 1.5 began half a record in and every sample\n after it was decoded from the wrong offset: on the two-channel test fixture it\n returned channel 2's values under channel 1's signal, with no error. Clamping\n silently would be no better, since a caller asking for record 1.5 has a bug the\n library should name rather than paper over.\n */\n for (const [name, value] of [\n ['startRecord', options.startRecord],\n ['endRecord', options.endRecord],\n ] as const) {\n if (value !== undefined && !Number.isInteger(value)) {\n throw new EdfError(\n 'BAD_HEADER_FIELD',\n `readRecords: ${name} must be a whole record index, got ${value}.`,\n 'Record boundaries are the unit the file can be read in; a fractional index would decode samples from the middle of a record.',\n );\n }\n }\n\n const start = Math.max(0, options.startRecord ?? 0);\n const end = Math.min(this.recordCount, options.endRecord ?? this.recordCount);\n if (start >= end) return;\n\n const { recordBytes } = this.header;\n const budget = options.chunkBytes ?? DEFAULT_CHUNK_BYTES;\n /*\n The budget is a ceiling, not an amount to reserve.\n\n `Math.floor(budget / recordBytes)` is how many records would fit in it, and the buffer\n was that many — whether or not the file had that many. A 848-byte fixture read with a\n 512 MB budget allocated 536,870,880 bytes for its two records, and every ordinary read\n of a small file reserved the full 8 MB default. Nothing was wrong with the data; the\n memory just had nothing to do with it.\n\n Bounded by what is actually going to be read, so a batch of five hundred short\n recordings costs five hundred short buffers rather than five hundred 8 MB ones.\n */\n const perChunk = Math.max(1, Math.min(Math.floor(budget / recordBytes), end - start));\n const buffer = Buffer.alloc(perChunk * recordBytes);\n\n for (let record = start; record < end; record += perChunk) {\n const count = Math.min(perChunk, end - record);\n const bytes = count * recordBytes;\n const position = this.header.headerBytes + record * recordBytes;\n\n const bytesRead = await readFully(this.#handle, buffer, 0, bytes, position);\n if (bytesRead < bytes) {\n // The file is shorter than its own size said. Quietly stopping here would\n // hand back a conversion missing its tail with nothing to show for it.\n throw new EdfError(\n 'UNREADABLE',\n `Expected ${bytes} bytes of data at record ${record} but only ${bytesRead} were ` +\n `available; the file appears to have changed size while it was being read.`,\n 'Make sure the recording is not still being written to, then try again.',\n );\n }\n\n yield { firstRecordIndex: record, recordCount: count, data: buffer.subarray(0, bytes) };\n }\n }\n\n /** Read one sample as its raw digital value. */\n sampleAt(batch: RecordBatch, recordOffset: number, signal: EdfSignal, sampleIndex: number): number {\n const position =\n recordOffset * this.header.recordBytes +\n signal.byteOffsetInRecord +\n sampleIndex * this.header.bytesPerSample;\n\n if (this.header.bytesPerSample === 3) {\n // BDF stores 24-bit little-endian two's complement. Loading the three bytes\n // into the top of a 32-bit word and shifting back down sign-extends them.\n const data = batch.data;\n return (\n ((data[position] as number) << 8) |\n ((data[position + 1] as number) << 16) |\n ((data[position + 2] as number) << 24)\n ) >> 8;\n }\n return readInt16LE(batch.data, position);\n }\n\n /** Byte offset of a signal's samples within a batch. */\n offsetOf(batch: RecordBatch, recordOffset: number, signal: EdfSignal): number {\n return recordOffset * this.header.recordBytes + signal.byteOffsetInRecord;\n }\n\n /** The annotation channel's raw bytes for one record in a batch. */\n annotationBytes(batch: RecordBatch, recordOffset: number, signal: EdfSignal): Uint8Array {\n const start = this.offsetOf(batch, recordOffset, signal);\n return batch.data.subarray(start, start + signal.samplesPerRecord * this.header.bytesPerSample);\n }\n\n /**\n * Read every EDF+ annotation in the file, plus the start time each record declares.\n *\n * Only the annotation channel is read, seeking straight to it inside each record\n * rather than pulling whole records through memory. On a multi-gigabyte recording\n * that is the difference between a few kilobytes of I/O and all of it.\n *\n * The whole file is always scanned, never just the records inside a requested\n * window: writers are not obliged to store an annotation in the record its onset\n * falls in, and some put every annotation in the first record. Reading only the\n * window's records would drop those entirely.\n */\n /**\n * Where this continuous recording begins, from the first record that says.\n *\n * A few records' worth of annotation bytes rather than the whole channel. A continuous\n * recording's origin is the fraction of a second by which its first record follows the\n * header's start time, and `--info` needs that to place a requested window — but it does\n * not need the events, and finding one number by reading every record costs a seek per\n * record across the whole file, which is the scan `--info` was deliberately spared.\n *\n * It reads on past record 0 because a conversion does. This used to stop there, so the\n * moment one timekeeping TAL was unreadable the two disagreed: the conversion took the\n * origin from record 1 and timed the file from 0.5s, while `--info` found nothing at\n * record 0 and reported a recording starting at zero — the same file described two ways by\n * one tool. Records are contiguous, so record `i` beginning at `t` puts the origin at\n * `t - i * duration`, and any one of them settles it.\n *\n * The bound is what keeps this cheap: a file whose first `RECORDS_SEARCHED_FOR_ORIGIN`\n * timekeeping entries are all unreadable reports an origin of zero here, and converting it\n * raises ANNOTATION_DECODE_FAILED for every one of them.\n *\n * Returns null when there is nothing to read it from, in which case the origin is zero.\n */\n async readOrigin(): Promise<number | null> {\n this.#assertOpen();\n\n const channel = this.timekeepingSignal;\n if (!channel || this.recordCount === 0) return null;\n\n const { headerBytes, bytesPerSample, recordBytes, recordDuration } = this.header;\n const buffer = Buffer.alloc(channel.samplesPerRecord * bytesPerSample);\n if (buffer.length === 0) return null;\n\n const searched = Math.min(this.recordCount, RECORDS_SEARCHED_FOR_ORIGIN);\n for (let record = 0; record < searched; record++) {\n const offset = headerBytes + record * recordBytes + channel.byteOffsetInRecord;\n const bytesRead = await readFully(this.#handle, buffer, 0, buffer.length, offset);\n if (bytesRead < buffer.length) return null;\n\n const start = decodeRecordAnnotations(buffer, record).recordStart;\n if (start !== null) return start - record * recordDuration;\n }\n return null;\n }\n\n async readAnnotations(): Promise<{\n annotations: Annotation[];\n recordStarts: (number | null)[];\n malformed: number;\n /** Unreadable TALs in first position, which carry timing rather than an event. */\n malformedTimekeeping: number;\n }> {\n this.#assertOpen();\n\n const annotations: Annotation[] = [];\n const recordStarts: (number | null)[] = new Array<number | null>(this.recordCount).fill(null);\n let malformed = 0;\n let malformedTimekeeping = 0;\n\n const channels = this.annotationSignals;\n if (channels.length === 0) {\n return { annotations, recordStarts, malformed, malformedTimekeeping };\n }\n\n const { headerBytes, recordBytes, bytesPerSample } = this.header;\n const buffers = channels.map((c) => Buffer.alloc(c.samplesPerRecord * bytesPerSample));\n const timekeeping = this.timekeepingSignal;\n\n for (let record = 0; record < this.recordCount; record++) {\n for (const [position, channel] of channels.entries()) {\n const buffer = buffers[position];\n if (!buffer || buffer.length === 0) continue;\n\n const offset = headerBytes + record * recordBytes + channel.byteOffsetInRecord;\n const bytesRead = await readFully(this.#handle, buffer, 0, buffer.length, offset);\n if (bytesRead < buffer.length) {\n throw changedWhileReading(record, buffer.length, bytesRead, 'annotation data');\n }\n\n const decoded = decodeRecordAnnotations(buffer, record);\n // Only the timekeeping channel carries the record's start; see timekeepingSignal.\n if (channel === timekeeping) recordStarts[record] = decoded.recordStart;\n for (const annotation of decoded.annotations) annotations.push(annotation);\n malformed += decoded.malformed;\n malformedTimekeeping += decoded.malformedTimekeeping;\n }\n }\n\n annotations.sort((a, b) => a.onset - b.onset || a.recordIndex - b.recordIndex);\n return { annotations, recordStarts, malformed, malformedTimekeeping };\n }\n\n async close(): Promise<void> {\n if (this.#closed) return;\n this.#closed = true;\n await this.#handle.close();\n }\n\n #assertOpen(): void {\n if (this.#closed) throw new EdfError('UNREADABLE', 'This EDF file has already been closed.');\n }\n}\n\nfunction describe(cause: unknown): string {\n if (cause instanceof Error) {\n const code = (cause as NodeJS.ErrnoException).code;\n if (code === 'ENOENT') return 'no such file';\n if (code === 'EACCES') return 'permission denied';\n return cause.message;\n }\n return String(cause);\n}\n\n/**\n * The most `fs.read` will accept as a length.\n *\n * Node asserts on a length that does not fit in a signed 32-bit integer, and it asserts in\n * C++: `Assertion failed: args[3]->IsInt32()`, forty frames of native stack, SIGABRT. Not an\n * exception — nothing in JavaScript sees it, so no catch block and no `uncaughtException`\n * handler runs, and a library consumer's whole process goes down with it.\n *\n * A round gigabyte rather than the exact limit, so the loop below does whole even reads.\n */\nconst MAX_READ_BYTES = 1024 * 1024 * 1024;\n\n/** Fill a requested region unless EOF is reached; regular-file reads may legally be short. */\nasync function readFully(\n handle: FileHandle,\n buffer: Buffer,\n offset: number,\n length: number,\n position: number,\n): Promise<number> {\n let total = 0;\n while (total < length) {\n /*\n Capped, because one data record can be larger than a single read may be.\n\n A record is read in one call when it exceeds the chunk budget — there is nothing\n smaller to divide it by, since a record is the unit the format is addressed in. EDF's\n samples-per-record field is 8 characters, so eleven channels at 99,999,999 samples make\n a record of 2.2 GB, and a long record duration at ordinary rates gets there too. That\n went to `fs.read` as a single length over 2^31-1 and took the process out with a native\n assertion rather than an error.\n\n Looping was already how a short read is handled, so the cap costs one more iteration\n per gigabyte and nothing else.\n */\n const want = Math.min(length - total, MAX_READ_BYTES);\n const { bytesRead } = await handle.read(buffer, offset + total, want, position + total);\n if (bytesRead === 0) break;\n total += bytesRead;\n }\n return total;\n}\n\nfunction changedWhileReading(\n record: number,\n expected: number,\n actual: number,\n subject = 'data',\n): EdfError {\n return new EdfError(\n 'UNREADABLE',\n `Expected ${expected} bytes of ${subject} at record ${record} but only ${actual} were ` +\n `available; the file appears to have changed size while it was being read.`,\n 'Make sure the recording is not still being written to, then try again.',\n );\n}\n"]}
|
|
1
|
+
{"version":3,"file":"reader.js","sourceRoot":"","sources":["../../src/edf/reader.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Chunked reader for EDF / EDF+ files.\n *\n * Data records are read in batches sized by a byte budget rather than all at once,\n * so peak memory stays flat regardless of how long the recording is. A 4 GB file\n * and a 4 MB file use the same working set.\n */\n\nimport { open, stat } from 'node:fs/promises';\nimport type { FileHandle } from 'node:fs/promises';\n\nimport { createHash } from 'node:crypto';\n\nimport { EdfError } from './errors.js';\nimport type { Diagnostic } from './errors.js';\nimport { FIXED_HEADER_BYTES, SIGNAL_HEADER_BYTES, parseHeader, peekSignalCount } from './header.js';\nimport type { EdfHeader, EdfSignal } from './header.js';\nimport { decodeRecordAnnotations } from './annotations.js';\nimport type { Annotation } from './annotations.js';\nimport { decodeLatin1, readInt16LE } from './bytes.js';\n\n/**\n * How far `readOrigin` looks for a record that states its own start time.\n *\n * Enough that one or two unreadable timekeeping entries at the top of a file cost nothing,\n * few enough that `--info` stays a header read rather than a scan.\n */\nconst RECORDS_SEARCHED_FOR_ORIGIN = 16;\n\n/** Default read budget per batch. Large enough to amortise syscalls, small enough to stay cheap. */\nexport const DEFAULT_CHUNK_BYTES = 8 * 1024 * 1024;\n\nexport interface RecordBatch {\n /** Index of the first record in this batch, relative to the whole file. */\n firstRecordIndex: number;\n recordCount: number;\n /**\n * Raw record bytes, `recordCount * header.recordBytes` long.\n *\n * The buffer is reused between iterations. Copy anything you need to keep past\n * the current loop turn.\n */\n data: Uint8Array;\n}\n\nexport interface ReadRecordsOptions {\n /** First record to read, inclusive. Defaults to 0. */\n startRecord?: number;\n /** Last record to read, exclusive. Defaults to the file's record count. */\n endRecord?: number;\n chunkBytes?: number;\n}\n\nexport class EdfFile {\n readonly path: string;\n readonly fileSize: number;\n /**\n * Last-modified time when this file was opened, in milliseconds, for the same reason as\n * `fileSize`.\n *\n * Kept as the raw number rather than a Date because `new Date(ms).getTime()` truncates to\n * whole milliseconds: comparing that against a later `fstat`, which carries the\n * filesystem's sub-millisecond precision, reported every undisturbed conversion as one\n * whose input had changed underneath it.\n */\n readonly modifiedAtOpenMs: number;\n readonly header: EdfHeader;\n /** Records actually present in the file, which may differ from the header's claim. */\n readonly recordCount: number;\n readonly trailingBytes: number;\n readonly diagnostics: Diagnostic[];\n\n #handle: FileHandle;\n #closed = false;\n /** The last answer `changedSinceOpen` computed, so it survives the file being closed. */\n #changed: boolean | null = null;\n\n private constructor(init: {\n path: string;\n fileSize: number;\n modifiedAtOpenMs: number;\n header: EdfHeader;\n recordCount: number;\n trailingBytes: number;\n diagnostics: Diagnostic[];\n handle: FileHandle;\n }) {\n this.path = init.path;\n this.fileSize = init.fileSize;\n this.modifiedAtOpenMs = init.modifiedAtOpenMs;\n this.header = init.header;\n this.recordCount = init.recordCount;\n this.trailingBytes = init.trailingBytes;\n this.diagnostics = init.diagnostics;\n this.#handle = init.handle;\n }\n\n /**\n * SHA-256 of the bytes this conversion actually read.\n *\n * Hashed through the open descriptor, over exactly the `fileSize` bytes that were there\n * when the file was opened — the same number every record count and window in the output\n * was derived from. Re-opening the path to hash it afterwards described whatever was at\n * that name by then: a recording still being written grew from 2,000 records to 3,000\n * mid-conversion and metadata.json recorded `data_records: 2000` beside the checksum and\n * byte count of the 3,000-record file, which is provenance for bytes nobody converted.\n * Replacing the file at that path did the same thing more completely.\n */\n async sha256(): Promise<string> {\n this.#assertOpen();\n const hash = createHash('sha256');\n const buffer = Buffer.alloc(Math.min(this.fileSize, 4 * 1024 * 1024) || 1);\n for (let at = 0; at < this.fileSize; ) {\n const want = Math.min(buffer.length, this.fileSize - at);\n const { bytesRead } = await this.#handle.read(buffer, 0, want, at);\n if (bytesRead <= 0) {\n throw new EdfError(\n 'UNREADABLE',\n `Expected ${this.fileSize} bytes to checksum but the file ended at ${at}; ` +\n `it appears to have changed size while it was being read.`,\n 'Make sure the recording is not still being written to, then try again.',\n );\n }\n hash.update(buffer.subarray(0, bytesRead));\n at += bytesRead;\n }\n return hash.digest('hex');\n }\n\n /**\n * Whether the file has changed since it was opened, by size or by modification time.\n *\n * Checked through the descriptor, so it answers for the bytes that were read rather than\n * for whatever now answers to the same name. A recording still being written is the\n * ordinary cause, and the conversion is still correct for the data it saw — it is the\n * claim that the output describes the file as it now stands that stops being true.\n */\n async changedSinceOpen(): Promise<boolean> {\n /*\n A closed file remembers its last answer rather than inventing a new one.\n\n Returning false once closed asserted \"it did not change\", which is not something a\n closed descriptor can know — and `convert()` closes the file before it returns, so\n `result.file.changedSinceOpen()` denied the very change the INPUT_CHANGED diagnostic\n in the same result object had just reported. One object, two answers.\n\n `convert()` always asks before closing, so the cached answer is the true one. A caller\n who closed the file without ever asking gets an error, which is the same treatment\n every other method on a closed file gets.\n */\n if (this.#closed) {\n if (this.#changed !== null) return this.#changed;\n throw new EdfError(\n 'UNREADABLE',\n `\"${this.path}\" is closed, and whether it changed while it was open was never checked.`,\n 'Ask before closing the file. A ConvertResult carries the answer already, since ' +\n 'convert() checks it on the way out.',\n );\n }\n const now = await this.#handle.stat().catch(() => null);\n if (now === null) return this.#changed ?? false;\n this.#changed = now.size !== this.fileSize || now.mtimeMs !== this.modifiedAtOpenMs;\n return this.#changed;\n }\n\n static async open(path: string): Promise<EdfFile> {\n const info = await stat(path).catch((cause: unknown) => {\n throw new EdfError('UNREADABLE', `Cannot read \"${path}\": ${describe(cause)}`);\n });\n if (info.isDirectory()) {\n throw new EdfError('UNREADABLE', `\"${path}\" is a directory, not an EDF file.`);\n }\n if (!info.isFile()) {\n throw new EdfError('UNREADABLE', `\"${path}\" is not a regular file.`);\n }\n\n const handle = await open(path, 'r');\n try {\n const fixed = Buffer.alloc(Math.min(FIXED_HEADER_BYTES, info.size));\n if (fixed.length > 0) {\n const bytesRead = await readFully(handle, fixed, 0, fixed.length, 0);\n if (bytesRead < fixed.length) throw changedWhileReading(0, fixed.length, bytesRead);\n }\n\n // The signal count decides how much more header there is to read. Read by the header\n // parser itself, so the two cannot disagree about which files are readable: this used\n // to have its own Number(), which tolerated the NUL padding sloppy writers emit but\n // not the comma decimal separator that COMMA_DECIMAL exists to accept.\n let headerBuffer = fixed;\n if (fixed.length === FIXED_HEADER_BYTES) {\n const ns = peekSignalCount(fixed);\n if (ns !== null) {\n const total = FIXED_HEADER_BYTES + ns * SIGNAL_HEADER_BYTES;\n if (total <= info.size) {\n headerBuffer = Buffer.alloc(total);\n const bytesRead = await readFully(handle, headerBuffer, 0, total, 0);\n if (bytesRead < total) throw changedWhileReading(0, total, bytesRead);\n }\n }\n }\n\n const { header, recordCount, trailingBytes, diagnostics } = parseHeader(\n headerBuffer,\n info.size,\n );\n\n return new EdfFile({\n path,\n fileSize: info.size,\n modifiedAtOpenMs: info.mtimeMs,\n header,\n recordCount,\n trailingBytes,\n diagnostics,\n handle,\n });\n } catch (error) {\n await handle.close().catch(() => {});\n throw error;\n }\n }\n\n /** Signal channels, excluding the EDF+ annotations channel. */\n get dataSignals(): EdfSignal[] {\n return this.header.signals.filter((s) => !s.isAnnotations);\n }\n\n /**\n * The annotation channel a record's start time is read from.\n *\n * EDF+ puts the timekeeping TAL first in the first annotation channel, and this was read as\n * `annotationSignals[0]` — the first one declared, whether or not it can hold anything. A\n * writer that declares an annotation channel and gives it zero samples per record leaves a\n * slot of zero bytes, so nothing was read from it, and the timekeeping in the channel after\n * it went unread: a three-record EDF+D reported \"3 of 3 data records carry no readable\n * timekeeping annotation\" about three that were perfectly readable, and timed the file from\n * zero.\n *\n * A channel with no room carries nothing, so it is not the one the TAL is in.\n */\n get timekeepingSignal(): EdfSignal | undefined {\n return this.annotationSignals.find((signal) => signal.samplesPerRecord > 0);\n }\n\n get annotationSignals(): EdfSignal[] {\n return this.header.signals.filter((s) => s.isAnnotations);\n }\n\n /** Total recording duration in seconds, based on records actually present. */\n get durationSeconds(): number {\n return this.recordCount * this.header.recordDuration;\n }\n\n /** Read a half-open range of records in batches. */\n async *readRecords(options: ReadRecordsOptions = {}): AsyncGenerator<RecordBatch> {\n this.#assertOpen();\n\n /*\n Record bounds have to be whole records.\n\n A fractional `startRecord` was carried straight into `position = headerBytes +\n record * recordBytes`, so reading from 1.5 began half a record in and every sample\n after it was decoded from the wrong offset: on the two-channel test fixture it\n returned channel 2's values under channel 1's signal, with no error. Clamping\n silently would be no better, since a caller asking for record 1.5 has a bug the\n library should name rather than paper over.\n */\n for (const [name, value] of [\n ['startRecord', options.startRecord],\n ['endRecord', options.endRecord],\n ] as const) {\n if (value !== undefined && !Number.isInteger(value)) {\n throw new EdfError(\n 'BAD_HEADER_FIELD',\n `readRecords: ${name} must be a whole record index, got ${value}.`,\n 'Record boundaries are the unit the file can be read in; a fractional index would decode samples from the middle of a record.',\n );\n }\n }\n\n const start = Math.max(0, options.startRecord ?? 0);\n const end = Math.min(this.recordCount, options.endRecord ?? this.recordCount);\n if (start >= end) return;\n\n const { recordBytes } = this.header;\n /*\n Checked rather than handed to Buffer.alloc.\n\n `chunkBytes: NaN` came back as `RangeError: The value of \"size\" is out of range` from\n inside Node, with no mention of the option that caused it — while a fractional\n `startRecord` two lines up gets a typed EdfError naming the field. Every other option\n here is checked; this one reached the allocator.\n */\n const budget = options.chunkBytes ?? DEFAULT_CHUNK_BYTES;\n if (!Number.isFinite(budget) || budget < 1) {\n throw new EdfError(\n 'UNREADABLE',\n `chunkBytes must be a positive number of bytes, got ${String(options.chunkBytes)}.`,\n 'It is a ceiling on how much of the file is held at once; one record is read whatever it says.',\n );\n }\n /*\n The budget is a ceiling, not an amount to reserve.\n\n `Math.floor(budget / recordBytes)` is how many records would fit in it, and the buffer\n was that many — whether or not the file had that many. A 848-byte fixture read with a\n 512 MB budget allocated 536,870,880 bytes for its two records, and every ordinary read\n of a small file reserved the full 8 MB default. Nothing was wrong with the data; the\n memory just had nothing to do with it.\n\n Bounded by what is actually going to be read, so a batch of five hundred short\n recordings costs five hundred short buffers rather than five hundred 8 MB ones.\n */\n const perChunk = Math.max(1, Math.min(Math.floor(budget / recordBytes), end - start));\n const buffer = Buffer.alloc(perChunk * recordBytes);\n\n for (let record = start; record < end; record += perChunk) {\n const count = Math.min(perChunk, end - record);\n const bytes = count * recordBytes;\n const position = this.header.headerBytes + record * recordBytes;\n\n const bytesRead = await readFully(this.#handle, buffer, 0, bytes, position);\n if (bytesRead < bytes) {\n // The file is shorter than its own size said. Quietly stopping here would\n // hand back a conversion missing its tail with nothing to show for it.\n throw new EdfError(\n 'UNREADABLE',\n `Expected ${bytes} bytes of data at record ${record} but only ${bytesRead} were ` +\n `available; the file appears to have changed size while it was being read.`,\n 'Make sure the recording is not still being written to, then try again.',\n );\n }\n\n yield { firstRecordIndex: record, recordCount: count, data: buffer.subarray(0, bytes) };\n }\n }\n\n /** Read one sample as its raw digital value. */\n sampleAt(batch: RecordBatch, recordOffset: number, signal: EdfSignal, sampleIndex: number): number {\n const position =\n recordOffset * this.header.recordBytes +\n signal.byteOffsetInRecord +\n sampleIndex * this.header.bytesPerSample;\n\n if (this.header.bytesPerSample === 3) {\n // BDF stores 24-bit little-endian two's complement. Loading the three bytes\n // into the top of a 32-bit word and shifting back down sign-extends them.\n const data = batch.data;\n return (\n ((data[position] as number) << 8) |\n ((data[position + 1] as number) << 16) |\n ((data[position + 2] as number) << 24)\n ) >> 8;\n }\n return readInt16LE(batch.data, position);\n }\n\n /** Byte offset of a signal's samples within a batch. */\n offsetOf(batch: RecordBatch, recordOffset: number, signal: EdfSignal): number {\n return recordOffset * this.header.recordBytes + signal.byteOffsetInRecord;\n }\n\n /** The annotation channel's raw bytes for one record in a batch. */\n annotationBytes(batch: RecordBatch, recordOffset: number, signal: EdfSignal): Uint8Array {\n const start = this.offsetOf(batch, recordOffset, signal);\n return batch.data.subarray(start, start + signal.samplesPerRecord * this.header.bytesPerSample);\n }\n\n /**\n * Read every EDF+ annotation in the file, plus the start time each record declares.\n *\n * Only the annotation channel is read, seeking straight to it inside each record\n * rather than pulling whole records through memory. On a multi-gigabyte recording\n * that is the difference between a few kilobytes of I/O and all of it.\n *\n * The whole file is always scanned, never just the records inside a requested\n * window: writers are not obliged to store an annotation in the record its onset\n * falls in, and some put every annotation in the first record. Reading only the\n * window's records would drop those entirely.\n */\n /**\n * Where this continuous recording begins, from the first record that says.\n *\n * A few records' worth of annotation bytes rather than the whole channel. A continuous\n * recording's origin is the fraction of a second by which its first record follows the\n * header's start time, and `--info` needs that to place a requested window — but it does\n * not need the events, and finding one number by reading every record costs a seek per\n * record across the whole file, which is the scan `--info` was deliberately spared.\n *\n * It reads on past record 0 because a conversion does. This used to stop there, so the\n * moment one timekeeping TAL was unreadable the two disagreed: the conversion took the\n * origin from record 1 and timed the file from 0.5s, while `--info` found nothing at\n * record 0 and reported a recording starting at zero — the same file described two ways by\n * one tool. Records are contiguous, so record `i` beginning at `t` puts the origin at\n * `t - i * duration`, and any one of them settles it.\n *\n * The bound is what keeps this cheap: a file whose first `RECORDS_SEARCHED_FOR_ORIGIN`\n * timekeeping entries are all unreadable reports an origin of zero here, and converting it\n * raises ANNOTATION_DECODE_FAILED for every one of them.\n *\n * Returns null when there is nothing to read it from, in which case the origin is zero.\n */\n async readOrigin(): Promise<number | null> {\n this.#assertOpen();\n\n const channel = this.timekeepingSignal;\n if (!channel || this.recordCount === 0) return null;\n\n const { headerBytes, bytesPerSample, recordBytes, recordDuration } = this.header;\n const buffer = Buffer.alloc(channel.samplesPerRecord * bytesPerSample);\n if (buffer.length === 0) return null;\n\n const searched = Math.min(this.recordCount, RECORDS_SEARCHED_FOR_ORIGIN);\n for (let record = 0; record < searched; record++) {\n const offset = headerBytes + record * recordBytes + channel.byteOffsetInRecord;\n const bytesRead = await readFully(this.#handle, buffer, 0, buffer.length, offset);\n if (bytesRead < buffer.length) return null;\n\n const start = decodeRecordAnnotations(buffer, record).recordStart;\n if (start !== null) return start - record * recordDuration;\n }\n return null;\n }\n\n async readAnnotations(): Promise<{\n annotations: Annotation[];\n recordStarts: (number | null)[];\n malformed: number;\n /** Unreadable TALs in first position, which carry timing rather than an event. */\n malformedTimekeeping: number;\n }> {\n this.#assertOpen();\n\n const annotations: Annotation[] = [];\n const recordStarts: (number | null)[] = new Array<number | null>(this.recordCount).fill(null);\n let malformed = 0;\n let malformedTimekeeping = 0;\n\n const channels = this.annotationSignals;\n if (channels.length === 0) {\n return { annotations, recordStarts, malformed, malformedTimekeeping };\n }\n\n const { headerBytes, recordBytes, bytesPerSample } = this.header;\n const buffers = channels.map((c) => Buffer.alloc(c.samplesPerRecord * bytesPerSample));\n const timekeeping = this.timekeepingSignal;\n\n for (let record = 0; record < this.recordCount; record++) {\n for (const [position, channel] of channels.entries()) {\n const buffer = buffers[position];\n if (!buffer || buffer.length === 0) continue;\n\n const offset = headerBytes + record * recordBytes + channel.byteOffsetInRecord;\n const bytesRead = await readFully(this.#handle, buffer, 0, buffer.length, offset);\n if (bytesRead < buffer.length) {\n throw changedWhileReading(record, buffer.length, bytesRead, 'annotation data');\n }\n\n const decoded = decodeRecordAnnotations(buffer, record);\n // Only the timekeeping channel carries the record's start; see timekeepingSignal.\n if (channel === timekeeping) recordStarts[record] = decoded.recordStart;\n for (const annotation of decoded.annotations) annotations.push(annotation);\n malformed += decoded.malformed;\n malformedTimekeeping += decoded.malformedTimekeeping;\n }\n }\n\n annotations.sort((a, b) => a.onset - b.onset || a.recordIndex - b.recordIndex);\n return { annotations, recordStarts, malformed, malformedTimekeeping };\n }\n\n async close(): Promise<void> {\n if (this.#closed) return;\n this.#closed = true;\n await this.#handle.close();\n }\n\n #assertOpen(): void {\n if (this.#closed) throw new EdfError('UNREADABLE', 'This EDF file has already been closed.');\n }\n}\n\nfunction describe(cause: unknown): string {\n if (cause instanceof Error) {\n const code = (cause as NodeJS.ErrnoException).code;\n if (code === 'ENOENT') return 'no such file';\n if (code === 'EACCES') return 'permission denied';\n return cause.message;\n }\n return String(cause);\n}\n\n/**\n * The most `fs.read` will accept as a length.\n *\n * Node asserts on a length that does not fit in a signed 32-bit integer, and it asserts in\n * C++: `Assertion failed: args[3]->IsInt32()`, forty frames of native stack, SIGABRT. Not an\n * exception — nothing in JavaScript sees it, so no catch block and no `uncaughtException`\n * handler runs, and a library consumer's whole process goes down with it.\n *\n * A round gigabyte rather than the exact limit, so the loop below does whole even reads.\n */\nconst MAX_READ_BYTES = 1024 * 1024 * 1024;\n\n/** Fill a requested region unless EOF is reached; regular-file reads may legally be short. */\nasync function readFully(\n handle: FileHandle,\n buffer: Buffer,\n offset: number,\n length: number,\n position: number,\n): Promise<number> {\n let total = 0;\n while (total < length) {\n /*\n Capped, because one data record can be larger than a single read may be.\n\n A record is read in one call when it exceeds the chunk budget — there is nothing\n smaller to divide it by, since a record is the unit the format is addressed in. EDF's\n samples-per-record field is 8 characters, so eleven channels at 99,999,999 samples make\n a record of 2.2 GB, and a long record duration at ordinary rates gets there too. That\n went to `fs.read` as a single length over 2^31-1 and took the process out with a native\n assertion rather than an error.\n\n Looping was already how a short read is handled, so the cap costs one more iteration\n per gigabyte and nothing else.\n */\n const want = Math.min(length - total, MAX_READ_BYTES);\n const { bytesRead } = await handle.read(buffer, offset + total, want, position + total);\n if (bytesRead === 0) break;\n total += bytesRead;\n }\n return total;\n}\n\nfunction changedWhileReading(\n record: number,\n expected: number,\n actual: number,\n subject = 'data',\n): EdfError {\n return new EdfError(\n 'UNREADABLE',\n `Expected ${expected} bytes of ${subject} at record ${record} but only ${actual} were ` +\n `available; the file appears to have changed size while it was being read.`,\n 'Make sure the recording is not still being written to, then try again.',\n );\n}\n"]}
|
package/dist/format/csv.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"csv.d.ts","sourceRoot":"","sources":["../../src/format/csv.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAG5C;;;;;;GAMG;AACH,eAAO,MAAM,uBAAuB,QAAU,CAAC;AAI/C;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,QAAQ,WAAW,CAAC;AAEjC,6EAA6E;AAC7E,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAIpD;AAED,wBAAgB,MAAM,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,CAEvD;AAED;;;;;GAKG;AACH,qBAAa,kBAAkB;;
|
|
1
|
+
{"version":3,"file":"csv.d.ts","sourceRoot":"","sources":["../../src/format/csv.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAG5C;;;;;;GAMG;AACH,eAAO,MAAM,uBAAuB,QAAU,CAAC;AAI/C;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,QAAQ,WAAW,CAAC;AAEjC,6EAA6E;AAC7E,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAIpD;AAED,wBAAgB,MAAM,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,CAEvD;AAED;;;;;GAKG;AACH,qBAAa,kBAAkB;;gBAgBjB,MAAM,EAAE,QAAQ,EAAE,SAAS,GAAE,MAAgC;IAuCzE,iFAAiF;IACjF,IAAI,MAAM,IAAI,OAAO,CAEpB;IAED,8EAA8E;IAC9E,IAAI,YAAY,IAAI,MAAM,CAEzB;IAED;;;;;;;OAOG;IACH,IAAI,QAAQ,IAAI,MAAM,CAErB;IAED,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAKxB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAK5B;;;;;OAKG;IACH,IAAI,IAAI,IAAI,OAAO,CAElB;IAED,qEAAqE;IAC/D,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAI3B,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAqE5B,gDAAgD;IAC1C,GAAG,IAAI,OAAO,CAAC,IAAI,CAAC;IAmD1B,oEAAoE;IACpE,OAAO,IAAI,IAAI;CAQhB"}
|
package/dist/format/csv.js
CHANGED
|
@@ -58,13 +58,22 @@ export class BufferedLineWriter {
|
|
|
58
58
|
/** The reader closed the pipe; there is nowhere left to write, but this is not a failure. */
|
|
59
59
|
#hungUp = false;
|
|
60
60
|
#failure = null;
|
|
61
|
+
/** Kept so the listener can come off again; see the constructor. */
|
|
62
|
+
#onError = null;
|
|
61
63
|
constructor(stream, threshold = DEFAULT_FLUSH_THRESHOLD) {
|
|
62
64
|
this.#stream = stream;
|
|
63
65
|
this.#threshold = threshold;
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
66
|
+
/*
|
|
67
|
+
A stream with no 'error' listener throws asynchronously and takes the whole process
|
|
68
|
+
down with a raw stack trace. Capturing the error here lets the next flush surface it as
|
|
69
|
+
a normal failure with a usable message.
|
|
70
|
+
|
|
71
|
+
Removed again when the writer is finished with, because one of these streams outlives
|
|
72
|
+
the writer: `process.stdout`. A library caller converting twelve recordings with
|
|
73
|
+
`toStdout` left twelve listeners on it and got Node's MaxListenersExceededWarning on
|
|
74
|
+
the eleventh — a leak warning that was, for once, describing a real leak.
|
|
75
|
+
*/
|
|
76
|
+
this.#onError = (error) => {
|
|
68
77
|
/*
|
|
69
78
|
EPIPE is the reader hanging up, not a write failure.
|
|
70
79
|
|
|
@@ -79,7 +88,15 @@ export class BufferedLineWriter {
|
|
|
79
88
|
return;
|
|
80
89
|
}
|
|
81
90
|
this.#failure ??= error;
|
|
82
|
-
}
|
|
91
|
+
};
|
|
92
|
+
this.#stream.on('error', this.#onError);
|
|
93
|
+
}
|
|
94
|
+
/** Take the 'error' listener off a stream that outlives this writer. */
|
|
95
|
+
#release() {
|
|
96
|
+
if (!this.#onError)
|
|
97
|
+
return;
|
|
98
|
+
this.#stream.off('error', this.#onError);
|
|
99
|
+
this.#onError = null;
|
|
83
100
|
}
|
|
84
101
|
/** True once the reader closed the pipe. Nothing more can reach the consumer. */
|
|
85
102
|
get hungUp() {
|
|
@@ -209,25 +226,42 @@ export class BufferedLineWriter {
|
|
|
209
226
|
routine shell idiom, a claim about files that were never written, and advice about disk
|
|
210
227
|
space — reappearing on the one path 0.3.1 said would behave identically.
|
|
211
228
|
*/
|
|
212
|
-
if (this.#hungUp)
|
|
229
|
+
if (this.#hungUp) {
|
|
230
|
+
this.#release();
|
|
213
231
|
return;
|
|
232
|
+
}
|
|
214
233
|
// stdout must not be closed; ending it would break piping for the rest of the process.
|
|
215
234
|
// The failure still has to be reported: returning here without looking meant an error
|
|
216
235
|
// recorded on the stream, but not yet surfaced by a later flush, was simply dropped.
|
|
217
236
|
if (this.#stream === process.stdout || this.#stream === process.stderr) {
|
|
218
|
-
|
|
219
|
-
|
|
237
|
+
const failure = this.#failure;
|
|
238
|
+
this.#release();
|
|
239
|
+
if (failure)
|
|
240
|
+
throw failure;
|
|
220
241
|
return;
|
|
221
242
|
}
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
243
|
+
/*
|
|
244
|
+
Released after the stream has finished ending, not before.
|
|
245
|
+
|
|
246
|
+
Releasing first meant an EACCES arriving during that final end had no listener left and
|
|
247
|
+
went out as an unhandled 'error' event — a raw stack trace, which is the one thing this
|
|
248
|
+
listener exists to prevent. The leak it was added to fix is about streams this writer
|
|
249
|
+
does not own; the window it must stay attached for runs to the last byte either way.
|
|
250
|
+
*/
|
|
251
|
+
try {
|
|
252
|
+
await new Promise((resolve, reject) => {
|
|
253
|
+
this.#stream.end((error) => {
|
|
254
|
+
const failure = error ?? this.#failure;
|
|
255
|
+
if (failure)
|
|
256
|
+
reject(failure);
|
|
257
|
+
else
|
|
258
|
+
resolve();
|
|
259
|
+
});
|
|
229
260
|
});
|
|
230
|
-
}
|
|
261
|
+
}
|
|
262
|
+
finally {
|
|
263
|
+
this.#release();
|
|
264
|
+
}
|
|
231
265
|
}
|
|
232
266
|
/** Close the stream without caring whether the data made it out. */
|
|
233
267
|
destroy() {
|
|
@@ -236,6 +270,7 @@ export class BufferedLineWriter {
|
|
|
236
270
|
this.#ended = true;
|
|
237
271
|
this.#parts = [];
|
|
238
272
|
this.#pending = 0;
|
|
273
|
+
this.#release();
|
|
239
274
|
this.#stream.destroy();
|
|
240
275
|
}
|
|
241
276
|
}
|
package/dist/format/csv.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"csv.js","sourceRoot":"","sources":["../../src/format/csv.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * CSV escaping and a buffered, backpressure-aware line writer.\n *\n * Rows are accumulated in memory and flushed in large blocks. Writing row by row\n * would issue millions of small stream writes, and ignoring the return value of\n * `write()` would let Node buffer the entire output in RAM — which for an hour of\n * 23-channel EEG is several hundred megabytes.\n */\n\nimport type { Writable } from 'node:stream';\nimport { once } from 'node:events';\n\n/**\n * Flush once this many characters have accumulated.\n *\n * Exported because it is a budget for the whole conversion rather than a per-writer\n * constant: a mixed-rate recording opens one writer per rate, and each taking a megabyte\n * meant memory followed the number of tables. See writeSignalFiles.\n */\nexport const DEFAULT_FLUSH_THRESHOLD = 1 << 20; // 1 MiB\n\nconst NEEDS_QUOTING = /[\",\\r\\n]/u;\n\n/**\n * U+FEFF, written as EF BB BF, which is what `--bom` prepends to each CSV.\n *\n * Excel on Windows reads a CSV with no byte order mark in the system code page rather than\n * UTF-8, so `µV` — two bytes of UTF-8 for one character — shows up as `µ`. The mark tells\n * it the file is UTF-8.\n *\n * Opt-in because it is not free. pandas strips it either engine (checked against 3.0.5),\n * but Python's own `csv.reader` over a plain `open()` does not, and neither does\n * `fs.readFileSync(path, 'utf8')`: the first column name comes back as `\\ufefftime_s`, and\n * a lookup of `time_s` misses. Readers that want it gone ask for `utf-8-sig`.\n */\nexport const UTF8_BOM = '\\ufeff';\n\n/** Quote a field only when CSV requires it, doubling any embedded quotes. */\nexport function escapeCsvField(value: string): string {\n if (value === '') return '';\n if (!NEEDS_QUOTING.test(value)) return value;\n return `\"${value.replaceAll('\"', '\"\"')}\"`;\n}\n\nexport function csvRow(cells: readonly string[]): string {\n return cells.map(escapeCsvField).join(',');\n}\n\n/**\n * Accumulates text and flushes it to a stream, respecting backpressure.\n *\n * Call `push()` freely, then `await maybeFlush()` at a row boundary. Memory stays\n * bounded by the flush threshold plus whatever the stream has not yet drained.\n */\nexport class BufferedLineWriter {\n #stream: Writable;\n #parts: string[] = [];\n #pending = 0;\n #threshold: number;\n #bytesWritten = 0;\n /** Real bytes handed to the stream, as opposed to characters. See `bytesOut`. */\n #bytesOut = 0;\n #ended = false;\n /** The reader closed the pipe; there is nowhere left to write, but this is not a failure. */\n #hungUp = false;\n #failure: Error | null = null;\n\n constructor(stream: Writable, threshold: number = DEFAULT_FLUSH_THRESHOLD) {\n this.#stream = stream;\n this.#threshold = threshold;\n // A stream with no 'error' listener throws asynchronously and takes the whole\n // process down with a raw stack trace. Capturing the error here lets the next\n // flush surface it as a normal failure with a usable message.\n this.#stream.on('error', (error: NodeJS.ErrnoException) => {\n /*\n EPIPE is the reader hanging up, not a write failure.\n\n `edf2csv recording.edf --stdout | head -1` closes the pipe while the conversion is\n still writing, and treating that as an error produced exit 1 and a message about\n files that do not exist and disk space that is not the problem. A file stream\n cannot raise EPIPE, so this only ever means \"the consumer stopped reading\", which\n is what a shell pipeline does routinely and on purpose.\n */\n if (error.code === 'EPIPE') {\n this.#hungUp = true;\n return;\n }\n this.#failure ??= error;\n });\n }\n\n /** True once the reader closed the pipe. Nothing more can reach the consumer. */\n get hungUp(): boolean {\n return this.#hungUp;\n }\n\n /** Characters written to the stream so far, before any encoding expansion. */\n get charsWritten(): number {\n return this.#bytesWritten;\n }\n\n /**\n * Bytes handed to the stream, counting a multi-byte character once per byte.\n *\n * `charsWritten` drives the progress meter, where the difference does not matter. This is\n * for checking that what was handed over actually arrived, which is a byte question: a\n * channel labelled in Cyrillic makes the two differ by hundreds of bytes on the header\n * line alone.\n */\n get bytesOut(): number {\n return this.#bytesOut;\n }\n\n push(text: string): void {\n this.#parts.push(text);\n this.#pending += text.length;\n }\n\n pushLine(text: string): void {\n this.push(text);\n this.push('\\n');\n }\n\n /**\n * Whether enough has accumulated to be worth writing out.\n *\n * A synchronous question, so a row loop can ask it every row without paying for a\n * microtask on the twenty million that answer no.\n */\n get full(): boolean {\n return this.#pending >= this.#threshold;\n }\n\n /** Flush if enough has accumulated. Await this at row boundaries. */\n async maybeFlush(): Promise<void> {\n if (this.full) await this.flush();\n }\n\n async flush(): Promise<void> {\n if (this.#failure) throw this.#failure;\n /*\n The reader hung up (see the EPIPE note above); there is nowhere left to write.\n\n The buffer is DISCARDED rather than left in place. Returning without clearing it let\n push() keep appending for the rest of the conversion with nothing ever draining it,\n so `--stdout | head -1` on a 165 MB conversion grew to a 1.3 GB working set and died\n with a heap out-of-memory under a 256 MB limit — worse than the error this replaced.\n Bounded memory is the invariant this class exists to hold, and a hung-up reader is no\n reason to abandon it.\n\n Deliberately not `#ended`, which end() sets before its own final flush.\n */\n if (this.#hungUp) {\n this.#parts = [];\n this.#pending = 0;\n return;\n }\n if (this.#parts.length === 0) return;\n\n const chunk = this.#parts.join('');\n this.#parts = [];\n this.#pending = 0;\n this.#bytesWritten += chunk.length;\n this.#bytesOut += Buffer.byteLength(chunk);\n\n if (!this.#stream.write(chunk)) {\n // The consumer is behind; wait rather than letting Node buffer without bound.\n // Waiting only on 'drain' would hang forever if the stream fails instead.\n await this.#drain();\n }\n }\n\n /** Resolve on 'drain', reject if the stream fails first. */\n async #drain(): Promise<void> {\n if (this.#failure) throw this.#failure;\n await new Promise<void>((resolve, reject) => {\n const cleanup = (): void => {\n this.#stream.off('drain', onDrain);\n this.#stream.off('error', onError);\n this.#stream.off('close', onClose);\n };\n const onDrain = (): void => {\n cleanup();\n resolve();\n };\n const onError = (error: NodeJS.ErrnoException): void => {\n cleanup();\n // A pipe closing while we wait for drain is the same benign hang-up as one\n // closing during a write, and has to be handled here too: this listener is\n // registered for the duration of the wait and sees the error first.\n if (error.code === 'EPIPE') {\n this.#hungUp = true;\n resolve();\n return;\n }\n reject(error);\n };\n const onClose = (): void => {\n cleanup();\n reject(this.#failure ?? new Error('The output stream closed before the data was written.'));\n };\n this.#stream.once('drain', onDrain);\n this.#stream.once('error', onError);\n this.#stream.once('close', onClose);\n });\n }\n\n /** Flush anything left and close the stream. */\n async end(): Promise<void> {\n if (this.#ended) return;\n this.#ended = true;\n await this.flush();\n /*\n The reader is gone, so there is nothing to close and possibly nothing left to close it\n on. Under --gzip this stream is the compressor, and the EPIPE forwarded from stdout\n destroyed it — end() on a destroyed stream fails with ERR_STREAM_DESTROYED, which is\n not EPIPE, so it escaped the hang-up guard and came back as a write failure:\n\n edf2csv big.edf --stdout --gzip | head\n error: Writing to stdout failed: Cannot call end after a stream was destroyed\n\n Exactly the three symptoms 0.2.30 removed from the uncompressed path — exit 1 for a\n routine shell idiom, a claim about files that were never written, and advice about disk\n space — reappearing on the one path 0.3.1 said would behave identically.\n */\n if (this.#hungUp) return;\n // stdout must not be closed; ending it would break piping for the rest of the process.\n // The failure still has to be reported: returning here without looking meant an error\n // recorded on the stream, but not yet surfaced by a later flush, was simply dropped.\n if (this.#stream === process.stdout || this.#stream === process.stderr) {\n if (this.#failure) throw this.#failure;\n return;\n }\n await new Promise<void>((resolve, reject) => {\n this.#stream.end((error?: Error | null) => {\n const failure = error ?? this.#failure;\n if (failure) reject(failure);\n else resolve();\n });\n });\n }\n\n /** Close the stream without caring whether the data made it out. */\n destroy(): void {\n if (this.#ended) return;\n this.#ended = true;\n this.#parts = [];\n this.#pending = 0;\n this.#stream.destroy();\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"csv.js","sourceRoot":"","sources":["../../src/format/csv.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * CSV escaping and a buffered, backpressure-aware line writer.\n *\n * Rows are accumulated in memory and flushed in large blocks. Writing row by row\n * would issue millions of small stream writes, and ignoring the return value of\n * `write()` would let Node buffer the entire output in RAM — which for an hour of\n * 23-channel EEG is several hundred megabytes.\n */\n\nimport type { Writable } from 'node:stream';\nimport { once } from 'node:events';\n\n/**\n * Flush once this many characters have accumulated.\n *\n * Exported because it is a budget for the whole conversion rather than a per-writer\n * constant: a mixed-rate recording opens one writer per rate, and each taking a megabyte\n * meant memory followed the number of tables. See writeSignalFiles.\n */\nexport const DEFAULT_FLUSH_THRESHOLD = 1 << 20; // 1 MiB\n\nconst NEEDS_QUOTING = /[\",\\r\\n]/u;\n\n/**\n * U+FEFF, written as EF BB BF, which is what `--bom` prepends to each CSV.\n *\n * Excel on Windows reads a CSV with no byte order mark in the system code page rather than\n * UTF-8, so `µV` — two bytes of UTF-8 for one character — shows up as `µ`. The mark tells\n * it the file is UTF-8.\n *\n * Opt-in because it is not free. pandas strips it either engine (checked against 3.0.5),\n * but Python's own `csv.reader` over a plain `open()` does not, and neither does\n * `fs.readFileSync(path, 'utf8')`: the first column name comes back as `\\ufefftime_s`, and\n * a lookup of `time_s` misses. Readers that want it gone ask for `utf-8-sig`.\n */\nexport const UTF8_BOM = '\\ufeff';\n\n/** Quote a field only when CSV requires it, doubling any embedded quotes. */\nexport function escapeCsvField(value: string): string {\n if (value === '') return '';\n if (!NEEDS_QUOTING.test(value)) return value;\n return `\"${value.replaceAll('\"', '\"\"')}\"`;\n}\n\nexport function csvRow(cells: readonly string[]): string {\n return cells.map(escapeCsvField).join(',');\n}\n\n/**\n * Accumulates text and flushes it to a stream, respecting backpressure.\n *\n * Call `push()` freely, then `await maybeFlush()` at a row boundary. Memory stays\n * bounded by the flush threshold plus whatever the stream has not yet drained.\n */\nexport class BufferedLineWriter {\n #stream: Writable;\n #parts: string[] = [];\n #pending = 0;\n #threshold: number;\n #bytesWritten = 0;\n /** Real bytes handed to the stream, as opposed to characters. See `bytesOut`. */\n #bytesOut = 0;\n #ended = false;\n /** The reader closed the pipe; there is nowhere left to write, but this is not a failure. */\n #hungUp = false;\n #failure: Error | null = null;\n\n /** Kept so the listener can come off again; see the constructor. */\n #onError: ((error: NodeJS.ErrnoException) => void) | null = null;\n\n constructor(stream: Writable, threshold: number = DEFAULT_FLUSH_THRESHOLD) {\n this.#stream = stream;\n this.#threshold = threshold;\n /*\n A stream with no 'error' listener throws asynchronously and takes the whole process\n down with a raw stack trace. Capturing the error here lets the next flush surface it as\n a normal failure with a usable message.\n\n Removed again when the writer is finished with, because one of these streams outlives\n the writer: `process.stdout`. A library caller converting twelve recordings with\n `toStdout` left twelve listeners on it and got Node's MaxListenersExceededWarning on\n the eleventh — a leak warning that was, for once, describing a real leak.\n */\n this.#onError = (error: NodeJS.ErrnoException) => {\n /*\n EPIPE is the reader hanging up, not a write failure.\n\n `edf2csv recording.edf --stdout | head -1` closes the pipe while the conversion is\n still writing, and treating that as an error produced exit 1 and a message about\n files that do not exist and disk space that is not the problem. A file stream\n cannot raise EPIPE, so this only ever means \"the consumer stopped reading\", which\n is what a shell pipeline does routinely and on purpose.\n */\n if (error.code === 'EPIPE') {\n this.#hungUp = true;\n return;\n }\n this.#failure ??= error;\n };\n this.#stream.on('error', this.#onError);\n }\n\n /** Take the 'error' listener off a stream that outlives this writer. */\n #release(): void {\n if (!this.#onError) return;\n this.#stream.off('error', this.#onError);\n this.#onError = null;\n }\n\n /** True once the reader closed the pipe. Nothing more can reach the consumer. */\n get hungUp(): boolean {\n return this.#hungUp;\n }\n\n /** Characters written to the stream so far, before any encoding expansion. */\n get charsWritten(): number {\n return this.#bytesWritten;\n }\n\n /**\n * Bytes handed to the stream, counting a multi-byte character once per byte.\n *\n * `charsWritten` drives the progress meter, where the difference does not matter. This is\n * for checking that what was handed over actually arrived, which is a byte question: a\n * channel labelled in Cyrillic makes the two differ by hundreds of bytes on the header\n * line alone.\n */\n get bytesOut(): number {\n return this.#bytesOut;\n }\n\n push(text: string): void {\n this.#parts.push(text);\n this.#pending += text.length;\n }\n\n pushLine(text: string): void {\n this.push(text);\n this.push('\\n');\n }\n\n /**\n * Whether enough has accumulated to be worth writing out.\n *\n * A synchronous question, so a row loop can ask it every row without paying for a\n * microtask on the twenty million that answer no.\n */\n get full(): boolean {\n return this.#pending >= this.#threshold;\n }\n\n /** Flush if enough has accumulated. Await this at row boundaries. */\n async maybeFlush(): Promise<void> {\n if (this.full) await this.flush();\n }\n\n async flush(): Promise<void> {\n if (this.#failure) throw this.#failure;\n /*\n The reader hung up (see the EPIPE note above); there is nowhere left to write.\n\n The buffer is DISCARDED rather than left in place. Returning without clearing it let\n push() keep appending for the rest of the conversion with nothing ever draining it,\n so `--stdout | head -1` on a 165 MB conversion grew to a 1.3 GB working set and died\n with a heap out-of-memory under a 256 MB limit — worse than the error this replaced.\n Bounded memory is the invariant this class exists to hold, and a hung-up reader is no\n reason to abandon it.\n\n Deliberately not `#ended`, which end() sets before its own final flush.\n */\n if (this.#hungUp) {\n this.#parts = [];\n this.#pending = 0;\n return;\n }\n if (this.#parts.length === 0) return;\n\n const chunk = this.#parts.join('');\n this.#parts = [];\n this.#pending = 0;\n this.#bytesWritten += chunk.length;\n this.#bytesOut += Buffer.byteLength(chunk);\n\n if (!this.#stream.write(chunk)) {\n // The consumer is behind; wait rather than letting Node buffer without bound.\n // Waiting only on 'drain' would hang forever if the stream fails instead.\n await this.#drain();\n }\n }\n\n /** Resolve on 'drain', reject if the stream fails first. */\n async #drain(): Promise<void> {\n if (this.#failure) throw this.#failure;\n await new Promise<void>((resolve, reject) => {\n const cleanup = (): void => {\n this.#stream.off('drain', onDrain);\n this.#stream.off('error', onError);\n this.#stream.off('close', onClose);\n };\n const onDrain = (): void => {\n cleanup();\n resolve();\n };\n const onError = (error: NodeJS.ErrnoException): void => {\n cleanup();\n // A pipe closing while we wait for drain is the same benign hang-up as one\n // closing during a write, and has to be handled here too: this listener is\n // registered for the duration of the wait and sees the error first.\n if (error.code === 'EPIPE') {\n this.#hungUp = true;\n resolve();\n return;\n }\n reject(error);\n };\n const onClose = (): void => {\n cleanup();\n reject(this.#failure ?? new Error('The output stream closed before the data was written.'));\n };\n this.#stream.once('drain', onDrain);\n this.#stream.once('error', onError);\n this.#stream.once('close', onClose);\n });\n }\n\n /** Flush anything left and close the stream. */\n async end(): Promise<void> {\n if (this.#ended) return;\n this.#ended = true;\n await this.flush();\n /*\n The reader is gone, so there is nothing to close and possibly nothing left to close it\n on. Under --gzip this stream is the compressor, and the EPIPE forwarded from stdout\n destroyed it — end() on a destroyed stream fails with ERR_STREAM_DESTROYED, which is\n not EPIPE, so it escaped the hang-up guard and came back as a write failure:\n\n edf2csv big.edf --stdout --gzip | head\n error: Writing to stdout failed: Cannot call end after a stream was destroyed\n\n Exactly the three symptoms 0.2.30 removed from the uncompressed path — exit 1 for a\n routine shell idiom, a claim about files that were never written, and advice about disk\n space — reappearing on the one path 0.3.1 said would behave identically.\n */\n if (this.#hungUp) {\n this.#release();\n return;\n }\n // stdout must not be closed; ending it would break piping for the rest of the process.\n // The failure still has to be reported: returning here without looking meant an error\n // recorded on the stream, but not yet surfaced by a later flush, was simply dropped.\n if (this.#stream === process.stdout || this.#stream === process.stderr) {\n const failure = this.#failure;\n this.#release();\n if (failure) throw failure;\n return;\n }\n /*\n Released after the stream has finished ending, not before.\n\n Releasing first meant an EACCES arriving during that final end had no listener left and\n went out as an unhandled 'error' event — a raw stack trace, which is the one thing this\n listener exists to prevent. The leak it was added to fix is about streams this writer\n does not own; the window it must stay attached for runs to the last byte either way.\n */\n try {\n await new Promise<void>((resolve, reject) => {\n this.#stream.end((error?: Error | null) => {\n const failure = error ?? this.#failure;\n if (failure) reject(failure);\n else resolve();\n });\n });\n } finally {\n this.#release();\n }\n }\n\n /** Close the stream without caring whether the data made it out. */\n destroy(): void {\n if (this.#ended) return;\n this.#ended = true;\n this.#parts = [];\n this.#pending = 0;\n this.#release();\n this.#stream.destroy();\n }\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "edf2csv",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.37",
|
|
4
4
|
"description": "Convert EDF, EDF+ and BDF biosignal recordings (European Data Format) to CSV from the command line. Local, streaming, and never resamples or alters units.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"edf",
|