mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-01 07:33:12 +08:00
Merge main
This commit is contained in:
@@ -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);
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
export declare function hello(who: string): string;
|
||||
export declare const NAME: string;
|
||||
+8
@@ -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 };
|
||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
||||
{ "name": "pkg-a", "version": "1.0.0", "main": "index.js", "types": "index.d.ts" }
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
export declare class Counter {
|
||||
n: number;
|
||||
next(): number;
|
||||
}
|
||||
export declare function make(): Counter;
|
||||
+14
@@ -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 };
|
||||
Generated
Vendored
+1
@@ -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);
|
||||
+1
@@ -0,0 +1 @@
|
||||
export declare function currentMode(): string;
|
||||
+7
@@ -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 };
|
||||
+1
@@ -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());
|
||||
Generated
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
export declare function quadruple(n: number): number;
|
||||
export declare function calls(): number;
|
||||
Generated
Vendored
+14
@@ -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 };
|
||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
||||
{ "name": "native-wrapper", "version": "1.0.0", "main": "index.js", "types": "index.d.ts" }
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
export declare function twice(n: number): number;
|
||||
export declare function count(): number;
|
||||
+18
@@ -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 };
|
||||
Generated
Vendored
+1
@@ -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"));
|
||||
+1
@@ -0,0 +1 @@
|
||||
hello from data
|
||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
||||
export declare function greet(who: string): string;
|
||||
+14
@@ -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 };
|
||||
Generated
Vendored
+1
@@ -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"));
|
||||
+1
@@ -0,0 +1 @@
|
||||
export declare function toAscii(domain: string): string;
|
||||
+8
@@ -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 };
|
||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
||||
{ "name": "puny-pkg", "version": "1.0.0", "main": "index.js", "types": "index.d.ts" }
|
||||
+1
@@ -0,0 +1 @@
|
||||
export declare function ok(): string;
|
||||
+6
@@ -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";
|
||||
Generated
Vendored
+1
@@ -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
@@ -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;
|
||||
+1
@@ -0,0 +1 @@
|
||||
{ "name": "envkit", "version": "1.0.0", "main": "index.js" }
|
||||
+8
@@ -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";
|
||||
+1
@@ -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())
|
||||
+5
@@ -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;
|
||||
+1
@@ -0,0 +1 @@
|
||||
{ "name": "lazykit", "version": "1.0.0", "main": "index.js" }
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
// Ships no declarations; required only with a comment before the paren.
|
||||
exports.wave = (name) => `wave ${name}`;
|
||||
+1
@@ -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);
|
||||
+8
@@ -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;
|
||||
+17
@@ -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);
|
||||
}
|
||||
+1
@@ -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" } } }
|
||||
@@ -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(","));
|
||||
Generated
Vendored
+9
@@ -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;
|
||||
Generated
Vendored
+35
@@ -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 };
|
||||
Generated
Vendored
+1
@@ -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]);
|
||||
@@ -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));
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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();
|
||||
@@ -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(","));
|
||||
@@ -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
Reference in New Issue
Block a user