Skip to content

Commit 1ee9b51

Browse files
authored
Simplify API (#22)
1 parent 841060e commit 1ee9b51

12 files changed

Lines changed: 1201 additions & 1099 deletions

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,4 @@ out.ixx
66
out2.ix
77
out2.ixx
88
esm
9+
.eslintcache

eslint.config.mjs

Lines changed: 64 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,71 @@
1-
import typescriptEslint from '@typescript-eslint/eslint-plugin'
2-
import tsParser from '@typescript-eslint/parser'
3-
import path from 'node:path'
4-
import { fileURLToPath } from 'node:url'
5-
import js from '@eslint/js'
6-
import { FlatCompat } from '@eslint/eslintrc'
1+
import eslint from '@eslint/js'
2+
import { defineConfig } from 'eslint/config'
3+
import eslintPluginUnicorn from 'eslint-plugin-unicorn'
4+
import tseslint from 'typescript-eslint'
75

8-
const __filename = fileURLToPath(import.meta.url)
9-
const __dirname = path.dirname(__filename)
10-
const compat = new FlatCompat({
11-
baseDirectory: __dirname,
12-
recommendedConfig: js.configs.recommended,
13-
allConfig: js.configs.all,
14-
})
15-
16-
export default [
17-
...compat.extends(
18-
'eslint:recommended',
19-
'plugin:@typescript-eslint/recommended',
20-
'plugin:@typescript-eslint/recommended-type-checked',
21-
'plugin:@typescript-eslint/stylistic-type-checked',
22-
),
6+
export default defineConfig(
7+
{
8+
ignores: ['dist/*', 'esm/*', 'eslint.config.mjs'],
9+
},
2310
{
24-
plugins: {
25-
'@typescript-eslint': typescriptEslint,
26-
},
27-
2811
languageOptions: {
29-
parser: tsParser,
30-
ecmaVersion: 5,
31-
sourceType: 'script',
32-
3312
parserOptions: {
34-
project: './tsconfig.lint.json',
13+
project: ['./tsconfig.lint.json'],
14+
tsconfigRootDir: import.meta.dirname,
3515
},
3616
},
3717
},
38-
]
18+
eslint.configs.recommended,
19+
...tseslint.configs.recommended,
20+
...tseslint.configs.stylisticTypeChecked,
21+
...tseslint.configs.strictTypeChecked,
22+
eslintPluginUnicorn.configs.recommended,
23+
{
24+
rules: {
25+
'no-empty': 'off',
26+
'no-console': [
27+
'warn',
28+
{
29+
allow: ['error', 'warn'],
30+
},
31+
],
32+
curly: 'error',
33+
semi: ['error', 'never'],
34+
35+
'@typescript-eslint/ban-ts-comment': 'off',
36+
'@typescript-eslint/no-explicit-any': 'off',
37+
'@typescript-eslint/no-non-null-assertion': 'off',
38+
'@typescript-eslint/no-unsafe-member-access': 'off',
39+
'@typescript-eslint/no-unsafe-argument': 'off',
40+
'@typescript-eslint/no-unsafe-assignment': 'off',
41+
'@typescript-eslint/no-unsafe-call': 'off',
42+
'@typescript-eslint/no-unsafe-return': 'off',
43+
'@typescript-eslint/require-await': 'off',
44+
'@typescript-eslint/restrict-template-expressions': 'off',
45+
46+
'@typescript-eslint/no-unused-vars': [
47+
'warn',
48+
{
49+
argsIgnorePattern: '^_',
50+
ignoreRestSiblings: true,
51+
caughtErrors: 'none',
52+
},
53+
],
54+
55+
'unicorn/filename-case': 'off',
56+
'unicorn/prevent-abbreviations': 'off',
57+
'unicorn/no-null': 'off',
58+
'unicorn/no-process-exit': 'off',
59+
'unicorn/prefer-module': 'off',
60+
'unicorn/prefer-top-level-await': 'off',
61+
'unicorn/no-array-for-each': 'off',
62+
'unicorn/no-for-loop': 'off',
63+
'unicorn/prefer-spread': 'off',
64+
'unicorn/consistent-function-scoping': 'off',
65+
'unicorn/prefer-node-protocol': 'off',
66+
'unicorn/no-nested-ternary': 'off',
67+
'unicorn/no-useless-undefined': 'off',
68+
'unicorn/prefer-ternary': 'off',
69+
},
70+
},
71+
)

package.json

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,19 +11,18 @@
1111
],
1212
"license": "MIT",
1313
"devDependencies": {
14-
"@eslint/eslintrc": "^3.1.0",
1514
"@eslint/js": "^9.8.0",
1615
"@types/command-exists": "^1.2.1",
1716
"@types/node": "^20.5.9",
1817
"@types/split2": "^4.2.0",
1918
"@types/tmp": "^0.2.3",
20-
"@typescript-eslint/eslint-plugin": "^8.0.1",
21-
"@typescript-eslint/parser": "^8.0.1",
2219
"eslint": "^9.8.0",
20+
"eslint-plugin-unicorn": "^62.0.0",
2321
"prettier": "^3.0.3",
2422
"rimraf": "^6.0.1",
2523
"typescript": "^5.5.4",
26-
"vitest": "^3.0.1"
24+
"typescript-eslint": "^8.0.1",
25+
"vitest": "^4.0.15"
2726
},
2827
"scripts": {
2928
"lint": "eslint --report-unused-disable-directives --max-warnings 0",

src/TrixInputTransform.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,14 @@ import { Transform } from 'stream'
22

33
export class TrixInputTransform extends Transform {
44
_transform(chunk: Buffer, _encoding: unknown, done: () => void) {
5-
const [id, ...words] = chunk.toString().split(/\s+/)
6-
7-
this.push(words.map(word => `${word.toLowerCase()} ${id}\n`).join(''))
5+
const line = chunk.toString()
6+
const parts = line.split(/\s+/)
7+
const id = parts[0]
8+
let result = ''
9+
for (let i = 1; i < parts.length; i++) {
10+
result += `${parts[i].toLowerCase()} ${id}\n`
11+
}
12+
this.push(result)
813
done()
914
}
1015
}

src/TrixOutputTransform.ts

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,27 @@
11
import { Transform } from 'stream'
22

33
function elt(buff: string[], current: string) {
4-
return `${current} ${buff.map((elt, idx) => `${elt},${idx + 1}`).join(' ')}\n`
4+
let result = current
5+
for (let i = 0; i < buff.length; i++) {
6+
result += ` ${buff[i]},${i + 1}`
7+
}
8+
return result + '\n'
59
}
10+
611
export class TrixOutputTransform extends Transform {
7-
buff = [] as string[]
12+
buff: string[] = []
813
current = ''
14+
915
_transform(chunk: Buffer, _encoding: unknown, done: () => void) {
1016
// weird: need to strip nulls from string, xref
1117
// https://github.com/GMOD/jbrowse-components/pull/2451
12-
const [id, data] = chunk.toString().replace(/\0/g, '').split(' ')
18+
const line = chunk.toString().replaceAll('\0', '')
19+
const spaceIdx = line.indexOf(' ')
20+
const id = spaceIdx === -1 ? line : line.slice(0, spaceIdx)
21+
const data = spaceIdx === -1 ? '' : line.slice(spaceIdx + 1)
22+
1323
if (this.current !== id) {
14-
if (this.buff.length) {
24+
if (this.buff.length > 0) {
1525
this.push(elt(this.buff, this.current))
1626
this.buff = []
1727
}
@@ -20,8 +30,9 @@ export class TrixOutputTransform extends Transform {
2030
this.buff.push(data)
2131
done()
2232
}
33+
2334
_flush(done: () => void) {
24-
if (this.buff.length) {
35+
if (this.buff.length > 0) {
2536
this.push(elt(this.buff, this.current))
2637
}
2738
done()

src/bin.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,15 @@ import { ixIxx } from './index'
44
const [file, out1 = 'out.ix', out2 = 'out.ixx'] = process.argv.slice(2)
55

66
if (!file) {
7+
// eslint-disable-next-line no-console
78
console.log('usage: ixixx file.txt [out.ix] [out.ixx]')
89
process.exit()
910
}
1011
// eslint-disable-next-line @typescript-eslint/no-floating-promises
1112
;(async () => {
1213
try {
1314
await ixIxx(file, out1, out2)
14-
} catch (e) {
15-
console.error(e)
15+
} catch (error) {
16+
console.error(error)
1617
}
1718
})()

src/makeIx.ts

Lines changed: 52 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -1,106 +1,73 @@
1-
import { pipeline, Readable } from 'stream'
2-
import esort from 'external-sorting'
3-
import tmp from 'tmp'
1+
import { pipeline } from 'stream/promises'
2+
import { Readable } from 'stream'
43
import { sync as commandExistsSync } from 'command-exists'
5-
64
import split2 from 'split2'
75
import fs from 'fs'
86
import { spawn } from 'child_process'
97
import { TrixInputTransform } from './TrixInputTransform'
108
import { TrixOutputTransform } from './TrixOutputTransform'
9+
import { sortLinesExternal } from './sortLines'
1110

12-
// Characters that may be part of a word
13-
const wordMiddleChars = [] as boolean[]
14-
const wordBeginChars = [] as boolean[]
11+
const isWin =
12+
typeof process === 'undefined' ? false : process.platform === 'win32'
1513

16-
function isalpha(c: string) {
17-
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')
18-
}
14+
const useExternalSort = !isWin && commandExistsSync('sort')
1915

20-
function isdigit(c: string) {
21-
return c >= '0' && c <= '9'
22-
}
16+
async function makeIxWithExternalSort(
17+
fileStream: Readable,
18+
outIxFilename: string,
19+
) {
20+
const out = fs.createWriteStream(outIxFilename)
21+
const sort = spawn('sort', ['-k1,1'], {
22+
env: { ...process.env, LC_ALL: 'C' },
23+
})
2324

24-
function isalnum(c: string) {
25-
return isalpha(c) || isdigit(c)
26-
}
25+
sort.on('error', function onSortError(err) {
26+
throw err
27+
})
2728

28-
function initCharTables() {
29-
for (let c = 0; c < 256; ++c) {
30-
if (isalnum(String.fromCharCode(c))) {
31-
wordBeginChars[c] = true
32-
wordMiddleChars[c] = true
33-
}
34-
}
35-
wordBeginChars['_'.charCodeAt(0)] = wordMiddleChars['_'.charCodeAt(0)] = true
36-
wordMiddleChars['.'.charCodeAt(0)] = true
37-
wordMiddleChars['-'.charCodeAt(0)] = true
38-
}
29+
const inputDone = pipeline(
30+
fileStream,
31+
split2(),
32+
new TrixInputTransform(),
33+
sort.stdin,
34+
)
3935

40-
const isWin =
41-
typeof process !== 'undefined' ? process.platform === 'win32' : false
36+
const outputDone = pipeline(
37+
sort.stdout,
38+
split2(),
39+
new TrixOutputTransform(),
40+
out,
41+
)
4242

43-
export async function makeIxStream(
44-
fileStream: Readable,
45-
outIxFilename: string,
46-
) {
47-
return new Promise((resolve, reject) => {
48-
initCharTables()
43+
await Promise.all([inputDone, outputDone])
44+
}
4945

50-
const out = fs.createWriteStream(outIxFilename)
46+
async function makeIxWithJsSort(fileStream: Readable, outIxFilename: string) {
47+
const out = fs.createWriteStream(outIxFilename)
5148

52-
// see https://stackoverflow.com/questions/68835344/ for explainer of
53-
// writer
49+
// Transform input
50+
const transformedInput = fileStream.pipe(split2()).pipe(new TrixInputTransform())
5451

55-
// override locale to C, but keep other env vars
56-
if (commandExistsSync('sort') && !isWin) {
57-
const sort = spawn('sort', ['-k1,1'], {
58-
env: { ...process.env, LC_ALL: 'C' },
59-
})
60-
pipeline(
61-
fileStream,
62-
split2(),
63-
new TrixInputTransform(),
64-
sort.stdin,
65-
err => {
66-
if (err) {
67-
reject(err)
68-
}
69-
},
70-
)
52+
// Sort lines using external merge sort
53+
const sortedOutput = split2()
54+
const sortDone = sortLinesExternal(transformedInput, sortedOutput)
7155

72-
pipeline(sort.stdout, split2(), new TrixOutputTransform(), out, err => {
73-
if (err) {
74-
reject(err)
75-
} else {
76-
resolve(true)
77-
}
78-
})
79-
} else {
80-
const dir = tmp.dirSync({
81-
prefix: 'jbrowse-trix-sort',
82-
})
83-
const tempDir = dir.name
84-
const output = split2()
56+
// Transform sorted output and write to file
57+
const writeDone = pipeline(sortedOutput, new TrixOutputTransform(), out)
8558

86-
pipeline(output, new TrixOutputTransform(), out, err => {
87-
if (err) {
88-
reject(err)
89-
}
90-
})
91-
esort({
92-
input: pipeline(fileStream, split2(), new TrixInputTransform(), err => {
93-
if (err) {
94-
reject(err)
95-
}
96-
}),
97-
output,
98-
tempDir,
99-
})
100-
.asc()
101-
.then(resolve, reject)
102-
}
103-
})
59+
await Promise.all([sortDone, writeDone])
60+
}
61+
62+
export async function makeIxStream(
63+
fileStream: Readable,
64+
outIxFilename: string,
65+
) {
66+
if (useExternalSort) {
67+
await makeIxWithExternalSort(fileStream, outIxFilename)
68+
} else {
69+
await makeIxWithJsSort(fileStream, outIxFilename)
70+
}
10471
}
10572

10673
export async function makeIx(inFile: string, outIndex: string) {

0 commit comments

Comments
 (0)