edf2csv 0.5.44 → 0.5.45
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 +29 -0
- package/dist/format/csv.js +16 -1
- package/dist/format/csv.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,35 @@
|
|
|
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.45
|
|
7
|
+
|
|
8
|
+
### Fixed: 0.5.36 let a full disk take the process down with a raw stack trace
|
|
9
|
+
|
|
10
|
+
The listener that turns a stream failure into a message came off one tick too early. An
|
|
11
|
+
`fs.WriteStream` whose write failed emits `'error'` a second time during its own auto-destroy,
|
|
12
|
+
after `end()`'s callback has settled — and 0.5.36 released the listener at that callback. So a
|
|
13
|
+
destination that filled up during the final flush produced this:
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
node:events:485
|
|
17
|
+
throw er; // Unhandled 'error' event
|
|
18
|
+
^
|
|
19
|
+
Error: ENOSPC: no space left on device, write
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
The process down, no `WRITE_FAILED` line, `signals.csv` truncated with no `channels.csv`
|
|
23
|
+
beside it and nothing saying so, and `convert()` never rejecting — a library caller's
|
|
24
|
+
try/catch bypassed and their process taken with it. Which is, word for word, the failure the
|
|
25
|
+
constructor's comment says this listener exists to prevent.
|
|
26
|
+
|
|
27
|
+
Released only from `process.stdout` and `process.stderr` now. Those are the streams that
|
|
28
|
+
outlive the writer, and the leak 0.5.36 fixed was only ever about them; a stream this writer
|
|
29
|
+
opened is finished with and about to be collected, so a listener left on it leaks nothing.
|
|
30
|
+
|
|
31
|
+
The existing disk-full tests missed it because a conversion that overshoots by a lot fails in
|
|
32
|
+
a mid-stream flush and reports correctly. Leaving a little under the whole output free is what
|
|
33
|
+
puts the failure in the last flush, and that is what the new test does.
|
|
34
|
+
|
|
6
35
|
## 0.5.44
|
|
7
36
|
|
|
8
37
|
### Fixed: `--info` promised nothing for a recording that converts to three files
|
package/dist/format/csv.js
CHANGED
|
@@ -91,10 +91,25 @@ export class BufferedLineWriter {
|
|
|
91
91
|
};
|
|
92
92
|
this.#stream.on('error', this.#onError);
|
|
93
93
|
}
|
|
94
|
-
/**
|
|
94
|
+
/**
|
|
95
|
+
* Take the 'error' listener off a stream that outlives this writer.
|
|
96
|
+
*
|
|
97
|
+
* Only those streams. 0.5.36 released it from every stream once `end()`'s callback had
|
|
98
|
+
* settled, and an `fs.WriteStream` whose write failed emits 'error' again during its own
|
|
99
|
+
* auto-destroy — after that callback. With the listener already gone that reached Node as
|
|
100
|
+
* an unhandled 'error' event: a raw stack trace, the process down, `convert()` never
|
|
101
|
+
* rejecting, and no `WRITE_FAILED` message. Which is the failure this listener exists to
|
|
102
|
+
* prevent, and the one the constructor's comment describes.
|
|
103
|
+
*
|
|
104
|
+
* A stream this writer opened is finished with and about to be collected, so a listener
|
|
105
|
+
* left on it leaks nothing. The leak was only ever about `process.stdout`, which outlives
|
|
106
|
+
* every writer in the process.
|
|
107
|
+
*/
|
|
95
108
|
#release() {
|
|
96
109
|
if (!this.#onError)
|
|
97
110
|
return;
|
|
111
|
+
if (this.#stream !== process.stdout && this.#stream !== process.stderr)
|
|
112
|
+
return;
|
|
98
113
|
this.#stream.off('error', this.#onError);
|
|
99
114
|
this.#onError = null;
|
|
100
115
|
}
|
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 /** 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"]}
|
|
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 /**\n * Take the 'error' listener off a stream that outlives this writer.\n *\n * Only those streams. 0.5.36 released it from every stream once `end()`'s callback had\n * settled, and an `fs.WriteStream` whose write failed emits 'error' again during its own\n * auto-destroy — after that callback. With the listener already gone that reached Node as\n * an unhandled 'error' event: a raw stack trace, the process down, `convert()` never\n * rejecting, and no `WRITE_FAILED` message. Which is the failure this listener exists to\n * prevent, and the one the constructor's comment describes.\n *\n * A stream this writer opened is finished with and about to be collected, so a listener\n * left on it leaks nothing. The leak was only ever about `process.stdout`, which outlives\n * every writer in the process.\n */\n #release(): void {\n if (!this.#onError) return;\n if (this.#stream !== process.stdout && this.#stream !== process.stderr) 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.45",
|
|
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",
|