Skip to content

Commit 6a53960

Browse files
committed
feat: update iterator implementation for browser compatibility
BREAKING CHANGE: The stream should be piped through a TextDecoderStream like for example: const textDecoder = new TextDecoderStream(); for await (const entry of iterator(file.stream().pipeThrough(textDecoder)))
1 parent ab74e7e commit 6a53960

File tree

6 files changed

+80
-45
lines changed

6 files changed

+80
-45
lines changed

README.md

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -59,13 +59,19 @@ var result = parse(sdf, {
5959

6060
## Iterator
6161

62-
This API is only available on Node.js.
63-
6462
```js
6563
const { iterator } = require('sdf-parser');
66-
const readStream = createReadStream(join(__dirname, 'test.sdf.gz'));
67-
const stream = readStream.pipe(createGunzip());
64+
const file = await openAsBlob(join(__dirname, 'test.sdf.gz'));
65+
66+
const decompressionStream = new DecompressionStream('gzip');
67+
const textDecoder = new TextDecoderStream();
68+
69+
const stream = file
70+
.stream()
71+
.pipeThrough(decompressionStream)
72+
.pipeThrough(textDecoder);
6873
const results = [];
74+
6975
for await (const entry of iterator(stream)) {
7076
results.push(entry);
7177
}

package.json

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,6 @@
88
"lib",
99
"src"
1010
],
11-
"browser": {
12-
"./src/iterator.js": "./src/iterator.browser.js"
13-
},
1411
"sideEffects": false,
1512
"scripts": {
1613
"build": "cheminfo-build --entry src/index.js --root SDFParser",
@@ -45,6 +42,7 @@
4542
"devDependencies": {
4643
"@babel/plugin-transform-modules-commonjs": "^7.25.9",
4744
"@types/jest": "^29.5.14",
45+
"@types/node": "^22.8.6",
4846
"@vitest/coverage-v8": "^2.1.4",
4947
"babel-eslint": "^10.1.0",
5048
"callback-stream": "^1.1.0",

src/__tests__/iterator.test.js

Lines changed: 27 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
1-
import { createReadStream, ReadStream } from 'fs';
1+
import { openAsBlob } from 'fs';
22
import { join } from 'path';
3-
import { createGunzip } from 'zlib';
43

54
import { fileCollectionFromPath } from 'filelist-utils';
65
import { test, expect } from 'vitest';
@@ -13,12 +12,15 @@ test('iterator', async () => {
1312
).files.filter((file) => file.name === 'test.sdf');
1413
const results = [];
1514

16-
if (parseInt(process.versions.node, 10) >= 18) {
17-
for await (const entry of iterator(ReadStream.fromWeb(files[0].stream()))) {
18-
results.push(entry);
19-
}
20-
expect(results).toHaveLength(128);
21-
expect(results[0]).toMatchInlineSnapshot(`
15+
const textDecoder = new TextDecoderStream();
16+
for await (const entry of iterator(
17+
files[0].stream().pipeThrough(textDecoder),
18+
)) {
19+
results.push(entry);
20+
}
21+
22+
expect(results).toHaveLength(128);
23+
expect(results[0]).toMatchInlineSnapshot(`
2224
{
2325
"CLogP": 2.7,
2426
"Code": 100380824,
@@ -64,16 +66,24 @@ test('iterator', async () => {
6466
",
6567
}
6668
`);
67-
}
6869
});
6970

7071
test('iterator on stream', async () => {
71-
const readStream = createReadStream(join(__dirname, 'test.sdf.gz'));
72-
const stream = readStream.pipe(createGunzip());
72+
const file = await openAsBlob(join(__dirname, 'test.sdf.gz'));
73+
74+
const decompressionStream = new DecompressionStream('gzip');
75+
const textDecoder = new TextDecoderStream();
76+
77+
const stream = file
78+
.stream()
79+
.pipeThrough(decompressionStream)
80+
.pipeThrough(textDecoder);
7381
const results = [];
82+
7483
for await (const entry of iterator(stream)) {
7584
results.push(entry);
7685
}
86+
7787
expect(results).toHaveLength(128);
7888
expect(results[0]).toMatchInlineSnapshot(`
7989
{
@@ -129,12 +139,12 @@ test('iterator on fileCollection stream', async () => {
129139
).files[0];
130140
const results = [];
131141

132-
if (parseInt(process.versions.node, 10) >= 18) {
133-
for await (const entry of iterator(ReadStream.fromWeb(file.stream()))) {
134-
results.push(entry);
135-
}
136-
expect(results).toHaveLength(128);
137-
expect(results[0]).toMatchInlineSnapshot(`
142+
const textDecoder = new TextDecoderStream();
143+
for await (const entry of iterator(file.stream().pipeThrough(textDecoder))) {
144+
results.push(entry);
145+
}
146+
expect(results).toHaveLength(128);
147+
expect(results[0]).toMatchInlineSnapshot(`
138148
{
139149
"CLogP": 2.7,
140150
"Code": 100380824,
@@ -180,5 +190,4 @@ test('iterator on fileCollection stream', async () => {
180190
",
181191
}
182192
`);
183-
}
184193
});

