edfcore 0.3.54 → 0.3.55

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -6,6 +6,29 @@ alone does not tell you whether you were affected.
6
6
  edfcore is pre-1.0. Patch releases have carried behaviour changes where the old behaviour was a
7
7
  defect; those are called out below.
8
8
 
9
+ ## 0.3.55
10
+
11
+ - **Fixed** `streamRecords` never comparing record onsets across a chunk boundary, so an
12
+ always-fatal `TIMELINE_NOT_MONOTONIC` was suppressed and chunks were yielded in reverse time
13
+ order — and whether it fired at all depended on `chunkRecords`.
14
+ - `readRecords` runs `assertMonotonicOnsetArray` over the onsets of the chunk it just read, so
15
+ every adjacent pair **inside** a chunk is checked and no pair that straddles two is.
16
+ `readWindow` hands a whole contiguous run to one call, so it checks all of them; splitting the
17
+ same run into `chunkRecords`-sized reads checked none of the seams.
18
+ - On an eight-record file whose only backwards pair is 3 → 4, `readWindow` threw and
19
+ `streamRecords` threw at `chunkRecords` 3 and 5 while returning the data at 1, 2, 4 and 256.
20
+ At `chunkRecords: 1` **every** pair is a seam, so nothing was checked at all and the chunks came
21
+ back at 0, 1, 2, 4, 3, 5, 6, 7 seconds.
22
+ - `chunkRecords` is documented as "the unit of I/O and of memory". A performance knob must never
23
+ decide whether a file is refused — the fourth time that shape has been swept out of this
24
+ package.
25
+ - A consumer that places each chunk at its own `startSeconds`, which is what the docs prescribe,
26
+ silently overwrote earlier trace with later samples.
27
+ - The seam check costs no extra read. A chunk's span is `lastOnset + recordDuration - firstOnset`,
28
+ so its last record's onset is `startTicks + durationTicks - recordDurationTicks`, already on the
29
+ same rebased axis as the next chunk's `startTicks`. It is reset per run, because `readWindow` does
30
+ not compare across a gap either and a streamed chunk must stay the object a read would give.
31
+
9
32
  ## 0.3.54
10
33
 
11
34
  - **Fixed** `decodeStatusWord` reading the BioSemi quality flags from the wrong bits. Both
@@ -109,5 +109,5 @@ export declare const SIGNAL_FIELD_BLOCK_OFFSETS: {
109
109
  readonly reserved: 224;
110
110
  };
111
111
  /** Published package version. Kept in sync with package.json by a test. */
112
- export declare const VERSION = "0.3.54";
112
+ export declare const VERSION = "0.3.55";
113
113
  //# sourceMappingURL=constants.d.ts.map
package/dist/constants.js CHANGED
@@ -79,5 +79,5 @@ export const SIGNAL_FIELD_BLOCK_OFFSETS = {
79
79
  reserved: 224,
80
80
  };
81
81
  /** Published package version. Kept in sync with package.json by a test. */
82
- export const VERSION = '0.3.54';
82
+ export const VERSION = '0.3.55';
83
83
  //# sourceMappingURL=constants.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"stream.d.ts","sourceRoot":"","sources":["../src/stream.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAIH,OAAO,KAAK,EAAE,QAAQ,EAAE,YAAY,EAAE,WAAW,EAAe,eAAe,EAAE,MAAM,YAAY,CAAC;AAKpG;;;;;;;GAOG;AACH,wBAAuB,aAAa,CAClC,SAAS,EAAE,YAAY,EACvB,SAAS,EAAE,eAAe,EAC1B,OAAO,CAAC,EAAE,WAAW,GACpB,cAAc,CAAC,QAAQ,EAAE,IAAI,EAAE,SAAS,CAAC,CAyC3C"}
