GitHub Gist: https://gist.github.com/CNSeniorious000/9fc1a72e45358dd7c9e2f16e5d26df5c
起因是要给pyodide.http
支持在 cancelled 的时候自动 abort,就需要 AbortSignal.any
,结果 maintainers 之一发现这个 API 很新,甚至 FireFox 2024年才刚实现。所以叫我得 polyfill
研究了半天发现这还真不容易,而且似乎没有现成的可供 copy,只好自己实现
为此接触了 JavaScript 的WeakRef
和FinalizationRegistry
,还锻炼了一下写 JsDoc 的技能
想法:
- JavaScript 的
FinalizationRegistry
相比 python 的__del__
还是有不方便的地方的,因为一个 registery 必须自己不被 gc 才能有用 - JavaScript 里不好手动触发 gc,搞得调试比较玄学
- JavaScript 里似乎即使 gc 了内存还是不会立即释放,python 里倒似乎能在任务管理器看到内存降下去
下面附上 gist 的内容:
export function polyfillAbortSignalAny() {
/** @param {AbortSignal[]} signals */
return (signals) => {
// if (AbortSignal.any) {
// return AbortSignal.any(signals);
// }
const controller = new AbortController();
const controllerRef = new WeakRef(controller);
/** @type {[WeakRef<AbortSignal>, (() => void)][]} */
const eventListenerPairs = [];
let followingCount = signals.length;
/** @type {FinalizationRegistry<(callback: () => any) => void>} */
const registry = (globalThis.__abortSignalCleanups =
globalThis.__abortSignalCleanups ??
new FinalizationRegistry((callback) => void callback()));
signals.forEach((signal) => {
const signalRef = new WeakRef(signal);
function abort() {
controllerRef.deref()?.abort(signalRef.deref()?.reason);
}
signal.addEventListener("abort", abort);
eventListenerPairs.push([signalRef, abort]);
registry.register(signal, () => !--followingCount && clear(), signal);
});
function clear() {
eventListenerPairs.forEach(([signalRef, abort]) => {
const signal = signalRef.deref();
if (signal) {
signal.removeEventListener("abort", abort);
registry.unregister(signal);
}
const controller = controllerRef.deref();
if (controller) {
registry.unregister(controller.signal);
delete controller.signal.__controller;
}
console.log("clear", ++count);
});
}
const { signal } = controller;
registry.register(signal, clear, signal);
signal.addEventListener("abort", clear);
registry.register(controller, () => console.log("controller", count));
signal.__controller = controller;
return signal;
};
}
export let count = 0; // for test only
// node --expose-gc
({ polyfillAbortSignalAny } = await import("./polyfill.js"));
any = polyfillAbortSignalAny();
a = Array.from({ length: 2 }).map(() => { const c = new AbortController(); return [c, any([c.signal])]; })
[x, y] = a[0]
x.abort()
// a = a.map(([x, y]) => x) // to remove the references for followers
// a = a.map(([x, y]) => y) // to remove the references for origins
gc()
// look at the output
好了现在更多疑点了。不知道为什么在 vitest 或者以脚本形式运行的话,就是不会触发 gc
所以总是得用命令行打
一开始是用 vitest 发现不行,又用 watchfiles 发现也不行
不过这也算是积累了小型 JavaScript 脚本的调试经验:watchfiles 比 vitest 甚至还方便
但是我 tsconfig 还是配置不对 好烦 而且这玩意算是搞了今天一天