Chandra

 view release on metacpan or  search on metacpan

include/chandra/chandra.h  view on Meta::CPAN

        dSP;
        int count;
        ENTER; SAVETMPS;
        PUSHMARK(SP);
        XPUSHs(ctx->json_encoder);
        XPUSHs(params_json);
        PUTBACK;
        count = call_method("decode", G_SCALAR | G_EVAL);
        SPAGAIN;
        if (count > 0 && !SvTRUE(ERRSV)) {
            SV *decoded = POPs;
            SvREFCNT_dec(params);
            params = newSVsv(decoded);
        }
        PUTBACK;
        FREETMPS; LEAVE;
        if (SvTRUE(ERRSV)) sv_setpvs(ERRSV, "");
    }

    {
        dSP;
        int count;
        ENTER; SAVETMPS;

include/chandra/chandra_store.h  view on Meta::CPAN

    int count;
    HV *result = NULL;
    ENTER; SAVETMPS;
    PUSHMARK(SP);
    XPUSHs(json);
    XPUSHs(json_sv);
    PUTBACK;
    count = call_method("decode", G_SCALAR | G_EVAL);
    SPAGAIN;
    if (count > 0 && !SvTRUE(ERRSV)) {
        SV *decoded = POPs;
        if (SvOK(decoded) && SvROK(decoded)
                && SvTYPE(SvRV(decoded)) == SVt_PVHV) {
            result = (HV *)SvRV(decoded);
            SvREFCNT_inc_simple_void((SV *)result);
        }
    }
    if (SvTRUE(ERRSV)) sv_setpvs(ERRSV, "");
    PUTBACK; FREETMPS; LEAVE;
    if (!result)
        warn("Chandra::Store: corrupt store '%s', starting fresh\n",
             path ? path : "unknown");
    return result;
}

include/chandra/chandra_store.h  view on Meta::CPAN

    flock(fd, LOCK_UN);
    fclose(fh);

    json_sv = sv_2mortal(newSVpvn(buf, (STRLEN)size));
    Safefree(buf);

    new_data = chandra_store_decode(aTHX_ json_sv, path);
    if (new_data) {
        SV **old_svp = hv_fetchs(self_hv, "_data", 0);
        if (old_svp && SvROK(*old_svp)) {
            /* Replace _data with freshly decoded HV */
            (void)hv_stores(self_hv, "_data", newRV_noinc((SV *)new_data));
        } else {
            (void)hv_stores(self_hv, "_data", newRV_noinc((SV *)new_data));
        }
    }
    /* If new_data is NULL, we leave _data as-is (start fresh, already set in new) */
}

