mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-21 05:50:55 +08:00
- Keep empty inferred objects mutable across class fields, accessor parameters, and function values. - Preserve object identity through named, private, symbol-keyed, and static JavaScript fields.
195 lines
6.6 KiB
JavaScript
195 lines
6.6 KiB
JavaScript
const { EventEmitter } = require("node:events");
|
|
const brand = Symbol.for("@opentui/core/Renderable");
|
|
|
|
class BaseRenderable extends EventEmitter {
|
|
[brand] = true;
|
|
static renderableNumber = 1;
|
|
_id;
|
|
num;
|
|
_dirty = false;
|
|
parent = null;
|
|
_visible = true;
|
|
constructor(options) {
|
|
super();
|
|
this.num = BaseRenderable.renderableNumber++;
|
|
this._id = options.id ?? `renderable-${this.num}`;
|
|
}
|
|
get id() { return this._id; }
|
|
set id(value) { this._id = value; }
|
|
get isDirty() { return this._dirty; }
|
|
markClean() { this._dirty = false; }
|
|
markDirty() { this._dirty = true; }
|
|
get visible() { return this._visible; }
|
|
set visible(value) { this._visible = value; }
|
|
}
|
|
|
|
const first = new BaseRenderable({ id: "first" });
|
|
const second = new BaseRenderable({});
|
|
console.log(first.id, first.num, first[brand], first.visible, first.isDirty);
|
|
console.log(second.id, second.num);
|
|
first.id = "renamed";
|
|
first.visible = false;
|
|
first.markDirty();
|
|
console.log(first.id, first.visible, first.isDirty);
|
|
first.markClean();
|
|
console.log(first.isDirty);
|
|
|
|
// Allocation must make early reads safe; a bare declaration then resets
|
|
// the slot at its own position, after earlier initializer side effects.
|
|
class Ordered {
|
|
// @ts-expect-error Deliberately read before the declaration runs.
|
|
before = this.later;
|
|
write = this.seed();
|
|
later;
|
|
// @ts-expect-error A bare JS field is initialized to undefined.
|
|
after = this.later;
|
|
#private;
|
|
seed() { this.later = "temporary"; return "temporary"; }
|
|
readPrivate() { return this.#private; }
|
|
setPrivate(value) { this.#private = value; }
|
|
}
|
|
const ordered = new Ordered();
|
|
console.log(ordered.before, ordered.write, ordered.after, typeof ordered.later);
|
|
console.log(ordered.readPrivate());
|
|
ordered.setPrivate("secret");
|
|
console.log(ordered.readPrivate());
|
|
ordered.later = ["live"];
|
|
console.log(ordered.later[0]);
|
|
ordered.later = undefined;
|
|
console.log(ordered.later === undefined);
|
|
|
|
// A base constructor can call an override before derived initializers.
|
|
class EarlyBase {
|
|
value;
|
|
constructor(seed) {
|
|
console.log("early", this.read(undefined));
|
|
this.value = seed;
|
|
}
|
|
read(value) { return value; }
|
|
}
|
|
class EarlyDerived extends EarlyBase {
|
|
// @ts-expect-error Observe the base constructor's write before the reset.
|
|
sawBase = this.value;
|
|
// @ts-expect-error Intentionally reset the inherited slot to undefined.
|
|
value;
|
|
later;
|
|
read(value) { return this.later; }
|
|
}
|
|
const early = new EarlyDerived("base");
|
|
console.log(early.sawBase, early.value, early.later);
|
|
|
|
// String-keyed views and inspection include uninitialized public fields.
|
|
const { inspect } = require("node:util");
|
|
class Empty { value; }
|
|
const empty = new Empty();
|
|
function stringView(value) {
|
|
console.log(Object.keys(value).join(","), JSON.stringify(value));
|
|
}
|
|
console.log(inspect(empty));
|
|
stringView(empty);
|
|
|
|
// Reference-valued assignments remain live and release on replacement.
|
|
for (let i = 0; i < 50; i++) {
|
|
const item = new Empty();
|
|
item.value = { name: `item-${i}` };
|
|
if (i === 49) console.log(item.value.name);
|
|
item.value = ["replacement"];
|
|
item.value = undefined;
|
|
}
|
|
|
|
// Prefix/postfix updates share the declaring class's numeric static slot.
|
|
console.log(BaseRenderable.renderableNumber++, ++BaseRenderable.renderableNumber);
|
|
console.log(BaseRenderable.renderableNumber--, --BaseRenderable.renderableNumber);
|
|
BaseRenderable.renderableNumber++;
|
|
--BaseRenderable.renderableNumber;
|
|
console.log(BaseRenderable.renderableNumber);
|
|
|
|
// Explicit undefined is a value in JS fields, accessors and returns.
|
|
// Getter inference also determines an unannotated setter's parameter.
|
|
class Listener {
|
|
// @ts-expect-error Deliberately observe the slot before initialization.
|
|
before = this.value;
|
|
value = undefined;
|
|
#private = undefined;
|
|
get current() { return this.value; }
|
|
set current(value) { this.value = value; }
|
|
readPrivate() { return this.#private; }
|
|
setPrivate(value) { this.#private = value; }
|
|
}
|
|
const listener = new Listener();
|
|
console.log(listener.before, listener.value, listener.current, listener.readPrivate());
|
|
listener.current = "ready";
|
|
listener.setPrivate("secret");
|
|
console.log(listener.value, listener.current, listener.readPrivate());
|
|
listener.current = undefined;
|
|
listener.setPrivate(undefined);
|
|
console.log(listener.current, listener.readPrivate());
|
|
stringView(listener);
|
|
|
|
class UndefinedBase {
|
|
value = undefined;
|
|
constructor(value) { this.value = value; }
|
|
}
|
|
class UndefinedDerived extends UndefinedBase {
|
|
// @ts-expect-error Observe the base write before the derived reset.
|
|
seen = this.value;
|
|
value = undefined;
|
|
}
|
|
const reset = new UndefinedDerived("base");
|
|
console.log(reset.seen, reset.value);
|
|
function undefinedResult() { return undefined; }
|
|
console.log(undefinedResult());
|
|
|
|
// Assigning an accessor invokes its setter; it never declares a field,
|
|
// including from a constructor and through an inherited setter body.
|
|
class Positioned {
|
|
_left = 0;
|
|
constructor() { this.left = 1; }
|
|
get left() { return this._left; }
|
|
set left(value) { this._left = value; }
|
|
get x() { return this.left; }
|
|
set x(value) { this.left = value; }
|
|
}
|
|
class PositionedChild extends Positioned {
|
|
constructor() { super(); this.left = 2; }
|
|
}
|
|
const positioned = new PositionedChild();
|
|
console.log(positioned.left);
|
|
positioned.x = 3;
|
|
console.log(positioned.left);
|
|
stringView(positioned);
|
|
|
|
// Empty object inference leaves these mutable slots open to new properties
|
|
// and replacement objects. Accessors and aliases retain the same object.
|
|
const TRAITS = Symbol("traits");
|
|
class Traits {
|
|
value = {};
|
|
#private = {};
|
|
[TRAITS] = {};
|
|
static shared = {};
|
|
static listener = undefined;
|
|
constructor() { this.implicit = {}; }
|
|
get current() { return this.value; }
|
|
set current(value) { this.value = value; }
|
|
get privateValue() { return this.#private; }
|
|
set privateValue(value) { this.#private = value; }
|
|
readSymbol() { return this[TRAITS]; }
|
|
writeSymbol(value) { this[TRAITS] = value; }
|
|
}
|
|
const traits = new Traits();
|
|
const otherTraits = new Traits();
|
|
traits.value.enabled = true;
|
|
console.log(traits.current.enabled, otherTraits.current.enabled);
|
|
const replacement = { name: "replacement" };
|
|
traits.current = replacement;
|
|
console.log(traits.current === replacement, Object.keys(traits.current).join(","));
|
|
replacement.name = "updated";
|
|
console.log(traits.current.name);
|
|
traits.privateValue = replacement;
|
|
traits.writeSymbol(replacement);
|
|
traits.implicit = replacement;
|
|
console.log(traits.privateValue === replacement, traits.readSymbol() === replacement, traits.implicit === replacement);
|
|
Traits.shared = replacement;
|
|
Traits.listener = "ready";
|
|
console.log(Traits.shared === replacement, Traits.shared.name, Traits.listener);
|