2026-08-05 15:55:16 +02:00
// === AsyncifyScheduler (S2 — scheduler core: registry, deferred wakes, single writer) ===
2026-08-05 14:16:33 +02:00
// __WX_SCHEDULER_SHIM_SOURCE__ — injector idempotence sentinel. Must appear ONLY in
// this file: the obvious marker (__wxSchedulerInstalled) also occurs in evtloop.cpp's
// EM_JS probe text inside every glue, which made the injector skip real injections.
2026-08-05 13:43:23 +02:00
// docs/features/async/17-mailbox-scheduler-plan.md · injected only on WX_SCHEDULER=1 builds.
//
2026-08-05 15:55:16 +02:00
// S2 state: this file REPLACES the legacy handlesleep.js on scheduler builds (the
// injector's either-or flip). It carries:
// S1 · the wx mailbox (timer/wheel messages, wx/wasm/private/mailbox.h) and the
// embind mutator lane (doc 18 classification).
// S2 · the scheduler core — every behavior the legacy shim provided (per-sleep
// currData capture/restore, the stale-fiber consume-once/quarantine guard,
// wake-window flags, flight recorder, trampoline heal) PLUS:
// - DEFERRED WAKES: a sleep wake arriving while a transition is in flight
// (state != Normal, or the fiber trampoline mid-loop) is queued and
// delivered from a clean macrotask when the slot frees — the aliased-wake
// class becomes unrepresentable instead of merely detected (doc 12 §law).
// - N1 SINGLE-WRITER TRIPWIRE: Asyncify.currData is an accessor; a write
// from pure JS (no wasm frames on the export stack) without scheduler
// authorization is a STRAY — beaconed, counted, and (opt-in strict mode)
// thrown. Wasm-driven writes (fiber_swap, handleSleep internals) are
// runtime-legitimate and pass through.
// Contract surfaces kept name-identical (external readers!): Asyncify.__wakingRoot
// (libcontext EM_JS wasm_root_wake_in_flight), __inSleepWake, __wakingOwnerFiber,
// __pendingSleepContexts, Fibers.__fcsTotal/__rootHotTotal/__rootFiber/
// __validSuspensions/__internallyParked/__parkSleepBuf/__inFiberEntry,
// the "[wx-asyncify] STATE"/"RECORDER" dump formats, window.__wxAsyncifyDump.
2026-08-05 13:43:23 +02:00
if ( typeof Asyncify !== "undefined" && ! globalThis . _ _wxSchedulerInstalled ) {
globalThis . _ _wxSchedulerInstalled = true ;
Asyncify . _ _schedulerBuild = 1 ;
var AsyncifyScheduler = {
2026-08-05 15:55:16 +02:00
// --- S1 wx mailbox ----------------------------------------------------
2026-08-05 14:16:33 +02:00
mailbox : [ ] ,
enqueued : 0 ,
delivered : 0 ,
_tickArmed : false ,
enqueueAfter : function ( fn , arg , ms ) {
var self = this ;
setTimeout ( function ( ) {
2026-08-05 17:46:28 +02:00
if ( self . dead ) return ; // S6: never deliver into a torn-down app
2026-08-05 14:16:33 +02:00
self . mailbox . push ( { fn : fn , arg : arg } ) ;
self . enqueued ++ ;
self . _armDeliveryTick ( ) ;
} , ms ) ;
} ,
pop : function ( ) {
var m = this . mailbox . shift ( ) ;
if ( m ) this . delivered ++ ;
return m || null ;
} ,
// Deliver via a dedicated PLAIN export call (wxWasmMailboxTick), never
// from inside a pump's awaited ProcessEvents ccall — a fiber swap there
// sits on the JS-awaits-a-suspending-export boundary (#13302) and traps.
2026-08-07 01:29:56 +02:00
// Resume ready contexts from a FRESH task. Separate from the mailbox tick
// because it must run even when the mailbox is empty: a context wake is
// work the pump owns, not a queued message.
_armSchedPump : function ( ) {
if ( this . _pumpArmed ) return ;
this . _pumpArmed = true ;
var self = this ;
setTimeout ( function ( ) {
self . _pumpArmed = false ;
if ( self . dead ) return ;
try {
if ( Module [ "_wxWasmSchedPump" ] ) Module [ "_wxWasmSchedPump" ] ( ) ;
} catch ( e ) {
if ( Module [ "_wx_dispatch_abandon" ] ) Module [ "_wx_dispatch_abandon" ] ( ) ;
docs 22 Phase B: gaps 1+2 closed, and the D-on boundary measured on KiCad
Phase B increment recorded in §10. The wx battery is GREEN at D-on (395/1,
the 1 pre-existing) with dispatch contexts, context waits and star transfers
all live - the first clean battery of the migration. Gaps 1 and 2 from the
D-on probe are closed (terminal coroutine finish; wake/refusal semantics),
and a third containment was found and added: an exception escaping a handler
propagates out through drain()'s fiber swap and would otherwise leave the
registry mid-transition, dead-pumping every later wait. Shim carries the new
abandon call; .ci-cache-epoch -> 12.
THE BOUNDARY: on the full KiCad suite D-on loses four canvas-tool specs
(draw-wires, draw-lines, move-with-m, presence-locks move) to `index out of
bounds` in doRewind - the blue screen itself. Real tool coroutines park IN
PLACE inside their bodies, and a star transfer over an already-parked stack
rewinds state the fiber layer cannot see; the harness's coroutines yield
cleanly, so it goes green while KiCad does not (the doc-19 lesson again:
the harness models the shape, not the parks). So D cannot carry KiCad until
the tool-body park sites are contexts too - C+E completion, which §5 already
ordered before the flip. This measurement makes that ordering non-negotiable.
Landing state verified: STAR_DISPATCH=0, kicad 139 passed / 1 (pre-existing
occ-probe glb) = the Phase A baseline exactly, wx battery 395/1.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LBjomQfKyRa3jBdeAKpmTw
2026-08-07 19:05:00 +02:00
// The exception escaped a context through drain()'s fiber swap, so
// the transition it started never completed: without this every
// later pump refuses ("transition in flight") and all outstanding
// waits stall forever (doc 22 Phase B).
if ( Module [ "_wxWasmSchedAbandon" ] ) Module [ "_wxWasmSchedAbandon" ] ( ) ;
2026-08-07 01:29:56 +02:00
throw e ;
}
} , 0 ) ;
} ,
2026-08-05 14:16:33 +02:00
_armDeliveryTick : function ( ) {
if ( this . _tickArmed ) return ;
this . _tickArmed = true ;
var self = this ;
setTimeout ( function tick ( ) {
2026-08-05 17:46:28 +02:00
if ( self . dead ) { self . _tickArmed = false ; return ; }
2026-08-05 14:16:33 +02:00
try {
if ( Module [ "_wxWasmMailboxTick" ] ) Module [ "_wxWasmMailboxTick" ] ( ) ;
} catch ( e ) {
self . _tickArmed = false ;
if ( Module [ "_wx_dispatch_abandon" ] ) Module [ "_wx_dispatch_abandon" ] ( ) ;
2026-08-05 20:10:33 +02:00
// Same containment as the top-level tick's error path (evtloop.cpp):
// a throwing handler must not leave a parked quasi-modal unresolved.
// No-ops when no such wait is open (5101 = wxID_CANCEL).
self . resolveTopWait ( 'nested' , 0 ) ;
self . resolveTopWait ( 'modal' , 5101 ) ;
2026-08-05 14:16:33 +02:00
throw e ;
}
if ( self . mailbox . length > 0 ) {
setTimeout ( tick , 17 ) ;
} else {
self . _tickArmed = false ;
}
} , 0 ) ;
} ,
2026-08-05 15:01:52 +02:00
// --- S1 embind lane ---------------------------------------------------
MUTATOR _NAMES : [
"kicadSetChrome" , "kicadSetReadOnly" ,
"kicadCollabApply" , "kicadCollabApplyItems" ,
"kicadCollabSnapshot" , "kicadCollabSnapshotItems" ,
"kicadCollabPresenceStart" , "kicadCollabSetRemote" ,
"kicadCollabSetPins" , "kicadCollabSetStyle" ,
"kicadCollabSetViewport" , "kicadCollabFitViewport" ,
"kicadCollabReleaseSelection" , "kicadSetColorTheme" ,
"kicadSaveBoard" , "kicadSaveSchematic" , "kicadSaveDrawingSheet" ,
] ,
mutatorQueue : [ ] ,
mutatorsWrapped : 0 ,
mutatorsDelivered : 0 ,
_mutatorPumpArmed : false ,
_openBusy : function ( ) {
var probe = Module [ "kicadOpenFileBusy" ] ;
if ( typeof probe !== "function" ) return false ;
try { return ! ! probe ( ) ; } catch ( e ) { return true ; }
} ,
_wrapMutators : function ( ) {
var self = this ;
this . MUTATOR _NAMES . forEach ( function ( name ) {
var orig = Module [ name ] ;
if ( typeof orig !== "function" ) return ;
self . mutatorsWrapped ++ ;
Module [ name ] = function ( ) {
var args = arguments ;
var call = function ( ) { return orig . apply ( Module , args ) ; } ;
if ( self . mutatorQueue . length === 0 && ! self . _openBusy ( ) ) {
self . mutatorsDelivered ++ ;
return call ( ) ;
}
return new Promise ( function ( resolve , reject ) {
self . mutatorQueue . push ( { name : name , call : call , resolve : resolve , reject : reject } ) ;
self . _armMutatorPump ( ) ;
} ) ;
} ;
} ) ;
if ( this . mutatorsWrapped > 0 )
console . log ( "[wx-scheduler] embind lane: wrapped " + this . mutatorsWrapped + " mutator(s)" ) ;
} ,
_armMutatorPump : function ( ) {
if ( this . _mutatorPumpArmed ) return ;
this . _mutatorPumpArmed = true ;
var self = this ;
var now = ( typeof performance !== "undefined" && performance . now )
? function ( ) { return performance . now ( ) ; }
: function ( ) { return Date . now ( ) ; } ;
setTimeout ( function pump ( ) {
2026-08-05 17:46:28 +02:00
if ( self . dead ) { self . _mutatorPumpArmed = false ; return ; }
2026-08-05 15:55:16 +02:00
// Unkillable: an exception escaping this body would end the setTimeout
// chain and wedge the queue forever (observed: 559 frozen messages).
2026-08-05 15:01:52 +02:00
try {
if ( ! self . _openBusy ( ) ) {
2026-08-05 15:55:16 +02:00
// Time-boxed drain: ~8 ms of work per 16 ms tick keeps the page
// live while a long backlog drains in order.
2026-08-05 15:01:52 +02:00
var t0 = now ( ) ;
while ( self . mutatorQueue . length > 0 && now ( ) - t0 < 8 ) {
2026-08-05 15:55:16 +02:00
if ( self . _openBusy ( ) ) break ;
2026-08-05 15:01:52 +02:00
var m = self . mutatorQueue . shift ( ) ;
self . mutatorsDelivered ++ ;
try { m . resolve ( m . call ( ) ) ; } catch ( e ) { m . reject ( e ) ; }
}
}
} catch ( e ) {
self . _pumpErrors = ( self . _pumpErrors || 0 ) + 1 ;
if ( self . _pumpErrors <= 5 )
console . warn ( "[wx-scheduler] mutator pump error (occurrence "
+ self . _pumpErrors + "): " + e ) ;
}
if ( self . mutatorQueue . length > 0 ) setTimeout ( pump , 16 ) ;
else self . _mutatorPumpArmed = false ;
} , 16 ) ;
} ,
2026-08-05 16:54:29 +02:00
// --- S4 wait registry ---------------------------------------------------
// Token-based waits (doc 13 §2: wasm_begin_async_wait / wasm_yield_until /
// wasm_resolve_wait). A wait is begun BEFORE the C++ side parks, so a
// resolve that races ahead of the park (EndModal during Show()) simply
// pre-resolves the promise — yieldUntil then returns immediately. Per-kind
// LIFO stacks give wx modal/nested semantics ("innermost first") without
// the legacy per-wait resolver stacks (_wxModalResolvers /
2026-08-05 20:10:33 +02:00
// _wxNestedLoopExit — deleted at doc 20 D-1). Resolution flows
2026-08-05 16:54:29 +02:00
// through the S2 deferred-wake law automatically: resolving a wait wakes
// its parked sleep via the wrapped handleSleep path.
2026-08-08 16:18:00 +02:00
waits : new Map ( ) , // token → {kind, promise, resolve, resolved, result, awaited, contextParked}
2026-08-05 16:54:29 +02:00
waitSeq : 0 ,
waitStacks : { } , // kind → [unresolved tokens], LIFO
waitsBegun : 0 ,
waitsResolved : 0 ,
2026-08-08 16:18:00 +02:00
earlyWaitResolves : 0 , // resolves that landed before their waiter parked (Phase E)
2026-08-05 16:54:29 +02:00
beginWait : function ( kind ) {
var token = ++ this . waitSeq ;
var entry = { kind : kind , resolved : false , resolve : null , promise : null } ;
var self = this ;
entry . promise = new Promise ( function ( resolve ) { entry . resolve = resolve ; } ) ;
this . waits . set ( token , entry ) ;
( this . waitStacks [ kind ] = this . waitStacks [ kind ] || [ ] ) . push ( token ) ;
this . waitsBegun ++ ;
return token ;
} ,
waitPromise : function ( token ) {
var entry = this . waits . get ( token ) ;
if ( ! entry ) {
console . warn ( "[wx-scheduler] waitPromise(" + token + "): unknown token" ) ;
return Promise . resolve ( 0 ) ;
}
2026-08-08 16:18:00 +02:00
if ( entry . resolved ) {
// Resolved before the waiter parked (Phase E early-resolve window).
// Consume the retained entry and hand the real result over — the old
// path warned "unknown token" and returned 0, dropping it.
this . waits . delete ( token ) ;
return Promise . resolve ( entry . result | 0 ) ;
}
entry . awaited = true ;
2026-08-05 16:54:29 +02:00
return entry . promise ;
} ,
2026-08-08 16:18:00 +02:00
// Phase E: a wait resolved before its C++ waiter reached the park keeps its
// entry (see resolveWait) so the result is not lost. wxWasmYieldUntil peeks
// before parking a context and consumes the result instead of parking a
// context nobody will ever resume.
waitEarlyResolved : function ( token ) {
var entry = this . waits . get ( token ) ;
return entry && entry . resolved ? 1 : 0 ;
} ,
takeWaitResult : function ( token ) {
var entry = this . waits . get ( token ) ;
if ( ! entry || ! entry . resolved ) return 0 ;
this . waits . delete ( token ) ;
return entry . result | 0 ;
} ,
2026-08-07 01:29:56 +02:00
// doc 22 Phase C: this token's waiter parked a SCHEDULER CONTEXT instead of
// suspending its stack in place, so there is no promise anyone awaits —
// resolving one would strand the context forever. Marked from C++ at park
// time; resolveWait routes such tokens to the registry instead.
noteContextWait : function ( token ) {
var entry = this . waits . get ( token ) ;
if ( entry ) entry . contextParked = true ;
} ,
2026-08-05 16:54:29 +02:00
resolveWait : function ( token , result ) {
var entry = this . waits . get ( token ) ;
if ( ! entry || entry . resolved ) return false ;
entry . resolved = true ;
this . waitsResolved ++ ;
var stack = this . waitStacks [ entry . kind ] ;
if ( stack ) {
var idx = stack . indexOf ( token ) ;
if ( idx !== - 1 ) stack . splice ( idx , 1 ) ;
}
2026-08-07 01:29:56 +02:00
if ( entry . contextParked ) {
// Mark ready only — never resume inline. The pump picks it up from a
// fresh task, which is doc 13 §1.4's deferred-wake law applied to
// contexts (a rewind inside this resolver's own turn is the whole
// class of bug the scheduler exists to remove).
2026-08-08 16:18:00 +02:00
this . waits . delete ( token ) ;
2026-08-07 01:29:56 +02:00
try {
Module [ "_wxWasmSchedResolveContextWait" ] ( token , result | 0 ) ;
} catch ( e ) {
console . warn ( "[wx-scheduler] context wait " + token + " resolve failed: " + e ) ;
}
this . _armSchedPump ( ) ;
return true ;
}
2026-08-08 16:18:00 +02:00
entry . result = result | 0 ;
2026-08-05 16:54:29 +02:00
entry . resolve ( result | 0 ) ;
2026-08-08 16:18:00 +02:00
if ( entry . awaited ) {
this . waits . delete ( token ) ;
} else {
// Nobody has parked on this token yet (Phase E early-resolve window:
// a bridge whose request settled before the C++ frame reached the
// park). Keep the entry, result attached — wxWasmYieldUntil or a late
// waitPromise consumes it. Deleting here is what stranded the first
// Phase E attempt: the later park waited on a wake nobody could send.
this . earlyWaitResolves ++ ;
}
2026-08-05 16:54:29 +02:00
return true ;
} ,
// Resolve the INNERMOST unresolved wait of a kind (wx LIFO semantics).
resolveTopWait : function ( kind , result ) {
var stack = this . waitStacks [ kind ] ;
if ( ! stack || stack . length === 0 ) return false ;
return this . resolveWait ( stack [ stack . length - 1 ] , result ) ;
} ,
pendingWaits : function ( kind ) {
var stack = this . waitStacks [ kind ] ;
return stack ? stack . length : 0 ;
} ,
2026-08-05 15:55:16 +02:00
// --- S2 scheduler core state -------------------------------------------
// Deferred sleep wakes: {deliver, result} queued because a transition was
// in flight when the wake arrived. Delivered FIFO from a clean macrotask.
readyWakes : [ ] ,
deferredWakes : 0 ,
drainedWakes : 0 ,
_wakeDrainArmed : false ,
// N1: pure-JS currData writes seen without scheduler authorization.
strayWrites : 0 ,
2026-08-08 17:58:41 +02:00
// Phase E: fresh in-place Asyncify parks that began on a NON-main stack
// (tool coroutine / scheduler context). Must be ZERO at the flip.
inplaceParksOnFiberStack : 0 ,
2026-08-05 15:55:16 +02:00
strictStrays : false , // tests set true → stray throws instead of beaconing
_authorizedWrite : 0 ,
authorize : function ( fn ) {
this . _authorizedWrite ++ ;
try { return fn ( ) ; } finally { this . _authorizedWrite -- ; }
} ,
2026-08-05 13:43:23 +02:00
2026-08-05 17:46:28 +02:00
// --- S6 lifetime --------------------------------------------------------
// Called when the wx main loop exits (DoRun's top-level return path). The
// app object is about to be destroyed: delivering anything after this
// point runs callbacks into freed C++ state. Queued mutators reject,
// queued messages and wakes drop — loudly, so a teardown that strands
// work is visible in the console instead of surfacing as a later UAF.
dead : false ,
shutdown : function ( reason ) {
if ( this . dead ) return ;
this . dead = true ;
var stranded = {
mailbox : this . mailbox . length ,
mutators : this . mutatorQueue . length ,
wakes : this . readyWakes . length ,
waits : this . waits . size ,
} ;
this . mailbox . length = 0 ;
for ( var i = 0 ; i < this . mutatorQueue . length ; i ++ ) {
try { this . mutatorQueue [ i ] . reject ( new Error ( "[wx-scheduler] shutdown: " + reason ) ) ; } catch ( e ) { }
}
this . mutatorQueue . length = 0 ;
this . readyWakes . length = 0 ;
if ( stranded . mailbox || stranded . mutators || stranded . wakes || stranded . waits ) {
console . warn ( "[wx-scheduler] shutdown (" + reason + ") stranded:"
+ " mailbox=" + stranded . mailbox
+ " mutators=" + stranded . mutators
+ " wakes=" + stranded . wakes
+ " pendingWaits=" + stranded . waits ) ;
} else {
console . log ( "[wx-scheduler] shutdown (" + reason + ") clean" ) ;
}
} ,
2026-08-05 13:43:23 +02:00
state : function ( ) {
2026-08-05 15:55:16 +02:00
return "[wx-scheduler] build=1 impl=S2-core"
2026-08-05 17:46:28 +02:00
+ ( this . dead ? " DEAD" : "" )
2026-08-05 14:16:33 +02:00
+ " mailbox=" + this . mailbox . length
+ " enqueued=" + this . enqueued
+ " delivered=" + this . delivered
2026-08-05 15:01:52 +02:00
+ " mutQ=" + this . mutatorQueue . length
+ " mutWrapped=" + this . mutatorsWrapped
+ " mutDelivered=" + this . mutatorsDelivered
2026-08-05 15:55:16 +02:00
+ " readyWakes=" + this . readyWakes . length
+ " deferredWakes=" + this . deferredWakes
+ " drainedWakes=" + this . drainedWakes
2026-08-05 16:54:29 +02:00
+ " strayWrites=" + this . strayWrites
+ " waits=" + this . waits . size
+ " waitsBegun=" + this . waitsBegun
2026-08-08 17:58:41 +02:00
+ " waitsResolved=" + this . waitsResolved
+ " earlyWaitResolves=" + this . earlyWaitResolves
+ " fiberStackParks=" + this . inplaceParksOnFiberStack ;
2026-08-05 13:43:23 +02:00
} ,
} ;
globalThis . _ _wxScheduler = AsyncifyScheduler ;
2026-08-05 15:01:52 +02:00
2026-08-05 15:55:16 +02:00
// ======================================================================
// S2 core install. Skipped defensively if the legacy shim somehow got in
// first — double-managing the wake path corrupts (the injector's either-or
// flip should make this unreachable).
// ======================================================================
if ( Asyncify . _ _nestedHandleSleepInstalled ) {
console . warn ( "[wx-scheduler] legacy handlesleep present - S2 core NOT installed (dual-management guard)" ) ;
} else if ( typeof Asyncify . handleSleep === "function"
&& typeof Asyncify . allocateData === "function" ) {
// --- flight recorder + state dump (ported verbatim-in-spirit from
// handlesleep.js; formats are parsed by guard-beacons.ts and
// apps/tests/tools/repro-board-load.ts — do not change shapes) ---------
Asyncify . _ _pendingSleepContexts = [ ] ;
var _ _recMax = 96 ;
Asyncify . _ _rec = [ ] ;
var _ _rec = function ( ev ) {
var r = Asyncify . _ _rec ;
r . push ( ( ( typeof performance !== "undefined" ? performance . now ( ) : 0 ) | 0 ) + " " + ev ) ;
if ( r . length > _ _recMax ) r . shift ( ) ;
} ;
Asyncify . _ _recPush = _ _rec ;
var _ _dumpState = function ( ) {
var F = ( typeof Fibers !== "undefined" ) ? Fibers : null ;
var pend = Array . isArray ( Asyncify . _ _pendingSleepContexts )
? Asyncify . _ _pendingSleepContexts . map ( function ( c ) { return c . capturedData || 0 ; } ) . join ( "," )
: "n/a" ;
var head = "[wx-asyncify] STATE"
+ " state=" + Asyncify . state
+ " currData=" + ( Asyncify . currData || 0 )
+ " inSleepWake=" + ( Asyncify . _ _inSleepWake || 0 )
+ " exportStack=" + ( Asyncify . exportCallStack ? Asyncify . exportCallStack . length : - 1 )
+ " pendingSleeps=[" + pend + "]"
+ ( F ? ( " nextFiber=" + F . nextFiber
+ " trampolining=" + F . trampolineRunning
+ " root=" + F . _ _rootFiber
+ " fcsTotal=" + ( F . _ _fcsTotal || 0 )
+ " rootHotTotal=" + ( F . _ _rootHotTotal || 0 )
+ " valid=[" + ( F . _ _validSuspensions ? Array . from ( F . _ _validSuspensions ) . join ( "," ) : "" ) + "]"
+ " parked=[" + ( F . _ _internallyParked ? Array . from ( F . _ _internallyParked ) . join ( "," ) : "" ) + "]"
+ " deferrals=" + ( F . _ _rootDeferrals || 0 ) )
: " (no Fibers)" )
+ " | " + AsyncifyScheduler . state ( ) ;
return head + "\n[wx-asyncify] RECORDER (oldest first):\n " + Asyncify . _ _rec . join ( "\n " ) ;
} ;
if ( typeof window !== "undefined" ) {
window . _ _wxAsyncifyDump = _ _dumpState ;
var _ _dumps = 0 ;
var _ _onTrap = function ( msg ) {
if ( _ _dumps >= 2 ) return ;
if ( ! /index out of bounds|unreachable executed|table index|indirect call signature|null function or function signature|memory access out of bounds/i . test ( msg ) ) return ;
++ _ _dumps ;
try { console . error ( _ _dumpState ( ) ) ; } catch ( e ) { }
} ;
window . addEventListener ( "error" , function ( e ) {
_ _onTrap ( e && e . error instanceof Error ? e . error . message : String ( ( e && e . message ) || "" ) ) ;
} ) ;
window . addEventListener ( "unhandledrejection" , function ( e ) {
_ _onTrap ( e && e . reason instanceof Error ? e . reason . message : String ( ( e && e . reason ) || "" ) ) ;
} ) ;
}
var _ _wxAsyncifyReport = ( function ( ) {
var counts = { } ;
return function ( kind , msg , withStack ) {
var n = ( counts [ kind ] = ( counts [ kind ] || 0 ) + 1 ) ;
if ( n > 10 && n % 100 !== 0 ) return ;
var line = "[wx-asyncify] " + kind + ": " + msg + " (occurrence " + n + ")" ;
if ( withStack ) {
try { line += "\n" + String ( new Error ( ) . stack ) . split ( "\n" ) . slice ( 1 , 8 ) . join ( "\n" ) ; } catch ( e ) { }
}
console . warn ( line ) ;
} ;
} ) ( ) ;
// --- N1: single-writer accessor on Asyncify.currData ------------------
// Writes made while compiled code is on the export stack are the wasm
// runtime's own (fiber_swap, handleSleep's park/stop paths) — legitimate.
// A pure-JS write (empty export stack) must come from a scheduler-
// authorized span; anything else is a STRAY: the exact shape of every
// historical corruption's bad write. Beacon + count; strict mode throws.
( function ( ) {
var realCurrData = Asyncify . currData ; // null at install time
Object . defineProperty ( Asyncify , "currData" , {
configurable : true ,
get : function ( ) { return realCurrData ; } ,
set : function ( v ) {
if ( ( ! Asyncify . exportCallStack || Asyncify . exportCallStack . length === 0 )
&& AsyncifyScheduler . _authorizedWrite === 0
&& ! ( typeof Fibers !== "undefined" && Fibers . trampolineRunning ) ) {
AsyncifyScheduler . strayWrites ++ ;
_ _wxAsyncifyReport ( "stray-currdata-write" ,
"currData=" + ( v || 0 ) + " written from pure JS without scheduler authorization" , true ) ;
if ( AsyncifyScheduler . strictStrays )
throw new Error ( "[wx-scheduler] stray currData write (strict mode)" ) ;
}
realCurrData = v ;
} ,
} ) ;
} ) ( ) ;
// --- deferred-wake drain ----------------------------------------------
var _ _transitionFree = function ( ) {
return Asyncify . state === 0
&& ! ( typeof Fibers !== "undefined" && Fibers . trampolineRunning ) ;
} ;
AsyncifyScheduler . _scheduleWakeDrain = function ( ) {
if ( this . _wakeDrainArmed ) return ;
this . _wakeDrainArmed = true ;
var self = this ;
setTimeout ( function ( ) {
self . _wakeDrainArmed = false ;
2026-08-05 17:46:28 +02:00
if ( self . dead ) return ; // S6: parked stacks are gone with the app
2026-08-05 15:55:16 +02:00
// Deliver from a CLEAN macrotask (export stack empty by construction).
while ( self . readyWakes . length > 0 && _ _transitionFree ( ) ) {
var w = self . readyWakes . shift ( ) ;
self . drainedWakes ++ ;
w . deliver ( w . result ) ;
}
if ( self . readyWakes . length > 0 ) self . _scheduleWakeDrain ( ) ;
} , 0 ) ;
} ;
// --- handleSleep wrap: registry + capture/restore + deferral ----------
var _ _originalAllocateData = Asyncify . allocateData . bind ( Asyncify ) ;
Asyncify . allocateData = function ( ) {
var ptr = _ _originalAllocateData ( ) ;
for ( var i = Asyncify . _ _pendingSleepContexts . length - 1 ; i >= 0 ; -- i ) {
var ctx = Asyncify . _ _pendingSleepContexts [ i ] ;
if ( ! ctx . capturedData ) {
ctx . capturedData = ptr ;
break ;
}
}
return ptr ;
} ;
var _ _originalHandleSleep = Asyncify . handleSleep . bind ( Asyncify ) ;
Asyncify . handleSleep = function ( startAsync ) {
_ _rec ( "sleep s=" + Asyncify . state + " cd=" + ( Asyncify . currData || 0 )
+ " w=" + ( Asyncify . _ _inSleepWake || 0 ) ) ;
if ( Asyncify . state === 0 && Asyncify . currData ) {
_ _wxAsyncifyReport ( "concurrent-park" ,
"handleSleep entered while currData=" + Asyncify . currData , true ) ;
}
if ( Asyncify . state === 1 ) {
_ _wxAsyncifyReport ( "reentrant-state" ,
"handleSleep entered mid-unwind (state=1) currData=" + Asyncify . currData , true ) ;
}
// Only a FRESH park (state 0) allocates data and needs tracking; the
// state-2 resume re-entry returns synchronously through the rewind
// branch (a context pushed for it leaks one per resume).
if ( Asyncify . state !== 0 ) {
return _ _originalHandleSleep ( startAsync ) ;
}
2026-08-09 14:46:06 +02:00
// Phase F (doc 22 §10 F2/F3): report every fresh in-place park to the
// REGISTRY. Begin() returns the owning context id (0 = main stack);
// while recorded, fiber_enterable()/fiber_transfer refuse entering that
// context — the registry-owned replacement for the deleted quarantine.
// Also the Phase E telemetry: fiberStackParks must be 0 at the flip's
// repro gate. Leaf probe into wasm; state is 0 here so no unwind is in
2026-08-08 17:58:41 +02:00
// flight yet.
2026-08-09 14:46:06 +02:00
var parkOwnerCtx = 0 ;
2026-08-08 17:58:41 +02:00
try {
2026-08-09 14:46:06 +02:00
if ( Module [ "_wxWasmSchedInplaceParkBegin" ] ) {
parkOwnerCtx = Module [ "_wxWasmSchedInplaceParkBegin" ] ( ) | 0 ;
if ( parkOwnerCtx ) {
AsyncifyScheduler . inplaceParksOnFiberStack ++ ;
_ _rec ( "inplace-park-on-fiber-stack ctx=" + parkOwnerCtx
+ " n=" + AsyncifyScheduler . inplaceParksOnFiberStack ) ;
}
2026-08-08 17:58:41 +02:00
}
} catch ( e ) { /* probe must never break a park */ }
2026-08-05 15:55:16 +02:00
var sleepCtx = {
capturedData : null ,
cleanedUp : false ,
2026-08-09 14:46:06 +02:00
parkOwnerCtx : parkOwnerCtx ,
2026-08-05 15:55:16 +02:00
rootOwned : ( typeof Fibers === "undefined" )
|| ( ! Fibers . _ _inFiberEntry
&& ! ( Asyncify . _ _wakingOwnerFiber || false ) ) ,
} ;
Asyncify . _ _pendingSleepContexts . push ( sleepCtx ) ;
var cleanup = function ( ) {
if ( sleepCtx . cleanedUp ) return ;
sleepCtx . cleanedUp = true ;
2026-08-09 14:46:06 +02:00
if ( sleepCtx . parkOwnerCtx ) {
try {
if ( Module [ "_wxWasmSchedInplaceParkEnd" ] ) {
Module [ "_wxWasmSchedInplaceParkEnd" ] ( sleepCtx . parkOwnerCtx ) ;
}
} catch ( e ) { /* never break a wake */ }
sleepCtx . parkOwnerCtx = 0 ;
}
2026-08-05 15:55:16 +02:00
var idx = Asyncify . _ _pendingSleepContexts . indexOf ( sleepCtx ) ;
if ( idx !== - 1 ) Asyncify . _ _pendingSleepContexts . splice ( idx , 1 ) ;
} ;
try {
return _ _originalHandleSleep ( function ( wakeUp ) {
// deliver(): the legacy shim's whole wake path — restore OUR buffer,
// mark the wake window, swallow the "unwind" sentinel.
var deliver = function ( result ) {
_ _rec ( "wake buf=" + ( sleepCtx . capturedData || 0 ) + " cdWas=" + ( Asyncify . currData || 0 )
+ ( sleepCtx . rootOwned ? " R" : " f" ) ) ;
if ( sleepCtx . capturedData ) {
if ( Asyncify . currData !== sleepCtx . capturedData ) {
_ _wxAsyncifyReport (
Asyncify . currData ? "aliased-wake-live" : "overlapped-wake" ,
"restoring currData=" + sleepCtx . capturedData +
" over " + ( Asyncify . currData || "null" ) +
" state=" + Asyncify . state , ! ! Asyncify . currData ) ;
}
AsyncifyScheduler . authorize ( function ( ) {
Asyncify . currData = sleepCtx . capturedData ;
} ) ;
}
cleanup ( ) ;
Asyncify . _ _inSleepWake = ( Asyncify . _ _inSleepWake || 0 ) + 1 ;
var prevWakingOwnerFiber = Asyncify . _ _wakingOwnerFiber || false ;
Asyncify . _ _wakingOwnerFiber = ! sleepCtx . rootOwned ;
var prevWakingRoot = Asyncify . _ _wakingRoot || 0 ;
if ( sleepCtx . rootOwned ) Asyncify . _ _wakingRoot = ( Asyncify . _ _wakingRoot || 0 ) + 1 ;
try {
return wakeUp ( result ) ;
} catch ( e ) {
if ( e === "unwind" ) return ;
throw e ;
} finally {
Asyncify . _ _inSleepWake -= 1 ;
Asyncify . _ _wakingOwnerFiber = prevWakingOwnerFiber ;
if ( sleepCtx . rootOwned ) Asyncify . _ _wakingRoot = prevWakingRoot ;
}
} ;
return startAsync ( function ( result ) {
// THE S2 LAW (doc 12): a wake never starts a rewind while another
// transition is in flight — it enqueues and the drain delivers
// from a clean macrotask when the slot frees. The legacy shim
// could only beacon this window (aliased-wake-live); the
// scheduler removes it.
if ( ! _ _transitionFree ( ) ) {
AsyncifyScheduler . deferredWakes ++ ;
_ _rec ( "defer-wake buf=" + ( sleepCtx . capturedData || 0 )
+ " s=" + Asyncify . state ) ;
AsyncifyScheduler . readyWakes . push ( { deliver : deliver , result : result } ) ;
AsyncifyScheduler . _scheduleWakeDrain ( ) ;
return ;
}
return deliver ( result ) ;
} ) ;
} ) ;
} catch ( e ) {
cleanup ( ) ;
throw e ;
}
} ;
// Transition-completion signal: maybeStopUnwind is where an unwind
// finishes (state → Normal) and the trampoline runs queued fiber
// switches. After it settles, deferred wakes may proceed.
var _ _originalMaybeStopUnwind = Asyncify . maybeStopUnwind . bind ( Asyncify ) ;
Asyncify . maybeStopUnwind = function ( ) {
var ret = _ _originalMaybeStopUnwind ( ) ;
if ( AsyncifyScheduler . readyWakes . length > 0 && _ _transitionFree ( ) )
AsyncifyScheduler . _scheduleWakeDrain ( ) ;
return ret ;
} ;
Asyncify . _ _nestedHandleSleepInstalled = true ; // compat: tools probe this
console . log ( "[wx-scheduler] S2 core installed (deferred wakes + N1 accessor)" ) ;
}
// --- stale-fiber-rewind guard (ported from handlesleep.js; semantics
// unchanged — these encode the consume-once/quarantine contracts of
// docs/features/async/16) + trampoline heal ownership -------------------
2026-08-09 14:46:06 +02:00
// Phase F F2/F3 (doc 22 §10, 2026-08-09): deletion was built, measured and
// REVERTED. The registry now carries the in-place-park fact
// (wxWasmSchedInplaceParkBegin/End) and refuses on the transfer lane, but
// the quarantine's DROP is still the only correct recovery for a misrouted
// yield-back under attribution rot (a C++-level refusal ghost-resumes the
// yielding coroutine — measured as the lever's phase2 overshoot). This
// block stays until attribution is registry-authoritative (gap 3).
2026-08-05 15:55:16 +02:00
if ( typeof Fibers !== "undefined"
&& typeof Fibers . finishContextSwitch === "function"
&& ! Fibers . _ _staleRewindGuardInstalled ) {
Fibers . _ _validSuspensions = new Set ( ) ;
Fibers . _ _internallyParked = new Set ( ) ;
Fibers . _ _parkSleepBuf = new Map ( ) ;
var _ _origFinishContextSwitch = Fibers . finishContextSwitch . bind ( Fibers ) ;
var _ _fiberRefusals = 0 ;
var _ _fcsRec = ( typeof Asyncify !== "undefined" && Asyncify . _ _recPush )
? Asyncify . _ _recPush
: function ( ) { } ;
var _ _refuseFiber = function ( newFiber , why ) {
_ _fcsRec ( "refuse new=" + newFiber ) ;
++ _ _fiberRefusals ;
if ( _ _fiberRefusals <= 10 || _ _fiberRefusals % 100 === 0 ) {
console . warn ( "[wx-asyncify] fiber-resume-refused: fiber=" + newFiber + " " + why
+ " (occurrence " + _ _fiberRefusals + ")" ) ;
}
AsyncifyScheduler . authorize ( function ( ) {
Asyncify . currData = null ;
} ) ;
} ;
Fibers . finishContextSwitch = function ( newFiber ) {
Fibers . _ _fcsTotal = ( Fibers . _ _fcsTotal || 0 ) + 1 ;
if ( newFiber === Fibers . _ _rootFiber && ( Asyncify . _ _inSleepWake || 0 ) > 0 ) {
Fibers . _ _rootHotTotal = ( Fibers . _ _rootHotTotal || 0 ) + 1 ;
}
var _ _remStr = "" ;
if ( Asyncify . currData ) {
var _ _H = ( typeof GROWABLE _HEAP _U32 === "function" ) ? GROWABLE _HEAP _U32 ( ) : HEAPU32 ;
_ _remStr = " rem=" + ( _ _H [ ( ( Asyncify . currData + 4 ) >>> 2 ) >>> 0 ] - _ _H [ ( Asyncify . currData >>> 2 ) >>> 0 ] )
+ " rf=" + ( Asyncify . getDataRewindFuncName ? Asyncify . getDataRewindFuncName ( Asyncify . currData ) : "?" )
+ " es=[" + ( Asyncify . exportCallStack || [ ] ) . join ( "|" ) + "]" ;
}
_ _fcsRec ( "fcs old=" + ( Asyncify . currData ? Asyncify . currData - 20 : 0 )
+ " new=" + newFiber
+ ( newFiber === Fibers . _ _rootFiber ? " ROOT" : "" )
+ " w=" + ( Asyncify . _ _inSleepWake || 0 ) + _ _remStr ) ;
if ( Asyncify . currData ) {
var oldFiber = Asyncify . currData - 20 ;
if ( Fibers . _ _rootFiber === undefined ) {
Fibers . _ _rootFiber = oldFiber ;
}
var parkBuf = Fibers . _ _parkSleepBuf . get ( oldFiber ) ;
var stillParked = parkBuf !== undefined
&& Array . isArray ( Asyncify . _ _pendingSleepContexts )
&& Asyncify . _ _pendingSleepContexts . some ( function ( c ) { return c . capturedData === parkBuf ; } ) ;
if ( ! stillParked ) {
Fibers . _ _validSuspensions . add ( oldFiber ) ;
Fibers . _ _internallyParked . delete ( oldFiber ) ;
Fibers . _ _parkSleepBuf . delete ( oldFiber ) ;
}
}
var isRoot = newFiber === Fibers . _ _rootFiber ;
var HEAPU32v = ( typeof GROWABLE _HEAP _U32 === "function" ) ? GROWABLE _HEAP _U32 ( ) : HEAPU32 ;
var entryPoint = HEAPU32v [ ( ( newFiber + 12 ) >>> 2 ) >>> 0 ] ;
if ( ! isRoot && Fibers . _ _internallyParked . has ( newFiber ) ) {
_ _refuseFiber ( newFiber , "is asyncify-parked mid-body (sleep in flight)" ) ;
return ;
}
if ( entryPoint === 0 ) {
if ( ! Fibers . _ _validSuspensions . has ( newFiber ) ) {
_ _refuseFiber ( newFiber , isRoot
? "root suspension already consumed - a second rewind would replay stale frames"
: "has no live suspension - rewinding would replay stale data" ) ;
return ;
}
Fibers . _ _validSuspensions . delete ( newFiber ) ;
}
if ( ! isRoot ) Fibers . _ _inFiberEntry = ( Fibers . _ _inFiberEntry || 0 ) + 1 ;
var ret ;
try {
// The original writes currData (entry path nulls it, resume path sets
// the fiber's buffer) from pure JS — scheduler-supervised here.
ret = AsyncifyScheduler . authorize ( function ( ) {
return _ _origFinishContextSwitch ( newFiber ) ;
} ) ;
} finally {
if ( ! isRoot ) Fibers . _ _inFiberEntry -= 1 ;
}
if ( ! isRoot && ! Fibers . nextFiber && Asyncify . currData ) {
Fibers . _ _internallyParked . add ( newFiber ) ;
Fibers . _ _parkSleepBuf . set ( newFiber , Asyncify . currData ) ;
}
return ret ;
} ;
// Trampoline heal ownership (subsumes inject-dyncall-shims §3c): a throw
// escaping the trampoline loop must not leave trampolineRunning wedged —
// that guard being stuck turns every later fiber swap into a silent no-op.
var _ _origTrampoline = Fibers . trampoline . bind ( Fibers ) ;
Fibers . trampoline = function ( ) {
try {
return _ _origTrampoline ( ) ;
} catch ( e ) {
Fibers . trampolineRunning = false ;
throw e ;
}
} ;
Fibers . _ _staleRewindGuardInstalled = true ;
}
// Wrap the embind mutators once the runtime has registered them.
2026-08-05 15:01:52 +02:00
if ( typeof Module !== "undefined" ) {
if ( Module [ "calledRun" ] ) {
AsyncifyScheduler . _wrapMutators ( ) ;
} else {
var _ _wxSchedPrevInit = Module [ "onRuntimeInitialized" ] ;
Module [ "onRuntimeInitialized" ] = function ( ) {
if ( typeof _ _wxSchedPrevInit === "function" ) _ _wxSchedPrevInit ( ) ;
AsyncifyScheduler . _wrapMutators ( ) ;
} ;
}
}
2026-08-05 15:55:16 +02:00
console . log ( "[wx-scheduler] scaffolding installed (S2, core live)" ) ;
2026-08-05 13:43:23 +02:00
}
// === End AsyncifyScheduler ===