Skip to content

Commit 4fa6bd8

Browse files
authored
fix: Support canceling a read request (#6549)
1 parent 5311b3d commit 4fa6bd8

20 files changed

+387
-65
lines changed

cspell.code-workspace

+2-1
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,8 @@
7070
"editor.defaultFormatter": "esbenp.prettier-vscode",
7171
"[javascript]": {
7272
"editor.defaultFormatter": "esbenp.prettier-vscode"
73-
}
73+
},
74+
"editor.formatOnSave": true
7475
},
7576
"extensions": {
7677
"recommendations": ["streetsidesoftware.code-spell-checker", "dbaeumer.vscode-eslint", "esbenp.prettier-vscode"]

packages/cspell-io/package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,8 @@
5353
},
5454
"devDependencies": {
5555
"lorem-ipsum": "^2.0.8",
56-
"typescript": "~5.6.3"
56+
"typescript": "~5.6.3",
57+
"vitest-fetch-mock": "^0.4.2"
5758
},
5859
"dependencies": {
5960
"@cspell/cspell-service-bus": "workspace:*",

packages/cspell-io/src/CSpellIO.ts

+17-2
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,21 @@ import type { BufferEncoding } from './models/BufferEncoding.js';
22
import type { FileReference, TextFileResource, UrlOrFilename, UrlOrReference } from './models/FileResource.js';
33
import type { DirEntry, Stats } from './models/index.js';
44

5+
export interface ReadFileOptions {
6+
signal?: AbortSignal;
7+
encoding?: BufferEncoding;
8+
}
9+
10+
export type ReadFileOptionsOrEncoding = ReadFileOptions | BufferEncoding;
11+
512
export interface CSpellIO {
613
/**
714
* Read a file
815
* @param urlOrFilename - uri of the file to read
9-
* @param encoding - optional encoding.
16+
* @param options - optional options for reading the file.
1017
* @returns A TextFileResource.
1118
*/
12-
readFile(urlOrFilename: UrlOrReference, encoding?: BufferEncoding): Promise<TextFileResource>;
19+
readFile(urlOrFilename: UrlOrReference, options?: ReadFileOptionsOrEncoding): Promise<TextFileResource>;
1320
/**
1421
* Read a file in Sync mode.
1522
* Note: `http` requests will fail.
@@ -99,3 +106,11 @@ export interface CSpellIO {
99106
// */
100107
// resolveUrl(urlOrFilename: UrlOrFilename, relativeTo: UrlOrFilename): URL;
101108
}
109+
110+
export function toReadFileOptions(options?: ReadFileOptionsOrEncoding): ReadFileOptions | undefined {
111+
if (!options) return options;
112+
if (typeof options === 'string') {
113+
return { encoding: options };
114+
}
115+
return options;
116+
}

packages/cspell-io/src/CSpellIONode.ts

+6-4
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import { isServiceResponseSuccess, ServiceBus } from '@cspell/cspell-service-bus';
22

3-
import { isFileReference, toFileReference } from './common/CFileReference.js';
3+
import { isFileReference, toFileReference, toFileResourceRequest } from './common/CFileReference.js';
44
import { CFileResource } from './common/CFileResource.js';
55
import { compareStats } from './common/stat.js';
6-
import type { CSpellIO } from './CSpellIO.js';
6+
import type { CSpellIO, ReadFileOptionsOrEncoding } from './CSpellIO.js';
7+
import { toReadFileOptions } from './CSpellIO.js';
78
import { ErrorNotImplemented } from './errors/errors.js';
89
import { registerHandlers } from './handlers/node/file.js';
910
import type {
@@ -31,8 +32,9 @@ export class CSpellIONode implements CSpellIO {
3132
registerHandlers(serviceBus);
3233
}
3334

34-
readFile(urlOrFilename: UrlOrReference, encoding?: BufferEncoding): Promise<TextFileResource> {
35-
const ref = toFileReference(urlOrFilename, encoding);
35+
readFile(urlOrFilename: UrlOrReference, options?: ReadFileOptionsOrEncoding): Promise<TextFileResource> {
36+
const readOptions = toReadFileOptions(options);
37+
const ref = toFileResourceRequest(urlOrFilename, readOptions?.encoding, readOptions?.signal);
3638
const res = this.serviceBus.dispatch(RequestFsReadFile.create(ref));
3739
if (!isServiceResponseSuccess(res)) {
3840
throw genError(res.error, 'readFile');

packages/cspell-io/src/CVirtualFS.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ function fsPassThroughCore(fs: (url: URL) => WrappedProviderFs): Required<VFileS
146146
providerInfo: { name: 'default' },
147147
hasProvider: true,
148148
stat: async (url) => gfs(url, 'stat').stat(url),
149-
readFile: async (url) => gfs(url, 'readFile').readFile(url),
149+
readFile: async (url, options) => gfs(url, 'readFile').readFile(url, options),
150150
writeFile: async (file) => gfs(file, 'writeFile').writeFile(file),
151151
readDirectory: async (url) =>
152152
gfs(url, 'readDirectory')

packages/cspell-io/src/VFileSystem.ts

+13-1
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,26 @@ export interface FileSystemProviderInfo {
1717
name: string;
1818
}
1919

20+
export interface ReadFileOptions {
21+
signal?: AbortSignal;
22+
encoding?: BufferEncoding;
23+
}
24+
2025
export interface VFileSystemCore {
2126
/**
2227
* Read a file.
2328
* @param url - URL to read
2429
* @param encoding - optional encoding
2530
* @returns A FileResource, the content will not be decoded. Use `.getText()` to get the decoded text.
2631
*/
27-
readFile(url: UrlOrReference, encoding?: BufferEncoding): Promise<TextFileResource>;
32+
readFile(url: UrlOrReference, encoding: BufferEncoding): Promise<TextFileResource>;
33+
/**
34+
* Read a file.
35+
* @param url - URL to read
36+
* @param options - options for reading the file.
37+
* @returns A FileResource, the content will not be decoded. Use `.getText()` to get the decoded text.
38+
*/
39+
readFile(url: UrlOrReference, options?: ReadFileOptions | BufferEncoding): Promise<TextFileResource>;
2840
/**
2941
* Write a file
3042
* @param file - the file to write

packages/cspell-io/src/VirtualFS.ts

+9-1
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,16 @@ export interface VirtualFS extends Disposable {
4242
enableLogging(value?: boolean): void;
4343
}
4444

45+
export interface OptionAbort {
46+
signal?: AbortSignal;
47+
}
48+
49+
export type VProviderFileSystemReadFileOptions = OptionAbort;
50+
51+
export type VProviderFileSystemReadDirectoryOptions = OptionAbort;
52+
4553
export interface VProviderFileSystem extends Disposable {
46-
readFile(url: UrlOrReference): Promise<FileResource>;
54+
readFile(url: UrlOrReference, options?: VProviderFileSystemReadFileOptions): Promise<FileResource>;
4755
writeFile(file: FileResource): Promise<FileReference>;
4856
/**
4957
* Information about the provider.

packages/cspell-io/src/VirtualFS/WrappedProviderFs.ts

+13-4
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,13 @@ import {
1414
FileSystemProviderInfo,
1515
FSCapabilities,
1616
FSCapabilityFlags,
17+
ReadFileOptions,
1718
UrlOrReference,
1819
VFileSystemCore,
1920
VfsDirEntry,
2021
VfsStat,
2122
} from '../VFileSystem.js';
22-
import { VFileSystemProvider, VProviderFileSystem } from '../VirtualFS.js';
23+
import type { VFileSystemProvider, VProviderFileSystem } from '../VirtualFS.js';
2324

2425
export function cspellIOToFsProvider(cspellIO: CSpellIO): VFileSystemProvider {
2526
const capabilities = FSCapabilityFlags.Stat | FSCapabilityFlags.ReadWrite | FSCapabilityFlags.ReadDir;
@@ -34,7 +35,7 @@ export function cspellIOToFsProvider(cspellIO: CSpellIO): VFileSystemProvider {
3435
const fs: VProviderFileSystem = {
3536
providerInfo: { name },
3637
stat: (url) => cspellIO.getStat(url),
37-
readFile: (url) => cspellIO.readFile(url),
38+
readFile: (url, options) => cspellIO.readFile(url, options),
3839
readDirectory: (url) => cspellIO.readDirectory(url),
3940
writeFile: (file) => cspellIO.writeFile(file.url, file.content),
4041
dispose: () => undefined,
@@ -145,13 +146,17 @@ export class WrappedProviderFs implements VFileSystemCore {
145146
}
146147
}
147148

148-
async readFile(urlRef: UrlOrReference, encoding?: BufferEncoding): Promise<TextFileResource> {
149+
async readFile(
150+
urlRef: UrlOrReference,
151+
optionsOrEncoding?: BufferEncoding | ReadFileOptions,
152+
): Promise<TextFileResource> {
149153
const traceID = performance.now();
150154
const url = urlOrReferenceToUrl(urlRef);
151155
this.logEvent('readFile', 'start', traceID, url);
152156
try {
153157
checkCapabilityOrThrow(this.fs, this.capabilities, FSCapabilityFlags.Read, 'readFile', url);
154-
return createTextFileResource(await this.fs.readFile(urlRef), encoding);
158+
const readOptions = toOptions(optionsOrEncoding);
159+
return createTextFileResource(await this.fs.readFile(urlRef, readOptions), readOptions?.encoding);
155160
} catch (e) {
156161
this.logEvent('readFile', 'error', traceID, url, e instanceof Error ? e.message : '');
157162
throw wrapError(e);
@@ -282,3 +287,7 @@ export function chopUrl(url: URL | undefined): string {
282287
export function rPad(str: string, len: number, ch = ' '): string {
283288
return str.padEnd(len, ch);
284289
}
290+
291+
function toOptions(val: BufferEncoding | ReadFileOptions | undefined): ReadFileOptions | undefined {
292+
return typeof val === 'string' ? { encoding: val } : val;
293+
}

packages/cspell-io/src/VirtualFS/redirectProvider.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import assert from 'node:assert';
33
import { renameFileReference, renameFileResource, urlOrReferenceToUrl } from '../common/index.js';
44
import type { DirEntry, FileReference, FileResource } from '../models/index.js';
55
import type { FSCapabilityFlags } from '../VFileSystem.js';
6-
import type { VFileSystemProvider, VProviderFileSystem } from '../VirtualFS.js';
6+
import type { VFileSystemProvider, VProviderFileSystem, VProviderFileSystemReadFileOptions } from '../VirtualFS.js';
77
import { fsCapabilities, VFSErrorUnsupportedRequest } from './WrappedProviderFs.js';
88

99
type UrlOrReference = URL | FileReference;
@@ -132,9 +132,9 @@ function remapFS(
132132
return stat;
133133
},
134134

135-
readFile: async (url) => {
135+
readFile: async (url, options?: VProviderFileSystemReadFileOptions) => {
136136
const url2 = mapUrlOrReferenceToPrivate(url);
137-
const file = await fs.readFile(url2);
137+
const file = await fs.readFile(url2, options);
138138
return mapFileResourceToPublic(file);
139139
},
140140

packages/cspell-io/src/common/CFileReference.ts

+11-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { BufferEncoding } from '../models/BufferEncoding.js';
2-
import type { FileReference, UrlOrReference } from '../models/FileResource.js';
2+
import type { FileReference, FileResourceRequest, UrlOrReference } from '../models/FileResource.js';
33
import { toFileURL } from '../node/file/url.js';
44

55
export class CFileReference implements FileReference {
@@ -82,3 +82,13 @@ export function isFileReference(ref: UrlOrReference): ref is FileReference {
8282
export function renameFileReference(ref: FileReference, newUrl: URL): FileReference {
8383
return new CFileReference(newUrl, ref.encoding, ref.baseFilename, ref.gz);
8484
}
85+
86+
export function toFileResourceRequest(
87+
file: UrlOrReference,
88+
encoding?: BufferEncoding,
89+
signal?: AbortSignal,
90+
): FileResourceRequest {
91+
const fileReference = typeof file === 'string' ? toFileURL(file) : file;
92+
if (fileReference instanceof URL) return { url: fileReference, encoding, signal };
93+
return { url: fileReference.url, encoding: encoding ?? fileReference.encoding, signal };
94+
}

packages/cspell-io/src/common/CFileResource.ts

+9-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { assert } from '../errors/assert.js';
22
import type { BufferEncoding } from '../models/BufferEncoding.js';
33
import type { FileReference, FileResource, TextFileResource } from '../models/FileResource.js';
4-
import { decode, isGZipped } from './encode-decode.js';
4+
import { decode, encodeString, isGZipped } from './encode-decode.js';
55

66
export class CFileResource implements TextFileResource {
77
private _text?: string;
@@ -33,6 +33,14 @@ export class CFileResource implements TextFileResource {
3333
return text;
3434
}
3535

36+
getBytes(): Uint8Array {
37+
const arrayBufferview =
38+
typeof this.content === 'string' ? encodeString(this.content, this.encoding) : this.content;
39+
return arrayBufferview instanceof Uint8Array
40+
? arrayBufferview
41+
: new Uint8Array(arrayBufferview.buffer, arrayBufferview.byteOffset, arrayBufferview.byteLength);
42+
}
43+
3644
public toJson() {
3745
return {
3846
url: this.url.href,

packages/cspell-io/src/common/encode-decode.test.ts

+8-5
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ const samples = ['This is a bit of text'];
88

99
const sampleText = 'a Ā 𐀀 文 🦄';
1010
const sampleText2 = [...sampleText].reverse().join('');
11+
const encoderUTF8 = new TextEncoder();
1112

1213
describe('encode-decode', () => {
1314
test.each`
@@ -117,9 +118,11 @@ describe('encode-decode', () => {
117118
});
118119

119120
function ab(data: string | Buffer | ArrayBufferView, encoding?: BufferEncoding): ArrayBufferView {
120-
return typeof data === 'string'
121-
? Buffer.from(data, encoding)
122-
: data instanceof Buffer
123-
? Buffer.from(data)
124-
: Buffer.from(arrayBufferViewToBuffer(data));
121+
if (typeof data === 'string') {
122+
if (!encoding || encoding === 'utf8' || encoding === 'utf-8') {
123+
return encoderUTF8.encode(data);
124+
}
125+
return Buffer.from(data, encoding);
126+
}
127+
return data instanceof Buffer ? Buffer.from(data) : Buffer.from(arrayBufferViewToBuffer(data));
125128
}

packages/cspell-io/src/common/encode-decode.ts

+6-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ const decoderUTF8 = new TextDecoder('utf8');
1111
const decoderUTF16LE = new TextDecoder('utf-16le');
1212
const decoderUTF16BE = createTextDecoderUtf16BE();
1313

14-
// const encoderUTF8 = new TextEncoder();
14+
const encoderUTF8 = new TextEncoder();
1515
// const encoderUTF16LE = new TextEncoder('utf-16le');
1616

1717
export function decodeUtf16LE(data: ArrayBufferView): string {
@@ -71,6 +71,11 @@ export function decode(data: ArrayBufferView, encoding?: BufferEncodingExt): str
7171

7272
export function encodeString(str: string, encoding?: BufferEncodingExt, bom?: boolean): ArrayBufferView {
7373
switch (encoding) {
74+
case undefined:
75+
case 'utf-8':
76+
case 'utf8': {
77+
return encoderUTF8.encode(str);
78+
}
7479
case 'utf-16be':
7580
case 'utf16be': {
7681
return encodeUtf16BE(str, bom);

packages/cspell-io/src/handlers/node/file.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -97,9 +97,9 @@ const supportedFetchProtocols: Record<string, true | undefined> = { 'http:': tru
9797
*/
9898
const handleRequestFsReadFileHttp = RequestFsReadFile.createRequestHandler(
9999
(req: RequestFsReadFile, next) => {
100-
const { url } = req.params;
100+
const { url, signal, encoding } = req.params;
101101
if (!(url.protocol in supportedFetchProtocols)) return next(req);
102-
return createResponse(fetchURL(url).then((content) => CFileResource.from({ ...req.params, content })));
102+
return createResponse(fetchURL(url, signal).then((content) => CFileResource.from({ url, encoding, content })));
103103
},
104104
undefined,
105105
'Node: Read Http(s) file.',

packages/cspell-io/src/models/FileResource.ts

+22
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,23 @@ export interface FileReference {
2323
readonly gz?: boolean | undefined;
2424
}
2525

26+
export interface FileResourceRequest {
27+
/**
28+
* The URL of the File
29+
*/
30+
readonly url: URL;
31+
32+
/**
33+
* The encoding to use when reading the file.
34+
*/
35+
readonly encoding?: BufferEncoding | undefined;
36+
37+
/**
38+
* The signal to use to abort the request.
39+
*/
40+
readonly signal?: AbortSignal | undefined;
41+
}
42+
2643
export interface FileResource extends FileReference {
2744
/**
2845
* The contents of the file
@@ -38,6 +55,11 @@ export interface TextFileResource extends FileResource {
3855
* If the content is a string, then the encoding is ignored.
3956
*/
4057
getText(encoding?: BufferEncoding): string;
58+
59+
/**
60+
* Get the bytes of the file.
61+
*/
62+
getBytes(): Uint8Array;
4163
}
4264

4365
export type UrlOrFilename = string | URL;

0 commit comments

Comments
 (0)