Chapter 2

JavaScript and Node Core

Closures, prototypes, the event loop, async/await, and JavaScript's weirdest corners.

JavaScript and Node Core

This file is the language-and-runtime half of your refresher: what JavaScript values actually are, how scope and this resolve, how the prototype chain and class sugar relate, how iterators and generators work, and then the two topics senior interviews spend the most time on — the event loop (with Node’s libuv phases, real “predict the output” puzzles, and a spec-faithful Promise built from scratch) and the V8 execution model (tiers, hidden classes, inline caches, elements kinds, GC). Everything here was executed on Node 22.22.2 / V8 12.4 and the pasted output is real. Where a feature is specified in ES2025/ES2026 but not yet in Node 22’s V8, it is labelled; see Feature availability at the end.

Table of contents


Cross-language note. Python made the opposite choice on loop-variable scoping, and its nonlocal/closure rules are the natural counterpart to this chapter’s material. See 20 §5.4 and 20 §6.3.

1. Values, types and coercion

1.1 Primitives vs objects

Intuition. JavaScript has exactly seven primitive types and one non-primitive type. Primitives are immutable values compared by value; objects are references compared by identity. Every “method call on a primitive” is a temporary boxing operation that is thrown away immediately.

TypetypeofWrapperNotes
undefined"undefined"absence of a value; default for uninitialised bindings
null"object"historical bug preserved for web compat
boolean"boolean"Boolean
number"number"NumberIEEE-754 binary64
bigint"bigint"BigIntarbitrary precision integers, ES2020
string"string"StringUTF-16 code units, immutable
symbol"symbol"Symbolunique property keys
object"object" / "function"functions are callable objects
const s = 'abc';
s.toUpperCase();          // boxes to a temporary String object, calls, discards the box
s.custom = 1;             // silently dropped in sloppy mode, TypeError in strict mode
console.log(s.custom);    // undefined

typeof null;              // 'object'   <- the famous bug
typeof function () {};    // 'function' <- the only non-'object' object
typeof document?.all;     // 'undefined' in browsers: [[IsHTMLDDA]], the one falsy object

Because typeof null === 'object', the reliable null check is x === null, and the reliable “is it a plain object” check is:

const isPlainObject = (v) =>
  typeof v === 'object' && v !== null && (Object.getPrototypeOf(v) === Object.prototype || Object.getPrototypeOf(v) === null);

For general tagging, Object.prototype.toString.call(v) still beats typeof:

const tag = (v) => Object.prototype.toString.call(v).slice(8, -1);
// tag([]) === 'Array', tag(null) === 'Null', tag(new Date()) === 'Date', tag(/x/) === 'RegExp'

1.2 Numbers are IEEE-754 doubles

Intuition. Every number is a 64-bit float: 1 sign bit, 11 exponent bits, 52 stored mantissa bits (53 effective, because of the implicit leading 1). Decimal fractions like 0.1 have no exact binary representation, exactly the way 1/3 has no exact decimal representation. Integers are exact only up to 2^53.

 63    62 ......... 52 51 ................................................ 0
 ┌───┬──────────────────┬──────────────────────────────────────────────────┐
 │ s │  exponent (11)   │              mantissa / fraction (52)            │
 └───┴──────────────────┴──────────────────────────────────────────────────┘
 value = (-1)^s x 1.fraction x 2^(exponent - 1023)
// 01-numbers.js
console.log('0.1 + 0.2                 =', 0.1 + 0.2);
console.log('(0.1+0.2).toFixed(20)     =', (0.1 + 0.2).toFixed(20));
console.log('Number.EPSILON            =', Number.EPSILON);
console.log('nearlyEqual(0.1+0.2, 0.3) =', Math.abs(0.1 + 0.2 - 0.3) < Number.EPSILON);
console.log('MAX_SAFE_INTEGER          =', Number.MAX_SAFE_INTEGER);
console.log('2**53                     =', 2 ** 53);
console.log('2**53 + 1                 =', 2 ** 53 + 1);
console.log('2**53 === 2**53 + 1       =', 2 ** 53 === 2 ** 53 + 1);
console.log('isSafeInteger(2**53)      =', Number.isSafeInteger(2 ** 53));
console.log('MAX_VALUE * 2             =', Number.MAX_VALUE * 2);
console.log('(0.1+0.7)*10              =', (0.1 + 0.7) * 10);
console.log('Math.floor((0.1+0.7)*10)  =', Math.floor((0.1 + 0.7) * 10));
0.1 + 0.2                 = 0.30000000000000004
(0.1+0.2).toFixed(20)     = 0.30000000000000004441
Number.EPSILON            = 2.220446049250313e-16
nearlyEqual(0.1+0.2, 0.3) = true
MAX_SAFE_INTEGER          = 9007199254740991
2**53                     = 9007199254740992
2**53 + 1                 = 9007199254740992
2**53 === 2**53 + 1       = true
isSafeInteger(2**53)      = false
MAX_VALUE * 2             = Infinity
(0.1+0.7)*10              = 7.999999999999999
Math.floor((0.1+0.7)*10)  = 7

Why 2**53 + 1 fails. At magnitude 2^53 the gap between consecutive representable doubles (the ULP) is exactly 1.0. At 2^53 + 1 the gap becomes 2.0, so 2^53 + 1 is not representable at all — it rounds to nearest-even, which is 2^53. Number.MAX_SAFE_INTEGER is 2^53 − 1 precisely because that is the last integer n for which n and n + 1 are both representable.

Number.EPSILON (2^-52) is the ULP at 1.0, so Math.abs(a - b) < Number.EPSILON is only a valid “nearly equal” test for values near 1. The general form scales it:

const nearlyEqual = (a, b, eps = Number.EPSILON) =>
  Math.abs(a - b) <= eps * Math.max(1, Math.abs(a), Math.abs(b));

For money, do not use doubles at all: use integer minor units (cents as a number while under 2^53, or BigInt), or a decimal library. Math.sumPrecise (ES2026) gives an exactly-rounded sum of an iterable of doubles but is not in Node 22.

1.3 BigInt

const big = 2n ** 53n;
console.log(big + 1n);                        // 9007199254740993n  <- exact
console.log(typeof 1n);                       // 'bigint'
console.log(1n == 1, 1n === 1);               // true false
console.log(BigInt(Number.MAX_SAFE_INTEGER) * 2n);  // 18014398509481982n
1n + 1;                                       // TypeError: Cannot mix BigInt and other types
9007199254740993n
bigint
true false
18014398509481982n
1n + 1 throws              = TypeError: Cannot mix BigInt and other types, use explicit conversions

BigInt is arbitrary precision but has no fractional part, cannot be mixed with number in arithmetic (deliberately: implicit conversion would silently lose precision), is not accepted by Math.*, and is not serialisable by JSON.stringify without a toJSON/replacer. Relational comparison (<, >, ==) does work across the two types because it cannot lose precision.

1.4 -0 and NaN

IEEE-754 has two zeros and a quiet NaN with no identity.

console.log(-0 === 0);               // true      <- === cannot see the sign
console.log(Object.is(-0, 0));       // false     <- Object.is can
console.log(1 / -0);                 // -Infinity <- the usual way to detect it before Object.is
console.log(String(-0));             // '0'       <- ToString normalises
console.log(JSON.stringify(-0));     // '0'
console.log([-0].includes(0));       // true      <- SameValueZero
console.log([-0].indexOf(0));        // 0         <- strict equality
console.log(Math.round(-0.4));       // -0        <- an easy way to produce one accidentally
console.log(Math.sign(-0));          // -0

console.log(NaN === NaN);            // false
console.log(Object.is(NaN, NaN));    // true
console.log([NaN].includes(NaN));    // true      <- SameValueZero
console.log([NaN].indexOf(NaN));     // -1        <- strict equality
console.log(new Set([NaN, NaN]).size); // 1       <- SameValueZero
console.log(isNaN('foo'), Number.isNaN('foo'));  // true false
console.log(Math.max(), Math.min());             // -Infinity Infinity

isNaN coerces its argument first (isNaN('foo') is isNaN(NaN)), which is almost never what you want. Number.isNaN does not coerce. Same story for parseInt/parseFloat vs Number.

1.5 The three (four) equality algorithms

AlgorithmUsed by0 vs -0NaN vs NaNcross-type
Abstract equality (==)==, !=equalnot equalcoerces
Strict equality (===)===, !==, indexOf, switchequalnot equalnever equal
SameValueZeroincludes, Map/Set keys, Array.prototype.fill searchequalequalnever equal
SameValueObject.is, defineProperty change detectionnot equalequalnever equal
a, b              | ==    | ===   | Object.is
------------------+-------+-------+----------
0, -0             | true  | true  | false
NaN, NaN          | false | false | true
1, "1"            | true  | false | false
null, undefined   | true  | false | false

Note that Map and Set use SameValueZero, and they normalise -0 to +0 on insert:

const m = new Map();
m.set(NaN, 'nan').set(-0, 'minus zero');
m.get(NaN);                                 // 'nan'
m.get(0);                                   // 'minus zero'
Object.is([...m.keys()][1], 0);             // true — stored as +0

1.6 The abstract equality algorithm, in full

x == y (ECMAScript IsLooselyEqual) runs top to bottom; the first matching rule wins:

  1. If Type(x) is Type(y), return x === y.
  2. null == undefined and undefined == nulltrue.
  3. Number == Stringx == ToNumber(y).
  4. String == NumberToNumber(x) == y.
  5. BigInt == StringStringToBigInt(y); if it fails, false.
  6. Boolean on either side → ToNumber(boolean) == other (so true → 1, false → 0).
  7. Number|BigInt|String|Symbol == Objectx == ToPrimitive(y).
  8. Object == Number|BigInt|String|SymbolToPrimitive(x) == y.
  9. BigInt vs Number → mathematically equal (no precision loss), false for NaN/±Infinity.
  10. Otherwise false.

Two consequences worth memorising: null/undefined are loosely equal only to each other and to nothing else (null == 0 is false — rule 2 fires before any numeric coercion), and an object is never loosely equal to a boolean without going through ToPrimitive then ToNumber.

Measured surprises (02-equality.js):

expression                | result
--------------------------+--------
[] == ![]                 | true
[] == false               | true
[] == ""                  | true
[] == 0                   | true
[[]] == 0                 | true
[0] == false              | true
[1] == true               | true
[2] == true               | false
null == undefined         | true
null === undefined        | false
null == 0                 | false
null >= 0                 | true
null > 0                  | false
undefined == 0            | false
NaN == NaN                | false
"" == 0                   | true
"0" == 0                  | true
"" == "0"                 | false
" \t\n" == 0              | true
false == "false"          | false
false == "0"              | true
true == "1"               | true
null == false             | false
undefined == false        | false
{} == "[object Object]"   | true
new String("a") == "a"    | true
new String("a") === "a"   | false
Symbol() == Symbol()      | false
1n == 1                   | true

Walk through [] == ![] out loud in an interview:

  1. ![] — every object is truthy, so this is false.
  2. [] == false — rule 6: the boolean becomes 0, giving [] == 0.
  3. Rule 8: ToPrimitive([]) with default hint → valueOf returns the array itself (not primitive) → toString[].join(',')"".
  4. "" == 0 — rule 4: ToNumber("") is 0.
  5. 0 == 0true.

And null >= 0 === true while null > 0 === false and null == 0 === false: relational operators use ToNumber (null → 0, so 0 >= 0), but == has that special-cased rule 2 that short-circuits before any numeric coercion. This also means == is not transitive:

"" == 0        -> true
0 == "0"       -> true
"" == "0"      -> false

1.7 ToPrimitive and Symbol.toPrimitive

Intuition. Whenever an object must become a primitive, the engine calls ToPrimitive(obj, hint) where hint is "number", "string" or "default". If obj[Symbol.toPrimitive] exists it is used; otherwise the engine tries valueOf then toString for the number/default hints, and toString then valueOf for the string hint. Date is the one built-in that treats "default" as "string".

const traced = {
  [Symbol.toPrimitive](hint) {
    console.log('  hint:', hint);
    return hint === 'number' ? 42 : 'forty-two';
  },
};
+traced;          // hint: number  -> 42
`${traced}`;      // hint: string  -> 'forty-two'
traced + '';      // hint: default -> 'forty-two'
traced * 2;       // hint: number  -> 84
String(traced);   // hint: string
traced == 42;     // hint: default -> 'forty-two' == 42 -> false
  Symbol.toPrimitive called with hint: number
  +traced      -> 42
  Symbol.toPrimitive called with hint: string
  `${traced}`  -> forty-two
  Symbol.toPrimitive called with hint: default
  traced + ""  -> forty-two
  Symbol.toPrimitive called with hint: number
  traced * 2   -> 84
  Symbol.toPrimitive called with hint: string
  String(traced)-> forty-two
  Symbol.toPrimitive called with hint: default
  traced == 42 -> false

Without Symbol.toPrimitive:

Without Symbol.toPrimitive, valueOf then toString (default/number hint):
  valueOf
  vt + 1   -> 8
  toString
  `${vt}`  -> seven
  valueOf
  vt * 2   -> 14
  toString
  [vt] + ""-> seven

Which operators use which hint:

OperationHint
+ (binary), ==, Date in +default
-, *, /, %, **, unary +, <, >, bitwisenumber
Template literals, String(x), property keys, ${}string

Template literal coercion always uses the string hint and calls ToString, not String() on symbols — `${Symbol()}` throws a TypeError while String(Symbol()) works. Arrays stringify by join(','), and null/undefined inside an array join to the empty string:

[].toString()                 -> ""
[1,2].toString()              -> "1,2"
[null, undefined].toString()  -> ","
[[1,[2]],3].toString()        -> "1,2,3"

Pitfalls

  • parseInt without a radix used to be octal-sensitive; modern engines default to 10 except for a 0x prefix, but ['1','7','11'].map(parseInt) is still [1, NaN, 3] because map passes the index as the second argument (the radix).
  • x == null is the one loose comparison worth using deliberately: it means “null or undefined” and nothing else. Most style guides allow it explicitly.
  • +new Date() and new Date() - 0 are numbers; new Date() + 0 is a string. That is the default hint special case.
  • JSON.stringify drops undefined, functions and symbols in objects, and turns them into null inside arrays. It also turns NaN/Infinity into null.
  • Sorting numbers without a comparator sorts them as strings: [10, 9, 1].sort() is [1, 10, 9].

Interview follow-ups

Q: Why is typeof null === 'object'?

A: In the original implementation values were tagged in the low bits of a machine word; the object tag was 000 and the null pointer was all-zero bits, so it read as an object. Fixing it was proposed and rejected as web-breaking.

Q: How do you check “is this an integer” safely?

A: Number.isInteger(x) for representability, Number.isSafeInteger(x) if you also need x ± 1 to be distinguishable. x % 1 === 0 also accepts Infinity-adjacent nonsense and coerces strings.

Q: Why does 0.1 + 0.2 !== 0.3 but 0.5 + 0.25 === 0.75?

A: 0.5 and 0.25 are negative powers of two, so they are exact in binary; 0.1 and 0.2 are repeating binary fractions and get rounded to the nearest double, and the rounding errors do not cancel.

Q: When is == acceptable?

A: x == null. Everywhere else use === — the coercion table is not something a reviewer should have to hold in their head.

Q: What is the difference between Object.is and ===?

A: Only two cases: Object.is(NaN, NaN) is true where === is false, and Object.is(0, -0) is false where === is true. Everything else is identical.

Q: Why does new String('a') == 'a' but !== 'a'?

A: == applies ToPrimitive to the wrapper object (rule 8) producing the primitive 'a'; === compares types first and an object is never strictly equal to a string.


2. Scope, hoisting, closures and this

2.1 var, let, const and the temporal dead zone

varletconst
Scopefunction (or global)blockblock
Hoistedyes, initialised to undefinedyes, uninitialised (TDZ)yes, uninitialised (TDZ)
Redeclarable in same scopeyesnono
Reassignableyesyesno
Creates a property on globalThisyes (at top level, script)nono
typeof before declaration'undefined'throwsthrows

Intuition. All declarations are hoisted — the binding is created when the scope is entered. The difference is initialisation. var bindings are initialised to undefined immediately; let/const bindings stay in the temporal dead zone until control flow reaches the declaration. The TDZ is a runtime state, not a syntactic region: a function defined before the let but called after it works fine.

// 03-scope.js
console.log('typeof hoistedFn :', typeof hoistedFn);   // function — full declaration hoisting
console.log('varX before decl :', typeof varX, varX);  // undefined undefined
try { letY; } catch (e) { console.log(e.constructor.name + ': ' + e.message); }
function hoistedFn() {}
var varX = 1;
let letY = 2;

console.log('typeof neverDeclared :', typeof neverDeclared);  // 'undefined' — safe
try { typeof tdzVar; } catch (e) { console.log('typeof tdzVar :', e.constructor.name); }
let tdzVar = 1;
typeof hoistedFn : function
varX before decl : undefined undefined
letY before decl : ReferenceError: Cannot access 'letY' before initialization
typeof neverDeclared : undefined
typeof tdzVar        : ReferenceError

That last pair is the point people miss: typeof is not a safe probe for a TDZ binding, only for a completely undeclared identifier.

const freezes the binding, not the value:

const object mutated : { a: 2 }
reassign const       : TypeError
frozen               : true
write to frozen      : TypeError (strict); silent no-op in sloppy
after frozen write   : { a: 2 }

2.2 Function scope vs block scope, and function declarations in blocks

blockFn in block    : block
blockFn after block : undefined

In a strict-mode script/module a function declaration inside a block is block-scoped. In sloppy mode, Annex B web-compat semantics hoist a var-like binding to the enclosing function scope and assign it when the declaration is evaluated — which is why the name exists but is undefined after the block in some configurations and defined in others. Never rely on it; use const fn = () => {} inside blocks.

2.3 Closures

Intuition. A closure is a function together with the environment record it captured. V8 allocates a Context object on the heap for exactly the variables an inner function references (it does escape analysis at parse time via the pre-parser), and every closure created in the same scope shares that one context. Two calls to the enclosing function create two contexts.

scope chain at call time

  [ inner function ]
        │ [[Environment]]

  [ Context: { count: 2 } ]  ← shared by every closure from the same call
        │ outer

  [ module/script scope ]
        │ outer

  [ global scope ] → null
function counter() {
  let count = 0;
  return { inc: () => ++count, get: () => count };
}
const c1 = counter(), c2 = counter();
c1.inc(); c1.inc(); c2.inc();
console.log(c1.get(), c2.get());     // 2 1 — independent contexts

2.4 The classic for (var i) bug and three fixes

const varFns = [];
for (var i = 0; i < 3; i++) varFns.push(() => i);
varFns.map((f) => f());              // [3, 3, 3]

// fix 1: let — the spec creates a NEW binding per iteration and copies the value forward
const letFns = [];
for (let j = 0; j < 3; j++) letFns.push(() => j);
letFns.map((f) => f());              // [0, 1, 2]

// fix 2: IIFE — an explicit new scope per iteration
const iifeFns = [];
for (var k = 0; k < 3; k++) iifeFns.push(((captured) => () => captured)(k));
iifeFns.map((f) => f());             // [0, 1, 2]

// fix 3: bind — freeze the value as a bound argument
const bindFns = [];
for (var m = 0; m < 3; m++) bindFns.push(((x) => x).bind(null, m));
bindFns.map((f) => f());             // [0, 1, 2]
var loop (bug):
   [ 3, 3, 3 ]
let loop (fix 1 - per-iteration binding):
   [ 0, 1, 2 ]
IIFE (fix 2):
   [ 0, 1, 2 ]
bind (fix 3):
   [ 0, 1, 2 ]

The mechanism behind fix 1 is worth knowing precisely: for a let head, the spec runs CreatePerIterationEnvironment before each iteration, which creates a fresh binding and copies the previous iteration’s value into it. That is why the increment j++ still works across iterations even though each closure sees a different binding. const in a for(;;) head is a SyntaxError for exactly this reason (you cannot increment it); for (const x of xs) is fine because there is no increment.

2.5 IIFE and module scope

Before modules, the IIFE was the only way to get a private scope:

var MyLib = (function () {
  var privateState = 0;
  return { bump: function () { return ++privateState; } };
})();

ESM made this obsolete: every module has its own scope, var at module top level does not touch globalThis, and modules are always strict. In CommonJS the module body is wrapped by Node in a function with (exports, require, module, __filename, __dirname), so module scope is function scope — which is why this at CJS top level is module.exports (an object) while in ESM it is undefined.

arrow as method       : module.exports = {}     // CJS
module-scope this     : undefined               // ESM

2.6 this: the five binding rules

Resolve them in this priority order at the call site:

  1. new bindingnew Fn() creates a fresh object whose prototype is Fn.prototype and binds this to it.
  2. Explicit bindingfn.call(o), fn.apply(o), fn.bind(o). A bound function’s this cannot be re-bound later.
  3. Implicit bindingo.fn(); this is o. Only the last property access matters.
  4. Default binding — a bare fn(). undefined in strict mode/modules, globalThis in sloppy.
  5. Arrow functions — no own this at all; they close over the this of the enclosing lexical scope, and this beats every rule above (you cannot call/bind an arrow’s this).
// 04-this.js
const obj = { name: 'obj', who() { return this?.name; } };
obj.who();                     // 'obj'
const detached = obj.who;
detached();                    // undefined (strict)
(0, obj.who)();                // undefined — the comma operator discards the Reference
obj.who.call({ name: 'call' });// 'call'

const hard = obj.who.bind({ name: 'first' });
hard.call({ name: 'second' }); // 'first' — hard binding wins

function Person(name) { this.name = name; }
const BoundPerson = Person.bind({ name: 'ignored' });
new BoundPerson('still-works').name;   // 'still-works' — new beats bind
strict default this   : undefined
sloppy default this   : globalThis
obj.who()             : obj
detached()            : undefined
wrapper.inner.who()   : obj
(0, obj.who)()        : undefined
call/apply/bind       : call apply bind
rebind a bound fn     : first
new binding           : newed
new on bound ctor     : still-works
arrow in method       : lex
new Arrow             : TypeError
detached method       : undefined
detached field arrow  : widget
static block ran      : true | static field: Widget
static this === class : true

The key insight for this questions: this is determined by how a function is called, not where it is defined — except for arrows, which are the exact opposite.

2.7 Losing this, and the four fixes

class Timer {
  ticks = 0;
  incBroken() { this.ticks++; }       // prototype method — needs a receiver
  incArrow = () => { this.ticks++; }; // instance field — captures `this` at construction
}
const t = new Timer();
[1].forEach(t.incBroken);             // TypeError: this is undefined
[1].forEach(t.incBroken.bind(t));     // fix 1: bind
[1].forEach(() => t.incBroken());     // fix 2: arrow wrapper
[1].forEach(t.incArrow);              // fix 3: class field arrow
[1].forEach(t.incBroken, t);          // fix 4: the thisArg parameter many array methods take
forEach(t.incBroken) -> TypeError
bind        -> ticks=1
arrow wrap  -> ticks=2
field arrow -> ticks=3
thisArg     -> ticks=4

bind vs class-field arrow. A prototype method exists once per class and lives on Class.prototype, so it is shared, patchable, spy-able in tests, and visible to super. A class-field arrow is created per instance during construction: N instances means N closures and N property slots, it is not on the prototype (so super.method() and subclass overrides do not see it), and it cannot be stubbed via the prototype. Prefer prototype methods plus bind in the constructor when you need a stable identity, or bind at the call site. Use field arrows when the ergonomics of React-style handlers matter more than the memory.

2.8 new.target

new.target is undefined in a normal call and the constructor function in a new call. Inside a derived constructor after super(), it is the most-derived constructor.

function Guard() {
  if (new.target === undefined) return 'called without new';
  return 'constructed as ' + new.target.name;
}
class Base { constructor() { this.builtBy = new.target.name; } }
class Derived extends Base {}
new Derived().builtBy;      // 'Derived'
Guard()               : called without new
new.target in super   : Derived

This is how you write an abstract class (if (new.target === Abstract) throw ...) and how Babel’s class downlevelling implements the “class constructor cannot be invoked without new” check.

Pitfalls

  • A let inside a loop body (not the head) is a fresh binding per iteration too; a let in the head of a while loop is not — while has no per-iteration environment.
  • this inside a plain callback passed to setTimeout is Timeout in Node and window in browsers, not your object.
  • Class fields are initialised in order, after super(), so a field cannot reference a field declared below it, and a base-class constructor cannot see a derived class’s fields (they do not exist yet). This is a real source of undefined in TypeScript classes with strictPropertyInitialization.
  • Closures over loop variables inside setTimeout still surprise people in code review; if you see var in a loop with any async callback, it is a bug or about to be one.
  • Arrow functions have no arguments, no prototype, cannot be newed, and cannot be generators.

Interview follow-ups

Q: Is the TDZ a compile-time or runtime concept?

A: Runtime. The binding exists from scope entry but is marked uninitialised; any read before the declaration executes throws ReferenceError. That is why a function that closes over a let can be defined earlier as long as it is called later.

Q: What does let cost compared to var?

A: Effectively nothing in modern V8 for straight-line code. In loops, the per-iteration environment can mean an extra context allocation per iteration if something captures the binding; if nothing captures it, V8 optimises the copy away.

Q: How do you make truly private state?

A: #private fields (hard-private, enforced by the engine, brand-checked), a closure, or a WeakMap keyed by the instance. _underscore is a convention, not privacy.

Q: Why does (0, obj.method)() lose this?

A: obj.method evaluates to a Reference with a base of obj. The comma operator calls GetValue on it, producing the bare function value and discarding the base, so the call uses default binding.

Q: What is the difference between fn.call(null) in strict and sloppy mode?

A: Sloppy mode applies ToObject to the thisArg and substitutes globalThis for null/undefined; strict mode passes the value through untouched.

Q: Can you bind an arrow function?

A: You can call .bind() on it — it returns a new function — but the this argument is ignored, because an arrow has no this binding to set. Bound arguments still work.


3. Objects and prototypes

3.1 Property descriptors

Every own property is a descriptor, one of two shapes:

KindFields
Data descriptorvalue, writable, enumerable, configurable
Accessor descriptorget, set, enumerable, configurable

Literal syntax creates writable: true, enumerable: true, configurable: true. Object.defineProperty defaults every omitted flag to false.

const o = {};
o.a = 1;                                  // all true
Object.defineProperty(o, 'b', { value: 2 }); // all false

console.log(Object.getOwnPropertyDescriptor(o, 'a'));
console.log(Object.getOwnPropertyDescriptor(o, 'b'));
console.log(Object.keys(o));   // 'b' is non-enumerable
o.b = 99;                      // silently ignored in sloppy mode, TypeError in strict
console.log(o.b);
{ value: 1, writable: true, enumerable: true, configurable: true }
{ value: 2, writable: false, enumerable: false, configurable: false }
[ 'a' ]
2

configurable: false is the only irreversible flag: you cannot make a property configurable again, and you cannot delete it or change it from data to accessor. The one legal mutation is writable: true -> false.

Freezing levels, weakest to strongest:

CallNew propertiesDeleteReassign existingReconfigure
Object.preventExtensionsnoyesyesyes
Object.sealnonoyesno
Object.freezenononono

All three are shallow. A deep freeze is a recursive walk (watch for cycles).

3.2 The prototype chain

[[Prototype]] is an internal slot. Three different things share the word “prototype”:

ExpressionMeaning
obj.__proto__legacy accessor on Object.prototype for the [[Prototype]] slot
Object.getPrototypeOf(obj)the modern, correct way to read that slot
Fn.prototypean ordinary property on a function, used as the [[Prototype]] of objects new Fn() creates

Lookup walks the chain on read, but assignment does not: obj.x = 1 creates an own property on obj even if x exists up the chain — unless the inherited property is a setter or a non-writable data property.

const d = new Dog('Rex')

  d ──[[Proto]]──> Dog.prototype ──[[Proto]]──> Animal.prototype ──[[Proto]]──> Object.prototype ──> null
  own: name              own: bark                   own: speak                     own: toString, hasOwnProperty, ...
function Animal(name) { this.name = name; }
Animal.prototype.speak = function () { return `${this.name} makes a noise`; };
function Dog(name) { Animal.call(this, name); }
Dog.prototype = Object.create(Animal.prototype);   // link
Dog.prototype.constructor = Dog;                   // repair
Dog.prototype.bark = function () { return `${this.name}: woof`; };

const d = new Dog('Rex');
console.log(d.bark(), '|', d.speak());
console.log(Object.getPrototypeOf(d) === Dog.prototype);
console.log(d instanceof Animal, Object.prototype.hasOwnProperty.call(d, 'speak'));
Rex: woof | Rex makes a noise
true
true false

