rong_jscore 0.3.1

JavaScriptCore backend for RongJS
use crate::{JSCContext, JSCValue};
use rong_core::{
    FromJSValue, JSContext, JSContextImpl, JSFunc, JSObject, JSProxyOps, JSTypeOf, JSValue,
    JSValueImpl, RongJSError, Source,
};

#[derive(Clone)]
struct ProxyHelper(JSCValue);

const PROXY_HELPER_SOURCE: &[u8] = br#"
(() => {
    if (globalThis.__rongProxyHelper__ !== undefined) {
        return globalThis.__rongProxyHelper__;
    }

    const NativeProxy = globalThis.Proxy;
    const meta = new WeakMap();

    function track(proxy, target, handler) {
        meta.set(proxy, { target, handler });
        return proxy;
    }

    function TrackedProxy(target, handler) {
        if (new.target === undefined) {
            throw new TypeError("Constructor Proxy requires 'new'");
        }
        return track(new NativeProxy(target, handler), target, handler);
    }

    Object.setPrototypeOf(TrackedProxy, NativeProxy);
    TrackedProxy.prototype = NativeProxy.prototype;
    TrackedProxy.revocable = function revocable(target, handler) {
        const revocable = NativeProxy.revocable(target, handler);
        track(revocable.proxy, target, handler);
        return revocable;
    };
    globalThis.Proxy = TrackedProxy;

    const helper = {
        create(target, handler) {
            return track(new NativeProxy(target, handler), target, handler);
        },
        isProxy(value) {
            return value !== null &&
                (typeof value === "object" || typeof value === "function") &&
                meta.has(value);
        },
        target(value) {
            if (value === null || (typeof value !== "object" && typeof value !== "function") || !meta.has(value)) {
                throw new TypeError("Not JS Proxy");
            }
            return meta.get(value).target;
        }
    };

    Object.defineProperty(globalThis, "__rongProxyHelper__", {
        value: helper,
        configurable: false,
        enumerable: false,
        writable: false,
    });

    return helper;
})()
"#;

pub(crate) fn prime_proxy_helper(ctx: &JSCContext) -> JSCValue {
    ctx.eval(Source::from_bytes(PROXY_HELPER_SOURCE))
}

pub(crate) fn install_proxy_helper(ctx: &JSCContext) -> JSCValue {
    let host_ctx = JSContext::<JSCContext>::from_borrowed_raw_ptr(ctx.as_raw());
    if let Some(helper) = host_ctx.get_state::<ProxyHelper>() {
        return helper.0.clone();
    }

    let helper = prime_proxy_helper(ctx);
    if !helper.is_exception() {
        host_ctx.set_state(ProxyHelper(helper.clone()));
    }
    helper
}

fn get_proxy_helper(ctx: &JSCContext) -> JSCValue {
    install_proxy_helper(ctx)
}

fn thrown_or_undefined(ctx: &JSContext<JSCContext>, err: RongJSError) -> JSCValue {
    err.thrown_value(ctx)
        .unwrap_or_else(|| JSValue::undefined(ctx))
        .into_value()
}

fn helper_method(ctx: &JSCContext, name: &str) -> Result<JSFunc<JSCValue>, JSCValue> {
    let host_ctx = JSContext::<JSCContext>::from_borrowed_raw_ptr(ctx.as_raw());
    let helper = get_proxy_helper(ctx);
    if helper.is_exception() {
        return Err(helper);
    }

    let helper_obj = JSObject::from_js_value(&host_ctx, JSValue::from_raw(&host_ctx, helper))
        .map_err(|err: RongJSError| thrown_or_undefined(&host_ctx, err))?;

    helper_obj
        .get::<_, JSFunc<JSCValue>>(name)
        .map_err(|err: RongJSError| thrown_or_undefined(&host_ctx, err))
}

pub(crate) fn is_proxy(value: &JSCValue) -> bool {
    let ctx = JSCContext::from_borrowed_raw(*value.as_raw_context());
    let Ok(is_proxy) = helper_method(&ctx, "isProxy") else {
        return false;
    };
    is_proxy
        .call::<_, bool>(
            None,
            (JSValue::from_raw(
                &JSContext::from_borrowed_raw_ptr(ctx.as_raw()),
                value.clone(),
            ),),
        )
        .unwrap_or(false)
}

impl JSProxyOps for JSCValue {
    fn new_proxy(ctx: &Self::Context, target: Self, handler: Self) -> Result<Self, Self> {
        let create = helper_method(ctx, "create")?;
        let host_ctx = JSContext::<JSCContext>::from_borrowed_raw_ptr(ctx.as_raw());
        create
            .call::<_, JSValue<JSCValue>>(
                None,
                (
                    JSValue::from_raw(&host_ctx, target),
                    JSValue::from_raw(&host_ctx, handler),
                ),
            )
            .map(JSValue::into_value)
            .map_err(|err: RongJSError| thrown_or_undefined(&host_ctx, err))
    }

    fn proxy_target(&self) -> Result<Self, Self> {
        let ctx = JSCContext::from_borrowed_raw(*self.as_raw_context());
        let target = helper_method(&ctx, "target")?;
        let host_ctx = JSContext::<JSCContext>::from_borrowed_raw_ptr(ctx.as_raw());
        target
            .call::<_, JSValue<JSCValue>>(None, (JSValue::from_raw(&host_ctx, self.clone()),))
            .map(JSValue::into_value)
            .map_err(|err: RongJSError| thrown_or_undefined(&host_ctx, err))
    }
}