src/iterator.browser.js

Lines changed: 0 additions & 3 deletions
This file was deleted.

src/iterator.js

Lines changed: 42 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,26 @@
1-
import { createInterface } from 'readline';
2-
31
import { parseString } from 'dynamic-typing';
2+
43
/**
54
* Parse a SDF file
6-
* @param {NodeJS.ReadableStream} readStream SDF file to parse
5+
* @param {ReadableStream} readStream SDF file to parse
76
* @param {object} [options={}]
87
* @param {Function} [options.filter] Callback allowing to filter the molecules
8+
* @param {string} [options.eol='\n'] End of line character
99
* @param {boolean} [options.dynamicTyping] Dynamically type the data
1010
*/
11-
1211
export async function* iterator(readStream, options = {}) {
13-
const lines = createInterface(readStream);
12+
const { eol = '\n', dynamicTyping = true } = options;
13+
14+
const lineStream = readStream.pipeThrough(createLineStream());
1415
const currentLines = [];
15-
options = { ...options };
1616
if (options.dynamicTyping === undefined) options.dynamicTyping = true;
1717

18-
options.eol = '\n';
19-
for await (let line of lines) {
18+
for await (let line of lineStream) {
2019
if (line.startsWith('$$$$')) {
21-
const molecule = getMolecule(currentLines.join(options.eol), options);
20+
const molecule = getMolecule(currentLines.join(eol), {
21+
eol,
22+
dynamicTyping,
23+
});
2224
if (!options.filter || options.filter(molecule)) {
2325
yield molecule;
2426
}
@@ -29,26 +31,52 @@ export async function* iterator(readStream, options = {}) {
2931
}
3032
}
3133

34+
/**
35+
* Convert a SDF part to an object
36+
* @param {string} sdfPart
37+
* @param {object} options
38+
* @param {string} options.eol
39+
* @param {boolean} options.dynamicTyping
40+
* @returns
41+
*/
3242
function getMolecule(sdfPart, options) {
33-
let parts = sdfPart.split(`${options.eol}>`);
43+
const { eol, dynamicTyping } = options;
44+
let parts = sdfPart.split(`${eol}>`);
3445
if (parts.length === 0 || parts[0].length <= 5) return;
3546
let molecule = {};
36-
molecule.molfile = parts[0] + options.eol;
47+
molecule.molfile = parts[0] + eol;
3748
for (let j = 1; j < parts.length; j++) {
38-
let lines = parts[j].split(options.eol);
49+
let lines = parts[j].split(eol);
3950
let from = lines[0].indexOf('<');
4051
let to = lines[0].indexOf('>');
4152
let label = lines[0].substring(from + 1, to);
4253
for (let k = 1; k < lines.length - 1; k++) {
4354
if (molecule[label]) {
44-
molecule[label] += options.eol + lines[k];
55+
molecule[label] += eol + lines[k];
4556
} else {
4657
molecule[label] = lines[k];
4758
}
4859
}
49-
if (options.dynamicTyping) {
60+
if (dynamicTyping) {
5061
molecule[label] = parseString(molecule[label]);
5162
}
5263
}
5364
return molecule;
5465
}
66+
67+
function createLineStream() {
68+
let buffer = '';
69+
return new TransformStream({
70+
async transform(chunk, controller) {
71+
buffer += chunk;
72+
let lines = buffer.split('\n');
73+
for (let i = 0; i < lines.length - 1; i++) {
74+
controller.enqueue(lines[i]);
75+
}
76+
buffer = lines[lines.length - 1];
77+
},
78+
flush(controller) {
79+
if (buffer) controller.enqueue(buffer);
80+
},
81+
});
82+
}

src/stream.browser.js

Lines changed: 0 additions & 3 deletions
This file was deleted.

0 commit comments

Comments
 (0)