Skip to content

Commit

Permalink
Merge pull request #104 from nodelib/abort_signal
Browse files Browse the repository at this point in the history
[fs.walk]: Support AbortSignal
  • Loading branch information
mrmlnc committed Jan 12, 2024
2 parents 7875cc1 + 94f559c commit 9bde8b8
Show file tree
Hide file tree
Showing 10 changed files with 98 additions and 9 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
"@nodelib-internal/tools.size-limit": "file:tools/size-limit",
"@times-components/depend": "2.3.0",
"@types/mocha": "^10.0.1",
"@types/node": "^16.18.39",
"@types/node": "^18.19.6",
"@types/sinon": "^10.0.14",
"bencho": "^0.1.1",
"eslint": "^8.45.0",
Expand Down
6 changes: 2 additions & 4 deletions packages/fs/fs.macchiato/src/stats.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,8 @@ import * as sinon from 'sinon';

import { Stats, StatsMode } from './stats';

const isWindows = process.platform === 'win32';

const uid = isWindows ? undefined : process.getuid();
const gid = isWindows ? undefined : process.getgid();
const uid = process.getuid?.();
const gid = process.getgid?.();

const FAKE_DATE = new Date();

Expand Down
6 changes: 2 additions & 4 deletions packages/fs/fs.macchiato/src/stats.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,8 @@ import * as fs from 'node:fs';

import type { PrepareOptionsFromClass } from './types';

const isWindows = process.platform === 'win32';

const uid = isWindows ? undefined : process.getuid();
const gid = isWindows ? undefined : process.getgid();
const uid = process.getuid?.();
const gid = process.getgid?.();

// https://github.com/nodejs/node/blob/6675505686310771b8016805a381945826aad887/typings/internalBinding/constants.d.ts#L148-L154
export enum StatsMode {
Expand Down
7 changes: 7 additions & 0 deletions packages/fs/fs.walk/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,13 @@ const settings = new fsWalk.Settings({
});
```

### `signal`

* Type: [`AbortSignal`](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal)
* Default: `undefined`

A signal to abort the walk. Works only with asynchronous walk.

## Changelog

See the [Releases section of our GitHub project](https://github.com/nodelib/nodelib/releases) for changelog for each release version.
Expand Down
38 changes: 38 additions & 0 deletions packages/fs/fs.walk/src/readers/async.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,44 @@ describe('Readers → Async', () => {

reader.read('directory');
});

describe('AbortSignal', () => {
it('should abort processing with abort signal', (done) => {
const ac = new AbortController();

const settings = new Settings({ signal: ac.signal });
const reader = new TestReader(settings);

reader.onError((error) => {
assert.deepStrictEqual(error.name, 'AbortError');

done();
});

reader.read('directory');

setTimeout(() => {
ac.abort();
}, 100);
});

it('should work with already aborted signal', (done) => {
const ac = new AbortController();

ac.abort();

const settings = new Settings({ signal: ac.signal });
const reader = new TestReader(settings);

reader.onError((error) => {
assert.deepStrictEqual(error.name, 'AbortError');

done();
});

reader.read('directory');
});
});
});

describe('.destroy', () => {
Expand Down
31 changes: 31 additions & 0 deletions packages/fs/fs.walk/src/readers/async.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { EventEmitter } from 'node:events';
import * as fastq from 'fastq';

import * as common from './common';
import { AbortError } from '../utils';

import type { IFileSystemAdapter } from '../adapters/fs';
import type { Settings } from '../settings';
Expand Down Expand Up @@ -74,6 +75,8 @@ export class AsyncReader extends AsyncReaderEmitter implements IAsyncReader {
this.#isFatalError = false;
this.#isDestroyed = false;

this.#attachAbortSignal();

const directory = common.replacePathSegmentSeparator(root, this.#settings.pathSegmentSeparator);

this.#pushToQueue(directory, this.#settings.basePath);
Expand All @@ -92,6 +95,34 @@ export class AsyncReader extends AsyncReaderEmitter implements IAsyncReader {
this.#queue.killAndDrain();
}

#attachAbortSignal(): void {
const signal = this.#settings.signal;

if (signal?.aborted === true) {
this.#handleError(this.#getAbortSignalReason(signal));
}

signal?.addEventListener('abort', () => {
this.#handleError(this.#getAbortSignalReason(signal));
}, { once: true });
}

/**
* In Node.js 16.14+ the AbortSignal has an empty reason by default. This issue was fixed in 16.17.
* Remove this code and just use the `AbortSignal.reason` from event when targeting Node.js 18.
*/
#getAbortSignalReason(signal: AbortSignal): Error {
if (signal.reason instanceof Error) {
return signal.reason;
}

if (typeof signal.reason === 'string') {
return new AbortError(signal.reason);
}

return new AbortError();
}

#pushToQueue(directory: string, base: string | undefined): void {
this.#queue.push({ directory, base }, (error) => {
if (error !== null) {
Expand Down
1 change: 1 addition & 0 deletions packages/fs/fs.walk/src/settings.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ describe('Settings', () => {
assert.strictEqual(fsWalkSettings.entryFilter, null);
assert.strictEqual(fsWalkSettings.errorFilter, null);
assert.deepStrictEqual(fsWalkSettings.fsScandirSettings, fsScandirSettings);
assert.strictEqual(fsWalkSettings.signal, undefined);
});

it('should return instance with custom values', () => {
Expand Down
3 changes: 3 additions & 0 deletions packages/fs/fs.walk/src/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export interface Options {
pathSegmentSeparator?: string;
stats?: boolean;
throwErrorOnBrokenSymbolicLink?: boolean;
signal?: AbortSignal;
}

export class Settings {
Expand All @@ -30,6 +31,7 @@ export class Settings {
public readonly errorFilter: ErrorFilterFunction | null;
public readonly pathSegmentSeparator: string;
public readonly fsScandirSettings: fsScandir.Settings;
public readonly signal?: AbortSignal;

constructor(options: Options = {}) {
this.basePath = options.basePath ?? undefined;
Expand All @@ -38,6 +40,7 @@ export class Settings {
this.entryFilter = options.entryFilter ?? null;
this.errorFilter = options.errorFilter ?? null;
this.pathSegmentSeparator = options.pathSegmentSeparator ?? path.sep;
this.signal = options.signal;

this.fsScandirSettings = new fsScandir.Settings({
followSymbolicLinks: options.followSymbolicLinks,
Expand Down
12 changes: 12 additions & 0 deletions packages/fs/fs.walk/src/utils/error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/**
* An error to be thrown when the request is aborted by AbortController.
*/
export class AbortError extends Error {
constructor(message: string = 'This operation was aborted.') {
super(message);

this.name = 'AbortError';

Error.captureStackTrace(this, this.constructor);
}
}
1 change: 1 addition & 0 deletions packages/fs/fs.walk/src/utils/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './error';

0 comments on commit 9bde8b8

Please sign in to comment.