@@ -66,11 +66,16 @@ biggest pain points of our API. This was mostly caused by how we defined
66
66
` FutureJob ` :
67
67
68
68
``` rust
69
+ pub struct NativeJob {
70
+ f : Box <dyn FnOnce (& mut Context ) -> JsResult <JsValue >>,
71
+ realm : Option <Realm >,
72
+ }
73
+
69
74
pub type FutureJob = Pin <Box <dyn Future <Output = NativeJob > + 'static >>;
70
75
```
71
76
72
77
With 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
74
79
with awaiting ` Future ` s would have to be split into multiple parts:
75
80
76
81
``` rust
@@ -99,8 +104,7 @@ let fetch = async move {
99
104
// `JobQueue`.
100
105
context
101
106
. job_queue ()
102
- . enqueue_future_job (Box :: pin (fetch ), context )
103
- }
107
+ . enqueue_future_job (Box :: pin (fetch ), context );
104
108
```
105
109
106
110
We wanted to improve this API, and the solution we thought about was to make
@@ -173,16 +177,228 @@ pub trait JobExecutor: Any {
173
177
}
174
178
```
175
179
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 ` :
177
181
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.
179
195
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.
181
200
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.
183
359
184
360
### Built-ins updates
185
361
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
+
186
402
#### Set methods
187
403
188
404
This 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]);
234
450
console .log (sum); // 0.1
235
451
```
236
452
237
- #### Atomics.waitAsync
453
+ #### Array.fromAsync
238
454
239
- TODO
455
+ This release adds support for ` Array.fromAsync ` , which will be introduced in
456
+ ECMAScript's 2026 specification.
240
457
458
+ ` Array.fromAsync ` allows to conveniently create a array from an async iterable by
459
+ awaiting all of the items consecutively.
241
460
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
+ ```
243
481
244
482
## Boa Runtime
245
483
0 commit comments