Skip to content

Commit 6cc8660

Browse files
committed
one left!
1 parent 07ea1cf commit 6cc8660

File tree

10 files changed

+858
-165
lines changed

10 files changed

+858
-165
lines changed

src/Asynkron.JsEngine/Ast/AwaitExpressionExtensions.cs

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using Asynkron.JsEngine.Execution;
2+
using Asynkron.JsEngine.JsTypes;
23

34
namespace Asynkron.JsEngine.Ast;
45

@@ -26,15 +27,42 @@ public static partial class TypedAstEvaluator
2627
return awaited;
2728
}
2829

29-
// For top-level await (module level, no generator), use synchronous blocking.
30-
// This ensures promises are resolved before continuing.
31-
if (!AwaitScheduler.TryAwaitPromiseSync(awaited, context, out var resolved))
30+
// Always await asynchronously: wrap non-promises with Promise.resolve and drive through scheduler.
31+
if (awaited is not JsObject jsObj || !AwaitScheduler.IsPromiseLike(jsObj))
3232
{
33-
// TryAwaitPromiseSync returns false if there was a rejection that set context.IsThrow
34-
return resolved;
33+
var promiseCtor = context.RealmState.PromiseConstructor;
34+
JsObject? wrappedPromise = null;
35+
36+
if (promiseCtor is IJsPropertyAccessor accessor &&
37+
accessor.TryGetProperty("resolve", out var resolveValue) &&
38+
resolveValue is IJsCallable resolveCallable &&
39+
resolveCallable.Invoke([awaited], promiseCtor as object) is JsObject resolvedPromise)
40+
{
41+
wrappedPromise = resolvedPromise;
42+
}
43+
44+
if (wrappedPromise is null)
45+
{
46+
// Fallback: create a resolved promise in the current realm.
47+
var engine = context.RealmState.Engine;
48+
var promise = engine?.CreateRealmPromise();
49+
promise?.Resolve(awaited);
50+
wrappedPromise = promise?.JsObject;
51+
}
52+
53+
awaited = wrappedPromise ?? awaited;
54+
}
55+
56+
if (!AwaitScheduler.TryAwaitPromiseSync(
57+
awaited,
58+
context,
59+
out var resolvedValue,
60+
context.DrainAwaitMicrotasks))
61+
{
62+
return resolvedValue;
3563
}
3664

37-
return resolved;
65+
return resolvedValue;
3866
}
3967

4068
private Symbol? GetAwaitStateKey()

src/Asynkron.JsEngine/Ast/JsObjectExtensions.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,11 @@ private void IteratorClose(EvaluationContext context, bool preserveExistingThrow
175175

176176
if (IsPromiseLike(returnObject))
177177
{
178-
AwaitScheduler.TryAwaitPromiseSync(returnObject, context, out _);
178+
AwaitScheduler.TryAwaitPromiseSync(
179+
returnObject,
180+
context,
181+
out _,
182+
context.DrainAwaitMicrotasks);
179183
}
180184
}
181185
catch (ThrowSignal)

