Skip to content

Commit 1214356

Browse files
committed
Simplify Array.fromAsync to handle only array-like objects
- Remove iterator/iterable support (will be added after Iterator PR merges) - Replace LambdaFunction with BaseFunction for Context safety - Remove AsyncIteratorProcessor (not needed without iterator support) - Keep AsyncArrayLikeProcessor for array-like processing - Avoids dependency on IteratorLikeIterable which has Context capture issues This simplified version ensures Array.fromAsync can be merged independently without waiting for the Iterator constructor PR (mozilla#2078) to be fixed.
1 parent 890fa94 commit 1214356

File tree

1 file changed

+165
-69
lines changed

1 file changed

+165
-69
lines changed

rhino/src/main/java/org/mozilla/javascript/NativeArray.java

Lines changed: 165 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -749,12 +749,22 @@ private static Object js_from(Context cx, Scriptable scope, Scriptable thisObj,
749749
return result;
750750
}
751751

752+
/**
753+
* ES2024 Array.fromAsync implementation.
754+
* Context-safe: Uses BaseFunction instead of LambdaFunction to avoid Context capture.
755+
*
756+
* @param cx Current context (never stored)
757+
* @param scope Current scope
758+
* @param thisObj Constructor this (Array constructor)
759+
* @param args Arguments [items, mapfn, thisArg]
760+
* @return Promise that resolves to the created array
761+
*/
752762
private static Object js_fromAsync(
753763
Context cx, Scriptable scope, Scriptable thisObj, Object[] args) {
754764

755-
Object items = args.length >= 1 ? args[0] : Undefined.instance;
756-
Object mapfn = args.length >= 2 ? args[1] : Undefined.instance;
757-
Object thisArg = args.length >= 3 ? args[2] : Undefined.instance;
765+
final Object items = args.length >= 1 ? args[0] : Undefined.instance;
766+
final Object mapfn = args.length >= 2 ? args[1] : Undefined.instance;
767+
final Object thisArg = args.length >= 3 ? args[2] : Undefined.instance;
758768

759769
// Get Promise constructor
760770
Object promiseCtor = ScriptableObject.getProperty(scope, "Promise");
@@ -763,62 +773,62 @@ private static Object js_fromAsync(
763773
}
764774
Function promiseFunc = (Function) promiseCtor;
765775

766-
// Create promise executor
767-
Callable executor =
768-
new LambdaFunction(
769-
scope,
770-
2,
771-
(Context lcx,
772-
Scriptable lscope,
773-
Scriptable lthisObj,
774-
Object[] executorArgs) -> {
775-
if (executorArgs.length < 2) {
776-
return Undefined.instance;
777-
}
776+
// Create promise executor using BaseFunction to avoid Context capture
777+
Function executor = new BaseFunction() {
778+
@Override
779+
public Object call(Context lcx, Scriptable lscope, Scriptable lthisObj, Object[] executorArgs) {
780+
// lcx is FRESH Context from Promise - never stored
781+
if (executorArgs.length < 2) {
782+
return Undefined.instance;
783+
}
778784

779-
Function resolveFunc = (Function) executorArgs[0];
780-
Function rejectFunc = (Function) executorArgs[1];
785+
Function resolveFunc = (Function) executorArgs[0];
786+
Function rejectFunc = (Function) executorArgs[1];
781787

782-
try {
783-
// Validate mapfn if provided
784-
Function mapFunction = null;
785-
Scriptable mapThis = null;
788+
try {
789+
// Validate mapfn if provided
790+
Function mapFunction = null;
791+
Scriptable mapThis = null;
786792

787-
if (!Undefined.isUndefined(mapfn)) {
788-
if (!(mapfn instanceof Function)) {
789-
throw ScriptRuntime.typeErrorById("msg.map.function.not");
790-
}
791-
mapFunction = (Function) mapfn;
792-
mapThis =
793-
ScriptRuntime.getApplyOrCallThis(
794-
lcx, lscope, thisArg, 1, mapFunction);
795-
}
793+
if (!Undefined.isUndefined(mapfn)) {
794+
if (!(mapfn instanceof Function)) {
795+
throw ScriptRuntime.typeErrorById("msg.map.function.not");
796+
}
797+
mapFunction = (Function) mapfn;
798+
mapThis = ScriptRuntime.getApplyOrCallThis(
799+
lcx, lscope, thisArg, 1, mapFunction);
800+
}
796801

797-
// Process items
798-
processAsyncItems(
799-
lcx,
800-
lscope,
801-
thisObj,
802-
items,
803-
mapFunction,
804-
mapThis,
805-
resolveFunc,
806-
rejectFunc);
807-
808-
} catch (RhinoException re) {
809-
rejectFunc.call(
810-
lcx,
811-
lscope,
812-
null,
813-
new Object[] {getErrorObject(lcx, lscope, re)});
814-
}
802+
// Process items with fresh Context
803+
processAsyncItems(
804+
lcx,
805+
lscope,
806+
thisObj,
807+
items,
808+
mapFunction,
809+
mapThis,
810+
resolveFunc,
811+
rejectFunc);
812+
813+
} catch (RhinoException re) {
814+
rejectFunc.call(
815+
lcx,
816+
lscope,
817+
null,
818+
new Object[] {getErrorObject(lcx, lscope, re)});
819+
}
815820

816-
return Undefined.instance;
817-
});
821+
return Undefined.instance;
822+
}
823+
};
818824

819825
return promiseFunc.construct(cx, scope, new Object[] {executor});
820826
}
821827

828+
/**
829+
* Process items for Array.fromAsync - Context-safe implementation.
830+
* Simplified version that only handles array-like objects (no iterator support yet).
831+
*/
822832
private static void processAsyncItems(
823833
Context cx,
824834
Scriptable scope,
@@ -830,33 +840,23 @@ private static void processAsyncItems(
830840
Function reject) {
831841

832842
try {
843+
// Convert to object
833844
Scriptable itemsObj = ScriptRuntime.toObject(scope, items);
834845

835-
// Check for iterator
836-
Object iteratorProp = ScriptableObject.getProperty(itemsObj, SymbolKey.ITERATOR);
837-
838-
if (iteratorProp != Scriptable.NOT_FOUND && !Undefined.isUndefined(iteratorProp)) {
839-
// Iterable path
840-
Object iterator = ScriptRuntime.callIterator(itemsObj, cx, scope);
841-
if (!Undefined.isUndefined(iterator)) {
842-
Scriptable A = callConstructorOrCreateArray(cx, scope, thisObj, 0, false);
843-
IteratorLikeIterable iter = new IteratorLikeIterable(cx, scope, iterator);
844-
845-
processAsyncIterable(cx, scope, iter, A, 0, mapFn, mapThis, resolve, reject);
846-
return;
847-
}
848-
}
849-
850-
// Array-like path
846+
// Get length and create result array
851847
long len = getLengthProperty(cx, itemsObj);
852-
Scriptable A = callConstructorOrCreateArray(cx, scope, thisObj, len, true);
848+
Scriptable result = callConstructorOrCreateArray(cx, scope, thisObj, len, true);
853849

854850
if (len == 0) {
855-
resolve.call(cx, scope, null, new Object[] {A});
851+
// Empty array - resolve immediately
852+
resolve.call(cx, scope, null, new Object[] {result});
856853
return;
857854
}
858855

859-
processAsyncArrayLike(cx, scope, itemsObj, A, 0, len, mapFn, mapThis, resolve, reject);
856+
// Process array-like object with Context-safe async processor
857+
AsyncArrayLikeProcessor processor = new AsyncArrayLikeProcessor(
858+
result, itemsObj, len, mapFn, mapThis, resolve, reject);
859+
processor.processNext(cx, scope, 0);
860860

861861
} catch (RhinoException re) {
862862
reject.call(cx, scope, null, new Object[] {getErrorObject(cx, scope, re)});
@@ -1063,6 +1063,102 @@ private static Object getErrorObject(Context cx, Scriptable scope, RhinoExceptio
10631063
return re;
10641064
}
10651065

1066+
/**
1067+
* Context-safe async array-like processor.
1068+
* Never stores Context - receives fresh Context on each call.
1069+
*/
1070+
private static class AsyncArrayLikeProcessor {
1071+
private final Scriptable result;
1072+
private final Scriptable items;
1073+
private final long length;
1074+
private final Function mapFn;
1075+
private final Scriptable mapThis;
1076+
private final Function resolve;
1077+
private final Function reject;
1078+
1079+
AsyncArrayLikeProcessor(
1080+
Scriptable result,
1081+
Scriptable items,
1082+
long length,
1083+
Function mapFn,
1084+
Scriptable mapThis,
1085+
Function resolve,
1086+
Function reject) {
1087+
this.result = result;
1088+
this.items = items;
1089+
this.length = length;
1090+
this.mapFn = mapFn;
1091+
this.mapThis = mapThis;
1092+
this.resolve = resolve;
1093+
this.reject = reject;
1094+
}
1095+
1096+
void processNext(Context cx, Scriptable scope, final long index) {
1097+
if (index >= length) {
1098+
setLengthProperty(cx, result, length);
1099+
resolve.call(cx, scope, null, new Object[] {result});
1100+
return;
1101+
}
1102+
1103+
try {
1104+
Object value = getElem(cx, items, index);
1105+
1106+
// Apply mapping function if provided
1107+
if (mapFn != null) {
1108+
value = mapFn.call(cx, scope, mapThis, new Object[] {value, Long.valueOf(index)});
1109+
}
1110+
1111+
// Check if value is a promise/thenable
1112+
if (isThenable(value)) {
1113+
// Handle promise resolution with Context-safe callbacks
1114+
Function onFulfilled = new BaseFunction() {
1115+
@Override
1116+
public Object call(Context lcx, Scriptable lscope, Scriptable thisObj, Object[] args) {
1117+
// lcx is FRESH Context when promise resolves
1118+
Object resolvedValue = args.length > 0 ? args[0] : Undefined.instance;
1119+
ArrayLikeAbstractOperations.defineElem(lcx, result, index, resolvedValue);
1120+
processNext(lcx, lscope, index + 1);
1121+
return Undefined.instance;
1122+
}
1123+
};
1124+
1125+
Function onRejected = new BaseFunction() {
1126+
@Override
1127+
public Object call(Context lcx, Scriptable lscope, Scriptable thisObj, Object[] args) {
1128+
// lcx is FRESH Context when promise rejects
1129+
Object error = args.length > 0 ? args[0] : Undefined.instance;
1130+
reject.call(lcx, lscope, null, new Object[] {error});
1131+
return Undefined.instance;
1132+
}
1133+
};
1134+
1135+
// Attach callbacks to promise
1136+
Object thenMethod = ScriptableObject.getProperty((Scriptable) value, "then");
1137+
if (thenMethod instanceof Function) {
1138+
((Function) thenMethod).call(cx, scope, (Scriptable) value,
1139+
new Object[] {onFulfilled, onRejected});
1140+
}
1141+
} else {
1142+
// Synchronous value - store and continue
1143+
ArrayLikeAbstractOperations.defineElem(cx, result, index, value);
1144+
processNext(cx, scope, index + 1);
1145+
}
1146+
1147+
} catch (Exception e) {
1148+
reject.call(cx, scope, null, new Object[] {e});
1149+
}
1150+
}
1151+
}
1152+
1153+
// Helper method to check if value is thenable
1154+
private static boolean isThenable(Object value) {
1155+
if (!(value instanceof Scriptable)) {
1156+
return false;
1157+
}
1158+
Object then = ScriptableObject.getProperty((Scriptable) value, "then");
1159+
return then instanceof Function;
1160+
}
1161+
10661162
private static Object js_of(Context cx, Scriptable scope, Scriptable thisObj, Object[] args) {
10671163
final Scriptable result =
10681164
callConstructorOrCreateArray(cx, scope, thisObj, args.length, true);

0 commit comments

Comments
 (0)