Skip to content

Proxy and Reflect

Roger Johansson edited this page Jan 14, 2026 · 1 revision

Proxy and Reflect

How the metaprogramming Proxy and Reflect APIs work in Asynkron.JsEngine.


Overview

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
Loading

JsProxy Structure

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;
}

Trap Implementation Pattern

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);
}

All 13 Traps

Property Access

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)

Property Definition

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)

Prototype

Trap Triggered By Arguments
getPrototypeOf Object.getPrototypeOf(proxy) (target)
setPrototypeOf Object.setPrototypeOf(proxy, proto) (target, prototype)

Extensibility

Trap Triggered By Arguments
isExtensible Object.isExtensible(proxy) (target)
preventExtensions Object.preventExtensions(proxy) (target)

Function Proxies

Trap Triggered By Arguments
apply proxy(...), proxy.call(...) (target, thisArg, argumentsList)
construct new proxy(...) (target, argumentsList, newTarget)

Trap Implementations

get Trap

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;
}

set Trap

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");
    }
}

ownKeys Trap

if (TryGetTrap("ownKeys", out var trap))
{
    var trapResult = trap.Invoke(new SingleValueArgs(_targetJsValue), _handlerJsValue);
    keys = ExtractKeys(trapResult);
}
else
{
    keys = Target.GetOwnPropertyKeysInOrder(includeSymbols, includeNonEnumerable);
}

Revocable Proxies

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;
}

Revocation Check

Every trap checks for revocation:

_ = Handler ?? throw StandardLibrary.ThrowTypeError(
    "Cannot perform operation on a revoked Proxy", realm: _realm);

Private Fields on Proxies

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)) { ... }
}

Private Brand Checking

public void AddPrivateBrand(object brand)
{
    _privateBrands.Add(brand);
}

public bool HasPrivateBrand(object brand)
{
    return _privateBrands.Contains(brand);
}

Reflect API

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

Reflect.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);
}

Reflect.apply

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);
}

Property Key Encoding

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;
}

Usage Examples

Logging Proxy

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"

Validation Proxy

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

See Also

Clone this wiki locally