strtok3 10.3.4 → 10.4.0

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.
@@ -0,0 +1,2 @@
1
+ /** Reject an already-cancelled operation before acquiring tokenizer resources. */
2
+ export declare function throwIfAborted(signal?: AbortSignal): void;
@@ -0,0 +1,7 @@
1
+ import { AbortError } from './stream/Errors.js';
2
+ /** Reject an already-cancelled operation before acquiring tokenizer resources. */
3
+ export function throwIfAborted(signal) {
4
+ if (signal?.aborted) {
5
+ throw new AbortError();
6
+ }
7
+ }
@@ -1,5 +1,5 @@
1
- import type { ITokenizer, IFileInfo, IReadChunkOptions, ITokenizerOptions } from './types.js';
2
1
  import type { IGetToken, IToken } from '@tokenizer/token';
2
+ import type { IFileInfo, IReadChunkOptions, ITokenizer, ITokenizerOptions } from './types.js';
3
3
  interface INormalizedReadChunkOptions extends IReadChunkOptions {
4
4
  length: number;
5
5
  position: number;
@@ -10,7 +10,10 @@ interface INormalizedReadChunkOptions extends IReadChunkOptions {
10
10
  */
11
11
  export declare abstract class AbstractTokenizer implements ITokenizer {
12
12
  private onClose?;
13
+ private abortSignal?;
14
+ private abortListener?;
13
15
  private numBuffer;
16
+ protected readonly maxBufferSize: number;
14
17
  abstract fileInfo: IFileInfo;
15
18
  /**
16
19
  * Tokenizer-stream position
@@ -22,6 +25,8 @@ export declare abstract class AbstractTokenizer implements ITokenizer {
22
25
  * @protected
23
26
  */
24
27
  protected constructor(options?: ITokenizerOptions);
28
+ private addAbortListener;
29
+ protected removeAbortListener(): void;
25
30
  abstract supportsRandomAccess(): boolean;
26
31
  /**
27
32
  * Read buffer from tokenizer
@@ -50,7 +55,7 @@ export declare abstract class AbstractTokenizer implements ITokenizer {
50
55
  * @param position - Offset where to begin reading within the file. If position is null, data will be read from the current file position.
51
56
  * @returns Promise with token data
52
57
  */
53
- peekToken<Value>(token: IGetToken<Value>, position?: number): Promise<Value>;
58
+ peekToken<Value>(token: IGetToken<Value>, position?: number | null): Promise<Value>;
54
59
  /**
55
60
  * Read a numeric token from the stream
56
61
  * @param token - Numeric token
@@ -65,11 +70,44 @@ export declare abstract class AbstractTokenizer implements ITokenizer {
65
70
  peekNumber(token: IToken<number>): Promise<number>;
66
71
  /**
67
72
  * Ignore number of bytes, advances the pointer in under tokenizer-stream.
68
- * @param length - Number of bytes to ignore
73
+ * @param length - Number of bytes to ignore. Must be ≥ 0.
69
74
  * @return resolves the number of bytes ignored, equals length if this available, otherwise the number of bytes available
70
75
  */
71
76
  ignore(length: number): Promise<number>;
72
77
  close(): Promise<void>;
78
+ /**
79
+ * Validate a byte length or position before using it in buffer calculations.
80
+ * @param value - Value that must be a non-negative safe integer
81
+ * @param name - Label used in the validation error
82
+ * @throws RangeError if the value is negative or not a safe integer
83
+ */
84
+ protected validateLength(value: number, name: string): void;
85
+ /**
86
+ * Validate a requested buffer size against the configured allocation limit.
87
+ * @param length - Requested buffer size in bytes
88
+ * @throws RangeError if the length is negative or not a safe integer
89
+ * @throws BufferSizeError if the length exceeds maxBufferSize
90
+ */
91
+ protected validateBufferSize(length: number): void;
92
+ /**
93
+ * Validate a token request before allocating a buffer or consuming input.
94
+ * @param length - Token length in bytes
95
+ * @param position - Byte offset where the token starts
96
+ * @param peek - Whether to include skipped stream bytes in the lookahead limit
97
+ * @throws RangeError if a length or position is negative or not a safe integer
98
+ * @throws BufferSizeError if the token buffer or stream lookahead exceeds maxBufferSize
99
+ * @throws Error if a stream request starts before the current position
100
+ * @throws EndOfStreamError if the token extends beyond the known input size
101
+ */
102
+ private validateTokenRead;
103
+ /**
104
+ * Apply defaults and validate buffer read or peek options.
105
+ * @param uint8Array - Target buffer whose length is the default read length
106
+ * @param options - Requested length, position and partial-read behavior
107
+ * @returns Validated options with an explicit length, position and mayBeLess flag
108
+ * @throws RangeError if a length or position is invalid, or the length exceeds the target buffer
109
+ * @throws Error if a stream request starts before the current position
110
+ */
73
111
  protected normalizeOptions(uint8Array: Uint8Array, options?: IReadChunkOptions): INormalizedReadChunkOptions;
74
112
  abort(): Promise<void>;
75
113
  }
@@ -1,3 +1,5 @@
1
+ import { throwIfAborted } from './AbortSignal.js';
2
+ import { BufferSizeError, getMaxBufferSize } from './BufferSizeError.js';
1
3
  import { EndOfStreamError } from './stream/index.js';
2
4
  /**
3
5
  * Core tokenizer
@@ -14,13 +16,29 @@ export class AbstractTokenizer {
14
16
  * Tokenizer-stream position
15
17
  */
16
18
  this.position = 0;
19
+ this.maxBufferSize = getMaxBufferSize(options);
20
+ throwIfAborted(options?.abortSignal);
17
21
  this.onClose = options?.onClose;
18
22
  if (options?.abortSignal) {
19
- options.abortSignal.addEventListener('abort', () => {
20
- this.abort();
23
+ this.addAbortListener(options.abortSignal, () => {
24
+ void this.abort().catch(() => {
25
+ // Signal events cannot propagate cancellation errors; explicit abort/close calls still reject.
26
+ });
21
27
  });
22
28
  }
23
29
  }
30
+ addAbortListener(signal, listener) {
31
+ this.abortSignal = signal;
32
+ this.abortListener = listener;
33
+ signal.addEventListener('abort', listener, { once: true });
34
+ }
35
+ removeAbortListener() {
36
+ if (this.abortListener) {
37
+ this.abortSignal?.removeEventListener('abort', this.abortListener);
38
+ }
39
+ this.abortSignal = undefined;
40
+ this.abortListener = undefined;
41
+ }
24
42
  /**
25
43
  * Read a token from the tokenizer-stream
26
44
  * @param token - The token to read
@@ -28,10 +46,12 @@ export class AbstractTokenizer {
28
46
  * @returns Promise with token data
29
47
  */
30
48
  async readToken(token, position = this.position) {
49
+ this.validateTokenRead(token.len, position);
31
50
  const uint8Array = new Uint8Array(token.len);
32
51
  const len = await this.readBuffer(uint8Array, { position });
33
- if (len < token.len)
52
+ if (len < token.len) {
34
53
  throw new EndOfStreamError();
54
+ }
35
55
  return token.get(uint8Array, 0);
36
56
  }
37
57
  /**
@@ -41,10 +61,13 @@ export class AbstractTokenizer {
41
61
  * @returns Promise with token data
42
62
  */
43
63
  async peekToken(token, position = this.position) {
64
+ const peekPosition = position ?? this.position;
65
+ this.validateTokenRead(token.len, peekPosition, true);
44
66
  const uint8Array = new Uint8Array(token.len);
45
- const len = await this.peekBuffer(uint8Array, { position });
46
- if (len < token.len)
67
+ const len = await this.peekBuffer(uint8Array, { position: peekPosition });
68
+ if (len < token.len) {
47
69
  throw new EndOfStreamError();
70
+ }
48
71
  return token.get(uint8Array, 0);
49
72
  }
50
73
  /**
@@ -54,8 +77,9 @@ export class AbstractTokenizer {
54
77
  */
55
78
  async readNumber(token) {
56
79
  const len = await this.readBuffer(this.numBuffer, { length: token.len });
57
- if (len < token.len)
80
+ if (len < token.len) {
58
81
  throw new EndOfStreamError();
82
+ }
59
83
  return token.get(this.numBuffer, 0);
60
84
  }
61
85
  /**
@@ -65,16 +89,22 @@ export class AbstractTokenizer {
65
89
  */
66
90
  async peekNumber(token) {
67
91
  const len = await this.peekBuffer(this.numBuffer, { length: token.len });
68
- if (len < token.len)
92
+ if (len < token.len) {
69
93
  throw new EndOfStreamError();
94
+ }
70
95
  return token.get(this.numBuffer, 0);
71
96
  }
72
97
  /**
73
98
  * Ignore number of bytes, advances the pointer in under tokenizer-stream.
74
- * @param length - Number of bytes to ignore
99
+ * @param length - Number of bytes to ignore. Must be ≥ 0.
75
100
  * @return resolves the number of bytes ignored, equals length if this available, otherwise the number of bytes available
76
101
  */
77
102
  async ignore(length) {
103
+ if (length < 0) {
104
+ throw new RangeError('ignore length must be ≥ 0 bytes');
105
+ }
106
+ this.validateLength(length, 'ignore length');
107
+ this.validateLength(this.position + length, 'end position');
78
108
  if (this.fileInfo.size !== undefined) {
79
109
  const bytesLeft = this.fileInfo.size - this.position;
80
110
  if (length > bytesLeft) {
@@ -86,23 +116,81 @@ export class AbstractTokenizer {
86
116
  return length;
87
117
  }
88
118
  async close() {
119
+ this.removeAbortListener();
89
120
  await this.abort();
90
121
  await this.onClose?.();
91
122
  }
123
+ /**
124
+ * Validate a byte length or position before using it in buffer calculations.
125
+ * @param value - Value that must be a non-negative safe integer
126
+ * @param name - Label used in the validation error
127
+ * @throws RangeError if the value is negative or not a safe integer
128
+ */
129
+ validateLength(value, name) {
130
+ if (!Number.isSafeInteger(value) || value < 0) {
131
+ throw new RangeError(`${name} must be a non-negative safe integer`);
132
+ }
133
+ }
134
+ /**
135
+ * Validate a requested buffer size against the configured allocation limit.
136
+ * @param length - Requested buffer size in bytes
137
+ * @throws RangeError if the length is negative or not a safe integer
138
+ * @throws BufferSizeError if the length exceeds maxBufferSize
139
+ */
140
+ validateBufferSize(length) {
141
+ this.validateLength(length, 'buffer length');
142
+ if (length > this.maxBufferSize) {
143
+ throw new BufferSizeError(length, this.maxBufferSize);
144
+ }
145
+ }
146
+ /**
147
+ * Validate a token request before allocating a buffer or consuming input.
148
+ * @param length - Token length in bytes
149
+ * @param position - Byte offset where the token starts
150
+ * @param peek - Whether to include skipped stream bytes in the lookahead limit
151
+ * @throws RangeError if a length or position is negative or not a safe integer
152
+ * @throws BufferSizeError if the token buffer or stream lookahead exceeds maxBufferSize
153
+ * @throws Error if a stream request starts before the current position
154
+ * @throws EndOfStreamError if the token extends beyond the known input size
155
+ */
156
+ validateTokenRead(length, position, peek = false) {
157
+ this.validateBufferSize(length);
158
+ this.validateLength(position, 'position');
159
+ this.validateLength(position + length, 'end position');
160
+ if (!this.supportsRandomAccess() && position < this.position) {
161
+ throw new Error('`options.position` must be equal or greater than `tokenizer.position`');
162
+ }
163
+ if (peek && !this.supportsRandomAccess()) {
164
+ this.validateBufferSize(length + position - this.position);
165
+ }
166
+ if (this.fileInfo.size !== undefined && length > this.fileInfo.size - position) {
167
+ throw new EndOfStreamError();
168
+ }
169
+ }
170
+ /**
171
+ * Apply defaults and validate buffer read or peek options.
172
+ * @param uint8Array - Target buffer whose length is the default read length
173
+ * @param options - Requested length, position and partial-read behavior
174
+ * @returns Validated options with an explicit length, position and mayBeLess flag
175
+ * @throws RangeError if a length or position is invalid, or the length exceeds the target buffer
176
+ * @throws Error if a stream request starts before the current position
177
+ */
92
178
  normalizeOptions(uint8Array, options) {
93
- if (!this.supportsRandomAccess() && options && options.position !== undefined && options.position < this.position) {
179
+ const length = options?.length ?? uint8Array.length;
180
+ const position = options?.position ?? this.position;
181
+ this.validateLength(length, 'length');
182
+ this.validateLength(position, 'position');
183
+ this.validateLength(position + length, 'end position');
184
+ if (length > uint8Array.length) {
185
+ throw new RangeError('length must not exceed the target buffer length');
186
+ }
187
+ if (!this.supportsRandomAccess() && position < this.position) {
94
188
  throw new Error('`options.position` must be equal or greater than `tokenizer.position`');
95
189
  }
96
- return {
97
- ...{
98
- mayBeLess: false,
99
- offset: 0,
100
- length: uint8Array.length,
101
- position: this.position
102
- }, ...options
103
- };
190
+ return { length, position, mayBeLess: options?.mayBeLess ?? false };
104
191
  }
105
192
  abort() {
193
+ this.removeAbortListener();
106
194
  return Promise.resolve(); // Ignore abort signal
107
195
  }
108
196
  }
@@ -1,5 +1,5 @@
1
- import type { ITokenizerOptions, IReadChunkOptions, IRandomAccessFileInfo, IRandomAccessTokenizer } from './types.js';
2
1
  import { AbstractTokenizer } from './AbstractTokenizer.js';
2
+ import type { IRandomAccessFileInfo, IRandomAccessTokenizer, IReadChunkOptions, ITokenizerOptions } from './types.js';
3
3
  export declare class BlobTokenizer extends AbstractTokenizer implements IRandomAccessTokenizer {
4
4
  private blob;
5
5
  fileInfo: IRandomAccessFileInfo;
@@ -1,5 +1,5 @@
1
- import { EndOfStreamError } from './stream/index.js';
2
1
  import { AbstractTokenizer } from './AbstractTokenizer.js';
2
+ import { EndOfStreamError } from './stream/index.js';
3
3
  export class BlobTokenizer extends AbstractTokenizer {
4
4
  /**
5
5
  * Construct BufferTokenizer
@@ -9,7 +9,7 @@ export class BlobTokenizer extends AbstractTokenizer {
9
9
  constructor(blob, options) {
10
10
  super(options);
11
11
  this.blob = blob;
12
- this.fileInfo = { ...options?.fileInfo ?? {}, ...{ size: blob.size, mimeType: blob.type } };
12
+ this.fileInfo = { ...(options?.fileInfo ?? {}), ...{ size: blob.size, mimeType: blob.type } };
13
13
  }
14
14
  /**
15
15
  * Read buffer from tokenizer
@@ -18,11 +18,9 @@ export class BlobTokenizer extends AbstractTokenizer {
18
18
  * @returns {Promise<number>}
19
19
  */
20
20
  async readBuffer(uint8Array, options) {
21
- if (options?.position) {
22
- this.position = options.position;
23
- }
24
- const bytesRead = await this.peekBuffer(uint8Array, options);
25
- this.position += bytesRead;
21
+ const normOptions = this.normalizeOptions(uint8Array, options);
22
+ const bytesRead = await this.peekBuffer(uint8Array, normOptions);
23
+ this.position = normOptions.position + bytesRead;
26
24
  return bytesRead;
27
25
  }
28
26
  /**
@@ -33,12 +31,17 @@ export class BlobTokenizer extends AbstractTokenizer {
33
31
  */
34
32
  async peekBuffer(buffer, options) {
35
33
  const normOptions = this.normalizeOptions(buffer, options);
36
- const bytes2read = Math.min(this.blob.size - normOptions.position, normOptions.length);
37
- if ((!normOptions.mayBeLess) && bytes2read < normOptions.length) {
34
+ const bytes2read = Math.max(0, Math.min(this.blob.size - normOptions.position, normOptions.length));
35
+ if (!normOptions.mayBeLess && bytes2read < normOptions.length) {
38
36
  throw new EndOfStreamError();
39
37
  }
40
- const arrayBuffer = await this.blob.slice(normOptions.position, normOptions.position + bytes2read).arrayBuffer();
41
- buffer.set(new Uint8Array(arrayBuffer));
38
+ for (let offset = 0; offset < bytes2read;) {
39
+ const chunkLength = Math.min(this.maxBufferSize, bytes2read - offset);
40
+ const position = normOptions.position + offset;
41
+ const arrayBuffer = await this.blob.slice(position, position + chunkLength).arrayBuffer();
42
+ buffer.set(new Uint8Array(arrayBuffer), offset);
43
+ offset += chunkLength;
44
+ }
42
45
  return bytes2read;
43
46
  }
44
47
  close() {
@@ -0,0 +1,19 @@
1
+ import type { ITokenizerOptions } from './types.js';
2
+ /** Thrown before a dynamically sized tokenizer buffer exceeds its configured limit. */
3
+ export declare class BufferSizeError extends RangeError {
4
+ readonly requestedSize: number;
5
+ readonly maxBufferSize: number;
6
+ /**
7
+ * Create an error describing an allocation that exceeds the configured limit.
8
+ * @param requestedSize - Requested buffer size in bytes
9
+ * @param maxBufferSize - Configured maximum buffer size in bytes
10
+ */
11
+ constructor(requestedSize: number, maxBufferSize: number);
12
+ }
13
+ /**
14
+ * Validate options before acquiring file handles or stream readers.
15
+ * @param options - Tokenizer options containing the optional allocation limit
16
+ * @returns Configured buffer limit in bytes, or Infinity when the limit is disabled
17
+ * @throws RangeError if maxBufferSize is neither a positive safe integer nor Infinity
18
+ */
19
+ export declare function getMaxBufferSize(options?: ITokenizerOptions): number;
@@ -0,0 +1,27 @@
1
+ /** Thrown before a dynamically sized tokenizer buffer exceeds its configured limit. */
2
+ export class BufferSizeError extends RangeError {
3
+ /**
4
+ * Create an error describing an allocation that exceeds the configured limit.
5
+ * @param requestedSize - Requested buffer size in bytes
6
+ * @param maxBufferSize - Configured maximum buffer size in bytes
7
+ */
8
+ constructor(requestedSize, maxBufferSize) {
9
+ super(`Requested buffer size ${requestedSize} exceeds maxBufferSize ${maxBufferSize}`);
10
+ this.requestedSize = requestedSize;
11
+ this.maxBufferSize = maxBufferSize;
12
+ this.name = 'BufferSizeError';
13
+ }
14
+ }
15
+ /**
16
+ * Validate options before acquiring file handles or stream readers.
17
+ * @param options - Tokenizer options containing the optional allocation limit
18
+ * @returns Configured buffer limit in bytes, or Infinity when the limit is disabled
19
+ * @throws RangeError if maxBufferSize is neither a positive safe integer nor Infinity
20
+ */
21
+ export function getMaxBufferSize(options) {
22
+ const limit = options?.maxBufferSize ?? Number.POSITIVE_INFINITY;
23
+ if (limit !== Number.POSITIVE_INFINITY && (!Number.isSafeInteger(limit) || limit <= 0)) {
24
+ throw new RangeError('maxBufferSize must be a positive safe integer or Infinity');
25
+ }
26
+ return limit;
27
+ }
@@ -1,5 +1,5 @@
1
- import type { ITokenizerOptions, IReadChunkOptions, IRandomAccessFileInfo, IRandomAccessTokenizer } from './types.js';
2
1
  import { AbstractTokenizer } from './AbstractTokenizer.js';
2
+ import type { IRandomAccessFileInfo, IRandomAccessTokenizer, IReadChunkOptions, ITokenizerOptions } from './types.js';
3
3
  export declare class BufferTokenizer extends AbstractTokenizer implements IRandomAccessTokenizer {
4
4
  private uint8Array;
5
5
  fileInfo: IRandomAccessFileInfo;
@@ -1,5 +1,5 @@
1
- import { EndOfStreamError } from './stream/index.js';
2
1
  import { AbstractTokenizer } from './AbstractTokenizer.js';
2
+ import { EndOfStreamError } from './stream/index.js';
3
3
  export class BufferTokenizer extends AbstractTokenizer {
4
4
  /**
5
5
  * Construct BufferTokenizer
@@ -9,7 +9,7 @@ export class BufferTokenizer extends AbstractTokenizer {
9
9
  constructor(uint8Array, options) {
10
10
  super(options);
11
11
  this.uint8Array = uint8Array;
12
- this.fileInfo = { ...options?.fileInfo ?? {}, ...{ size: uint8Array.length } };
12
+ this.fileInfo = { ...(options?.fileInfo ?? {}), ...{ size: uint8Array.length } };
13
13
  }
14
14
  /**
15
15
  * Read buffer from tokenizer
@@ -18,11 +18,9 @@ export class BufferTokenizer extends AbstractTokenizer {
18
18
  * @returns {Promise<number>}
19
19
  */
20
20
  async readBuffer(uint8Array, options) {
21
- if (options?.position) {
22
- this.position = options.position;
23
- }
24
- const bytesRead = await this.peekBuffer(uint8Array, options);
25
- this.position += bytesRead;
21
+ const normOptions = this.normalizeOptions(uint8Array, options);
22
+ const bytesRead = await this.peekBuffer(uint8Array, normOptions);
23
+ this.position = normOptions.position + bytesRead;
26
24
  return bytesRead;
27
25
  }
28
26
  /**
@@ -34,7 +32,7 @@ export class BufferTokenizer extends AbstractTokenizer {
34
32
  async peekBuffer(uint8Array, options) {
35
33
  const normOptions = this.normalizeOptions(uint8Array, options);
36
34
  const bytes2read = Math.min(this.uint8Array.length - normOptions.position, normOptions.length);
37
- if ((!normOptions.mayBeLess) && bytes2read < normOptions.length) {
35
+ if (!normOptions.mayBeLess && bytes2read < normOptions.length) {
38
36
  throw new EndOfStreamError();
39
37
  }
40
38
  uint8Array.set(this.uint8Array.subarray(normOptions.position, normOptions.position + bytes2read));
@@ -1,6 +1,6 @@
1
- import { AbstractTokenizer } from './AbstractTokenizer.js';
2
- import type { IRandomAccessTokenizer, IRandomAccessFileInfo, IReadChunkOptions, ITokenizerOptions } from './types.js';
3
1
  import { type FileHandle } from 'node:fs/promises';
2
+ import { AbstractTokenizer } from './AbstractTokenizer.js';
3
+ import type { IRandomAccessFileInfo, IRandomAccessTokenizer, IReadChunkOptions, ITokenizerOptions } from './types.js';
4
4
  interface IFileTokenizerOptions extends ITokenizerOptions {
5
5
  /**
6
6
  * Pass additional file information to the tokenizer
@@ -13,8 +13,9 @@ export declare class FileTokenizer extends AbstractTokenizer implements IRandomA
13
13
  /**
14
14
  * Create tokenizer from provided file path
15
15
  * @param sourceFilePath File path
16
+ * @param options Tokenizer options
16
17
  */
17
- static fromFile(sourceFilePath: string): Promise<FileTokenizer>;
18
+ static fromFile(sourceFilePath: string, options?: ITokenizerOptions): Promise<FileTokenizer>;
18
19
  protected constructor(fileHandle: FileHandle, options: IFileTokenizerOptions);
19
20
  /**
20
21
  * Read buffer from file
@@ -1,15 +1,29 @@
1
+ import { open as fsOpen } from 'node:fs/promises';
2
+ import { throwIfAborted } from './AbortSignal.js';
1
3
  import { AbstractTokenizer } from './AbstractTokenizer.js';
4
+ import { getMaxBufferSize } from './BufferSizeError.js';
2
5
  import { EndOfStreamError } from './stream/index.js';
3
- import { open as fsOpen } from 'node:fs/promises';
4
6
  export class FileTokenizer extends AbstractTokenizer {
5
7
  /**
6
8
  * Create tokenizer from provided file path
7
9
  * @param sourceFilePath File path
10
+ * @param options Tokenizer options
8
11
  */
9
- static async fromFile(sourceFilePath) {
12
+ static async fromFile(sourceFilePath, options) {
13
+ getMaxBufferSize(options);
14
+ throwIfAborted(options?.abortSignal);
10
15
  const fileHandle = await fsOpen(sourceFilePath, 'r');
11
- const stat = await fileHandle.stat();
12
- return new FileTokenizer(fileHandle, { fileInfo: { path: sourceFilePath, size: stat.size } });
16
+ try {
17
+ const stat = await fileHandle.stat();
18
+ return new FileTokenizer(fileHandle, {
19
+ ...options,
20
+ fileInfo: { ...options?.fileInfo, path: sourceFilePath, size: stat.size }
21
+ });
22
+ }
23
+ catch (error) {
24
+ await fileHandle.close();
25
+ throw error;
26
+ }
13
27
  }
14
28
  constructor(fileHandle, options) {
15
29
  super(options);
@@ -25,8 +39,9 @@ export class FileTokenizer extends AbstractTokenizer {
25
39
  async readBuffer(uint8Array, options) {
26
40
  const normOptions = this.normalizeOptions(uint8Array, options);
27
41
  this.position = normOptions.position;
28
- if (normOptions.length === 0)
42
+ if (normOptions.length === 0) {
29
43
  return 0;
44
+ }
30
45
  const res = await this.fileHandle.read(uint8Array, 0, normOptions.length, normOptions.position);
31
46
  this.position += res.bytesRead;
32
47
  if (res.bytesRead < normOptions.length && (!options || !options.mayBeLess)) {
@@ -43,12 +58,13 @@ export class FileTokenizer extends AbstractTokenizer {
43
58
  async peekBuffer(uint8Array, options) {
44
59
  const normOptions = this.normalizeOptions(uint8Array, options);
45
60
  const res = await this.fileHandle.read(uint8Array, 0, normOptions.length, normOptions.position);
46
- if ((!normOptions.mayBeLess) && res.bytesRead < normOptions.length) {
61
+ if (!normOptions.mayBeLess && res.bytesRead < normOptions.length) {
47
62
  throw new EndOfStreamError();
48
63
  }
49
64
  return res.bytesRead;
50
65
  }
51
66
  async close() {
67
+ this.removeAbortListener();
52
68
  await this.fileHandle.close();
53
69
  return super.close();
54
70
  }
@@ -24,6 +24,9 @@ export declare class ReadStreamTokenizer extends AbstractTokenizer {
24
24
  * @returns Promise with number of bytes peeked
25
25
  */
26
26
  peekBuffer(uint8Array: Uint8Array, options?: IReadChunkOptions): Promise<number>;
27
+ /**
28
+ * @param length Number of bytes to ignore. Must be ≥ 0.
29
+ */
27
30
  ignore(length: number): Promise<number>;
28
31
  abort(): Promise<void>;
29
32
  close(): Promise<void>;
@@ -1,6 +1,6 @@
1
1
  import { AbstractTokenizer } from './AbstractTokenizer.js';
2
2
  import { EndOfStreamError } from './stream/index.js';
3
- const maxBufferSize = 256000;
3
+ const ignoreChunkSize = 256000;
4
4
  export class ReadStreamTokenizer extends AbstractTokenizer {
5
5
  /**
6
6
  * Constructor
@@ -31,9 +31,17 @@ export class ReadStreamTokenizer extends AbstractTokenizer {
31
31
  if (normOptions.length === 0) {
32
32
  return 0;
33
33
  }
34
- const bytesRead = await this.streamReader.read(uint8Array.subarray(0, normOptions.length), normOptions.mayBeLess);
35
- this.position += bytesRead;
36
- if ((!options || !options.mayBeLess) && bytesRead < normOptions.length) {
34
+ let bytesRead = 0;
35
+ while (bytesRead < normOptions.length) {
36
+ const chunkLength = Math.min(this.maxBufferSize, normOptions.length - bytesRead);
37
+ const count = await this.streamReader.read(uint8Array.subarray(bytesRead, bytesRead + chunkLength), normOptions.mayBeLess);
38
+ bytesRead += count;
39
+ this.position += count;
40
+ if (count < chunkLength) {
41
+ break;
42
+ }
43
+ }
44
+ if (!normOptions.mayBeLess && bytesRead < normOptions.length) {
37
45
  throw new EndOfStreamError();
38
46
  }
39
47
  return bytesRead;
@@ -46,14 +54,20 @@ export class ReadStreamTokenizer extends AbstractTokenizer {
46
54
  */
47
55
  async peekBuffer(uint8Array, options) {
48
56
  const normOptions = this.normalizeOptions(uint8Array, options);
57
+ if (normOptions.length === 0) {
58
+ return 0;
59
+ }
60
+ const lookaheadSize = normOptions.length + normOptions.position - this.position;
61
+ this.validateBufferSize(lookaheadSize);
49
62
  let bytesRead = 0;
50
63
  if (normOptions.position) {
51
64
  const skipBytes = normOptions.position - this.position;
52
65
  if (skipBytes > 0) {
53
66
  const skipBuffer = new Uint8Array(normOptions.length + skipBytes);
54
67
  bytesRead = await this.peekBuffer(skipBuffer, { mayBeLess: normOptions.mayBeLess });
55
- uint8Array.set(skipBuffer.subarray(skipBytes));
56
- return bytesRead - skipBytes;
68
+ const bytesPeeked = Math.max(0, bytesRead - skipBytes);
69
+ uint8Array.set(skipBuffer.subarray(skipBytes, skipBytes + bytesPeeked));
70
+ return bytesPeeked;
57
71
  }
58
72
  if (skipBytes < 0) {
59
73
  throw new Error('Cannot peek from a negative offset in a stream');
@@ -69,15 +83,22 @@ export class ReadStreamTokenizer extends AbstractTokenizer {
69
83
  }
70
84
  throw err;
71
85
  }
72
- if ((!normOptions.mayBeLess) && bytesRead < normOptions.length) {
86
+ if (!normOptions.mayBeLess && bytesRead < normOptions.length) {
73
87
  throw new EndOfStreamError();
74
88
  }
75
89
  }
76
90
  return bytesRead;
77
91
  }
92
+ /**
93
+ * @param length Number of bytes to ignore. Must be ≥ 0.
94
+ */
78
95
  async ignore(length) {
79
- // debug(`ignore ${this.position}...${this.position + length - 1}`);
80
- const bufSize = Math.min(maxBufferSize, length);
96
+ if (length < 0) {
97
+ throw new RangeError('ignore length must be ≥ 0 bytes');
98
+ }
99
+ this.validateLength(length, 'ignore length');
100
+ this.validateLength(this.position + length, 'end position');
101
+ const bufSize = Math.min(ignoreChunkSize, this.maxBufferSize, length);
81
102
  const buf = new Uint8Array(bufSize);
82
103
  let totBytesRead = 0;
83
104
  while (totBytesRead < length) {
@@ -91,9 +112,11 @@ export class ReadStreamTokenizer extends AbstractTokenizer {
91
112
  return totBytesRead;
92
113
  }
93
114
  abort() {
115
+ this.removeAbortListener();
94
116
  return this.streamReader.abort();
95
117
  }
96
118
  async close() {
119
+ this.removeAbortListener();
97
120
  return this.streamReader.close();
98
121
  }
99
122
  supportsRandomAccess() {
package/lib/core.d.ts CHANGED
@@ -1,13 +1,14 @@
1
1
  import type { Readable } from 'node:stream';
2
- import { type AnyWebByteStream } from './stream/index.js';
3
- import { ReadStreamTokenizer } from './ReadStreamTokenizer.js';
2
+ import { BlobTokenizer } from './BlobTokenizer.js';
4
3
  import { BufferTokenizer } from './BufferTokenizer.js';
4
+ import { ReadStreamTokenizer } from './ReadStreamTokenizer.js';
5
+ import { type AnyWebByteStream } from './stream/index.js';
5
6
  import type { ITokenizerOptions } from './types.js';
6
- import { BlobTokenizer } from './BlobTokenizer.js';
7
- export { EndOfStreamError, AbortError, type AnyWebByteStream } from './stream/index.js';
8
- export type { ITokenizer, IRandomAccessTokenizer, IFileInfo, IRandomAccessFileInfo, ITokenizerOptions, IReadChunkOptions, OnClose } from './types.js';
9
- export type { IToken, IGetToken } from '@tokenizer/token';
7
+ export type { IGetToken, IToken } from '@tokenizer/token';
10
8
  export { AbstractTokenizer } from './AbstractTokenizer.js';
9
+ export { BufferSizeError } from './BufferSizeError.js';
10
+ export { AbortError, type AnyWebByteStream, EndOfStreamError } from './stream/index.js';
11
+ export type { IFileInfo, IRandomAccessFileInfo, IRandomAccessTokenizer, IReadChunkOptions, ITokenizer, ITokenizerOptions, OnClose } from './types.js';
11
12
  /**
12
13
  * Construct ReadStreamTokenizer from given Stream.
13
14
  * Will set fileSize, if provided given Stream has set the .path property/