diff --git a/package.json b/package.json index 4a30599e..343eead1 100644 --- a/package.json +++ b/package.json @@ -67,9 +67,7 @@ "test": "node --test ./test-node/**/*.test.js && pnpm run test-jest:ci" }, "dependencies": { - "@paralleldrive/cuid2": "2.2.2", - "dezalgo": "^1.0.4", - "once": "^1.4.0" + "@paralleldrive/cuid2": "2.2.2" }, "packageManager": "pnpm@10.8.1", "devDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2e33f4d3..83954dc9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,12 +11,6 @@ importers: '@paralleldrive/cuid2': specifier: 2.2.2 version: 2.2.2 - dezalgo: - specifier: ^1.0.4 - version: 1.0.4 - once: - specifier: ^1.4.0 - version: 1.4.0 devDependencies: '@rollup/plugin-commonjs': specifier: ^25.0.2 @@ -538,9 +532,6 @@ packages: resolution: {integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==} engines: {node: '>= 0.4'} - asap@2.0.6: - resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==} - astral-regex@1.0.0: resolution: {integrity: sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==} engines: {node: '>=4'} @@ -877,9 +868,6 @@ packages: resolution: {integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==} engines: {node: '>=8'} - dezalgo@1.0.4: - resolution: {integrity: sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==} - diff-sequences@27.5.1: resolution: {integrity: sha512-k1gCAXAsNgLwEL+Y8Wvl+M6oEFj5bgazfZULpS5CneoPPXRaCCW7dm+q21Ky2VEE5X+VeRDBVg1Pcvvsr4TtNQ==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} @@ -3389,8 +3377,6 @@ snapshots: get-intrinsic: 1.3.0 is-array-buffer: 3.0.5 - asap@2.0.6: {} - astral-regex@1.0.0: {} async-function@1.0.0: {} @@ -3733,11 +3719,6 @@ snapshots: detect-newline@3.1.0: {} - dezalgo@1.0.4: - dependencies: - asap: 2.0.6 - wrappy: 1.0.2 - diff-sequences@27.5.1: {} dir-glob@3.0.1: diff --git a/src/Formidable.js b/src/Formidable.js index 0b04539d..733ff673 100644 --- a/src/Formidable.js +++ b/src/Formidable.js @@ -2,7 +2,6 @@ /* eslint-disable no-underscore-dangle */ import { init as cuid2init } from "@paralleldrive/cuid2"; -import dezalgo from "dezalgo"; import { EventEmitter } from "node:events"; import fsPromises from "node:fs/promises"; import os from "node:os"; @@ -15,13 +14,13 @@ import { createBrotliDecompress, createUnzip, } from "node:zlib"; -import once from "once"; import FormidableError, * as errors from "./FormidableError.js"; import PersistentFile from "./PersistentFile.js"; import VolatileFile from "./VolatileFile.js"; import DummyParser from "./parsers/Dummy.js"; import MultipartParser from "./parsers/Multipart.js"; import { json, multipart, octetstream, querystring } from "./plugins/index.js"; +import { dezalgo, once } from "./utils.js"; const CUID2_FINGERPRINT = `${ process.env.NODE_ENV diff --git a/src/utils.js b/src/utils.js new file mode 100644 index 00000000..8f6d92c4 --- /dev/null +++ b/src/utils.js @@ -0,0 +1,52 @@ +function copyOwnProperties(source, target) { + Object.keys(source).forEach((key) => { + // eslint-disable-next-line no-param-reassign + target[key] = source[key]; + }); + return target; +} + +function once(fn) { + if (typeof fn !== "function") { + throw new TypeError("Expected a function"); + } + + const wrapped = function func(...args) { + if (wrapped.called) { + return wrapped.value; + } + + wrapped.called = true; + wrapped.value = fn.apply(this, args); + return wrapped.value; + }; + + wrapped.called = false; + return copyOwnProperties(fn, wrapped); +} + +function dezalgo(fn) { + if (typeof fn !== "function") { + throw new TypeError("Expected a function"); + } + + let sync = true; + queueMicrotask(() => { + sync = false; + }); + + const wrapped = function funcWrapper(...args) { + if (sync) { + queueMicrotask(() => { + fn.apply(this, args); + }); + return undefined; + } + + return fn.apply(this, args); + }; + + return copyOwnProperties(fn, wrapped); +} + +export { once, dezalgo }; diff --git a/test-node/utils/dezalgo.test.js b/test-node/utils/dezalgo.test.js new file mode 100644 index 00000000..aeb99ae7 --- /dev/null +++ b/test-node/utils/dezalgo.test.js @@ -0,0 +1,57 @@ +import { deepStrictEqual, strictEqual } from "node:assert"; +import test from "node:test"; +import { dezalgo } from "../../src/utils.js"; + +test("dezalgo contains the dark pony", async () => { + let n = 0; + let called = 0; + const order = [0, 2, 4, 6, 8, 1, 3, 5, 7, 9]; + let index = 0; + + function foo(i, cb) { + const wrapped = dezalgo(cb); + if (++n % 2) { + wrapped(true, i); + } else { + process.nextTick(wrapped.bind(null, false, i)); + } + } + + for (let i = 0; i < 10; i += 1) { + foo(i, (cached, value) => { + strictEqual(value, order[index]); + index += 1; + strictEqual(value % 2, cached ? 0 : 1); + called += 1; + }); + strictEqual(called, 0); + } + + await new Promise((resolve) => { + setTimeout(resolve, 10); + }); + + strictEqual(called, 10); +}); + +test("dezalgo preserves callback own properties", async () => { + const callback = () => {}; + callback.meta = { keep: true }; + + const wrapped = dezalgo(callback); + deepStrictEqual(wrapped.meta, { keep: true }); + + let called = false; + + const done = new Promise((resolve) => { + const asyncWrapped = dezalgo(() => { + called = true; + resolve(); + }); + asyncWrapped(); + strictEqual(called, false); + }); + + await done; + strictEqual(called, true); +}); diff --git a/test-node/utils/once.test.js b/test-node/utils/once.test.js new file mode 100644 index 00000000..4e3015cb --- /dev/null +++ b/test-node/utils/once.test.js @@ -0,0 +1,37 @@ +import { deepStrictEqual, ok, strictEqual } from "node:assert"; +import test from "node:test"; +import { once } from "../../src/utils.js"; + +test("once(fn)", () => { + let f = 0; + function fn(g) { + strictEqual(f, 0); + f += 1; + return f + g + this; + } + + fn.ownProperty = {}; + const wrapped = once(fn); + + strictEqual(fn.ownProperty, wrapped.ownProperty); + strictEqual(wrapped.called, false); + + for (let i = 0; i < 1e3; i += 1) { + strictEqual(f, i === 0 ? 0 : 1); + const g = wrapped.call(1, 1); + ok(wrapped.called); + strictEqual(g, 3); + strictEqual(f, 1); + } +}); + +test("once wrapper preserves callback own properties", () => { + function fn() { + return true; + } + + fn.meta = { keep: true }; + const wrapped = once(fn); + + deepStrictEqual(wrapped.meta, { keep: true }); +});