mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-26 06:34:20 +08:00
Use the events module and EventEmitter as untyped constructor values
In JavaScript, `ReleaseEmitter = EventEmitter` over `const EventEmitter =
require("events")` was refused with SC1090 ("a runtime-provided class value
as an untyped value"), and `class Channel extends require("events")` failed
on the require call as a value. Bundled npm code does both, for example a
small semaphore that aliases EventEmitter and constructs its release channel
through the alias.
- The events module binding and the EventEmitter or stream class itself
turn untyped as the runtime's untyped constructor that constructor
functions already use (scr_dyn_builtin_class.c). `new` through it builds
an emitter object with Node's listener API, and `===` against the class
compares identity.
- `x instanceof EventEmitter` on an untyped value also recognizes a boxed
compiled emitter (an instance of a program class extending EventEmitter),
which the prototype test alone answered false for. The same holds through
an untyped binding narrowed to the class.
- `class X extends require("events")` extends the runtime emitter, as
through a require binding.
- A typed array of the class built from such values (`[EventEmitter]`) is
refused with SC1090 rather than failing its class check at run time.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
a77ee000e7
commit
fec779abbb
@@ -8,6 +8,8 @@ Some builtins can be stored and passed as functions:
|
||||
In JavaScript, `setTimeout`, `setInterval`, `setImmediate`, `clearTimeout`, `clearInterval`, `clearImmediate`, `queueMicrotask`, and `process.nextTick` are function values wherever they are stored, including conditional and logical expressions and assignments: `typeof` answers `"function"`, identity holds across spellings, and calls schedule as the direct calls do, returning the same numeric timer handles.
|
||||
Calls through these values bypass `node:test` mock timers.
|
||||
Other builtin globals stored in JavaScript remain opaque identity values.
|
||||
In JavaScript, `require("events")` and the `EventEmitter` class assigned to an untyped binding or stored in an object (`ReleaseEmitter = EventEmitter`) are the untyped `EventEmitter` constructor that constructor functions inherit from (see below): `new` through it builds such an emitter object, and identity and `instanceof` agree with the class.
|
||||
A class can extend `require("events")` directly.
|
||||
|
||||
Other immutable aliases of table-backed Node builtins support direct calls, but cannot be passed elsewhere.
|
||||
`util.promisify` supports compile-time conversions of `child_process.execFile`, `child_process.exec`, `setTimeout` and `setImmediate` (to the `timers/promises` functions, as in Node), `stream.finished` and `stream.pipeline` (to the `stream/promises` functions, as in Node), the `zlib` callback one-shots `deflate`, `inflate`, `deflateRaw`, `inflateRaw`, `gzip`, `gunzip` and `unzip` (with or without options), the `crypto` callback functions `randomBytes`, `pbkdf2`, `scrypt`, `hkdf`, `randomFill`, `randomInt` and `generateKey`, `dns.lookup` (settling with `{ address, family }`, or the address array with `all: true`) and `dns.lookupService` (settling with `{ hostname, service }`), and the `fs` callback functions that have an `fs/promises` counterpart (`access`, `appendFile`, `chmod`, `chown`, `copyFile`, `lchown`, `link`, `lstat`, `lutimes`, `mkdir`, `mkdtemp`, `readFile`, `readdir`, `readlink`, `realpath`, `rename`, `rm`, `rmdir`, `stat`, `statfs`, `symlink`, `truncate`, `unlink`, `utimes`, and `writeFile`); calls through the converted function lower as the `fs/promises` call.
|
||||
|
||||
@@ -1,8 +1,18 @@
|
||||
import { numLit } from "../../../ir/build.js";
|
||||
import { BOOL, DYN, RUNTIME_EMITTER_CLASS, type IrExpr, type SrcLoc } from "../../../ir/ir.js";
|
||||
import {
|
||||
BOOL,
|
||||
DYN,
|
||||
RUNTIME_EMITTER_CLASS,
|
||||
type IrExpr,
|
||||
type IrStmt,
|
||||
type SrcLoc,
|
||||
} from "../../../ir/ir.js";
|
||||
import * as ts from "../../ts7/adapter.js";
|
||||
import { locOf } from "../../program.js";
|
||||
import type { Lowerer } from "../lowerer.js";
|
||||
import type { ClassInfo } from "../lower-classes.js";
|
||||
import { classInstanceOf } from "../class-dynamic-dispatch.js";
|
||||
import { isSafeToRepeat } from "../expressions/evaluation-safety.js";
|
||||
|
||||
/* EventEmitter and the stream classes as untyped constructor values, the
|
||||
* bases of JavaScript constructor functions:
|
||||
@@ -147,3 +157,55 @@ export function builtinClassInstanceOf(value: IrExpr, kind: number, loc: SrcLoc)
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
/** `value instanceof Base` for an untyped value that may also hold a
|
||||
* compiled instance: an object built by a constructor function inherits
|
||||
* the untyped constructor's prototype, and an instance of a program class
|
||||
* extending Base boxes as a typed reference the class test recognizes.
|
||||
* With `callee`, the right-hand side is an untyped value and the class
|
||||
* test applies only when it is Base's untyped constructor. */
|
||||
export function builtinClassInstanceOfAny(
|
||||
lowerer: Lowerer,
|
||||
value: IrExpr,
|
||||
kind: number,
|
||||
info: ClassInfo,
|
||||
loc: SrcLoc,
|
||||
callee?: IrExpr,
|
||||
): IrExpr {
|
||||
const stmts: IrStmt[] = [];
|
||||
const once = (expr: IrExpr, name: string): IrExpr => {
|
||||
if (isSafeToRepeat(expr)) return expr;
|
||||
const local = lowerer.declareHiddenLocal(name, DYN);
|
||||
stmts.push({ kind: "varDecl", localId: local.id, init: expr, loc });
|
||||
return { kind: "varRef", localId: local.id, type: DYN, loc };
|
||||
};
|
||||
const tested = once(value, "%instanceValue");
|
||||
const ctor = callee ? once(callee, "%instanceCallee") : null;
|
||||
const compiled = classInstanceOf(lowerer, tested, info, loc);
|
||||
const result: IrExpr = {
|
||||
kind: "logical",
|
||||
op: "||",
|
||||
left: ctor
|
||||
? { kind: "libCall", fn: "dyn.functionInstanceOf", args: [tested, ctor], type: BOOL, loc }
|
||||
: builtinClassInstanceOf(tested, kind, loc),
|
||||
right: ctor
|
||||
? {
|
||||
kind: "logical",
|
||||
op: "&&",
|
||||
left: {
|
||||
kind: "dynScalarEq",
|
||||
left: ctor,
|
||||
right: builtinClassValue(kind, loc),
|
||||
type: BOOL,
|
||||
loc,
|
||||
},
|
||||
right: compiled,
|
||||
type: BOOL,
|
||||
loc,
|
||||
}
|
||||
: compiled,
|
||||
type: BOOL,
|
||||
loc,
|
||||
};
|
||||
return stmts.length ? { kind: "seqExpr", stmts, result, type: BOOL, loc } : result;
|
||||
}
|
||||
|
||||
@@ -45,6 +45,7 @@ import { lowerElementCompoundParts } from "./element-access.js";
|
||||
import { InternalCompilerError } from "../../../errors.js";
|
||||
import { isJsSourceFile, locOf } from "../../program.js";
|
||||
import { storedClassValueType } from "../lower-classes.js";
|
||||
import { builtinClassKind } from "../builtins/builtin-class-dyn.js";
|
||||
import { hasRuntimeStatics } from "../class-runtime-statics.js";
|
||||
import { tryLowerExpression } from "./try-lower-expression.js";
|
||||
import { fenceNodeModuleMutation, lowerRequireCacheHas } from "../lower-node-module.js";
|
||||
@@ -1486,6 +1487,25 @@ export function lowerBinary(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr
|
||||
loc,
|
||||
};
|
||||
}
|
||||
// EventEmitter or a stream class against an untyped value in
|
||||
// JavaScript: the untyped value can hold the class's untyped
|
||||
// constructor (coerceToExpected), which compares by identity.
|
||||
if (
|
||||
dynSide.type.kind === "dyn" &&
|
||||
isJsSourceFile(expr.getSourceFile()) &&
|
||||
((scalarSide.type.kind === "moduleNs" && scalarSide.type.moduleId === "builtin:events") ||
|
||||
(scalarSide.kind === "classRef" && builtinClassKind(scalarSide.className) !== null))
|
||||
) {
|
||||
const boxed = lowerer.coerceToExpected(scalarSide, DYN);
|
||||
return {
|
||||
kind: "dynScalarEq",
|
||||
left: scalarSide === left ? boxed : left,
|
||||
right: scalarSide === right ? boxed : right,
|
||||
...(negated ? { negated: true as const } : {}),
|
||||
type: BOOL,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
if (
|
||||
dynSide.type.kind === "dyn" &&
|
||||
(scalarSide.type.kind === "f64" ||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* instanceof and in, including private-name and runtime-key in. */
|
||||
import * as ts from "../../ts7/adapter.js";
|
||||
import { builtinClassInstanceOf, builtinClassKind } from "../builtins/builtin-class-dyn.js";
|
||||
import { builtinClassInstanceOfAny, builtinClassKind } from "../builtins/builtin-class-dyn.js";
|
||||
import {
|
||||
checkedPrivateIn,
|
||||
localPrivateBrandClass,
|
||||
@@ -401,6 +401,16 @@ export function lowerInstanceOf(lowerer: Lowerer, expr: ts.BinaryExpression, loc
|
||||
}
|
||||
const left = lowerer.lowerExpr(expr.left);
|
||||
if (left.type.kind === "dyn") {
|
||||
// An untyped binding the checker narrowed to EventEmitter or a
|
||||
// stream class (`ReleaseEmitter = EventEmitter` in JavaScript):
|
||||
// the binding holds an untyped constructor, so the test is
|
||||
// Function.prototype[Symbol.hasInstance] over it.
|
||||
const builtinKind = targetInfo.def.runtime ? builtinClassKind(targetInfo.def.name) : null;
|
||||
if (builtinKind !== null) {
|
||||
const right = lowerer.lowerExpr(expr.right);
|
||||
if (right.type.kind === "dyn")
|
||||
return builtinClassInstanceOfAny(lowerer, left, builtinKind, targetInfo, loc, right);
|
||||
}
|
||||
if (targetInfo.def.runtime) {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
@@ -600,11 +610,11 @@ export function lowerInstanceOf(lowerer: Lowerer, expr: ts.BinaryExpression, loc
|
||||
}
|
||||
// `u instanceof EventEmitter` (or a stream class): an untyped object
|
||||
// built by a constructor function inherits the untyped constructor's
|
||||
// prototype (builtin-class-dyn.ts). Compiled emitters and streams have
|
||||
// no untyped form, so the prototype chain answers for every value.
|
||||
// prototype (builtin-class-dyn.ts), and a compiled emitter or stream
|
||||
// boxes as a typed reference the class test answers for.
|
||||
const builtinKind = builtinClassKind(target.def.name);
|
||||
if (left.type.kind === "dyn" && builtinKind !== null) {
|
||||
return builtinClassInstanceOf(left, builtinKind, loc);
|
||||
return builtinClassInstanceOfAny(lowerer, left, builtinKind, target, loc);
|
||||
}
|
||||
if (left.type.kind === "dyn") {
|
||||
lowerer.unsupported(
|
||||
|
||||
@@ -89,14 +89,17 @@ import {
|
||||
withUnitArm,
|
||||
} from "../type-mapper.js";
|
||||
import {
|
||||
canonicalBuiltinModule,
|
||||
cjsClassExprWholeExportOf,
|
||||
isCjsJsFile,
|
||||
isJsSourceFile,
|
||||
isModuleExportsAccess,
|
||||
locOf,
|
||||
requireSpecOf,
|
||||
} from "../program.js";
|
||||
import { PoisonError, dynFallbackType, jsFuncNameOf, newFnCtx, own } from "./lowerer.js";
|
||||
import { isAssertClassNew } from "./assert-instances.js";
|
||||
import { isCjsRequireGlobal } from "./lower-node-module.js";
|
||||
import { lowerArrayConstructor } from "./containers/array-construction.js";
|
||||
import { lowerMapSeedNew, lowerSetSeedNew } from "./containers/collection-methods.js";
|
||||
import { lowerObjectConstructor } from "./containers/indexed-objects.js";
|
||||
@@ -978,6 +981,19 @@ export function registerBuiltinEmitterClass(lowerer: Lowerer): void {
|
||||
lowerer.classes.set(RUNTIME_EMITTER_CLASS, info);
|
||||
}
|
||||
|
||||
/** `class Bus extends require("events") {}` in a CommonJS module: the
|
||||
* events module is the EventEmitter class itself, so the base is the
|
||||
* runtime emitter, exactly as through a `require("events")` binding. */
|
||||
export function builtinRequireBaseOf(lowerer: Lowerer, node: ts.Expression): ClassInfo | null {
|
||||
let expr = node;
|
||||
while (ts.isParenthesizedExpression(expr)) expr = expr.expression;
|
||||
if (!ts.isCallExpression(expr) || !isJsSourceFile(expr.getSourceFile())) return null;
|
||||
const spec = requireSpecOf(expr);
|
||||
if (spec === null || canonicalBuiltinModule(spec) !== "events") return null;
|
||||
if (!isCjsRequireGlobal(lowerer, expr.expression)) return null;
|
||||
return lowerer.classes.get(RUNTIME_EMITTER_CLASS) ?? null;
|
||||
}
|
||||
|
||||
/** The runtime-provided node:stream classes as eagerly-registered
|
||||
* ClassInfos (the emitter story): mapType names `%Readable` et al the
|
||||
* moment a stream type appears, so the infos must exist before any
|
||||
@@ -2092,6 +2108,11 @@ export function collectClassShapeInner(
|
||||
// callee is NOT a mixin function keeps the computed-expression
|
||||
// fence below.
|
||||
if (t && ts.isCallExpression(t.expression) && !t.typeArguments?.nodes) {
|
||||
const requiredBase = builtinRequireBaseOf(lowerer, t.expression);
|
||||
if (requiredBase) {
|
||||
base = requiredBase;
|
||||
continue;
|
||||
}
|
||||
const mixinBase = lowerer.mixinCallClassInfoOf(t.expression);
|
||||
if (mixinBase) {
|
||||
base = mixinBase;
|
||||
@@ -5724,7 +5745,7 @@ export function lowerClassExpressionInfo(
|
||||
)?.types?.nodes[0]?.expression;
|
||||
if (
|
||||
heritage &&
|
||||
(ts.isCallExpression(heritage) ||
|
||||
((ts.isCallExpression(heritage) && !builtinRequireBaseOf(lowerer, heritage)) ||
|
||||
(ts.isIdentifier(heritage) &&
|
||||
(lowerer.peekLocal(heritage) ?? lowerer.globalOf(heritage)) != null))
|
||||
) {
|
||||
|
||||
@@ -132,6 +132,7 @@ import { recordTextCodecClass } from "../../ir/ir.js";
|
||||
import { isUnitOnlyTsType, unitOnlyUnion } from "../type-mapper.js";
|
||||
import type { ClassInfo } from "./lower-classes.js";
|
||||
import {
|
||||
builtinRequireBaseOf,
|
||||
collectInheritedJsFieldWrites,
|
||||
collectVirtualJsMethods,
|
||||
decoratorNodesOf,
|
||||
@@ -373,7 +374,8 @@ export function collectProgram(lowerer: Lowerer, parts: FileParts[]): void {
|
||||
const heritage = decl.heritageClauses?.nodes.find(
|
||||
(clause) => clause.token === ts.SyntaxKind.ExtendsKeyword,
|
||||
)?.types?.nodes[0]?.expression;
|
||||
if (heritage && ts.isCallExpression(heritage)) computed.add(decl);
|
||||
if (heritage && ts.isCallExpression(heritage) && !builtinRequireBaseOf(lowerer, heritage))
|
||||
computed.add(decl);
|
||||
}
|
||||
// Descendants need the computed ancestor's completed layout before
|
||||
// their own fields and inherited methods can be collected.
|
||||
|
||||
@@ -6,7 +6,12 @@ import {
|
||||
} from "./general-streams.js";
|
||||
import { extendAssertedRecordMembers } from "./assertion-members.js";
|
||||
import { jsDynamicDeclarations } from "./js-dynamic-objects.js";
|
||||
import { builtinClassValue, builtinMemberClassKind } from "./builtins/builtin-class-dyn.js";
|
||||
import { isJsSourceFileName } from "../tsc-codes.js";
|
||||
import {
|
||||
builtinClassKind,
|
||||
builtinClassValue,
|
||||
builtinMemberClassKind,
|
||||
} from "./builtins/builtin-class-dyn.js";
|
||||
import {
|
||||
lifetimeRegionType,
|
||||
lowerLifetimeRegionCall,
|
||||
@@ -8925,7 +8930,33 @@ export class Lowerer {
|
||||
}
|
||||
// A runtime-provided class value has no checked-dynamic form that
|
||||
// inspects as Node's does (`[Function: EventEmitter] { ... }`).
|
||||
// JavaScript sources already read EventEmitter and the stream
|
||||
// classes as the runtime's untyped constructors (builtin-class-dyn.ts),
|
||||
// so one of those classes turning untyped there becomes the same
|
||||
// constructor. `require("events")` is the EventEmitter class itself,
|
||||
// so its module binding is too (`ReleaseEmitter = EventEmitter` over
|
||||
// `const EventEmitter = require("events")`).
|
||||
if (
|
||||
expr.type.kind === "moduleNs" &&
|
||||
expr.type.moduleId === "builtin:events" &&
|
||||
isJsSourceFileName(expr.loc.file)
|
||||
) {
|
||||
const value = builtinClassValue(0, expr.loc);
|
||||
if (expr.kind === "moduleNsRef") return value;
|
||||
return {
|
||||
kind: "seqExpr",
|
||||
stmts: [{ kind: "exprStmt", expr, loc: expr.loc }],
|
||||
result: value,
|
||||
type: DYN,
|
||||
loc: expr.loc,
|
||||
};
|
||||
}
|
||||
if (expr.type.kind === "classval" && this.classes.get(expr.type.className)?.def.runtime) {
|
||||
// Only the class itself: a slot of the class's type may hold a
|
||||
// program subclass, which this constructor would misidentify.
|
||||
const kind = builtinClassKind(expr.type.className);
|
||||
if (kind !== null && expr.kind === "classRef" && isJsSourceFileName(expr.loc.file))
|
||||
return builtinClassValue(kind, expr.loc);
|
||||
this.pushDiag(
|
||||
unsupportedDiag(
|
||||
"SC1090",
|
||||
@@ -9005,6 +9036,24 @@ export class Lowerer {
|
||||
);
|
||||
const handleOk = DYN_HANDLE_KINDS.has(expected.kind);
|
||||
noteUntypedStreamFlow(this, expected);
|
||||
// `[EventEmitter]` in JavaScript builds an untyped array of the
|
||||
// untyped constructor, which a typed array of the class cannot
|
||||
// hold: no class check admits that constructor.
|
||||
if (
|
||||
expr.kind === "dynArrLit" &&
|
||||
expected.kind === "array" &&
|
||||
expected.elem.kind === "classval" &&
|
||||
expr.elems.some((elem) => elem.kind === "libCall" && elem.fn === "dyn.builtinClass")
|
||||
) {
|
||||
this.pushDiag(
|
||||
unsupportedDiag(
|
||||
"SC1090",
|
||||
expr.loc,
|
||||
`holding runtime-provided class values ('${expected.elem.className.slice(1)}') in a typed array`,
|
||||
),
|
||||
);
|
||||
throw new PoisonError();
|
||||
}
|
||||
if (
|
||||
canDynCheckTo(
|
||||
expected,
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6571-events-module-class-value.cjs"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6572-extends-require-events.cjs"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6573-semaphore-release-emitter.cjs"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/fixtures/npm/cases/2595-bundled-chunk-emitter/main.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
// require("events") is the EventEmitter class. Assigned to an untyped
|
||||
// binding or stored in an object, it stays a constructor: construction
|
||||
// through it builds an emitter with the whole listener API, and identity,
|
||||
// instanceof, typeof and .name agree with the class.
|
||||
var EventEmitter = require("events"), ReleaseEmitter;
|
||||
ReleaseEmitter = EventEmitter;
|
||||
|
||||
const e = new ReleaseEmitter();
|
||||
e.on("x", (v) => console.log("x", v));
|
||||
e.prependListener("x", (v) => console.log("first x", v));
|
||||
e.once("x", (v) => console.log("once x", v));
|
||||
console.log("emit", e.emit("x", 1), e.emit("x", 2), e.emit("missing"));
|
||||
console.log("count", e.listenerCount("x"), e.eventNames(), e.getMaxListeners());
|
||||
console.log("instanceof", e instanceof EventEmitter, e instanceof ReleaseEmitter);
|
||||
console.log("identity", ReleaseEmitter === EventEmitter, ReleaseEmitter !== EventEmitter);
|
||||
console.log("typeof", typeof ReleaseEmitter, ReleaseEmitter.name);
|
||||
|
||||
const holder = { E: EventEmitter };
|
||||
const h = new holder.E();
|
||||
h.on("error", (err) => console.log("error", err.message));
|
||||
h.emit("error", new Error("boom"));
|
||||
console.log("holder", h instanceof EventEmitter, h instanceof holder.E, holder.E === EventEmitter);
|
||||
|
||||
try {
|
||||
new ReleaseEmitter().emit("error", new Error("unhandled"));
|
||||
} catch (err) {
|
||||
console.log("caught", err.message);
|
||||
}
|
||||
e.removeAllListeners("x");
|
||||
console.log("removed", e.listenerCount("x"), e.emit("x", 3));
|
||||
@@ -0,0 +1,27 @@
|
||||
// A class whose heritage is the require call itself extends EventEmitter,
|
||||
// as through a require binding. The class is a value: stored in an untyped
|
||||
// binding, construction through it runs the subclass constructor.
|
||||
class Channel extends require("events") {
|
||||
constructor(name) {
|
||||
super();
|
||||
this.name = name;
|
||||
}
|
||||
send(text) {
|
||||
return this.emit("message", this.name, text);
|
||||
}
|
||||
}
|
||||
|
||||
const a = new Channel("a");
|
||||
a.on("message", (from, text) => console.log("message", from, text));
|
||||
console.log("sent", a.send("hello"), new Channel("b").send("nobody"));
|
||||
|
||||
let Ctor;
|
||||
Ctor = Channel;
|
||||
const c = new Ctor("c");
|
||||
c.once("message", (from, text) => console.log("once", from, text));
|
||||
c.send("one");
|
||||
c.send("two");
|
||||
|
||||
const EventEmitter = require("events");
|
||||
console.log("instanceof", a instanceof EventEmitter, c instanceof EventEmitter, c instanceof Channel);
|
||||
console.log("class", typeof Ctor, Ctor.name, Ctor === Channel, a.listenerCount("message"));
|
||||
@@ -0,0 +1,62 @@
|
||||
// A token semaphore whose constructor builds its release channel from the
|
||||
// events module, the way small npm semaphores do: once through a subclass
|
||||
// declared beside the require, once through an untyped alias of the class.
|
||||
// acquire() hands out free tokens and queues the rest; release() emits, and
|
||||
// the listener wakes the oldest waiter or returns the token.
|
||||
var EventEmitter = require("events"), Subclassed = class extends EventEmitter {}, Aliased;
|
||||
Aliased = EventEmitter;
|
||||
|
||||
function defaultInit() {
|
||||
return "1";
|
||||
}
|
||||
|
||||
class Semaphore {
|
||||
constructor(nr, { initFn = defaultInit, aliased = false } = {}) {
|
||||
this.nrTokens = nr;
|
||||
this.free = [];
|
||||
this.waiting = [];
|
||||
this.releaseEmitter = aliased ? new Aliased() : new Subclassed();
|
||||
this.noTokens = initFn === defaultInit;
|
||||
this.releaseEmitter.on("release", (token) => {
|
||||
const p = this.waiting.shift();
|
||||
if (p) p.resolve(token);
|
||||
else this.free.push(token);
|
||||
});
|
||||
for (let i = 0; i < nr; i++) this.free.push(initFn());
|
||||
}
|
||||
async acquire() {
|
||||
const token = this.free.pop();
|
||||
if (token !== undefined) return token;
|
||||
return new Promise((resolve, reject) => {
|
||||
this.waiting.push({ resolve, reject });
|
||||
});
|
||||
}
|
||||
release(token) {
|
||||
this.releaseEmitter.emit("release", this.noTokens ? "1" : token);
|
||||
}
|
||||
nrWaiting() {
|
||||
return this.waiting.length;
|
||||
}
|
||||
}
|
||||
|
||||
async function run(label, sema) {
|
||||
const order = [];
|
||||
const tasks = [1, 2, 3, 4].map(async (n) => {
|
||||
const token = await sema.acquire();
|
||||
order.push(`${n}:${token}`);
|
||||
await new Promise((resolve) => setTimeout(resolve, 2));
|
||||
sema.release(token);
|
||||
});
|
||||
console.log(label, "waiting", sema.nrWaiting());
|
||||
await Promise.all(tasks);
|
||||
console.log(label, order.join(" "), "free", sema.free.length, "waiting", sema.nrWaiting());
|
||||
}
|
||||
|
||||
(async () => {
|
||||
await run("plain", new Semaphore(2));
|
||||
let next = 0;
|
||||
await run("tokens", new Semaphore(2, { initFn: () => `t${next++}` }));
|
||||
await run("aliased", new Semaphore(1, { aliased: true }));
|
||||
const s = new Semaphore(1);
|
||||
console.log("channel", s.releaseEmitter instanceof EventEmitter, s.releaseEmitter instanceof Subclassed);
|
||||
})();
|
||||
@@ -0,0 +1,11 @@
|
||||
// A bundled CommonJS chunk whose top-level var statement mixes
|
||||
// require("events"), require("util"), a sibling chunk's require and a class
|
||||
// expression extending the events binding, followed by an untyped alias of
|
||||
// the events module. The require bindings own no storage: the semaphore's
|
||||
// constructor builds its release channel through them, its queue through
|
||||
// the sibling chunk's constructor function, and both channels emit.
|
||||
import { aliased, limited } from "gtchunkee";
|
||||
|
||||
console.log(aliased());
|
||||
console.log(await limited("two slots", 4, 2));
|
||||
console.log(await limited("one slot", 3, 1));
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
export declare function limited(label: string, jobs: number, slots: number): Promise<string>;
|
||||
export declare function aliased(): string;
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
var __create = Object.create;
|
||||
var __defProp = Object.defineProperty;
|
||||
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||
var __getProtoOf = Object.getPrototypeOf;
|
||||
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||
var __commonJS = (cb, mod) => function __require() {
|
||||
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
|
||||
};
|
||||
var __export = (target, all) => {
|
||||
for (var name in all)
|
||||
__defProp(target, name, { get: all[name], enumerable: true });
|
||||
};
|
||||
var __copyProps = (to, from, except, desc) => {
|
||||
if (from && typeof from === "object" || typeof from === "function") {
|
||||
for (let key of __getOwnPropNames(from))
|
||||
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||
}
|
||||
return to;
|
||||
};
|
||||
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
||||
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
||||
mod
|
||||
));
|
||||
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||
|
||||
// node_modules/queue/index.js
|
||||
var require_queue = __commonJS({
|
||||
"node_modules/queue/index.js"(exports2, module2) {
|
||||
"use strict";function Queue(capacity){this._items=[],this._capacity=capacity||16}Queue.prototype.push=function(item){return this._items.push(item)};Queue.prototype.shift=function(){return this._items.shift()};Queue.prototype.pop=function(){return this._items.pop()};Object.defineProperty(Queue.prototype,"length",{get:function(){return this._items.length}});module2.exports=Queue;
|
||||
}
|
||||
});
|
||||
|
||||
// node_modules/sema/index.js
|
||||
var require_sema = __commonJS({
|
||||
"node_modules/sema/index.js"(exports2) {
|
||||
var EventEmitter=require("events"),util=require("util"),Queue=require_queue(),ReleaseEmitter=class extends EventEmitter{},AliasedEmitter;AliasedEmitter=EventEmitter;function defaultInit(){return"1"}class Sema{constructor(nr,{initFn=defaultInit,aliased=!1}={}){this.nrTokens=nr,this.free=new Queue(nr),this.waiting=new Queue(10),this.releaseEmitter=aliased?new AliasedEmitter:new ReleaseEmitter,this.noTokens=initFn===defaultInit,this.releaseEmitter.on("release",token=>{let p=this.waiting.shift();p?p.resolve(token):this.free.push(token)});for(let i=0;i<nr;i++)this.free.push(initFn())}async acquire(){let token=this.free.pop();return token||new Promise((resolve,reject)=>{this.waiting.push({resolve,reject})})}release(token){this.releaseEmitter.emit("release",this.noTokens?"1":token)}nrWaiting(){return this.waiting.length}describe(){return util.format("%d tokens, %s",this.nrTokens,this.releaseEmitter instanceof EventEmitter?"emitter":"other")}}exports2.create=function(nr,options){return new Sema(nr,options)};
|
||||
}
|
||||
});
|
||||
|
||||
// src/index.ts
|
||||
var index_exports = {};
|
||||
__export(index_exports, {
|
||||
aliased: () => aliased,
|
||||
limited: () => limited
|
||||
});
|
||||
module.exports = __toCommonJS(index_exports);
|
||||
var import_sema = __toESM(require_sema());
|
||||
async function limited(label, jobs, slots) {
|
||||
let next = 0;
|
||||
const sema = (0, import_sema.create)(slots, { initFn: () => "t" + next++ });
|
||||
const order = [];
|
||||
const tasks = [];
|
||||
for (let i = 0; i < jobs; i++) {
|
||||
tasks.push((async () => {
|
||||
const token = await sema.acquire();
|
||||
order.push(i + ":" + token);
|
||||
await new Promise((resolve) => setTimeout(resolve, 2));
|
||||
sema.release(token);
|
||||
})());
|
||||
}
|
||||
const queued = sema.nrWaiting();
|
||||
await Promise.all(tasks);
|
||||
return label + " queued " + queued + " order " + order.join(" ") + " (" + sema.describe() + ")";
|
||||
}
|
||||
function aliased() {
|
||||
const sema = (0, import_sema.create)(1, { aliased: true });
|
||||
return sema.describe() + " waiting " + sema.nrWaiting();
|
||||
}
|
||||
// Annotate the CommonJS export names for ESM import in node:
|
||||
0 && (module.exports = {
|
||||
aliased,
|
||||
limited
|
||||
});
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"name": "gtchunkee",
|
||||
"version": "1.0.0",
|
||||
"description": "esbuild-style CommonJS bundle (--bundle --platform=node --format=cjs, whitespace+syntax minified chunk bodies): a semaphore chunk whose top-level var statement mixes require(\"events\"), require(\"util\"), a sibling chunk require and a class extending the events binding",
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts"
|
||||
}
|
||||
@@ -19,13 +19,14 @@ export function npmCases(fixturesRoot: string): NpmCase[] {
|
||||
return [
|
||||
...globSync(join(fixturesRoot, "npm/cases/*/main.ts"))
|
||||
.sort()
|
||||
// 2465-2469, 2556-2567, 2571 and 2580-2584 are the --npm-static bundler-emitted-CJS
|
||||
// 2465-2469, 2556-2567, 2571, 2580-2584 and 2595 are the --npm-static bundler-emitted-CJS
|
||||
// cases (npm-static.test.ts drives them with the opt-in): their
|
||||
// consumers exercise the static rewrite's surface (lexer-visible
|
||||
// names the shipped .d.ts never declares, the __toESM interop
|
||||
// family), so they stay out of the flagless island lane by design.
|
||||
.filter(
|
||||
(entry) => !/\/(246[5-9]|255[6-9]|256[0-7]|2571|258[0-4])-[^/]+\/main\.ts$/.test(entry),
|
||||
(entry) =>
|
||||
!/\/(246[5-9]|255[6-9]|256[0-7]|2571|258[0-4]|2595)-[^/]+\/main\.ts$/.test(entry),
|
||||
)
|
||||
.map((entry) => ({
|
||||
name: entry.split("/").at(-2)!,
|
||||
|
||||
@@ -1438,6 +1438,15 @@ describe(`npm-static pilots${sanitize ? " (sanitized)" : ""}`, () => {
|
||||
]);
|
||||
}, 180_000);
|
||||
|
||||
// 2595: a minified chunk whose top-level var statement mixes require
|
||||
// bindings with a class extending the events binding; the bindings own no
|
||||
// storage, so the constructor reaches the modules through them.
|
||||
test("bundled chunk 2595-bundled-chunk-emitter builds its emitters through require bindings", async () => {
|
||||
await expectNodeParity(join(fixturesRoot, "npm/cases/2595-bundled-chunk-emitter/main.ts"), [
|
||||
"gtchunkee",
|
||||
]);
|
||||
}, 180_000);
|
||||
|
||||
// A whitespace/syntax-minified bundle whose helpers keep their names is
|
||||
// judged by the recognizers, not refused up front as minified.
|
||||
test("--npm-static=auto opts a minified bundle with named helpers in", () => {
|
||||
|
||||
Reference in New Issue
Block a user