Merge main

This commit is contained in:
Chris Tate
2026-10-30 03:40:32 -05:00
1032 changed files with 44893 additions and 2937 deletions
@@ -1,4 +1,5 @@
// @requires-libnode
// @order: race
// X { T 0 { W { L i-worker } } } T 2 { B 300 L h-sync }
import { callSync } from "@scriptc/node";
import { readFile, writeSync } from "node:fs";
@@ -0,0 +1,85 @@
// A record field the checker types `any`/`unknown` flowing into a slot whose
// declared type is a UNION: the checked extraction (dynCheck) is the
// conversion, exactly as for a dyn value in a top-level union slot. Before
// this, such a field only lifted into a NON-union destination, so a tool
// result's `details: unknown` could not reach `Details | undefined`, a
// `structuredContent: unknown` could not reach a JSON-value union, and a
// spread's dynamic key could not reach an index-signature member.
interface Details {
limit: number;
flag?: boolean;
}
interface Wide {
label: string;
details: any;
}
interface Narrow {
label: string;
details: Details | undefined;
}
function narrow(w: Wide): Narrow {
return w;
}
type Json = string | number | boolean | null | { [key: string]: Json } | Json[];
interface Payload {
text: string;
structuredContent: any;
}
interface JsonPayload {
text: string;
structuredContent: Json | undefined;
}
function widen(p: Payload): JsonPayload {
return p;
}
// A spread whose source carries the dynamic field: the same field lift.
interface Built {
content: string;
sections: any;
}
interface Message {
role: string;
content: string;
sections: { [key: string]: string | null } | undefined;
timestamp: number;
}
function fromState(flag: boolean): Built {
return { content: flag ? "on" : "off", sections: { preamble: "p" } };
}
function message(flag: boolean): Message {
return { role: "system", ...fromState(flag), timestamp: 0 };
}
// A callback whose result parameter narrows the dynamic field back to a
// record type adapts into the slot's dynamic-details signature.
interface SlotResult {
content: { type: string; text: string }[];
details: any;
}
interface ToolResult {
content: { type: string; text: string }[];
details: { limit: number } | undefined;
}
type Renderer = (result: SlotResult, expanded: boolean) => string;
function format(result: ToolResult, expanded: boolean): string {
return `${result.content.length}:${result.details?.limit ?? "none"}:${expanded}`;
}
const render: Renderer = (result, expanded) => format(result, expanded);
const wide: Wide = { label: "a", details: { limit: 3 } };
const n = narrow(wide);
console.log(n.label, n.details === undefined ? "none" : n.details.limit);
const p = widen({ text: "t", structuredContent: { nested: [1, true, null] } });
console.log(p.text, JSON.stringify(p.structuredContent ?? null));
const m = message(true);
console.log(m.role, m.content, m.timestamp, m.sections?.["preamble"] ?? "-");
console.log(render({ content: [{ type: "text", text: "abc" }], details: { limit: 7 } }, true));
console.log(render({ content: [], details: undefined }, false));
@@ -0,0 +1,76 @@
// A tool definition whose `execute` is an unannotated async method inside a
// contextually typed object literal. tsc checks the method against the
// contextual signature's result (`AgentToolResult<Loaded>`) but INFERS its
// own return type from the returned literal (`{ content: { type, text }[];
// details: { loaded } }`). The fresh literal must construct at the declared
// result slot, not at the contextual type it was checked against: the
// contextual type's `content` is the wider content-part union array, which
// no copy narrows back into the inferred element record.
interface TextContent {
type: string;
text: string;
textSignature?: string;
}
interface ImageContent {
type: string;
data: string;
mimeType: string;
}
interface Candidate {
name: string;
score: number;
}
interface Definition<D> {
name: string;
search(query: string): Promise<D>;
execute(id: string, query: string): Promise<Result<D>>;
}
interface Result<D> {
content: (TextContent | ImageContent)[];
details: D;
structuredContent?: unknown;
isError?: boolean;
}
interface Loaded {
loaded: string[];
}
function rank(query: string, names: string[]): Candidate[] {
return names.filter((name) => name.includes(query)).map((name) => ({ name, score: name.length }));
}
function makeDefinition(names: string[]): Definition<Loaded> {
return {
name: "search",
async search(query: string) {
return { loaded: rank(query, names).map((candidate) => candidate.name) };
},
async execute(_id: string, query: string) {
return {
content: [{ type: "text", text: rank(query, names).map((c) => c.name).join(",") }],
details: { loaded: rank(query, names).map((c) => c.name) },
};
},
};
}
// The same shape through a record of definitions, each with its own details.
type AnyDefinition = Definition<any>;
function makeNamed(names: string[]): Record<"search", AnyDefinition> {
return { search: makeDefinition(names) };
}
const definition = makeDefinition(["find-file", "grep-tree", "read-file"]);
definition
.search("file")
.then((loaded) => {
console.log("search:", loaded.loaded.join(","));
return definition.execute("call-1", "file");
})
.then((result) => {
console.log("execute:", result.content.length, result.content[0]!.type, result.details.loaded.join(","));
return makeNamed(["read-file"]).search.execute("call-2", "file");
})
.then((second) => {
console.log("second:", second.content.length, second.details.loaded.join(","));
});
@@ -0,0 +1,12 @@
// @requires-libnode
// Two deferred island packages that share no files (scope-B NI-7b): every
// static island import is deferred and none changes builtin state, so
// each package evaluates on its own first use, not when Node starts.
import { hello, NAME } from "pkg-a";
import { Counter, make } from "pkg-b";
console.log("start", NAME, typeof hello, typeof Counter, Counter.name);
console.log(hello("native"), hello("again"));
console.log("pkg-b untouched so far:", typeof make);
const c = make();
console.log(c.next(), c.next(), c instanceof Counter);
@@ -0,0 +1,2 @@
export declare function hello(who: string): string;
export declare const NAME: string;
@@ -0,0 +1,8 @@
"use strict";
let greeted = 0;
function hello(who) {
greeted += 1;
return `hello ${who} (${greeted})`;
}
const NAME = "pkg-a";
module.exports = { hello, NAME };
@@ -0,0 +1 @@
{ "name": "pkg-a", "version": "1.0.0", "main": "index.js", "types": "index.d.ts" }
@@ -0,0 +1,5 @@
export declare class Counter {
n: number;
next(): number;
}
export declare function make(): Counter;
@@ -0,0 +1,14 @@
"use strict";
class Counter {
constructor() {
this.n = 0;
}
next() {
this.n += 1;
return this.n;
}
}
function make() {
return new Counter();
}
module.exports = { Counter, make };
@@ -0,0 +1 @@
{ "name": "pkg-b", "version": "1.0.0", "main": "index.js", "types": "index.d.ts" }
@@ -0,0 +1 @@
{ "name": "island-deferred-per-module", "private": true, "type": "module", "scriptc": { "packages": { "pkg-a": "node", "pkg-b": "node" } } }
@@ -0,0 +1,11 @@
// @requires-libnode
// A deferred island package that reads an environment variable when it
// evaluates (scope-B NI-7b): the build records the key as a boot guard,
// and the late evaluation sees the value the variable had at the import
// position, even though the program changed it before the first use.
import { currentMode } from "env-pkg";
console.log("before", process.env.SCRIPTC_NI7B_MODE ?? "(unset)");
process.env.SCRIPTC_NI7B_MODE = "changed";
console.log("mode", currentMode());
console.log("after", process.env.SCRIPTC_NI7B_MODE);
@@ -0,0 +1 @@
export declare function currentMode(): string;
@@ -0,0 +1,7 @@
"use strict";
// Read once, when the module evaluates.
const mode = process.env.SCRIPTC_NI7B_MODE || "default";
function currentMode() {
return mode;
}
module.exports = { currentMode };
@@ -0,0 +1 @@
{ "name": "env-pkg", "version": "1.0.0", "main": "index.js", "types": "index.d.ts" }
@@ -0,0 +1 @@
{ "name": "island-deferred-env", "private": true, "type": "module", "scriptc": { "packages": { "env-pkg": "node" } } }
@@ -0,0 +1,16 @@
// @requires-libnode
// A native package's top-level require() of a Node island package is
// deferred like a static import (scope-B NI-7d): the island's load (which
// sets and clears a timer, leaving nothing behind) waits for the first
// call, and Node does not start at boot.
import { existsSync, readFileSync } from "node:fs";
import { calls, quadruple } from "native-wrapper";
function libnodeMapped(): boolean {
if (!existsSync("/proc/self/maps")) return false;
return readFileSync("/proc/self/maps", "utf8").includes("libnode");
}
console.log("before first use, libnode mapped:", libnodeMapped());
console.log("quadruple", quadruple(3), quadruple(5));
console.log("calls", calls());
@@ -0,0 +1,2 @@
export declare function quadruple(n: number): number;
export declare function calls(): number;
@@ -0,0 +1,14 @@
"use strict";
// A native package that requires an island at its top level, as
// async-retry requires retry.
const timers = require("timer-pkg");
function quadruple(n) {
return timers.twice(timers.twice(n));
}
function calls() {
return timers.count();
}
module.exports = { quadruple, calls };
@@ -0,0 +1 @@
{ "name": "native-wrapper", "version": "1.0.0", "main": "index.js", "types": "index.d.ts" }
@@ -0,0 +1,2 @@
export declare function twice(n: number): number;
export declare function count(): number;
@@ -0,0 +1,18 @@
"use strict";
// Load-time timer housekeeping that leaves nothing behind: a clear of
// nothing, and a timer set and cleared again (smol-toml-like).
clearTimeout(undefined);
const probe = setTimeout(() => {
throw new Error("never runs");
}, 1000);
clearTimeout(probe);
let calls = 0;
function twice(n) {
calls += 1;
return n * 2;
}
function count() {
return calls;
}
module.exports = { twice, count };
@@ -0,0 +1 @@
{ "name": "timer-pkg", "version": "1.0.0", "main": "index.js", "types": "index.d.ts" }
@@ -0,0 +1 @@
{ "name": "island-deferred-require", "private": true, "type": "module", "scriptc": { "packages": { "timer-pkg": "node" } } }
@@ -0,0 +1,14 @@
// @requires-libnode
// A Node island package that reads its own files when it loads (scope-B
// NI-7e): reads inside its package are guarded inputs, so the load defers
// and the first call sees the same contents.
import { existsSync, readFileSync } from "node:fs";
import { greet } from "data-pkg";
function libnodeMapped(): boolean {
if (!existsSync("/proc/self/maps")) return false;
return readFileSync("/proc/self/maps", "utf8").includes("libnode");
}
console.log("before first use, libnode mapped:", libnodeMapped());
console.log(greet("native"));
@@ -0,0 +1 @@
hello from data
@@ -0,0 +1 @@
export declare function greet(who: string): string;
@@ -0,0 +1,14 @@
"use strict";
// Reads its own package's files at load: guarded inputs (the build records
// their size and mtime as boot guards), so the load still defers.
const fs = require("fs");
const path = require("path");
const dataFile = path.join(__dirname, "data.txt");
const greeting = fs.existsSync(dataFile) ? fs.readFileSync(dataFile, "utf8").trim() : "missing";
const version = JSON.parse(fs.readFileSync(path.join(__dirname, "package.json"), "utf8")).version;
function greet(who) {
return `${greeting}, ${who} (v${version})`;
}
module.exports = { greet };
@@ -0,0 +1 @@
{ "name": "data-pkg", "version": "2.3.4", "main": "index.js", "types": "index.d.ts" }
@@ -0,0 +1 @@
{ "name": "island-deferred-own-files", "private": true, "type": "module", "scriptc": { "packages": { "data-pkg": "node" } } }
@@ -0,0 +1,4 @@
"use strict";
// Island code outside node_modules that loads punycode on first use: Node
// prints its DEP0040 deprecation warning at that call.
exports.toAscii = (domain) => require("punycode").toASCII(domain);
@@ -0,0 +1,18 @@
// @requires-libnode
// @normalize-pid
// Node's deprecation warnings from island code (scope-B NI-7f):
// - punycode required first by island code outside node_modules: DEP0040
// on stderr, with Node's text, at that call;
// - punycode required by an island package (inside node_modules, as
// node-fetch's whatwg-url): no warning, in Node as here;
// - a package that warns when it loads: not deferred (the probe records
// the warning), so it warns at its import position.
import { requireModule } from "@scriptc/node";
import { toAscii } from "puny-pkg";
import { ok } from "warn-pkg";
console.log("start", ok());
const legacy = requireModule("./legacy.cjs");
const ascii: string = legacy.toAscii("bücher.de");
console.log("legacy", ascii);
console.log("package", toAscii("mañana.com"));
@@ -0,0 +1 @@
export declare function toAscii(domain: string): string;
@@ -0,0 +1,8 @@
"use strict";
// An island package that uses the deprecated punycode builtin, as
// node-fetch's whatwg-url does (here on first use). A require from inside
// node_modules does not warn.
function toAscii(domain) {
return require("punycode").toASCII(domain);
}
module.exports = { toAscii };
@@ -0,0 +1 @@
{ "name": "puny-pkg", "version": "1.0.0", "main": "index.js", "types": "index.d.ts" }
@@ -0,0 +1 @@
export declare function ok(): string;
@@ -0,0 +1,6 @@
"use strict";
// An island package that emits a deprecation warning when it loads: the
// build's probe records the warning, so the package is not deferred and the
// warning appears where Node prints it.
process.emitWarning("warn-pkg's default export is deprecated", "DeprecationWarning", "DEP_WARN_PKG");
exports.ok = () => "warn-pkg ok";
@@ -0,0 +1 @@
{ "name": "warn-pkg", "version": "1.0.0", "main": "index.js", "types": "index.d.ts" }
@@ -0,0 +1 @@
{ "name": "island-punycode-warning", "private": true, "type": "module", "scriptc": { "packages": { "puny-pkg": "node", "warn-pkg": "node" } } }
@@ -0,0 +1,30 @@
// @requires-libnode
// The shape of a Next.js standalone server.js: a CommonJS entry that writes
// process.env, then requires packages that ship no declarations (TS7016 in
// a JavaScript file is relaxed), destructures one of them, and calls into
// it. The packages run in Node islands. Each require loads its island module
// at the statement, so the environment writes before it are visible to the
// module's evaluation, as in Node.
const path = require('path')
console.log('entry start')
process.env.ISLAND_MODE = 'production'
process.env.ISLAND_CONFIG = JSON.stringify({ name: path.basename('/srv/app'), port: 3000 })
require('envkit')
const envkit = require('envkit')
const { startServer, version: startVersion } = require('startkit')
console.log(envkit.seen())
console.log('loads', envkit.loads())
console.log('startkit', startVersion, typeof startServer)
const config = JSON.parse(process.env.ISLAND_CONFIG)
startServer({ name: config.name, port: config.port })
.then((message) => {
console.log(message)
return startServer({ name: 'bad', port: -1 })
})
.catch((err) => {
console.log('caught', err.message)
})
+7
View File
@@ -0,0 +1,7 @@
// Ships no declarations, like the traced node_modules of a Next.js
// standalone build. Records the environment its evaluation saw.
const seenMode = process.env.ISLAND_MODE;
const seenConfig = process.env.ISLAND_CONFIG;
globalThis.__envkitLoads = (globalThis.__envkitLoads || 0) + 1;
exports.seen = () => `envkit saw ISLAND_MODE=${seenMode}, config=${seenConfig}`;
exports.loads = () => globalThis.__envkitLoads;
@@ -0,0 +1 @@
{ "name": "envkit", "version": "1.0.0", "main": "index.js" }
@@ -0,0 +1,8 @@
// Its evaluation has effects (a timer), so the island loads eagerly.
const seenMode = process.env.ISLAND_MODE;
setTimeout(() => {}, 0);
exports.startServer = async function startServer(opts) {
if (opts.port < 0) throw new Error(`bad port ${opts.port}`);
return `started ${opts.name} on ${opts.port} (startkit saw ISLAND_MODE=${seenMode}, now ${process.env.ISLAND_MODE})`;
};
exports.version = "1.0.0";
@@ -0,0 +1 @@
{ "name": "startkit", "version": "1.0.0", "main": "index.js" }
@@ -0,0 +1 @@
{ "name": "island-cjs-entry", "private": true }
@@ -0,0 +1,34 @@
// A JavaScript binding whose later assignments store values its
// initializer's type does not admit (TS2322, relaxed in JavaScript files):
// the binding holds whatever the program stores, as in Node. Next.js's
// standalone server.js does this with KEEP_ALIVE_TIMEOUT.
let keepAliveTimeout = parseInt(process.env.SCRIPTC_UNSET_TIMEOUT, 10)
if (Number.isNaN(keepAliveTimeout) || !Number.isFinite(keepAliveTimeout) || keepAliveTimeout < 0) {
keepAliveTimeout = undefined
}
console.log(keepAliveTimeout, typeof keepAliveTimeout)
const opts = { keepAliveTimeout }
console.log(JSON.stringify(opts), 'keepAliveTimeout' in opts, opts.keepAliveTimeout === undefined)
let label = 'none'
function relabel(n) {
label = n
}
relabel(42)
console.log(label, typeof label)
function show(raw) {
let value = Number(raw)
if (Number.isNaN(value)) value = null
console.log('value', value, value === null)
}
show('12')
show('x')
var counter = 0
counter = counter + 1
console.log('counter', counter)
let port = parseInt('8080', 10)
if (port > 65535) port = undefined
console.log('port', port, typeof port)
@@ -0,0 +1,37 @@
// @requires-libnode
// require() of a Node island package inside functions of a CommonJS
// entry: the module loads at the first call that requires it (after the
// environment write before that call), later calls get the same module.
'use strict'
function viaExpression(name) {
return require('lazykit').greet(name)
}
function viaBinding(name) {
const lazykit = require('lazykit')
return `${lazykit.greet(name)} (${lazykit.loads()} load)`
}
const lazy = {
get kit() {
return require('lazykit')
},
}
console.log('start')
process.env.ISLAND_PHASE = 'after-start'
console.log(viaExpression('first'))
console.log(viaBinding('second'))
console.log(lazy.kit.greet('getter'), lazy.kit === require('lazykit'))
// A function's own binding of a package the module also requires at its
// top level, called before that line runs.
function early(name) {
const kit = require('lazykit')
return kit.greet(name)
}
console.log(early('local binding'))
console.log(require('./spaced').viaSpaced('spaced'))
const topKit = require('lazykit')
console.log('top level', topKit.loads())
@@ -0,0 +1,5 @@
// Ships no declarations. Counts its evaluations: Node evaluates it once.
globalThis.__lazykitLoads = (globalThis.__lazykitLoads || 0) + 1;
console.log('lazykit evaluated, ISLAND_PHASE =', process.env.ISLAND_PHASE);
exports.greet = (name) => `hello ${name}`;
exports.loads = () => globalThis.__lazykitLoads;
@@ -0,0 +1 @@
{ "name": "lazykit", "version": "1.0.0", "main": "index.js" }
@@ -0,0 +1,2 @@
// Ships no declarations; required only with a comment before the paren.
exports.wave = (name) => `wave ${name}`;
@@ -0,0 +1 @@
{ "name": "spacedkit", "version": "1.0.0", "main": "index.js" }
@@ -0,0 +1 @@
{ "name": "island-nested-require", "private": true }
@@ -0,0 +1,4 @@
'use strict'
// This file spells its one require() call with a comment before the
// parenthesis.
exports.viaSpaced = (name) => require /* lazily */ ('spacedkit').wave(name)
@@ -0,0 +1,21 @@
// @requires-libnode
// Island values typed with builtin classes (URLSearchParams, Headers, URL)
// stay island handles: their members and methods run in the island, and
// passing them back hands the island its own object.
import { RequestLike, describeParams, makeParams } from "urlkit";
const request = new RequestLike("http://localhost/api/products?tag=software&limit=2", {
"x-trace": "abc",
});
const tag = request.searchParams.get("tag") ?? "none";
const missing = request.searchParams.get("missing") ?? "none";
console.log("tag", tag, "missing", missing, "has limit", request.searchParams.has("limit"));
console.log("pathname", request.url.pathname, "host", request.url.host);
console.log("trace", request.headers.get("x-trace"), "absent", request.headers.get("x-absent"));
const params = request.searchParams;
params.append("page", "3");
console.log("described", describeParams(params));
console.log("round trip", describeParams(makeParams("a=1&b=two")));
console.log("same object", params === request.searchParams);
console.log("size", params.size);
@@ -0,0 +1,8 @@
export declare class RequestLike {
constructor(url: string, headers?: Record<string, string>);
readonly url: URL;
readonly headers: Headers;
get searchParams(): URLSearchParams;
}
export declare function describeParams(params: URLSearchParams): string;
export declare function makeParams(query: string): URLSearchParams;
@@ -0,0 +1,17 @@
// An island class whose members are builtin class instances (the shape of
// Next.js's NextRequest.nextUrl.searchParams).
export class RequestLike {
constructor(url, headers = {}) {
this.url = new URL(url);
this.headers = new Headers(headers);
}
get searchParams() {
return this.url.searchParams;
}
}
export function describeParams(params) {
return [...params.entries()].map(([k, v]) => `${k}=${v}`).join("&");
}
export function makeParams(query) {
return new URLSearchParams(query);
}
@@ -0,0 +1 @@
{ "name": "urlkit", "version": "1.0.0", "type": "module", "main": "index.js", "types": "index.d.ts" }
@@ -0,0 +1 @@
{ "name": "island-builtin-handles", "private": true, "type": "module", "scriptc": { "packages": { "urlkit": "node" } } }
+7
View File
@@ -0,0 +1,7 @@
// Declarations that mark a member readonly, as Next.js's global types do
// for process.env.NODE_ENV.
declare namespace NodeJS {
interface ProcessEnv {
readonly APP_STAGE?: "development" | "production";
}
}
@@ -0,0 +1,9 @@
/// <reference path="./env.d.ts" />
// A JavaScript entry writes a member its declarations mark readonly
// (TS2540, relaxed in JavaScript files): `readonly` is a type annotation,
// and the write behaves as it does in Node.
console.log('before', process.env.APP_STAGE)
process.env.APP_STAGE = 'production'
console.log('after', process.env.APP_STAGE, typeof process.env.APP_STAGE)
delete process.env.APP_STAGE
console.log('deleted', process.env.APP_STAGE)
@@ -0,0 +1,34 @@
"use strict";
const { EventEmitter } = require("node:events");
class Ticker extends EventEmitter {
constructor() {
super();
// Each emitter carries some weight, so leaked cycles show in memory.
this.payload = new Array(256).fill(0).map((_, i) => ({ i }));
}
tick(n) {
return this.emit("tick", n);
}
}
// Allocates and drops JavaScript garbage so V8 runs its collector.
function churn(kib) {
let total = 0;
for (let i = 0; i < kib; i++) total += new Array(128).fill(i).length;
return total;
}
// Emitters island code keeps alive on its own (a registry), whose native
// listeners must keep working whatever the collector does.
const kept = new Set();
function keep(emitter) {
kept.add(emitter);
}
function tickAll(n) {
let delivered = 0;
for (const e of kept) if (e.tick(n)) delivered++;
return delivered;
}
module.exports = { Ticker, churn, keep, kept, tickAll };
@@ -0,0 +1,43 @@
// @requires-libnode
// Cross-heap cycles through an island emitter (scope-B NI-8): a native
// object holds an island EventEmitter whose listener is a native closure
// capturing that object. Neither heap alone can free such a pair; the
// collector frees the pairs nothing holds and keeps the ones either side
// still holds, which keep working.
import { requireModule } from "@scriptc/node";
const { churn, keep, kept, Ticker, tickAll } = requireModule("./lib.cjs");
class Counter {
constructor(name) {
this.name = name;
this.hits = 0;
this.ticker = new Ticker();
this.ticker.on("tick", (n) => {
this.hits += n;
});
}
}
// Pairs held by native code, and pairs held by island code only.
const nativeKept = [];
let islandHeld = 0;
let total = 0;
for (let i = 0; i < 4000; i++) {
const c = new Counter(`c${i}`);
c.ticker.tick(1);
total += c.hits;
if (i % 500 === 0) nativeKept.push(c);
if (i % 700 === 0) {
keep(c.ticker);
islandHeld++;
}
churn(4);
}
console.log("total", total, "native kept", nativeKept.length, "island kept", islandHeld);
// After the churn, every kept pair still works from both sides.
for (const c of nativeKept) c.ticker.tick(10);
console.log("native kept hits", nativeKept.map((c) => c.hits).join(","));
console.log("island delivered", tickAll(100), "registry", kept.size);
console.log("native kept hits", nativeKept.map((c) => c.hits).join(","));
@@ -0,0 +1,22 @@
"use strict";
// Island objects that hold native callbacks.
function make(n) {
return { n, payload: new Array(128).fill(n), onPing: null, onBoth: null };
}
function ping(o, k) {
return o.onPing(k);
}
function churn(kib) {
let total = 0;
for (let i = 0; i < kib; i++) total += new Array(128).fill(i).length;
return total;
}
// Island-side roots.
const held = [];
function hold(o) {
held.push(o);
}
function pingHeld(k) {
return held.map((o) => o.onPing(k)).join(",");
}
module.exports = { make, ping, churn, hold, pingHeld };
@@ -0,0 +1,34 @@
// @requires-libnode
// Cross-heap cycles through native closures (scope-B NI-8): an island
// object holds a native closure that captures that island object, and a
// pair of island objects hold closures capturing each other. The collector
// frees the cycles nothing holds; those held by native code (a native
// closure, a native array) or by island code keep working.
import { requireModule } from "@scriptc/node";
const { churn, hold, make, ping, pingHeld } = requireModule("./lib.cjs");
const nativeHeld = [];
const viaClosure = [];
let total = 0;
for (let i = 0; i < 4000; i++) {
const o = make(i);
o.onPing = (k) => o.n + k + o.payload.length;
total += ping(o, 1);
// Two island objects whose closures capture each other.
const a = make(i);
const b = make(-i);
a.onPing = (k) => b.n * k;
b.onPing = (k) => a.n * k;
total += ping(a, 2) + ping(b, 3);
if (i % 800 === 0) nativeHeld.push(o);
if (i % 900 === 0) viaClosure.push(() => ping(a, 1) + ping(b, 1));
if (i % 1000 === 0) hold(b);
churn(4);
}
console.log("total", total);
console.log("native held", nativeHeld.map((o) => ping(o, 0)).join(","));
console.log("via closure", viaClosure.map((f) => f()).join(","));
console.log("island held", pingHeld(5));
@@ -0,0 +1,35 @@
// @requires-libnode
// Cross-heap cycles through native subclasses of an island class (scope-B
// NI-8): the instance is an island object; its native constructor registers
// a native listener that captures the instance. The pairs nothing holds are
// freed; those native code or island code holds keep working.
import { adopt, broadcast, Channel, churn } from "channel-pkg";
class CountingChannel extends Channel {
count = 0;
constructor(name: string) {
super(name);
this.onMessage((text: string) => {
this.count += text.length;
});
}
summary(): string {
return `${this.name}:${this.count}`;
}
}
const nativeHeld: CountingChannel[] = [];
let total = 0;
for (let i = 0; i < 3000; i++) {
const c = new CountingChannel(`c${i}`);
total += c.deliver("ping");
total += c.count;
if (i % 600 === 0) nativeHeld.push(c);
if (i % 700 === 0) adopt(c);
churn(4);
}
console.log("total", total);
for (const c of nativeHeld) c.deliver("again");
console.log("native held", nativeHeld.map((c) => c.summary()).join(","));
console.log("island delivered", broadcast("hi"));
console.log("native held", nativeHeld.map((c) => c.summary()).join(","));
@@ -0,0 +1,9 @@
export declare class Channel {
name: string;
constructor(name: string);
onMessage(fn: (text: string) => void): void;
deliver(text: string): number;
}
export declare function churn(kib: number): number;
export declare function adopt(channel: Channel): void;
export declare function broadcast(text: string): number;
@@ -0,0 +1,35 @@
"use strict";
// An island class whose instances keep the listeners they are given.
class Channel {
constructor(name) {
this.name = name;
this.listeners = [];
this.payload = new Array(128).fill(0);
}
onMessage(fn) {
this.listeners.push(fn);
}
deliver(text) {
for (const fn of this.listeners) fn(text);
return this.listeners.length;
}
}
function churn(kib) {
let total = 0;
for (let i = 0; i < kib; i++) total += new Array(128).fill(i).length;
return total;
}
// Channels island code keeps on its own.
const adopted = [];
function adopt(channel) {
adopted.push(channel);
}
function broadcast(text) {
let n = 0;
for (const c of adopted) n += c.deliver(text);
return n;
}
module.exports = { Channel, churn, adopt, broadcast };
@@ -0,0 +1 @@
{ "name": "channel-pkg", "version": "1.0.0", "main": "index.js", "types": "index.d.ts" }
@@ -0,0 +1 @@
{ "name": "island-gc-native-subclass", "private": true, "type": "module", "scriptc": { "packages": { "channel-pkg": "node" } } }
@@ -0,0 +1,20 @@
// @requires-libnode
// Cross-heap cycles through a shared module (scope-B NI-8): island code
// pairs native sessions of a program module it shares with island peers
// that point back at them. The pairs island code drops are garbage on both
// sides; the ones the shared module keeps stay alive and keep working.
import { requireModule } from "@scriptc/node";
import { openSessions } from "./sessions.mjs";
const { churn, pair } = requireModule("./peers.mjs");
let total = 0;
for (let i = 0; i < 3000; i++) {
total += pair(i, i % 500 === 0);
churn(4);
}
console.log("total", total);
const open = openSessions();
console.log("open", open.length, open.map((s) => s.id).join(","));
console.log("sent", open.map((s) => s.send("again")).join(","));
console.log("logs", open.map((s) => s.log.length).join(","));
@@ -0,0 +1,30 @@
// Island code: island peers paired with native sessions of the shared
// module, each pair a cycle across both heaps.
import { keep, Session } from "./sessions.mjs";
class Peer {
constructor(session) {
this.session = session;
this.received = 0;
this.payload = new Array(128).fill(1);
}
receive(text) {
this.received += text.length;
return this.received;
}
}
export function pair(id, keepIt) {
const session = new Session(id);
const peer = new Peer(session);
session.peer = peer;
const n = session.send(`hello ${id}`);
if (keepIt) keep(session);
return n;
}
export function churn(kib) {
let total = 0;
for (let i = 0; i < kib; i++) total += new Array(128).fill(i).length;
return total;
}
@@ -0,0 +1,20 @@
// A program module island code imports too (shared, scope-B NI-6): native
// sessions that island code pairs with island peers.
export class Session {
constructor(id) {
this.id = id;
this.peer = null;
this.log = [];
}
send(text) {
this.log.push(text);
return this.peer ? this.peer.receive(text) : 0;
}
}
const open = [];
export function keep(session) {
open.push(session);
}
export function openSessions() {
return open;
}
@@ -0,0 +1,38 @@
"use strict";
// Promises that tie island objects to native values.
function make(n) {
return { n, payload: new Array(128).fill(n), settle: null, promise: null };
}
function deferred() {
let resolve;
const promise = new Promise((r) => {
resolve = r;
});
return { promise, resolve, payload: new Array(64).fill(0) };
}
function settle(o, v) {
o.settle(v);
}
function churn(kib) {
let total = 0;
for (let i = 0; i < kib; i++) total += new Array(128).fill(i).length;
return total;
}
const held = [];
function hold(x) {
held.push(x);
}
function resolveHeld() {
let k = 0;
for (const x of held) if (x.resolve) x.resolve(++k);
return k;
}
async function awaitHeld() {
const out = [];
for (const x of held) {
const v = await x.promise;
out.push(typeof v === "number" ? v : v.n);
}
return out.join(",");
}
module.exports = { make, deferred, settle, churn, hold, resolveHeld, awaitHeld };
@@ -0,0 +1,40 @@
// @requires-libnode
// Cross-heap cycles through promises (scope-B NI-8): an island object holds
// a native promise that settles with that island object; a native promise
// reaction to an island promise captures the island object holding it. The
// collector frees the settled cycles nothing holds, and never a pending
// one that will still settle.
import { requireModule } from "@scriptc/node";
const { awaitHeld, churn, deferred, hold, make, resolveHeld, settle } = requireModule("./lib.cjs");
const pending = [];
let total = 0;
for (let i = 0; i < 3000; i++) {
// island object -> native promise -> settled with the island object
const o = make(i);
o.promise = new Promise((resolve) => {
o.settle = (v) => resolve(v);
});
settle(o, o);
total += (await o.promise).n;
// island deferred -> native reaction -> captures the deferred
const d = deferred();
const reaction = d.promise.then((v) => v + d.payload.length);
d.resolve(i);
total += await reaction;
// Pending cycles, settled only after the loop.
if (i % 600 === 0) {
const late = deferred();
pending.push(late.promise.then((v) => v * 2 + late.payload.length));
hold(late);
}
if (i % 700 === 0) hold(o);
churn(4);
}
console.log("total", total);
console.log("resolved", resolveHeld());
console.log("pending", (await Promise.all(pending)).join(","));
console.log("held", await awaitHeld());
@@ -0,0 +1,32 @@
"use strict";
// An island WeakMap keyed by native objects, and WeakRefs to them.
const notes = new WeakMap();
function note(key, value) {
notes.set(key, value);
}
function noteOf(key) {
return notes.get(key);
}
function has(key) {
return notes.has(key);
}
function make(n) {
return { n, payload: new Array(128).fill(n), read: null };
}
function read(v) {
return v.read();
}
const refs = [];
function watch(x) {
refs.push(new WeakRef(x));
}
// Watched objects native code still holds must be alive and the same.
function watchedAlive(objs) {
return refs.filter((r, i) => r.deref() === objs[i]).length;
}
function churn(kib) {
let total = 0;
for (let i = 0; i < kib; i++) total += new Array(128).fill(i).length;
return total;
}
module.exports = { note, noteOf, has, make, read, watch, watchedAlive, churn };
@@ -0,0 +1,30 @@
// @requires-libnode
// Cross-heap cycles through an island WeakMap (scope-B NI-8): a native key
// object maps to an island value that holds a native closure capturing the
// key. In Node the entry dies with the key; across the boundary the
// collector must honor the same ephemeron semantics, and keep the entries
// (and WeakRefs) of keys native code still holds.
import { requireModule } from "@scriptc/node";
const { churn, has, make, note, noteOf, read, watch, watchedAlive } = requireModule("./lib.cjs");
const kept = [];
let total = 0;
for (let i = 0; i < 4000; i++) {
const key = { i, label: `k${i}` };
const value = make(i);
value.read = () => key.i + key.label.length;
key.value = value;
note(key, value);
total += read(noteOf(key));
if (i % 500 === 0) {
kept.push(key);
watch(key);
}
churn(4);
}
console.log("total", total);
console.log("kept entries", kept.filter((k) => has(k)).length, "of", kept.length);
console.log("kept values", kept.map((k) => read(noteOf(k))).join(","));
console.log("same value", kept.every((k) => noteOf(k) === k.value));
console.log("watched alive", watchedAlive(kept), "of", kept.length);
@@ -0,0 +1,9 @@
"use strict";
// Island objects that hold whatever functions they are given.
function make(n) {
return { n, label: `item-${n}`, cb: null, fmt: null, nested: { run: null } };
}
function callFromIsland(o, ...args) {
return o.cb(...args);
}
module.exports = { make, callFromIsland };
@@ -0,0 +1,40 @@
// @requires-libnode
// Native functions stored on island objects and called as methods from
// native code: the island object is the receiver, arguments and results
// cross both ways, throws propagate, and island code calling the same
// member sees the same function.
import { requireModule } from "@scriptc/node";
const { callFromIsland, make } = requireModule("./lib.cjs");
const o = make(3);
o.cb = (a, b) => o.n * a + b;
console.log("arrow", o.cb(2, 1), callFromIsland(o, 2, 1));
o.fmt = function (prefix) {
return `${prefix}:${this.label}:${this.n}`;
};
console.log("this", o.fmt("p"), o.fmt.call(make(9), "q"));
o.nested.run = (x) => [x, x * 2];
console.log("nested", o.nested.run(4).join(","));
o.cb = () => {
throw new RangeError("from native member");
};
try {
o.cb();
} catch (e) {
console.log("caught", e instanceof RangeError, e.message);
}
console.log("optional", o.missing?.(1), typeof o.cb);
try {
o.label();
} catch (e) {
console.log("not callable", e instanceof TypeError);
}
const items = [make(1), make(2), make(3)];
for (const it of items) it.cb = (k) => it.n + k;
console.log("loop", items.map((it) => it.cb(10)).join(","));
@@ -0,0 +1,24 @@
// process.prependListener registers the listener at the FRONT of an
// event's listener list (Node's EventEmitter order) for the registries
// that own a prepend entry: 'exit' and the signal family. on/once keep
// appending, so a prepend registered between them still dispatches first.
import { spawn } from "node:child_process";
process.on("exit", (code) => console.log(`exit on 1 (${code})`));
process.prependListener("exit", (code) => console.log(`exit prepend 0 (${code})`));
process.once("exit", (code) => console.log(`exit once 2 (${code})`));
process.prependListener("exit", () => console.log("exit prepend last-registered, still first"));
let ints = 0;
const onInt = () => {
ints++;
console.log("SIGINT on", ints);
};
process.on("SIGINT", onInt);
process.prependListener("SIGINT", () => console.log("SIGINT prepend", ints));
const child = spawn("/bin/sh", ["-c", "sleep 0.2; kill -INT $PPID; sleep 0.2"], { stdio: "ignore" });
child.on("exit", (code) => {
console.log("child exit", code ?? -1, "ints", ints);
});
console.log("main done");
@@ -0,0 +1,15 @@
// URLSearchParams crossing an implicit ToString: a template span, string
// concatenation, String() and an explicit toString() all serialize the
// pair list in insertion order, percent-encoded exactly as Node does. The
// record init accepts shorthand members (`{ model }`) like `{ model: model }`.
const model = "m 1";
const sp = new URLSearchParams({ model, autoload: "false", q: "a&b" });
console.log(`${sp}`);
console.log("prefix:" + sp);
console.log(String(sp));
console.log(sp.toString());
console.log(sp.get("model"), sp.get("q"), sp.get("missing"));
const empty = new URLSearchParams({});
console.log(`[${empty}]`, JSON.stringify(empty.toString()));
const pairs = [...sp];
console.log(pairs.length, pairs[0]![0], pairs[0]![1]);
+27
View File
@@ -0,0 +1,27 @@
// The optional-chain form of the ChildProcess members: `this.child?.kill()`
// and `this.child?.on(...)` reach the same lowerings as the plain calls, so
// an absent child is a no-op and a present one keeps Node's shapes.
import { spawn, type ChildProcess } from "node:child_process";
class Runner {
child: ChildProcess | undefined;
constructor(child: ChildProcess | undefined) {
this.child = child;
}
kill(): void {
this.child?.kill("SIGTERM");
console.log("kill attempted, child present:", this.child !== undefined);
}
watch(): void {
this.child?.on("exit", (code) => console.log("exit", code));
}
}
const withChild = new Runner(spawn("sleep", ["5"], { stdio: "ignore" }));
withChild.watch();
withChild.kill();
const withoutChild = new Runner(undefined);
withoutChild.watch();
withoutChild.kill();
console.log("done");
@@ -0,0 +1,12 @@
// A match row read through an optional chain: `re.exec(s)?.index` and the
// `?.input` beside it take the same member reads as the guarded form.
const re = /b(c)/g;
const text = "abcz";
const m = re.exec(text);
console.log(m !== null ? m.index : -1);
console.log(re.exec(text)?.index);
console.log(re.exec(text)?.input);
console.log(/(z)/.exec(text)?.index);
console.log(/q/.exec(text)?.index);
const groups = /(?<lead>b)/.exec(text)?.groups;
console.log(groups?.lead);
@@ -0,0 +1,19 @@
// Array.from over a string the checker narrowed through a guard, and over
// code points (surrogate pairs stay whole), each a fresh array.
const values: (string | undefined)[] = ["abc", undefined, "a\u{1F600}b"];
for (const value of values) {
if (value === undefined) {
console.log("skip");
continue;
}
const chars = Array.from(value);
console.log(chars.length, chars.join("|"));
}
const maybe: string | undefined = "xy";
if (maybe !== undefined) console.log(Array.from(maybe).length);
const plain: string = "q";
console.log(Array.from(plain), Array.from(plain).length);
function guarded(v: string | undefined): boolean {
return v !== undefined && Array.from(v).length > 1;
}
console.log(guarded("ab"), guarded("a"), guarded(undefined));
+27
View File
@@ -0,0 +1,27 @@
// server.off / removeListener: the LAST registered listener for the same
// function leaves that event's list (Node's removeListener order), the
// remaining listeners still run, and removing an unregistered function is
// a no-op. The EADDRINUSE from a second listen on a bound port is the real
// event here.
import * as net from "node:net";
const first = net.createServer();
first.listen(0, "127.0.0.1", () => {
const port = first.address().port;
const second = net.createServer();
const removed = (err: Error) => console.log("removed listener ran", (err as NodeJS.ErrnoException).code);
const kept = (err: Error) => console.log("kept listener ran", (err as NodeJS.ErrnoException).code);
second.on("error", removed);
second.on("error", kept);
second.off("error", removed);
second.off("error", () => console.log("never registered"));
second.listen(port, "127.0.0.1", () => {
console.log("unexpected: second listen succeeded");
second.close();
});
setTimeout(() => {
console.log("closing");
second.close();
first.close(() => console.log("first closed"));
}, 20);
});
@@ -0,0 +1,11 @@
// The process streams' descriptors and stdin's raw-mode flag: fd is the
// compile-time 0/1/2 Node reports for a TTY, a pipe, and a redirected file
// alike; isRaw is the TTY's raw-mode flag and Node's undefined when stdin
// is not a terminal (the harness's own pipe).
import { writeSync } from "node:fs";
console.log("stdin fd", process.stdin.fd);
console.log("stdout fd", process.stdout.fd);
console.log("stderr fd", process.stderr.fd);
console.log("stdin isRaw", process.stdin.isRaw, typeof process.stdin.isRaw);
writeSync(process.stdout.fd, "writeSync through process.stdout.fd\n");
@@ -0,0 +1,47 @@
// Reflect.has over the checked-dynamic view, and the Object reflection
// operations over records, arrays and class instances (own fields and
// elements in property order). Reflect.has is [[HasProperty]], so inherited
// names (Array.prototype's, Object.prototype's, a class's methods) answer
// true; a primitive target is Node's TypeError and stays a lowerer fence
// (SC2020) rather than a silent `false`.
class Point {
x = 1;
y = 2;
sum(): number {
return this.x + this.y;
}
}
const record = { a: 1, b: "two" };
const list = [10, 20];
const point = new Point();
console.log(Reflect.has(record, "a"), Reflect.has(record, "z"));
console.log(Reflect.has(list, "0"), Reflect.has(list, "length"), Reflect.has(list, "9"));
console.log(Reflect.has(point, "x"), Reflect.has(point, "sum"));
const key = "b";
console.log(Reflect.has(record, key), Reflect.has(record, key.toUpperCase()));
console.log(Reflect.has(record, "toString"), Reflect.has(record, "constructor"));
console.log(Object.getOwnPropertyNames(record).join(","));
console.log(Object.getOwnPropertyNames(point).join(","));
console.log(Object.getOwnPropertyNames(list).length);
const descriptors = Object.getOwnPropertyDescriptors(record);
console.log(Object.keys(descriptors).join(","));
console.log(Object.getOwnPropertySymbols(record).length);
// A primitive target held as any, unknown or a union throws Node's
// TypeError; its key is evaluated first.
function hasThrough(target: unknown, name: string): string {
try {
return String(Reflect.has(target as any, name));
} catch (e) {
return `${(e as Error).name}: ${(e as Error).message}`;
}
}
const loose: any = "text";
const either: string | { a: number } = Math.random() > 2 ? { a: 1 } : "text";
console.log(hasThrough(loose, "length"), hasThrough(42, "x"), hasThrough(undefined, "x"));
console.log(hasThrough({ a: 1 }, "a"), hasThrough(either, "a"));
let keys = 0;
try {
Reflect.has(loose, String(++keys));
} catch (e) {
console.log((e as Error).name, keys);
}
+31
View File
@@ -0,0 +1,31 @@
// A class extending EventEmitter<EventMap>: the type argument is the
// listener MAP — a pure typing aid with no runtime layout of its own, so
// the emitted class adopts the same emitter base (Node erases it too).
// on/emit dispatch keeps Node's order, and the subclass's own members
// stay alongside the inherited ones.
import { EventEmitter } from "node:events";
interface BufferEvents {
data: [chunk: string];
end: [];
}
class StdinBuffer extends EventEmitter<BufferEvents> {
private chunks: string[] = [];
push(chunk: string): void {
this.chunks.push(chunk);
this.emit("data", chunk);
}
finish(): void {
console.log("joined", this.chunks.join(""));
this.emit("end");
}
}
const buffer = new StdinBuffer();
buffer.on("data", (chunk) => console.log("data", chunk));
buffer.on("end", () => console.log("end"));
buffer.push("a");
buffer.push("b");
buffer.finish();
console.log("listeners", buffer.listenerCount("data"));
@@ -0,0 +1,63 @@
// copyFileSync/copyFile's third argument (libuv's flag bits) and
// writeFileSync/writeFile's `flag` option ("w" and the exclusive "wx").
import { copyFileSync, mkdtempSync, readFileSync, rmSync, writeFileSync, constants } from "node:fs";
import { copyFile, writeFile, readFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
const dir = mkdtempSync(join(tmpdir(), "scriptc-flags-"));
const src = join(dir, "src.txt");
const dst = join(dir, "dst.txt");
writeFileSync(src, "payload");
copyFileSync(src, dst, constants.COPYFILE_EXCL);
console.log(readFileSync(dst, "utf8"));
try {
copyFileSync(src, dst, constants.COPYFILE_EXCL);
console.log("no throw");
} catch (e) {
const err = e as NodeJS.ErrnoException;
console.log(err.code, err.syscall);
}
copyFileSync(src, dst);
console.log(readFileSync(dst, "utf8"));
try {
copyFileSync(src, join(dir, "x.txt"), 8);
} catch (e) {
const err = e as Error;
console.log(err.name, (err as NodeJS.ErrnoException).code, err.message);
}
const wx = join(dir, "wx.txt");
writeFileSync(wx, "one", { flag: "wx" });
console.log(readFileSync(wx, "utf8"));
try {
writeFileSync(wx, "two", { flag: "wx" });
console.log("no throw");
} catch (e) {
const err = e as NodeJS.ErrnoException;
console.log(err.code, err.syscall);
}
writeFileSync(wx, "three", { flag: "w" });
console.log(readFileSync(wx, "utf8"));
const modeFile = join(dir, "mode.txt");
writeFileSync(modeFile, "m", { mode: 0o600, flag: "wx" });
console.log(readFileSync(modeFile, "utf8"));
await copyFile(src, join(dir, "p.txt"), constants.COPYFILE_EXCL);
console.log(await readFile(join(dir, "p.txt"), "utf8"));
try {
await copyFile(src, join(dir, "p.txt"), constants.COPYFILE_EXCL);
console.log("no throw");
} catch (e) {
const err = e as NodeJS.ErrnoException;
console.log(err.code, err.syscall);
}
const pwx = join(dir, "pwx.txt");
await writeFile(pwx, "async-one", { flag: "wx" });
console.log(await readFile(pwx, "utf8"));
try {
await writeFile(pwx, "async-two", { flag: "wx" });
console.log("no throw");
} catch (e) {
const err = e as NodeJS.ErrnoException;
console.log(err.code, err.syscall);
}
rmSync(dir, { recursive: true, force: true });
@@ -0,0 +1,36 @@
// spawn's `detached` option as a RUNTIME boolean — the portable daemon
// idiom `detached: process.platform !== "win32"` — reaches the same
// POSIX_SPAWN_SETSID flag as the literal form: a detached child leads its
// own process group (pgid == pid), a non-detached one stays in the
// parent's group.
import { spawn } from "node:child_process";
const PGID = "ps -o pgid= -p $$ | tr -d ' '";
const mode = process.platform !== "win32";
const lead = spawn("/bin/sh", ["-c", PGID], { detached: mode, stdio: ["ignore", "pipe", "ignore"] });
let leadOut = "";
if (lead.stdout) {
lead.stdout.setEncoding("utf8");
lead.stdout.on("data", (chunk: string) => {
leadOut += chunk;
});
}
lead.on("close", () => {
console.log("detached leads its own group:", leadOut.trim() === String(lead.pid));
const joined = spawn("/bin/sh", ["-c", PGID], {
detached: false,
stdio: ["ignore", "pipe", "ignore"],
});
let joinedOut = "";
if (joined.stdout) {
joined.stdout.setEncoding("utf8");
joined.stdout.on("data", (chunk: string) => {
joinedOut += chunk;
});
}
joined.on("close", () => {
console.log("non-detached stays behind:", joinedOut.trim() !== String(joined.pid));
});
});
@@ -0,0 +1,71 @@
// StringDecoder.write over a chunk that crossed the checked-dynamic
// boundary: a string passes through unchanged, an ArrayBufferView decodes,
// and anything else throws Node's ERR_INVALID_ARG_TYPE. The decoder state
// survives an interleaved string chunk.
import { StringDecoder } from "node:string_decoder";
// The chunk arrives untyped (a stream "data" payload), like pi's exec.ts.
function write(decoder: StringDecoder, chunk: any): string {
return decoder.write(chunk);
}
const decoder = new StringDecoder("utf8");
console.log(JSON.stringify(write(decoder, "plain")));
console.log(JSON.stringify(write(decoder, Buffer.from([0x61, 0xe2]))));
console.log(JSON.stringify(write(decoder, Buffer.from([0x82, 0xac, 0x62]))));
console.log(JSON.stringify(write(decoder, "mid")));
try {
write(decoder, 123);
} catch (e) {
const err = e as Error;
console.log(err.name, (err as NodeJS.ErrnoException).code, err.message);
}
try {
write(decoder, {});
} catch (e) {
const err = e as Error;
console.log(err.name, (err as NodeJS.ErrnoException).code, err.message);
}
const ender = new StringDecoder("base64");
console.log(JSON.stringify(write(ender, Buffer.from([0x66]))));
console.log(JSON.stringify(ender.end()));
// end(chunk) is write(chunk) followed by the flush, over the same dynamic
// chunk shapes.
function endWith(decoder: StringDecoder, chunk: any): string {
return decoder.end(chunk);
}
const tail = new StringDecoder("utf8");
console.log(JSON.stringify(write(tail, "x")));
console.log(JSON.stringify(endWith(tail, Buffer.from([0xe2, 0x82]))));
console.log(JSON.stringify(endWith(new StringDecoder("utf8"), "mid")));
try {
endWith(new StringDecoder("utf8"), 123);
} catch (e) {
const err = e as Error;
console.log(err.name, (err as NodeJS.ErrnoException).code, err.message);
}
// end(undefined) only flushes; null is not a chunk.
const flushed = new StringDecoder("utf8");
console.log(JSON.stringify(write(flushed, Buffer.from([0x61, 0xe2, 0x82]))));
console.log(JSON.stringify(endWith(flushed, undefined)));
try {
endWith(new StringDecoder("utf8"), null);
} catch (e) {
const err = e as Error;
console.log(err.name, (err as NodeJS.ErrnoException).code, err.message);
}
// An ArrayBuffer or SharedArrayBuffer is not an ArrayBufferView.
for (const chunk of [new ArrayBuffer(2), new SharedArrayBuffer(2)]) {
try {
write(new StringDecoder("utf8"), chunk);
} catch (e) {
const err = e as Error;
console.log(err.name, (err as NodeJS.ErrnoException).code, err.message);
}
try {
endWith(new StringDecoder("utf8"), chunk);
} catch (e) {
const err = e as Error;
console.log(err.name, (err as NodeJS.ErrnoException).code, err.message);
}
}
@@ -0,0 +1,79 @@
// Optional-chained METHOD calls on structural receivers: a class seen
// through `Pick<Class, ...>`, an interface implemented by program classes,
// and the same receivers held in optional fields. Node calls the method
// when the receiver is present and answers undefined (no call) when not.
class CacheWarmer {
status = "idle";
onAgentSettled(): void {
console.log("settled", this.status);
}
onModeChanged(): void {
console.log("mode", this.status);
}
}
interface Renderer {
requestRender(): void;
}
class MainScreen implements Renderer {
requestRender(): void {
console.log("main");
}
}
class AltScreen implements Renderer {
requestRender(): void {
console.log("alt");
}
}
function settle(w: Pick<CacheWarmer, "onAgentSettled"> | undefined): void {
w?.onAgentSettled();
}
function tick(tui: Renderer | undefined): void {
tui?.requestRender();
}
class Session {
private cacheWarmer?: Pick<CacheWarmer, "onModeChanged" | "status">;
private renderer: Renderer | undefined;
constructor(w: CacheWarmer | undefined, r: Renderer | undefined) {
this.cacheWarmer = w;
this.renderer = r;
}
settle(): void {
this.cacheWarmer?.onModeChanged();
this.renderer?.requestRender();
}
describe(): string {
return `${this.cacheWarmer?.status ?? "none"}/${this.renderer === undefined ? "none" : "ui"}`;
}
}
settle(new CacheWarmer());
settle(undefined);
tick(new MainScreen());
tick(new AltScreen());
tick(undefined);
new Session(new CacheWarmer(), new MainScreen()).settle();
new Session(undefined, undefined).settle();
console.log(new Session(new CacheWarmer(), undefined).describe());
console.log(new Session(undefined, new AltScreen()).describe());
// A present but non-callable member still throws Node's catchable TypeError;
// the `?.` step only skips the call when the RECEIVER is absent.
const loose: any = { render: 42 };
try {
loose?.render();
} catch (e) {
console.log("call:", e instanceof TypeError, (e as Error).message);
}
const looseStep: any = { render: 42 };
try {
looseStep.render?.();
} catch (e) {
console.log("step:", e instanceof TypeError, (e as Error).message);
}
@@ -0,0 +1,28 @@
// `(x ?? fallback).method(...)` over storage the checker types
// `T | undefined`: the default takes over exactly when the stored value is
// absent — an optional record field, an optional parameter, a callback
// parameter, and an unchecked element read.
interface Options {
tools?: string[];
}
const DEFAULTS = ["read", "write"];
function active(options: Options): string[] {
const selected = options.tools;
return (selected ?? DEFAULTS).filter((name) => name !== "write");
}
function collect(...failures: Array<string[] | undefined>): string[] {
return failures.flatMap((entries) => (entries ?? []).map((entry) => entry.toUpperCase()));
}
function totals(groups: number[][] | undefined): number {
return (groups ?? []).reduce((sum, group) => sum + group.length, 0);
}
console.log(active({ tools: ["read", "write"] }).join(","));
console.log(active({}).join(","));
console.log(collect(["a"], undefined, ["b"]).join(","));
console.log(totals([[1, 2], [3]]), totals(undefined));
@@ -0,0 +1,23 @@
// Module specifiers the compiler can evaluate: a `const` binding and a `+`
// concatenation of literals name their module exactly as a string literal
// does, so dynamic import() and import.meta.resolve stay build-time
// resolutions instead of computed-specifier refusals.
import { readFileSync } from "node:fs";
import { join } from "node:path";
const FS_SPECIFIER = "node:" + "fs";
const PATH_SPECIFIER = "node:path" as const;
const PREFIX = "node:";
const JOIN_SPECIFIER = PREFIX + "path";
async function loadBuiltin(): Promise<void> {
const fs = await import(FS_SPECIFIER);
const path = await import(JOIN_SPECIFIER);
console.log(fs.existsSync("/"), fs.existsSync("/definitely-not-here-xyz"));
console.log(path.join("a", "b") === join("a", "b"), path.basename("/a/b.ts"));
}
await loadBuiltin();
console.log(import.meta.resolve(PATH_SPECIFIER).startsWith("node:path"));
console.log(import.meta.resolve(FS_SPECIFIER).startsWith("node:fs"));
@@ -0,0 +1,96 @@
// Async iteration over values whose [Symbol.asyncIterator] shape only exists
// at run time. JavaScript puts no static demand on the receiver: the protocol
// reads the method, calls it, and throws TypeError when the method is missing,
// is not callable, or returns a non-object. The checker must not refuse the
// source for the shapes inference cannot prove.
function report(label, fn) {
return Promise.resolve()
.then(fn)
.then(
(value) => console.log(label, "ok", value),
(error) => console.log(label, error instanceof TypeError, error.name),
);
}
async function* delegate(value) {
yield* value;
}
async function drain(value) {
const values = [];
for await (const item of value) values.push(item);
return values.join(",");
}
async function main() {
// A well-formed async iterator written inline still delegates.
await report("inline", () =>
drain({
async *[Symbol.asyncIterator]() {
yield "a";
yield "b";
},
}),
);
// The method is a data property, not a function.
await report("method-not-callable", () => drain({ [Symbol.asyncIterator]: 42 }));
// The method returns a number instead of an iterator object.
await report("result-not-object", () => drain({ [Symbol.asyncIterator]() { return 42; } }));
// The returned iterator's next is not callable.
await report("next-not-callable", () =>
drain({
[Symbol.asyncIterator]() {
return { next: "nope" };
},
}),
);
// No async iterator at all and no sync fallback either.
await report("no-protocol", () => drain({ value: 1 }));
// yield* delegates through the same protocol, so it throws the same way.
await report("yield-star-not-callable", async () => {
let seen = "";
for await (const value of delegate({ [Symbol.asyncIterator]: true })) seen += value;
return seen;
});
await report("yield-star-result-not-object", async () => {
let seen = "";
for await (const value of delegate({ [Symbol.asyncIterator]() { return "text"; } })) seen += value;
return seen;
});
// A non-callable async iterator wins over a working sync iterator: the
// protocol only falls back when the method is undefined.
await report("async-shadows-sync", () =>
drain({
[Symbol.asyncIterator]: 0,
*[Symbol.iterator]() {
yield "never";
},
}),
);
// A value with only a sync iterable iterates through the sync fallback.
await report("sync-only", () =>
drain({
*[Symbol.iterator]() {
yield "s1";
yield "s2";
},
}),
);
// yield* over arrays and strings keeps working.
await report("yield-star-array", async () => {
let seen = "";
for await (const value of delegate([1, 2])) seen += value;
return seen;
});
}
main();
+40
View File
@@ -0,0 +1,40 @@
// A JavaScript READ of a name nothing declares resolves at run time, exactly
// as Node does: the access throws a catchable ReferenceError whose message
// names the identifier, `typeof` answers "undefined" without throwing, and a
// call through the same name throws at the callee read (plan D-2 / C5). The
// undeclared name is lowered to the runtime's global-read helper, which
// throws Node's "<name> is not defined".
'use strict';
const describe = (label, fn) => {
try {
console.log(`${label}: ${String(fn())}`);
} catch (e) {
console.log(`${label}: threw ${e.name}: ${e.message}`);
}
};
console.log("typeof:", typeof neverDeclared);
describe("bare read", () => neverDeclared);
describe("call", () => neverDeclared(1, 2));
describe("member read", () => neverDeclared.length);
describe("arithmetic", () => neverDeclared + 1);
describe("comparison", () => neverDeclared === undefined);
describe("shorthand property", () => {
const wrapper = { neverDeclared };
return String(wrapper.neverDeclared);
});
describe("destructuring default", () => {
const pick = ({ value = unresolvableReference } = {}) => value;
return pick({});
});
describe("typeof test", () => typeof neverDeclared === "undefined");
// `typeof` never throws, so a guarded read is the one safe shape.
if (typeof maybeConfigured !== "undefined") console.log("configured:", maybeConfigured);
else console.log("configured: unset");
// A declared binding keeps the ordinary lowered path.
let declared = 3;
describe("declared", () => declared * 2);
@@ -0,0 +1,42 @@
// The encoding argument a typed write or transform callback receives:
// undefined for an object-mode write without one, the given string with
// one, and 'buffer' for byte-mode chunks.
import { Transform, Writable } from "node:stream";
const seen: string[] = [];
const objects = new Writable({
objectMode: true,
write(chunk: { n: number }, encoding, cb) {
seen.push(`object ${chunk.n} ${typeof encoding} ${String(encoding)}`);
cb();
},
});
objects.write({ n: 1 });
objects.write({ n: 2 }, "utf8");
objects.write({ n: 3 }, undefined);
objects.end(() => {
const bytes = new Writable({
write(chunk: Buffer, encoding, cb) {
seen.push(`bytes ${chunk.length} ${typeof encoding} ${String(encoding)}`);
cb();
},
});
bytes.write("abc");
bytes.write(Buffer.from("de"));
bytes.end(() => {
const upper = new Transform({
objectMode: true,
transform(row: { n: number }, encoding, cb) {
seen.push(`transform ${row.n} ${typeof encoding} ${String(encoding)}`);
cb(null, { n: row.n * 10 });
},
});
upper.on("data", (row: { n: number }) => seen.push(`data ${row.n}`));
upper.on("end", () => {
for (const line of seen) console.log(line);
});
upper.write({ n: 4 });
upper.write({ n: 5 }, "latin1");
upper.end();
});
});
@@ -0,0 +1,84 @@
// Lowering erases flow narrowing, so a checker-NARROWED receiver still
// stores its wider union. A member read dispatches on the arm the value
// actually holds: an arm that declares the member answers its slot, an arm
// that does not answers undefined (Node's get), and an arm the narrowing
// ruled out traps in the checked conversion.
interface ApiKeyCredential {
type: "api_key";
key?: string;
env?: Record<string, string>;
}
interface OAuthCredential {
type: "oauth";
access: string;
refresh: string;
expires: number;
}
type Credential = ApiKeyCredential | OAuthCredential;
function pick(stored: Credential | undefined): string | undefined {
if (stored?.type === "oauth") return stored.access;
const credential = stored?.type === "api_key" ? stored : undefined;
return credential?.key;
}
console.log("key", pick({ type: "api_key", key: "k1" }));
console.log("key absent", pick({ type: "api_key" }));
console.log("oauth", pick({ type: "oauth", access: "a1", refresh: "r1", expires: 1 }));
console.log("none", pick(undefined));
// An assertion narrows the checker's view of arms that carry different
// payloads; every arm declares the asserted member here.
interface Message {
role: string;
content: string;
}
interface ToolResult {
role: string;
output: string[];
}
type AgentMessage = Message | ToolResult;
function roleOf(message: AgentMessage): string {
return (message as { role: string }).role;
}
console.log("role", roleOf({ role: "user", content: "hi" }));
console.log("role", roleOf({ role: "tool", output: ["x"] }));
// One arm lacks the member the assertion names: Node's read answers
// undefined for that arm.
interface Stamped {
timestamp?: number;
text: string;
}
interface Unstamped {
text: string;
}
function stamp(entry: Stamped | Unstamped): number | undefined {
return (entry as { timestamp?: number }).timestamp;
}
console.log("stamp", stamp({ timestamp: 5, text: "a" }));
console.log("stamp", stamp({ text: "b" }));
// A discriminant read re-narrows the union it came from.
interface BashOperations {
exec(command: string): string;
}
interface BashResult {
exitCode: number;
}
type BashEventResult =
| { operations: BashOperations; result?: undefined }
| { operations?: undefined; result: BashResult };
function describe(result: BashEventResult | undefined): string {
if (result?.result) return `exit ${result.result.exitCode}`;
return `operations ${typeof result?.operations}`;
}
console.log("event", describe({ result: { exitCode: 3 } }));
console.log("event", describe({ operations: { exec: (command) => command } }));
console.log("event", describe(undefined));
// A narrowed array whose stored union still owns an absent arm.
function firstLength(models: string[] | undefined): number {
if (models) return models.length;
return -1;
}
console.log("first", firstLength(["a", "b"]), firstLength(undefined));
@@ -0,0 +1,58 @@
// A member the arms answer through an ACCESSOR — an inherited getter, or a
// getter a subclass overrides — has no field slot to read, so the read
// dispatches on the union's tag and calls the arm's own getter, once per
// read, as Node does. A data-field arm beside a getter arm reads its slot.
const logs: string[] = [];
class Panel {
items: string[];
constructor(items: string[]) {
this.items = items;
}
get hasOverlayEntries(): boolean {
logs.push("has");
return this.items.length > 0;
}
get count(): number {
logs.push("count");
return this.items.length;
}
}
class MainPanel extends Panel {
mode = "main";
}
class AltPanel extends Panel {
mode = "alt";
get count(): number {
logs.push("alt count");
return this.items.length * 10;
}
}
class FixedPanel {
mode = "fixed";
items: string[];
hasOverlayEntries = true;
constructor(items: string[]) {
this.items = items;
}
get count(): number {
logs.push("fixed count");
return this.items.length + 100;
}
}
function pickPanel(which: "m" | "a", items: string[]): MainPanel | AltPanel {
return which === "m" ? new MainPanel(items) : new AltPanel(items);
}
for (const which of ["m", "a"] as const) {
const panel = pickPanel(which, which === "m" ? ["x"] : []);
console.log("panel", which, panel.mode, panel.hasOverlayEntries, panel.count);
}
function pickMixed(which: "a" | "f", items: string[]): AltPanel | FixedPanel {
return which === "a" ? new AltPanel(items) : new FixedPanel(items);
}
for (const which of ["a", "f"] as const) {
const panel = pickMixed(which, ["y", "z"]);
console.log("mixed", which, panel.mode, panel.hasOverlayEntries, panel.count);
}
console.log("logs", logs.join(","));
+37
View File
@@ -0,0 +1,37 @@
// `.length` on a union of arrays (and strings): every arm answers its own
// length, so the read dispatches on the tag instead of demanding one
// record arm that carries a `length` field.
interface Model {
id: string;
}
interface ModelRef {
ref: string;
}
function ids(models: Model[] | ModelRef[]): number {
return models.length;
}
console.log("models", ids([{ id: "a" }, { id: "b" }]));
console.log("refs", ids([{ ref: "r" }, { ref: "s" }, { ref: "t" }]));
function scalars(values: string[] | number[]): number {
return values.length;
}
console.log("strings", scalars(["a", "b", "c"]));
console.log("numbers", scalars([1, 2]));
function textual(part: string | string[]): number {
return part.length;
}
console.log("text", textual("hello"));
console.log("parts", textual(["a", "b"]));
// The dispatch reads the value ONCE, even when the receiver is a call.
let calls = 0;
function make(fill: string): string[] {
calls++;
return [fill, fill];
}
function measured(part: string | string[]): number {
return part.length;
}
console.log("call", measured(make("q")), "calls", calls);
@@ -0,0 +1,40 @@
// A `keyof` read over a record that holds an `unknown` member: the
// checker's answer collapses to `unknown` (the union of every field type
// absorbs into it), so there is no typed join. The read dispatches per
// declared key through the checked-dynamic slot instead.
interface Retry {
baseDelayMs?: number;
enabled?: boolean;
}
interface Settings {
$schema?: string;
enabledModels?: string[];
branchSummary: unknown;
fullscreenWheelScrollLines?: number | string;
modelThinkingLevels?: { [key: string]: string };
retry?: Retry;
outputPad?: number;
}
function read(settings: Settings, key: keyof Settings): unknown {
return settings[key];
}
const settings: Settings = {
branchSummary: { note: "x", count: 2 },
outputPad: 4,
enabledModels: ["m"],
fullscreenWheelScrollLines: "wide",
modelThinkingLevels: { high: "big" },
retry: { enabled: true },
};
const keys: (keyof Settings)[] = [
"outputPad",
"enabledModels",
"branchSummary",
"retry",
"$schema",
"fullscreenWheelScrollLines",
];
for (const key of keys) {
const value = read(settings, key);
console.log(key, typeof value, value === undefined ? "absent" : JSON.stringify(value));
}

Some files were not shown because too many files have changed in this diff Show More