Dog.prototype = Animal.prototype (a common mistake) would make Dog.prototype.bark also visible on every Animal, because there is only one object.

3.3 instanceof and Symbol.hasInstance

a instanceof B asks: is B.prototype anywhere in a’s prototype chain? It is therefore a question about objects, not constructors, and it breaks across realms (an array from an iframe or a Node vm context is not instanceof Array in your realm — use Array.isArray).

class Even { static [Symbol.hasInstance](x) { return typeof x === 'number' && x % 2 === 0; } }
console.log(4 instanceof Even, 5 instanceof Even);   // true false

3.4 class is sugar — but not only sugar

What class gives you beyond the ES5 pattern:

  • The body is always strict mode.
  • Methods are non-enumerable (Object.keys(Dog.prototype) is empty for a class).
  • The constructor cannot be called without new (TypeError).
  • Class declarations are hoisted but in the TDZ, so no use-before-declaration.
  • extends also links the constructors (Object.getPrototypeOf(Dog) === Animal), which is how static members are inherited. The ES5 pattern needs an explicit second link.
  • Derived constructors have no this until super() returns; the base constructor is what actually allocates the object, which is why extends Array works and the ES5 trick does not.
  • #private fields are enforced by the engine, not by convention, and are not visible to Object.keys, JSON.stringify, Proxy traps, or the debugger’s normal property view.
