@@ -46,6 +46,9 @@ const emnapiTSFN = {
4646 /* size_t */ queue_size : 0 ,
4747 /* bool */ is_some : 0 ,
4848 /* void* */ queue : 0 ,
49+ // Reuse uv_async_t storage as JS-side wakeup state: pending event + scheduled drain.
50+ async_pending : 0 ,
51+ async_u_fd : 0 ,
4952 /* size_t */ thread_count : 0 ,
5053 /* int32_t */ state : 0 ,
5154 /* atomic_uchar */ dispatch_state : 0 ,
@@ -70,6 +73,8 @@ const emnapiTSFN = {
7073 emnapiTSFN . offset . queue_size = NapiTSFNOffset64 . queue_size
7174 emnapiTSFN . offset . is_some = NapiTSFNOffset64 . async_resource_is_some
7275 emnapiTSFN . offset . queue = NapiTSFNOffset64 . queue
76+ emnapiTSFN . offset . async_pending = NapiTSFNOffset64 . async_pending
77+ emnapiTSFN . offset . async_u_fd = NapiTSFNOffset64 . async_u_fd
7378 emnapiTSFN . offset . thread_count = NapiTSFNOffset64 . thread_count
7479 emnapiTSFN . offset . state = NapiTSFNOffset64 . state
7580 emnapiTSFN . offset . dispatch_state = NapiTSFNOffset64 . dispatch_state
@@ -92,6 +97,8 @@ const emnapiTSFN = {
9297 emnapiTSFN . offset . queue_size = NapiTSFNOffset32 . queue_size
9398 emnapiTSFN . offset . is_some = NapiTSFNOffset32 . async_resource_is_some
9499 emnapiTSFN . offset . queue = NapiTSFNOffset32 . queue
100+ emnapiTSFN . offset . async_pending = NapiTSFNOffset32 . async_pending
101+ emnapiTSFN . offset . async_u_fd = NapiTSFNOffset32 . async_u_fd
95102 emnapiTSFN . offset . thread_count = NapiTSFNOffset32 . thread_count
96103 emnapiTSFN . offset . state = NapiTSFNOffset32 . state
97104 emnapiTSFN . offset . dispatch_state = NapiTSFNOffset32 . dispatch_state
@@ -129,7 +136,10 @@ const emnapiTSFN = {
129136 const type = __emnapi__ . type
130137 const payload = __emnapi__ . payload
131138 if ( type === 'tsfn-send' ) {
132- emnapiTSFN . dispatch ( payload . tsfn )
139+ const pendng = payload . tsfn + emnapiTSFN . offset . async_pending
140+ if ( Atomics . load ( new Int32Array ( wasmMemory . buffer ) , pendng >>> 2 ) !== 0 ) {
141+ emnapiTSFN . enqueue ( payload . tsfn )
142+ }
133143 }
134144 }
135145 }
@@ -721,24 +731,77 @@ const emnapiTSFN = {
721731 emnapiTSFN . send ( func )
722732 }
723733 } ,
734+ enqueue ( func : number ) : void {
735+ // `pending` means a worker thread has requested a wakeup that has not
736+ // been drained on the main thread yet.
737+ const pending = func + emnapiTSFN . offset . async_pending
738+ // `scheduled` prevents queueing the same main-thread drain chain more than
739+ // once while a previous wakeup is still in flight.
740+ const scheduled = func + emnapiTSFN . offset . async_u_fd
741+ const i32a = new Int32Array ( wasmMemory . buffer )
742+ if ( Atomics . exchange ( i32a , scheduled >>> 2 , 1 ) !== 0 ) {
743+ return
744+ }
745+
746+ // Match uv_async_send-style coalescing in JS: the first turn represents
747+ // the wakeup reaching the main thread, and the second turn performs the
748+ // actual TSFN drain after nearby Send/Signal calls have had a chance to
749+ // collapse into the shared AsyncProgressWorker state.
750+ emnapiCtx . features . setImmediate ( ( ) => {
751+ if ( Atomics . load ( i32a , pending >>> 2 ) === 0 ) {
752+ Atomics . store ( i32a , scheduled >>> 2 , 0 )
753+ return
754+ }
755+
756+ emnapiCtx . features . setImmediate ( ( ) => {
757+ try {
758+ // Consume the coalesced wakeup once, then let dispatch() observe any
759+ // queue mutations through dispatch_state like the C implementation.
760+ if ( Atomics . exchange ( i32a , pending >>> 2 , 0 ) === 0 ) {
761+ return
762+ }
763+ emnapiTSFN . dispatch ( func )
764+ } finally {
765+ // Allow a later wakeup to schedule a new drain chain. If another
766+ // worker-thread send raced with this drain, enqueue one more turn.
767+ Atomics . store ( i32a , scheduled >>> 2 , 0 )
768+ if ( Atomics . load ( i32a , pending >>> 2 ) !== 0 ) {
769+ emnapiTSFN . enqueue ( func )
770+ }
771+ }
772+ } )
773+ } )
774+ } ,
724775 send ( func : number ) : void {
725776 const current_state = Atomics . or ( new Uint32Array ( wasmMemory . buffer ) , ( func + emnapiTSFN . offset . dispatch_state ) >>> 2 , 1 << 1 )
726777 if ( ( current_state & 1 ) === 1 ) {
727778 return
728779 }
729- if ( ( typeof ENVIRONMENT_IS_PTHREAD !== 'undefined' ) && ENVIRONMENT_IS_PTHREAD ) {
730- postMessage ( {
731- __emnapi__ : {
732- type : 'tsfn-send' ,
733- payload : {
734- tsfn : func
780+
781+ const pendng = func + emnapiTSFN . offset . async_pending
782+ // A wakeup is already pending, so this send only needs to leave the queued
783+ // work in the TSFN queue and let the existing drain pick it up.
784+ if ( Atomics . load ( new Int32Array ( wasmMemory . buffer ) , pendng >>> 2 ) !== 0 ) {
785+ return
786+ }
787+
788+ if ( Atomics . exchange ( new Int32Array ( wasmMemory . buffer ) , pendng >>> 2 , 1 ) === 0 ) {
789+ if ( ( typeof ENVIRONMENT_IS_PTHREAD !== 'undefined' ) && ENVIRONMENT_IS_PTHREAD ) {
790+ // Worker threads only post a wakeup token. Main-thread draining is
791+ // serialized by enqueue() once the message is received.
792+ postMessage ( {
793+ __emnapi__ : {
794+ type : 'tsfn-send' ,
795+ payload : {
796+ tsfn : func
797+ }
735798 }
736- }
737- } )
738- } else {
739- emnapiCtx . features . setImmediate ( ( ) => {
740- emnapiTSFN . dispatch ( func )
741- } )
799+ } )
800+ } else {
801+ // On the main thread we can skip the cross-thread hop and schedule the
802+ // coalesced drain chain directly.
803+ emnapiTSFN . enqueue ( func )
804+ }
742805 }
743806 }
744807}
0 commit comments