declare class ShadowRealm {
constructor();
evaluate(sourceText: string): PrimitiveValueOrCallable;
importValue(specifier: string, bindingName: string): Promise<PrimitiveValueOrCallable>;
}
The ShadowRealm#evaluate method is most analogous to indirect eval though PerformEval, diverging at where the source is evaluated and what can be returned, ie. primitive values or callable objects. Other non-primitive values would throw a TypeError in the incubator realm.
When ShadowRealm#evaluate
results in callable objects - generally functions - it creates a new wrapped function in the incubator realm that chains to the inner ShadowRealm's function when called.
This wrapped function can also only receive primitive values or callable objects as arguments. If the incubator realm calls the wrapped function with another function as argument, the chained function in the child realm will receive a wrapped function of the given argument.
The wrapped functions are frozen and do not share any identity cross realms with the function they chain onto. They must be connected through the realm instance as weakrefs enabling eventual garbage collection.
const red = new ShadowRealm();
const redNumber = red.evaluate('var x = 42; x;');
redNumber; // yields 42
const redFunction = red.evaluate('(function(value) { return value * 2; })');
redFunction(21); // yields 42
A good analogy here is a cross realm bound function, which is a function in a realm that is available in the incubator realm, this function's job is to call another function, this time, a function inside the realm, that might or might not return a completion value.
This mechanism allows the incubator realm to define logic inside the realm without relying on populating the global object with global names just for the purpose of communication between the two realms.
To avoid identity discontinuity, the evaluation cannot transfer objects. When evaluation completes with a non-callable object, the incubator realm throws a TypeError.
const red = new ShadowRealm();
try {
red.evaluate('[]');
} catch(err) {
assert(err instanceof TypeError);
}
Callable values resolved in the evaluation are auto wrapped.
const red = new ShadowRealm();
const blueFunction = (x, y) => x + y;
const redFunction = red.evaluate(`
0, function(redFunctionArg, a, b, c) {
return redFunctionArg(a, b) * c;
}
`);
redFunction(blueFunction, 2, 3, 4); // yields 20
let myValue;
const red = new ShadowRealm();
function blueFunction(x) {
globalThis.myValue = x;
};
// cb is a new function in the red ShadowRealm that chains the call to the blueFunction
const redFunction = red.evaluate(`
0, function(cb) {
globalThis.myValue = "red";
cb(42);
return globalThis.myValue;
}
`);
redFunction(blueFunction); // yields the string 'red'
myValue === 42; // true
The wrapped function throws a TypeError if it returns a non-callable object.
const red = new ShadowRealm();
const redFunction = red.evaluate(`
function() {
return {};
}
`);
try {
redFunction();
} catch(err) {
assert(err instanceof TypeError);
}
Errors are wrapped into a TypeError while traveling from one realm to another.
const red = new ShadowRealm();
try {
red.evaluate('throw "foo"');
} catch(err) {
assert(err.constructor === TypeError);
}
try {
red.evaluate('throw new Error()');
} catch(err) {
assert(err.constructor === TypeError);
}
This also applies to errors caused in the wrapped functions.
const red = new ShadowRealm();
class CustomError extends Error {};
function blueFunction(x) {
throw new CustomError('meep');
};
const redFunction = red.evaluate(`
0, function(cb) {
try {
cb();
} catch (err) {
// The error is a TypeError wrapping the abrupt completion
// CustomError from the blueFunction call
err.constructor === TypeError; // true
throw 'foo';
}
}
`);
try {
redFunction(blueFunction);
} catch(err) {
// The error is a TypeError wrapping the abrupt completion 'foo' from the redFunction call.
err.constructor === TypeError // true
}
The wrapped functions are frozen and they share no properties from the other realm.
const red = new ShadowRealm();
function blueFunction() {
return 42;
}
blueFunction.x = 'noop';
const redFunction = red.evaluate(`
0, function(cb) {
Object.isFrozen(cb); // true
cb(); // yields 42;
cb.x; // undefined
Object.prototype.hasOwnProperty.call(cb, 'x'); // false
return 1;
}
`);
assert(Object.isFrozen(redFunction));
redFunction(blueFunction); // yields 1
The autowrapping creates a new regular function within the other realm that chains a call to the given function. The new function inherits from the realm's %Function%
.
const red = new ShadowRealm();
const redFunction = red.evaluate(`
0, function(cb) {
return cb instanceof Function &&
Object.getPrototypeOf(cb) === Function.prototype;
}
`);
function blueFunction() {}
redFunction(blueFunction); // true
All the API checks if the given value is callable. It does not run extra magic wrapping other functions.
While this part still works, there is no automatic wrapping proposed here for returned promises or iterators. Sending async functions, classes, generators, and async generators are not useful.
const red = new ShadowRealm();
const redFunction = red.evaluate(`
0, function(cb) {
return cb instanceof Function &&
Object.getPrototypeOf(cb) === Function.prototype;
}
`);
redFunction(async function() {}); // true
const red = new ShadowRealm();
const redFunction = red.evaluate(`
0, function(cb) {
return cb();
}
`);
// Throws a TypeError, cb() returned a Promise, which is a non primitive, non callable value.
redFunction(async function() {});
Addressing callable objects allows chaining to Proxy wrapped functions.
const red = new ShadowRealm();
const redFunction = red.evaluate(`
new Proxy(function fn() {}, {
call(...) { ... }
});
`);
The same auto wrapping happens for callable values returned from a realm chain.
const red = new ShadowRealm();
const redFunction = red.evaluate(`
0, function() {
globalThis.redValue = 42;
return function() {
return globalThis.redValue;
};
}
`);
const wrapped = redFunction();
globalThis.redValue = 'fake';
wrapped(); // yields 42
As the API only provides a losely connecting function, so this
is not exposed and new.target
cannot be transfered to the other realm.
const red = new ShadowRealm();
const redFunction = red.evaluate(`
0, function(cb) {
// .call only applies to the wrapped function created in this realm
// The chain will only transfer the arguments
return cb.call({x: 'poison!'}, 2);
}
`);
function blueFunction(arg) {
return this.x * arg;
}
globalThis.x = 21;
redFunction(blueFunction); // yields 42
importValue
connectorThe Realm#importValue
can be used to inject modules using the dynamic import
expression within the created ShadowRealm. This module returns a promise that is resolved when the import is resolved within the ShadowRealm. This promise will be resolved with a matching value of the given binding name.
const r = new ShadowRealm();
const promise = r.importValue('./my-module.js', 'foo');
const res = await promise;
// res === <the foo binding for ./my-module.js>
The resolved value can be a primitive (Symbols included), or a wrapped function. Other non-primitive values would reject the promise in the incubator ShadowRealm.
// ./module.js
export const fooNumber = 42;
export const timesTwo = (x) => x * 2;
export default function(x, y) { return x * y; }
export const nono = {};
// incubator ShadowRealm script
const r = new ShadowRealm();
const specifier = './my-module.js';
// As the module is resolved within the child ShadowRealm r, we can just reuse
// importValue
const [ fooN, timesTwo, myWrappedFn ] = await Promise.all(
r.importValue(specifier, 'fooNumber'),
r.importValue(specifier, 'timesTwo'),
r.importValue(specifier, 'default'),
);
fooN; // 42
typeof timesTwo; // 'function'
// timesTwo is just a wrapped function that chains to the original timesTwo
// inside the child ShadowRealm r
timesTwo instanceof Function; // true
Object.getPrototypeOf(timesTwo) === Function.prototype; // true
A TypeError is thrown if the binding has a non-primitive, non-callable value.
try {
await r.importValue(specifier, 'nono'); // Throws TypeError
} catch(err) {
err instanceof TypeError; // No identity discontinuity
}
There's no dynamic mapping to the primitive values from the imported names. The wrapped function defers a call to the imported function in the child realm.
importValue
auto wrappingconst red = new ShadowRealm();
const wrappedRedFn = await red.importValue('./specifier.js', 'injectedFunction');
The received wrappedRedFn
is a Blue Function. When called, it triggers a call to the Red Function captured from the module import.
assert(wrappedRedFn instanceof Function);
assert.sameValue(Object.getPrototypeOf(wrappedRedFn), Function.prototype);
The injected module namespace and function is not leaked within the Red ShadowRealm, but can observe things only from the Red ShadowRealm.
// specifier.js:
export function injectedFunction(x) {
return `${globalThis.someValue}, ${x}!`;
};
const red = new ShadowRealm();
const wrappedRedFn = await red.importValue('./specifier.js', 'injectedFunction');
// sets a global someValue in the red ShadowRealm
red.evaluate('globalThis.someValue = "Hello"');
// and a global someValue in the parent realm
globalThis.someValue = 'Olá';
wrappedRedFn('World'); // yields to 'Hello, World!'