Skip to content

Commit 76efb2d

Browse files
committed
pass more options to parseUrl, reverse break/continue for safety, handle last bytes
1 parent a64ca52 commit 76efb2d

2 files changed

Lines changed: 66 additions & 31 deletions

File tree

src/chunk.ts

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,16 @@ import { validateQuoteChar } from './quoteChar'
77
import type { ParseResult } from './types'
88
import { testEmptyLine } from './utils'
99

10+
export interface ParseChunkOptions {
11+
delimiter?: string
12+
newline?: string
13+
quoteChar?: string
14+
escapeChar?: string
15+
comments?: boolean | string
16+
delimitersToGuess?: string[]
17+
skipEmptyLines?: boolean | 'greedy'
18+
}
19+
1020
/**
1121
* Parses a chunk of bytes into CSV data.
1222
* @param bytes The chunk of bytes to parse.
@@ -32,14 +42,7 @@ export function* parseChunk(bytes: Uint8Array,
3242
delimitersToGuess,
3343
skipEmptyLines,
3444
ignoreLastRow,
35-
}: {
36-
delimiter?: string
37-
newline?: string
38-
quoteChar?: string
39-
escapeChar?: string
40-
comments?: boolean | string
41-
delimitersToGuess?: string[]
42-
skipEmptyLines?: boolean | 'greedy'
45+
}: ParseChunkOptions & {
4346
ignoreLastRow?: boolean
4447
} = {}): Generator<ParseResult, void, unknown> {
4548
const decoder = new TextDecoder('utf-8')

src/url.ts

Lines changed: 55 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,18 @@
11
import { checkNonNegativeInteger, checkStrictlyPositiveInteger } from './check'
2-
import { parseChunk } from './chunk'
2+
import { parseChunk, type ParseChunkOptions } from './chunk'
33
import { defaultChunkSize } from './constants'
44
import { fetchChunk } from './fetch'
55
import type { ParseResult } from './types'
66

7+
interface ParseUrlOptions extends ParseChunkOptions {
8+
chunkSize?: number
9+
firstByte?: number
10+
lastByte?: number
11+
requestInit?: RequestInit
12+
fetchChunk?: typeof fetchChunk
13+
parseChunk?: typeof parseChunk
14+
}
15+
716
/**
817
* Parses a remote text file in chunks using HTTP range requests.
918
* @param url The URL of the remote text file.
@@ -19,32 +28,25 @@ import type { ParseResult } from './types'
1928
*/
2029
export async function* parseUrl(
2130
url: string,
22-
options?: {
23-
chunkSize?: number
24-
firstByte?: number
25-
lastByte?: number
26-
requestInit?: RequestInit
27-
fetchChunk?: typeof fetchChunk
28-
parseChunk?: typeof parseChunk
29-
},
31+
options: ParseUrlOptions = {},
3032
): AsyncGenerator<ParseResult, void, unknown> {
31-
const chunkSize = checkStrictlyPositiveInteger(options?.chunkSize) ?? defaultChunkSize
33+
const chunkSize = checkStrictlyPositiveInteger(options.chunkSize) ?? defaultChunkSize
3234
// TODO(SL): should we accept negative values (from the end)?
33-
let firstByte = checkNonNegativeInteger(options?.firstByte) ?? 0
34-
let lastByte = checkNonNegativeInteger(options?.lastByte)
35+
let firstByte = checkNonNegativeInteger(options.firstByte) ?? 0
36+
let lastByte = checkNonNegativeInteger(options.lastByte)
3537
if (lastByte !== undefined && lastByte < firstByte) {
3638
throw new Error('lastByte must be greater than firstByte')
3739
}
3840

3941
let cursor = firstByte
4042
let bytes: Uint8Array<ArrayBufferLike> = new Uint8Array(0)
4143
while (true) {
42-
const { bytes: chunkBytes, fileSize } = await (options?.fetchChunk ?? fetchChunk)({
44+
const { bytes: chunkBytes, fileSize } = await (options.fetchChunk ?? fetchChunk)({
4345
url,
4446
chunkSize,
4547
firstByte,
4648
maxLastByte: lastByte,
47-
requestInit: options?.requestInit,
49+
requestInit: options.requestInit,
4850
})
4951

5052
// Update lastByte in case it is undefined or greater than the file size
@@ -71,8 +73,16 @@ export async function* parseUrl(
7173
}
7274

7375
let consumedBytes = 0
74-
for (const result of (options?.parseChunk ?? parseChunk)(bytes, {
76+
for (const result of (options.parseChunk ?? parseChunk)(bytes, {
7577
ignoreLastRow: true, // the remaining bytes may not contain a full last row
78+
// pass other options
79+
delimiter: options.delimiter,
80+
newline: options.newline,
81+
quoteChar: options.quoteChar,
82+
escapeChar: options.escapeChar,
83+
comments: options.comments,
84+
delimitersToGuess: options.delimitersToGuess,
85+
skipEmptyLines: options.skipEmptyLines,
7686
})) {
7787
consumedBytes += result.meta.byteCount
7888
if (consumedBytes > bytes.length) {
@@ -93,16 +103,38 @@ export async function* parseUrl(
93103
cursor += consumedBytes
94104
firstByte += chunkSize
95105

96-
if (firstByte > lastByte) {
97-
break
98-
}
99-
/* v8 ignore if -- @preserve */
100-
if (cursor + bytes.length !== firstByte) {
106+
if (firstByte <= lastByte) {
107+
/* v8 ignore if -- @preserve */
108+
if (cursor + bytes.length !== firstByte) {
101109
// assertion: it should not happen.
102-
throw new Error('Invalid state: non-contiguous offsets')
110+
throw new Error('Invalid state: non-contiguous offsets')
111+
}
112+
continue
103113
}
114+
115+
break
104116
}
105117

106-
// TODO(SL): What to do with remaining bytes? For now, ignore them.
107-
// call again with ignoreLastRow: false for the remaining bytes?
118+
// Parse remaining bytes, if any
119+
if (bytes.length > 0) {
120+
for (const result of (options.parseChunk ?? parseChunk)(bytes, {
121+
ignoreLastRow: false,
122+
// pass other options
123+
delimiter: options.delimiter,
124+
newline: options.newline,
125+
quoteChar: options.quoteChar,
126+
escapeChar: options.escapeChar,
127+
comments: options.comments,
128+
delimitersToGuess: options.delimitersToGuess,
129+
skipEmptyLines: options.skipEmptyLines,
130+
})) {
131+
yield {
132+
...result,
133+
meta: {
134+
...result.meta,
135+
offset: result.meta.offset + cursor,
136+
},
137+
}
138+
}
139+
}
108140
}

0 commit comments

Comments
 (0)