Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Support canceling a read request #6549

Merged
merged 2 commits into from
Nov 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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