use anyhow::Result;
use rusty_v8 as v8;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Arc, Mutex};
use std::time::{SystemTime, UNIX_EPOCH};
static SYNC_TAGS: std::sync::OnceLock<Arc<Mutex<Vec<String>>>> = std::sync::OnceLock::new();
static PENDING_WAIT_UNTIL: AtomicUsize = AtomicUsize::new(0);
fn get_sync_tags() -> &'static Arc<Mutex<Vec<String>>> {
SYNC_TAGS.get_or_init(|| Arc::new(Mutex::new(Vec::new())))
}
pub fn has_pending_wait_until() -> bool {
PENDING_WAIT_UNTIL.load(Ordering::SeqCst) > 0
}
pub fn reset_pending_wait_until() {
PENDING_WAIT_UNTIL.store(0, Ordering::SeqCst);
}
pub fn setup_background_sync_api(
scope: &mut v8::ContextScope<v8::HandleScope>,
context: &v8::Local<v8::Context>,
) -> Result<()> {
let global = context.global(scope);
setup_sync_event(scope, context, global)?;
setup_sync_manager(scope, context, global)?;
Ok(())
}
fn setup_sync_event(
scope: &mut v8::ContextScope<v8::HandleScope>,
_context: &v8::Local<v8::Context>,
global: v8::Local<v8::Object>,
) -> Result<()> {
let sync_event_fn = v8::FunctionTemplate::new(scope, sync_event_constructor_callback);
let sync_event_constructor = sync_event_fn.get_function(scope).unwrap();
let sync_event_key = v8::String::new(scope, "SyncEvent").unwrap();
global.set(scope, sync_event_key.into(), sync_event_constructor.into());
Ok(())
}
fn setup_sync_manager(
scope: &mut v8::ContextScope<v8::HandleScope>,
_context: &v8::Local<v8::Context>,
global: v8::Local<v8::Object>,
) -> Result<()> {
let sync_manager = v8::Object::new(scope);
let register_fn = v8::FunctionTemplate::new(scope, sync_manager_register_callback);
let register_key = v8::String::new(scope, "register").unwrap();
let register_func = register_fn.get_function(scope).unwrap();
sync_manager.set(scope, register_key.into(), register_func.into());
let get_tags_fn = v8::FunctionTemplate::new(scope, sync_manager_get_tags_callback);
let get_tags_key = v8::String::new(scope, "getTags").unwrap();
let get_tags_func = get_tags_fn.get_function(scope).unwrap();
sync_manager.set(scope, get_tags_key.into(), get_tags_func.into());
let registration_key = v8::String::new(scope, "registration").unwrap();
let registration = v8::Object::new(scope);
let sync_key = v8::String::new(scope, "sync").unwrap();
registration.set(scope, sync_key.into(), sync_manager.into());
global.set(scope, registration_key.into(), registration.into());
Ok(())
}
fn sync_event_constructor_callback(
scope: &mut v8::PinScope,
args: v8::FunctionCallbackArguments,
mut rv: v8::ReturnValue,
) {
let event_obj = v8::Object::new(scope);
let event_type = if args.length() > 0 {
args.get(0)
.to_string(scope)
.unwrap_or_else(|| v8::String::new(scope, "sync").unwrap())
.to_rust_string_lossy(scope)
} else {
"sync".to_string()
};
let tag_value = if args.length() > 1 {
let options = args.get(1);
if let Some(options_obj) = options.to_object(scope) {
let tag_key = v8::String::new(scope, "tag").unwrap();
if let Some(tag) = options_obj.get(scope, tag_key.into()) {
tag.to_string(scope)
.unwrap_or_else(|| v8::String::new(scope, "").unwrap())
.to_rust_string_lossy(scope)
} else {
String::from("default-sync")
}
} else {
String::from("default-sync")
}
} else {
String::from("default-sync")
};
let last_chance_value = if args.length() > 1 {
let options = args.get(1);
if let Some(options_obj) = options.to_object(scope) {
let lc_key = v8::String::new(scope, "lastChance").unwrap();
if let Some(lc) = options_obj.get(scope, lc_key.into()) {
lc.to_boolean(scope).is_true()
} else {
false
}
} else {
false
}
} else {
false
};
let type_key = v8::String::new(scope, "type").unwrap();
let type_val = v8::String::new(scope, &event_type).unwrap();
event_obj.set(scope, type_key.into(), type_val.into());
let internal_type_key = v8::String::new(scope, "_type").unwrap();
event_obj.set(scope, internal_type_key.into(), type_val.into());
let tag_key = v8::String::new(scope, "tag").unwrap();
let tag_val = v8::String::new(scope, &tag_value).unwrap();
event_obj.set(scope, tag_key.into(), tag_val.into());
let last_chance_key = v8::String::new(scope, "lastChance").unwrap();
let last_chance_val = v8::Boolean::new(scope, last_chance_value);
event_obj.set(scope, last_chance_key.into(), last_chance_val.into());
let bubbles_key = v8::String::new(scope, "bubbles").unwrap();
let bubbles_val = v8::Boolean::new(scope, false);
event_obj.set(scope, bubbles_key.into(), bubbles_val.into());
let cancelable_key = v8::String::new(scope, "cancelable").unwrap();
let cancelable_val = v8::Boolean::new(scope, true);
event_obj.set(scope, cancelable_key.into(), cancelable_val.into());
let time_stamp_key = v8::String::new(scope, "timeStamp").unwrap();
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_millis() as f64;
let time_stamp_val = v8::Number::new(scope, now);
event_obj.set(scope, time_stamp_key.into(), time_stamp_val.into());
let wait_until_fn = v8::FunctionTemplate::new(scope, sync_event_wait_until_callback);
let wait_until_func = wait_until_fn.get_function(scope).unwrap();
let wait_until_key = v8::String::new(scope, "waitUntil").unwrap();
event_obj.set(scope, wait_until_key.into(), wait_until_func.into());
rv.set(event_obj.into());
}
fn sync_event_wait_until_callback(
scope: &mut v8::PinScope,
args: v8::FunctionCallbackArguments,
mut rv: v8::ReturnValue,
) {
if args.length() > 0 {
let promise = args.get(0);
let resolver = v8::PromiseResolver::new(scope).unwrap();
let undefined: v8::Local<v8::Value> = v8::undefined(scope).into();
if promise.is_promise() {
PENDING_WAIT_UNTIL.fetch_add(1, Ordering::SeqCst);
let done_func = v8::FunctionTemplate::new(scope, sync_event_wait_until_done_callback)
.get_function(scope)
.unwrap();
let then_key = v8::String::new(scope, "then").unwrap();
let mut attached_handler = false;
if let Ok(promise_obj) = v8::Local::<v8::Object>::try_from(promise) {
if let Some(then_value) = promise_obj.get(scope, then_key.into()) {
if let Ok(then_func) = v8::Local::<v8::Function>::try_from(then_value) {
let done_value: v8::Local<v8::Value> = done_func.into();
let args = [done_value, done_value];
if then_func.call(scope, promise, &args).is_some() {
attached_handler = true;
}
}
}
}
if !attached_handler {
decrement_pending_wait_until();
}
rv.set(promise);
} else {
resolver.resolve(scope, undefined).unwrap();
rv.set(resolver.into());
}
} else {
let error = v8::String::new(scope, "waitUntil requires a promise").unwrap();
let exception = v8::Exception::type_error(scope, error);
scope.throw_exception(exception);
}
}
fn sync_event_wait_until_done_callback(
_scope: &mut v8::PinScope,
_args: v8::FunctionCallbackArguments,
_rv: v8::ReturnValue,
) {
decrement_pending_wait_until();
}
fn decrement_pending_wait_until() {
let _ = PENDING_WAIT_UNTIL.fetch_update(Ordering::SeqCst, Ordering::SeqCst, |count| {
Some(count.saturating_sub(1))
});
}
fn sync_manager_register_callback(
scope: &mut v8::PinScope,
args: v8::FunctionCallbackArguments,
mut rv: v8::ReturnValue,
) {
let promise_resolver = v8::PromiseResolver::new(scope).unwrap();
let promise = promise_resolver.get_promise(scope);
rv.set(promise.into());
let message = if args.length() == 0 {
"Background Sync register requires a tag"
} else {
"Background Sync registration is not supported yet"
};
let message = v8::String::new(scope, message).unwrap();
let error = v8::Exception::error(scope, message);
promise_resolver.reject(scope, error).unwrap();
}
fn sync_manager_get_tags_callback(
scope: &mut v8::PinScope,
_args: v8::FunctionCallbackArguments,
mut rv: v8::ReturnValue,
) {
let sync_tags = get_sync_tags();
let tags = sync_tags.lock().unwrap();
let tags_array = v8::Array::new(scope, tags.len() as i32);
for (i, tag) in tags.iter().enumerate() {
let tag_val = v8::String::new(scope, tag).unwrap();
tags_array.set_index(scope, i as u32, tag_val.into());
}
let promise_resolver = v8::PromiseResolver::new(scope).unwrap();
let promise = promise_resolver.get_promise(scope);
rv.set(promise.into());
promise_resolver.resolve(scope, tags_array.into()).unwrap();
}