use anyhow::Result;
use rusty_v8 as v8;
pub fn setup_abort_api(
scope: &mut v8::ContextScope<v8::HandleScope>,
context: &v8::Local<v8::Context>,
) -> Result<()> {
let abort_controller_template = v8::FunctionTemplate::new(
scope,
|scope: &mut v8::PinScope,
_args: v8::FunctionCallbackArguments,
mut retval: v8::ReturnValue| {
let signal_key = v8::String::new(scope, "signal").unwrap();
let aborted_key = v8::String::new(scope, "aborted").unwrap();
let listeners_key = v8::String::new(scope, "_abortListeners").unwrap();
let add_event_listener_key = v8::String::new(scope, "addEventListener").unwrap();
let abort_key = v8::String::new(scope, "abort").unwrap();
let controller_obj: v8::Local<v8::Object> = v8::Object::new(scope);
let signal_obj: v8::Local<v8::Object> = v8::Object::new(scope);
let listeners_array: v8::Local<v8::Array> = v8::Array::new(scope, 0);
let false_bool: v8::Local<v8::Value> = v8::Boolean::new(scope, false).into();
signal_obj.set(scope, aborted_key.into(), false_bool);
signal_obj.set(scope, listeners_key.into(), listeners_array.into());
let add_listener_fn = v8::FunctionTemplate::new(
scope,
|_scope: &mut v8::PinScope,
args: v8::FunctionCallbackArguments,
_rv: v8::ReturnValue| {
if args.length() > 1 {
let listener = args.get(1);
let signal = args.this();
let lkey = v8::String::new(_scope, "_abortListeners").unwrap();
let listeners_val = signal.get(_scope, lkey.into());
let listeners_array = if let Some(lv) = listeners_val {
if lv.is_array() {
v8::Local::<v8::Array>::try_from(lv).unwrap()
} else {
v8::Array::new(_scope, 0)
}
} else {
v8::Array::new(_scope, 0)
};
let length = listeners_array.length();
listeners_array.set_index(_scope, length, listener);
signal.set(_scope, lkey.into(), listeners_array.into());
}
},
);
let add_listener_instance = add_listener_fn.get_function(scope).unwrap();
signal_obj.set(
scope,
add_event_listener_key.into(),
add_listener_instance.into(),
);
let abort_fn = v8::FunctionTemplate::new(
scope,
|_scope: &mut v8::PinScope,
args: v8::FunctionCallbackArguments,
_rv: v8::ReturnValue| {
let controller_obj = args.this();
let sig_key = v8::String::new(_scope, "signal").unwrap();
if let Some(signal_val) = controller_obj.get(_scope, sig_key.into()) {
if let Ok(signal_obj) = v8::Local::<v8::Object>::try_from(signal_val) {
let aborted_key = v8::String::new(_scope, "aborted").unwrap();
let true_bool: v8::Local<v8::Value> =
v8::Boolean::new(_scope, true).into();
signal_obj.set(_scope, aborted_key.into(), true_bool);
let reason = if args.length() > 0 && !args.get(0).is_undefined() {
args.get(0)
} else {
let msg =
v8::String::new(_scope, "The operation was aborted").unwrap();
v8::Exception::error(_scope, msg)
};
let reason_key = v8::String::new(_scope, "reason").unwrap();
signal_obj.set(_scope, reason_key.into(), reason);
let list_key = v8::String::new(_scope, "_abortListeners").unwrap();
let event_type_key = v8::String::new(_scope, "type").unwrap();
let abort_type = v8::String::new(_scope, "abort").unwrap();
if let Some(listeners_val) = signal_obj.get(_scope, list_key.into()) {
if let Ok(listeners_array) =
v8::Local::<v8::Array>::try_from(listeners_val)
{
let event_obj = v8::Object::new(_scope);
event_obj.set(_scope, event_type_key.into(), abort_type.into());
for i in 0..listeners_array.length() {
if let Some(listener) = listeners_array.get_index(_scope, i)
{
if listener.is_function() {
let listener_fn =
v8::Local::<v8::Function>::try_from(listener)
.unwrap();
let _ = listener_fn.call(
_scope,
signal_obj.into(),
&[event_obj.into()],
);
}
}
}
}
}
}
}
},
);
let abort_instance = abort_fn.get_function(scope).unwrap();
controller_obj.set(scope, signal_key.into(), signal_obj.into());
controller_obj.set(scope, abort_key.into(), abort_instance.into());
retval.set(controller_obj.into());
},
);
let abort_controller_constructor: v8::Local<v8::Function> =
abort_controller_template.get_function(scope).unwrap();
let abort_signal_obj: v8::Local<v8::Object> = v8::Object::new(scope);
let aborted_key = v8::String::new(scope, "aborted").unwrap();
let false_bool: v8::Local<v8::Value> = v8::Boolean::new(scope, false).into();
abort_signal_obj.set(scope, aborted_key.into(), false_bool);
let global: v8::Local<v8::Object> = context.global(scope);
let abort_controller_key: v8::Local<v8::String> =
v8::String::new(scope, "AbortController").unwrap();
let abort_controller_val: v8::Local<v8::Value> = abort_controller_constructor.into();
global.set(
scope,
abort_controller_key.into(),
abort_controller_val.into(),
);
let abort_signal_key: v8::Local<v8::String> = v8::String::new(scope, "AbortSignal").unwrap();
global.set(scope, abort_signal_key.into(), abort_signal_obj.into());
let helper_js = r#"
(function() {
if (typeof AbortSignal === 'undefined') return;
AbortSignal.timeout = function(ms) {
const controller = new AbortController();
const delay = typeof ms === 'number' ? ms : 0;
const timer = setTimeout(() => {
const err = new Error('The operation was aborted due to timeout');
err.name = 'TimeoutError';
controller.abort(err);
}, delay);
if (timer && typeof timer.unref === 'function') timer.unref();
return controller.signal;
};
AbortSignal.any = function(signals) {
const controller = new AbortController();
const sigList = Array.isArray(signals) ? signals : Array.from(signals || []);
for (const s of sigList) {
if (!s) continue;
if (s.aborted) {
controller.abort(s.reason);
return controller.signal;
}
if (typeof s.addEventListener === 'function') {
s.addEventListener('abort', () => {
controller.abort(s.reason);
});
}
}
return controller.signal;
};
AbortSignal.abort = function(reason) {
const controller = new AbortController();
controller.abort(reason);
return controller.signal;
};
})();
"#;
if let Some(code) = v8::String::new(scope, helper_js) {
if let Some(script) = v8::Script::compile(scope, code, None) {
let _ = script.run(scope);
}
}
Ok(())
}