Skip to content

Commit

Permalink
fix: Support canceling a read request (#6549)
Browse files Browse the repository at this point in the history
  • Loading branch information
Jason3S authored Nov 17, 2024
1 parent 5311b3d commit 4fa6bd8
Show file tree
Hide file tree
Showing 20 changed files with 387 additions and 65 deletions.
3 changes: 2 additions & 1 deletion cspell.code-workspace
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,8 @@
"editor.defaultFormatter": "esbenp.prettier-vscode",
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
}
},
"editor.formatOnSave": true
},
"extensions": {
"recommendations": ["streetsidesoftware.code-spell-checker", "dbaeumer.vscode-eslint", "esbenp.prettier-vscode"]
Expand Down
3 changes: 2 additions & 1 deletion packages/cspell-io/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,8 @@
},
"devDependencies": {
"lorem-ipsum": "^2.0.8",
"typescript": "~5.6.3"
"typescript": "~5.6.3",
"vitest-fetch-mock": "^0.4.2"
},
"dependencies": {
"@cspell/cspell-service-bus": "workspace:*",
Expand Down
19 changes: 17 additions & 2 deletions packages/cspell-io/src/CSpellIO.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,21 @@ import type { BufferEncoding } from './models/BufferEncoding.js';
import type { FileReference, TextFileResource, UrlOrFilename, UrlOrReference } from './models/FileResource.js';
import type { DirEntry, Stats } from './models/index.js';

export interface ReadFileOptions {
signal?: AbortSignal;
encoding?: BufferEncoding;
}

export type ReadFileOptionsOrEncoding = ReadFileOptions | BufferEncoding;

export interface CSpellIO {
/**
* Read a file
* @param urlOrFilename - uri of the file to read
* @param encoding - optional encoding.
* @param options - optional options for reading the file.
* @returns A TextFileResource.
*/
readFile(urlOrFilename: UrlOrReference, encoding?: BufferEncoding): Promise<TextFileResource>;
readFile(urlOrFilename: UrlOrReference, options?: ReadFileOptionsOrEncoding): Promise<TextFileResource>;
/**
* Read a file in Sync mode.
* Note: `http` requests will fail.
Expand Down Expand Up @@ -99,3 +106,11 @@ export interface CSpellIO {
// */
// resolveUrl(urlOrFilename: UrlOrFilename, relativeTo: UrlOrFilename): URL;
}

export function toReadFileOptions(options?: ReadFileOptionsOrEncoding): ReadFileOptions | undefined {
if (!options) return options;
if (typeof options === 'string') {
return { encoding: options };
}
return options;
}
10 changes: 6 additions & 4 deletions packages/cspell-io/src/CSpellIONode.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { isServiceResponseSuccess, ServiceBus } from '@cspell/cspell-service-bus';

import { isFileReference, toFileReference } from './common/CFileReference.js';
import { isFileReference, toFileReference, toFileResourceRequest } from './common/CFileReference.js';
import { CFileResource } from './common/CFileResource.js';
import { compareStats } from './common/stat.js';
import type { CSpellIO } from './CSpellIO.js';
import type { CSpellIO, ReadFileOptionsOrEncoding } from './CSpellIO.js';
import { toReadFileOptions } from './CSpellIO.js';
import { ErrorNotImplemented } from './errors/errors.js';
import { registerHandlers } from './handlers/node/file.js';
import type {
Expand Down Expand Up @@ -31,8 +32,9 @@ export class CSpellIONode implements CSpellIO {
registerHandlers(serviceBus);
}

readFile(urlOrFilename: UrlOrReference, encoding?: BufferEncoding): Promise<TextFileResource> {
const ref = toFileReference(urlOrFilename, encoding);
readFile(urlOrFilename: UrlOrReference, options?: ReadFileOptionsOrEncoding): Promise<TextFileResource> {
const readOptions = toReadFileOptions(options);
const ref = toFileResourceRequest(urlOrFilename, readOptions?.encoding, readOptions?.signal);
const res = this.serviceBus.dispatch(RequestFsReadFile.create(ref));
if (!isServiceResponseSuccess(res)) {
throw genError(res.error, 'readFile');
Expand Down
2 changes: 1 addition & 1 deletion packages/cspell-io/src/CVirtualFS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ function fsPassThroughCore(fs: (url: URL) => WrappedProviderFs): Required<VFileS
providerInfo: { name: 'default' },
hasProvider: true,
stat: async (url) => gfs(url, 'stat').stat(url),
readFile: async (url) => gfs(url, 'readFile').readFile(url),
readFile: async (url, options) => gfs(url, 'readFile').readFile(url, options),
writeFile: async (file) => gfs(file, 'writeFile').writeFile(file),
readDirectory: async (url) =>
gfs(url, 'readDirectory')
Expand Down
14 changes: 13 additions & 1 deletion packages/cspell-io/src/VFileSystem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,26 @@ export interface FileSystemProviderInfo {
name: string;
}

export interface ReadFileOptions {
signal?: AbortSignal;
encoding?: BufferEncoding;
}

export interface VFileSystemCore {
/**
* Read a file.
* @param url - URL to read
* @param encoding - optional encoding
* @returns A FileResource, the content will not be decoded. Use `.getText()` to get the decoded text.
*/
readFile(url: UrlOrReference, encoding?: BufferEncoding): Promise<TextFileResource>;
readFile(url: UrlOrReference, encoding: BufferEncoding): Promise<TextFileResource>;
/**
* Read a file.
* @param url - URL to read
* @param options - options for reading the file.
* @returns A FileResource, the content will not be decoded. Use `.getText()` to get the decoded text.
*/
readFile(url: UrlOrReference, options?: ReadFileOptions | BufferEncoding): Promise<TextFileResource>;
/**
* Write a file
* @param file - the file to write
Expand Down
10 changes: 9 additions & 1 deletion packages/cspell-io/src/VirtualFS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,16 @@ export interface VirtualFS extends Disposable {
enableLogging(value?: boolean): void;
}

export interface OptionAbort {
signal?: AbortSignal;
}

export type VProviderFileSystemReadFileOptions = OptionAbort;

export type VProviderFileSystemReadDirectoryOptions = OptionAbort;

export interface VProviderFileSystem extends Disposable {
readFile(url: UrlOrReference): Promise<FileResource>;
readFile(url: UrlOrReference, options?: VProviderFileSystemReadFileOptions): Promise<FileResource>;
writeFile(file: FileResource): Promise<FileReference>;
/**
* Information about the provider.
Expand Down
17 changes: 13 additions & 4 deletions packages/cspell-io/src/VirtualFS/WrappedProviderFs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,13 @@ import {
FileSystemProviderInfo,
FSCapabilities,
FSCapabilityFlags,
ReadFileOptions,
UrlOrReference,
VFileSystemCore,
VfsDirEntry,
VfsStat,
} from '../VFileSystem.js';
import { VFileSystemProvider, VProviderFileSystem } from '../VirtualFS.js';
import type { VFileSystemProvider, VProviderFileSystem } from '../VirtualFS.js';

export function cspellIOToFsProvider(cspellIO: CSpellIO): VFileSystemProvider {
const capabilities = FSCapabilityFlags.Stat | FSCapabilityFlags.ReadWrite | FSCapabilityFlags.ReadDir;
Expand All @@ -34,7 +35,7 @@ export function cspellIOToFsProvider(cspellIO: CSpellIO): VFileSystemProvider {
const fs: VProviderFileSystem = {
providerInfo: { name },
stat: (url) => cspellIO.getStat(url),
readFile: (url) => cspellIO.readFile(url),
readFile: (url, options) => cspellIO.readFile(url, options),
readDirectory: (url) => cspellIO.readDirectory(url),
writeFile: (file) => cspellIO.writeFile(file.url, file.content),
dispose: () => undefined,
Expand Down Expand Up @@ -145,13 +146,17 @@ export class WrappedProviderFs implements VFileSystemCore {
}
}

async readFile(urlRef: UrlOrReference, encoding?: BufferEncoding): Promise<TextFileResource> {
async readFile(
urlRef: UrlOrReference,
optionsOrEncoding?: BufferEncoding | ReadFileOptions,
): Promise<TextFileResource> {
const traceID = performance.now();
const url = urlOrReferenceToUrl(urlRef);
this.logEvent('readFile', 'start', traceID, url);
try {
checkCapabilityOrThrow(this.fs, this.capabilities, FSCapabilityFlags.Read, 'readFile', url);
return createTextFileResource(await this.fs.readFile(urlRef), encoding);
const readOptions = toOptions(optionsOrEncoding);
return createTextFileResource(await this.fs.readFile(urlRef, readOptions), readOptions?.encoding);
} catch (e) {
this.logEvent('readFile', 'error', traceID, url, e instanceof Error ? e.message : '');
throw wrapError(e);
Expand Down Expand Up @@ -282,3 +287,7 @@ export function chopUrl(url: URL | undefined): string {
export function rPad(str: string, len: number, ch = ' '): string {
return str.padEnd(len, ch);
}

function toOptions(val: BufferEncoding | ReadFileOptions | undefined): ReadFileOptions | undefined {
return typeof val === 'string' ? { encoding: val } : val;
}
6 changes: 3 additions & 3 deletions packages/cspell-io/src/VirtualFS/redirectProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import assert from 'node:assert';
import { renameFileReference, renameFileResource, urlOrReferenceToUrl } from '../common/index.js';
import type { DirEntry, FileReference, FileResource } from '../models/index.js';
import type { FSCapabilityFlags } from '../VFileSystem.js';
import type { VFileSystemProvider, VProviderFileSystem } from '../VirtualFS.js';
import type { VFileSystemProvider, VProviderFileSystem, VProviderFileSystemReadFileOptions } from '../VirtualFS.js';
import { fsCapabilities, VFSErrorUnsupportedRequest } from './WrappedProviderFs.js';

type UrlOrReference = URL | FileReference;
Expand Down Expand Up @@ -132,9 +132,9 @@ function remapFS(
return stat;
},

readFile: async (url) => {
readFile: async (url, options?: VProviderFileSystemReadFileOptions) => {
const url2 = mapUrlOrReferenceToPrivate(url);
const file = await fs.readFile(url2);
const file = await fs.readFile(url2, options);
return mapFileResourceToPublic(file);
},

Expand Down
12 changes: 11 additions & 1 deletion packages/cspell-io/src/common/CFileReference.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { BufferEncoding } from '../models/BufferEncoding.js';
import type { FileReference, UrlOrReference } from '../models/FileResource.js';
import type { FileReference, FileResourceRequest, UrlOrReference } from '../models/FileResource.js';
import { toFileURL } from '../node/file/url.js';

export class CFileReference implements FileReference {
Expand Down Expand Up @@ -82,3 +82,13 @@ export function isFileReference(ref: UrlOrReference): ref is FileReference {
export function renameFileReference(ref: FileReference, newUrl: URL): FileReference {
return new CFileReference(newUrl, ref.encoding, ref.baseFilename, ref.gz);
}

export function toFileResourceRequest(
file: UrlOrReference,
encoding?: BufferEncoding,
signal?: AbortSignal,
): FileResourceRequest {
const fileReference = typeof file === 'string' ? toFileURL(file) : file;
if (fileReference instanceof URL) return { url: fileReference, encoding, signal };
return { url: fileReference.url, encoding: encoding ?? fileReference.encoding, signal };
}
10 changes: 9 additions & 1 deletion packages/cspell-io/src/common/CFileResource.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { assert } from '../errors/assert.js';
import type { BufferEncoding } from '../models/BufferEncoding.js';
import type { FileReference, FileResource, TextFileResource } from '../models/FileResource.js';
import { decode, isGZipped } from './encode-decode.js';
import { decode, encodeString, isGZipped } from './encode-decode.js';

export class CFileResource implements TextFileResource {
private _text?: string;
Expand Down Expand Up @@ -33,6 +33,14 @@ export class CFileResource implements TextFileResource {
return text;
}

getBytes(): Uint8Array {
const arrayBufferview =
typeof this.content === 'string' ? encodeString(this.content, this.encoding) : this.content;
return arrayBufferview instanceof Uint8Array
? arrayBufferview
: new Uint8Array(arrayBufferview.buffer, arrayBufferview.byteOffset, arrayBufferview.byteLength);
}

public toJson() {
return {
url: this.url.href,
Expand Down
13 changes: 8 additions & 5 deletions packages/cspell-io/src/common/encode-decode.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const samples = ['This is a bit of text'];

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

describe('encode-decode', () => {
test.each`
Expand Down Expand Up @@ -117,9 +118,11 @@ describe('encode-decode', () => {
});

function ab(data: string | Buffer | ArrayBufferView, encoding?: BufferEncoding): ArrayBufferView {
return typeof data === 'string'
? Buffer.from(data, encoding)
: data instanceof Buffer
? Buffer.from(data)
: Buffer.from(arrayBufferViewToBuffer(data));
if (typeof data === 'string') {
if (!encoding || encoding === 'utf8' || encoding === 'utf-8') {
return encoderUTF8.encode(data);
}
return Buffer.from(data, encoding);
}
return data instanceof Buffer ? Buffer.from(data) : Buffer.from(arrayBufferViewToBuffer(data));
}
7 changes: 6 additions & 1 deletion packages/cspell-io/src/common/encode-decode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ const decoderUTF8 = new TextDecoder('utf8');
const decoderUTF16LE = new TextDecoder('utf-16le');
const decoderUTF16BE = createTextDecoderUtf16BE();

// const encoderUTF8 = new TextEncoder();
const encoderUTF8 = new TextEncoder();
// const encoderUTF16LE = new TextEncoder('utf-16le');

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

export function encodeString(str: string, encoding?: BufferEncodingExt, bom?: boolean): ArrayBufferView {
switch (encoding) {
case undefined:
case 'utf-8':
case 'utf8': {
return encoderUTF8.encode(str);
}
case 'utf-16be':
case 'utf16be': {
return encodeUtf16BE(str, bom);
Expand Down
4 changes: 2 additions & 2 deletions packages/cspell-io/src/handlers/node/file.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,9 +97,9 @@ const supportedFetchProtocols: Record<string, true | undefined> = { 'http:': tru
*/
const handleRequestFsReadFileHttp = RequestFsReadFile.createRequestHandler(
(req: RequestFsReadFile, next) => {
const { url } = req.params;
const { url, signal, encoding } = req.params;
if (!(url.protocol in supportedFetchProtocols)) return next(req);
return createResponse(fetchURL(url).then((content) => CFileResource.from({ ...req.params, content })));
return createResponse(fetchURL(url, signal).then((content) => CFileResource.from({ url, encoding, content })));
},
undefined,
'Node: Read Http(s) file.',
Expand Down
22 changes: 22 additions & 0 deletions packages/cspell-io/src/models/FileResource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,23 @@ export interface FileReference {
readonly gz?: boolean | undefined;
}

export interface FileResourceRequest {
/**
* The URL of the File
*/
readonly url: URL;

/**
* The encoding to use when reading the file.
*/
readonly encoding?: BufferEncoding | undefined;

/**
* The signal to use to abort the request.
*/
readonly signal?: AbortSignal | undefined;
}

export interface FileResource extends FileReference {
/**
* The contents of the file
Expand All @@ -38,6 +55,11 @@ export interface TextFileResource extends FileResource {
* If the content is a string, then the encoding is ignored.
*/
getText(encoding?: BufferEncoding): string;

/**
* Get the bytes of the file.
*/
getBytes(): Uint8Array;
}

export type UrlOrFilename = string | URL;
Expand Down
Loading

0 comments on commit 4fa6bd8

Please sign in to comment.