zdc-runtime 0.1.0

Embeds the ZDeceptron JavaScript runtime and evaluates it without an external toolchain.
Documentation
use zdc_runtime::{Sandbox, RPC_JS, SIGNAL_JS, STORE_JS, WIRE_JS};

fn without_imports(source: &str) -> String {
    source
        .lines()
        .filter(|line| !line.trim_start().starts_with("import "))
        .collect::<Vec<_>>()
        .join("\n")
}

fn rpc() -> Sandbox {
    let mut sandbox = Sandbox::new();
    sandbox.load(SIGNAL_JS).expect("signal module loads");
    sandbox.load(WIRE_JS).expect("wire module loads");
    sandbox
        .load(&without_imports(RPC_JS))
        .expect("RPC module loads");
    sandbox
}

fn store() -> Sandbox {
    let mut sandbox = rpc();
    sandbox
        .load("const decodeValue = decode;")
        .expect("wire import alias installs");
    sandbox
        .load(&without_imports(STORE_JS))
        .expect("store module loads");
    sandbox
}

#[test]
fn endpoint_urls_encode_names_as_one_path_segment() {
    let mut sandbox = rpc();
    assert_eq!(
        sandbox.text("endpointUrl('visits.incr')").unwrap(),
        "/_zd/visits.incr"
    );
    assert_eq!(
        sandbox.text("endpointUrl('a/b c')").unwrap(),
        "/_zd/a%2Fb%20c"
    );
    assert_eq!(
        sandbox.text("endpointUrl('café')").unwrap(),
        "/_zd/caf%C3%A9"
    );
}

#[test]
fn the_atomic_endpoint_name_cannot_collide_with_a_language_identifier() {
    let mut sandbox = rpc();
    assert_eq!(sandbox.text("ATOMIC").unwrap(), "~atomic");
    assert_eq!(sandbox.text("endpointUrl(ATOMIC)").unwrap(), "/_zd/~atomic");
}

#[test]
fn calls_preserve_endpoint_argument_order_and_atomic_batch_shape() {
    let mut sandbox = rpc();
    sandbox
        .load(
            "const seen = []; setTransport((name, args) => { seen.push([name, args]); return null; });",
        )
        .unwrap();
    sandbox
        .load("call('single', 1, 'two'); atomic([]); atomic([['first', [3]], ['second', [4, 5]]]);")
        .unwrap();

    assert_eq!(
        sandbox.text("JSON.stringify(seen)").unwrap(),
        r#"[["single",[1,"two"]],["~atomic",[["first",[3]],["second",[4,5]]]]]"#
    );
}

#[test]
fn absent_atomic_batches_do_not_make_transport_requests() {
    let mut sandbox = rpc();
    sandbox
        .load("let calls = 0; setTransport(() => { calls += 1; });")
        .unwrap();
    sandbox.load("atomic(); atomic(null); atomic([]);").unwrap();

    assert_eq!(sandbox.text("calls").unwrap(), "0");
}

#[test]
fn custom_failure_sinks_receive_the_original_error() {
    let mut sandbox = rpc();
    sandbox
        .load("let reported = null; setFailureSink(error => { reported = error; });")
        .unwrap();
    sandbox
        .load("const originalFailure = new Error('write failed'); reportFailure(originalFailure);")
        .unwrap();

    assert_eq!(
        sandbox.text("reported === originalFailure").unwrap(),
        "true"
    );
    assert_eq!(sandbox.text("reported.message").unwrap(), "write failed");
}

#[test]
fn a_fresh_store_has_no_watched_keys_or_bound_updates() {
    let mut sandbox = store();
    assert_eq!(sandbox.text("JSON.stringify(watchedKeys())").unwrap(), "[]");
    assert_eq!(sandbox.text("applyUpdate('missing', 1)").unwrap(), "false");
    assert_eq!(
        sandbox.text("typeof subscribe({keys: []})").unwrap(),
        "function"
    );
}

#[test]
fn live_sync_receive_advances_only_numeric_cursors() {
    let mut sandbox = store();
    assert_eq!(
        sandbox
            .text("receive({event: 'ready', seq: 8}, 3)")
            .unwrap(),
        "8"
    );
    assert_eq!(
        sandbox
            .text("receive({event: 'newer', seq: '9'}, 3)")
            .unwrap(),
        "3"
    );
    assert_eq!(
        sandbox
            .text("receive({event: 'update', key: 'none', value: 1}, 4)")
            .unwrap(),
        "4"
    );
    assert_eq!(sandbox.text("receive({event: 'resync'}, 5)").unwrap(), "5");
}

#[test]
fn stream_frames_decode_payloads_cursors_and_nested_maps() {
    let mut sandbox = store();
    let expression = r#"
        (() => {
          const frame = decodeFrame(
            'update',
            '{"key":"cards","value":{"$map":[["ada",1]]}}',
            '12'
          );
          return frame.event === 'update' && frame.seq === 12 && frame.key === 'cards' &&
            frame.value instanceof Map && frame.value.get('ada') === 1;
        })()
    "#;
    assert_eq!(sandbox.text(expression).unwrap(), "true");
}

#[test]
fn malformed_stream_frames_are_safe_empty_events() {
    let mut sandbox = store();
    assert_eq!(
        sandbox
            .text(
                "(() => { const f = decodeFrame('ready', '{bad', 'not-a-number'); return f.event === 'ready' && f.seq === undefined && f.key === undefined && f.value === null; })()",
            )
            .unwrap(),
        "true"
    );
}