src/Asynkron.JsEngine/Ast/ProgramNodeExtensions.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ public static partial class TypedAstEvaluator
1818
ExecutionKind executionKind = ExecutionKind.Script,
1919
bool createStrictEnvironment = true,
2020
Symbol? functionNameHint = null,
21-
ImmutableArray<PrivateNameScope>? inheritedPrivateNameScopes = null)
21+
ImmutableArray<PrivateNameScope>? inheritedPrivateNameScopes = null,
22+
bool drainAwaitMicrotasks = true)
2223
{
2324
var context = realmState.CreateContext(
2425
ScopeKind.Program,
@@ -27,6 +28,7 @@ public static partial class TypedAstEvaluator
2728
cancellationToken,
2829
executionKind,
2930
false);
31+
context.DrainAwaitMicrotasks = drainAwaitMicrotasks;
3032
if (inheritedPrivateNameScopes is { IsDefault: false, Length: > 0 } scopes)
3133
{
3234
context.EnterPrivateNameScopes(scopes);

src/Asynkron.JsEngine/Ast/TypedAstEvaluator.DelegatedYieldState.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,11 @@ public static DelegatedYieldState FromEnumerable(IEnumerable<object?> enumerable
157157
if (nextCandidate is IJsObjectLike promiseCandidate && IsPromiseLike(promiseCandidate))
158158
{
159159
awaitedPromise = true;
160-
if (!AwaitScheduler.TryAwaitPromiseSync(promiseCandidate, context, out awaitedCandidate))
160+
if (!AwaitScheduler.TryAwaitPromiseSync(
161+
promiseCandidate,
162+
context,
163+
out awaitedCandidate,
164+
context.DrainAwaitMicrotasks))
161165
{
162166
return (Symbol.Undefined, true, true, propagateThrow, null);
163167
}

src/Asynkron.JsEngine/Ast/TypedAstEvaluator.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,11 @@ private static object NormalizeIterableTarget(object? value, EvaluationContext c
177177
// pipeline is in place.
178178
private static bool TryAwaitPromise(object? candidate, EvaluationContext context, out object? resolvedValue)
179179
{
180-
return AwaitScheduler.TryAwaitPromiseSync(candidate, context, out resolvedValue);
180+
return AwaitScheduler.TryAwaitPromiseSync(
181+
candidate,
182+
context,
183+
out resolvedValue,
184+
context.DrainAwaitMicrotasks);
181185
}
182186

183187

src/Asynkron.JsEngine/EvaluationContext.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,13 @@ public sealed class EvaluationContext(
142142
/// </summary>
143143
public bool IsThrow => CurrentSignal is ThrowFlowSignal;
144144

145+
/// <summary>
146+
/// Controls whether await expressions should synchronously drain the microtask queue
147+
/// to resolve promises. For non-blocking top-level await we defer draining until
148+
/// the host resumes the job.
149+
/// </summary>
150+
public bool DrainAwaitMicrotasks { get; set; } = true;
151+
145152
/// <summary>
146153
/// Returns true if the current signal is Yield.
147154
/// </summary>

src/Asynkron.JsEngine/Execution/AwaitScheduler.cs

Lines changed: 39 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using System.Threading;
12
using Asynkron.JsEngine.Ast;
23
using Asynkron.JsEngine.JsTypes;
34

@@ -17,15 +18,22 @@ public static bool IsPromiseLike(object? candidate)
1718
thenValue is IJsCallable;
1819
}
1920

20-
public static bool TryAwaitPromiseSync(object? candidate, EvaluationContext context, out object? resolvedValue)
21+
public static bool TryAwaitPromiseSync(
22+
object? candidate,
23+
EvaluationContext context,
24+
out object? resolvedValue,
25+
bool drainMicrotasks = true)
2126
{
2227
resolvedValue = candidate;
2328

24-
// Drain any pending microtasks first - this may resolve the promise we're about to await
2529
var engine = context.RealmState?.Engine;
26-
engine?.DrainMicrotasks();
30+
if (drainMicrotasks)
31+
{
32+
engine?.DrainMicrotasks();
33+
}
2734

28-
if (candidate is JsPromise jsPromise &&
35+
if (drainMicrotasks &&
36+
candidate is JsPromise jsPromise &&
2937
jsPromise.TryGetSettled(out var settledValue, out var isRejected))
3038
{
3139
if (isRejected)
@@ -41,7 +49,8 @@ public static bool TryAwaitPromiseSync(object? candidate, EvaluationContext cont
4149

4250
while (resolvedValue is JsObject promiseObj && IsPromiseLike(promiseObj))
4351
{
44-
if (promiseObj.TryGetProperty("__promise__", out var internalPromise) &&
52+
if (drainMicrotasks &&
53+
promiseObj.TryGetProperty("__promise__", out var internalPromise) &&
4554
internalPromise is JsPromise promise &&
4655
promise.TryGetSettled(out var settled, out var rejected))
4756
{
@@ -56,7 +65,8 @@ internalPromise is JsPromise promise &&
5665
continue;
5766
}
5867

59-
if (!promiseObj.TryGetProperty("then", out var thenValue) || thenValue is not IJsCallable thenCallable)
68+
if (!promiseObj.TryGetProperty("then", out var thenValue) ||
69+
thenValue is not IJsCallable thenCallable)
6070
{
6171
break;
6272
}
@@ -99,32 +109,33 @@ internalPromise is JsPromise promise &&
99109
(bool Success, object? Value) awaited;
100110
try
101111
{
102-
// Drain microtasks until the promise settles
103-
var maxIterations = 10000; // Prevent infinite loops
104-
var iterations = 0;
105-
while (!tcs.Task.IsCompleted && iterations++ < maxIterations)
112+
if (drainMicrotasks)
106113
{
107-
engine?.DrainMicrotasks();
108-
109-
// If still not completed after draining, we have an async promise
110-
// that requires the event loop - this shouldn't happen for proper
111-
// top-level await scenarios
112-
if (!tcs.Task.IsCompleted)
114+
var iterations = 0;
115+
while (!tcs.Task.IsCompleted)
113116
{
114-
// Try one more drain in case new microtasks were queued
115117
engine?.DrainMicrotasks();
116-
}
117-
}
118118

119-
if (tcs.Task.IsCompleted)
120-
{
121-
awaited = tcs.Task.GetAwaiter().GetResult();
122-
}
123-
else
124-
{
125-
throw new InvalidOperationException(
126-
"Promise did not resolve after draining microtasks. This may indicate an infinite promise chain or external async dependency.");
119+
if (tcs.Task.IsCompleted)
120+
{
121+
break;
122+
}
123+
124+
if (engine is not null)
125+
{
126+
engine.StartEventLoop();
127+
engine.DrainEventLoopAsync(CancellationToken.None).GetAwaiter().GetResult();
128+
engine.DrainMicrotasks();
129+
}
130+
131+
if (++iterations > 10_000)
132+
{
133+
throw new InvalidOperationException(
134+
"Promise did not resolve after draining microtasks and the event loop.");
135+
}
136+
}
127137
}
138+
awaited = tcs.Task.GetAwaiter().GetResult();
128139
}
129140
catch (InvalidOperationException)
130141
{
@@ -164,7 +175,7 @@ public static bool TryAwaitPromiseOrSchedule(object? candidate, bool asyncStepMo
164175
// existing blocking semantics.
165176
if (!asyncStepMode)
166177
{
167-
return TryAwaitPromiseSync(candidate, context, out resolvedValue);
178+
return TryAwaitPromiseSync(candidate, context, out resolvedValue, context.DrainAwaitMicrotasks);
168179
}
169180

170181
// Async-aware mode: if this is a promise-like object, surface it as

0 commit comments

Comments
 (0)