-
Notifications
You must be signed in to change notification settings - Fork 1
Proxy and Reflect
Roger Johansson edited this page Jan 14, 2026
·
1 revision
How the metaprogramming Proxy and Reflect APIs work in Asynkron.JsEngine.
flowchart TB
subgraph ProxyOps["Proxy Operation"]
Op["obj.property"]
Handler{Has trap?}
Trap["Call trap"]
Target["Forward to target"]
end
subgraph Traps["13 Trap Handlers"]
Get["get"]
Set["set"]
Has["has"]
Delete["deleteProperty"]
Define["defineProperty"]
GetDesc["getOwnPropertyDescriptor"]
OwnKeys["ownKeys"]
GetProto["getPrototypeOf"]
SetProto["setPrototypeOf"]
Apply["apply"]
Construct["construct"]
IsExt["isExtensible"]
Prevent["preventExtensions"]
end
Op --> Handler
Handler -->|Yes| Trap
Handler -->|No| Target
Trap --> Result((Result))
Target --> Result
File: JsTypes/JsProxy.cs
public sealed class JsProxy : IJsObjectLike, IJsCallable, IPrivateBrandHolder, IAsJsValue
{
// The underlying object being proxied
public IJsObjectLike Target { get; }
// Handler object containing trap methods (null if revoked)
public IJsObjectLike? Handler { get; set; }
// Metadata object for prototype chain
private readonly JsObject _meta = new();
// Separate storage for private fields
private readonly JsObject _privateStorage = new();
// Private brand tracking for ES2022 private fields
private readonly HashSet<object> _privateBrands = new();
// Cached JsValue wrappers
private readonly JsValue _cachedJsValue;
private readonly JsValue _targetJsValue;
private JsValue _handlerJsValue;
}Each proxy trap follows a consistent pattern:
public bool TryGetProperty(string name, JsValue receiver, out JsValue value)
{
// 1. Handle private slots separately (bypass proxy)
if (name.IsPrivateSlotName())
{
return _privateStorage.TryGetProperty(name, receiver, out value);
}
// 2. Check for trap in handler
if (TryGetTrap("get", out var trap))
{
// 3. Call trap with (target, property, receiver)
var args = new[] { _targetJsValue, DecodePropertyKey(name), receiver };
value = trap.Invoke(args, _handlerJsValue);
return true;
}
// 4. Fall back to target
return Target.TryGetProperty(name, receiver, out value);
}| Trap | Triggered By | Arguments |
|---|---|---|
get |
proxy.prop, proxy[key]
|
(target, property, receiver) |
set |
proxy.prop = val |
(target, property, value, receiver) |
has |
'prop' in proxy |
(target, property) |
deleteProperty |
delete proxy.prop |
(target, property) |
| Trap | Triggered By | Arguments |
|---|---|---|
defineProperty |
Object.defineProperty(proxy, ...) |
(target, property, descriptor) |
getOwnPropertyDescriptor |
Object.getOwnPropertyDescriptor(proxy, ...) |
(target, property) |
ownKeys |
Object.keys(proxy), Reflect.ownKeys(proxy)
|
(target) |
| Trap | Triggered By | Arguments |
|---|---|---|
getPrototypeOf |
Object.getPrototypeOf(proxy) |
(target) |
setPrototypeOf |
Object.setPrototypeOf(proxy, proto) |
(target, prototype) |
| Trap | Triggered By | Arguments |
|---|---|---|
isExtensible |
Object.isExtensible(proxy) |
(target) |
preventExtensions |
Object.preventExtensions(proxy) |
(target) |
| Trap | Triggered By | Arguments |
|---|---|---|
apply |
proxy(...), proxy.call(...)
|
(target, thisArg, argumentsList) |
construct |
new proxy(...) |
(target, argumentsList, newTarget) |
if (TryGetTrap("get", out var trap))
{
var args = new[]
{
_targetJsValue,
JsValue.FromObjectUnsafe(DecodePropertyKey(name)),
receiver.IsUndefined ? JsValue.FromJsProxy(this) : receiver
};
value = trap.Invoke(args, _handlerJsValue);
return true;
}if (TryGetTrap("set", out var trap))
{
var args = new[]
{
_targetJsValue,
JsValue.FromObjectUnsafe(DecodePropertyKey(name)),
value,
receiver
};
var result = trap.Invoke(args, _handlerJsValue);
// Trap must return truthy value
if (!JsOps.ToBoolean(result))
{
throw StandardLibrary.ThrowTypeError("Proxy 'set' trap returned a falsy value");
}
}if (TryGetTrap("ownKeys", out var trap))
{
var trapResult = trap.Invoke(new SingleValueArgs(_targetJsValue), _handlerJsValue);
keys = ExtractKeys(trapResult);
}
else
{
keys = Target.GetOwnPropertyKeysInOrder(includeSymbols, includeNonEnumerable);
}const { proxy, revoke } = Proxy.revocable(target, handler);
proxy.foo; // Works
revoke(); // Revoke the proxy
proxy.foo; // TypeError: Cannot perform operation on a revoked Proxy// In ProxyConstructor
public static JsValue Revocable(JsValue _, IReadOnlyList<JsValue> args, RealmState? realm)
{
var target = args.GetArgument(0).AsObject<IJsObjectLike>();
var handler = args.GetArgument(1).AsObject<IJsObjectLike>();
var proxy = new JsProxy(target, handler, realm);
// Create revoke function that sets handler to null
var revoke = new HostFunction(_ =>
{
proxy.Handler = null; // Revoke!
return JsValue.Undefined;
});
var result = new JsObject();
result.SetProperty("proxy", (JsValue)proxy);
result.SetProperty("revoke", (JsValue)revoke);
return (JsValue)result;
}Every trap checks for revocation:
_ = Handler ?? throw StandardLibrary.ThrowTypeError(
"Cannot perform operation on a revoked Proxy", realm: _realm);Private fields bypass the proxy and go to _privateStorage:
public void SetProperty(string name, JsValue value, JsValue receiver)
{
// Private slots bypass proxy traps
if (name.IsPrivateSlotName())
{
_privateStorage.SetProperty(name, value, receiver);
return;
}
// Normal property - use traps
if (TryGetTrap("set", out var trap)) { ... }
}public void AddPrivateBrand(object brand)
{
_privateBrands.Add(brand);
}
public bool HasPrivateBrand(object brand)
{
return _privateBrands.Contains(brand);
}File: StdLib/Reflect/ReflectHelper.cs
The Reflect object provides methods that correspond to proxy traps:
| Reflect Method | Proxy Trap |
|---|---|
Reflect.get(target, prop, receiver) |
get |
Reflect.set(target, prop, value, receiver) |
set |
Reflect.has(target, prop) |
has |
Reflect.deleteProperty(target, prop) |
deleteProperty |
Reflect.defineProperty(target, prop, desc) |
defineProperty |
Reflect.getOwnPropertyDescriptor(target, prop) |
getOwnPropertyDescriptor |
Reflect.ownKeys(target) |
ownKeys |
Reflect.getPrototypeOf(target) |
getPrototypeOf |
Reflect.setPrototypeOf(target, proto) |
setPrototypeOf |
Reflect.isExtensible(target) |
isExtensible |
Reflect.preventExtensions(target) |
preventExtensions |
Reflect.apply(target, thisArg, args) |
apply |
Reflect.construct(target, args, newTarget) |
construct |
internal static JsValue ReflectConstruct(JsValue _, IReadOnlyList<JsValue> args, RealmState? realm)
{
var target = args[0].AsObject<IJsCallable>();
var argList = args[1].AsObject<JsArray>()?.Items.ToArray() ?? [];
// Optional newTarget for subclassing
IJsCallable newTarget = args.Count > 2
? args[2].AsObject<IJsCallable>()
: target;
return Construct(target, argList, newTarget, realm);
}internal static JsValue ReflectApply(JsValue _, IReadOnlyList<JsValue> args)
{
var callable = args[0].AsObject<IJsCallable>();
var thisArg = args.Count > 1 ? args[1] : JsValue.Undefined;
var argList = args.Count > 2 && args[2].TryGetObject<JsArray>(out var arr)
? arr.Items.ToArray()
: [];
return callable.Invoke(argList, thisArg);
}Symbol-keyed properties use a special encoding:
private static object? DecodePropertyKey(string name)
{
// Symbol keys are encoded as "@@symbol:{id}"
if (JsSymbol.TryGetByInternalKey(name, out var symbol))
{
return symbol;
}
return name;
}const handler = {
get(target, prop, receiver) {
console.log(`Getting ${prop}`);
return Reflect.get(target, prop, receiver);
},
set(target, prop, value, receiver) {
console.log(`Setting ${prop} = ${value}`);
return Reflect.set(target, prop, value, receiver);
}
};
const obj = { x: 1 };
const proxy = new Proxy(obj, handler);
proxy.x; // Logs: "Getting x"
proxy.y = 2; // Logs: "Setting y = 2"const validator = {
set(target, prop, value) {
if (prop === 'age' && typeof value !== 'number') {
throw new TypeError('Age must be a number');
}
return Reflect.set(target, prop, value);
}
};
const person = new Proxy({}, validator);
person.age = 25; // OK
person.age = "25"; // TypeError- JsObject & Properties - Property descriptor handling
- Symbol System - Symbol key encoding
- Standard Library Architecture - Constructor patterns