mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-27 06:43:19 +08:00
Keep the global object in type assertions of globalThis
`globalThis as { [KEY]?: State }` is the usual way for several modules
to share process-wide state. globalThis lowers to a checked-dynamic
value, and asserting a checked-dynamic value to a record or array type
took the checked conversion, which builds a new native value. A write
through the asserted value went to that copy, so other modules never
saw it; string-keyed and index-signature views threw "expected object".
- An assertion of the global object (globalThis, global, also through
`as unknown as`) keeps the checked-dynamic value when the conversion
would copy, so member reads and writes reach the global object.
- File-scope and local bindings of such an assertion hold the global
object (collectGlobals, lowerVarDecl), so an exported
`const state = globalThis as ...` works across modules.
- The divergence scan no longer reports these assertions as SC6001
width copies.
- Runtime: one global-object box per thread (scr_global_object), which
globalThis, global and globalThis.globalThis all answer, and direct
`globalThis[symbol]` accesses use that box's symbol-keyed properties,
so every spelling of the global object sees one property.
Other checked-dynamic values keep the checked conversion: a native
value held as unknown unwraps by identity there, and a checked-dynamic
tree is copied at the documented boundary.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
26946d6b22
commit
6255c9351b
@@ -0,0 +1,78 @@
|
||||
/* Type assertions of the global object.
|
||||
*
|
||||
* `globalThis as { [KEY]?: State }` is the process-global state idiom: a
|
||||
* module asserts the global object to a record type and reads and writes
|
||||
* its members, and every other module that asserts it sees the same
|
||||
* members. Node evaluates the assertion to the global object itself.
|
||||
*
|
||||
* The global object is a checked-dynamic value, and a checked-dynamic value
|
||||
* asserted to a record or array type takes the checked extraction
|
||||
* (dynCheck), which builds a new native value. For the global object that
|
||||
* copy is always wrong: a write through it never reached the global object,
|
||||
* and a view bound before another module's write never saw it. So an
|
||||
* assertion of the global object keeps the checked-dynamic value itself: a
|
||||
* view whose member reads and writes go to the global object, and bindings
|
||||
* of it hold that object.
|
||||
*
|
||||
* Other checked-dynamic operands keep the checked extraction: a native value
|
||||
* held as `unknown` unwraps by identity there, and a checked-dynamic tree is
|
||||
* copied at the documented checked-value boundary.
|
||||
*/
|
||||
import * as ts from "../ts7/adapter.js";
|
||||
import type { IrType } from "../../ir/ir.js";
|
||||
import type { Lowerer } from "./lowerer.js";
|
||||
import { stdlibGlobalNameOf } from "./surfaces.js";
|
||||
|
||||
/** A target whose checked extraction builds a new mutable value. */
|
||||
export function extractionCopiesReference(lowerer: Lowerer, type: IrType): boolean {
|
||||
const seen = new Set<string>();
|
||||
const copies = (t: IrType): boolean => {
|
||||
if (t.kind === "record" || t.kind === "array") return true;
|
||||
if (t.kind !== "union" || seen.has(t.unionId)) return false;
|
||||
seen.add(t.unionId);
|
||||
return lowerer.unions.get(t.unionId)?.arms.some(copies) ?? false;
|
||||
};
|
||||
return copies(type);
|
||||
}
|
||||
|
||||
/** The asserted expression without parentheses, `!` and nested
|
||||
* assertions (`globalThis as unknown as T`). */
|
||||
function assertedSource(expr: ts.Expression): ts.Expression {
|
||||
let e = expr;
|
||||
while (
|
||||
ts.isParenthesizedExpression(e) ||
|
||||
ts.isNonNullExpression(e) ||
|
||||
ts.isAsExpression(e) ||
|
||||
ts.isTypeAssertion(e) ||
|
||||
ts.isSatisfiesExpression(e)
|
||||
)
|
||||
e = e.expression;
|
||||
return e;
|
||||
}
|
||||
|
||||
/** `expr` asserts the global object (`globalThis`, `global`). */
|
||||
export function assertsGlobalObject(
|
||||
lowerer: Lowerer,
|
||||
expr: ts.AsExpression | ts.TypeAssertion,
|
||||
): boolean {
|
||||
const name = stdlibGlobalNameOf(lowerer, assertedSource(expr.expression));
|
||||
return name === "globalThis" || name === "global";
|
||||
}
|
||||
|
||||
/** The assertion a binding initializer is, when it keeps the global object:
|
||||
* bindings of it hold that object rather than a native value of the
|
||||
* asserted type. A target with no native layout keeps the object as well
|
||||
* (the assertion lowers to the checked-dynamic value either way). */
|
||||
export function keptIdentityAssertion(
|
||||
lowerer: Lowerer,
|
||||
initializer: ts.Expression,
|
||||
): ts.AsExpression | ts.TypeAssertion | undefined {
|
||||
let e = initializer;
|
||||
while (ts.isParenthesizedExpression(e)) e = e.expression;
|
||||
if (!ts.isAsExpression(e) && !ts.isTypeAssertion(e)) return undefined;
|
||||
if (!assertsGlobalObject(lowerer, e)) return undefined;
|
||||
const target = lowerer.mapTypeOf(lowerer.checker.getTypeFromTypeNode(e.type));
|
||||
return !target || target.kind === "dyn" || extractionCopiesReference(lowerer, target)
|
||||
? e
|
||||
: undefined;
|
||||
}
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
} from "../../diagnostics/diagnostic.js";
|
||||
import { isCollationCovered } from "./collation-repertoire.js";
|
||||
import type { Lowerer } from "./lowerer.js";
|
||||
import { keptIdentityAssertion } from "./assertion-identity.js";
|
||||
|
||||
/** How an identity-sensitive binding is used after a copy could replace it. */
|
||||
interface IdentityUse {
|
||||
@@ -217,6 +218,8 @@ export function scanDivergences(
|
||||
}
|
||||
|
||||
for (const { decl, source, type } of castDecls) {
|
||||
// An assertion of the global object keeps that object: no copy.
|
||||
if (keptIdentityAssertion(lowerer, decl.initializer!) !== undefined) continue;
|
||||
const name = (decl.name as ts.Identifier).text;
|
||||
const scope = enclosingScope(decl);
|
||||
if (scope === undefined) continue;
|
||||
|
||||
@@ -148,6 +148,7 @@ import {
|
||||
unsupportedDiag,
|
||||
} from "../../diagnostics/diagnostic.js";
|
||||
import { PoisonError, dynFallbackType, jsFuncNameOf, neverTaintedJsType, own } from "./lowerer.js";
|
||||
import { assertsGlobalObject, extractionCopiesReference } from "./assertion-identity.js";
|
||||
import { lowerCollectionSpread } from "./containers/collection-methods.js";
|
||||
import { lowerHelperReturnCall } from "./containers/iterator-helpers.js";
|
||||
import {
|
||||
@@ -9527,6 +9528,11 @@ export function lowerAsExpression(
|
||||
return inner;
|
||||
}
|
||||
}
|
||||
// The global object: the checked extraction would build a separate
|
||||
// value, so the assertion keeps the object and its member operations
|
||||
// reach it (assertion-identity.ts).
|
||||
if (extractionCopiesReference(lowerer, target) && assertsGlobalObject(lowerer, expr))
|
||||
return inner;
|
||||
if (
|
||||
target.kind === "void" ||
|
||||
!canDynCheckTo(
|
||||
|
||||
@@ -144,6 +144,7 @@ import { workerTargetPaths } from "../worker-target.js";
|
||||
import { isNativeProxyInitializer } from "./expressions/native-proxy.js";
|
||||
import { handleEventReaderPrelude } from "./lower-event-target.js";
|
||||
import { jsHoldsDynObject } from "./lower-exprs.js";
|
||||
import { keptIdentityAssertion } from "./assertion-identity.js";
|
||||
|
||||
/** One file's declarations, split for collection and init-body lowering. */
|
||||
export interface FileParts {
|
||||
@@ -1970,6 +1971,30 @@ export function collectGlobals(
|
||||
}
|
||||
continue;
|
||||
}
|
||||
// A file-scope binding of an assertion of the global object
|
||||
// (`const state = globalThis as { [KEY]?: State }`): the binding
|
||||
// holds that object, so writes through it reach the object other
|
||||
// modules read (assertion-identity.ts).
|
||||
const asserted = decl.initializer
|
||||
? keptIdentityAssertion(lowerer, decl.initializer)
|
||||
: undefined;
|
||||
if (nameNode === decl.name && !decl.type && asserted) {
|
||||
const symbol = lowerer.checker.getSymbolAtLocation(nameNode);
|
||||
if (symbol && !lowerer.globalsBySymbol.has(symbol)) {
|
||||
const g: IrGlobal = {
|
||||
id: `%g.${tag}${nsPrefix}${nameNode.text}`,
|
||||
name: nameNode.text,
|
||||
type: DYN,
|
||||
mutable: isLet,
|
||||
source: bindingSource(nameNode),
|
||||
...(!isVarDeclared(decl) ? { tdz: true as const } : {}),
|
||||
};
|
||||
lowerer.globalsBySymbol.set(symbol, g);
|
||||
lowerer.globalsList.push(g);
|
||||
if (isVarDeclared(decl)) noteVarGlobalEntryInit(lowerer, sf, g);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
// A JS file-scope evolving ARRAY (`const mustCallChecks = [];`
|
||||
// — test/common's exit-accounting ledger): the strict type
|
||||
// (any[]) has no mapping, but the VALUE is the dyn array the
|
||||
|
||||
@@ -195,6 +195,7 @@ import {
|
||||
lowerNativeFunctionAssignment,
|
||||
lowerStrictEquality,
|
||||
} from "./lower-exprs.js";
|
||||
import { keptIdentityAssertion } from "./assertion-identity.js";
|
||||
import { isNativeProxyInitializer } from "./expressions/native-proxy.js";
|
||||
import { tryLowerExpression } from "./expressions/try-lower-expression.js";
|
||||
import { lowerUnionFieldWrite } from "./expressions/union-fields.js";
|
||||
@@ -4918,9 +4919,16 @@ export function lowerVarDecl(
|
||||
lowerer.unions.get(inferredObjectType.unionId)?.arms.every(isUnitType))))) &&
|
||||
!decl.type &&
|
||||
!hasJsTypeAnnotation(decl);
|
||||
// The binding of an assertion of the global object holds that object,
|
||||
// whatever its asserted type (assertion-identity.ts).
|
||||
const keepsAssertedObject =
|
||||
init.type.kind === "dyn" &&
|
||||
!decl.type &&
|
||||
keptIdentityAssertion(lowerer, decl.initializer) !== undefined;
|
||||
if (
|
||||
expandsObject ||
|
||||
preservesObjectIdentity ||
|
||||
keepsAssertedObject ||
|
||||
(isJsSourceFile(decl.getSourceFile()) &&
|
||||
!decl.type &&
|
||||
isNativeBuiltinValueInitializer(lowerer, decl.initializer))
|
||||
@@ -5133,7 +5141,7 @@ export function lowerVarDecl(
|
||||
? npmStaticOptionalSelfWriteType(lowerer, decl, settledType)
|
||||
: null;
|
||||
settledType =
|
||||
expandsObject || preservesObjectIdentity
|
||||
expandsObject || preservesObjectIdentity || keepsAssertedObject
|
||||
? DYN
|
||||
: lowerer.runtimeOptionalBindingType(decl.name, settledType);
|
||||
if (optionalSelfWriteType !== null) settledType = optionalSelfWriteType;
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6350-globalthis-assertion-state.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6352-globalthis-state-modules/boot.ts",
|
||||
"<repo>/tests/corpus/6352-globalthis-state-modules/state.ts",
|
||||
"<repo>/tests/corpus/6352-globalthis-state-modules/main.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -926,6 +926,10 @@ void scr_dyn_key_set(ScrDyn *recv, ScrStr *key, ScrDyn *value);
|
||||
void scr_dyn_arr_named_put(ScrDyn *arr, const char *key, size_t key_len, ScrDyn *value);
|
||||
void scr_dyn_key_set_computed(ScrDyn *recv, ScrDyn *key, ScrDyn *value);
|
||||
void scr_dyn_key_delete(ScrDyn *recv, const ScrStr *key, bool strict);
|
||||
/* This thread's global object box (borrowed). Every spelling of the global
|
||||
* object answers it; its symbol-keyed properties are the global object's
|
||||
* (scr_node_builtin.c). */
|
||||
ScrDyn *scr_global_object(void);
|
||||
ScrDyn *scr_dyn_global_symbol_get(ScrSym *key); /* borrowed key; +1 value */
|
||||
void scr_dyn_global_symbol_set(ScrSym *key, ScrDyn *value); /* both borrowed */
|
||||
bool scr_dyn_global_symbol_has(ScrSym *key);
|
||||
|
||||
@@ -86,6 +86,10 @@ ScrDyn *scr_util_system_error_entries(void) {
|
||||
|
||||
static SCR_TL ScrDyn *scr_global_known;
|
||||
static SCR_TL ScrDyn *scr_global_own;
|
||||
/* The one box denoting this thread's global object. Symbol-keyed properties
|
||||
* live on the box (string-keyed ones in scr_global_own), so every spelling
|
||||
* of the global object answers this box. */
|
||||
static SCR_TL ScrDyn *scr_global_box;
|
||||
static SCR_TL bool scr_global_cleanup_registered;
|
||||
static SCR_TL ScrDyn *(*scr_global_console_get)(void);
|
||||
static SCR_TL ScrDyn *(*scr_global_fetch_get)(void);
|
||||
@@ -180,6 +184,8 @@ ScrDyn *scr_process_hrtime_value(void) {
|
||||
}
|
||||
|
||||
static void scr_global_cleanup(void) {
|
||||
scr_dyn_release(scr_global_box);
|
||||
scr_global_box = NULL;
|
||||
scr_dyn_release(scr_global_known);
|
||||
scr_dyn_release(scr_global_own);
|
||||
scr_global_known = NULL;
|
||||
@@ -195,7 +201,7 @@ static ScrDyn *scr_global_get(void *handle, const char *key, size_t length) {
|
||||
if (own) return scr_dyn_retain(own);
|
||||
if ((length == 10 && memcmp(key, "globalThis", length) == 0) ||
|
||||
(length == 6 && memcmp(key, "global", length) == 0))
|
||||
return scr_dyn_new_handle(&scr_global_known, SCR_DYNH_GLOBAL);
|
||||
return scr_dyn_retain(scr_global_box);
|
||||
if (length == 7 && memcmp(key, "console", length) == 0 && scr_global_console_get)
|
||||
return scr_global_console_get();
|
||||
if (length == 5 && memcmp(key, "fetch", length) == 0 && scr_global_fetch_get) return scr_global_fetch_get();
|
||||
@@ -223,26 +229,32 @@ static bool scr_global_set(void *handle, const char *key, size_t length, const S
|
||||
return true;
|
||||
}
|
||||
|
||||
ScrDyn *scr_global_native_init(ScrArr *known, ScrDyn *(*console_get)(void)) {
|
||||
ScrDyn *scr_global_object(void) {
|
||||
static const ScrDynHandleOps ops = {
|
||||
.cls = "Object", .retain = scr_global_retain, .release = scr_global_release,
|
||||
.get = scr_global_get, .set = scr_global_set,
|
||||
};
|
||||
scr_dyn_handle_install(SCR_DYNH_GLOBAL, &ops);
|
||||
scr_global_console_get = console_get;
|
||||
if (!scr_global_known) scr_global_known = scr_dyn_new_obj();
|
||||
if (!scr_global_own) scr_global_own = scr_dyn_new_obj();
|
||||
if (!scr_global_box) scr_global_box = scr_dyn_new_handle(&scr_global_known, SCR_DYNH_GLOBAL);
|
||||
if (!scr_global_cleanup_registered) {
|
||||
scr_atexit(scr_global_cleanup);
|
||||
scr_global_cleanup_registered = true;
|
||||
}
|
||||
return scr_global_box;
|
||||
}
|
||||
|
||||
ScrDyn *scr_global_native_init(ScrArr *known, ScrDyn *(*console_get)(void)) {
|
||||
ScrDyn *global = scr_global_object();
|
||||
scr_global_console_get = console_get;
|
||||
for (size_t i = 0; i < known->len; i++) {
|
||||
ScrStr *key = (ScrStr *)scr_arr_get_ref(known, (double)i);
|
||||
if (!scr_dyn_obj_get(scr_global_known, key->data, key->len))
|
||||
scr_dyn_obj_set(scr_global_known, key->data, key->len, scr_dyn_new_bool(true));
|
||||
scr_str_release(key);
|
||||
}
|
||||
return scr_dyn_new_handle(&scr_global_known, SCR_DYNH_GLOBAL);
|
||||
return scr_dyn_retain(global);
|
||||
}
|
||||
|
||||
typedef struct ScrBuiltinModule {
|
||||
|
||||
@@ -181,72 +181,33 @@ void scr_assert_eq_sym(ScrSym *a, ScrSym *b, bool negated, bool deep,
|
||||
scr_str_release(ib);
|
||||
}
|
||||
|
||||
/* Symbol-keyed globals share the symbol unit's link gate. The always-linked
|
||||
* JSON unit must not depend on optional symbol retain/release functions. */
|
||||
typedef struct ScrGlobalSymbolProperty {
|
||||
ScrSym *key;
|
||||
ScrDyn *value;
|
||||
struct ScrGlobalSymbolProperty *next;
|
||||
} ScrGlobalSymbolProperty;
|
||||
|
||||
static SCR_TL ScrGlobalSymbolProperty *scr_global_symbols = NULL;
|
||||
static SCR_TL bool scr_global_symbols_cleanup_registered = false;
|
||||
|
||||
static void scr_global_symbols_cleanup(void) {
|
||||
while (scr_global_symbols) {
|
||||
ScrGlobalSymbolProperty *entry = scr_global_symbols;
|
||||
scr_global_symbols = entry->next;
|
||||
scr_sym_release(entry->key);
|
||||
scr_dyn_release(entry->value);
|
||||
free(entry);
|
||||
}
|
||||
}
|
||||
|
||||
/* Direct `globalThis[symbol]` accesses use the symbol-keyed properties of
|
||||
* the global object's box (scr_global_object), the storage every other
|
||||
* spelling of the global object reaches, so all of them see one property. */
|
||||
ScrDyn *scr_dyn_global_symbol_get(ScrSym *key) {
|
||||
for (ScrGlobalSymbolProperty *entry = scr_global_symbols; entry; entry = entry->next) {
|
||||
if (entry->key == key) return scr_dyn_retain(entry->value);
|
||||
}
|
||||
return scr_dyn_retain(scr_dyn_undefined());
|
||||
ScrDyn *k = scr_dyn_new_symbol(key);
|
||||
ScrDyn *value = scr_dyn_symbol_key_get(scr_global_object(), k, false);
|
||||
scr_dyn_release(k);
|
||||
return value;
|
||||
}
|
||||
|
||||
void scr_dyn_global_symbol_set(ScrSym *key, ScrDyn *value) {
|
||||
for (ScrGlobalSymbolProperty *entry = scr_global_symbols; entry; entry = entry->next) {
|
||||
if (entry->key != key) continue;
|
||||
ScrDyn *previous = entry->value;
|
||||
entry->value = scr_dyn_retain(value);
|
||||
scr_dyn_release(previous);
|
||||
return;
|
||||
}
|
||||
if (!scr_global_symbols_cleanup_registered) {
|
||||
scr_atexit(scr_global_symbols_cleanup);
|
||||
scr_global_symbols_cleanup_registered = true;
|
||||
}
|
||||
ScrGlobalSymbolProperty *entry = malloc(sizeof *entry);
|
||||
if (!entry) scr_trap("scriptc: out of memory\n");
|
||||
entry->key = scr_sym_retain(key);
|
||||
entry->value = scr_dyn_retain(value);
|
||||
entry->next = scr_global_symbols;
|
||||
scr_global_symbols = entry;
|
||||
ScrDyn *k = scr_dyn_new_symbol(key);
|
||||
scr_dyn_symbol_key_set(scr_global_object(), k, value);
|
||||
scr_dyn_release(k);
|
||||
}
|
||||
|
||||
bool scr_dyn_global_symbol_has(ScrSym *key) {
|
||||
for (ScrGlobalSymbolProperty *entry = scr_global_symbols; entry; entry = entry->next) {
|
||||
if (entry->key == key) return true;
|
||||
}
|
||||
return false;
|
||||
ScrDyn *k = scr_dyn_new_symbol(key);
|
||||
bool present = scr_dyn_has_own_computed(scr_global_object(), k);
|
||||
scr_dyn_release(k);
|
||||
return present;
|
||||
}
|
||||
|
||||
void scr_dyn_global_symbol_delete(ScrSym *key) {
|
||||
ScrGlobalSymbolProperty **slot = &scr_global_symbols;
|
||||
while (*slot) {
|
||||
ScrGlobalSymbolProperty *entry = *slot;
|
||||
if (entry->key != key) { slot = &entry->next; continue; }
|
||||
*slot = entry->next;
|
||||
scr_sym_release(entry->key);
|
||||
scr_dyn_release(entry->value);
|
||||
free(entry);
|
||||
return;
|
||||
}
|
||||
ScrDyn *k = scr_dyn_new_symbol(key);
|
||||
scr_dyn_key_delete_computed(scr_global_object(), k, false);
|
||||
scr_dyn_release(k);
|
||||
}
|
||||
|
||||
/* ── fs.Dirent as a dynamic value ────────────────────────────────────
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
// Process-global state on the global object through a type assertion:
|
||||
// every assertion of globalThis denotes the global object itself, so a
|
||||
// write through one view is visible through every other view and through
|
||||
// a view bound before the write.
|
||||
const KEY = Symbol.for("example.state");
|
||||
interface State {
|
||||
n: number;
|
||||
log: string[];
|
||||
}
|
||||
type Holder = { [KEY]?: State; label?: string };
|
||||
|
||||
const early = globalThis as Holder;
|
||||
|
||||
function get(): State | undefined {
|
||||
return early[KEY];
|
||||
}
|
||||
|
||||
early[KEY] = { n: 1, log: [] };
|
||||
console.log(get()?.n, (globalThis as Holder)[KEY]?.n);
|
||||
|
||||
function touch(entry: string): void {
|
||||
const view = globalThis as Holder;
|
||||
const state = view[KEY] ?? (view[KEY] = { n: 0, log: [] });
|
||||
state.n++;
|
||||
state.log.push(entry);
|
||||
}
|
||||
touch("a");
|
||||
touch("b");
|
||||
console.log(early[KEY]?.n, early[KEY]?.log.join(","), get() === early[KEY]);
|
||||
|
||||
// string-keyed members of the same object
|
||||
(globalThis as Holder).label = "ready";
|
||||
console.log(early.label, (globalThis as unknown as { label?: string }).label);
|
||||
|
||||
// ??= initializes once; delete removes the member for every view
|
||||
const COUNTER = Symbol.for("example.counter");
|
||||
type Counters = { [COUNTER]?: { hits: number } };
|
||||
(globalThis as Counters)[COUNTER] ??= { hits: 0 };
|
||||
(globalThis as Counters)[COUNTER] ??= { hits: 100 };
|
||||
const counters = globalThis as Counters;
|
||||
counters[COUNTER]!.hits += 2;
|
||||
console.log((globalThis as Counters)[COUNTER]?.hits, COUNTER in globalThis);
|
||||
delete counters[COUNTER];
|
||||
console.log((globalThis as Counters)[COUNTER], COUNTER in globalThis);
|
||||
|
||||
// an index-signature view
|
||||
const dict = globalThis as unknown as Record<string, number | undefined>;
|
||||
dict.exampleTotal = 1;
|
||||
dict["exampleTotal"] = (dict["exampleTotal"] ?? 0) + 1;
|
||||
console.log((globalThis as unknown as { exampleTotal?: number }).exampleTotal);
|
||||
@@ -0,0 +1,14 @@
|
||||
// The boot module writes the state through its own assertion of globalThis.
|
||||
const STATE = Symbol.for("example.boot-state");
|
||||
const PENDING = Symbol.for("example.boot-pending");
|
||||
|
||||
interface Early {
|
||||
startedAt: number;
|
||||
steps: string[];
|
||||
}
|
||||
|
||||
export function boot(): void {
|
||||
const view = globalThis as { [STATE]?: Early; [PENDING]?: Promise<string> };
|
||||
view[STATE] = { startedAt: 42, steps: ["boot"] };
|
||||
view[PENDING] = Promise.resolve("container-1");
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
// Boot writes the state; main reads it through a binding exported by
|
||||
// another module, then takes the pending value out of the global object.
|
||||
import { boot } from "./boot.ts";
|
||||
import { PENDING_KEY, STATE_KEY, globalState, readState } from "./state.ts";
|
||||
|
||||
console.log(readState() === undefined);
|
||||
boot();
|
||||
const state = readState();
|
||||
console.log(state?.startedAt, state?.steps.join(","));
|
||||
state?.steps.push("main");
|
||||
console.log(globalState[STATE_KEY]?.steps.join(","));
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const pending = globalState[PENDING_KEY];
|
||||
globalState[PENDING_KEY] = undefined;
|
||||
console.log(await pending, globalState[PENDING_KEY]);
|
||||
}
|
||||
void main();
|
||||
@@ -0,0 +1,18 @@
|
||||
// Shared by the boot and main modules: the state lives on the global
|
||||
// object, keyed by registered symbols.
|
||||
export const STATE_KEY = Symbol.for("example.boot-state");
|
||||
export const PENDING_KEY = Symbol.for("example.boot-pending");
|
||||
|
||||
export interface BootState {
|
||||
startedAt: number;
|
||||
steps: string[];
|
||||
}
|
||||
|
||||
export const globalState = globalThis as {
|
||||
[STATE_KEY]?: BootState;
|
||||
[PENDING_KEY]?: Promise<string>;
|
||||
};
|
||||
|
||||
export function readState(): BootState | undefined {
|
||||
return globalState[STATE_KEY];
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
// Ordinary code: identity-preserving calls, declaration-ordered literals,
|
||||
// Latin-script comparisons, related-class casts and ISO dates report no
|
||||
// divergence.
|
||||
// Latin-script comparisons, related-class casts, ISO dates and views of the
|
||||
// global object report no divergence.
|
||||
interface Item {
|
||||
name: string;
|
||||
qty: number;
|
||||
@@ -32,3 +32,9 @@ function wingsOf(a: Animal): number {
|
||||
console.log(wingsOf(new Bird()), wingsOf(new Animal()));
|
||||
console.log(["b", "A", "a"].sort((p, q) => p.localeCompare(q)).join(","));
|
||||
console.log(Date.parse("2024-01-02"), new Date("2024-01-02T03:04:05+01:00").getTime());
|
||||
|
||||
// A type assertion of the global object denotes the global object itself.
|
||||
const COUNT = Symbol.for("no-divergences.count");
|
||||
const globalState = globalThis as { [COUNT]?: { n: number } };
|
||||
globalState[COUNT] = { n: 1 };
|
||||
console.log(globalState[COUNT]?.n);
|
||||
|
||||
Reference in New Issue
Block a user