fix(contract): the flush gate's internals are #-private — they are not doors

The 10.4.11 flush single-flight work added `startFlushLeader` and
`promoteQueuedFlush` as TypeScript `private` methods. `private` is erased at
compile time, so both still land on the prototype — and the contract manifest
emitter reads the surface the BUILD exposes, skipping only names that start with
an underscore. On the next regeneration both would have been emitted as contract
doors, obliging every engine implementing contract 1 to provide the flush gate's
own bookkeeping. A door is a promise; these are internals.

Converted to ECMAScript-private (`#`), which keeps them off the prototype
entirely, and the reason is recorded on both so the next internal is not written
as `private` by habit. `_runFlush` — the flush body itself — was already safe by
the emitter's underscore rule.

Verified: `npm run build && node scripts/emit-contract-manifest.mjs` then
`--check` green at 302 doors, with neither name present.

TWO MANIFEST NOTES, both deliberate and neither hidden:

1. The regenerated manifest gains `MetadataArrayTooLargeError`. The emitter
   lists every `*Error` export from brainyError.js, and that class is the write
   door's refusal for an over-bound metadata array (this branch's array-bound
   commit). It is a real addition to the engine's error surface, so the manifest
   is right to carry it — flagged here because it is a contract-surface change
   that the cut should accept knowingly, not a side effect that slipped in.

2. `armIdleFlushTimer` and `kickBackgroundFlush` are TypeScript `private` in
   src and ARE already in the committed manifest as doors — the same leak, one
   release older. They are left exactly as they are: removing a name the
   manifest already publishes is a contract deletion, not a hygiene fix, and it
   belongs to whoever owns contract 1 rather than to this branch.
This commit is contained in:
David Snelling 2026-09-02 13:56:57 -07:00
parent f27a777615
commit 72c8ee6acd
2 changed files with 18 additions and 7 deletions

View file

@ -1507,6 +1507,7 @@
"BrainyError", "BrainyError",
"DerivedArtifactMissingError", "DerivedArtifactMissingError",
"GraphIndexNotReadyError", "GraphIndexNotReadyError",
"MetadataArrayTooLargeError",
"MetadataIndexNotReadyError", "MetadataIndexNotReadyError",
"MigrationInProgressError", "MigrationInProgressError",
"ProtectedArtifactError", "ProtectedArtifactError",

View file

@ -13342,25 +13342,33 @@ export class Brainy<T = any> implements BrainyInterface<T> {
} }
return this._flushQueued return this._flushQueued
} }
return this.startFlushLeader() return this.#startFlushLeader()
} }
/** /**
* @description Run one flush body as the leader and install it as * @description Run one flush body as the leader and install it as
* `_flushInFlight`. On settle resolved OR rejected the gate opens and * `_flushInFlight`.
*
* ECMAScript-private (`#`), not TypeScript `private`: `private` is erased at
* compile time, so the method still lands on the prototype and the contract
* manifest which reads the surface the BUILD exposes emitted it as a
* door. A door is a promise every engine implementing the contract must
* keep, and this is the flush gate's own bookkeeping, not a promise. `#`
* keeps it off the prototype, so the emitter cannot see it.
* On settle resolved OR rejected the gate opens and
* the ONE queued waiter (if any) is promoted. The `finally` callback returns * the ONE queued waiter (if any) is promoted. The `finally` callback returns
* nothing on purpose: a callback that returned the promoted run's promise * nothing on purpose: a callback that returned the promoted run's promise
* would make the leader await its own follower. * would make the leader await its own follower.
* @returns The leader's own promise, settling on its own body alone. * @returns The leader's own promise, settling on its own body alone.
*/ */
private startFlushLeader(): Promise<void> { #startFlushLeader(): Promise<void> {
const run = this._runFlush() const run = this._runFlush()
// `finally` and not `then`: a failed flush must still open the gate, or // `finally` and not `then`: a failed flush must still open the gate, or
// one rejection would wedge every later flush behind a promise nobody // one rejection would wedge every later flush behind a promise nobody
// will ever settle. // will ever settle.
const gated: Promise<void> = run.finally(() => { const gated: Promise<void> = run.finally(() => {
if (this._flushInFlight === gated) this._flushInFlight = null if (this._flushInFlight === gated) this._flushInFlight = null
this.promoteQueuedFlush() this.#promoteQueuedFlush()
}) })
this._flushInFlight = gated this._flushInFlight = gated
return gated return gated
@ -13368,12 +13376,14 @@ export class Brainy<T = any> implements BrainyInterface<T> {
/** /**
* @description Promote the single queued waiter (if one is waiting) to * @description Promote the single queued waiter (if one is waiting) to
* leader and settle its deferred from that run. Never throws into the * leader and settle its deferred from that run. ECMAScript-private for the
* same reason as {@link flush}'s leader starter: internals are not doors.
* Never throws into the
* leader's `finally`: a synchronous failure starting the promoted run is * leader's `finally`: a synchronous failure starting the promoted run is
* reported to the waiter, which must be settled on every path. * reported to the waiter, which must be settled on every path.
* @returns Nothing. * @returns Nothing.
*/ */
private promoteQueuedFlush(): void { #promoteQueuedFlush(): void {
const settle = this._flushQueuedSettle const settle = this._flushQueuedSettle
if (!settle) return if (!settle) return
// Clear BEFORE starting, so the promoted run's own joiners queue afresh // Clear BEFORE starting, so the promoted run's own joiners queue afresh
@ -13381,7 +13391,7 @@ export class Brainy<T = any> implements BrainyInterface<T> {
this._flushQueued = null this._flushQueued = null
this._flushQueuedSettle = null this._flushQueuedSettle = null
try { try {
this.startFlushLeader().then(settle.resolve, settle.reject) this.#startFlushLeader().then(settle.resolve, settle.reject)
} catch (error) { } catch (error) {
settle.reject(error) settle.reject(error)
} }