1
+ {"version":3,"file":"stream.d.ts","sourceRoot":"","sources":["../src/stream.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAKH,OAAO,KAAK,EAAE,QAAQ,EAAE,YAAY,EAAE,WAAW,EAAe,eAAe,EAAE,MAAM,YAAY,CAAC;AAKpG;;;;;;;GAOG;AACH,wBAAuB,aAAa,CAClC,SAAS,EAAE,YAAY,EACvB,SAAS,EAAE,eAAe,EAC1B,OAAO,CAAC,EAAE,WAAW,GACpB,cAAc,CAAC,QAAQ,EAAE,IAAI,EAAE,SAAS,CAAC,CAgF3C"}
package/dist/stream.js CHANGED
@@ -13,6 +13,7 @@
13
13
  * `readWindow` chunk would, so a consumer sees the discontinuities rather than a smooth lie.
14
14
  */
15
15
  import { readRecords, resolveSignals } from './recording.js';
16
+ import { assertMonotonicOnsets } from './time/timeline.js';
16
17
  import { resolveTimeWindow } from './time/window.js';
17
18
  /** How many records to read at a time when the caller does not say. */
18
19
  const DEFAULT_STREAM_RECORDS = 256;
@@ -38,8 +39,12 @@ export async function* streamRecords(recording, selection, options) {
38
39
  // Every other selection error in the package surfaces on the spot; this one waited for data.
39
40
  resolveSignals(recording.header, selection.signalIndices);
40
41
  const ranges = resolveTimeWindow(recording.timeline, recording.index, selection.startSeconds, selection.durationSeconds);
42
+ const recordDurationTicks = recording.header.recordDurationTicks;
41
43
  for (const run of ranges) {
42
44
  let scanned = 0;
45
+ // The last record of the previous chunk of THIS run. Reset per run, because `readWindow` does
46
+ // not compare across a gap either and a streamed chunk must be the same object it would get.
47
+ let previousRecord;
43
48
  while (scanned < run.count) {
44
49
  const records = {
45
50
  start: run.start + scanned,
@@ -47,7 +52,38 @@ export async function* streamRecords(recording, selection, options) {
47
52
  };
48
53
  // readRecords, not a private path: the chunk a consumer gets from streaming and the chunk
49
54
  // it gets from reading must be the same object in every respect, including its diagnostics.
50
- yield await readRecords(recording, { signalIndices: selection.signalIndices, records }, options);
55
+ const chunk = await readRecords(recording, { signalIndices: selection.signalIndices, records }, options);
56
+ /*
57
+ * The one pair `readRecords` structurally cannot see: the SEAM between two chunks.
58
+ *
59
+ * It runs `assertMonotonicOnsetArray` over the onsets of the chunk it just read, so every
60
+ * adjacent pair inside a chunk is compared and no pair that straddles two is. `readWindow`
61
+ * hands a whole contiguous run to one call and therefore checks all of them; splitting the
62
+ * same run into `chunkRecords`-sized reads checked none of the seams.
63
+ *
64
+ * TIMELINE_NOT_MONOTONIC is ALWAYS fatal — `readWindow` on the identical window throws —
65
+ * and the streaming path returned the data instead, in reverse time order: a consumer that
66
+ * places each chunk at its own `startSeconds`, which is what the docs prescribe, overwrote
67
+ * earlier trace with later samples. Worse, whether it fired at all depended on
68
+ * `chunkRecords`: on one eight-record file it threw at 3 and 5 and stayed silent at 1, 2, 4
69
+ * and 256, and `chunkRecords` is documented as a memory knob. A performance parameter must
70
+ * never decide whether a file is refused (fixed in 0.3.55).
71
+ *
72
+ * No extra read: a chunk's span is `lastOnset + recordDuration - firstOnset`, so its last
73
+ * record's onset is `startTicks + durationTicks - recordDurationTicks`, on the same rebased
74
+ * axis as the next chunk's `startTicks`.
75
+ */
76
+ if (previousRecord !== undefined) {
77
+ assertMonotonicOnsets(previousRecord, {
78
+ recordIndex: records.start,
79
+ onsetTicks: chunk.startTicks,
80
+ });
81
+ }
82
+ previousRecord = {
83
+ recordIndex: records.start + records.count - 1,
84
+ onsetTicks: chunk.startTicks + chunk.durationTicks - recordDurationTicks,
85
+ };
86
+ yield chunk;
51
87
  scanned += records.count;
52
88
  }
53
89
  }
@@ -1 +1 @@
1
- {"version":3,"file":"stream.js","sourceRoot":"","sources":["../src/stream.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAC7D,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAGrD,uEAAuE;AACvE,MAAM,sBAAsB,GAAG,GAAG,CAAC;AAEnC;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,SAAS,CAAC,CAAC,aAAa,CAClC,SAAuB,EACvB,SAA0B,EAC1B,OAAqB;IAErB,MAAM,YAAY,GAAG,SAAS,CAAC,YAAY,IAAI,sBAAsB,CAAC;IACtE,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,YAAY,CAAC,IAAI,YAAY,GAAG,CAAC,EAAE,CAAC;QAC5D,MAAM,IAAI,UAAU,CAClB,2EAA2E,YAAY,IAAI;YACzF,mFAAmF,CACtF,CAAC;IACJ,CAAC;IAED,+FAA+F;IAC/F,+FAA+F;IAC/F,2FAA2F;IAC3F,sFAAsF;IACtF,6FAA6F;IAC7F,6FAA6F;IAC7F,cAAc,CAAC,SAAS,CAAC,MAAM,EAAE,SAAS,CAAC,aAAa,CAAC,CAAC;IAE1D,MAAM,MAAM,GAAG,iBAAiB,CAC9B,SAAS,CAAC,QAAQ,EAClB,SAAS,CAAC,KAAK,EACf,SAAS,CAAC,YAAY,EACtB,SAAS,CAAC,eAAe,CAC1B,CAAC;IAEF,KAAK,MAAM,GAAG,IAAI,MAAM,EAAE,CAAC;QACzB,IAAI,OAAO,GAAG,CAAC,CAAC;QAChB,OAAO,OAAO,GAAG,GAAG,CAAC,KAAK,EAAE,CAAC;YAC3B,MAAM,OAAO,GAAgB;gBAC3B,KAAK,EAAE,GAAG,CAAC,KAAK,GAAG,OAAO;gBAC1B,KAAK,EAAE,IAAI,CAAC,GAAG,CAAC,YAAY,EAAE,GAAG,CAAC,KAAK,GAAG,OAAO,CAAC;aACnD,CAAC;YACF,0FAA0F;YAC1F,4FAA4F;YAC5F,MAAM,MAAM,WAAW,CACrB,SAAS,EACT,EAAE,aAAa,EAAE,SAAS,CAAC,aAAa,EAAE,OAAO,EAAE,EACnD,OAAO,CACR,CAAC;YACF,OAAO,IAAI,OAAO,CAAC,KAAK,CAAC;QAC3B,CAAC;IACH,CAAC;AACH,CAAC"}
1
+ {"version":3,"file":"stream.js","sourceRoot":"","sources":["../src/stream.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAC7D,OAAO,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAC;AAC3D,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAGrD,uEAAuE;AACvE,MAAM,sBAAsB,GAAG,GAAG,CAAC;AAEnC;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,SAAS,CAAC,CAAC,aAAa,CAClC,SAAuB,EACvB,SAA0B,EAC1B,OAAqB;IAErB,MAAM,YAAY,GAAG,SAAS,CAAC,YAAY,IAAI,sBAAsB,CAAC;IACtE,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,YAAY,CAAC,IAAI,YAAY,GAAG,CAAC,EAAE,CAAC;QAC5D,MAAM,IAAI,UAAU,CAClB,2EAA2E,YAAY,IAAI;YACzF,mFAAmF,CACtF,CAAC;IACJ,CAAC;IAED,+FAA+F;IAC/F,+FAA+F;IAC/F,2FAA2F;IAC3F,sFAAsF;IACtF,6FAA6F;IAC7F,6FAA6F;IAC7F,cAAc,CAAC,SAAS,CAAC,MAAM,EAAE,SAAS,CAAC,aAAa,CAAC,CAAC;IAE1D,MAAM,MAAM,GAAG,iBAAiB,CAC9B,SAAS,CAAC,QAAQ,EAClB,SAAS,CAAC,KAAK,EACf,SAAS,CAAC,YAAY,EACtB,SAAS,CAAC,eAAe,CAC1B,CAAC;IAEF,MAAM,mBAAmB,GAAG,SAAS,CAAC,MAAM,CAAC,mBAAmB,CAAC;IAEjE,KAAK,MAAM,GAAG,IAAI,MAAM,EAAE,CAAC;QACzB,IAAI,OAAO,GAAG,CAAC,CAAC;QAChB,8FAA8F;QAC9F,6FAA6F;QAC7F,IAAI,cAAuE,CAAC;QAE5E,OAAO,OAAO,GAAG,GAAG,CAAC,KAAK,EAAE,CAAC;YAC3B,MAAM,OAAO,GAAgB;gBAC3B,KAAK,EAAE,GAAG,CAAC,KAAK,GAAG,OAAO;gBAC1B,KAAK,EAAE,IAAI,CAAC,GAAG,CAAC,YAAY,EAAE,GAAG,CAAC,KAAK,GAAG,OAAO,CAAC;aACnD,CAAC;YACF,0FAA0F;YAC1F,4FAA4F;YAC5F,MAAM,KAAK,GAAG,MAAM,WAAW,CAC7B,SAAS,EACT,EAAE,aAAa,EAAE,SAAS,CAAC,aAAa,EAAE,OAAO,EAAE,EACnD,OAAO,CACR,CAAC;YAEF;;;;;;;;;;;;;;;;;;;eAmBG;YACH,IAAI,cAAc,KAAK,SAAS,EAAE,CAAC;gBACjC,qBAAqB,CAAC,cAAc,EAAE;oBACpC,WAAW,EAAE,OAAO,CAAC,KAAK;oBAC1B,UAAU,EAAE,KAAK,CAAC,UAAU;iBAC7B,CAAC,CAAC;YACL,CAAC;YACD,cAAc,GAAG;gBACf,WAAW,EAAE,OAAO,CAAC,KAAK,GAAG,OAAO,CAAC,KAAK,GAAG,CAAC;gBAC9C,UAAU,EAAE,KAAK,CAAC,UAAU,GAAG,KAAK,CAAC,aAAa,GAAG,mBAAmB;aACzE,CAAC;YAEF,MAAM,KAAK,CAAC;YACZ,OAAO,IAAI,OAAO,CAAC,KAAK,CAAC;QAC3B,CAAC;IACH,CAAC;AACH,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "edfcore",
3
- "version": "0.3.54",
3
+ "version": "0.3.55",
4
4
  "description": "Modern, typed, zero-dependency reader for EDF, EDF+, BDF and BDF+ biosignal files. Works in browsers and Node with true random access.",
5
5
  "keywords": [
6
6
  "edf",
package/src/constants.ts CHANGED
@@ -93,4 +93,4 @@ export const SIGNAL_FIELD_BLOCK_OFFSETS = {
93
93
  } as const;
94
94
 
95
95
  /** Published package version. Kept in sync with package.json by a test. */
96
- export const VERSION = '0.3.54';
96
+ export const VERSION = '0.3.55';
package/src/stream.ts CHANGED
@@ -14,6 +14,7 @@
14
14
  */
15
15
 
16
16
  import { readRecords, resolveSignals } from './recording.js';
17
+ import { assertMonotonicOnsets } from './time/timeline.js';
17
18
  import { resolveTimeWindow } from './time/window.js';
18
19
  import type { EdfChunk, EdfRecording, ReadOptions, RecordRange, StreamSelection } from './types.js';
19
20
 
@@ -56,8 +57,14 @@ export async function* streamRecords(
56
57
  selection.durationSeconds,
57
58
  );
58
59
 
60
+ const recordDurationTicks = recording.header.recordDurationTicks;
61
+
59
62
  for (const run of ranges) {
60
63
  let scanned = 0;
64
+ // The last record of the previous chunk of THIS run. Reset per run, because `readWindow` does
65
+ // not compare across a gap either and a streamed chunk must be the same object it would get.
66
+ let previousRecord: { recordIndex: number; onsetTicks: bigint } | undefined;
67
+
61
68
  while (scanned < run.count) {
62
69
  const records: RecordRange = {
63
70
  start: run.start + scanned,
@@ -65,11 +72,44 @@ export async function* streamRecords(
65
72
  };
66
73
  // readRecords, not a private path: the chunk a consumer gets from streaming and the chunk
67
74
  // it gets from reading must be the same object in every respect, including its diagnostics.
68
- yield await readRecords(
75
+ const chunk = await readRecords(
69
76
  recording,
70
77
  { signalIndices: selection.signalIndices, records },
71
78
  options,
72
79
  );
80
+
81
+ /*
82
+ * The one pair `readRecords` structurally cannot see: the SEAM between two chunks.
83
+ *
84
+ * It runs `assertMonotonicOnsetArray` over the onsets of the chunk it just read, so every
85
+ * adjacent pair inside a chunk is compared and no pair that straddles two is. `readWindow`
86
+ * hands a whole contiguous run to one call and therefore checks all of them; splitting the
87
+ * same run into `chunkRecords`-sized reads checked none of the seams.
88
+ *
89
+ * TIMELINE_NOT_MONOTONIC is ALWAYS fatal — `readWindow` on the identical window throws —
90
+ * and the streaming path returned the data instead, in reverse time order: a consumer that
91
+ * places each chunk at its own `startSeconds`, which is what the docs prescribe, overwrote
92
+ * earlier trace with later samples. Worse, whether it fired at all depended on
93
+ * `chunkRecords`: on one eight-record file it threw at 3 and 5 and stayed silent at 1, 2, 4
94
+ * and 256, and `chunkRecords` is documented as a memory knob. A performance parameter must
95
+ * never decide whether a file is refused (fixed in 0.3.55).
96
+ *
97
+ * No extra read: a chunk's span is `lastOnset + recordDuration - firstOnset`, so its last
98
+ * record's onset is `startTicks + durationTicks - recordDurationTicks`, on the same rebased
99
+ * axis as the next chunk's `startTicks`.
100
+ */
101
+ if (previousRecord !== undefined) {
102
+ assertMonotonicOnsets(previousRecord, {
103
+ recordIndex: records.start,
104
+ onsetTicks: chunk.startTicks,
105
+ });
106
+ }
107
+ previousRecord = {
108
+ recordIndex: records.start + records.count - 1,
109
+ onsetTicks: chunk.startTicks + chunk.durationTicks - recordDurationTicks,
110
+ };
111
+
112
+ yield chunk;
73
113
  scanned += records.count;
74
114
  }
75
115
  }