|
21 | 21 | use openworkers_core::{Event, HttpMethod, HttpRequest, RequestBody, Script}; |
22 | 22 | use openworkers_runtime_v8::Worker; |
23 | 23 | use std::collections::HashMap; |
24 | | -use std::time::Duration; |
| 24 | +use std::time::{Duration, Instant}; |
25 | 25 |
|
26 | 26 | /// Helper to run async tests in a LocalSet (required for tokio spawn_local) |
27 | 27 | async fn run_local<F, Fut, T>(f: F) -> T |
@@ -580,3 +580,76 @@ async fn test_export_as_default_bundler_pattern_transformed() { |
580 | 580 | }) |
581 | 581 | .await; |
582 | 582 | } |
| 583 | + |
| 584 | +// ============================================================================= |
| 585 | +// waitUntil tests |
| 586 | +// ============================================================================= |
| 587 | + |
| 588 | +/// waitUntil should NOT block the response or exec() completion. |
| 589 | +/// |
| 590 | +/// The handler sends a response immediately, then calls waitUntil with a 200ms |
| 591 | +/// delayed promise. exec() should return as soon as the response is ready, |
| 592 | +/// NOT after the waitUntil promise resolves. |
| 593 | +/// |
| 594 | +/// Verify that the HTTP response is sent to the client immediately, |
| 595 | +/// WITHOUT waiting for waitUntil promises to resolve. |
| 596 | +/// |
| 597 | +/// The worker's waitUntil schedules a 200ms timer. The response should |
| 598 | +/// arrive well before that. exec() itself will block (Worker pumps V8 |
| 599 | +/// microtasks via FullyComplete), but from the HTTP client's perspective, |
| 600 | +/// the response is not delayed by waitUntil — that's the important invariant. |
| 601 | +#[tokio::test] |
| 602 | +async fn test_wait_until_does_not_block_response() { |
| 603 | + run_local(|| async { |
| 604 | + let script = Script::new( |
| 605 | + r#" |
| 606 | + addEventListener('fetch', (event) => { |
| 607 | + event.respondWith(new Response('ok')); |
| 608 | + event.waitUntil(new Promise(resolve => setTimeout(resolve, 200))); |
| 609 | + }); |
| 610 | + "#, |
| 611 | + ); |
| 612 | + |
| 613 | + let mut worker = Worker::new(script, None) |
| 614 | + .await |
| 615 | + .expect("Worker should initialize"); |
| 616 | + |
| 617 | + let (task, rx) = Event::fetch(make_request()); |
| 618 | + |
| 619 | + let start = Instant::now(); |
| 620 | + |
| 621 | + // Run exec in background — it will block for ~200ms (Worker uses FullyComplete |
| 622 | + // to pump V8 microtasks), but the response is sent before that. |
| 623 | + let exec_handle = tokio::task::spawn_local(async move { worker.exec(task).await }); |
| 624 | + |
| 625 | + // Response should arrive quickly (before waitUntil's 200ms timer) |
| 626 | + let response = tokio::time::timeout(Duration::from_millis(100), rx) |
| 627 | + .await |
| 628 | + .expect("Response should arrive before waitUntil completes") |
| 629 | + .expect("Should get response"); |
| 630 | + |
| 631 | + let response_time = start.elapsed(); |
| 632 | + assert_eq!(response.status, 200); |
| 633 | + |
| 634 | + // The response must arrive well before the 200ms waitUntil timer |
| 635 | + assert!( |
| 636 | + response_time < Duration::from_millis(100), |
| 637 | + "Response should arrive before waitUntil (200ms), took {:?}", |
| 638 | + response_time |
| 639 | + ); |
| 640 | + |
| 641 | + // exec() will block until waitUntil completes (Worker uses FullyComplete) |
| 642 | + // This is expected — Worker is oneshot, so pumping V8 is fine. |
| 643 | + // For warm reuse (ExecutionContext), StreamsComplete returns earlier. |
| 644 | + let exec_result = exec_handle.await.unwrap(); |
| 645 | + exec_result.expect("exec should succeed"); |
| 646 | + |
| 647 | + let exec_time = start.elapsed(); |
| 648 | + |
| 649 | + eprintln!( |
| 650 | + "Response arrived in {:?}, exec completed in {:?}", |
| 651 | + response_time, exec_time |
| 652 | + ); |
| 653 | + }) |
| 654 | + .await; |
| 655 | +} |
0 commit comments