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 ---
(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);
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()));
}
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
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);
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;
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 )