@@ -66,11 +66,16 @@ biggest pain points of our API. This was mostly caused by how we defined
6666` FutureJob ` :
6767
6868``` rust
69+ pub struct NativeJob {
70+ f : Box <dyn FnOnce (& mut Context ) -> JsResult <JsValue >>,
71+ realm : Option <Realm >,
72+ }
73+
6974pub type FutureJob = Pin <Box <dyn Future <Output = NativeJob > + 'static >>;
7075```
7176
7277With this definition, it was pretty much impossible to capture the ` Context `
73- inside the future , and functions that needed to interweave engine operations
78+ inside the ` Future ` , and functions that needed to interweave engine operations
7479with awaiting ` Future ` s would have to be split into multiple parts:
7580
7681``` rust
@@ -99,8 +104,7 @@ let fetch = async move {
99104// `JobQueue`.
100105context
101106 . job_queue ()
102- . enqueue_future_job (Box :: pin (fetch ), context )
103- }
107+ . enqueue_future_job (Box :: pin (fetch ), context );
104108```
105109
106110We wanted to improve this API, and the solution we thought about was to make
@@ -173,16 +177,228 @@ pub trait JobExecutor: Any {
173177}
174178```
175179
176- As you can probably tell, we made a lot of changes on the ` JobExecutor ` :
180+ As you can probably tell, we made a lot of changes on ` JobExecutor ` :
177181
178- TODO
182+ - All methods now take ` Rc<Self> ` as their receiver, making it consistent with
183+ how the ` Context ` itself stores the ` JobExecutor ` .
184+ - [ ` enqueue_promise_job ` ] and [ ` enqueue_future_job ` ] now are unified in a single
185+ ` enqueue_job ` , where ` Job ` is an enum containing the type of job that needs to
186+ be scheduled. This makes it much simpler to extend the engine with newer job
187+ types in the future, such as the newly introduced ` TimeoutJob ` and ` GenericJob `
188+ types.
189+ - ` run_jobs_async ` was converted to a proper async function, and excluded from
190+ ` JobExecutor ` 's VTable. Additionally, this method now takes a ` &RefCell<&mut Context> `
191+ as its context, which is the missing piece that enables sharing the ` Context ` between
192+ multiple ` Future ` s at the same time. This, however, means that we cannot provide
193+ a convenient wrapper such as [ ` Context::run_jobs ` ] anymore, which is one of the
194+ reasons why we decided to exclude that method from ` JobExecutor ` 's VTable.
179195
180- ### Revamped ` ModuleLoader `
196+ These changes not only made ` JobExecutor ` much simpler, but it also expanded
197+ the places where we could use its async capabilities to handle "special"
198+ features of ECMAScript that are more suited to an async way of doing things.
199+ ` ModuleLoader ` is one of those places.
181200
182- TODO
201+ [ `enqueue_promise_job` ] : https://docs.rs/boa_engine/0.20.0/boa_engine/job/trait.JobQueue.html#tymethod.enqueue_promise_job
202+ [ `enqueue_future_job` ] : https://docs.rs/boa_engine/0.20.0/boa_engine/job/trait.JobQueue.html#tymethod.enqueue_future_job
203+ [ `Context::run_jobs` ] : https://docs.rs/boa_engine/0.20.0/boa_engine/context/struct.Context.html#method.run_jobs
204+
205+ ### Asyncified ` ModuleLoader `
206+
207+ Looking at the previous definition of ` ModuleLoader ` :
208+
209+ ``` rust
210+ pub trait ModuleLoader {
211+ // Required method
212+ fn load_imported_module (
213+ & self ,
214+ referrer : Referrer ,
215+ specifier : JsString ,
216+ finish_load : Box <dyn FnOnce (JsResult <Module >, & mut Context )>,
217+ context : & mut Context ,
218+ );
219+
220+ // Provided methods
221+ fn register_module (& self , _specifier : JsString , _module : Module ) { ... }
222+ fn get_module (& self , _specifier : JsString ) -> Option <Module > { ... }
223+ fn init_import_meta (
224+ & self ,
225+ _import_meta : & JsObject ,
226+ _module : & Module ,
227+ _context : & mut Context ,
228+ ) { ... }
229+ }
230+ ```
231+
232+ ... the weird ` finish_load ` on ` load_imported_module ` immediately pops up as an anomaly.
233+ In this case, ` finish_load ` is Boa's equivalent to
234+ [ HostLoadImportedModule ( referrer, moduleRequest, hostDefined, payload )] [ hlim ] ,
235+ which is an abstract operation that is primarily used to define how an application
236+ will load and resolve a "module request"; think of it as a function that takes
237+ the ` "module-name" ` from ` import * as name from "module-name" ` , then does
238+ "things" to load the module that corresponds to ` "module_name" ` .
239+
240+ [ hlim ] : https://tc39.es/ecma262/#sec-HostLoadImportedModule
241+
242+ The peculiarity about this abstract operation is that it doesn't return anything!
243+ Instead, it just has a special requirement:
244+
245+ > The host environment must perform ` FinishLoadingImportedModule(referrer, moduleRequest, payload, result) ` ,
246+ where result is either a normal completion containing the loaded ` Module Record ` or a throw completion,
247+ either synchronously or asynchronously.
248+
249+ Why expose the hook this way? Well, there is a clue in the previous requirement:
250+
251+ > ... either synchronously or asynchronously.
252+
253+ Aha! Directly returning from the hook makes it very hard to enable use cases
254+ where an application wants to load multiple modules asynchronously. Thus, the
255+ specification instead exposes a hook to pass the name of the module that needs to
256+ be loaded, and delegates the task of running the "post-load" phase to the host, which
257+ enables fetching modules synchronously or asynchronously, depending on the specific
258+ requirements of each application.
259+
260+ One downside of this definition, however, is that any data that is required
261+ by the engine to properly process the returned module would need to be transparently
262+ passed to the ` FinishLoadingImportedModule ` abstract operation, which is why
263+ the hook also has an additional requirement:
264+
265+ > The operation must treat ` payload ` as an opaque value to be passed through to
266+ ` FinishLoadingImportedModule ` .
267+
268+ ` payload ` is precisely that data, and it may change depending on how the module
269+ is imported in the code; ` import "module" ` and ` import("module") ` are two examples
270+ of this.
271+
272+ We could expose this as an opaque ` *const () ` pointer argument and call it a day,
273+ but we're using Rust, dang it! and we like statically guaranteed safety!
274+ So, instead, we exposed ` FinishLoadingImportedModule ` as ` finish_load ` , which is a
275+ "closure" that captures ` payload ` on its stack, and can be called anywhere
276+ (like inside a ` Future ` ) on the application with a proper ` Module ` and ` Context `
277+ to further continue processing the module loaded by the ` ModuleLoader ` .
278+
279+ ``` rust
280+ ...
281+ finish_load : Box <dyn FnOnce (JsResult <Module >, & mut Context )>,
282+ ...
283+ ```
284+
285+ Unfortunately,
286+ this API has downsides: it is still possible to forget to call ` finish_load ` ,
287+ which is still safe but prone to bugs. It is also really painful to work with,
288+ because you cannot capture the ` Context ` to further process the module after
289+ loading it ... Sounds familiar? ** This is exactly [ the code snippet we talked about before!] ( #async-apis-enhancements ) **
290+
291+ Fast forward a couple of years and we're now changing big parts of ` JobExecutor ` :
292+ adding new job types, tinkering with ` JobExecutor ` , changing API signatures, etc.
293+ Then, while looking at the definition of ` ModuleLoader ` , we thought...
294+
295+ > Huh, can't we make ` load_imported_module ` async now?
296+
297+ And that's exactly what we did! Behold, the new ` ModuleLoader ` !
298+
299+ ``` rust
300+ pub trait ModuleLoader : Any {
301+ async fn load_imported_module (
302+ self : Rc <Self >,
303+ referrer : Referrer ,
304+ specifier : JsString ,
305+ context : & RefCell <& mut Context >,
306+ ) -> JsResult <Module >;
307+
308+ fn init_import_meta (
309+ self : Rc <Self >,
310+ _import_meta : & JsObject ,
311+ _module : & Module ,
312+ _context : & mut Context ,
313+ ) {
314+ }
315+ }
316+ ```
317+
318+ Then, the code snippet we mentioned before nicely simplifies to:
319+
320+ ``` rust
321+ async fn load_imported_module (
322+ self : Rc <Self >,
323+ _referrer : boa_engine :: module :: Referrer ,
324+ specifier : JsString ,
325+ context : & RefCell <& mut Context >,
326+ ) -> JsResult <Module > {
327+ let url = specifier . to_std_string_escaped ();
328+
329+ let response = async {
330+ let request = Request :: get (& url )
331+ . redirect_policy (RedirectPolicy :: Limit (5 ))
332+ . body (())? ;
333+ let response = request . send_async (). await ? . text (). await ? ;
334+ Ok (response )
335+ }
336+ . await
337+ . map_err (| err : isahc :: Error | JsNativeError :: typ (). with_message (err . to_string ()))? ;
338+
339+ let source = Source :: from_bytes (& response );
340+
341+ Module :: parse (source , None , & mut context . borrow_mut ())
342+ }
343+ ```
344+
345+ > * What about synchronous applications?*
346+
347+ The advantage of having ` JobExecutor ` be the main entry point for any Rust
348+ ` Future ` s that are enqueued by the engine is that an application can decide how to
349+ handle all ` Future ` s received by the implementation of ` JobExecutor ` . Thus, an application
350+ that doesn't want to deal with async Rust executors can implement a completely synchronous
351+ ` ModuleLoader ` and poll on all futures received by ` JobExecutor ` using something like
352+ [ ` futures_lite::poll_once ` ] [ poll_once ] .
353+
354+ > * Why not just block on each ` Future ` one by one instead?*
355+
356+ Well, there is one new built-in that was introduced on this release which heavily
357+ depends on "properly" running ` Future ` s, and by "properly" we mean "not blocking
358+ the whole thread waiting on a future to finish". More on that in a bit.
183359
184360### Built-ins updates
185361
362+ #### Atomics.waitAsync
363+
364+ This release adds support for the ` Atomics.waitAsync ` method introduced in
365+ ECMAScript's 2024 specification.
366+ This method allows doing thread synchronization just like ` Atomics.wait ` , but with
367+ the big difference that it will return a ` Promise ` that will resolve when the
368+ thread gets notified with the ` Atomics.notify ` method, instead of blocking until
369+ that happens.
370+
371+ ``` javascript
372+ // Given an `Int32Array` shared between two threads:
373+
374+ const sab = new SharedArrayBuffer (1024 );
375+ const int32 = new Int32Array (sab);
376+
377+ // Thread 1 runs the following:
378+ // { async: true, value: Promise {<pending>} }
379+ const result = Atomics .waitAsync (int32, 0 , 0 , 1000 );
380+ result .value .then (() => console .log (" waited!" ));
381+
382+ // And thread 2 runs the following after Thread 1:
383+ Atomics .notify (int32, 0 );
384+
385+ // Then, in thread 1 we will (eventually) see "waited!" printed.
386+ ```
387+
388+ Note that this built-in requires having a "proper" implementation of a ` JobExecutor ` ; again, "proper"
389+ in the sense of "not blocking the whole thread waiting on a future to finish", which can be accomplished
390+ with [ ` FutureGroup ` ] and [ ` futures_lite::poll_once ` ] [ poll_once ] if an async executor is not required
391+ (see [ ` SimpleJobExecutor ` 's implementation] [ sje-impl ] ).
392+ This is because it heavily relies on ` TimeoutJob ` and ` NativeAsyncJob ` to timeout if a notification
393+ doesn't arrive and communicate with the notifier threads, respectively. This is the reason why
394+ we don't recommend just blocking on each received ` Future ` ; that could cause
395+ ` TimeoutJob ` s to run much later than required, or even make it so that they don't
396+ run at all!
397+
398+ [ poll_once ] : https://docs.rs/futures-lite/latest/futures_lite/future/fn.poll_once.html
399+ [ `FutureGroup` ] : https://docs.rs/futures-concurrency/latest/futures_concurrency/future/future_group/struct.FutureGroup.html
400+ [ sje-impl ] : https://github.com/boa-dev/boa/blob/0468498b4bb9da31caa20123201e4d8ee132c608/core/engine/src/job.rs#L678
401+
186402#### Set methods
187403
188404This release adds support for the new set methods added in ECMAScript's 2025
@@ -234,12 +450,34 @@ let sum = Math.sumPrecise([1e20, 0.1, -1e20]);
234450console .log (sum); // 0.1
235451```
236452
237- #### Atomics.waitAsync
453+ #### Array.fromAsync
238454
239- TODO
455+ This release adds support for ` Array.fromAsync ` , which will be introduced in
456+ ECMAScript's 2026 specification.
240457
458+ ` Array.fromAsync ` allows to conveniently create a array from an async iterable by
459+ awaiting all of the items consecutively.
241460
242- #### Array.fromAsync
461+ ``` javascript
462+ // Array.fromAsync is roughly equivalent to:
463+ async function toArray (asyncIterator ){
464+ const arr = [];
465+ for await (const i of asyncIterator) arr .push (i);
466+ return arr;
467+ }
468+
469+ async function * asyncIterable () {
470+ for (let i = 0 ; i < 5 ; i++ ) {
471+ await new Promise ((resolve ) => setTimeout (resolve, 10 * i));
472+ yield i;
473+ }
474+ };
475+
476+ Array .fromAsync (asyncIterable ()).then ((array ) => console .log (array));
477+ // [0, 1, 2, 3, 4]
478+ toArray (asyncIterable ()).then ((array ) => console .log (array));
479+ // [0, 1, 2, 3, 4]
480+ ```
243481
244482## Boa Runtime
245483
0 commit comments