/* ============================================================================
 * Constructor helper - build default path from name

lib/Chandra/Socket/Connection.pm  view on Meta::CPAN

=head2 reply

	$conn->reply($original_msg, \%response_data);

Send a response correlated to a request (uses C<_reply_to>).

=head2 recv

	my @messages = $conn->recv;

Non-blocking read. Returns decoded message hashes, or an empty list if
no complete frames are available. Sets C<is_connected> to false on EOF
or error.

=head2 is_connected

	if ($conn->is_connected) { ... }

=head2 name / set_name

	my $name = $conn->name;

lib/Chandra/Socket/Hub.pm  view on Meta::CPAN


=back

=head1 METHODS

=head2 on

	$hub->on($channel => sub { my ($data, $conn) = @_; ... });

Register a handler for messages on C<$channel>. The callback receives
the decoded data hash and the sender's L<Chandra::Socket::Connection>.

=head2 on_connect

	$hub->on_connect(sub { my ($conn) = @_; ... });

Called when a client completes the authenticated handshake.

=head2 on_disconnect

	$hub->on_disconnect(sub { my ($conn) = @_; ... });

t/26_socket.t  view on Meta::CPAN

	my $frame = Chandra::Socket::Connection->encode_frame({ channel => 'test', data => { x => 1 } });
	ok(defined $frame, 'encode_frame returns data');
	my $len = unpack('N', substr($frame, 0, 4));
	is($len, length($frame) - 4, 'length prefix matches payload');
}

# === Frame decoding ===
{
	my $msg = { channel => 'hello', data => { name => 'world' } };
	my $frame = Chandra::Socket::Connection->encode_frame($msg);
	my @decoded = Chandra::Socket::Connection->decode_frames($frame);
	is(scalar @decoded, 1, 'decoded one message');
	is($decoded[0]->{channel}, 'hello', 'channel preserved');
	is($decoded[0]->{data}{name}, 'world', 'data preserved');
}

# === Multiple frames in one buffer ===
{
	my $f1 = Chandra::Socket::Connection->encode_frame({ channel => 'a', data => 1 });
	my $f2 = Chandra::Socket::Connection->encode_frame({ channel => 'b', data => 2 });
	my @decoded = Chandra::Socket::Connection->decode_frames($f1 . $f2);
	is(scalar @decoded, 2, 'decoded two messages from concatenated buffer');
	is($decoded[0]->{channel}, 'a', 'first message channel');
	is($decoded[1]->{channel}, 'b', 'second message channel');
}

# === Partial frame decoded as empty ===
{
	my $frame = Chandra::Socket::Connection->encode_frame({ channel => 'x', data => 1 });
	my $partial = substr($frame, 0, 6);  # incomplete payload
	my @decoded = Chandra::Socket::Connection->decode_frames($partial);
	is(scalar @decoded, 0, 'partial frame returns no messages');
}

# === Connection via socketpair ===
SKIP: {
	skip 'AF_UNIX socketpair not available on Windows', 13 if $is_win32;
	use Socket;
	socketpair(my $s1, my $s2, AF_UNIX, SOCK_STREAM, 0)
		or die "socketpair: $!";
	$s1->blocking(0);
	$s2->blocking(0);

t/34_socket_edge.t  view on Meta::CPAN

    is($msgs[0]->{_id}, 99, 'extra _id field preserved');
    is($msgs[0]->{custom}, 'field', 'custom extra field preserved');

    $conn1->close;
    $conn2->close;
}

# === encode_frame / decode_frames with empty data ===
{
    my $frame = Chandra::Socket::Connection->encode_frame({ channel => 'empty', data => {} });
    my @decoded = Chandra::Socket::Connection->decode_frames($frame);
    is(scalar @decoded, 1, 'empty data decoded');
    is_deeply($decoded[0]->{data}, {}, 'empty data preserved');
}

# === encode_frame / decode_frames with unicode ===
{
    my $frame = Chandra::Socket::Connection->encode_frame({ channel => 'unicode', data => { text => "日本語" } });
    my @decoded = Chandra::Socket::Connection->decode_frames($frame);
    is($decoded[0]->{data}{text}, "日本語", 'unicode preserved through encode/decode');
}

# === decode_frames with empty buffer ===
{
    my @decoded = Chandra::Socket::Connection->decode_frames('');
    is(scalar @decoded, 0, 'empty buffer returns no messages');
}

# === decode_frames with incomplete length header ===
{
    my @decoded = Chandra::Socket::Connection->decode_frames("\x00\x00");
    is(scalar @decoded, 0, 'incomplete header returns no messages');
}

# === Hub send_to nonexistent client returns 0 ===
{
    my $name = "test-send-nonexist-$$";
    my $hub = Chandra::Socket::Hub->new(name => $name);
    my $result = $hub->send_to('ghost', 'test', { data => 1 });
    is($result, 0, 'send_to nonexistent client returns 0');
    $hub->close;
}

t/35_tray.t  view on Meta::CPAN

{
	my $tray = Chandra::Tray->new;
	$tray->add_item('Show' => sub {});
	$tray->add_separator;
	$tray->add_item('Quit' => sub {});

	my $menu_json = $tray->_menu_json;
	ok(defined $menu_json, 'menu_json produced');

	require Cpanel::JSON::XS;
	my $decoded = Cpanel::JSON::XS::decode_json($menu_json);
	is(ref $decoded, 'ARRAY', 'decoded JSON is array');
	is(scalar @$decoded, 3, '3 entries');
	is($decoded->[0]{label}, 'Show', 'first item label');
	ok($decoded->[1]{separator}, 'separator present');
	is($decoded->[2]{label}, 'Quit', 'third item label');
}

# --- show without app returns self ---
{
	my $tray = Chandra::Tray->new;
	my $ret = $tray->show;
	is($ret, $tray, 'show without app returns self');
	is($tray->is_active, 0, 'not active without app');
}

t/36_tray_edge.t  view on Meta::CPAN

	is($tray->items->[0]{checked}, 0, 'checked flag cleared');
}

# --- Disabled and checked in JSON ---
{
	my $tray = Chandra::Tray->new;
	$tray->add_item('D' => sub {});
	$tray->update_item('D', disabled => 1, checked => 1);

	require Cpanel::JSON::XS;
	my $decoded = Cpanel::JSON::XS::decode_json($tray->_menu_json);
	is($decoded->[0]{disabled}, 1, 'disabled in JSON');
	is($decoded->[0]{checked}, 1, 'checked in JSON');
}

# --- Many items ---
{
	my $tray = Chandra::Tray->new;
	$tray->add_item("Item $_" => sub {}) for 1..50;
	is($tray->item_count, 50, '50 items added');
}

# --- Rapid tooltip changes ---

xs/bind.xs  view on Meta::CPAN

    (void)hv_store(reg, nstr, (I32)nlen, SvREFCNT_inc(SvROK(callback) ? SvRV(callback) : callback), 0);
}

SV *
dispatch(self, json_str)
    SV *self
    SV *json_str
CODE:
{
    SV *err = NULL;
    SV *decoded = _bind_json_decode(aTHX_ json_str, &err);

    if (err) {
        /* warn "Chandra::Bind: Failed to parse JSON: $@" */
        warn("Chandra::Bind: Failed to parse JSON: %" SVf, SVfARG(err));
        HV *ret = newHV();
        SV *msg = newSVpvs("Invalid JSON: ");
        sv_catsv(msg, err);
        (void)hv_stores(ret, "error", msg);
        SvREFCNT_dec(err);
        RETVAL = newRV_noinc((SV *)ret);
    }
    else if (!SvROK(decoded) || SvTYPE(SvRV(decoded)) != SVt_PVHV) {
        /* Not a hash - raw */
        HV *ret = newHV();
        (void)hv_stores(ret, "type", newSVpvs("raw"));
        (void)hv_stores(ret, "data", SvREFCNT_inc(json_str));
        SvREFCNT_dec(decoded);
        RETVAL = newRV_noinc((SV *)ret);
    }
    else {
        HV *msg_hv = (HV *)SvRV(decoded);
        SV **type_svp = hv_fetchs(msg_hv, "type", 0);
        const char *type = (type_svp && *type_svp && SvOK(*type_svp))
                           ? SvPV_nolen(*type_svp) : "";

        if (strEQ(type, "call")) {
            /* === _handle_call === */
            SV **id_svp     = hv_fetchs(msg_hv, "id", 0);
            SV **method_svp = hv_fetchs(msg_hv, "method", 0);
            SV **args_svp   = hv_fetchs(msg_hv, "args", 0);

xs/bind.xs  view on Meta::CPAN

                    SvREFCNT_dec((SV *)err_hv);
                }
                else {
                    (void)hv_stores(ret, "result", call_result); /* already inc'd */
                }

                FREETMPS;
                LEAVE;
            }

            SvREFCNT_dec(decoded);
            RETVAL = newRV_noinc((SV *)ret);
        }
        else if (strEQ(type, "event")) {
            /* === _handle_event === */
            SV **handler_id_svp = hv_fetchs(msg_hv, "handler", 0);
            SV **event_data_svp = hv_fetchs(msg_hv, "event", 0);

            const char *handler_id = (handler_id_svp && *handler_id_svp && SvOK(*handler_id_svp))
                                     ? SvPV_nolen(*handler_id_svp) : "";

            HV *reg = _bind_get_registry(aTHX);
            STRLEN hlen = strlen(handler_id);
            SV **handler_svp = hv_fetch(reg, handler_id, (I32)hlen, 0);

            if (!handler_svp || !*handler_svp) {
                warn("Chandra::Bind: Unknown event handler: %s", handler_id);
                HV *ret = newHV();
                (void)hv_stores(ret, "error", newSVpvf("Unknown handler: %s", handler_id));
                SvREFCNT_dec(decoded);
                RETVAL = newRV_noinc((SV *)ret);
            }
            else {
                /* Create Event using direct C function */
                SV *event_data;
                if (event_data_svp && *event_data_svp && SvOK(*event_data_svp)) {
                    event_data = *event_data_svp;
                } else {
                    event_data = sv_2mortal(newRV_noinc((SV *)newHV()));
                }

xs/bind.xs  view on Meta::CPAN

                        SvREFCNT_dec(fmt);

                        HV *ret = newHV();
                        SV **msg_svp = hv_fetchs(err_hv, "message", 0);
                        if (msg_svp && *msg_svp) {
                            (void)hv_stores(ret, "error", SvREFCNT_inc(*msg_svp));
                        }
                        SvREFCNT_dec((SV *)err_hv);

                        SvREFCNT_dec(event_obj);
                        SvREFCNT_dec(decoded);
                        FREETMPS;
                        LEAVE;
                        RETVAL = newRV_noinc((SV *)ret);
                    }
                    else {
                        SvREFCNT_dec(event_obj);
                        SvREFCNT_dec(decoded);
                        FREETMPS;
                        LEAVE;
                        HV *ret = newHV();
                        (void)hv_stores(ret, "ok", newSViv(1));
                        RETVAL = newRV_noinc((SV *)ret);
                    }
                }
            }
        }
        else {
            /* Unknown type - raw */
            HV *ret = newHV();
            (void)hv_stores(ret, "type", newSVpvs("raw"));
            (void)hv_stores(ret, "data", SvREFCNT_inc(json_str));
            SvREFCNT_dec(decoded);
            RETVAL = newRV_noinc((SV *)ret);
        }
    }
}
OUTPUT:
    RETVAL

