Repository: streamich/memfs (ts)

Issue:
Switch to `queueMicrotask()`
Use `queueMicrotask()` instead of `process.nextTick()` throughout the codebase.

Pull request:
refactor: switch from `process.nextTick()` to `queueMicrotask()`
The PR closes #934.

The PR does the following things:

- Add `queueMicrotask` **ponyfill**
  - The ponyfill will prefer native `queueMicrotask` when available, and will fallback to `Promise.resolve().then` otherwise.
  - The unit test cases for the ponyfill have also been added
- Replace all `process.nextTick` with `queueMicrotask`
  - Note that `process.nextTick` has a function signature of `process.nextTick(fn, ...fnArgs)` (passing arguments as rest parameters), while `queueMicrotask` does not have `...fnArgs`. 
- Remove the `nextTick` implementation and test cases 
  - The PR also removes the `nextTick` from the `process` ponyfill and its type definition

Also a question: should we also replace `setImmediate` with `queueMicrotask`? Both of them will flush the callback after microtask queue has emptied.

Patch:
diff --git a/src/fsa-to-node/FsaNodeFs.ts b/src/fsa-to-node/FsaNodeFs.ts
index 1ff691c8..2cb97aea 100644
--- a/src/fsa-to-node/FsaNodeFs.ts
+++ b/src/fsa-to-node/FsaNodeFs.ts
@@ -9,7 +9,7 @@ import { FsaNodeDirent } from './FsaNodeDirent';
 import { AMODE } from '../consts/AMODE';
 import { constants } from '../constants';
 import { FsaNodeStats } from './FsaNodeStats';
-import process from '../process';
+import queueMicrotask from '../queueMicrotask';
 import { FsSynchronousApi } from '../node/types/FsSynchronousApi';
 import { FsaNodeWriteStream } from './FsaNodeWriteStream';
 import { FsaNodeReadStream } from './FsaNodeReadStream';