class Counter {
  #n = 0;                                   // private instance field
  static #instances = 0;                    // private static
  static { Counter.registry = new Set(); }  // static initialization block
  id = ++Counter.#instances;                // public field, runs per-instance before ctor body
  constructor() { Counter.registry.add(this); }
  inc() { return ++this.#n; }
  get value() { return this.#n; }
  static has(o) { return #n in o; }         // ergonomic brand check
}
const c = new Counter();
c.inc(); c.inc();
console.log(c.value, c.id, Counter.has(c), Counter.has({}));
console.log(Object.keys(c));                // fields are enumerable; methods are not
2 1 true false
[ 'id' ]

Field-vs-method ordering trap. Public/private fields are installed in order, before the constructor body, and each initializer sees this. A field initialized with an arrow function is a per-instance property (bound this, costs memory per instance); a method is one shared function on the prototype (unbound this, cheap). That is the real trade-off behind onClick = () => {} in class components.

3.5 super needs a home object

super is not dynamic. It resolves through the method’s [[HomeObject]], set when the method is defined with shorthand syntax. Assign a function to a property instead and super is a SyntaxError.

const base = { greet() { return 'base'; } };
const good = { __proto__: base, greet() { return super.greet() + '+good'; } };
console.log(good.greet());          // base+good
// const bad = { __proto__: base, greet: function () { return super.greet(); } }; // SyntaxError

3.6 Mixins

There is no multiple inheritance, so the idiom is a function from class to class:

type Ctor<T = {}> = new (...args: any[]) => T;

const Serializable = <T extends Ctor>(Base: T) =>
  class extends Base { toJSON() { return { ...this }; } };

const Timestamped = <T extends Ctor>(Base: T) =>
  class extends Base { createdAt = new Date(0); };

class Model {}
class User extends Serializable(Timestamped(Model)) {
  constructor(public name: string) { super(); }
}
console.log(JSON.stringify(new User('ana')));
{"createdAt":"1970-01-01T00:00:00.000Z","name":"ana"}

Each mixin adds a real link in the prototype chain, so instanceof on the mixin is not available (that is what Symbol.hasInstance is for).

Pitfalls

  • for...in walks enumerable inherited properties. Use Object.keys/entries, or Object.hasOwn(obj, k) (ES2022) as the guard. Never obj.hasOwnProperty(k) on untrusted objects.
  • Mutating Object.prototype (or Array.prototype) breaks every for...in on the page and deoptimizes property access globally.
  • Object.assign copies values, invoking getters and losing descriptors. Object.defineProperties(target, Object.getOwnPropertyDescriptors(src)) is the faithful copy.
  • Spread and Object.assign only copy own enumerable string and symbol keys — not the prototype.
  • JSON.stringify drops undefined, functions, and symbols; turns Date into a string; throws on cycles and on BigInt; and calls toJSON if present.
  • delete obj.k is fine on plain objects but on arrays it creates a hole (see elements kinds).

Interview follow-ups

Q: What is the difference between Object.create(null) and {}?

A: The former has no prototype: no toString, no __proto__ accessor, no hasOwnProperty. That makes it the correct choice for a string-keyed dictionary because a key named __proto__ or constructor cannot collide with anything or trigger prototype pollution.

Q: How do you implement new yourself?

A: function myNew(Ctor, ...args) { const obj = Object.create(Ctor.prototype); const r = Ctor.apply(obj, args); return (typeof r === 'object' && r !== null) || typeof r === 'function' ? r : obj; } — the return-value rule is the part people forget.

Q: Why is Object.freeze not enough for immutability?

A: It is shallow, it does not stop mutation of objects reachable through frozen properties, and it does not prevent Map/Set contents from changing. Use a recursive freeze or a persistent-data-structure library.

Q: What does class A extends null do?

A: It is legal syntax but nearly unusable: you cannot call super(), so the only way to construct an instance is to return an object explicitly from the constructor.

Q: How would you check whether an object has a private field without throwing?

A: The #field in obj brand check (ES2022), typically wrapped in a static method as shown above.

Q: Prototype vs class inheritance — is there a performance difference?

A: No meaningful one; class compiles to the same shapes. The real performance lever is keeping object shapes consistent so inline caches stay monomorphic.


4. Functions

4.1 Parameters, arity, and arguments

function f(a, b = 2, ...rest) {}
console.log(f.length);   // 1 — stops at the first default or rest param

arguments is array-like, not an array; it is absent in arrow functions and in any function with default/rest/destructured parameters it is unmapped (no live link to the named parameters). Use rest parameters instead — they are a real array and do not force V8 to materialize an arguments object.

4.2 Currying and partial application

type AnyFn = (...args: any[]) => any;

function curry<F extends AnyFn>(fn: F) {
  const arity = fn.length;
  return function curried(...args: any[]): any {
    return args.length >= arity ? fn(...args) : (...more: any[]) => curried(...args, ...more);
  };
}

const volume = (l: number, w: number, h: number) => l * w * h;
const cv = curry(volume);
console.log(cv(2)(3)(4), cv(2, 3)(4), cv(2)(3, 4), cv(2, 3, 4));   // 24 24 24 24

Partial application is the weaker, cheaper cousin: fn.bind(null, 2, 3).

4.3 Composition

const pipe = <T>(...fns: Array<(x: T) => T>) => (x: T) => fns.reduce((v, f) => f(v), x);
const compose = <T>(...fns: Array<(x: T) => T>) => (x: T) => fns.reduceRight((v, f) => f(v), x);

const inc = (n: number) => n + 1, dbl = (n: number) => n * 2;
console.log(pipe(inc, dbl)(5), compose(inc, dbl)(5));   // 12 11

Properly variadic typed pipe needs variadic tuple types — see TypeScript: tuples and variadic types.

4.4 Memoization

function memoize<A, R>(fn: (a: A) => R, keyFn: (a: A) => unknown = (a) => a) {
  const cache = new Map<unknown, R>();
  return (a: A): R => {
    const k = keyFn(a);
    if (cache.has(k)) return cache.get(k)!;
    const v = fn(a);
    cache.set(k, v);
    return v;
  };
}

// Object keys without leaking: WeakMap lets the key be collected.
function memoizeObject<T extends object, R>(fn: (t: T) => R) {
  const cache = new WeakMap<T, R>();
  return (t: T): R => {
    let v = cache.get(t);
    if (v === undefined && !cache.has(t)) { v = fn(t); cache.set(t, v); }
    return v as R;
  };
}

The three real-world questions an interviewer will push on: what is the cache key for multiple arguments (JSON.stringify is correct-ish and slow; a nested Map trie is correct and fast), how do you bound the cache (LRU — see Data structures), and how do you avoid leaking (WeakMap for object keys, TTL/size caps otherwise).

4.5 Debounce and throttle

function debounce<F extends (...a: any[]) => void>(fn: F, ms: number, immediate = false) {
  let t: ReturnType<typeof setTimeout> | null = null;
  return function (this: unknown, ...args: Parameters<F>) {
    const callNow = immediate && t === null;
    if (t) clearTimeout(t);
    t = setTimeout(() => { t = null; if (!immediate) fn.apply(this, args); }, ms);
    if (callNow) fn.apply(this, args);
  };
}

function throttle<F extends (...a: any[]) => void>(fn: F, ms: number) {
  let last = 0, pending: Parameters<F> | null = null, timer: any = null;
  return function (this: unknown, ...args: Parameters<F>) {
    const now = Date.now();
    if (now - last >= ms) { last = now; fn.apply(this, args); }
    else {                                       // trailing edge
      pending = args;
      timer ??= setTimeout(() => {
        last = Date.now(); timer = null;
        if (pending) { fn.apply(this, pending); pending = null; }
      }, ms - (now - last));
    }
  };
}
FiresUse for
Debounceonce, ms after the last callsearch-as-you-type, resize end, autosave
Throttleat most once per ms windowscroll position, mousemove, analytics beacons

4.6 Interview follow-ups

Q: Implement Function.prototype.bind.

A:

Function.prototype.myBind = function (thisArg, ...bound) {
  const target = this;
  if (typeof target !== 'function') throw new TypeError('not callable');
  function Bound(...args) {
    // called with `new`: ignore thisArg, keep the prototype chain
    return target.apply(this instanceof Bound ? this : thisArg, [...bound, ...args]);
  }
  Bound.prototype = Object.create(target.prototype || null);
  return Bound;
};

Q: Why can’t you tail-call-optimize in Node?

A: ES2015 specifies proper tail calls, but only JavaScriptCore ever shipped them. V8 removed its implementation (stack-trace and debugging costs, plus a syntactic-vs-implicit design argument), so deep recursion in Node overflows around 11k frames. Convert to an explicit loop or an explicit stack.

Q: What is a pure function and why does it matter here?

A: Same inputs -> same output, no observable side effects. It is the precondition for memoization, for safe parallelism, and for the referential-transparency arguments you will use when justifying a refactor.

Q: fn.call vs fn.apply vs fn.bind?

A: call invokes now with spread arguments, apply invokes now with an array, bind invokes later and returns a new function with this and leading arguments fixed. bind is the only one that survives being passed around.


5. Iterators and generators

5.1 The protocol

An object is iterable if it has a [Symbol.iterator]() method returning an iterator: an object with next() returning { value, done }. for...of, spread, destructuring, Array.from, Promise.all, new Map(...) and yield* all consume it.

class Range implements Iterable<number> {
  constructor(private lo: number, private hi: number, private step = 1) {}
  [Symbol.iterator](): Iterator<number> {
    let i = this.lo;
    const { hi, step } = this;
    return {
      next: () => (i < hi ? { value: (i += step) - step, done: false } : { value: undefined, done: true }),
      return: () => { i = hi; return { value: undefined, done: true }; },   // called on break
    };
  }
}
console.log([...new Range(0, 10, 3)]);   // [ 0, 3, 6, 9 ]

The return() method is the cleanup hook: break, throw, and early return inside for...of all call it. That is how using-style resource release works for iterators.

5.2 Generators

A generator function returns an object that is both an iterator and an iterable, and whose execution suspends at each yield.

function* fib() { let [a, b] = [0, 1]; for (;;) { yield a; [a, b] = [b, a + b]; } }
function* take(it, n) { let i = 0; for (const v of it) { if (i++ >= n) return; yield v; } }
console.log([...take(fib(), 10)]);   // [0,1,1,2,3,5,8,13,21,34]

Two-way communication makes generators coroutines, not just lazy lists:

function* accumulator() {
  let total = 0;
  while (true) {
    const x = yield total;          // the value passed to next() lands here
    if (x === undefined) continue;
    total += x;
  }
}
const acc = accumulator();
acc.next();                          // prime: run to the first yield
console.log(acc.next(5).value, acc.next(10).value, acc.next(1).value);   // 5 15 16
  • gen.next(v) — resume, yield evaluates to v.
  • gen.throw(e) — resume by raising e at the suspended yield (catchable inside).
  • gen.return(v) — resume as if a return v executed there; finally blocks still run.
  • yield* delegates to another iterable and evaluates to its return value.
function* inner() { yield 1; yield 2; return 'inner done'; }
function* outer() { const r = yield* inner(); yield r; }
console.log([...outer()]);   // [ 1, 2, 'inner done' ]

5.3 Async iteration

Symbol.asyncIterator + for await...of. Each next() returns a promise.

async function* pages(total) {
  for (let p = 1; p <= total; p++) {
    await new Promise(r => setTimeout(r, 10));       // pretend fetch
    yield { page: p, items: [p * 10, p * 10 + 1] };
  }
}
(async () => {
  for await (const { page, items } of pages(3)) console.log(page, items);
})();
1 [ 10, 11 ]
2 [ 20, 21 ]
3 [ 30, 31 ]

for await also accepts a sync iterable of promises, awaiting each in turn — which is a common source of accidental serialization. If you want concurrency, collect promises and Promise.all them.

5.4 Iterator helpers (ES2026, shipped in Node 22)

Iterator helpers put lazy map/filter/take/drop/flatMap/reduce/toArray/some/every/find on Iterator.prototype, so they work on generators, Map.keys(), NodeList iterators — anything.

function* naturals() { let n = 1; while (true) yield n++; }
let evaluated = 0;
const out = naturals()
  .map(n => { evaluated++; return n * n; })
  .filter(n => n % 2 === 1)
  .take(4)
  .toArray();
console.log(out, 'source values pulled:', evaluated);
[ 1, 9, 25, 49 ] source values pulled: 7

Seven pulls, not infinity — the pipeline is demand-driven, and no intermediate array is ever built. Compare with array.map().filter().slice(), which allocates two full intermediates. Iterator.from(x) wraps a bare iterator so the helpers are available.

Pitfalls

  • Generators are single-pass. const g = gen(); [...g]; [...g] gives you the values then []. Return a fresh generator from [Symbol.iterator]() if you need repeat iteration.
  • yield inside a non-generator callback (arr.forEach(x => yield x)) is a SyntaxError; use a for...of loop.
  • return inside a try with a finally that also returns: the finally wins, silently.
  • Spreading an infinite iterator hangs the process. take first.
  • for await over an array of promises awaits sequentially. Promise.all is what you usually want.

Interview follow-ups

Q: Difference between an iterable and an iterator?

A: An iterable has [Symbol.iterator](); an iterator has next(). Generators are both, which is why for...of over a generator works and why you can only do it once.

Q: How do you make an object work with spread and for...of?

A: Give it [Symbol.iterator]. Object spread ({...o}) is different — it copies own enumerable keys and needs no protocol.

Q: Where would you actually use a generator in production?

A: Paginated API traversal, streaming parsers, ID generators, tree/graph traversal you want to consume lazily, backtracking search where you want the first N solutions, and test-data builders. Redux-saga made the coroutine use case famous.

Q: What is the memory advantage?

A: O(1) instead of O(n): you never materialize the sequence. Combined with iterator helpers you get a full lazy pipeline. See the Python equivalent in Python core.


6. Asynchrony and the event loop

6.1 The model

graph TD
    A["Synchronous script"] --> B["process.nextTick queue"]
    B --> C["Microtask queue"]
    C --> D["timers<br/>(setTimeout/setInterval)"]
    D -->|drain nextTick + microtasks| E["pending callbacks"]
    E -->|drain nextTick + microtasks| F["poll<br/>(I/O)"]
    F -->|drain nextTick + microtasks| G["check<br/>(setImmediate)"]
    G -->|drain nextTick + microtasks| H["close callbacks"]
    H -->|drain nextTick + microtasks| B

JavaScript has one call stack. Anything asynchronous is a job enqueued for later. There are two queue tiers, and the rule that explains almost every puzzle is:

After every macrotask, and after the synchronous script finishes, the engine drains the entire microtask queue — including microtasks enqueued by microtasks — before taking the next macrotask.

TierEnqueued byDrained
Microtaskpromise reactions, queueMicrotask, await resumption, MutationObserverfully, after each macrotask
MacrotasksetTimeout, setInterval, setImmediate, I/O completions, UI eventsone per loop turn

Node adds one queue above microtasks: process.nextTick. Priority is nextTick queue -> microtask queue -> next libuv phase.

6.2 Node’s libuv phases

   ┌───────────────────────────┐
┌─>│           timers          │  setTimeout / setInterval callbacks whose time has come
│  └────────────┬──────────────┘
│  ┌────────────┴──────────────┐
│  │     pending callbacks     │  deferred I/O callbacks (e.g. some TCP errors)
│  └────────────┬──────────────┘
│  ┌────────────┴──────────────┐
│  │       idle, prepare       │  internal
│  └────────────┬──────────────┘      ┌───────────────┐
│  ┌────────────┴──────────────┐      │   incoming:   │
│  │           poll            │<─────┤  connections, │  waits for I/O; this is where the
│  └────────────┬──────────────┘      │   data, ...   │  process actually blocks
│  ┌────────────┴──────────────┐      └───────────────┘
│  │           check           │  setImmediate callbacks
│  └────────────┬──────────────┘
│  ┌────────────┴──────────────┐
└──┤       close callbacks     │  socket.on('close'), etc.
   └───────────────────────────┘

   Between EVERY phase transition: drain nextTick queue, then drain microtask queue.

Practical consequences:

  • setTimeout(fn, 0) is really “at least 1 ms” and runs in the timers phase — a full loop turn away.
  • setImmediate(fn) runs in the check phase, i.e. after the current poll phase, which for I/O callbacks means sooner than setTimeout(fn, 0). From the main module the ordering of setTimeout(0) vs setImmediate is famously nondeterministic (it depends on how long process startup took relative to the 1 ms timer threshold).
  • process.nextTick runs before any promise callback and can starve the loop if it recurses.

6.3 Predict-the-output puzzles

Puzzle 1 — every queue at once.

console.log('1 sync');
setTimeout(() => console.log('2 setTimeout 0'), 0);
setImmediate(() => console.log('3 setImmediate'));
process.nextTick(() => console.log('4 nextTick'));
Promise.resolve().then(() => console.log('5 microtask'));
queueMicrotask(() => console.log('6 queueMicrotask'));
(async () => { console.log('7 async body sync'); await null; console.log('8 after await'); })();
console.log('9 sync end');
1 sync
7 async body sync
9 sync end
4 nextTick
5 microtask
6 queueMicrotask
8 after await
2 setTimeout 0
3 setImmediate

Read it as three groups: everything synchronous (1, 7, 9 — note the async function body runs synchronously up to its first await), then nextTick, then microtasks in FIFO order (5, 6, 8 — the await null resumption is just another microtask, queued third), then macrotasks.

Puzzle 2 — await costs one tick, not two.

async function a() { console.log('a1'); await b(); console.log('a2'); }
async function b() { console.log('b1'); }
console.log('start');
a();
new Promise(r => { console.log('p exec'); r(); }).then(() => console.log('p then'));
console.log('end');
start
a1
b1
p exec
end
a2
p then

a2 beats p then. Since the ES2019 “await optimization”, awaiting a native promise costs one microtask tick, not three — so a’s continuation is queued before the .then registered afterwards. Interviewers who learned this pre-2019 sometimes expect the opposite; if the awaited value is a thenable (not a native promise) you pay the extra ticks and the order flips.

Puzzle 3 — interleaving nextTick and microtasks.

setTimeout(() => console.log('timeout'), 0);
Promise.resolve().then(() => { console.log('mt1'); process.nextTick(() => console.log('tick in mt')); });
process.nextTick(() => { console.log('tick1'); Promise.resolve().then(() => console.log('mt in tick')); });
tick1
mt1
mt in tick
tick in mt
timeout

The nextTick queue is drained first (tick1), then microtasks (mt1, then mt in tick which was just enqueued), and only when both are empty does Node re-check nextTick (tick in mt) and finally move to the timers phase.

6.4 Promise combinators

CombinatorSettles whenResultRejects when
allall fulfilarray of values, input orderfirst rejection (fail fast)
allSettledall settlearray of `{status, valuereason}`
anyfirst fulfilmentthat valueall reject -> AggregateError
racefirst settle, either waythat value/reasonfirst settle is a rejection
Promise.try(fn)wraps a sync-or-async fn so a synchronous throw becomes a rejection

Two things to say out loud in an interview: Promise.all does not cancel the other operations on rejection (there is no cancellation in the promise model — that is what AbortController is for), and race with an already-settled promise in the list resolves in one tick regardless of timers.

6.5 A spec-faithful Promise from scratch

This passes the test suite below, including thenable adoption, the chaining-cycle check, and single-settlement.

const PENDING = 'pending', FULFILLED = 'fulfilled', REJECTED = 'rejected';

class MyPromise {
  #state = PENDING; #value; #cbs = [];

  constructor(executor) {
    const resolve = v => this.#settle(FULFILLED, v);
    const reject  = r => this.#settle(REJECTED, r);
    try { executor(v => this.#resolveWith(v, resolve, reject), reject); }
    catch (e) { reject(e); }
  }

  // 2.3 of the Promises/A+ spec: the Promise Resolution Procedure
  #resolveWith(v, resolve, reject) {
    if (v === this) return reject(new TypeError('Chaining cycle detected'));
    if (v && (typeof v === 'object' || typeof v === 'function')) {
      let then;
      try { then = v.then; } catch (e) { return reject(e); }
      if (typeof then === 'function') {
        let called = false;
        try {
          return then.call(v,
            y => { if (called) return; called = true; this.#resolveWith(y, resolve, reject); },
            r => { if (called) return; called = true; reject(r); });
        } catch (e) { if (!called) reject(e); return; }
      }
    }
    resolve(v);
  }

  #settle(state, value) {
    if (this.#state !== PENDING) return;          // settle exactly once
    this.#state = state; this.#value = value;
    for (const cb of this.#cbs) queueMicrotask(cb);
    this.#cbs = [];
  }

  then(onFul, onRej) {
    return new MyPromise((resolve, reject) => {
      const run = () => queueMicrotask(() => {
        const handler = this.#state === FULFILLED ? onFul : onRej;
        if (typeof handler !== 'function') {        // pass-through
          this.#state === FULFILLED ? resolve(this.#value) : reject(this.#value);
          return;
        }
        try { resolve(handler(this.#value)); } catch (e) { reject(e); }
      });
      this.#state === PENDING ? this.#cbs.push(run) : run();
    });
  }

  catch(onRej)  { return this.then(undefined, onRej); }
  finally(fn)   { return this.then(v => { fn(); return v; }, r => { fn(); throw r; }); }

  static resolve(v) { return v instanceof MyPromise ? v : new MyPromise(res => res(v)); }
  static reject(r)  { return new MyPromise((_, rej) => rej(r)); }

  static all(iter) {
    return new MyPromise((resolve, reject) => {
      const items = [...iter]; const out = new Array(items.length); let left = items.length;
      if (!left) return resolve([]);
      items.forEach((p, i) =>
        MyPromise.resolve(p).then(v => { out[i] = v; if (--left === 0) resolve(out); }, reject));
    });
  }
  static race(iter) {
    return new MyPromise((res, rej) => { for (const p of iter) MyPromise.resolve(p).then(res, rej); });
  }
  static allSettled(iter) {
    return MyPromise.all([...iter].map(p => MyPromise.resolve(p).then(
      value => ({ status: 'fulfilled', value }), reason => ({ status: 'rejected', reason }))));
  }
}
PASS resolve/then
PASS reject/catch
PASS thenable adoption
PASS nested promise
PASS all
PASS race
PASS allSettled
PASS throw in handler propagates
PASS settle once
PASS finally passthrough
PASS async ordering

The three details interviewers look for: then always returns a new promise (that is what makes chaining work), handlers always run in a microtask even if the promise is already settled (so ordering is deterministic), and resolving with a thenable adopts its state recursively.

6.6 async/await is generators plus a driver

async function desugars to a generator whose yields are awaits, driven by a trampoline:

function toAsync(genFn) {
  return function (...args) {
    const it = genFn.apply(this, args);
    return new Promise((resolve, reject) => {
      const step = (method, arg) => {
        let r;
        try { r = it[method](arg); } catch (e) { return reject(e); }
        if (r.done) return resolve(r.value);
        Promise.resolve(r.value).then(v => step('next', v), e => step('throw', e));
      };
      step('next');
    });
  };
}
const load = toAsync(function* () {
  const a = yield Promise.resolve(1);
  const b = yield Promise.resolve(a + 1);
  return a + b;
});
load().then(console.log);   // 3

That is exactly what Babel’s regenerator emitted, and it is the answer to “how does await work under the hood”.

6.7 The error-handling traps

// 1. await inside forEach does nothing — forEach ignores the returned promise
[1, 2, 3].forEach(async n => { await sleep(10); console.log(n); });   // returns immediately
for (const n of [1, 2, 3]) { await sleep(10); console.log(n); }        // actually sequential

// 2. sequential vs parallel
const a = await fetchA(); const b = await fetchB();          // 2 round trips, serialized
const [a2, b2] = await Promise.all([fetchA(), fetchB()]);     // 1 round trip of latency

// 3. unhandled rejection from a promise created but awaited later
const p = risky();                 // rejects at t=0
await sleep(1000);                 // by now Node may have reported an unhandled rejection
try { await p; } catch {}          // too late in some Node versions
// fix: attach the handler immediately, or create the promise where you await it

// 4. throwing inside a non-awaited async function is invisible
async function bg() { throw new Error('lost'); }
bg();                              // unhandledRejection
bg().catch(reportError);           // correct

// 5. try/finally around await, and the return-in-finally trap
async function f() { try { return 'a'; } finally { return 'b'; } }   // resolves 'b'

Node 22 default for an unhandled rejection is --unhandled-rejections=throw, i.e. the process crashes. That is deliberate — a swallowed rejection is a silent data-corruption bug.

6.8 Cancellation and concurrency control

// AbortController is the standard cancellation channel; fetch, streams and timers honour it.
async function fetchWithTimeout(url, ms = 100) {
  const ac = new AbortController();
  const timer = setTimeout(() => ac.abort(new Error('timeout')), ms);
  try {
    const res = await fetch(url, { signal: ac.signal });
    return await res.json();
  } finally { clearTimeout(timer); }
}

// AbortSignal.timeout(ms) is the one-liner; AbortSignal.any([...]) combines signals.

A bounded-concurrency map, which is the “rate limit N parallel requests” question:

async function pMap(items, mapper, concurrency = 4) {
  const out = new Array(items.length);
  let i = 0, active = 0;
  await new Promise((resolve, reject) => {
    const next = () => {
      if (i >= items.length && active === 0) return resolve();
      while (active < concurrency && i < items.length) {
        const idx = i++; active++;
        Promise.resolve(mapper(items[idx], idx))
          .then(v => { out[idx] = v; active--; next(); }, reject);
      }
    };
    next();
  });
  return out;
}
results 0,1,4,9,16,25,36,49,64,81
peak concurrency 3 | elapsed ~ 200 ms (serial would be 500)

Follow-ups to be ready for: make it fail-soft (collect errors instead of rejecting), add retries with exponential backoff and jitter, preserve order (this does — it writes by index), and make it a stream so memory stays O(concurrency) instead of O(n).

AsyncLocalStorage (from node:async_hooks) is the Node answer to thread-local storage: it propagates a context object across await boundaries, which is how request-scoped tracing and logging work without threading a ctx parameter through every function.

Interview follow-ups

Q: Is JavaScript single-threaded?

A: The JavaScript execution is single-threaded per realm, but the runtime is not: libuv keeps a threadpool (default 4, UV_THREADPOOL_SIZE) for filesystem, DNS and crypto work, and you can add real threads with worker_threads. Network I/O uses the OS event notification mechanism, not the pool.

Q: Why is setTimeout(fn, 0) not zero?

A: HTML clamps nested timers to 4 ms after 5 levels; Node’s timer resolution means the minimum is 1 ms; and the callback still has to wait for the current synchronous execution plus the microtask drain plus the loop to reach the timers phase.

Q: What is the difference between process.nextTick and setImmediate?

A: Despite the names, nextTick fires before the loop continues (higher priority than promises) and setImmediate fires in the next loop turn’s check phase. Prefer setImmediate for “yield to the loop”; reserve nextTick for internal invariants because it can starve I/O.

Q: How do you make a blocking CPU task not freeze the server?

A: Move it off the loop: worker_threads for CPU-bound JS, a native addon or child process for heavy work, or chunk it and await new Promise(r => setImmediate(r)) between chunks to let I/O breathe.

Q: Can a microtask starve the event loop?

A: Yes. A microtask that enqueues another microtask unconditionally never lets the loop advance — no timers, no I/O. The same is true of recursive process.nextTick.

Q: What does await do to a non-promise?

A: It wraps it via Promise.resolve and still yields — so await 1 costs one microtask tick. That is why “8 after await” appears in the microtask group in Puzzle 1.


7. Memory and garbage collection

7.1 V8’s generational collector

Two spaces, two very different collectors — the generational hypothesis is that most objects die young.

SpaceCollectorMechanicsCost
Young generation (“new space”, a few MB, two semi-spaces)Scavenger (Cheney’s algorithm)allocate in from-space; on GC copy live objects to to-space, swap. Objects surviving two scavenges are promoted to old space.proportional to live data, so very cheap when most objects die
Old generationMark-Sweep-Compacttri-colour mark (white/grey/black) from the roots, sweep free lists, compact when fragmentedproportional to heap size

Modern V8 (“Orinoco”) makes the major GC mostly non-blocking: incremental marking interleaved with JS, concurrent marking and sweeping on helper threads, parallel scavenging, and lazy sweeping. What remains stop-the-world is short. Node flags worth knowing: --max-old-space-size=N (MB), --expose-gc (then global.gc()), --trace-gc.

Roots are the stack, the global object, and handles held by native code. Reachability, not reference counting, decides liveness — which is why cycles are collected in JS but need a cycle detector in CPython (see Python core).

7.2 Weak references

APIHoldsUse
WeakMap<object, V>key weaklyattach metadata/private state to objects you do not own; memo caches keyed by object
WeakSet<object>member weakly“have I seen this object” marks (e.g. cycle detection in a deep clone)
WeakRef<T>target weakly, deref() may return undefinedcaches you are willing to lose
FinalizationRegistrybest-effort cleanup notification; never guaranteed to run

WeakMap is not enumerable by design — being able to list the keys would expose GC timing.

7.3 The four leak archetypes

  1. Accidental globals / module-level accumulation. const cache = new Map() at module scope that never evicts is the single most common Node leak.
  2. Closures capturing more than they need. A closure keeps its whole scope alive; one long-lived callback can pin a large object graph. Extract just the field you need before closing over it.
  3. Timers and listeners never removed. setInterval keeps its closure alive forever; emitter.on without off grows the listener array (Node warns at 11 listeners).
  4. Detached views/nodes still referenced. In the browser, removing a DOM node while JS still holds a reference keeps the whole subtree alive.

Finding one:

node --expose-gc app.js          # exposes global.gc() so you can force a collection
// coarse: watch the trend, forcing GC between samples so you see retention, not garbage
setInterval(() => {
  global.gc();
  const { heapUsed, external, arrayBuffers } = process.memoryUsage();
  console.log((heapUsed / 1e6).toFixed(1), 'MB heap |', (external / 1e6).toFixed(1), 'MB external');
}, 5000);

Then take two heap snapshots (node --inspect, Chrome DevTools Memory tab, or require('v8').writeHeapSnapshot()) at points where memory should be equal, and use the Comparison view: the retaining path of the top delta tells you which closure or map is holding on. heapUsed growing while rss is flat means fragmentation, not a leak.

Interview follow-ups

Q: What exactly makes an object eligible for collection?

A: Unreachability from any root. Setting a variable to null only helps if it was the last strong reference.

Q: Does delete obj.prop free memory?

A: It removes the reference (so the value may become collectable) but it also transitions the object into dictionary mode, which is slower forever. Prefer obj.prop = undefined in hot objects.

Q: What is external memory in process.memoryUsage()?

A: C++ objects bound to JS objects — Buffers, ArrayBuffer backing stores, native addon memory. It is not part of the V8 heap limit, so a buffer leak shows up here, not in heapUsed.

Q: Why can FinalizationRegistry not be used for resource cleanup?

A: Callbacks are best-effort: they may never run (process exit, engine choice), may run late, and must not resurrect the object. Use try/finally, Symbol.dispose/using, or explicit close().


8. The V8 performance model

8.1 The compilation pipeline

source ──> scanner/parser ──> AST ──> Ignition (bytecode interpreter)
                                          │  hot?
                                          ├──> Sparkplug   (baseline, non-optimizing, near-instant compile)
                                          ├──> Maglev      (mid-tier optimizing, since V8 11.x)
                                          └──> TurboFan    (top-tier optimizing, speculative)

                                              deoptimize ──> back to Ignition when a speculation fails

Key ideas to be able to state:

  • Lazy parsing. Function bodies are pre-parsed only, and fully parsed on first call. Wrapping a function in parentheses ((function(){})) is the old “eager parse” hint; the modern equivalent is not worrying about it.
  • Speculative optimization. TurboFan compiles code specialised on the types it has observed. If a function that always saw integers suddenly gets a string, the optimized code is thrown away (deoptimization) and the function re-warms. A single polymorphic call site can cost you the whole optimization.
  • Optimization is per-function and feedback-driven; that is why microbenchmarks need warm-up runs and why the first measurement in a loop is worthless.

8.2 Hidden classes and inline caches

Every object has a hidden class (V8 calls it a Map) describing its shape: which properties exist, in what order, at what offset. Adding a property creates a transition to a new hidden class. Objects built the same way share a hidden class, and a property access site caches “if the shape is M, the value is at offset 3” — an inline cache.

IC stateShapes seenBehaviour
uninitialized0first run, records feedback
monomorphic1direct offset load — as fast as a C struct field
polymorphic2–4small linear scan of cached shapes
megamorphic5+falls back to a global hash lookup, and TurboFan largely gives up on the site

Measured on this container (Node 22.22.2), same read loop compiled at four separate call sites, 20,000 objects, 2,000 iterations:

monomorphic (1 shape)           77.6 ms
polymorphic (2 shapes)         116.0 ms
polymorphic (4 shapes)         116.1 ms
megamorphic (8 shapes)         332.5 ms

Monomorphic to megamorphic is a 4.3x slowdown on pure property reads (a repeat run gave 4.6x). The practical rules:

  • Initialize every field in the constructor, in the same order, even to null.
  • Do not add or delete properties after construction.
  • {x, y} and {y, x} are different hidden classes. Object literal key order matters.
  • Prefer a Map when keys are genuinely dynamic — a plain object used as a dictionary goes into dictionary (slow, hash-backed) mode anyway, and Map is designed for it.

A warning about measuring this. My first attempt at this benchmark showed no difference, because a shared counter++ in the loop body dominated the cost and the two cases shared one call site. Isolate the call sites (a fresh function per case), keep the loop body trivial, warm up, and sanity-check that the numbers move when you make the effect bigger. A benchmark that shows no difference is more often a broken benchmark than a disproved theory.

8.3 Elements kinds: packed vs holey

Arrays have their own lattice of representations, and transitions are one-way (you can never go back to a more specific kind):

PACKED_SMI  ──>  PACKED_DOUBLE  ──>  PACKED_ELEMENTS
     │                 │                    │
     v                 v                    v
HOLEY_SMI   ──>  HOLEY_DOUBLE   ──>  HOLEY_ELEMENTS  ──> DICTIONARY_ELEMENTS

SMI = small integer (31-bit tagged, no boxing). DOUBLE = unboxed floats. ELEMENTS = arbitrary tagged values. Holey means the array has gaps, so every read must also consult the prototype chain to be spec-correct — which is why it is slower.

PACKED_SMI                      35.4 ms
HOLEY_SMI (one delete)          49.7 ms
HOLEY_SMI (50% holes)           68.8 ms

One delete arr[10] on a 20,000-element array made the sum loop measurably slower — 15% to 40% depending on the run — and permanently, because the transition is one-way. (This is the noisiest measurement in this guide; the direction is completely consistent, the magnitude is not.) Things that create holes: new Array(n) without filling, delete, assigning past the end (a[a.length+1]=x), and arr.length = biggerNumber.

const good = new Array(n).fill(0);                 // PACKED_SMI
const alsoGood = Array.from({ length: n }, () => 0);
const bad = new Array(n);                          // HOLEY_SMI from birth

DICTIONARY_ELEMENTS happens when an array becomes very sparse (a = []; a[100000] = 1); it is a hash map wearing an array costume, and iteration collapses.

8.4 Strings

V8 has several string representations and concatenation does not copy:

RepresentationWhat it is
SeqStringflat contiguous characters (one-byte or two-byte)
ConsStringa rope node: left + right, built by + in O(1)
SlicedStringa view: parent + offset + length, built by substring/slice in O(1)
ThinString/internalizeddeduplicated, for identifiers and literals

A ConsString is flattened lazily when something needs the characters contiguously (a regex, a char access, passing to C++). So:

+= in loop                   37.1 ms len=400000
array push + join            34.8 ms len=400000

s += x in a loop is fine in V8 (it builds a rope), essentially tying push/join. This is the opposite of Python, where += on str is quadratic unless CPython’s in-place optimization applies — so the “always use join” advice is Python advice, not universal advice. What is expensive in JS is forcing flattening repeatedly, and str.split('').reverse().join('') which allocates an array of one-character strings (and is also wrong for non-BMP characters — use [...str]).

8.5 A checklist for hot JavaScript

  • Keep object shapes stable and call sites monomorphic.
  • Keep arrays packed and single-typed; preallocate with fill.
  • Avoid arguments, with, eval, and delete in hot paths.
  • Do not mix types in a numeric array ([1, 2, 'x'] collapses to PACKED_ELEMENTS).
  • Prefer for / for...of over forEach in the hottest loops (callback allocation and megamorphic dispatch), but measure — usually it does not matter.
  • Map/Set for dynamic keys, plain objects for fixed shapes.
  • Typed arrays (Float64Array, Int32Array) when you have numeric bulk data — no boxing, cache-friendly.
  • Measure with --prof + node --prof-process, --cpu-prof, or perf; read %OptimizeFunctionOnNextCall-style diagnostics only with --allow-natives-syntax.
  • node --trace-deopt and --trace-opt-verbose tell you which functions keep getting thrown away.

Interview follow-ups

Q: What is a hidden class transition and why should I care?

A: Adding a property moves the object to a new shape. Two “identical” objects built in different orders have different shapes, which turns a monomorphic call site polymorphic and costs you the optimized code path.

Q: Why is arr.includes(x) slower than set.has(x)?

A: O(n) linear scan versus O(1) average hash lookup. The crossover is small — usually a few dozen elements — so for tiny arrays the linear scan wins on cache locality.

Q: What is deoptimization and what triggers it?

A: Discarding TurboFan code and resuming in the interpreter, triggered when a speculation fails: an unexpected type, a hole in an array assumed packed, an object shape change, or arguments escaping.

Q: Is for...of slower than a for loop?

A: It allocates an iterator and a result object per step in principle; V8 escape-analyses most of that away for arrays. Treat them as equal until a profile says otherwise.

Q: How do you make numeric code fast in JS?

A: Typed arrays, avoid NaN/undefined sentinels in the data, keep arrays packed-double, use Math.fround if you want float32 semantics, and consider WebAssembly for genuinely heavy kernels.


9. Modules

9.1 CJS vs ESM

CommonJSES modules
Syntaxrequire / module.exportsimport / export
Resolutionsynchronous, runtime, node_modules walkasynchronous, spec algorithm, honours exports map
Bindingsvalue copy at require timelive bindings (a view on the exporter’s variable)
Evaluationon first require, cached in require.cachelinked first (all imports hoisted), then evaluated depth-first
this at top levelmodule.exportsundefined
Top-level awaitnoyes
Tree-shakeablenot staticallyyes (static structure)
Loading a .mjs from CJSawait import() only

9.2 Live bindings, demonstrated

// counter.mjs
export let count = 0;
export const inc = () => ++count;
// main.mjs
import { count, inc } from './counter.mjs';
inc(); inc();
console.log(count);      // 2  — the imported name tracks the exporter's variable
// count = 5;            // TypeError: Assignment to constant variable (imports are read-only)

The CJS equivalent (const { count } = require('./counter')) would print 0, because destructuring copied the number.

9.3 Circular imports

The behaviour differs, and it is a favourite question. Same two-module cycle, both systems:

[cjs] a: start
[cjs] b: start
[cjs] b: saw a.aVal = A-initial (partial export!)      <- b got a HALF-BUILT module object
[cjs] a: saw b.bVal = B-final

CJS returns the partially-populated module.exports to break the cycle. Code that reads a value at require time gets whatever existed at that moment — a real source of undefined is not a function.

[esm] b: start                                          <- depth-first: b is evaluated before a
[esm] b: reading aVal threw ReferenceError - TDZ via live binding
[esm] a: start
[esm] a: saw bVal = B-final

ESM hoists the bindings but leaves them in the temporal dead zone, so a premature read throws a loud ReferenceError instead of silently yielding undefined. Function declarations are hoisted and initialized, which is why cycles between modules that only call each other’s functions work fine in both systems.

9.4 Other module facts worth knowing

  • "type": "module" in package.json makes .js files ESM; .cjs/.mjs always win over it.
  • The exports field is the modern entry map and it blocks deep imports not listed in it.
  • The dual package hazard: shipping both CJS and ESM builds can load two copies of your module, each with its own module-level state (two Symbol registries, two singletons, instanceof failures).
  • import() returns a promise for the namespace object and works in both systems; it is how you do lazy loading and how CJS reaches ESM.
  • import.meta.url replaces __filename; import.meta.dirname/filename exist in Node 20.11+; import.meta.resolve() resolves specifiers.
  • Import attributes: import cfg from './c.json' with { type: 'json' }.

10. Node specifics

10.1 Streams and backpressure

Streams exist so you can process data larger than memory, and backpressure is the mechanism that stops a fast producer from overwhelming a slow consumer. write() returning false means “the internal buffer is above highWaterMark, wait for 'drain'”. pipe/pipeline handle this for you.

import { pipeline } from 'node:stream/promises';
import { createReadStream, createWriteStream } from 'node:fs';
import { createGzip } from 'node:zlib';
import { Transform } from 'node:stream';

const upper = new Transform({
  transform(chunk, _enc, cb) { cb(null, chunk.toString().toUpperCase()); },
});

await pipeline(createReadStream('in.txt'), upper, createGzip(), createWriteStream('out.gz'));

Use pipeline (not .pipe()) because it propagates errors and destroys every stream in the chain; a bare .pipe() leaks file descriptors on error. Async iteration works too: for await (const chunk of readable).

10.2 Parallelism options

ToolShares memoryStartupUse for
worker_threadsyes, via SharedArrayBuffer/MessageChannel (structured clone otherwise)~msCPU-bound JS inside one process
child_processno~10ms+running other programs, isolation
clusterno (shares the listening socket)process-levelscaling an HTTP server across cores
libuv threadpoolinternalfs, dns.lookup, crypto.pbkdf2, zlib — tune with UV_THREADPOOL_SIZE

Network I/O does not use the threadpool; it uses epoll/kqueue/IOCP. That is why thousands of idle sockets cost almost nothing but four concurrent fs.readFiles can queue.

10.3 Buffers and binary

Buffer is a Uint8Array subclass with encoding helpers. Buffer.allocUnsafe(n) skips zero-filling (faster, may expose old memory — always overwrite it fully). buf.subarray() shares memory; Buffer.from(buf) copies. For structured binary, DataView gives you explicit endianness.


11. Modern syntax you should be fluent in

// Optional chaining and nullish coalescing
const city = user?.address?.city ?? 'unknown';
const first = list?.[0];
const out = fn?.(arg);                 // only calls if fn is not null/undefined
opts.retries ??= 3;                    // logical assignment: ??=, ||=, &&=

// Destructuring: rename, default, nested, rest, in parameters
const { a: alpha = 1, b: { c } = {}, ...rest } = obj;
const [x, , z = 0, ...tail] = arr;
function f({ id, tags = [] } = {}) {}

// Array methods that do NOT mutate (ES2023) — great for state updates
arr.toSorted((a, b) => a - b);  arr.toReversed();  arr.with(2, 'new');  arr.toSpliced(1, 2);
arr.at(-1);                     // last element
arr.findLast(p);  arr.findLastIndex(p);
arr.flat(Infinity);  arr.flatMap(f);

// Grouping (ES2024)
Object.groupBy([1, 2, 3, 4, 5], n => (n % 2 ? 'odd' : 'even'));
// -> {"odd":[1,3,5],"even":[2,4]}
Map.groupBy(users, u => u.teamId);      // Map keyed by anything

// Set algebra (ES2025) — verified on Node 22
const a = new Set([1, 2, 3]), b = new Set([3, 4]);
[...a.union(b)];                 // [1,2,3,4]
[...a.intersection(b)];          // [3]
[...a.difference(b)];            // [1,2]
[...a.symmetricDifference(b)];   // [1,2,4]
a.isSubsetOf(b); a.isSupersetOf(b); a.isDisjointFrom(new Set([9]));   // false false true

// Explicit resource management (ES2026)
class Conn { [Symbol.dispose]() { console.log('closed'); } }
{ using c = new Conn(); /* ... */ }     // disposed at end of block, even on throw
// await using for [Symbol.asyncDispose]

// Structured deep clone, handles cycles, Map/Set/Date/RegExp/ArrayBuffer
const copy = structuredClone(graph);    // NOT functions, DOM nodes, or prototypes

// Misc
Object.hasOwn(obj, 'k');                // replaces hasOwnProperty.call
String.prototype.replaceAll;
Array.fromAsync(asyncIterable);
AbortSignal.timeout(1000); AbortSignal.any([s1, s2]);
label: for (const i of xs) { for (const j of ys) if (bad) continue label; }

12. Interview questions

12.1 Implement these cold

Implement map, filter, reduce.

Array.prototype.myMap = function (cb, thisArg) {
  const out = new Array(this.length);
  for (let i = 0; i < this.length; i++) if (i in this) out[i] = cb.call(thisArg, this[i], i, this);
  return out;
};
Array.prototype.myFilter = function (cb, thisArg) {
  const out = [];
  for (let i = 0; i < this.length; i++) if (i in this && cb.call(thisArg, this[i], i, this)) out.push(this[i]);
  return out;
};
Array.prototype.myReduce = function (cb, ...init) {
  let i = 0, acc;
  if (init.length) acc = init[0];
  else { while (i < this.length && !(i in this)) i++;
         if (i >= this.length) throw new TypeError('Reduce of empty array with no initial value');
         acc = this[i++]; }
  for (; i < this.length; i++) if (i in this) acc = cb(acc, this[i], i, this);
  return acc;
};

The i in this check (skipping holes) and the empty-array-without-initial-value TypeError are the details that distinguish a real answer from a sketch.

Deep clone with cycles.

function deepClone(value, seen = new Map()) {
  if (value === null || typeof value !== 'object') return value;
  if (seen.has(value)) return seen.get(value);              // handles cycles AND shared refs
  if (value instanceof Date) return new Date(value);
  if (value instanceof RegExp) return new RegExp(value.source, value.flags);
  if (value instanceof Map) {
    const m = new Map(); seen.set(value, m);
    for (const [k, v] of value) m.set(deepClone(k, seen), deepClone(v, seen));
    return m;
  }
  if (value instanceof Set) {
    const s = new Set(); seen.set(value, s);
    for (const v of value) s.add(deepClone(v, seen));
    return s;
  }
  if (ArrayBuffer.isView(value)) return new value.constructor(value);
  const out = Array.isArray(value) ? [] : Object.create(Object.getPrototypeOf(value));
  seen.set(value, out);
  for (const k of Reflect.ownKeys(value)) {
    const d = Object.getOwnPropertyDescriptor(value, k);
    if ('value' in d) d.value = deepClone(d.value, seen);
    Object.defineProperty(out, k, d);
  }
  return out;
}

Say out loud: structuredClone does most of this natively now; hand-roll it only when you need to handle functions, prototypes, or custom classes.

Deep equal.

function deepEqual(a, b, seen = new Set()) {
  if (Object.is(a, b)) return true;
  if (typeof a !== 'object' || typeof b !== 'object' || a === null || b === null) return false;
  if (Object.getPrototypeOf(a) !== Object.getPrototypeOf(b)) return false;
  const pair = a; if (seen.has(pair)) return true; seen.add(pair);
  if (a instanceof Date) return a.getTime() === b.getTime();
  if (a instanceof RegExp) return a.source === b.source && a.flags === b.flags;
  if (a instanceof Map) return a.size === b.size && [...a].every(([k, v]) => b.has(k) && deepEqual(v, b.get(k), seen));
  if (a instanceof Set) return a.size === b.size && [...a].every(v => b.has(v));
  const ka = Reflect.ownKeys(a), kb = Reflect.ownKeys(b);
  return ka.length === kb.length && ka.every(k => Reflect.has(b, k) && deepEqual(a[k], b[k], seen));
}

Flatten with depth, iteratively.

function flatten(arr, depth = 1) {
  const out = [], stack = arr.map(v => [v, depth]);
  while (stack.length) {
    const [v, d] = stack.shift();
    if (Array.isArray(v) && d > 0) stack.unshift(...v.map(x => [x, d - 1]));
    else out.push(v);
  }
  return out;
}
// Note: shift/unshift make this O(n^2); the O(n) version pushes and reverses, or recurses.

Event emitter.

class Emitter {
  #m = new Map();
  on(ev, fn) { (this.#m.get(ev) ?? this.#m.set(ev, new Set()).get(ev)).add(fn); return () => this.off(ev, fn); }
  once(ev, fn) { const w = (...a) => { this.off(ev, w); fn(...a); }; return this.on(ev, w); }
  off(ev, fn) { this.#m.get(ev)?.delete(fn); }
  emit(ev, ...args) {
    // copy first: a handler may add/remove handlers during emit
    for (const fn of [...(this.#m.get(ev) ?? [])]) { try { fn(...args); } catch (e) { queueMicrotask(() => { throw e; }); } }
    return (this.#m.get(ev)?.size ?? 0) > 0;
  }
}

Retry with exponential backoff and jitter.

async function retry(fn, { attempts = 5, base = 100, cap = 5000, signal } = {}) {
  let lastErr;
  for (let i = 0; i < attempts; i++) {
    try { return await fn(i); } catch (e) {
      lastErr = e;
      if (signal?.aborted || i === attempts - 1) break;
      const exp = Math.min(cap, base * 2 ** i);
      const wait = Math.random() * exp;                  // full jitter
      await new Promise(r => setTimeout(r, wait));
    }
  }
  throw lastErr;
}

Promisify a callback API.

const promisify = fn => (...args) =>
  new Promise((res, rej) => fn(...args, (err, ...vals) => err ? rej(err) : res(vals.length > 1 ? vals : vals[0])));

once, chunk, groupBy, range.

const once = fn => { let done = false, val; return (...a) => (done ? val : (done = true, val = fn(...a))); };
const chunk = (a, n) => Array.from({ length: Math.ceil(a.length / n) }, (_, i) => a.slice(i * n, i * n + n));
const groupBy = (a, f) => a.reduce((m, v) => ((m[f(v)] ??= []).push(v), m), {});
const range = (n, start = 0, step = 1) => Array.from({ length: n }, (_, i) => start + i * step);

12.2 Rapid-fire Q&A

Q: null vs undefined?

A: undefined is “not assigned” (the engine produces it); null is “intentionally empty” (you produce it). typeof null === 'object' is a famous bug. null == undefined is true, null === undefined is false. JSON has null but not undefined.

Q: What is hoisting exactly?

A: Declarations are processed when the scope is entered. var is initialized to undefined; function declarations are fully initialized; let/const/class are created but uninitialized (TDZ), so touching them throws.

Q: Why does typeof NaN === 'number'?

A: NaN is an IEEE-754 double whose exponent is all ones and mantissa non-zero. It is a number that is not a numeric value.

Q: == vs === vs Object.is?

A: === is SameValueZero-minus-zero-handling: strict, no coercion, but NaN !== NaN and +0 === -0. Object.is fixes both. == coerces per the abstract equality algorithm.

Q: What is a closure, in one sentence, without the word “closure”?

A: A function together with the variable environment it was created in, which stays alive as long as the function does.

Q: Explain event delegation.

A: Attach one listener to a common ancestor and use event.target/closest() to find which descendant was hit. Fewer listeners, works for nodes added later, relies on bubbling.

Q: Capturing vs bubbling?

A: Dispatch goes capture (window -> target), then target, then bubble (target -> window). addEventListener(ev, fn, true) or {capture: true} listens on the way down. stopPropagation halts traversal; stopImmediatePropagation also skips other listeners on the same node.

Q: What is a Proxy good for?

A: Intercepting fundamental operations (get, set, has, deleteProperty, ownKeys, apply, construct). Real uses: reactivity systems (Vue 3), negative array indices, validation, lazy remote objects, immutability enforcement. Cost: every trapped access is a function call, and it defeats inline caches.

Q: Reflect — why does it exist?

A: It gives function forms of the internal operations that proxy traps mirror, with better return values than the old equivalents (Reflect.defineProperty returns a boolean instead of throwing) and correct receiver forwarding for inherited accessors.

Q: Explain currying vs partial application.

A: Currying turns an n-ary function into n unary functions; partial application fixes some arguments and returns a function of the rest. Currying is a special case of the general idea.

Q: What is the difference between Array.from and spread?

A: Spread requires an iterable. Array.from accepts array-likes too ({length: 3}) and takes a map function, which lets you avoid an intermediate array: Array.from({length: n}, (_, i) => i).

Q: Why is [10, 9, 1].sort() [1, 10, 9]?

A: The default comparator converts to strings and compares UTF-16 code units. Always pass (a, b) => a - b for numbers.

Q: Is sort stable?

A: Yes, guaranteed since ES2019 and implemented as TimSort in V8. TypedArray.prototype.sort is a separate, numerically-ordered path.

Q: What does JSON.parse(JSON.stringify(x)) lose?

A: undefined, functions, symbols, Map/Set contents, Date type (becomes a string), Infinity/NaN (become null), prototypes, cycles (throws), and BigInt (throws).

Q: Object.keys vs for...in vs Object.getOwnPropertyNames vs Reflect.ownKeys?

A: Own enumerable string keys / inherited and own enumerable string keys / own string keys including non-enumerable / everything own, strings and symbols.

Q: What is the temporal dead zone for?

A: It turns “used before initialized” from a silent undefined into a loud error, and it is what makes const meaningfully constant rather than “assigned later”.

Q: Debounce vs throttle in one line each?

A: Debounce: wait for quiet. Throttle: at most once per interval.

Q: How do you deep-freeze an object?

A: Recurse over Object.getOwnPropertyNames, freeze each object-valued property, guard against cycles with a WeakSet, then freeze the root.

Q: What is the difference between Map and a plain object?

A: Map accepts any key type, preserves insertion order for all keys, has size, is iterable, has no prototype keys to collide with, and is optimized for frequent add/delete. Objects are faster for small fixed shapes and are what JSON gives you.

Q: What happens if you await inside a for loop over 1000 URLs?

A: 1000 sequential round trips. Batch with Promise.all, or bound it with a concurrency pool — unbounded Promise.all over 1000 fetches will hit socket limits and blow memory.

Q: What is the output of console.log(typeof function(){}, typeof class{}, typeof Symbol(), typeof 10n)?

A: function function symbol bigint. class is a function; typeof has no separate class tag.

Q: let in a for loop — how many bindings?

A: One per iteration, which is why closures created inside capture distinct values. var creates one binding for the whole loop.

Q: How would you detect a memory leak in a Node service?

A: Trend process.memoryUsage().heapUsed over time with GC forced between samples, then diff two heap snapshots taken at equivalent points and inspect the retaining path of the largest delta.

Q: What is globalThis for?

A: One name for the global object across browsers (window), workers (self), and Node (global).

Q: Why avoid eval and with?

A: They make the scope dynamic, so the engine cannot resolve variables statically — it disables optimization for the whole enclosing function, and eval on untrusted input is code injection.

Q: What is tree shaking and what breaks it?

A: Static dead-code elimination over ESM. Breakers: CJS require, dynamic property access on a namespace, and side effects at module top level (mark packages "sideEffects": false when true).

Q: Explain Symbol in one paragraph.

A: A unique, non-string primitive usable as a property key that cannot collide and is skipped by Object.keys/JSON.stringify. Well-known symbols (Symbol.iterator, asyncIterator, toPrimitive, hasInstance, toStringTag, dispose) are the protocol hooks. Symbol.for uses a cross-realm registry.

Q: What does void 0 mean and why is it used?

A: It evaluates its operand and returns undefined. Historically used because undefined was writable in ES3; now mostly seen in minified output and void asyncFn() to mark a deliberately un-awaited promise.

Q: Explain Array.prototype.reduce’s type-level pain in TypeScript.

A: The accumulator type is inferred from the initial value, so [].reduce((a, b) => ..., {}) widens to {} and then rejects your writes. Annotate the initial value or pass a type argument: reduce<Record<string, number>>(...).

Q: What is the difference between shallow and deep equality in React-style code?

A: Shallow compares one level of keys by reference — cheap, and why you must produce new objects on state change. Deep equality is O(size) and hides accidental re-creation, which is why hooks use reference identity.


13. Feature availability on Node 22

Feature-detected on this container (Node 22.22.2). Useful because “available in ES2026” and “available in your runtime” are different questions, and an interviewer may run your code.

FeatureNode 22
Iterator helpers (.map/.filter/.take/... on iterators)yes
Set methods (union, intersection, difference, …)yes
Object.groupBy / Map.groupByyes
Array.fromAsyncyes
Symbol.dispose / usingyes (symbol present; using syntax needs a recent V8 or transpiler)
Array.prototype.toSorted/toReversed/with/toSplicedyes
Object.hasOwn, structuredClone, AbortSignal.timeoutyes
Promise.tryno
RegExp.escapeno
Error.isErrorno
Float16Array / Math.f16roundno
Math.sumPreciseno
Temporalno (behind a flag / polyfill)
Map.prototype.getOrInsertno
Uint8Array.prototype.toBase64no

Sources for the runtime and spec claims are listed in the README.

Verify it yourself

js-core/01-numbers.js

// IEEE-754 doubles and integer safety
console.log('0.1 + 0.2                 =', 0.1 + 0.2);
console.log('0.1 + 0.2 === 0.3         =', 0.1 + 0.2 === 0.3);
console.log('(0.1+0.2).toFixed(20)     =', (0.1 + 0.2).toFixed(20));
console.log('Number.EPSILON            =', Number.EPSILON);
console.log('nearlyEqual(0.1+0.2, 0.3) =', Math.abs(0.1 + 0.2 - 0.3) < Number.EPSILON);

console.log('MAX_SAFE_INTEGER          =', Number.MAX_SAFE_INTEGER);
console.log('2**53                     =', 2 ** 53);
console.log('2**53 + 1                 =', 2 ** 53 + 1);
console.log('2**53 === 2**53 + 1       =', 2 ** 53 === 2 ** 53 + 1);
console.log('isSafeInteger(2**53)      =', Number.isSafeInteger(2 ** 53));

console.log('MAX_VALUE                 =', Number.MAX_VALUE);
console.log('MAX_VALUE * 2             =', Number.MAX_VALUE * 2);
console.log('MIN_VALUE (denormal)      =', Number.MIN_VALUE);
console.log('0.1 + 0.7                 =', 0.1 + 0.7);
console.log('(0.1+0.7)*10              =', (0.1 + 0.7) * 10);
console.log('Math.floor((0.1+0.7)*10)  =', Math.floor((0.1 + 0.7) * 10));

// BigInt
const big = 2n ** 53n;
console.log('BigInt 2n**53n + 1n       =', big + 1n);
console.log('typeof 1n                 =', typeof 1n);
console.log('1n == 1                   =', 1n == 1);
console.log('1n === 1                  =', 1n === 1);
try { console.log(1n + 1); } catch (e) { console.log('1n + 1 throws              =', e.constructor.name + ': ' + e.message); }
console.log('BigInt(Number.MAX_SAFE_INTEGER) * 2n =', BigInt(Number.MAX_SAFE_INTEGER) * 2n);

// -0
console.log('-0 === 0                  =', -0 === 0);
console.log('Object.is(-0, 0)          =', Object.is(-0, 0));
console.log('1 / -0                    =', 1 / -0);
console.log('(-0).toString()           =', (-0).toString());
console.log('String(-0)                =', String(-0));
console.log('JSON.stringify(-0)        =', JSON.stringify(-0));
console.log('[-0].includes(0)          =', [-0].includes(0));   // SameValueZero -> true
console.log('[-0].indexOf(0)           =', [-0].indexOf(0));    // strict equality -> 0
console.log('Math.round(-0.4)          =', Math.round(-0.4));
console.log('Math.sign(-0)             =', Math.sign(-0));

// NaN
console.log('NaN === NaN               =', NaN === NaN);
console.log('Object.is(NaN, NaN)       =', Object.is(NaN, NaN));
console.log('[NaN].includes(NaN)       =', [NaN].includes(NaN));
console.log('[NaN].indexOf(NaN)        =', [NaN].indexOf(NaN));
console.log('new Set([NaN, NaN]).size  =', new Set([NaN, NaN]).size);
console.log('typeof NaN                =', typeof NaN);
console.log('isNaN("foo")              =', isNaN('foo'));
console.log('Number.isNaN("foo")       =', Number.isNaN('foo'));
console.log('Math.max()                =', Math.max());
console.log('Math.min()                =', Math.min());

// Precision helpers
console.log('Math.sumPrecise([0.1,0.2])=', Math.sumPrecise ? Math.sumPrecise([0.1, 0.2]) : 'unavailable');
console.log('0.1+0.2 rounded to 2dp    =', Math.round((0.1 + 0.2) * 100) / 100);

js-core/02-equality.js

// Abstract equality surprises, laid out as a table
const cases = [
  ['[] == ![]', () => [] == ![]],
  ['[] == false', () => [] == false],
  ['[] == ""', () => [] == ''],
  ['[] == 0', () => [] == 0],
  ['[[]] == 0', () => [[]] == 0],
  ['[0] == false', () => [0] == false],
  ['[1] == true', () => [1] == true],
  ['[2] == true', () => [2] == true],
  ['null == undefined', () => null == undefined],
  ['null === undefined', () => null === undefined],
  ['null == 0', () => null == 0],
  ['null >= 0', () => null >= 0],
  ['null > 0', () => null > 0],
  ['undefined == 0', () => undefined == 0],
  ['NaN == NaN', () => NaN == NaN],
  ['"" == 0', () => '' == 0],
  ['"0" == 0', () => '0' == 0],
  ['"" == "0"', () => '' == '0'],
  ['" \\t\\n" == 0', () => ' \t\n' == 0],
  ['false == "false"', () => false == 'false'],
  ['false == "0"', () => false == '0'],
  ['true == "1"', () => true == '1'],
  ['null == false', () => null == false],
  ['undefined == false', () => undefined == false],
  ['{} == "[object Object]"', () => ({}) == '[object Object]'],
  ['new String("a") == "a"', () => new String('a') == 'a'],
  ['new String("a") === "a"', () => new String('a') === 'a'],
  ['Symbol() == Symbol()', () => Symbol() == Symbol()],
  ['1n == 1', () => 1n == 1],
  ['document?"":"" ', () => true],
];
const pad = (s, n) => String(s).padEnd(n);
console.log(pad('expression', 26) + '| result');
console.log('-'.repeat(26) + '+' + '-'.repeat(8));
for (const [label, fn] of cases.slice(0, 29)) {
  console.log(pad(label, 26) + '| ' + fn());
}

// Non-transitivity of ==
console.log('\nNon-transitive:');
console.log('  "" == 0        ->', '' == 0);
console.log('  0 == "0"       ->', 0 == '0');
console.log('  "" == "0"      ->', '' == '0');

// ToPrimitive walkthrough
console.log('\nToPrimitive trace for `obj + ""` with default hint:');
const traced = {
  [Symbol.toPrimitive](hint) {
    console.log('  Symbol.toPrimitive called with hint:', hint);
    return hint === 'number' ? 42 : 'forty-two';
  },
};
console.log('  +traced      ->', +traced);
console.log('  `${traced}`  ->', `${traced}`);
console.log('  traced + ""  ->', traced + '');
console.log('  traced * 2   ->', traced * 2);
console.log('  String(traced)->', String(traced));
console.log('  traced == 42 ->', traced == 42);

console.log('\nWithout Symbol.toPrimitive, valueOf then toString (default/number hint):');
const vt = {
  valueOf() { console.log('  valueOf'); return 7; },
  toString() { console.log('  toString'); return 'seven'; },
};
console.log('  vt + 1   ->', vt + 1);
console.log('  `${vt}`  ->', `${vt}`);
console.log('  vt * 2   ->', vt * 2);
console.log('  [vt] + ""->', [vt] + '');

// Date is the special case: default hint behaves like string
const d = new Date(0);
console.log('\nDate default hint prefers string:');
console.log('  d + 1  (string concat) ->', typeof (d + 1));
console.log('  d - 1  (numeric)       ->', d - 1);

// Array.prototype.join is why [] == ""
console.log('\n[].toString() ->', JSON.stringify([].toString()));
console.log('[1,2].toString() ->', JSON.stringify([1, 2].toString()));
console.log('[null, undefined].toString() ->', JSON.stringify([null, undefined].toString()));
console.log('[[1,[2]],3].toString() ->', JSON.stringify([[1, [2]], 3].toString()));

// Object.is vs === vs ==
const rows = [
  ['0, -0'],
  ['NaN, NaN'],
  ['1, "1"'],
  ['null, undefined'],
];
const vals = [[0, -0], [NaN, NaN], [1, '1'], [null, undefined]];
console.log('\n' + pad('a, b', 18) + '| ==    | ===   | Object.is');
console.log('-'.repeat(18) + '+-------+-------+----------');
vals.forEach(([a, b], i) => {
  console.log(
    pad(rows[i][0], 18) + '| ' + pad(a == b, 6) + '| ' + pad(a === b, 6) + '| ' + Object.is(a, b)
  );
});

// SameValueZero (Map/Set/includes) vs SameValue (Object.is)
const m = new Map();
m.set(NaN, 'nan').set(-0, 'minus zero');
console.log('\nMap keyed by NaN  ->', m.get(NaN));
console.log('Map.get(0) after set(-0) ->', m.get(0), '(SameValueZero collapses -0 and 0)');
console.log('Map key stored as ->', Object.is([...m.keys()][1], 0) ? '+0 (normalized)' : '-0');

js-core/03-scope.js

'use strict';
// --- hoisting ---
console.log('typeof hoistedFn :', typeof hoistedFn);  // 'function'
console.log('varX before decl :', typeof varX, varX); // undefined
try { letY; } catch (e) { console.log('letY before decl :', e.constructor.name + ': ' + e.message); }
function hoistedFn() {}
var varX = 1;
let letY = 2;

// TDZ is per-binding, not per-file
{
  try {
    // eslint-disable-next-line no-use-before-define
    console.log(shadow);
  } catch (e) {
    console.log('TDZ inside block  :', e.constructor.name);
  }
  let shadow = 'inner';
  console.log('after decl        :', shadow);
}

// typeof is NOT safe for TDZ bindings (it is safe for undeclared ones)
console.log('typeof neverDeclared :', typeof neverDeclared);
try { typeof tdzVar; } catch (e) { console.log('typeof tdzVar        :', e.constructor.name); }
let tdzVar = 1;

// const is binding-immutable, not value-immutable
const cfg = { a: 1 };
cfg.a = 2;
console.log('const object mutated :', cfg);
try { eval('cfg = {}'); } catch (e) { console.log('reassign const       :', e.constructor.name); }
console.log('frozen               :', Object.isFrozen(Object.freeze(cfg)));
try { cfg.a = 99; } catch (e) { console.log('write to frozen      :', e.constructor.name, '(strict); silent no-op in sloppy'); }
console.log('after frozen write   :', cfg);

// --- the classic loop bug ---
console.log('\nvar loop (bug):');
const varFns = [];
for (var i = 0; i < 3; i++) varFns.push(() => i);
console.log('  ', varFns.map((f) => f()));

console.log('let loop (fix 1 - per-iteration binding):');
const letFns = [];
for (let j = 0; j < 3; j++) letFns.push(() => j);
console.log('  ', letFns.map((f) => f()));

console.log('IIFE (fix 2):');
const iifeFns = [];
for (var k = 0; k < 3; k++) iifeFns.push(((captured) => () => captured)(k));
console.log('  ', iifeFns.map((f) => f()));

console.log('bind (fix 3):');
const bindFns = [];
for (var m = 0; m < 3; m++) bindFns.push(((x) => x).bind(null, m));
console.log('  ', bindFns.map((f) => f()));

// let in a for loop copies the binding forward each iteration
const seen = [];
for (let n = 0; n < 3; n++) { setTimeout(() => seen.push(n), 0); }
setTimeout(() => console.log('let + setTimeout    :', seen), 1);

// closure over a shared cell
function counter() {
  let count = 0;
  return { inc: () => ++count, get: () => count };
}
const c1 = counter(), c2 = counter();
c1.inc(); c1.inc(); c2.inc();
console.log('\nindependent closures:', c1.get(), c2.get());

// function declarations in blocks (Annex B / strict differences)
{
  function blockFn() { return 'block'; }
  console.log('blockFn in block    :', blockFn());
}
console.log('blockFn after block :', typeof blockFn); // 'function' in sloppy web-compat, 'undefined' in strict module

// arguments vs rest, and why leaking arguments hurts
function sum() { return Array.prototype.reduce.call(arguments, (a, b) => a + b, 0); }
function sumRest(...xs) { return xs.reduce((a, b) => a + b, 0); }
console.log('sum(1,2,3)          :', sum(1, 2, 3), '| sumRest(1,2,3):', sumRest(1, 2, 3));

// function .length ignores rest and defaults-and-after
function f1(a, b) {}
function f2(a, b = 1, c) {}
function f3(a, ...rest) {}
function f4({ a, b }, [c]) {}
console.log('lengths             :', f1.length, f2.length, f3.length, f4.length);

js-core/04-this.js

'use strict';
// 1. Default binding
function showThis() { return this; }
console.log('strict default this   :', showThis());          // undefined
const sloppy = new Function('return this;');                  // compiled as sloppy
console.log('sloppy default this   :', sloppy() === globalThis ? 'globalThis' : sloppy());

// 2. Implicit binding — the call site decides
const obj = { name: 'obj', who() { return this?.name; } };
console.log('obj.who()             :', obj.who());
const detached = obj.who;
console.log('detached()            :', detached());          // undefined (strict)

// implicit binding is lost through any intermediate
const wrapper = { name: 'wrapper', inner: obj };
console.log('wrapper.inner.who()   :', wrapper.inner.who()); // "obj" - only the LAST . matters
console.log('(0, obj.who)()        :', (0, obj.who)());       // undefined - comma op strips the reference

// 3. Explicit binding
console.log('call/apply/bind       :', obj.who.call({ name: 'call' }),
  obj.who.apply({ name: 'apply' }), obj.who.bind({ name: 'bind' })());

// hard binding wins over later rebinding
const hard = obj.who.bind({ name: 'first' });
console.log('rebind a bound fn     :', hard.call({ name: 'second' }));  // "first"

// 4. new binding
function Person(name) { this.name = name; }
Person.prototype.who = function () { return this.name; };
console.log('new binding           :', new Person('newed').who());

// new beats bind (bound this is ignored, bound args are kept)
const BoundPerson = Person.bind({ name: 'ignored' });
console.log('new on bound ctor     :', new BoundPerson('still-works').name);

// 5. Arrow functions: lexical this, no own arguments/new.target, not constructible
const lex = {
  name: 'lex',
  method() { return (() => this.name)(); },
  // module-scope `this` is module.exports ({}) in CJS, undefined in ESM
  arrow: () => (typeof this === 'undefined' ? 'undefined (ESM module scope)' : 'module.exports = ' + JSON.stringify(this)),
};
console.log('arrow in method       :', lex.method());
console.log('arrow as method       :', lex.arrow());
try { new (() => {})(); } catch (e) { console.log('new Arrow             :', e.constructor.name); }

// 6. Class fields are per-instance and capture `this` when initialised with an arrow
class Widget {
  label = 'widget';
  onClickMethod() { return this?.label; }
  onClickField = () => this.label;      // bound at construction, one closure per instance
  static kind = 'Widget';
  static { this.registry = new Map(); } // static initialisation block (ES2022)
}
const w = new Widget();
const { onClickMethod, onClickField } = w;
console.log('detached method       :', (() => { try { return onClickMethod(); } catch (e) { return e.constructor.name; } })());
console.log('detached field arrow  :', onClickField());
console.log('static block ran      :', Widget.registry instanceof Map, '| static field:', Widget.kind);

// 7. this in class bodies is the class itself for statics
class Meta { static self() { return this === Meta; } }
console.log('static this === class :', Meta.self());

// 8. new.target
function Guard() {
  if (new.target === undefined) return 'called without new';
  return 'constructed as ' + new.target.name;
}
console.log('Guard()               :', Guard());
console.log('new Guard()           :', new Guard() instanceof Guard ? 'object' : Guard.call({}));
class Base { constructor() { this.builtBy = new.target.name; } }
class Derived extends Base {}
console.log('new.target in super   :', new Derived().builtBy);

// 9. Losing this in callbacks and the three fixes
class Timer {
  ticks = 0;
  incBroken() { this.ticks++; }
  incArrow = () => { this.ticks++; };
}
const t = new Timer();
const results = [];
try { [1].forEach(t.incBroken); } catch (e) { results.push('forEach(t.incBroken) -> ' + e.constructor.name); }
[1].forEach(t.incBroken.bind(t)); results.push('bind        -> ticks=' + t.ticks);
[1].forEach(() => t.incBroken()); results.push('arrow wrap  -> ticks=' + t.ticks);
[1].forEach(t.incArrow);          results.push('field arrow -> ticks=' + t.ticks);
[1].forEach(t.incBroken, t);      results.push('thisArg     -> ticks=' + t.ticks);
console.log('\n' + results.join('\n'));

// 10. this inside a getter and inside a Proxy trap
const g = { _v: 5, get v() { return this._v; } };
console.log('\ngetter this           :', g.v, Object.getOwnPropertyDescriptor(g, 'v').get.call({ _v: 9 }));

js-core/05-prototypes.js

'use strict';
// --- property descriptors ---
const o = {};
Object.defineProperty(o, 'hidden', { value: 1 });                       // all flags default false
Object.defineProperty(o, 'shown', { value: 2, enumerable: true, writable: true, configurable: true });
console.log('descriptors:', Object.getOwnPropertyDescriptors(o));
console.log('Object.keys :', Object.keys(o));
console.log('for..in     :', (() => { const r = []; for (const k in o) r.push(k); return r; })());
console.log('getOwnPropertyNames:', Object.getOwnPropertyNames(o));
console.log('JSON        :', JSON.stringify(o));
try { o.hidden = 9; } catch (e) { console.log('write non-writable:', e.constructor.name); }

// accessor descriptor
const temp = {
  _c: 0,
  get f() { return this._c * 9 / 5 + 32; },
  set f(v) { this._c = (v - 32) * 5 / 9; },
};
temp.f = 212;
console.log('\ngetter/setter: _c =', temp._c, ' f =', temp.f);
console.log('descriptor of f:', Object.getOwnPropertyDescriptor(temp, 'f'));

// defineProperty vs assignment: assignment consults setters up the chain
const proto = { set x(v) { this._x = v * 2; } };
const child = Object.create(proto);
child.x = 5;
console.log('\nassignment hits proto setter -> _x =', child._x, '| own x?', Object.hasOwn(child, 'x'));
Object.defineProperty(child, 'x', { value: 5, enumerable: true, writable: true, configurable: true });
console.log('defineProperty bypasses setter -> own x =', child.x, '| own?', Object.hasOwn(child, 'x'));

// --- prototype chain resolution ---
const grand = { greet() { return 'grand'; }, tag: 'G' };
const mid = Object.create(grand, { greet: { value() { return 'mid'; }, enumerable: false } });
const leaf = Object.create(mid);
console.log('\nleaf.greet()  :', leaf.greet());
console.log('leaf.tag      :', leaf.tag, '(found 2 links up)');
console.log('chain         :',
  [leaf, Object.getPrototypeOf(leaf), Object.getPrototypeOf(mid), Object.getPrototypeOf(grand),
    Object.getPrototypeOf(Object.prototype)]
    .map((p) => (p === null ? 'null' : p === grand ? 'grand' : p === mid ? 'mid' : p === Object.prototype ? 'Object.prototype' : 'leaf'))
    .join(' -> '));

// shadowing: writing to leaf creates an own property, it does not change grand
leaf.tag = 'L';
console.log('after shadow  : leaf.tag =', leaf.tag, ', grand.tag =', grand.tag);

// null-prototype objects (safe dictionaries)
const dict = Object.create(null);
dict.toString = 'not a function, and that is fine';
console.log('\nnull-proto dict:', dict.toString, '| has hasOwnProperty?', 'hasOwnProperty' in dict);
console.log('Object.hasOwn still works:', Object.hasOwn(dict, 'toString'));

// __proto__ vs prototype vs getPrototypeOf
function Ctor() {}
const inst = new Ctor();
console.log('\nCtor.prototype === Object.getPrototypeOf(inst) :', Ctor.prototype === Object.getPrototypeOf(inst));
console.log('inst.prototype                                 :', inst.prototype);
console.log('Object.getPrototypeOf(Ctor) === Function.prototype:', Object.getPrototypeOf(Ctor) === Function.prototype);
console.log('Ctor.prototype.constructor === Ctor            :', Ctor.prototype.constructor === Ctor);
console.log('"__proto__" own on {}?                          :', Object.hasOwn({}, '__proto__'),
  '(it is an accessor on Object.prototype)');

// instanceof and Symbol.hasInstance
class Even { static [Symbol.hasInstance](n) { return typeof n === 'number' && n % 2 === 0; } }
console.log('\n4 instanceof Even :', 4 instanceof Even, '| 5 instanceof Even :', 5 instanceof Even);
function Legacy() {}
const legacyInst = new Legacy();
Legacy.prototype = {};                       // rebinding prototype breaks old instances
console.log('legacyInst instanceof Legacy after prototype swap :', legacyInst instanceof Legacy);
console.log('[] instanceof Array :', [] instanceof Array, '| Array.isArray([]) :', Array.isArray([]));

// isPrototypeOf / in / hasOwn
console.log('\ngrand.isPrototypeOf(leaf) :', grand.isPrototypeOf(leaf));
console.log('"greet" in leaf           :', 'greet' in leaf, '| Object.hasOwn(leaf,"greet") :', Object.hasOwn(leaf, 'greet'));

js-core/06-classes.js

'use strict';
// ES2022+ class with every modern feature
class Animal {
  static #count = 0;
  static registry = new Map();
  static { Animal.registry.set('init', true); }   // static initialisation block

  #secret;                                         // private field
  name;

  constructor(name) {
    this.name = name;
    this.#secret = `${name}-${++Animal.#count}`;
  }

  static get count() { return Animal.#count; }
  static isAnimal(o) { return #secret in o; }      // ergonomic brand check (ES2022)

  get id() { return this.#secret; }
  #privateMethod() { return 'private:' + this.name; }
  callPrivate() { return this.#privateMethod(); }
  speak() { return `${this.name} makes a noise`; }
  toString() { return `Animal(${this.name})`; }
}

class Dog extends Animal {
  constructor(name) { super(name); this.kind = 'dog'; }
  speak() { return `${super.speak()} — specifically a bark`; }
}

const d = new Dog('Rex');
console.log('speak      :', d.speak());
console.log('id         :', d.id, '| count:', Animal.count);
console.log('brand check:', Animal.isAnimal(d), Animal.isAnimal({}));
console.log('callPrivate:', d.callPrivate());
try { JSON.parse('{}'); console.log('private access from outside:', eval('d.#secret')); }
catch (e) { console.log('outside access:', e.constructor.name); }
console.log('instanceof :', d instanceof Dog, d instanceof Animal);
console.log('proto chain:', [Dog.prototype, Animal.prototype].map((p) => p.constructor.name).join(' -> '));
console.log('static inherited:', Dog.count, '| Object.getPrototypeOf(Dog) === Animal:', Object.getPrototypeOf(Dog) === Animal);

// classes are not hoisted-callable and are always strict
try { new (class { constructor() { undeclared = 1; } })(); }
catch (e) { console.log('class body is strict:', e.constructor.name); }
try { Animal('x'); } catch (e) { console.log('call class w/o new  :', e.constructor.name); }
console.log('class methods enumerable? :',
  Object.getOwnPropertyDescriptor(Animal.prototype, 'speak').enumerable);

// --- the ES5 equivalent of `class Dog extends Animal` ---
function AnimalES5(name) {
  if (!(this instanceof AnimalES5)) throw new TypeError("Class constructor cannot be invoked without 'new'");
  this.name = name;
}
AnimalES5.prototype.speak = function () { return this.name + ' makes a noise'; };
Object.defineProperty(AnimalES5.prototype, 'speak', { enumerable: false });

function DogES5(name) {
  AnimalES5.call(this, name);       // == super(name)
  this.kind = 'dog';
}
DogES5.prototype = Object.create(AnimalES5.prototype, {
  constructor: { value: DogES5, writable: true, configurable: true, enumerable: false },
});
Object.setPrototypeOf(DogES5, AnimalES5);   // == static inheritance
DogES5.prototype.speak = function () {
  return AnimalES5.prototype.speak.call(this) + ' — specifically a bark';
};
const d5 = new DogES5('Rex5');
console.log('\nES5 equivalent :', d5.speak(), '| instanceof AnimalES5:', d5 instanceof AnimalES5);

// --- why `super` breaks with an assigned function ---
const base = { hello() { return 'base hello'; } };
const good = { __proto__: base, hello() { return super.hello() + ' + shorthand'; } };
console.log('\nmethod shorthand + super :', good.hello());
const bad = { __proto__: base };
try {
  // `super` is only legal syntactically inside a method definition; a function expression
  // assigned to a property has no [[HomeObject]], so this is a SyntaxError at parse time.
  eval('bad.hello = function () { return super.hello(); };');
} catch (e) {
  console.log('assigned function + super:', e.constructor.name + ': ' + e.message.split('\n')[0]);
}
// Reparenting a shorthand method keeps its ORIGINAL [[HomeObject]]:
const other = { hello() { return 'other hello'; } };
const moved = { __proto__: other, hello: good.hello };
console.log('reparented shorthand     :', moved.hello(), '(still resolves super through `base`)');

// --- mixins ---
const Serializable = (Base) => class extends Base {
  serialize() { return JSON.stringify(this, (k, v) => (k.startsWith('_') ? undefined : v)); }
};
const Comparable = (Base) => class extends Base {
  compareTo(other) { return this.name < other.name ? -1 : this.name > other.name ? 1 : 0; }
};
class Plain { constructor(name) { this.name = name; this._internal = 'skip'; } }
class Rich extends Serializable(Comparable(Plain)) {}
const r1 = new Rich('a'), r2 = new Rich('b');
console.log('\nmixin serialize :', r1.serialize());
console.log('mixin compareTo :', r1.compareTo(r2));
console.log('mixin chain     :', (function walk(p, acc = []) {
  return p === null ? acc.join(' -> ') : walk(Object.getPrototypeOf(p), [...acc, p.constructor?.name || '(anonymous mixin)']);
})(Object.getPrototypeOf(r1)));

// --- Symbol.species and extending built-ins ---
class MyArray extends Array { static get [Symbol.species]() { return Array; } }
const ma = MyArray.from([1, 2, 3]);
console.log('\nmapped instanceof MyArray :', ma.map((x) => x) instanceof MyArray, '(species -> Array)');
class MyArray2 extends Array {}
console.log('without species           :', MyArray2.from([1]).map((x) => x) instanceof MyArray2);

js-core/07-functions.js

'use strict';
// ---------- curry ----------
function curry(fn, arity = fn.length) {
  return function curried(...args) {
    if (args.length >= arity) return fn.apply(this, args);
    return function (...more) { return curried.apply(this, [...args, ...more]); };
  };
}
const volume = (l, w, h) => l * w * h;
const cv = curry(volume);
console.log('curry:', cv(2)(3)(4), cv(2, 3)(4), cv(2)(3, 4), cv(2, 3, 4));

// placeholder currying (lodash-style _)
const _ = Symbol('placeholder');
function curryP(fn, arity = fn.length) {
  return function next(prev) {
    return (...args) => {
      const merged = prev.slice();
      let ai = 0;
      for (let i = 0; i < merged.length && ai < args.length; i++) {
        if (merged[i] === _) merged[i] = args[ai++];
      }
      while (ai < args.length) merged.push(args[ai++]);
      const complete = merged.length >= arity && !merged.slice(0, arity).includes(_);
      return complete ? fn(...merged.slice(0, arity)) : next(merged);
    };
  }([]);
}
const div = (a, b) => a / b;
const cdiv = curryP(div);
console.log('curry w/ placeholder:', cdiv(_, 2)(10), cdiv(10)(2));

// partial application
const partial = (fn, ...bound) => (...rest) => fn(...bound, ...rest);
const partialRight = (fn, ...bound) => (...rest) => fn(...rest, ...bound);
console.log('partial:', partial(volume, 2, 3)(4), '| partialRight:', partialRight(div, 2)(10));

// ---------- composition ----------
const compose = (...fns) => (x) => fns.reduceRight((acc, f) => f(acc), x);
const pipe = (...fns) => (x) => fns.reduce((acc, f) => f(acc), x);
const inc = (n) => n + 1, dbl = (n) => n * 2, sq = (n) => n * n;
console.log('\ncompose(inc,dbl,sq)(3) =', compose(inc, dbl, sq)(3), '(sq -> dbl -> inc)');
console.log('pipe(inc,dbl,sq)(3)    =', pipe(inc, dbl, sq)(3), '(inc -> dbl -> sq)');

// variadic-first pipe that supports multiple args on the first call
const pipeN = (first, ...rest) => (...args) => rest.reduce((acc, f) => f(acc), first(...args));
console.log('pipeN(volume, inc)(2,3,4) =', pipeN(volume, inc)(2, 3, 4));

// async pipe
const pipeAsync = (...fns) => (x) => fns.reduce((p, f) => p.then(f), Promise.resolve(x));
pipeAsync(async (n) => n + 1, (n) => n * 10)(1).then((v) => console.log('pipeAsync(1)          =', v));

// ---------- memoize ----------
function memoize(fn, keyFn = (...a) => (a.length === 1 ? a[0] : JSON.stringify(a))) {
  const cache = new Map();
  const memo = function (...args) {
    const key = keyFn(...args);
    if (cache.has(key)) return cache.get(key);
    const val = fn.apply(this, args);
    cache.set(key, val);
    return val;
  };
  memo.cache = cache;
  return memo;
}
let calls = 0;
const slowFib = (n) => { calls++; return n < 2 ? n : fib(n - 1) + fib(n - 2); };
const fib = memoize(slowFib);
console.log('\nfib(35) =', fib(35), '| raw calls =', calls, '| cache size =', fib.cache.size);

// memoize with WeakMap for object keys — entries die with the key
function memoizeObj(fn) {
  const cache = new WeakMap();
  return (obj) => {
    if (cache.has(obj)) return cache.get(obj);
    const v = fn(obj);
    cache.set(obj, v);
    return v;
  };
}
let heavyCalls = 0;
const summarize = memoizeObj((o) => { heavyCalls++; return Object.keys(o).length; });
const key1 = { a: 1, b: 2 };
console.log('memoizeObj:', summarize(key1), summarize(key1), '| computed', heavyCalls, 'time(s)');

// multi-arg memoize with a WeakMap trie (no stringification, GC-friendly)
function memoizeDeep(fn) {
  const root = { primitives: new Map(), objects: new WeakMap(), has: false, value: undefined };
  return (...args) => {
    let node = root;
    for (const a of args) {
      const bucket = (a !== null && (typeof a === 'object' || typeof a === 'function')) ? node.objects : node.primitives;
      if (!bucket.has(a)) bucket.set(a, { primitives: new Map(), objects: new WeakMap(), has: false });
      node = bucket.get(a);
    }
    if (!node.has) { node.value = fn(...args); node.has = true; }
    return node.value;
  };
}
let deepCalls = 0;
const add3 = memoizeDeep((a, b, c) => { deepCalls++; return a + b + c; });
console.log('memoizeDeep:', add3(1, 2, 3), add3(1, 2, 3), '| computed', deepCalls, 'time(s)');

// ---------- debounce / throttle ----------
function debounce(fn, wait, { leading = false, trailing = true } = {}) {
  let timer = null, lastArgs = null, lastThis = null, result;
  const invoke = () => { result = fn.apply(lastThis, lastArgs); lastArgs = lastThis = null; };
  const debounced = function (...args) {
    lastArgs = args; lastThis = this;
    const callNow = leading && timer === null;
    if (timer) clearTimeout(timer);
    timer = setTimeout(() => {
      timer = null;
      if (trailing && lastArgs) invoke();
    }, wait);
    if (callNow) invoke();
    return result;
  };
  debounced.cancel = () => { if (timer) clearTimeout(timer); timer = null; lastArgs = lastThis = null; };
  debounced.flush = () => { if (timer) { clearTimeout(timer); timer = null; if (lastArgs) invoke(); } return result; };
  return debounced;
}

function throttle(fn, wait, { leading = true, trailing = true } = {}) {
  let last = 0, timer = null, lastArgs = null, lastThis = null;
  const throttled = function (...args) {
    const now = Date.now();
    if (!last && !leading) last = now;
    const remaining = wait - (now - last);
    lastArgs = args; lastThis = this;
    if (remaining <= 0 || remaining > wait) {
      if (timer) { clearTimeout(timer); timer = null; }
      last = now;
      fn.apply(lastThis, lastArgs);
      lastArgs = lastThis = null;
    } else if (!timer && trailing) {
      timer = setTimeout(() => {
        last = leading ? Date.now() : 0;
        timer = null;
        if (lastArgs) { fn.apply(lastThis, lastArgs); lastArgs = lastThis = null; }
      }, remaining);
    }
  };
  throttled.cancel = () => { clearTimeout(timer); timer = null; last = 0; lastArgs = null; };
  return throttled;
}

// deterministic demo: fire an event every 30ms for 300ms
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
async function demo() {
  const t0 = Date.now();
  const stamp = (tag) => (v) => log.push(`${tag}@~${Math.round((Date.now() - t0) / 30) * 30}ms(${v})`);
  const log = [];
  const d = debounce(stamp('debounce'), 100);
  const dl = debounce(stamp('debounce-leading'), 100, { leading: true, trailing: false });
  const th = throttle(stamp('throttle'), 100);
  for (let i = 0; i < 10; i++) { d(i); dl(i); th(i); await sleep(30); }
  await sleep(200);
  console.log('\ntimeline (10 events, 30ms apart, window 100ms):');
  for (const line of log) console.log('  ' + line);

  // tail call: V8 does NOT implement proper tail calls
  const deep = (n) => (n === 0 ? 'done' : deep(n - 1));
  try { console.log('\nrecursion depth 1e6 :', deep(1_000_000)); }
  catch (e) { console.log('\nrecursion depth 1e6 :', e.constructor.name, '- V8 has no proper tail calls'); }
  let depth = 0;
  const probe = () => { depth++; probe(); };
  try { probe(); } catch { console.log('max stack depth ~   :', depth); }
}
demo();

// arity / .length / name
console.log('\n.length of curried :', cv.length, '(curried wrappers use rest args -> 0)');
console.log('.name preserved    :', memoize(function namedFn() {}).name || '(empty - assign explicitly if you need it)');
console.log('bound fn name      :', volume.bind(null).name, '| length:', volume.bind(null, 1).length);

js-core/08-iterators.js

'use strict';
// ---------- the iteration protocol, by hand ----------
const range = {
  from: 1, to: 4,
  [Symbol.iterator]() {
    let cur = this.from; const last = this.to;
    return {
      next: () => (cur <= last ? { value: cur++, done: false } : { value: undefined, done: true }),
      return(v) { console.log('    (iterator.return called — cleanup)'); return { value: v, done: true }; },
      [Symbol.iterator]() { return this; },   // makes the iterator itself iterable
    };
  },
};
console.log('spread   :', [...range]);
console.log('for..of  :');
for (const n of range) { console.log('   ', n); if (n === 2) break; }   // break triggers return()
console.log('destructure:', (([a, b]) => [a, b])(range));

// Array.from consumes any iterable, and also array-likes
console.log('Array.from(iterable):', Array.from(range));
console.log('Array.from(arraylike):', Array.from({ length: 3, 0: 'a', 1: 'b', 2: 'c' }));
console.log('Array.from with mapFn:', Array.from({ length: 5 }, (_, i) => i * i));

// strings iterate by code point, not code unit
const s = 'a\u{1F600}b';
console.log('\nstring .length      :', s.length);
console.log('[...string]         :', [...s], '(length', [...s].length, ')');
console.log('s[1] (broken half)  :', JSON.stringify(s[1]));
console.log('Intl.Segmenter graphemes:', [...new Intl.Segmenter('en', { granularity: 'grapheme' }).segment('a👨‍👩‍👧b')].map((x) => x.segment));

// ---------- generators ----------
function* counter(start = 0) { let i = start; while (true) yield i++; }
const g = counter(10);
console.log('\ngenerator next  :', g.next(), g.next(), g.next());

function* inner() { yield 'i1'; yield 'i2'; return 'inner-return'; }
function* outer() {
  yield 'o1';
  const got = yield* inner();       // delegation; the return value flows to `got`
  yield 'got:' + got;
}
console.log('yield* delegation:', [...outer()]);

// two-way communication
function* dialogue() {
  const name = yield 'What is your name?';
  const age = yield `Hello ${name}, how old are you?`;
  try {
    yield `${name} is ${age}`;
  } catch (e) {
    yield 'caught inside generator: ' + e.message;
  } finally {
    console.log('    (generator finally ran)');
  }
}
const dg = dialogue();
console.log('\n1:', dg.next().value);
console.log('2:', dg.next('Hendrix').value);
console.log('3:', dg.next(41).value);
console.log('4:', dg.throw(new Error('boom')).value);
console.log('5:', dg.return('early'));

// generators as state machines / lazy infinite sequences
function* take(it, n) { let i = 0; for (const v of it) { if (i++ >= n) return; yield v; } }
function* mapG(it, f) { for (const v of it) yield f(v); }
function* filterG(it, p) { for (const v of it) if (p(v)) yield v; }
function* naturals() { let n = 1; while (true) yield n++; }
console.log('\nlazy pipeline   :', [...take(filterG(mapG(naturals(), (x) => x * x), (x) => x % 2 === 1), 5)]);

// how lazy? count how many source values are pulled
let pulled = 0;
function* countedNaturals() { let n = 1; while (true) { pulled++; yield n++; } }
const firstThree = [...take(filterG(countedNaturals(), (x) => x % 100 === 0), 3)];
console.log('pulled from source:', pulled, '-> got', firstThree);

// ---------- ES2025/2026 iterator helpers ----------
console.log('\nIterator global present:', typeof Iterator !== 'undefined');
if (typeof Iterator !== 'undefined') {
  let touched = 0;
  function* src() { let n = 1; while (true) { touched++; yield n++; } }
  const out = src()
    .map((x) => x * 3)
    .filter((x) => x % 2 === 0)
    .drop(1)
    .take(4)
    .toArray();
  console.log('helpers result  :', out, '| source values touched:', touched);
  console.log('flatMap         :', [1, 2, 3].values().flatMap((n) => [n, -n]).toArray());
  console.log('reduce          :', naturals().take(100).reduce((a, b) => a + b, 0));
  console.log('some/every/find :',
    naturals().take(10).some((x) => x > 8),
    naturals().take(10).every((x) => x > 0),
    naturals().find((x) => x % 7 === 0));
  console.log('Iterator.from   :', Iterator.from({ next: (() => { let i = 0; return () => ({ value: i, done: i++ >= 3 }); })() }).toArray());
  console.log('helpers are lazy & one-shot:', (() => { const it = naturals().take(3); it.toArray(); return it.toArray(); })());
}

// ---------- async iterators ----------
async function* asyncRange(n, delay = 5) {
  for (let i = 0; i < n; i++) {
    await new Promise((r) => setTimeout(r, delay));
    yield i;
  }
}
(async () => {
  const got = [];
  for await (const v of asyncRange(4)) got.push(v);
  console.log('\nfor await       :', got);

  // for await also accepts a sync iterable of promises (awaits each)
  const proms = [Promise.resolve('a'), Promise.resolve('b')];
  const seq = [];
  for await (const v of proms) seq.push(v);
  console.log('for await over promises:', seq);

  // Array.fromAsync (ES2024)
  console.log('Array.fromAsync :', await Array.fromAsync(asyncRange(3)));

  // Symbol.asyncIterator by hand: a paginated API
  const api = {
    pages: [[1, 2], [3, 4], [5]],
    async *[Symbol.asyncIterator]() {
      for (const page of this.pages) { await new Promise((r) => setImmediate(r)); yield* page; }
    },
  };
  const flat = [];
  for await (const item of api) flat.push(item);
  console.log('paginated stream:', flat);

  // async generator with early break -> return() runs finally
  async function* withCleanup() {
    try { yield 1; yield 2; yield 3; } finally { console.log('    (async cleanup ran)'); }
  }
  for await (const v of withCleanup()) { if (v === 2) break; }
})();

js-core/09-eventloop-1.js

console.log('1 sync start');

setTimeout(() => console.log('2 setTimeout 0'), 0);
setImmediate(() => console.log('3 setImmediate'));

Promise.resolve().then(() => console.log('4 promise.then'));
queueMicrotask(() => console.log('5 queueMicrotask'));
process.nextTick(() => console.log('6 process.nextTick'));

(async () => {
  console.log('7 async fn body (sync until first await)');
  await null;
  console.log('8 after await null');
})();

console.log('9 sync end');

js-core/09-eventloop-2.js

// Puzzle 2: microtask starvation and nesting order
console.log('script start');

setTimeout(() => {
  console.log('timeout 1');
  Promise.resolve().then(() => console.log('  microtask inside timeout 1'));
  process.nextTick(() => console.log('  nextTick inside timeout 1'));
}, 0);

setTimeout(() => console.log('timeout 2'), 0);

process.nextTick(() => {
  console.log('nextTick A');
  process.nextTick(() => console.log('  nextTick A.1 (nested — still drained before promises)'));
});

Promise.resolve()
  .then(() => { console.log('promise 1'); return Promise.resolve('x'); })
  .then((v) => console.log('promise 2 (took 2 extra ticks because a promise was returned)', v));

Promise.resolve().then(() => console.log('promise 3'));

console.log('script end');

js-core/09-eventloop-3.js

// Puzzle 3: async/await desugaring, and `await` on a thenable vs a native promise
async function a1() {
  console.log('a1 start');
  await a2();
  console.log('a1 end');
}
async function a2() { console.log('a2'); }

console.log('script start');
setTimeout(() => console.log('setTimeout'), 0);
a1();
new Promise((resolve) => { console.log('promise executor (synchronous!)'); resolve(); })
  .then(() => console.log('promise then 1'))
  .then(() => console.log('promise then 2'))
  .then(() => console.log('promise then 3'));
console.log('script end');

// Extra: awaiting a native promise costs 1 microtask tick; awaiting a thenable costs more.
setTimeout(() => {
  console.log('\n--- tick accounting ---');
  const order = [];
  (async () => { await Promise.resolve(); order.push('await native promise'); })();
  (async () => { await { then(r) { r(); } }; order.push('await thenable'); })();
  (async () => { await 1; order.push('await plain value'); })();
  Promise.resolve()
    .then(() => order.push('tick 1'))
    .then(() => order.push('tick 2'))
    .then(() => order.push('tick 3'))
    .then(() => order.push('tick 4'))
    .then(() => console.log(order.join('\n')));
}, 10);

js-core/09-eventloop-4.js

// Puzzle 4: setTimeout(0) vs setImmediate — indeterminate at top level, deterministic inside I/O
const fs = require('node:fs');

console.log('--- top level (order is NOT guaranteed; depends on loop start-up cost) ---');
setTimeout(() => console.log('  timeout'), 0);
setImmediate(() => console.log('  immediate'));

setTimeout(() => {
  console.log('\n--- inside an I/O callback (poll phase): immediate ALWAYS wins ---');
  fs.readFile(__filename, () => {
    setTimeout(() => console.log('  timeout (next timers phase, a full loop later)'), 0);
    setImmediate(() => console.log('  immediate (check phase, same iteration)'));
    process.nextTick(() => console.log('  nextTick (before either — drains on phase exit)'));
    Promise.resolve().then(() => console.log('  promise (after nextTick, still before both)'));
  });
}, 20);

// starvation demo: nextTick can block the loop forever
setTimeout(() => {
  console.log('\n--- nextTick starvation ---');
  let n = 0;
  const t = setTimeout(() => console.log('  timer fired after', n, 'nextTicks'), 0);
  (function spin() { if (n++ < 5) process.nextTick(spin); })();
  // with n < 5 the timer still fires; make it unbounded and the process hangs.
}, 100);

js-core/09-eventloop-5.js

// Close phase, deterministically: destroy a live socket from inside an I/O callback.
const net = require('node:net');
const server = net.createServer((sock) => {
  sock.on('close', () => { console.log('5 close phase   : socket "close" event'); server.close(); });
  sock.on('data', () => {
    console.log('1 poll phase    : socket "data" (I/O callback)');
    process.nextTick(() => console.log('2 nextTick      : drains as the phase exits'));
    Promise.resolve().then(() => console.log('3 microtask     : right after nextTick'));
    setImmediate(() => console.log('4 check phase   : setImmediate'));
    setTimeout(() => console.log('6 timers phase  : setTimeout 0, next loop turn'), 0);
    sock.destroy();
  });
});
server.listen(0, () => {
  const c = net.connect(server.address().port, () => c.end('ping'));
  c.on('error', () => {});
});

js-core/10-promise.js

'use strict';
const PENDING = 'pending', FULFILLED = 'fulfilled', REJECTED = 'rejected';

class MyPromise {
  #state = PENDING;
  #value = undefined;
  #handlers = [];          // { onFulfilled, onRejected, resolve, reject }
  #handled = false;

  constructor(executor) {
    if (typeof executor !== 'function') throw new TypeError('Promise resolver is not a function');
    const resolve = (v) => this.#resolveWith(v);
    const reject = (r) => this.#settle(REJECTED, r);
    try { executor(resolve, reject); } catch (err) { reject(err); }
  }

  // 2.3 The Promise Resolution Procedure
  #resolveWith(x) {
    if (this.#state !== PENDING) return;
    if (x === this) return this.#settle(REJECTED, new TypeError('Chaining cycle detected'));
    if (x !== null && (typeof x === 'object' || typeof x === 'function')) {
      let then;
      try { then = x.then; } catch (err) { return this.#settle(REJECTED, err); }
      if (typeof then === 'function') {
        let called = false;                       // a thenable may call back more than once
        try {
          then.call(x,
            (y) => { if (!called) { called = true; this.#resolveWith(y); } },
            (r) => { if (!called) { called = true; this.#settle(REJECTED, r); } });
        } catch (err) { if (!called) { called = true; this.#settle(REJECTED, err); } }
        return;
      }
    }
    this.#settle(FULFILLED, x);
  }

  #settle(state, value) {
    if (this.#state !== PENDING) return;
    this.#state = state;
    this.#value = value;
    if (state === REJECTED && this.#handlers.length === 0) {
      queueMicrotask(() => { if (!this.#handled) MyPromise.onUnhandled?.(value); });
    }
    for (const h of this.#handlers) this.#schedule(h);
    this.#handlers = [];
  }

  #schedule(handler) {
    // 2.2.4: handlers run asynchronously, on the microtask queue
    queueMicrotask(() => {
      const cb = this.#state === FULFILLED ? handler.onFulfilled : handler.onRejected;
      if (typeof cb !== 'function') {
        // pass through
        return this.#state === FULFILLED ? handler.resolve(this.#value) : handler.reject(this.#value);
      }
      try { handler.resolve(cb(this.#value)); } catch (err) { handler.reject(err); }
    });
  }

  then(onFulfilled, onRejected) {
    this.#handled = true;
    let resolve, reject;
    const next = new MyPromise((res, rej) => { resolve = res; reject = rej; });
    const handler = { onFulfilled, onRejected, resolve, reject };
    if (this.#state === PENDING) this.#handlers.push(handler);
    else this.#schedule(handler);
    return next;
  }

  catch(onRejected) { return this.then(undefined, onRejected); }

  finally(onFinally) {
    return this.then(
      (v) => MyPromise.resolve(onFinally()).then(() => v),
      (r) => MyPromise.resolve(onFinally()).then(() => { throw r; }),
    );
  }

  static resolve(v) { return v instanceof MyPromise ? v : new MyPromise((res) => res(v)); }
  static reject(r) { return new MyPromise((_, rej) => rej(r)); }

  static all(iterable) {
    return new MyPromise((resolve, reject) => {
      const items = [...iterable];
      const out = new Array(items.length);
      let remaining = items.length;
      if (remaining === 0) return resolve(out);
      items.forEach((item, i) => MyPromise.resolve(item).then((v) => {
        out[i] = v;
        if (--remaining === 0) resolve(out);
      }, reject));
    });
  }

  static allSettled(iterable) {
    return MyPromise.all([...iterable].map((p) => MyPromise.resolve(p).then(
      (value) => ({ status: 'fulfilled', value }),
      (reason) => ({ status: 'rejected', reason }),
    )));
  }

  static race(iterable) {
    return new MyPromise((resolve, reject) => {
      for (const item of iterable) MyPromise.resolve(item).then(resolve, reject);
    });
  }

  static any(iterable) {
    return new MyPromise((resolve, reject) => {
      const items = [...iterable];
      const errors = new Array(items.length);
      let remaining = items.length;
      if (remaining === 0) return reject(new AggregateError([], 'All promises were rejected'));
      items.forEach((item, i) => MyPromise.resolve(item).then(resolve, (e) => {
        errors[i] = e;
        if (--remaining === 0) reject(new AggregateError(errors, 'All promises were rejected'));
      }));
    });
  }

  static try(fn, ...args) {
    return new MyPromise((resolve) => resolve(fn(...args)));
  }

  static withResolvers() {
    let resolve, reject;
    const promise = new MyPromise((res, rej) => { resolve = res; reject = rej; });
    return { promise, resolve, reject };
  }

  get [Symbol.toStringTag]() { return 'MyPromise'; }
}

// ------------------------------------------------------------------ tests
let pass = 0, fail = 0;
const results = [];
function it(name, fn) {
  return Promise.resolve().then(fn).then(
    () => { pass++; results.push(`  ok   ${name}`); },
    (e) => { fail++; results.push(`  FAIL ${name}: ${e && e.message}`); },
  );
}
const assert = (cond, msg) => { if (!cond) throw new Error(msg || 'assertion failed'); };
const eq = (a, b, msg) => assert(JSON.stringify(a) === JSON.stringify(b), `${msg || ''} expected ${JSON.stringify(b)} got ${JSON.stringify(a)}`);
const toNative = (mp) => new Promise((res, rej) => mp.then(res, rej));

(async () => {
  await it('resolves with a value', async () => eq(await toNative(MyPromise.resolve(42)), 42));
  await it('rejects and is catchable', async () => {
    try { await toNative(MyPromise.reject(new Error('nope'))); assert(false, 'should throw'); }
    catch (e) { eq(e.message, 'nope'); }
  });
  await it('executor runs synchronously', async () => {
    let ran = false;
    new MyPromise(() => { ran = true; });
    assert(ran);
  });
  await it('then callbacks are asynchronous', async () => {
    const order = [];
    MyPromise.resolve(1).then(() => order.push('then'));
    order.push('sync');
    await toNative(MyPromise.resolve());
    await toNative(MyPromise.resolve());
    eq(order, ['sync', 'then']);
  });
  await it('chains and transforms', async () => {
    const v = await toNative(MyPromise.resolve(1).then((x) => x + 1).then((x) => x * 10));
    eq(v, 20);
  });
  await it('adopts a returned thenable', async () => {
    const v = await toNative(MyPromise.resolve(1).then(() => ({ then: (res) => res('adopted') })));
    eq(v, 'adopted');
  });
  await it('adopts a returned MyPromise', async () => {
    const v = await toNative(MyPromise.resolve(1).then(() => MyPromise.resolve('inner')));
    eq(v, 'inner');
  });
  await it('adopts a native promise', async () => {
    const v = await toNative(MyPromise.resolve(1).then(() => Promise.resolve('native')));
    eq(v, 'native');
  });
  await it('a throw becomes a rejection', async () => {
    try { await toNative(MyPromise.resolve().then(() => { throw new Error('thrown'); })); assert(false); }
    catch (e) { eq(e.message, 'thrown'); }
  });
  await it('rejection passes through missing handlers', async () => {
    try { await toNative(MyPromise.reject(new Error('deep')).then((x) => x).then((x) => x)); assert(false); }
    catch (e) { eq(e.message, 'deep'); }
  });
  await it('catch recovers', async () => {
    const v = await toNative(MyPromise.reject(new Error('x')).catch(() => 'recovered'));
    eq(v, 'recovered');
  });
  await it('settles only once', async () => {
    const p = new MyPromise((res, rej) => { res('first'); res('second'); rej(new Error('third')); });
    eq(await toNative(p), 'first');
  });
  await it('detects chaining cycles', async () => {
    const p = MyPromise.resolve();
    const q = p.then(() => q);
    try { await toNative(q); assert(false); } catch (e) { assert(e instanceof TypeError); }
  });
  await it('finally passes the value through', async () => {
    let ran = false;
    const v = await toNative(MyPromise.resolve('v').finally(() => { ran = true; return 'ignored'; }));
    assert(ran); eq(v, 'v');
  });
  await it('finally rethrows the reason', async () => {
    try { await toNative(MyPromise.reject(new Error('e')).finally(() => {})); assert(false); }
    catch (e) { eq(e.message, 'e'); }
  });
  await it('all resolves in input order', async () => {
    const slow = new MyPromise((r) => setTimeout(() => r('slow'), 20));
    eq(await toNative(MyPromise.all([slow, MyPromise.resolve('fast'), 'plain'])), ['slow', 'fast', 'plain']);
  });
  await it('all rejects fast', async () => {
    try { await toNative(MyPromise.all([MyPromise.resolve(1), MyPromise.reject(new Error('bad'))])); assert(false); }
    catch (e) { eq(e.message, 'bad'); }
  });
  await it('all([]) resolves immediately', async () => eq(await toNative(MyPromise.all([])), []));
  await it('allSettled reports both outcomes', async () => {
    const r = await toNative(MyPromise.allSettled([MyPromise.resolve(1), MyPromise.reject(new Error('z'))]));
    eq(r.map((x) => x.status), ['fulfilled', 'rejected']);
    eq(r[1].reason.message, 'z');
  });
  await it('race takes the first settlement', async () => {
    const slow = new MyPromise((r) => setTimeout(() => r('slow'), 30));
    const fast = new MyPromise((r) => setTimeout(() => r('fast'), 1));
    eq(await toNative(MyPromise.race([slow, fast])), 'fast');
  });
  await it('any ignores rejections', async () => {
    const r = await toNative(MyPromise.any([MyPromise.reject(new Error('a')), MyPromise.resolve('b')]));
    eq(r, 'b');
  });
  await it('any rejects with AggregateError', async () => {
    try { await toNative(MyPromise.any([MyPromise.reject(new Error('a')), MyPromise.reject(new Error('b'))])); assert(false); }
    catch (e) { assert(e instanceof AggregateError, 'AggregateError'); eq(e.errors.length, 2); }
  });
  await it('try catches synchronous throws', async () => {
    try { await toNative(MyPromise.try(() => { throw new Error('sync-boom'); })); assert(false); }
    catch (e) { eq(e.message, 'sync-boom'); }
  });
  await it('withResolvers exposes resolve/reject', async () => {
    const { promise, resolve } = MyPromise.withResolvers();
    setTimeout(() => resolve('later'), 1);
    eq(await toNative(promise), 'later');
  });
  await it('interoperates with native await (thenable)', async () => {
    eq(await MyPromise.resolve('interop'), 'interop');
  });
  await it('a thenable that calls back twice is ignored the second time', async () => {
    const weird = { then(res) { res('one'); res('two'); } };
    eq(await toNative(MyPromise.resolve().then(() => weird)), 'one');
  });
  await it('a thenable whose then throws after resolving is ignored', async () => {
    const weird = { then(res) { res('ok'); throw new Error('late'); } };
    eq(await toNative(MyPromise.resolve().then(() => weird)), 'ok');
  });
  await it('multiple then() on the same promise all fire', async () => {
    const p = MyPromise.resolve(5);
    eq(await toNative(MyPromise.all([p.then((x) => x + 1), p.then((x) => x + 2)])), [6, 7]);
  });

  console.log(results.join('\n'));
  console.log(`\n${pass} passed, ${fail} failed`);
  if (fail) process.exitCode = 1;
})();

js-core/11-async-patterns.js

'use strict';
const { setTimeout: sleep } = require('node:timers/promises');
const { AsyncLocalStorage } = require('node:async_hooks');

// Promise.try is ES2025 but is not in Node 22's V8 12.4 (it lands with V8 13.x / Node 24).
if (!Promise.try) {
  Promise.try = function (fn, ...args) { return new Promise((resolve) => resolve(fn(...args))); };
  console.log('(polyfilled Promise.try — absent in Node 22 / V8 12.4)');
}

const t0 = Date.now();
const ms = () => `${String(Date.now() - t0).padStart(4)}ms`;
const work = async (id, delay) => { await sleep(delay); return id; };

(async () => {
  // ---------- combinators ----------
  console.log('--- combinators ---');
  const settled = await Promise.allSettled([
    Promise.resolve('a'),
    Promise.reject(new Error('b failed')),
    sleep(10, 'c'),
  ]);
  console.log('allSettled :', settled.map((s) => (s.status === 'fulfilled' ? `ok(${s.value})` : `err(${s.reason.message})`)));
  console.log('any        :', await Promise.any([Promise.reject(new Error('x')), sleep(5, 'winner')]));
  console.log('race       :', await Promise.race([sleep(5, 'fast'), sleep(50, 'slow')]));
  try { await Promise.all([Promise.reject(new Error('first')), Promise.reject(new Error('second'))]); }
  catch (e) { console.log('all rejects:', e.message, '(the other rejection is swallowed but NOT unhandled)'); }
  try { await Promise.any([Promise.reject(new Error('e1')), Promise.reject(new Error('e2'))]); }
  catch (e) { console.log('any all-rejected:', e.constructor.name, e.errors.map((x) => x.message)); }
  console.log('Promise.try:', await Promise.try(() => 'sync value'),
    '| catches sync throw:', await Promise.try(() => { throw new Error('sync'); }).catch((e) => e.message));

  // ---------- sequential vs parallel ----------
  console.log('\n--- sequential vs parallel ---');
  let s = Date.now();
  const seq = [await work('a', 40), await work('b', 40), await work('c', 40)];
  console.log(`sequential awaits : ${seq} in ~${Date.now() - s}ms`);
  s = Date.now();
  const par = await Promise.all([work('a', 40), work('b', 40), work('c', 40)]);
  console.log(`Promise.all       : ${par} in ~${Date.now() - s}ms`);
  s = Date.now();
  const pa = work('a', 40), pb = work('b', 40);          // start both, then await
  const both = [await pa, await pb];
  console.log(`start-then-await  : ${both} in ~${Date.now() - s}ms`);

  // ---------- await inside forEach does not work ----------
  console.log('\n--- await in forEach ---');
  const out = [];
  [1, 2, 3].forEach(async (n) => { await sleep(5); out.push(n); });
  console.log('immediately after forEach :', out, '(forEach ignored the returned promises)');
  await sleep(20);
  console.log('20ms later                :', out);

  const out2 = [];
  for (const n of [1, 2, 3]) { await sleep(5); out2.push(n); }
  console.log('for..of + await           :', out2);
  const out3 = await Promise.all([1, 2, 3].map(async (n) => { await sleep(5); return n; }));
  console.log('map + Promise.all         :', out3);

  // reduce is the sequential-with-accumulator idiom
  const chained = await [1, 2, 3].reduce(async (accP, n) => { const acc = await accP; await sleep(2); return acc + n; }, Promise.resolve(0));
  console.log('sequential reduce         :', chained);

  // ---------- error handling shapes ----------
  console.log('\n--- error handling ---');
  const safe = async (p) => { try { return [null, await p]; } catch (e) { return [e, null]; } };
  console.log('tuple style      :', await safe(Promise.reject(new Error('tuple'))).then(([e]) => e.message));
  // finally does not swallow
  try { await Promise.reject(new Error('kept')).finally(() => 'ignored'); }
  catch (e) { console.log('finally rethrows :', e.message); }

  // errors thrown after the function returns escape the try
  async function leaky() {
    try { return sleep(1).then(() => { throw new Error('escaped'); }); }   // NOT awaited
    catch { return 'never reached'; }
  }
  console.log('return without await :', await leaky().catch((e) => 'escaped the try/catch: ' + e.message));
  async function tight() {
    try { return await sleep(1).then(() => { throw new Error('caught'); }); }
    catch (e) { return 'caught by try/catch: ' + e.message; }
  }
  console.log('return await         :', await tight());

  // ---------- AbortController ----------
  console.log('\n--- AbortController ---');
  async function fetchish(signal) {
    if (signal.aborted) throw signal.reason;
    return new Promise((resolve, reject) => {
      const id = setTimeout(() => resolve('payload'), 100);
      signal.addEventListener('abort', () => { clearTimeout(id); reject(signal.reason); }, { once: true });
    });
  }
  const ac = new AbortController();
  setTimeout(() => ac.abort(new Error('user cancelled')), 20);
  try { await fetchish(ac.signal); } catch (e) { console.log('aborted with     :', e.message); }

  // AbortSignal helpers
  try { await sleep(100, undefined, { signal: AbortSignal.timeout(20) }); }
  catch (e) { console.log('AbortSignal.timeout:', e.name); }
  const combined = AbortSignal.any([AbortSignal.timeout(15), new AbortController().signal]);
  try { await fetchish(combined); } catch (e) { console.log('AbortSignal.any  :', e.name); }

  // ---------- pMap: bounded concurrency ----------
  console.log('\n--- pMap (concurrency limit) ---');
  async function pMap(iterable, mapper, { concurrency = Infinity, stopOnError = true } = {}) {
    const items = [...iterable];
    const results = new Array(items.length);
    const errors = [];
    let nextIndex = 0;
    const limit = Math.max(1, Math.min(concurrency, items.length));
    async function worker() {
      while (nextIndex < items.length) {
        const i = nextIndex++;
        try { results[i] = await mapper(items[i], i); }
        catch (e) { if (stopOnError) throw e; errors.push(e); }
      }
    }
    await Promise.all(Array.from({ length: limit }, worker));
    if (errors.length) throw new AggregateError(errors, 'pMap failures');
    return results;
  }

  let inFlight = 0, peak = 0;
  const timeline = [];
  const job = async (n) => {
    peak = Math.max(peak, ++inFlight);
    timeline.push(`${ms()} start ${n} (inFlight=${inFlight})`);
    await sleep(30);
    inFlight--;
    return n * 2;
  };
  const mapped = await pMap([1, 2, 3, 4, 5, 6, 7], job, { concurrency: 3 });
  console.log('results          :', mapped);
  console.log('peak concurrency :', peak);
  console.log(timeline.slice(0, 5).join('\n'));

  // a semaphore, which is what pMap is underneath
  function semaphore(max) {
    let active = 0; const queue = [];
    const release = () => { active--; queue.shift()?.(); };
    return async function acquire(fn) {
      if (active >= max) await new Promise((r) => queue.push(r));
      active++;
      try { return await fn(); } finally { release(); }
    };
  }
  const gate = semaphore(2);
  let semPeak = 0, semActive = 0;
  await Promise.all([1, 2, 3, 4, 5].map((n) => gate(async () => {
    semPeak = Math.max(semPeak, ++semActive); await sleep(10); semActive--; return n;
  })));
  console.log('semaphore peak   :', semPeak);

  // ---------- AsyncLocalStorage ----------
  console.log('\n--- AsyncLocalStorage ---');
  const als = new AsyncLocalStorage();
  async function handler(id) {
    await sleep(Math.random() * 10);
    return `deep call sees requestId=${als.getStore()?.requestId}`;
  }
  const lines = await Promise.all([1, 2, 3].map((i) =>
    als.run({ requestId: `req-${i}` }, () => handler(i))));
  console.log(lines.join('\n'));
})();

js-core/12-unhandled.js

'use strict';
// Unhandled rejections: what Node does, and the "handled too late" warning.
process.on('unhandledRejection', (reason, promise) => {
  console.log('[unhandledRejection]', reason.message);
});
process.on('rejectionHandled', () => {
  console.log('[rejectionHandled] a handler was attached after the fact');
});

// 1. Never handled -> unhandledRejection fires at the end of the microtask checkpoint.
Promise.reject(new Error('never handled'));

// 2. Handled synchronously -> no event.
Promise.reject(new Error('handled sync')).catch(() => console.log('caught sync-attached handler'));

// 3. Handled LATE (after a macrotask) -> unhandledRejection fires first, then rejectionHandled.
const late = Promise.reject(new Error('handled late'));
setTimeout(() => late.catch((e) => console.log('caught late:', e.message)), 10);

// 4. Promise.all swallows the losing rejections WITHOUT marking them unhandled,
//    because .all attaches a handler to every input.
Promise.all([Promise.reject(new Error('all-first')), Promise.reject(new Error('all-second'))])
  .catch((e) => console.log('Promise.all rejected with:', e.message));

// 5. But an array you build and only PARTLY await does leak.
const leaked = [Promise.reject(new Error('leaked-0')), Promise.resolve(1)];
leaked[1].then(() => console.log('only awaited the resolved one'));

// 6. Throwing inside a setTimeout callback is an uncaughtException, not a rejection.
process.on('uncaughtException', (e) => console.log('[uncaughtException]', e.message));
setTimeout(() => { throw new Error('sync throw in a timer'); }, 20);

setTimeout(() => {
  console.log('\nDefault policy: `--unhandled-rejections=throw` (Node 15+) turns an');
  console.log('unhandled rejection into an uncaughtException and exits with code 1.');
  console.log('Node 22 flag values: throw | strict | warn | warn-with-error-code | none');
}, 40);

js-core/13-memory.js

'use strict';
const v8 = require('node:v8');

const mb = (b) => (b / 1024 / 1024).toFixed(2) + ' MB';
const snap = (label) => {
  const m = process.memoryUsage();
  console.log(`${label.padEnd(24)} rss=${mb(m.rss)} heapTotal=${mb(m.heapTotal)} heapUsed=${mb(m.heapUsed)} external=${mb(m.external)} arrayBuffers=${mb(m.arrayBuffers)}`);
};

snap('baseline');

// --- allocate a lot of short-lived garbage: scavenger territory ---
let sink = 0;
for (let i = 0; i < 2_000_000; i++) { const o = { i, s: 'x' }; sink += o.i; }
snap('after 2M short-lived');

// --- retain a lot: promotes to old space ---
const retained = [];
for (let i = 0; i < 500_000; i++) retained.push({ i, payload: 'y'.repeat(8) });
snap('after 500k retained');

// heap space breakdown
console.log('\nheap spaces:');
for (const s of v8.getHeapSpaceStatistics()) {
  if (s.space_used_size > 512 * 1024) {
    console.log('  ' + s.space_name.padEnd(24), 'used=' + mb(s.space_used_size), 'size=' + mb(s.space_size));
  }
}
const hs = v8.getHeapStatistics();
console.log('\nheap_size_limit  :', mb(hs.heap_size_limit));
console.log('used_heap_size   :', mb(hs.used_heap_size));
console.log('malloced_memory  :', mb(hs.malloced_memory));
console.log('number_of_native_contexts:', hs.number_of_native_contexts);
console.log('number_of_detached_contexts:', hs.number_of_detached_contexts);

retained.length = 0;
if (global.gc) { global.gc(); snap('\nafter explicit gc'); } else { console.log('\n(run with --expose-gc for a forced collection)'); }

// --- WeakRef / FinalizationRegistry ---
const registry = new FinalizationRegistry((held) => console.log('  finalized:', held));
let target = { big: new Array(1000).fill(0) };
const ref = new WeakRef(target);
registry.register(target, 'cache-entry-1');
console.log('\nWeakRef deref while alive :', ref.deref() !== undefined);
target = null;
setTimeout(() => {
  if (global.gc) global.gc();
  setTimeout(() => {
    console.log('WeakRef deref after drop  :', ref.deref() === undefined ? 'undefined (collected)' : 'still alive (GC has not run)');
  }, 50);
}, 10);

// --- WeakMap: private data that dies with the key ---
const privates = new WeakMap();
class Session {
  constructor(token) { privates.set(this, { token }); }
  get token() { return privates.get(this).token; }
}
let sess = new Session('abc');
console.log('WeakMap-backed private    :', sess.token, '| WeakMap is not enumerable:', privates.size === undefined);
sess = null;   // the entry becomes unreachable too — no manual delete needed

// --- WeakSet for "have I seen this object" without retaining it ---
const seen = new WeakSet();
const node = {};
seen.add(node);
console.log('WeakSet has              :', seen.has(node));

// --- leak source 4: an unbounded cache (and the WeakMap/LRU fixes) ---
const leakyCache = new Map();
function leakyLookup(key) {
  if (!leakyCache.has(key)) leakyCache.set(key, { key, data: 'x'.repeat(100) });
  return leakyCache.get(key);
}
for (let i = 0; i < 100_000; i++) leakyLookup('k' + i);
snap('\nunbounded Map cache');
leakyCache.clear();

// --- leak source 3: a timer that keeps a closure alive ---
function startLeakyTimer() {
  const bigBuffer = Buffer.alloc(4 * 1024 * 1024);
  const id = setInterval(() => { void bigBuffer[0]; }, 1000);
  return () => clearInterval(id);      // returning a disposer is the fix
}
const stop = startLeakyTimer();
snap('timer holding 4MB');
stop();

// --- leak source 2: listeners that are never removed ---
const { EventEmitter } = require('node:events');
const bus = new EventEmitter();
bus.setMaxListeners(0);
const handlers = [];
for (let i = 0; i < 50_000; i++) { const h = () => i; handlers.push(h); bus.on('tick', h); }
console.log('listener count           :', bus.listenerCount('tick'), '(each closure pins its scope)');
bus.removeAllListeners('tick');

// --- leak source 1: an accidental global ---
// `new Function` bodies are always sloppy, so an undeclared assignment creates a global.
const sloppyGlobal = new Function('accidental = new Array(1e5).fill(0); return typeof accidental;');
console.log('sloppy undeclared assign :', sloppyGlobal(), '-> globalThis.accidental exists:', 'accidental' in globalThis);
try { (function () { 'use strict'; undeclared2 = 1; })(); }
catch (e) { console.log('strict mode instead      :', e.constructor.name, '(this is why you use strict/ESM)'); }

js-core/14-v8-bench.js

'use strict';
// Measured V8 behaviour. Run with plain `node 14-v8-bench.js`.
const N = 5_000_000;

function bench(label, fn, iters = 9) {
  const times = [];
  fn(); fn();                      // warm up: let TurboFan tier the loop up first
  for (let i = 0; i < iters; i++) {
    const t = process.hrtime.bigint();
    const r = fn();
    const dt = Number(process.hrtime.bigint() - t) / 1e6;
    times.push(dt);
    if (r === Symbol.for('never')) console.log('unreachable');
  }
  times.sort((a, b) => a - b);
  const median = times[Math.floor(times.length / 2)];
  console.log(`  ${label.padEnd(42)} ${median.toFixed(1).padStart(8)} ms   (min ${times[0].toFixed(1)})`);
  return median;
}

// ---------------------------------------------------------------- 1. IC state
console.log('1) Inline caches: monomorphic vs polymorphic vs megamorphic property access');
// Same shape (same hidden class) -> monomorphic
const mono = Array.from({ length: 1000 }, (_, i) => ({ x: i, y: i }));
// 4 shapes -> polymorphic (V8 handles up to 4 maps in a polymorphic IC)
const poly = Array.from({ length: 1000 }, (_, i) => {
  switch (i % 4) {
    case 0: return { x: i, y: i };
    case 1: return { y: i, x: i };           // different insertion order = different map
    case 2: return { x: i, y: i, z: i };
    default: return { x: i, a: i, y: i };
  }
});
// 20 shapes -> megamorphic (IC gives up, falls back to the global megamorphic stub cache)
const mega = Array.from({ length: 1000 }, (_, i) => {
  const o = { x: i };
  for (let k = 0; k < (i % 20); k++) o['k' + k] = k;
  o.y = i;
  return o;
});

function readX(arr) {
  let s = 0;
  for (let i = 0; i < N; i++) s += arr[i % 1000].x;   // one call site, one IC
  return s;
}
// Force each call site to be its own function so the ICs do not merge.
const readMono = new Function('arr', 'N', 'let s=0; for(let i=0;i<N;i++) s+=arr[i%1000].x; return s;');
const readPoly = new Function('arr', 'N', 'let s=0; for(let i=0;i<N;i++) s+=arr[i%1000].x; return s;');
const readMega = new Function('arr', 'N', 'let s=0; for(let i=0;i<N;i++) s+=arr[i%1000].x; return s;');
bench('monomorphic (1 hidden class)', () => readMono(mono, N));
bench('polymorphic (4 hidden classes)', () => readPoly(poly, N));
bench('megamorphic (20 hidden classes)', () => readMega(mega, N));
void readX;

// ---------------------------------------------------------------- 2. elements kinds
console.log('\n2) Elements kinds: PACKED vs HOLEY vs DOUBLE vs generic');
const M = 100_000, REPS = 200;
const packedSmi = Array.from({ length: M }, (_, i) => i);
const packedDouble = Array.from({ length: M }, (_, i) => i + 0.5);
const packedElements = Array.from({ length: M }, (_, i) => ({ v: i }));
const holeySmi = Array.from({ length: M }, (_, i) => i);
delete holeySmi[5];                       // one delete converts the whole array to HOLEY
const holeyByHole = new Array(M);         // `new Array(n)` starts HOLEY_SMI
for (let i = 0; i < M; i++) holeyByHole[i] = i;

const sumSmi = new Function('a', 'R', 'let s=0; for(let r=0;r<R;r++) for(let i=0;i<a.length;i++) s+=a[i]; return s;');
const sumDbl = new Function('a', 'R', 'let s=0; for(let r=0;r<R;r++) for(let i=0;i<a.length;i++) s+=a[i]; return s;');
const sumHoley = new Function('a', 'R', 'let s=0; for(let r=0;r<R;r++) for(let i=0;i<a.length;i++) s+=a[i]||0; return s;');
const sumHoley2 = new Function('a', 'R', 'let s=0; for(let r=0;r<R;r++) for(let i=0;i<a.length;i++) s+=a[i]; return s;');
const sumObj = new Function('a', 'R', 'let s=0; for(let r=0;r<R;r++) for(let i=0;i<a.length;i++) s+=a[i].v; return s;');
bench('PACKED_SMI_ELEMENTS sum', () => sumSmi(packedSmi, REPS));
bench('PACKED_DOUBLE_ELEMENTS sum', () => sumDbl(packedDouble, REPS));
bench('HOLEY_SMI (one delete) sum', () => sumHoley(holeySmi, REPS));
bench('HOLEY_SMI (new Array(n), fully filled)', () => sumHoley2(holeyByHole, REPS));
bench('PACKED_ELEMENTS (objects) sum', () => sumObj(packedElements, REPS));

// ---------------------------------------------------------------- 3. shape stability
console.log('\n3) Shape stability: fixed shape vs shape mutated after construction');
function makeStable(i) { return { a: i, b: i, c: i, d: 0 }; }
function makeUnstable(i) { const o = { a: i, b: i, c: i }; if (i % 2) o.d = 0; return o; }
const stable = [], unstable = [];
for (let i = 0; i < 200_000; i++) { stable.push(makeStable(i)); unstable.push(makeUnstable(i)); }
const sumD1 = new Function('a', 'let s=0; for(let r=0;r<40;r++) for(const o of a) s+=o.d|0; return s;');
const sumD2 = new Function('a', 'let s=0; for(let r=0;r<40;r++) for(const o of a) s+=o.d|0; return s;');
bench('always-present property .d', () => sumD1(stable));
bench('sometimes-missing property .d', () => sumD2(unstable));

// ---------------------------------------------------------------- 4. strings
console.log('\n4) Strings: ConsString (+=) vs array-join vs split/reverse/join');
const WORDS = 200_000;
bench('s += chunk in a loop (ConsString rope)', () => {
  let s = '';
  for (let i = 0; i < WORDS; i++) s += 'abc';
  return s.length;
});
bench('parts.push + join', () => {
  const parts = [];
  for (let i = 0; i < WORDS; i++) parts.push('abc');
  return parts.join('').length;
});
const big = 'abcdefghij'.repeat(50_000);   // 500k chars
bench('split("").reverse().join("") 500k', () => big.split('').reverse().join('').length, 3);
bench('manual reverse loop 500k', () => {
  let out = '';
  for (let i = big.length - 1; i >= 0; i--) out += big[i];
  return out.length;
}, 3);
bench('Array.from + reverse + join 500k', () => Array.from(big).reverse().join('').length, 3);
bench('slice 500k (SlicedString, O(1))', () => { let n = 0; for (let i = 0; i < 200_000; i++) n += big.slice(i, i + 100).length; return n; }, 3);

// ---------------------------------------------------------------- 5. arguments
console.log('\n5) `arguments` leak vs rest parameters');
function useArguments() { let s = 0; for (let i = 0; i < arguments.length; i++) s += arguments[i]; return s; }
function useRest(...a) { let s = 0; for (let i = 0; i < a.length; i++) s += a[i]; return s; }
function leakArguments() { return arguments; }        // escapes -> must be materialised
const consume = (o) => { let s = 0; for (let i = 0; i < o.length; i++) s += o[i]; return s; };
const L = 3_000_000;
bench('arguments, consumed in place', () => { let s = 0; for (let i = 0; i < L; i++) s += useArguments(1, 2, 3); return s; });
bench('rest parameters', () => { let s = 0; for (let i = 0; i < L; i++) s += useRest(1, 2, 3); return s; });
bench('arguments escaping the frame', () => { let s = 0; for (let i = 0; i < L; i++) s += consume(leakArguments(1, 2, 3)); return s; });
bench('Array.prototype.slice.call(arguments)', () => { let s = 0; for (let i = 0; i < L; i++) s += consume(Array.prototype.slice.call(leakArguments(1, 2, 3))); return s; });

// ---------------------------------------------------------------- 6. try/catch + deopt triggers
console.log('\n6) Misc: delete vs undefined, for..in vs Object.keys');
const objs = Array.from({ length: 200_000 }, (_, i) => ({ a: i, b: i, c: i }));
bench('set property to undefined', () => { for (const o of objs) o.b = undefined; return objs.length; });
bench('delete property (dictionary mode)', () => { for (const o of objs) { o.b = 1; delete o.b; } return objs.length; });
const shape = { a: 1, b: 2, c: 3, d: 4, e: 5 };
bench('for..in', () => { let n = 0; for (let r = 0; r < 2_000_000; r++) for (const k in shape) n += k.length; return n; });
bench('Object.keys + for', () => { let n = 0; for (let r = 0; r < 2_000_000; r++) { const ks = Object.keys(shape); for (let i = 0; i < ks.length; i++) n += ks[i].length; } return n; });

js-core/15-natives.js

// Run with: node --allow-natives-syntax 15-natives.js
function kind(a) {
  // %DebugPrint writes to stderr; HasFastPackedElements/HasHoleyElements are cheap predicates.
  return [
    %HasSmiElements(a) ? 'SMI' : '',
    %HasDoubleElements(a) ? 'DOUBLE' : '',
    %HasObjectElements(a) ? 'OBJECT' : '',
    %HasDictionaryElements(a) ? 'DICTIONARY' : '',
    %HasHoleyElements(a) ? '(HOLEY)' : '(PACKED)',
  ].filter(Boolean).join(' ');
}
const p = (label, a) => console.log(('  ' + label).padEnd(40), kind(a));
const nl = () => console.log();

console.log('elements-kind transitions (one-way lattice):');
const a = [1, 2, 3];                     p('[1,2,3]', a);
a.push(4.5);                             p('after push(4.5)', a);
a.push('str');                           p('after push("str")', a);

nl();
const b = [1, 2, 3];                     p('[1,2,3]', b);
b[10] = 1;                               p('after b[10] = 1 (creates a hole)', b);

nl();
const c = [1, 2, 3];                     p('[1,2,3]', c);
delete c[1];                             p('after delete c[1]', c);

nl();
const d = new Array(3);                  p('new Array(3)', d);
d[0] = 1; d[1] = 2; d[2] = 3;            p('after filling all 3 slots', d);

nl();
const e = [1, 2, 3];                     p('[1,2,3]', e);
e[100000] = 1;                           p('after e[100000] = 1 (sparse)', e);

console.log('\nfast vs dictionary properties:');
const o = { a: 1, b: 2, c: 3 };
console.log('  literal object            fastProperties =', %HasFastProperties(o));
delete o.b;
console.log('  after delete o.b          fastProperties =', %HasFastProperties(o));
const big = {};
for (let i = 0; i < 50; i++) big['p' + i] = i;
console.log('  50 dynamically added keys fastProperties =', %HasFastProperties(big));

console.log('\noptimisation tiers:');
function hot(x) { return x * 2 + 1; }
const decode = (f) => {
  const st = %GetOptimizationStatus(f);
  const bits = { isFunction: 0, neverOptimize: 1, alwaysOptimize: 2, maybeDeopted: 3, optimized: 4,
    maglevved: 5, turbofanned: 6, interpreted: 7, markedForOpt: 8, markedForConcurrentOpt: 9,
    optimizingConcurrently: 10, isExecuting: 11, topmostIsTurbofanned: 12, liteMode: 13,
    markedForDeopt: 14, baseline: 15, topmostIsInterpreted: 16, topmostIsBaseline: 17, isLazy: 18 };
  return Object.entries(bits).filter(([, b]) => st & (1 << b)).map(([k]) => k).join(', ') || '(none)';
};
console.log('  cold            :', decode(hot));
for (let i = 0; i < 20; i++) hot(i);
console.log('  after 20 calls  :', decode(hot));
for (let i = 0; i < 2000; i++) hot(i);
console.log('  after 2k calls  :', decode(hot));
for (let i = 0; i < 200000; i++) hot(i);
console.log('  after 200k calls:', decode(hot));

console.log('\nstring representations:');
let s = 'a';
for (let i = 0; i < 10; i++) s += s;         // builds a ConsString rope
console.log('  s.length =', s.length);
console.log('  s.slice(1, 100).length =', s.slice(1, 100).length, '(SlicedString: no copy)');

js-core/16-node.js

'use strict';
const { Readable, Writable, Transform, pipeline } = require('node:stream');
const { pipeline: pipelineAsync } = require('node:stream/promises');
const { Worker, isMainThread, parentPort, workerData } = require('node:worker_threads');

// ---------------- streams & backpressure ----------------
async function streamDemo() {
  console.log('--- backpressure ---');
  let produced = 0, consumed = 0, pauses = 0;

  const source = new Readable({
    highWaterMark: 4,                       // 4 objects in the read buffer
    objectMode: true,
    read() {
      if (produced >= 20) return this.push(null);
      this.push({ n: produced++ });
    },
  });

  const slowSink = new Writable({
    highWaterMark: 2,
    objectMode: true,
    write(chunk, _enc, cb) {
      consumed++;
      setTimeout(cb, 2);                    // deliberately slower than the producer
    },
  });

  const doubler = new Transform({
    objectMode: true,
    transform(chunk, _enc, cb) { cb(null, { n: chunk.n * 2 }); },
  });

  // manual write() with backpressure handling, for contrast
  const manual = new Writable({ highWaterMark: 2, objectMode: true, write(c, e, cb) { setTimeout(cb, 1); } });
  for (let i = 0; i < 8; i++) {
    if (!manual.write({ i })) {
      pauses++;
      await new Promise((r) => manual.once('drain', r));   // THIS is backpressure
    }
  }
  manual.end();
  console.log('manual write(): hit a full buffer', pauses, 'times and awaited "drain"');

  await pipelineAsync(source, doubler, slowSink);
  console.log(`pipeline: produced ${produced}, consumed ${consumed} — the readable never ran ahead`);
  console.log('(pipeline/pipelineAsync also destroys every stream on error; .pipe() does not)');

  // async iteration over a stream is the modern consumer
  const nums = Readable.from(async function* () { for (let i = 0; i < 5; i++) yield i; }());
  const collected = [];
  for await (const n of nums) collected.push(n);
  console.log('for await over a stream:', collected);
}

// ---------------- Buffer ----------------
function bufferDemo() {
  console.log('\n--- Buffer ---');
  const b = Buffer.from('héllo', 'utf8');
  console.log('bytes            :', b, '| .length (bytes) =', b.length, '| string length =', 'héllo'.length);
  console.log('base64 / hex     :', b.toString('base64'), '/', b.toString('hex'));
  console.log('alloc vs unsafe  :', Buffer.alloc(4), Buffer.allocUnsafe(4).length, '(allocUnsafe may contain old memory)');
  const view = b.subarray(0, 2);
  view[0] = 0x48;
  console.log('subarray shares memory:', b.toString('utf8').slice(0, 3), '(mutating the view mutated the source)');
  console.log('Buffer is a Uint8Array:', b instanceof Uint8Array, '| pooled:', b.buffer.byteLength > b.length);
  const ab = new ArrayBuffer(8);
  const dv = new DataView(ab);
  dv.setUint32(0, 0xdeadbeef);
  console.log('DataView big-endian:', dv.getUint32(0).toString(16), '| little-endian:', dv.getUint32(0, true).toString(16));
  console.log('concat           :', Buffer.concat([Buffer.from('ab'), Buffer.from('cd')]).toString());
}

// ---------------- structuredClone ----------------
function cloneDemo() {
  console.log('\n--- structuredClone ---');
  const src = { d: new Date(0), m: new Map([['k', [1, 2]]]), s: new Set([1]), r: /ab+c/gi, t: new Uint8Array([1, 2, 3]), big: 1n };
  src.self = src;                                       // cycle
  const clone = structuredClone(src);
  console.log('cycle preserved  :', clone.self === clone);
  console.log('Map/Set/Date/RegExp/TypedArray/BigInt:',
    clone.m.get('k'), clone.s.has(1), clone.d.toISOString(), String(clone.r), clone.t, clone.big);
  console.log('deep, not shared :', clone.m !== src.m && clone.m.get('k') !== src.m.get('k'));
  try { structuredClone({ fn() {} }); } catch (e) { console.log('functions        :', e.name, '- not cloneable'); }
  try { structuredClone(Symbol('s')); } catch (e) { console.log('symbols          :', e.name); }
  class Tagged { constructor() { this.x = 1; } }
  console.log('prototype is LOST:', structuredClone(new Tagged()).constructor.name);
  // transfer moves an ArrayBuffer instead of copying it
  const buf = new ArrayBuffer(8);
  structuredClone(buf, { transfer: [buf] });
  console.log('after transfer   : source byteLength =', buf.byteLength, '(detached)');
  // JSON round-trip loses all of this
  console.log('JSON equivalent  :', JSON.stringify({ d: new Date(0), m: new Map([['k', 1]]), u: undefined, n: NaN }));
}

// ---------------- CPU-bound work blocks the loop ----------------
function blockDemo() {
  return new Promise((resolve) => {
    console.log('\n--- blocking the event loop ---');
    const start = Date.now();
    setTimeout(() => {
      console.log(`a 10ms timer actually fired after ${Date.now() - start}ms — the sync loop held the thread`);
      resolve();
    }, 10);
    let x = 0;
    for (let i = 0; i < 3e8; i++) x += i;   // synchronous CPU burn
    console.log(`sync loop done in ${Date.now() - start}ms (sum=${x})`);
  });
}

// ---------------- worker_threads ----------------
function workerDemo() {
  return new Promise((resolve) => {
    console.log('\n--- worker_threads ---');
    const start = Date.now();
    let ticks = 0;
    const tick = setInterval(() => ticks++, 1);
    const src = `
      const { parentPort, workerData } = require('node:worker_threads');
      let x = 0;
      for (let i = 0; i < workerData.n; i++) x += i;
      parentPort.postMessage({ x, threadId: require('node:worker_threads').threadId });
    `;
    const w = new Worker(src, { eval: true, workerData: { n: 3e8 } });
    w.on('message', (m) => {
      clearInterval(tick);
      console.log(`worker finished in ${Date.now() - start}ms; the main loop still ticked ${ticks} times`);
      console.log('worker result    :', m.x, '| threadId:', m.threadId);
      resolve();
    });
  });
}

// ---------------- SharedArrayBuffer + Atomics ----------------
function sabDemo() {
  return new Promise((resolve) => {
    console.log('\n--- SharedArrayBuffer / Atomics ---');
    const sab = new SharedArrayBuffer(8);
    const view = new Int32Array(sab);
    const src = `
      const { parentPort, workerData } = require('node:worker_threads');
      const v = new Int32Array(workerData.sab);
      for (let i = 0; i < 100000; i++) Atomics.add(v, 0, 1);
      parentPort.postMessage('done');
    `;
    const w = new Worker(src, { eval: true, workerData: { sab } });   // no copy: memory is shared
    for (let i = 0; i < 100000; i++) Atomics.add(view, 0, 1);
    w.on('message', () => {
      console.log('two threads, 100k Atomics.add each ->', view[0], '(exactly 200000; no lost updates)');
      resolve();
    });
  });
}

(async () => {
  await streamDemo();
  bufferDemo();
  cloneDemo();
  await blockDemo();
  await workerDemo();
  await sabDemo();
  console.log('\nprocess: pid', process.pid, '| cpus', require('node:os').cpus().length);
})();

js-core/17-modern-syntax.js

'use strict';
// ---------- optional chaining & nullish ----------
const user = { profile: { name: 'Hendrix' }, tags: ['a'], greet() { return 'hi'; } };
console.log('?. property   :', user.settings?.theme);
console.log('?. call       :', user.greet?.(), user.missing?.());
console.log('?. index      :', user.tags?.[0], user.other?.[0]);
console.log('short-circuits:', (() => { let n = 0; const f = () => n++; user.nope?.[f()]; return n; })(), '(the index expression never ran)');
console.log('?? vs ||      :', 0 ?? 'fallback', '|', 0 || 'fallback', '|', '' ?? 'f', '|', '' || 'f');
let a = null, b = 0, c;
a ??= 'assigned'; b ||= 'assigned'; c &&= 'never';
console.log('??= ||= &&=   :', a, b, c);
// ?? cannot be mixed with && / || without parens
try { eval('null ?? 1 || 2'); } catch (e) { console.log('mixing ?? and ||:', e.constructor.name); }

// ---------- destructuring ----------
const cfg = { host: 'x', port: undefined, nested: { deep: [1, 2, 3] } };
const { host: h = 'localhost', port = 8080, missing: m = 'default', nested: { deep: [, second, ...restDeep] } } = cfg;
console.log('\nrename+default:', h, port, m, second, restDeep);
const { host, ...others } = cfg;
console.log('rest in object:', Object.keys(others));
function draw({ x = 0, y = 0, ...opts } = {}) { return `${x},${y} ${JSON.stringify(opts)}`; }
console.log('param default :', draw(), '|', draw({ x: 1, color: 'red' }));
const [p = 1, q = 2] = [undefined, null];
console.log('defaults only fire on undefined:', p, q);
// swap and nested array destructuring
let s1 = 1, s2 = 2; [s1, s2] = [s2, s1];
console.log('swap          :', s1, s2);
const { length } = 'hello';
console.log('destructure a string:', length);
// destructuring in for..of over entries
for (const [k, v] of Object.entries({ a: 1 })) console.log('entries       :', k, v);

// ---------- labelled statements ----------
console.log('\nlabelled break/continue:');
const found = [];
outer:
for (let i = 0; i < 4; i++) {
  for (let j = 0; j < 4; j++) {
    if (i * j > 4) break outer;
    if (j === 2) continue outer;
    found.push(`${i}${j}`);
  }
}
console.log('  ', found.join(' '));
// a labelled block is a legal goto-forward
blk: { console.log('   in block'); if (true) break blk; console.log('   never'); }

// ---------- at() ----------
console.log('\nat():', [1, 2, 3].at(-1), 'abc'.at(-1), new Int8Array([1, 2]).at(-1));

// ---------- Object.groupBy / Map.groupBy ----------
const people = [
  { name: 'ann', dept: 'eng', age: 31 }, { name: 'bob', dept: 'eng', age: 45 },
  { name: 'cat', dept: 'ops', age: 27 },
];
console.log('\nObject.groupBy:', JSON.stringify(Object.groupBy(people, (p) => p.dept), null, 0));
const byDecade = Map.groupBy(people, (p) => Math.floor(p.age / 10) * 10);
console.log('Map.groupBy   :', [...byDecade].map(([k, v]) => `${k}s: ${v.map((x) => x.name)}`).join(' | '));
console.log('groupBy result has null prototype:', Object.getPrototypeOf(Object.groupBy([], () => 'k')) === null);

// ---------- non-mutating array methods (ES2023) ----------
const arr = [3, 1, 2];
console.log('\ntoSorted   :', arr.toSorted((x, y) => x - y), '| original:', arr);
console.log('toReversed :', arr.toReversed(), '| original:', arr);
console.log('toSpliced  :', arr.toSpliced(1, 1, 'X', 'Y'), '| original:', arr);
console.log('with       :', arr.with(0, 99), '| original:', arr);
console.log('with(-1)   :', arr.with(-1, 99));
console.log('sort is stable & default is LEXICOGRAPHIC:', [10, 9, 1].sort(), 'vs', [10, 9, 1].toSorted((a, b) => a - b));

// ---------- findLast / flat / flatMap ----------
console.log('\nfindLast      :', [1, 2, 3, 4].findLast((x) => x % 2 === 1), '| findLastIndex:', [1, 2, 3, 4].findLastIndex((x) => x % 2 === 1));
console.log('flat(Infinity):', [1, [2, [3, [4, [5]]]]].flat(Infinity));
console.log('flat removes holes:', [1, , 3].flat());
console.log('flatMap       :', [1, 2, 3].flatMap((x) => (x % 2 ? [x, x] : [])));
console.log('copyWithin/fill:', [1, 2, 3, 4, 5].copyWithin(0, 3), new Array(3).fill(0));

// ---------- Set methods (ES2025) ----------
const A = new Set([1, 2, 3]), B = new Set([3, 4]);
console.log('\nunion             :', [...A.union(B)]);
console.log('intersection      :', [...A.intersection(B)]);
console.log('difference        :', [...A.difference(B)]);
console.log('symmetricDifference:', [...A.symmetricDifference(B)]);
console.log('isSubsetOf/isSupersetOf/isDisjointFrom:',
  new Set([1]).isSubsetOf(A), A.isSupersetOf(new Set([1])), A.isDisjointFrom(new Set([9])));

// ---------- string helpers ----------
console.log('\nreplaceAll    :', 'a-b-c'.replaceAll('-', '+'));
console.log('matchAll      :', [...'a1b2'.matchAll(/(\w)(\d)/g)].map((m) => m[1] + m[2]));
console.log('padStart/trimEnd:', '5'.padStart(3, '0'), JSON.stringify('  x  '.trimEnd()));
console.log('raw template  :', String.raw`a\nb`);
console.log('tagged template:', ((strings, ...v) => strings.raw.join('|') + ' :: ' + v)`x${1}y${2}z`);
console.log('localeCompare :', ['b', 'a', 'ä'].sort((x, y) => x.localeCompare(y, 'de')));

// ---------- numeric & misc ----------
console.log('\nnumeric separators:', 1_000_000, '| exponent:', 2 ** 10, '| binary:', 0b1010, '| octal:', 0o17);
console.log('Object.fromEntries:', Object.fromEntries([['a', 1], ['b', 2]]));
console.log('spread into call  :', Math.max(...[1, 5, 3]));
console.log('computed keys     :', { [`k${1}`]: 'v' });
console.log('Symbol.toStringTag:', Object.prototype.toString.call(new (class { get [Symbol.toStringTag]() { return 'Custom'; } })()));
console.log('error cause       :', new Error('outer', { cause: new Error('inner') }).cause.message);
console.log('Array.prototype.includes vs indexOf on NaN:', [NaN].includes(NaN), [NaN].indexOf(NaN));
console.log('structuredClone   :', structuredClone(new Map([['a', 1]])).get('a'));

js-core/18-using.ts

// Explicit resource management (ES2026 `using` / `await using`).
// Node 22's V8 12.4 does not parse `using` natively; run through tsx (esbuild downlevels it).
// Native support arrives with V8 13.x (Node 24+).

class FileHandle {
  constructor(public readonly name: string) { console.log(`  open  ${name}`); }
  read(): string { return `<${this.name} contents>`; }
  [Symbol.dispose](): void { console.log(`  close ${this.name}`); }
}

class Connection {
  constructor(public readonly id: number) { console.log(`  connect ${id}`); }
  async query(): Promise<string> { return `rows for ${this.id}`; }
  async [Symbol.asyncDispose](): Promise<void> {
    await new Promise((r) => setTimeout(r, 5));
    console.log(`  disconnect ${this.id} (awaited)`);
  }
}

function syncScope(): void {
  console.log('sync scope:');
  using f1 = new FileHandle('a.txt');
  using f2 = new FileHandle('b.txt');
  console.log('  read  ', f1.read(), f2.read());
  // disposal happens in REVERSE order at scope exit, even on throw
}

async function asyncScope(): Promise<void> {
  console.log('\nasync scope:');
  await using c = new Connection(1);
  console.log('  query ', await c.query());
}

function throwingScope(): void {
  console.log('\nthrowing scope:');
  try {
    using f = new FileHandle('c.txt');
    throw new Error('boom');
  } catch (e) {
    console.log('  caught', (e as Error).message, '— dispose still ran first');
  }
}

// DisposableStack is part of the same proposal but is absent from Node 22 — here is the shape.
class MiniDisposableStack {
  #stack: Array<() => void> = [];
  use<T extends { [Symbol.dispose](): void }>(r: T): T { this.#stack.push(() => r[Symbol.dispose]()); return r; }
  defer(fn: () => void): void { this.#stack.push(fn); }
  dispose(): void { while (this.#stack.length) this.#stack.pop()!(); }
  [Symbol.dispose](): void { this.dispose(); }
}

function stackScope(): void {
  console.log('\nDisposableStack (manual composition; polyfilled — absent in Node 22):');
  console.log('  native DisposableStack available:', typeof (globalThis as Record<string, unknown>).DisposableStack !== 'undefined');
  using stack = new MiniDisposableStack();
  stack.use(new FileHandle('d.txt'));
  stack.defer(() => console.log('  deferred cleanup'));
}

syncScope();
throwingScope();
stackScope();
void asyncScope();

js-core/19-implementations.js

'use strict';
// ============================================================ tiny test harness
let pass = 0, fail = 0;
const lines = [];
function t(name, fn) {
  try { fn(); pass++; lines.push(`  ok   ${name}`); }
  catch (e) { fail++; lines.push(`  FAIL ${name}: ${e.message}`); }
}
const deepEqCheck = (a, b) => JSON.stringify(a) === JSON.stringify(b);
const eq = (a, b, m = '') => { if (!deepEqCheck(a, b)) throw new Error(`${m} expected ${JSON.stringify(b)}, got ${JSON.stringify(a)}`); };
const ok = (v, m = 'expected truthy') => { if (!v) throw new Error(m); };

// ============================================================ 1. Function.prototype.bind
Function.prototype.myBind = function (thisArg, ...bound) {
  if (typeof this !== 'function') throw new TypeError('Bind must be called on a function');
  const target = this;
  function BoundFunction(...args) {
    // `new BoundFunction()` ignores thisArg and constructs the target instead
    return new.target
      ? Reflect.construct(target, [...bound, ...args], new.target === BoundFunction ? target : new.target)
      : target.apply(thisArg, [...bound, ...args]);
  }
  // preserve the prototype chain so `instanceof` still works
  if (target.prototype) BoundFunction.prototype = Object.create(target.prototype);
  Object.defineProperty(BoundFunction, 'length', { value: Math.max(0, target.length - bound.length), configurable: true });
  Object.defineProperty(BoundFunction, 'name', { value: 'bound ' + target.name, configurable: true });
  return BoundFunction;
};
t('myBind binds this', () => eq(function () { return this.x; }.myBind({ x: 1 })(), 1));
t('myBind partially applies', () => eq(((a, b, c) => a + b + c).myBind(null, 1, 2)(3), 6));
t('myBind works with new', () => {
  function P(a, b) { this.a = a; this.b = b; }
  P.prototype.sum = function () { return this.a + this.b; };
  const B = P.myBind({ ignored: true }, 10);
  const inst = new B(5);
  eq(inst.sum(), 15); ok(inst instanceof P, 'instanceof target');
});
t('myBind length/name', () => { const f = ((a, b, c) => 0).myBind(null, 1); eq(f.length, 2); eq(f.name, 'bound '); });

// ============================================================ 2. call / apply
Function.prototype.myCall = function (thisArg, ...args) {
  const ctx = thisArg === null || thisArg === undefined ? globalThis : Object(thisArg);
  const key = Symbol('fn');
  Object.defineProperty(ctx, key, { value: this, configurable: true });
  try { return ctx[key](...args); } finally { delete ctx[key]; }
};
Function.prototype.myApply = function (thisArg, args = []) { return this.myCall(thisArg, ...args); };
t('myCall / myApply', () => {
  function greet(g, p) { return `${g}, ${this.name}${p}`; }
  eq(greet.myCall({ name: 'A' }, 'hi', '!'), 'hi, A!');
  eq(greet.myApply({ name: 'B' }, ['yo', '?']), 'yo, B?');
});

// ============================================================ 3. map / filter / reduce / forEach
Array.prototype.myMap = function (cb, thisArg) {
  if (this == null) throw new TypeError('called on null or undefined');
  if (typeof cb !== 'function') throw new TypeError(cb + ' is not a function');
  const o = Object(this), len = o.length >>> 0;
  const out = new Array(len);
  for (let i = 0; i < len; i++) if (i in o) out[i] = cb.call(thisArg, o[i], i, o);   // skip holes
  return out;
};
Array.prototype.myFilter = function (cb, thisArg) {
  const o = Object(this), len = o.length >>> 0, out = [];
  for (let i = 0; i < len; i++) if (i in o && cb.call(thisArg, o[i], i, o)) out.push(o[i]);
  return out;
};
Array.prototype.myReduce = function (cb, ...init) {
  const o = Object(this), len = o.length >>> 0;
  let i = 0, acc;
  if (init.length) acc = init[0];
  else {
    while (i < len && !(i in o)) i++;
    if (i >= len) throw new TypeError('Reduce of empty array with no initial value');
    acc = o[i++];
  }
  for (; i < len; i++) if (i in o) acc = cb(acc, o[i], i, o);
  return acc;
};
t('myMap', () => eq([1, 2, 3].myMap((x) => x * 2), [2, 4, 6]));
t('myMap preserves holes', () => { const r = [1, , 3].myMap((x) => x * 2); eq(1 in r, false); eq(r.length, 3); });
t('myMap passes index and array', () => eq([10, 20].myMap((v, i, a) => v + i + a.length), [12, 23]));
t('myFilter', () => eq([1, 2, 3, 4].myFilter((x) => x % 2 === 0), [2, 4]));
t('myReduce with init', () => eq([1, 2, 3].myReduce((a, b) => a + b, 10), 16));
t('myReduce without init', () => eq([1, 2, 3].myReduce((a, b) => a + b), 6));
t('myReduce empty throws', () => { try { [].myReduce((a, b) => a + b); throw new Error('no throw'); } catch (e) { ok(e instanceof TypeError); } });

// ============================================================ 4. deep clone with cycles
function deepClone(value, seen = new WeakMap()) {
  if (value === null || typeof value !== 'object') return value;      // primitives + functions returned as-is
  if (seen.has(value)) return seen.get(value);                        // cycle / shared reference
  if (value instanceof Date) return new Date(value.getTime());
  if (value instanceof RegExp) { const r = new RegExp(value.source, value.flags); r.lastIndex = value.lastIndex; return r; }
  if (ArrayBuffer.isView(value)) return new value.constructor(value);
  if (value instanceof ArrayBuffer) return value.slice(0);
  if (value instanceof Map) {
    const out = new Map(); seen.set(value, out);
    for (const [k, v] of value) out.set(deepClone(k, seen), deepClone(v, seen));
    return out;
  }
  if (value instanceof Set) {
    const out = new Set(); seen.set(value, out);
    for (const v of value) out.add(deepClone(v, seen));
    return out;
  }
  if (value instanceof Error) {
    const out = new value.constructor(value.message, value.cause !== undefined ? { cause: deepClone(value.cause, seen) } : undefined);
    seen.set(value, out); out.stack = value.stack;
    return out;
  }
  const out = Array.isArray(value) ? [] : Object.create(Object.getPrototypeOf(value));
  seen.set(value, out);
  for (const key of Reflect.ownKeys(value)) {
    const d = Object.getOwnPropertyDescriptor(value, key);
    if ('value' in d) Object.defineProperty(out, key, { ...d, value: deepClone(d.value, seen) });
    else Object.defineProperty(out, key, d);                          // keep getters/setters
  }
  return out;
}
t('deepClone nested', () => { const s = { a: { b: [1, { c: 2 }] } }; const c = deepClone(s); eq(c, s); ok(c.a !== s.a && c.a.b[1] !== s.a.b[1]); });
t('deepClone cycles', () => { const s = { n: 1 }; s.self = s; const c = deepClone(s); ok(c.self === c); });
t('deepClone shared refs stay shared', () => { const shared = { x: 1 }; const c = deepClone({ a: shared, b: shared }); ok(c.a === c.b); });
t('deepClone Map/Set/Date/RegExp', () => {
  const c = deepClone({ m: new Map([['k', { v: 1 }]]), s: new Set([1]), d: new Date(5), r: /x/g });
  eq(c.m.get('k').v, 1); ok(c.s.has(1)); eq(c.d.getTime(), 5); eq(String(c.r), '/x/g');
});
t('deepClone keeps prototype', () => { class C { constructor() { this.x = 1; } } ok(deepClone(new C()) instanceof C); });
t('deepClone keeps symbols and non-enumerables', () => {
  const sym = Symbol('s'); const src = { [sym]: 1 };
  Object.defineProperty(src, 'hidden', { value: 2, enumerable: false });
  const c = deepClone(src); eq(c[sym], 1); eq(c.hidden, 2);
});

// ============================================================ 5. deep equal
function deepEqual(a, b, seen = new WeakMap()) {
  if (Object.is(a, b)) return true;
  if (typeof a !== typeof b) return false;
  if (a === null || b === null || typeof a !== 'object') return false;
  if (Object.getPrototypeOf(a) !== Object.getPrototypeOf(b)) return false;
  if (seen.get(a) === b) return true;                                 // assume equal while recursing (cycles)
  seen.set(a, b);
  if (a instanceof Date) return a.getTime() === b.getTime();
  if (a instanceof RegExp) return a.source === b.source && a.flags === b.flags;
  if (a instanceof Map) {
    if (a.size !== b.size) return false;
    for (const [k, v] of a) {
      if (!b.has(k)) {                                                 // key may be a deep-equal object
        const match = [...b.keys()].find((bk) => deepEqual(k, bk, seen));
        if (match === undefined) return false;
        if (!deepEqual(v, b.get(match), seen)) return false;
      } else if (!deepEqual(v, b.get(k), seen)) return false;
    }
    return true;
  }
  if (a instanceof Set) {
    if (a.size !== b.size) return false;
    for (const v of a) if (!b.has(v) && ![...b].some((bv) => deepEqual(v, bv, seen))) return false;
    return true;
  }
  if (ArrayBuffer.isView(a)) return a.length === b.length && [...a].every((v, i) => Object.is(v, b[i]));
  const ka = Reflect.ownKeys(a), kb = Reflect.ownKeys(b);
  if (ka.length !== kb.length) return false;
  return ka.every((k) => Object.prototype.hasOwnProperty.call(b, k) && deepEqual(a[k], b[k], seen));
}
t('deepEqual basics', () => { ok(deepEqual({ a: [1, { b: 2 }] }, { a: [1, { b: 2 }] })); ok(!deepEqual({ a: 1 }, { a: 2 })); });
t('deepEqual NaN and -0', () => { ok(deepEqual(NaN, NaN)); ok(!deepEqual(0, -0)); });
t('deepEqual key count', () => ok(!deepEqual({ a: 1 }, { a: 1, b: undefined })));
t('deepEqual different prototypes', () => { class C {} ok(!deepEqual(new C(), {})); });
t('deepEqual cycles', () => { const a = { }; a.s = a; const b = {}; b.s = b; ok(deepEqual(a, b)); });
t('deepEqual Map/Set', () => { ok(deepEqual(new Map([['k', [1]]]), new Map([['k', [1]]]))); ok(deepEqual(new Set([{ a: 1 }]), new Set([{ a: 1 }]))); });

// ============================================================ 6. flatten with depth
function flatten(arr, depth = 1) {
  const out = [];
  const step = (a, d) => {
    for (const v of a) (Array.isArray(v) && d > 0) ? step(v, d - 1) : out.push(v);
  };
  step(arr, depth);
  return out;
}
function flattenIterative(arr) {          // no recursion: stack-based, safe for deep nesting
  const stack = [...arr], out = [];
  while (stack.length) {
    const v = stack.pop();
    if (Array.isArray(v)) stack.push(...v); else out.push(v);
  }
  return out.reverse();
}
t('flatten depth 1', () => eq(flatten([1, [2, [3, [4]]]]), [1, 2, [3, [4]]]));
t('flatten depth 2', () => eq(flatten([1, [2, [3, [4]]]], 2), [1, 2, 3, [4]]));
t('flatten Infinity', () => eq(flatten([1, [2, [3, [4]]]], Infinity), [1, 2, 3, 4]));
t('flattenIterative', () => eq(flattenIterative([1, [2, [3, [4]]]]), [1, 2, 3, 4]));
t('flatten deep without stack overflow', () => {
  let deep = [1]; for (let i = 0; i < 50_000; i++) deep = [deep];
  eq(flattenIterative(deep), [1]);
});

// ============================================================ 7. once
function once(fn) {
  let called = false, result;
  const wrapped = function (...args) {
    if (called) return result;
    called = true;
    result = fn.apply(this, args);
    fn = null;                      // let the closure be collected
    return result;
  };
  wrapped.reset = () => { called = false; result = undefined; };
  return wrapped;
}
t('once', () => { let n = 0; const f = once(() => ++n); eq([f(), f(), f()], [1, 1, 1]); eq(n, 1); });

// ============================================================ 8. EventEmitter
class Emitter {
  #events = new Map();
  on(name, fn) { (this.#events.get(name) ?? this.#events.set(name, new Set()).get(name)).add(fn); return () => this.off(name, fn); }
  once(name, fn) {
    const wrap = (...a) => { this.off(name, wrap); fn(...a); };
    wrap.listener = fn;
    return this.on(name, wrap);
  }
  off(name, fn) {
    const set = this.#events.get(name);
    if (!set) return this;
    for (const l of set) if (l === fn || l.listener === fn) set.delete(l);
    if (set.size === 0) this.#events.delete(name);
    return this;
  }
  emit(name, ...args) {
    const set = this.#events.get(name);
    if (!set || set.size === 0) {
      if (name === 'error') throw args[0] instanceof Error ? args[0] : new Error('Unhandled error event');
      return false;
    }
    for (const l of [...set]) l(...args);   // copy: a listener may unsubscribe during emit
    return true;
  }
  listenerCount(name) { return this.#events.get(name)?.size ?? 0; }
}
t('Emitter on/emit/off', () => {
  const e = new Emitter(); const seen = [];
  const off = e.on('x', (v) => seen.push('a' + v));
  e.on('x', (v) => seen.push('b' + v));
  e.emit('x', 1); off(); e.emit('x', 2);
  eq(seen, ['a1', 'b1', 'b2']);
});
t('Emitter once', () => { const e = new Emitter(); let n = 0; e.once('y', () => n++); e.emit('y'); e.emit('y'); eq(n, 1); });
t('Emitter safe removal during emit', () => {
  const e = new Emitter(); const seen = [];
  const h1 = () => { seen.push(1); e.off('z', h2); };
  const h2 = () => seen.push(2);
  e.on('z', h1); e.on('z', h2); e.emit('z');
  eq(seen, [1, 2]);   // snapshot semantics: h2 still runs this round
});
t('Emitter throws on unhandled error', () => { try { new Emitter().emit('error', new Error('e')); throw new Error('no throw'); } catch (e) { eq(e.message, 'e'); } });

// ============================================================ 9. LRU cache with Map
class LRU {
  #max; #map = new Map();
  constructor(max) { this.#max = max; }
  get size() { return this.#map.size; }
  has(k) { return this.#map.has(k); }
  get(k) {
    if (!this.#map.has(k)) return undefined;
    const v = this.#map.get(k);
    this.#map.delete(k); this.#map.set(k, v);      // Map preserves insertion order -> re-insert = "recently used"
    return v;
  }
  set(k, v) {
    if (this.#map.has(k)) this.#map.delete(k);
    else if (this.#map.size >= this.#max) this.#map.delete(this.#map.keys().next().value);  // evict oldest
    this.#map.set(k, v);
    return this;
  }
  delete(k) { return this.#map.delete(k); }
  keys() { return [...this.#map.keys()]; }
}
t('LRU evicts least recently used', () => {
  const c = new LRU(3);
  c.set('a', 1).set('b', 2).set('c', 3);
  c.get('a');                       // a becomes most-recent
  c.set('d', 4);                    // evicts b
  eq(c.keys(), ['c', 'a', 'd']);
  eq(c.get('b'), undefined);
});
t('LRU update refreshes recency', () => { const c = new LRU(2); c.set('a', 1).set('b', 2).set('a', 9).set('c', 3); eq(c.keys(), ['a', 'c']); });

// ============================================================ 10. retry with exponential backoff + jitter
async function retry(fn, { attempts = 5, baseMs = 10, maxMs = 1000, factor = 2, jitter = true, signal, shouldRetry = () => true } = {}) {
  let lastErr;
  for (let i = 0; i < attempts; i++) {
    if (signal?.aborted) throw signal.reason;
    try { return await fn(i); }
    catch (e) {
      lastErr = e;
      if (i === attempts - 1 || !shouldRetry(e, i)) break;
      const exp = Math.min(maxMs, baseMs * factor ** i);
      const delay = jitter ? Math.random() * exp : exp;              // full jitter
      await new Promise((res, rej) => {
        const id = setTimeout(res, delay);
        signal?.addEventListener('abort', () => { clearTimeout(id); rej(signal.reason); }, { once: true });
      });
    }
  }
  throw lastErr;
}

// ============================================================ 11. promisify / callbackify
function promisify(fn) {
  return function (...args) {
    return new Promise((resolve, reject) => {
      fn.call(this, ...args, (err, ...values) => (err ? reject(err) : resolve(values.length > 1 ? values : values[0])));
    });
  };
}
function callbackify(asyncFn) {
  return function (...args) {
    const cb = args.pop();
    asyncFn.apply(this, args).then((v) => cb(null, v), (e) => cb(e || new Error('rejected with falsy value')));
  };
}

// ============================================================ 12. chunk / zip / range / groupBy / uniqBy / partition
const chunk = (arr, size) => {
  if (size < 1) throw new RangeError('size must be >= 1');
  return Array.from({ length: Math.ceil(arr.length / size) }, (_, i) => arr.slice(i * size, i * size + size));
};
const zip = (...arrays) => Array.from({ length: Math.min(...arrays.map((a) => a.length)) }, (_, i) => arrays.map((a) => a[i]));
const range = (start, end, step = 1) => Array.from({ length: Math.max(0, Math.ceil((end - start) / step)) }, (_, i) => start + i * step);
const groupBy = (arr, keyFn) => arr.reduce((acc, item, i) => { const k = keyFn(item, i); (acc[k] ??= []).push(item); return acc; }, Object.create(null));
const uniqBy = (arr, keyFn = (x) => x) => { const seen = new Set(); return arr.filter((x) => { const k = keyFn(x); return seen.has(k) ? false : (seen.add(k), true); }); };
const partition = (arr, pred) => arr.reduce(([yes, no], x, i) => (pred(x, i) ? [[...yes, x], no] : [yes, [...no, x]]), [[], []]);
const countBy = (arr, keyFn) => arr.reduce((acc, x) => { const k = keyFn(x); acc[k] = (acc[k] ?? 0) + 1; return acc; }, Object.create(null));
t('chunk', () => eq(chunk([1, 2, 3, 4, 5], 2), [[1, 2], [3, 4], [5]]));
t('zip', () => eq(zip([1, 2, 3], 'ab'.split('')), [[1, 'a'], [2, 'b']]));
t('range', () => { eq(range(0, 5), [0, 1, 2, 3, 4]); eq(range(0, 10, 3), [0, 3, 6, 9]); eq(range(5, 0), []); });
t('groupBy', () => eq(groupBy(['one', 'two', 'three'], (s) => s.length), { 3: ['one', 'two'], 5: ['three'] }));
t('uniqBy', () => eq(uniqBy([{ id: 1 }, { id: 1 }, { id: 2 }], (o) => o.id).length, 2));
t('partition', () => eq(partition([1, 2, 3, 4], (x) => x % 2), [[1, 3], [2, 4]]));
t('countBy', () => eq(countBy('aabbbc'.split(''), (c) => c), { a: 2, b: 3, c: 1 }));

// ============================================================ 13. get / set by path, deepFreeze, pick/omit
const get = (obj, path, fallback) => {
  const keys = Array.isArray(path) ? path : path.replace(/\[(\d+)\]/g, '.$1').split('.').filter(Boolean);
  let cur = obj;
  for (const k of keys) { if (cur == null) return fallback; cur = cur[k]; }
  return cur === undefined ? fallback : cur;
};
const setPath = (obj, path, value) => {
  const keys = Array.isArray(path) ? path : path.replace(/\[(\d+)\]/g, '.$1').split('.').filter(Boolean);
  let cur = obj;
  keys.forEach((k, i) => {
    if (i === keys.length - 1) cur[k] = value;
    else { if (cur[k] == null || typeof cur[k] !== 'object') cur[k] = /^\d+$/.test(keys[i + 1]) ? [] : {}; cur = cur[k]; }
  });
  return obj;
};
function deepFreeze(o, seen = new WeakSet()) {
  if (o === null || typeof o !== 'object' || seen.has(o)) return o;
  seen.add(o);
  Object.freeze(o);
  for (const k of Reflect.ownKeys(o)) {
    const d = Object.getOwnPropertyDescriptor(o, k);
    if ('value' in d) deepFreeze(d.value, seen);       // do not trigger getters
  }
  return o;
}
const pick = (o, keys) => Object.fromEntries(keys.filter((k) => k in o).map((k) => [k, o[k]]));
const omit = (o, keys) => Object.fromEntries(Object.entries(o).filter(([k]) => !keys.includes(k)));
t('get by path', () => { eq(get({ a: { b: [{ c: 1 }] } }, 'a.b[0].c'), 1); eq(get({}, 'x.y', 'def'), 'def'); });
t('set by path', () => eq(setPath({}, 'a.b[1].c', 7), { a: { b: [null, { c: 7 }] } }));
t('deepFreeze', () => { const o = deepFreeze({ a: { b: 1 } }); try { o.a.b = 2; } catch {} eq(o.a.b, 1); ok(Object.isFrozen(o.a)); });
t('deepFreeze cycles', () => { const o = { }; o.self = o; deepFreeze(o); ok(Object.isFrozen(o)); });
t('pick/omit', () => { eq(pick({ a: 1, b: 2, c: 3 }, ['a', 'c']), { a: 1, c: 3 }); eq(omit({ a: 1, b: 2 }, ['a']), { b: 2 }); });

// ============================================================ 14. implement `new`
function myNew(Ctor, ...args) {
  if (typeof Ctor !== 'function') throw new TypeError('not a constructor');
  const proto = typeof Ctor.prototype === 'object' && Ctor.prototype !== null ? Ctor.prototype : Object.prototype;
  const obj = Object.create(proto);
  const ret = Ctor.apply(obj, args);
  return (ret !== null && (typeof ret === 'object' || typeof ret === 'function')) ? ret : obj;
}
t('myNew', () => {
  function P(x) { this.x = x; }
  P.prototype.get = function () { return this.x; };
  const i = myNew(P, 5);
  eq(i.get(), 5); ok(i instanceof P);
});
t('myNew honours an explicit object return', () => { function P() { this.a = 1; return { b: 2 }; } eq(myNew(P), { b: 2 }); });

// ============================================================ 15. instanceof
function myInstanceOf(obj, Ctor) {
  if (typeof Ctor !== 'function') throw new TypeError('Right-hand side is not callable');
  if (typeof Ctor[Symbol.hasInstance] === 'function' && Ctor[Symbol.hasInstance] !== Function.prototype[Symbol.hasInstance]) {
    return !!Ctor[Symbol.hasInstance](obj);
  }
  if (obj === null || (typeof obj !== 'object' && typeof obj !== 'function')) return false;
  const target = Ctor.prototype;
  let proto = Object.getPrototypeOf(obj);
  while (proto !== null) { if (proto === target) return true; proto = Object.getPrototypeOf(proto); }
  return false;
}
t('myInstanceOf', () => { class A {} class B extends A {} ok(myInstanceOf(new B(), A)); ok(!myInstanceOf({}, A)); ok(!myInstanceOf(1, A)); });

// ============================================================ 16. throttle/debounce/curry live in 07-functions.js
// ============================================================ 17. sleep, timeout wrapper, defer
const sleep = (ms, signal) => new Promise((res, rej) => {
  const id = setTimeout(res, ms);
  signal?.addEventListener('abort', () => { clearTimeout(id); rej(signal.reason); }, { once: true });
});
const withTimeout = (promise, ms, msg = 'Timed out') => {
  const ac = new AbortController();
  const timer = new Promise((_, rej) => setTimeout(() => rej(new Error(msg)), ms));
  return Promise.race([promise.finally(() => ac.abort()), timer]);
};

// ============================================================ async tests
(async () => {
  // retry
  let tries = 0;
  const flaky = async () => { if (++tries < 3) throw new Error('flaky'); return 'ok after ' + tries; };
  const r = await retry(flaky, { attempts: 5, baseMs: 1 });
  t('retry succeeds after transient failures', () => eq(r, 'ok after 3'));

  tries = 0;
  try {
    await retry(async () => { tries++; throw new Error('always'); }, { attempts: 3, baseMs: 1 });
    t('retry gives up', () => { throw new Error('should have thrown'); });
  } catch (e) { t('retry gives up after N attempts', () => { eq(e.message, 'always'); eq(tries, 3); }); }

  tries = 0;
  try {
    await retry(async () => { tries++; const e = new Error('fatal'); e.status = 400; throw e; },
      { attempts: 5, baseMs: 1, shouldRetry: (e) => e.status >= 500 });
  } catch { }
  t('retry respects shouldRetry', () => eq(tries, 1));

  // promisify / callbackify
  const readish = (n, cb) => setTimeout(() => (n < 0 ? cb(new Error('neg')) : cb(null, n * 2)), 1);
  const readAsync = promisify(readish);
  const promisifiedValue = await readAsync(5);
  t('promisify resolves', () => eq(promisifiedValue, 10));
  let promisifyErr; try { await readAsync(-1); } catch (e) { promisifyErr = e.message; }
  t('promisify rejects', () => eq(promisifyErr, 'neg'));
  const back = callbackify(async (x) => x + 1);
  await new Promise((res) => back(1, (err, v) => { t('callbackify', () => { eq(err, null); eq(v, 2); }); res(); }));

  // withTimeout
  let timedOut = false;
  try { await withTimeout(sleep(50), 10); } catch (e) { timedOut = e.message === 'Timed out'; }
  t('withTimeout rejects slow work', () => ok(timedOut));
  const fastValue = await withTimeout(Promise.resolve('fast'), 50);
  t('withTimeout passes fast work', () => eq(fastValue, 'fast'));

  console.log(lines.join('\n'));
  console.log(`\n${pass} passed, ${fail} failed`);
  if (fail) process.exitCode = 1;
})();

js-core/20-puzzles.js

'use strict';
const show = (label, fn) => { try { console.log(label.padEnd(52), '=>', JSON.stringify(fn(), (k, v) => (typeof v === 'undefined' ? '<undefined>' : v))); } catch (e) { console.log(label.padEnd(52), '=>', e.constructor.name + ': ' + e.message.split('\n')[0]); } };

show('typeof null', () => typeof null);
show('typeof function(){}', () => typeof function () {});
show('typeof NaN', () => typeof NaN);
show('typeof undeclared', () => typeof undeclaredThing);
show('[] + []', () => [] + []);
show('[] + {}', () => [] + {});
show('({}) + []', () => ({}) + []);
show('1 + "2" - 1', () => 1 + '2' - 1);
show('"5" - - "2"', () => '5' - - '2');
show('0.1 + 0.2 == 0.3', () => 0.1 + 0.2 == 0.3);
show('[1,2,3] + [4,5]', () => [1, 2, 3] + [4, 5]);
show('typeof (()=>{})()', () => typeof (() => {})());
show('[10,1,3].sort()', () => [10, 1, 3].sort());
show('["1","7","11"].map(parseInt)', () => ['1', '7', '11'].map(parseInt));
show('["1","7","11"].map(Number)', () => ['1', '7', '11'].map(Number));
show('[1,2,3].map(x=>x*2).filter(x=>x>2)', () => [1, 2, 3].map((x) => x * 2).filter((x) => x > 2));
show('Array(3)', () => Array(3));
show('Array(3).map(()=>1)', () => Array(3).map(() => 1));
show('[...Array(3)].map(()=>1)', () => [...Array(3)].map(() => 1));
show('Array.from({length:3}, (_,i)=>i)', () => Array.from({ length: 3 }, (_, i) => i));
show('[,,].length', () => [, , ].length);
show('"b"+"a"+ +"a"+"a"', () => 'b' + 'a' + +'a' + 'a');
show('!!"false" == !!"true"', () => !!'false' == !!'true');
show('typeof typeof 1', () => typeof typeof 1);
show('(function(){return typeof arguments})()', () => (function () { return typeof arguments; })());
show('parseInt(0.0000005)', () => parseInt(0.0000005));
show('parseInt("08")', () => parseInt('08'));
show('Number("")', () => Number(''));
show('Number(" 12 ")', () => Number(' 12 '));
show('Number(null) / Number(undefined)', () => [Number(null), Number(undefined)]);
show('+[] / +{} / +"" ', () => [+[], String(+{}), +'']);
show('JSON.stringify({a:undefined,b:()=>1,c:NaN})', () => JSON.stringify({ a: undefined, b: () => 1, c: NaN }));
show('JSON.stringify([undefined, ()=>1])', () => JSON.stringify([undefined, () => 1]));
show('Object.keys("abc")', () => Object.keys('abc'));
show('"abc".length === [..."abc"].length', () => 'abc'.length === [...'abc'].length);
show('{} instanceof Object', () => ({}) instanceof Object);
show('Object.create(null) instanceof Object', () => Object.create(null) instanceof Object);
show('[1,[2,[3]]].flat()', () => [1, [2, [3]]].flat());
show('new Set("hello").size', () => new Set('hello').size);
show('[..."hello"].reverse().join("")', () => [...'hello'].reverse().join(''));
show('typeof class{}', () => typeof class {});
show('(()=>{ let x = y = 5; return typeof y })()', () => { let x; try { eval('x = y2 = 5'); } catch (e) { return e.constructor.name; } });
show('void 0 === undefined', () => void 0 === undefined);
show('(function f(){ return f.name })()', () => (function f() { return f.name; })());
show('const o={a:1}; delete o.a; o.a', () => { const o = { a: 1 }; delete o.a; return o.a; });
show('[1,2,3].indexOf("2")', () => [1, 2, 3].indexOf('2'));
show('["a","b"].forEach returns', () => ['a', 'b'].forEach(() => 1));
show('[3,1,2].sort((a,b)=>a-b) mutates?', () => { const a = [3, 1, 2]; a.sort((x, y) => x - y); return a; });
show('"5" * "2"', () => '5' * '2');
show('null + 1 / undefined + 1', () => [null + 1, undefined + 1]);
show('true + true + true', () => true + true + true);
show('[] == false == true', () => [] == false == true);
show('Math.max([1,2,3])', () => Math.max([1, 2, 3]));
show('new Array(3).fill().map((_,i)=>i)', () => new Array(3).fill().map((_, i) => i));
show('{a:1, a:2}.a', () => ({ a: 1, a: 2 }).a);
show('obj key order (int keys sort first)', () => Object.keys({ b: 1, 2: 1, a: 1, 1: 1 }));
show('typeof Symbol.iterator', () => typeof Symbol.iterator);
show('"" || null || 0 || "last"', () => '' || null || 0 || 'last');
show('({} ?? "x")', () => JSON.stringify({} ?? 'x'));
show('[..."😀"].length vs "😀".length', () => [[...'😀'].length, '😀'.length]);

js-core/bench.js

function bench(name, fn, iters=3e7){ fn(); const t=process.hrtime.bigint(); let s=0; for(let i=0;i<iters;i++) s+=fn(i); const d=Number(process.hrtime.bigint()-t)/1e6; console.log(name.padEnd(34), d.toFixed(1)+' ms', '(sink '+ (s%7) +')'); }
const mono = Array.from({length:1000},(_,i)=>({x:i,y:i}));
const poly = Array.from({length:1000},(_,i)=> i%4===0?{x:i,y:i}: i%4===1?{y:i,x:i}: i%4===2?{x:i,y:i,z:i}:{a:i,x:i,y:i});
let m=0,p=0;
bench('monomorphic .x read', (i)=> mono[(m++)%1000].x);
bench('polymorphic(4 shapes) .x read', (i)=> poly[(p++)%1000].x);
const packed = Array.from({length:100000},(_,i)=>i);
const holey  = Array.from({length:100000},(_,i)=>i); delete holey[5000];
const dbl    = Array.from({length:100000},(_,i)=>i+0.5);
function sum(a){let s=0;for(let i=0;i<a.length;i++)s+=a[i];return s;}
function t(name,a){ for(let k=0;k<50;k++)sum(a); const s=process.hrtime.bigint(); let acc=0; for(let k=0;k<300;k++)acc+=sum(a); const d=Number(process.hrtime.bigint()-s)/1e6; console.log(name.padEnd(34), d.toFixed(1)+' ms'); }
t('PACKED_SMI sum x300', packed); t('HOLEY_SMI sum x300', holey); t('PACKED_DOUBLE sum x300', dbl);

js-core/bench2.js

// Isolate IC state: same read function, different callsites, no shared counters.
function makeReader() { return new Function('objs', 'let s=0; for (let i=0;i<objs.length;i++) s+=objs[i].x; return s;'); }
const N = 20000;
const shapesMono = Array.from({length:N}, (_,i)=>({x:i, y:i}));
const shapes2    = Array.from({length:N}, (_,i)=> i%2 ? {x:i,y:i} : {y:i,x:i});
const shapes4    = Array.from({length:N}, (_,i)=> [{x:i,y:i},{y:i,x:i},{x:i,y:i,z:i},{a:i,x:i}][i%4]);
const shapes8    = Array.from({length:N}, (_,i)=> [{x:i},{x:i,y:i},{y:i,x:i},{x:i,y:i,z:i},{a:i,x:i},{b:i,x:i},{c:i,x:i},{d:i,e:i,x:i}][i%8]);
function time(label, arr) {
  const read = makeReader();               // fresh callsite each time
  for (let k=0;k<200;k++) read(arr);       // warm up / let ICs settle
  const t=process.hrtime.bigint(); let acc=0;
  for (let k=0;k<2000;k++) acc += read(arr);
  console.log(label.padEnd(30), (Number(process.hrtime.bigint()-t)/1e6).toFixed(1)+' ms');
}
time('monomorphic (1 shape)',   shapesMono);
time('polymorphic (2 shapes)',  shapes2);
time('polymorphic (4 shapes)',  shapes4);
time('megamorphic (8 shapes)',  shapes8);
console.log('--- holey vs packed, fresh fn per case ---');
function timeSum(label, arr){
  const sum = new Function('a','let s=0;for(let i=0;i<a.length;i++)s+=a[i];return s;');
  for(let k=0;k<200;k++) sum(arr);
  const t=process.hrtime.bigint(); let acc=0;
  for(let k=0;k<2000;k++) acc+=sum(arr);
  console.log(label.padEnd(30), (Number(process.hrtime.bigint()-t)/1e6).toFixed(1)+' ms');
}
const packed = Array.from({length:20000},(_,i)=>i);
const holey  = Array.from({length:20000},(_,i)=>i); delete holey[10];
const holey2 = new Array(20000); for(let i=0;i<20000;i++) if(i%2) holey2[i]=i;
timeSum('PACKED_SMI', packed);
timeSum('HOLEY_SMI (one delete)', holey);
timeSum('HOLEY_SMI (50% holes)', holey2);

js-core/feat.mjs

const has = (label, fn) => { let r; try { r = fn(); } catch (e) { r = 'throws: ' + e.constructor.name; } console.log(label.padEnd(38), r); };
has('Iterator helpers (.map on iterator)', () => typeof [].values().map === 'function');
has('Set.prototype.union', () => typeof new Set().union === 'function');
has('Object.groupBy', () => typeof Object.groupBy === 'function');
has('Map.groupBy', () => typeof Map.groupBy === 'function');
has('Promise.try', () => typeof Promise.try === 'function');
has('RegExp.escape', () => typeof RegExp.escape === 'function');
has('Error.isError', () => typeof Error.isError === 'function');
has('Array.fromAsync', () => typeof Array.fromAsync === 'function');
has('Float16Array', () => typeof Float16Array !== 'undefined');
has('Math.f16round', () => typeof Math.f16round === 'function');
has('Math.sumPrecise', () => typeof Math.sumPrecise === 'function');
has('Temporal', () => typeof globalThis.Temporal !== 'undefined');
has('Map.prototype.getOrInsert', () => typeof new Map().getOrInsert === 'function');
has('Uint8Array.prototype.toBase64', () => typeof new Uint8Array().toBase64 === 'function');
has('Symbol.dispose', () => typeof Symbol.dispose !== 'undefined');
has('Array.prototype.toSorted', () => typeof [].toSorted === 'function');
has('Array.prototype.with', () => typeof [].with === 'function');
has('structuredClone', () => typeof structuredClone === 'function');
has('Object.hasOwn', () => typeof Object.hasOwn === 'function');
has('AbortSignal.timeout', () => typeof AbortSignal.timeout === 'function');
console.log('--- set ops demo ---');
const a = new Set([1,2,3]), b = new Set([3,4]);
console.log([...a.union(b)], [...a.intersection(b)], [...a.difference(b)], [...a.symmetricDifference(b)], a.isDisjointFrom(new Set([9])));
console.log('--- groupBy ---');
console.log(JSON.stringify(Object.groupBy([1,2,3,4,5], n => n%2 ? 'odd':'even')));

js-core/myp.js

const PENDING='pending', FULFILLED='fulfilled', REJECTED='rejected';
class MyPromise {
  #state = PENDING; #value; #cbs = [];
  constructor(executor) {
    const resolve = v => this.#settle(FULFILLED, v);
    const reject  = r => this.#settle(REJECTED, r);
    try { executor(v => this.#resolveWith(v, resolve, reject), reject); }
    catch (e) { reject(e); }
  }
  #resolveWith(v, resolve, reject) {
    if (v === this) return reject(new TypeError('Chaining cycle detected'));
    if (v && (typeof v === 'object' || typeof v === 'function')) {
      let then;
      try { then = v.then; } catch (e) { return reject(e); }
      if (typeof then === 'function') {
        let called = false;
        try {
          return then.call(v,
            y => { if (called) return; called = true; this.#resolveWith(y, resolve, reject); },
            r => { if (called) return; called = true; reject(r); });
        } catch (e) { if (!called) reject(e); return; }
      }
    }
    resolve(v);
  }
  #settle(state, value) {
    if (this.#state !== PENDING) return;
    this.#state = state; this.#value = value;
    for (const cb of this.#cbs) queueMicrotask(cb);
    this.#cbs = [];
  }
  then(onFul, onRej) {
    return new MyPromise((resolve, reject) => {
      const run = () => queueMicrotask(() => {
        const handler = this.#state === FULFILLED ? onFul : onRej;
        if (typeof handler !== 'function') {
          this.#state === FULFILLED ? resolve(this.#value) : reject(this.#value);
          return;
        }
        try { resolve(handler(this.#value)); } catch (e) { reject(e); }
      });
      this.#state === PENDING ? this.#cbs.push(run) : run();
    });
  }
  catch(onRej) { return this.then(undefined, onRej); }
  finally(fn) { return this.then(v => { fn(); return v; }, r => { fn(); throw r; }); }
  static resolve(v) { return v instanceof MyPromise ? v : new MyPromise(res => res(v)); }
  static reject(r) { return new MyPromise((_, rej) => rej(r)); }
  static all(iter) {
    return new MyPromise((resolve, reject) => {
      const items = [...iter]; const out = new Array(items.length); let left = items.length;
      if (!left) return resolve([]);
      items.forEach((p, i) => MyPromise.resolve(p).then(v => { out[i] = v; if (--left === 0) resolve(out); }, reject));
    });
  }
  static race(iter) { return new MyPromise((res, rej) => { for (const p of iter) MyPromise.resolve(p).then(res, rej); }); }
  static allSettled(iter) {
    return MyPromise.all([...iter].map(p => MyPromise.resolve(p).then(
      value => ({ status: 'fulfilled', value }), reason => ({ status: 'rejected', reason }))));
  }
}
// tests
const log = [];
const t = (name, ok) => log.push((ok ? 'PASS ' : 'FAIL ') + name);
(async () => {
  t('resolve/then', await new MyPromise(r => r(1)).then(v => v + 1) === 2);
  t('reject/catch', await new MyPromise((_, j) => j('e')).catch(e => e + '!') === 'e!');
  t('thenable adoption', await new MyPromise(r => r({ then: (res) => res(42) })) === 42);
  t('nested promise', await new MyPromise(r => r(MyPromise.resolve(7))) === 7);
  t('all', JSON.stringify(await MyPromise.all([1, MyPromise.resolve(2), Promise.resolve(3)])) === '[1,2,3]');
  t('race', await MyPromise.race([new MyPromise(r => setTimeout(() => r('slow'), 20)), MyPromise.resolve('fast')]) === 'fast');
  t('allSettled', (await MyPromise.allSettled([MyPromise.resolve(1), MyPromise.reject('x')])).map(o => o.status).join() === 'fulfilled,rejected');
  t('throw in handler propagates', await MyPromise.resolve(1).then(() => { throw 'boom'; }).catch(e => e) === 'boom');
  t('settle once', await new MyPromise(r => { r('a'); r('b'); }) === 'a');
  t('finally passthrough', await MyPromise.resolve(5).finally(() => 99) === 5);
  const order = [];
  MyPromise.resolve().then(() => order.push('mt'));
  order.push('sync');
  await null; await null;
  t('async ordering', order.join() === 'sync,mt');
  console.log(log.join('\n'));
})();

js-core/p1.js

console.log('1 sync');
setTimeout(() => console.log('2 setTimeout 0'), 0);
setImmediate(() => console.log('3 setImmediate'));
process.nextTick(() => console.log('4 nextTick'));
Promise.resolve().then(() => console.log('5 microtask'));
queueMicrotask(() => console.log('6 queueMicrotask'));
(async () => { console.log('7 async body sync'); await null; console.log('8 after await'); })();
console.log('9 sync end');

js-core/p2.js

async function a() { console.log('a1'); await b(); console.log('a2'); }
async function b() { console.log('b1'); }
console.log('start');
a();
new Promise(r => { console.log('p exec'); r(); }).then(() => console.log('p then'));
console.log('end');

js-core/p3.js

setTimeout(() => console.log('timeout'), 0);
Promise.resolve().then(() => { console.log('mt1'); process.nextTick(() => console.log('tick in mt')); });
process.nextTick(() => { console.log('tick1'); Promise.resolve().then(() => console.log('mt in tick')); });

js-core/pool.js

async function pMap(items, mapper, concurrency = 4) {
  const out = new Array(items.length);
  let i = 0, active = 0, peak = 0;
  await new Promise((resolve, reject) => {
    const next = () => {
      if (i >= items.length && active === 0) return resolve();
      while (active < concurrency && i < items.length) {
        const idx = i++; active++; peak = Math.max(peak, active);
        Promise.resolve(mapper(items[idx], idx))
          .then(v => { out[idx] = v; active--; next(); }, reject);
      }
    };
    next();
  });
  return { out, peak };
}
const sleep = ms => new Promise(r => setTimeout(r, ms));
(async () => {
  const t = Date.now();
  const { out, peak } = await pMap([...Array(10).keys()], async n => { await sleep(50); return n * n; }, 3);
  console.log('results', out.join(','));
  console.log('peak concurrency', peak, '| elapsed ~', Math.round((Date.now() - t) / 50) * 50, 'ms (serial would be 500)');
})();

js-core/sb.js

function t(n,f){const s=process.hrtime.bigint();const r=f();const d=Number(process.hrtime.bigint()-s)/1e6;console.log(n.padEnd(28),d.toFixed(1)+' ms len='+r.length);}
const N=200000;
t('+= in loop', ()=>{let s='';for(let i=0;i<N;i++)s+='ab';return s;});
t('array push + join', ()=>{const a=[];for(let i=0;i<N;i++)a.push('ab');return a.join('');});
t('split/reverse/join x200', ()=>{const src='x'.repeat(5000);let r='';for(let i=0;i<200;i++)r=src.split('').reverse().join('');return r;});