SV *
encode_result(self, result)
    SV *self

xs/form.xs  view on Meta::CPAN


void
dispatch(self, event_type, json_str)
    SV *self
    SV *event_type
    SV *json_str
CODE:
{
    HV *hv = (HV *)SvRV(self);
    const char *evt = SvPV_nolen(event_type);
    SV *decoded;

    /* Decode JSON */
    {
        dSP;
        int count;
        ENTER;
        SAVETMPS;
        PUSHMARK(SP);
        XPUSHs(json_str);
        PUTBACK;
        count = call_pv("Cpanel::JSON::XS::decode_json", G_SCALAR | G_EVAL);
        SPAGAIN;
        if (SvTRUE(ERRSV) || count < 1) {
            PUTBACK;
            FREETMPS;
            LEAVE;
            return;
        }
        decoded = (count == 1) ? SvREFCNT_inc(POPs) : NULL;
        PUTBACK;
        FREETMPS;
        LEAVE;
    }

    if (!decoded || !SvROK(decoded) || SvTYPE(SvRV(decoded)) != SVt_PVHV) {
        if (decoded) SvREFCNT_dec(decoded);
        return;
    }

    if (strEQ(evt, "_form_submit")) {
        SV **act = hv_fetchs(hv, "_action", 0);
        if (act && SvROK(*act) && SvTYPE(SvRV(*act)) == SVt_PVCV) {
            HV *dhv = (HV *)SvRV(decoded);
            SV **data_svp = hv_fetchs(dhv, "data", 0);
            dSP;
            ENTER;
            SAVETMPS;
            PUSHMARK(SP);
            XPUSHs(data_svp ? *data_svp : &PL_sv_undef);
            PUTBACK;
            call_sv(*act, G_DISCARD);
            FREETMPS;
            LEAVE;
        }
    } else if (strEQ(evt, "_form_change") || strEQ(evt, "_form_input")) {
        HV *dhv = (HV *)SvRV(decoded);
        SV **field_svp = hv_fetchs(dhv, "field", 0);
        SV **value_svp = hv_fetchs(dhv, "value", 0);

        if (field_svp && SvOK(*field_svp)) {
            STRLEN flen;
            const char *fname = SvPV(*field_svp, flen);

            /* Field-specific handler */
            {
                SV **oc_svp = hv_fetchs(hv, "_on_change", 0);

xs/form.xs  view on Meta::CPAN

                    PUTBACK;
                    call_sv(*gcb, G_DISCARD);
                    FREETMPS;
                    LEAVE;
                }
            }
        }
    } else if (strEQ(evt, "_form_values")) {
        SV **cb = hv_fetchs(hv, "_get_values_cb", 0);
        if (cb && SvROK(*cb) && SvTYPE(SvRV(*cb)) == SVt_PVCV) {
            HV *dhv = (HV *)SvRV(decoded);
            SV **data_svp = hv_fetchs(dhv, "data", 0);
            dSP;
            ENTER;
            SAVETMPS;
            PUSHMARK(SP);
            XPUSHs(data_svp ? *data_svp : &PL_sv_undef);
            PUTBACK;
            call_sv(*cb, G_DISCARD);
            FREETMPS;
            LEAVE;
            (void)hv_stores(hv, "_get_values_cb", &PL_sv_undef);
        }
    }

    SvREFCNT_dec(decoded);
}

 # ---- _route_event(event_type, json_str) - class method: routes to correct form ----

