use std::collections::VecDeque;
use std::ffi::{c_char, CStr, CString};
use std::os::raw::c_longlong;
use std::sync::{Arc, Condvar, Mutex};
use serde_json::{json, Value};
use syncular_client::SyncClient;
use syncular_command::{dispatch, CreateEffects};
pub mod transport;
use transport::HostTransport;
#[derive(Debug, Clone)]
struct Event {
json: Value,
}
pub struct Handle {
client: Option<SyncClient>,
transport: HostTransport,
effects: CreateEffects,
last: ObservedState,
queue: Arc<EventQueue>,
}
#[derive(Debug, Default, Clone)]
struct ObservedState {
sync_needed: bool,
conflicts: usize,
rejections: usize,
schema_floor: Option<Value>,
lease_error: Option<String>,
}
pub(crate) struct EventQueue {
inner: Mutex<VecDeque<Event>>,
ready: Condvar,
}
impl EventQueue {
fn new() -> Self {
EventQueue {
inner: Mutex::new(VecDeque::new()),
ready: Condvar::new(),
}
}
fn push(&self, event: Event) {
let mut guard = self.inner.lock().expect("event queue lock");
guard.push_back(event);
self.ready.notify_one();
}
fn pop(&self, timeout_ms: i64) -> Option<Event> {
let mut guard = self.inner.lock().expect("event queue lock");
if let Some(event) = guard.pop_front() {
return Some(event);
}
if timeout_ms == 0 {
return None;
}
if timeout_ms < 0 {
loop {
guard = self.ready.wait(guard).expect("event queue wait");
if let Some(event) = guard.pop_front() {
return Some(event);
}
}
}
let dur = std::time::Duration::from_millis(timeout_ms as u64);
let (mut guard2, _timeout) = self
.ready
.wait_timeout(guard, dur)
.expect("event queue wait_timeout");
guard2.pop_front()
}
}
impl Handle {
fn new(config: &Value) -> Result<Self, String> {
let queue = Arc::new(EventQueue::new());
let transport = HostTransport::from_config(config, Arc::clone(&queue))?;
Ok(Handle {
client: None,
transport,
effects: CreateEffects::default(),
last: ObservedState::default(),
queue,
})
}
fn command(&mut self, command: &Value) -> Value {
let method = command.get("method").and_then(Value::as_str).unwrap_or("");
let params = command.get("params").cloned().unwrap_or(Value::Null);
let result = dispatch(
&mut self.transport,
&mut self.client,
&mut self.effects,
method,
¶ms,
);
if method == "create" {
self.transport.set_signed_urls(self.effects.signed_urls);
}
self.drain_realtime();
self.derive_events();
match result {
Ok(value) => json!({ "result": value }),
Err((code, message)) => json!({ "error": { "code": code, "message": message } }),
}
}
fn drain_realtime(&mut self) {
let Some(client) = self.client.as_mut() else {
return;
};
for frame in self.transport.take_inbound() {
match frame {
transport::Inbound::Text(text) => {
if is_presence_control(&text) {
self.queue.push(Event {
json: json!({ "type": "presence" }),
});
}
client.on_realtime_text(&text);
}
transport::Inbound::Binary(bytes) => {
client.on_realtime_binary(&mut self.transport, &bytes)
}
}
}
}
fn derive_events(&mut self) {
let Some(client) = self.client.as_ref() else {
return;
};
let now = ObservedState {
sync_needed: client.sync_needed(),
conflicts: client.conflicts().len(),
rejections: client.rejections().len(),
schema_floor: client
.schema_floor()
.map(|f| serde_json::to_value(f).unwrap_or(Value::Null)),
lease_error: client.lease_state().and_then(|l| l.error_code.clone()),
};
if now.sync_needed && !self.last.sync_needed {
self.queue.push(Event {
json: json!({ "type": "sync-needed" }),
});
}
if now.conflicts > self.last.conflicts {
self.queue.push(Event {
json: json!({ "type": "conflict", "count": now.conflicts }),
});
}
if now.rejections > self.last.rejections {
self.queue.push(Event {
json: json!({ "type": "rejection", "count": now.rejections }),
});
}
if now.schema_floor != self.last.schema_floor {
if let Some(floor) = &now.schema_floor {
self.queue.push(Event {
json: json!({ "type": "schema-floor", "floor": floor }),
});
}
}
if now.lease_error != self.last.lease_error {
if let Some(code) = &now.lease_error {
self.queue.push(Event {
json: json!({ "type": "lease", "errorCode": code }),
});
}
}
self.last = now;
}
}
fn is_presence_control(text: &str) -> bool {
serde_json::from_str::<Value>(text)
.ok()
.and_then(|v| {
v.get("event")
.and_then(Value::as_str)
.map(|e| e == "presence")
})
.unwrap_or(false)
}
fn into_c_string(value: String) -> *mut c_char {
match CString::new(value) {
Ok(s) => s.into_raw(),
Err(_) => CString::new("").expect("empty CString").into_raw(),
}
}
fn c_str_to_value(ptr: *const c_char) -> Result<Value, String> {
if ptr.is_null() {
return Err("null pointer".to_owned());
}
let bytes = unsafe { CStr::from_ptr(ptr) };
let text = bytes
.to_str()
.map_err(|_| "config is not UTF-8".to_owned())?;
serde_json::from_str(text).map_err(|e| format!("config is not JSON: {e}"))
}
#[no_mangle]
pub extern "C" fn syncular_client_new(config_json: *const c_char) -> *mut Handle {
let config = match c_str_to_value(config_json) {
Ok(value) => value,
Err(_) => return std::ptr::null_mut(),
};
match Handle::new(&config) {
Ok(handle) => Box::into_raw(Box::new(handle)),
Err(_) => std::ptr::null_mut(),
}
}
#[allow(clippy::not_unsafe_ptr_arg_deref)]
#[no_mangle]
pub extern "C" fn syncular_client_command(
handle: *mut Handle,
command_json: *const c_char,
) -> *mut c_char {
if handle.is_null() {
return std::ptr::null_mut();
}
let handle = unsafe { &mut *handle };
let command = match c_str_to_value(command_json) {
Ok(value) => value,
Err(message) => {
return into_c_string(
json!({ "error": { "code": "client.failed", "message": message } }).to_string(),
)
}
};
let reply = handle.command(&command);
into_c_string(reply.to_string())
}
#[allow(clippy::not_unsafe_ptr_arg_deref)]
#[no_mangle]
pub extern "C" fn syncular_client_poll_event(
handle: *mut Handle,
timeout_ms: c_longlong,
) -> *mut c_char {
if handle.is_null() {
return std::ptr::null_mut();
}
let handle = unsafe { &*handle };
match handle.queue.pop(timeout_ms) {
Some(event) => into_c_string(event.json.to_string()),
None => std::ptr::null_mut(),
}
}
#[allow(clippy::not_unsafe_ptr_arg_deref)]
#[no_mangle]
pub extern "C" fn syncular_client_close(handle: *mut Handle) {
if handle.is_null() {
return;
}
let mut handle = unsafe { Box::from_raw(handle) };
handle.transport.shutdown();
drop(handle);
}
#[allow(clippy::not_unsafe_ptr_arg_deref)]
#[no_mangle]
pub extern "C" fn syncular_free_string(ptr: *mut c_char) {
if ptr.is_null() {
return;
}
unsafe {
let _ = CString::from_raw(ptr);
}
}
#[cfg(test)]
mod tests;
#[cfg(test)]
mod round_tests;