@@ -84,7 +84,7 @@ export class FsaNodeFs extends FsaNodeCore implements FsCallbackApi, FsSynchrono
     util.validateCallback(callback);
     // This `if` branch is from Node.js
     if (length === 0) {
-      return process.nextTick(() => {
+      return queueMicrotask(() => {
         if (callback) callback(null, 0, buffer);
       });
     }
diff --git a/src/node/util.ts b/src/node/util.ts
index 0fb34179..c46a210b 100644
--- a/src/node/util.ts
+++ b/src/node/util.ts
@@ -4,6 +4,7 @@ import type { FsCallbackApi } from './types';
 import type * as misc from './types/misc';
 import { ENCODING_UTF8, TEncodingExtended } from '../encoding';
 import { bufferFrom } from '../internal/buffer';
+import queueMicrotask from '../queueMicrotask';
 
 export const isWin = process.platform === 'win32';
 
@@ -44,7 +45,9 @@ export function nullCheck(path, callback?) {
     const er = new Error('Path must be a string without null bytes');
     (er as any).code = 'ENOENT';
     if (typeof callback !== 'function') throw er;
-    process.nextTick(callback, er);
+    queueMicrotask(() => {
+      callback(er);
+    });
     return false;
   }
   return true;
diff --git a/src/process.ts b/src/process.ts
index 1cbdf24e..f59559c8 100644
--- a/src/process.ts
+++ b/src/process.ts
@@ -8,7 +8,6 @@ export interface IProcess {
   cwd(): string;
 
   platform: string;
-  nextTick: (callback: (...args) => void, ...args) => void;
   emitWarning: (message: string, type: string) => void;
   env: {};
 }
@@ -39,7 +38,6 @@ export function createProcess(): IProcess {
   const p: IProcess = maybeReturnProcess() || ({} as IProcess);
 
   if (!p.cwd) p.cwd = () => '/';
-  if (!p.nextTick) p.nextTick = require('./setImmediate').default;
   if (!p.emitWarning)
     p.emitWarning = (message, type) => {
       // tslint:disable-next-line:no-console
diff --git a/src/queueMicrotask.ts b/src/queueMicrotask.ts
new file mode 100644
index 00000000..5032dbe7
--- /dev/null
+++ b/src/queueMicrotask.ts
@@ -0,0 +1,4 @@
+export default typeof queueMicrotask === 'function' ? queueMicrotask : <typeof queueMicrotask>(cb =>
+      Promise.resolve()
+        .then(() => cb())
+        .catch(() => {}));
diff --git a/src/volume.ts b/src/volume.ts
index e77ef17a..060181fc 100644
--- a/src/volume.ts
+++ b/src/volume.ts
@@ -4,6 +4,7 @@ import Stats from './Stats';
 import Dirent from './Dirent';
 import { Buffer, bufferAllocUnsafe, bufferFrom } from './internal/buffer';
 import setImmediate from './setImmediate';
+import queueMicrotask from './queueMicrotask';
 import process from './process';
 import setTimeoutUnref, { TSetTimeout } from './setTimeoutUnref';
 import { Readable, Writable } from 'stream';
@@ -793,7 +794,7 @@ export class Volume implements FsCallbackApi {
 
     // This `if` branch is from Node.js
     if (length === 0) {
-      return process.nextTick(() => {
+      return queueMicrotask(() => {
         if (callback) callback(null, 0, buffer);
       });
     }
@@ -1954,7 +1955,9 @@ export class StatWatcher extends EventEmitter {
 
   stop() {
     clearTimeout(this.timeoutRef);
-    process.nextTick(emitStop, this);
+    queueMicrotask(() => {
+      emitStop.call(this, this);
+    });
   }
 }
 
@@ -2103,7 +2106,7 @@ FsReadStream.prototype.close = function (cb) {
       this.once('open', closeOnOpen);
       return;
     }
-    return process.nextTick(() => this.emit('close'));
+    return queueMicrotask(() => this.emit('close'));
   }
 
   // Since Node 18, there is only a getter for '.closed'.
@@ -2269,7 +2272,7 @@ FsWriteStream.prototype.close = function (cb) {
       this.once('open', closeOnOpen);
       return;
     }
-    return process.nextTick(() => this.emit('close'));
+    return queueMicrotask(() => this.emit('close'));
   }
 
   // Since Node 18, there is only a getter for '.closed'.


Test patch:
diff --git a/src/__tests__/process.test.ts b/src/__tests__/process.test.ts
index 3752436e..1e5aa2af 100644
--- a/src/__tests__/process.test.ts
+++ b/src/__tests__/process.test.ts
@@ -9,10 +9,6 @@ describe('process', () => {
     it('.cwd()', () => {
       expect(typeof proc.cwd()).toBe('string');
     });
-    it('.nextTick()', done => {
-      expect(typeof proc.nextTick).toBe('function');
-      proc.nextTick(done);
-    });
     it('.env', () => {
       expect(typeof proc.env).toBe('object');
     });
diff --git a/src/__tests__/queueMicrotask.ts b/src/__tests__/queueMicrotask.ts
new file mode 100644
index 00000000..bbd42816
--- /dev/null
+++ b/src/__tests__/queueMicrotask.ts
@@ -0,0 +1,10 @@
+import queueMicrotask from '../queueMicrotask';
+
+describe('queueMicrotask', () => {
+  it('Is a function', () => {
+    expect(typeof queueMicrotask).toBe('function');
+  });
+  it('Execute callback on next event loop cycle', done => {
+    queueMicrotask(done);
+  });
+});
diff --git a/src/__tests__/volume.test.ts b/src/__tests__/volume.test.ts
index 7053715c..8900236a 100644
--- a/src/__tests__/volume.test.ts
+++ b/src/__tests__/volume.test.ts
@@ -6,6 +6,7 @@ import { Volume, filenameToSteps, StatWatcher } from '../volume';
 import hasBigInt from './hasBigInt';
 import { tryGetChild, tryGetChildNode } from './util';
 import { genRndStr6 } from '../node/util';
+import queueMicrotask from '../queueMicrotask';
 import { constants } from '../constants';
 
 const { O_RDWR, O_SYMLINK } = constants;
@@ -1270,7 +1271,7 @@ describe('volume', () => {
         vol.writeFileSync('/lol.txt', '1');
         setTimeout(() => {
           vol.watchFile('/lol.txt', { interval: 1 }, (curr, prev) => {
-            process.nextTick(() => {
+            queueMicrotask(() => {
               vol.unwatchFile('/lol.txt');
               done();
             });
diff --git a/src/fsa-to-node/FsaNodeWriteStream.ts b/src/fsa-to-node/FsaNodeWriteStream.ts
index 0c4ab98f..7b45ee77 100644
--- a/src/fsa-to-node/FsaNodeWriteStream.ts
+++ b/src/fsa-to-node/FsaNodeWriteStream.ts
@@ -4,6 +4,7 @@ import { concurrency } from 'thingies/es6/concurrency';
 import { flagsToNumber } from '../node/util';
 import { FLAG } from '../consts/FLAG';
 import { FsaNodeFsOpenFile } from './FsaNodeFsOpenFile';
+import queueMicrotask from '../queueMicrotask';
 import type { IFileSystemWritableFileStream } from '../fsa/types';
 import type { IWriteStream } from '../node/types/misc';
 import type { IWriteStreamOptions } from '../node/types/options';
@@ -86,7 +87,7 @@ export class FsaNodeWriteStream extends Writable implements IWriteStream {
     const emitClose = this.options.emitClose;
     await this.__mutex__(async () => {
       if (this.__closed__ && emitClose) {
-        process.nextTick(() => this.emit('close'));
+        queueMicrotask(() => this.emit('close'));
         return;
       }
       try {