void
_route_event(class, event_type, json_str)
    const char *class
    SV *event_type
    SV *json_str
CODE:
{
    SV *decoded;
    PERL_UNUSED_VAR(class);

    /* Decode JSON to extract the form id */
    {
        dSP;
        int count;
        ENTER;
        SAVETMPS;
        PUSHMARK(SP);
        XPUSHs(json_str);
        PUTBACK;
        count = call_pv("Cpanel::JSON::XS::decode_json", G_SCALAR | G_EVAL);
        SPAGAIN;
        if (SvTRUE(ERRSV) || count < 1) {
            PUTBACK;
            FREETMPS;
            LEAVE;
            return;
        }
        decoded = (count == 1) ? SvREFCNT_inc(POPs) : NULL;
        PUTBACK;
        FREETMPS;
        LEAVE;
    }

    if (!decoded || !SvROK(decoded) || SvTYPE(SvRV(decoded)) != SVt_PVHV) {
        if (decoded) SvREFCNT_dec(decoded);
        return;
    }

    /* Extract id from the decoded hash */
    {
        HV *dhv = (HV *)SvRV(decoded);
        SV **id_svp = hv_fetchs(dhv, "id", 0);
        if (id_svp && SvOK(*id_svp)) {
            STRLEN id_len;
            const char *id = SvPV(*id_svp, id_len);
            SV *form = _form_lookup(aTHX_ id, id_len);
            if (form) {
                /* Call $form->dispatch($event_type, $json_str) */
                dSP;
                ENTER;
                SAVETMPS;

xs/form.xs  view on Meta::CPAN

                XPUSHs(event_type);
                XPUSHs(json_str);
                PUTBACK;
                call_method("dispatch", G_DISCARD);
                FREETMPS;
                LEAVE;
            }
        }
    }

    SvREFCNT_dec(decoded);
}

 # ---- attach(app) - register form and bind events on the app ----

