Skip to content

Commit aaf1e43

Browse files
authored
refactor(utils): internalize callback helpers (#1083)
## Summary - replace external `once` and `dezalgo` runtime deps with internal helpers in `src/utils.js` - keep only the helper behavior actually used by formidable - add parity-oriented tests translated to `node:test` ## Why Formidable only uses a small subset of the behavior from these packages: - `once(cb)` semantics - `dezalgo(cb)` semantics - preservation of own properties on wrapped callbacks We do not use `once.strict()` or `once.proto()`, so those are intentionally not carried over. This also removes the need for the transitive helper packages behind them (`wrappy` and `asap`) from formidable's runtime dependency graph. ## Testing - translated the relevant upstream-style behavior into: - `test-node/utils/once.test.js` - `test-node/utils/dezalgo.test.js` - ran full test suite successfully with `pnpm run test`
1 parent 2405918 commit aaf1e43

6 files changed

Lines changed: 148 additions & 24 deletions

File tree

package.json

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -67,9 +67,7 @@
6767
"test": "node --test ./test-node/**/*.test.js && pnpm run test-jest:ci"
6868
},
6969
"dependencies": {
70-
"@paralleldrive/cuid2": "2.2.2",
71-
"dezalgo": "^1.0.4",
72-
"once": "^1.4.0"
70+
"@paralleldrive/cuid2": "2.2.2"
7371
},
7472
"packageManager": "pnpm@10.8.1",
7573
"devDependencies": {

pnpm-lock.yaml

Lines changed: 0 additions & 19 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Formidable.js

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
/* eslint-disable no-underscore-dangle */
33

44
import { init as cuid2init } from "@paralleldrive/cuid2";
5-
import dezalgo from "dezalgo";
65
import { EventEmitter } from "node:events";
76
import fsPromises from "node:fs/promises";
87
import os from "node:os";
@@ -15,13 +14,13 @@ import {
1514
createBrotliDecompress,
1615
createUnzip,
1716
} from "node:zlib";
18-
import once from "once";
1917
import FormidableError, * as errors from "./FormidableError.js";
2018
import PersistentFile from "./PersistentFile.js";
2119
import VolatileFile from "./VolatileFile.js";
2220
import DummyParser from "./parsers/Dummy.js";
2321
import MultipartParser from "./parsers/Multipart.js";
2422
import { json, multipart, octetstream, querystring } from "./plugins/index.js";
23+
import { dezalgo, once } from "./utils.js";
2524

2625
const CUID2_FINGERPRINT = `${
2726
process.env.NODE_ENV

src/utils.js

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
function copyOwnProperties(source, target) {
2+
Object.keys(source).forEach((key) => {
3+
// eslint-disable-next-line no-param-reassign
4+
target[key] = source[key];
5+
});
6+
return target;
7+
}
8+
9+
function once(fn) {
10+
if (typeof fn !== "function") {
11+
throw new TypeError("Expected a function");
12+
}
13+
14+
const wrapped = function func(...args) {
15+
if (wrapped.called) {
16+
return wrapped.value;
17+
}
18+
19+
wrapped.called = true;
20+
wrapped.value = fn.apply(this, args);
21+
return wrapped.value;
22+
};
23+
24+
wrapped.called = false;
25+
return copyOwnProperties(fn, wrapped);
26+
}
27+
28+
function dezalgo(fn) {
29+
if (typeof fn !== "function") {
30+
throw new TypeError("Expected a function");
31+
}
32+
33+
let sync = true;
34+
queueMicrotask(() => {
35+
sync = false;
36+
});
37+
38+
const wrapped = function funcWrapper(...args) {
39+
if (sync) {
40+
queueMicrotask(() => {
41+
fn.apply(this, args);
42+
});
43+
return undefined;
44+
}
45+
46+
return fn.apply(this, args);
47+
};
48+
49+
return copyOwnProperties(fn, wrapped);
50+
}
51+
52+
export { once, dezalgo };

test-node/utils/dezalgo.test.js

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { deepStrictEqual, strictEqual } from "node:assert";
2+
import test from "node:test";
3+
import { dezalgo } from "../../src/utils.js";
4+
5+
test("dezalgo contains the dark pony", async () => {
6+
let n = 0;
7+
let called = 0;
8+
const order = [0, 2, 4, 6, 8, 1, 3, 5, 7, 9];
9+
let index = 0;
10+
11+
function foo(i, cb) {
12+
const wrapped = dezalgo(cb);
13+
if (++n % 2) {
14+
wrapped(true, i);
15+
} else {
16+
process.nextTick(wrapped.bind(null, false, i));
17+
}
18+
}
19+
20+
for (let i = 0; i < 10; i += 1) {
21+
foo(i, (cached, value) => {
22+
strictEqual(value, order[index]);
23+
index += 1;
24+
strictEqual(value % 2, cached ? 0 : 1);
25+
called += 1;
26+
});
27+
strictEqual(called, 0);
28+
}
29+
30+
await new Promise((resolve) => {
31+
setTimeout(resolve, 10);
32+
});
33+
34+
strictEqual(called, 10);
35+
});
36+
37+
test("dezalgo preserves callback own properties", async () => {
38+
const callback = () => {};
39+
callback.meta = { keep: true };
40+
41+
const wrapped = dezalgo(callback);
42+
deepStrictEqual(wrapped.meta, { keep: true });
43+
44+
let called = false;
45+
46+
const done = new Promise((resolve) => {
47+
const asyncWrapped = dezalgo(() => {
48+
called = true;
49+
resolve();
50+
});
51+
asyncWrapped();
52+
strictEqual(called, false);
53+
});
54+
55+
await done;
56+
strictEqual(called, true);
57+
});

test-node/utils/once.test.js

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { deepStrictEqual, ok, strictEqual } from "node:assert";
2+
import test from "node:test";
3+
import { once } from "../../src/utils.js";
4+
5+
test("once(fn)", () => {
6+
let f = 0;
7+
function fn(g) {
8+
strictEqual(f, 0);
9+
f += 1;
10+
return f + g + this;
11+
}
12+
13+
fn.ownProperty = {};
14+
const wrapped = once(fn);
15+
16+
strictEqual(fn.ownProperty, wrapped.ownProperty);
17+
strictEqual(wrapped.called, false);
18+
19+
for (let i = 0; i < 1e3; i += 1) {
20+
strictEqual(f, i === 0 ? 0 : 1);
21+
const g = wrapped.call(1, 1);
22+
ok(wrapped.called);
23+
strictEqual(g, 3);
24+
strictEqual(f, 1);
25+
}
26+
});
27+
28+
test("once wrapper preserves callback own properties", () => {
29+
function fn() {
30+
return true;
31+
}
32+
33+
fn.meta = { keep: true };
34+
const wrapped = once(fn);
35+
36+
deepStrictEqual(wrapped.meta, { keep: true });
37+
});

0 commit comments

Comments
 (0)