SV *
attach(self, app)
    SV *self
    SV *app
CODE:
{

xs/socket_connection.xs  view on Meta::CPAN

                    av_push(result, msg_sv);
                } else {
                    if (msg_sv) SvREFCNT_dec(msg_sv);
                }
            }

            consumed += 4 + frame_len;
        }
    }

    /* Push all decoded messages onto Perl stack */
    result_count = av_len(result) + 1;
    for (ri = 0; ri < result_count; ri++) {
        SV **svp = av_fetch(result, ri, 0);
        if (svp) XPUSHs(sv_2mortal(SvREFCNT_inc(*svp)));
    }
    SvREFCNT_dec((SV *)result);
}

int
_xs_do_send(socket_sv, msg_sv)

xs/socket_connection.xs  view on Meta::CPAN

                SV *remaining = newSVpvn(
                    SvPVX(*buf_svp) + 4 + frame_len,
                    SvCUR(*buf_svp) - 4 - (STRLEN)frame_len);
                sv_setsv(*buf_svp, remaining);
                SvREFCNT_dec(remaining);

                /* JSON decode */
                {
                    dSP;
                    int count;
                    SV *decoded;
                    ENTER; SAVETMPS;
                    PUSHMARK(SP);
                    XPUSHs(json_enc);
                    XPUSHs(payload);
                    PUTBACK;
                    count = call_method("decode", G_SCALAR | G_EVAL);
                    SPAGAIN;
                    if (count > 0 && !SvTRUE(ERRSV)) {
                        decoded = newSVsv(POPs);
                        av_push(messages, decoded);
                    } else {
                        warn("Chandra::Socket::Connection: malformed JSON frame\n");
                        if (SvTRUE(ERRSV)) sv_setpvs(ERRSV, "");
                    }
                    PUTBACK;
                    FREETMPS; LEAVE;
                }

                SvREFCNT_dec(payload);
            }



( run in 0.773 second using v1.01-cache-2.11-cpan-600a1bdf6e4 )