Hypersonic

 view release on metacpan or  search on metacpan

lib/Hypersonic/UA.pm  view on Meta::CPAN


    # Core functions (always registered)
    my %functions = (
        # Constructor/destructor
        'Hypersonic::UA::new'           => { source => 'xs_ua_new', is_xs_native => 1 },
        'Hypersonic::UA::DESTROY'       => { source => 'xs_ua_destroy', is_xs_native => 1 },

        # URL/request utilities
        'Hypersonic::UA::parse_url'     => { source => 'xs_ua_parse_url', is_xs_native => 1 },
        'Hypersonic::UA::build_request' => { source => 'xs_ua_build_request', is_xs_native => 1 },

        # Blocking (sync) HTTP methods - always available
        'Hypersonic::UA::get'           => { source => 'xs_ua_get', is_xs_native => 1 },
        'Hypersonic::UA::post'          => { source => 'xs_ua_post', is_xs_native => 1 },
        'Hypersonic::UA::put'           => { source => 'xs_ua_put', is_xs_native => 1 },
        'Hypersonic::UA::patch'         => { source => 'xs_ua_patch', is_xs_native => 1 },
        'Hypersonic::UA::delete'        => { source => 'xs_ua_delete', is_xs_native => 1 },
        'Hypersonic::UA::head'          => { source => 'xs_ua_head', is_xs_native => 1 },
        'Hypersonic::UA::options'       => { source => 'xs_ua_options', is_xs_native => 1 },
        'Hypersonic::UA::request'       => { source => 'xs_ua_request', is_xs_native => 1 },
    );

    # Async functions (conditional - requires async => 1)
    if ($analysis->{needs_async}) {
        %functions = (%functions,
            # Future-based async methods
            'Hypersonic::UA::get_async'     => { source => 'xs_ua_get_async', is_xs_native => 1 },
            'Hypersonic::UA::post_async'    => { source => 'xs_ua_post_async', is_xs_native => 1 },
            'Hypersonic::UA::put_async'     => { source => 'xs_ua_put_async', is_xs_native => 1 },
            'Hypersonic::UA::delete_async'  => { source => 'xs_ua_delete_async', is_xs_native => 1 },
            'Hypersonic::UA::request_async' => { source => 'xs_ua_request_async', is_xs_native => 1 },

            # Run/poll methods
            'Hypersonic::UA::run'           => { source => 'xs_ua_run', is_xs_native => 1 },
            'Hypersonic::UA::run_one'       => { source => 'xs_ua_run_one', is_xs_native => 1 },
            'Hypersonic::UA::tick'          => { source => 'xs_ua_tick', is_xs_native => 1 },
            'Hypersonic::UA::pending'       => { source => 'xs_ua_pending', is_xs_native => 1 },
        );
    }

    # Parallel/race helpers (conditional - requires parallel => 1)
    if ($analysis->{needs_parallel}) {
        %functions = (%functions,
            'Hypersonic::UA::parallel'      => { source => 'xs_ua_parallel', is_xs_native => 1 },
            'Hypersonic::UA::race'          => { source => 'xs_ua_race', is_xs_native => 1 },
        );
    }

    return \%functions;
}

sub gen_ua_registry {
    my ($class, $builder, $max) = @_;
    $max //= MAX_CONNECTIONS;

    $builder->line("#define UA_MAX_CONNECTIONS $max")
      ->line("#define UA_MAX_INSTANCES 256")
      ->line("#define DNS_CACHE_SIZE 64")
      ->line("#define CONN_POOL_SIZE 32")
      ->blank
      ->comment('Connection pool entry for keep-alive')
      ->line('typedef struct {')
      ->line('    int fd;')
      ->line('    char host[256];')
      ->line('    int port;')
      ->line('    time_t expires;')
      ->line('} PooledConn;')
      ->blank
      ->line("static PooledConn conn_pool[CONN_POOL_SIZE];")
      ->blank
      ->comment('Get a pooled connection if available')
      ->line('static int pool_get(const char *host, int port) {')
      ->line('    int i;')
      ->line('    time_t now = time(NULL);')
      ->line('    for (i = 0; i < CONN_POOL_SIZE; i++) {')
      ->line('        if (conn_pool[i].fd > 0 && conn_pool[i].port == port &&')
      ->line('            strcmp(conn_pool[i].host, host) == 0 && conn_pool[i].expires > now) {')
      ->line('            int fd = conn_pool[i].fd;')
      ->line('            conn_pool[i].fd = 0;')
      ->line('            return fd;')
      ->line('        }')
      ->line('    }')
      ->line('    return -1;')
      ->line('}')
      ->blank
      ->comment('Return connection to pool (15 second keep-alive)')
      ->line('static void pool_put(int fd, const char *host, int port) {')
      ->line('    int i;')
      ->line('    time_t now = time(NULL);')
      ->line('    /* Find empty slot or expired entry */')
      ->line('    for (i = 0; i < CONN_POOL_SIZE; i++) {')
      ->line('        if (conn_pool[i].fd <= 0 || conn_pool[i].expires <= now) {')
      ->line('            if (conn_pool[i].fd > 0) close(conn_pool[i].fd);')
      ->line('            conn_pool[i].fd = fd;')
      ->line('            strncpy(conn_pool[i].host, host, 255);')
      ->line('            conn_pool[i].host[255] = 0;')
      ->line('            conn_pool[i].port = port;')
      ->line('            conn_pool[i].expires = now + 15;')
      ->line('            return;')
      ->line('        }')
      ->line('    }')
      ->line('    /* Pool full, just close */')
      ->line('    close(fd);')
      ->line('}')
      ->blank
      ->line('typedef struct {')
      ->line('    int fd;')
      ->line('    int tls;')
      ->line('    int state;')
      ->line('    char* host;')
      ->line('    int port;')
      ->line('    int timeout_ms;')
      ->line('    int connect_timeout_ms;')
      ->line('} UAConnection;')
      ->blank
      ->line("static UAConnection g_ua_connections[UA_MAX_CONNECTIONS];")
      ->blank
      ->comment('DNS cache entry')
      ->line('typedef struct {')
      ->line('    char host[256];')
      ->line('    struct in_addr addr;')
      ->line('    time_t expires;')
      ->line('} DNSCacheEntry;')
      ->blank
      ->line("static DNSCacheEntry dns_cache[DNS_CACHE_SIZE];")
      ->line("static int dns_cache_next = 0;")
      ->blank
      ->comment('Lookup DNS with caching (60 second TTL)')
      ->line('static int dns_lookup_cached(const char *host, struct in_addr *addr_out) {')
      ->line('    int i;')
      ->line('    int slot;')
      ->line('    struct hostent *he;')
      ->line('    time_t now = time(NULL);')
      ->line('    /* Check cache */')
      ->line('    for (i = 0; i < DNS_CACHE_SIZE; i++) {')
      ->line('        if (dns_cache[i].host[0] && strcmp(dns_cache[i].host, host) == 0 && dns_cache[i].expires > now) {')
      ->line('            *addr_out = dns_cache[i].addr;')
      ->line('            return 1;')
      ->line('        }')
      ->line('    }')
      ->line('    /* Cache miss - do lookup */')
      ->line('    he = gethostbyname(host);')
      ->line('    if (!he) return 0;')
      ->line('    memcpy(addr_out, he->h_addr_list[0], sizeof(struct in_addr));')
      ->line('    /* Store in cache */')
      ->line('    slot = dns_cache_next++ % DNS_CACHE_SIZE;')
      ->line('    strncpy(dns_cache[slot].host, host, 255);')
      ->line('    dns_cache[slot].host[255] = 0;')
      ->line('    dns_cache[slot].addr = *addr_out;')
      ->line('    dns_cache[slot].expires = now + 60;')
      ->line('    return 1;')
      ->line('}')
      ->blank
      ->line('typedef struct {')
      ->line('    int in_use;')
      ->line('    int timeout_ms;')
      ->line('    int connect_timeout_ms;')
      ->line('    int max_redirects;')
      ->line('    int keep_alive;')
      ->line('    SV *default_headers;')
      ->line('    char *base_url;')
      ->line('} UAContext;')
      ->blank
      ->line("static UAContext ua_registry[UA_MAX_INSTANCES];")
      ->blank
      ->line('static int ua_alloc_slot(void) {')
      ->line('    int i;')
      ->line('    for (i = 0; i < UA_MAX_INSTANCES; i++) {')
      ->line('        if (!ua_registry[i].in_use) {')
      ->line('            memset(&ua_registry[i], 0, sizeof(UAContext));')
      ->line('            ua_registry[i].in_use = 1;')
      ->line('            return i;')
      ->line('        }')
      ->line('    }')
      ->line('    return -1;')
      ->line('}')
      ->blank
      ->line('static void ua_free_slot(int slot) {')
      ->line('    if (slot >= 0 && slot < UA_MAX_INSTANCES) {')
      ->line('        ua_registry[slot].in_use = 0;')
      ->line('    }')
      ->line('}')
      ->blank;
}

sub gen_xs_new {
    my ($class, $builder) = @_;

    $builder->comment('Constructor: new() or new({ timeout => 30000, ... })')
      ->xs_function('xs_ua_new')
      ->xs_preamble
      ->line('HV *opts;')
      ->line('SV **val;')
      ->line('STRLEN len;')
      ->line('const char *url;')
      ->line('int slot;')
      ->line('UAContext *ctx;')
      ->line('AV *self;')
      ->line('SV *self_ref;')
      ->blank
      ->line('if (items < 1) croak("Usage: Hypersonic::UA->new([$opts])");')
      ->blank
      ->line('slot = ua_alloc_slot();')
      ->line('if (slot < 0) croak("Too many UA instances");')
      ->blank
      ->line('ctx = &ua_registry[slot];')
      ->blank
      ->comment('Defaults')
      ->line('ctx->timeout_ms = 30000;')
      ->line('ctx->connect_timeout_ms = 5000;')
      ->line('ctx->max_redirects = 5;')
      ->line('ctx->keep_alive = 1;')
      ->line('ctx->default_headers = NULL;')
      ->line('ctx->base_url = NULL;')
      ->blank
      ->comment('Parse options hash if provided')
      ->if('items >= 2 && SvROK(ST(1)) && SvTYPE(SvRV(ST(1))) == SVt_PVHV')
        ->line('opts = (HV *)SvRV(ST(1));')
        ->blank
        ->line('if ((val = hv_fetchs(opts, "timeout", 0)) && SvOK(*val)) ctx->timeout_ms = SvIV(*val);')
        ->line('if ((val = hv_fetchs(opts, "connect_timeout", 0)) && SvOK(*val)) ctx->connect_timeout_ms = SvIV(*val);')
        ->line('if ((val = hv_fetchs(opts, "max_redirects", 0)) && SvOK(*val)) ctx->max_redirects = SvIV(*val);')
        ->line('if ((val = hv_fetchs(opts, "keep_alive", 0)) && SvOK(*val)) ctx->keep_alive = SvTRUE(*val) ? 1 : 0;')
        ->line('if ((val = hv_fetchs(opts, "headers", 0)) && SvROK(*val)) ctx->default_headers = SvREFCNT_inc(*val);')
        ->if('(val = hv_fetchs(opts, "base_url", 0)) && SvOK(*val)')
          ->line('url = SvPV(*val, len);')
          ->line('ctx->base_url = (char *)malloc(len + 1);')
          ->line('memcpy(ctx->base_url, url, len + 1);')
        ->endif
      ->endif
      ->blank
      ->comment('Build array-based object')
      ->line('self = newAV();')
      ->line('av_extend(self, 6);')
      ->line('av_store(self, 0, newSViv(slot));')
      ->line('av_store(self, 1, newSViv(ctx->timeout_ms));')
      ->line('av_store(self, 2, newSViv(ctx->connect_timeout_ms));')
      ->line('av_store(self, 3, ctx->default_headers ? SvREFCNT_inc(ctx->default_headers) : newRV_noinc((SV *)newHV()));')
      ->line('av_store(self, 4, ctx->base_url ? newSVpv(ctx->base_url, 0) : &PL_sv_undef);')
      ->line('av_store(self, 5, newSViv(ctx->max_redirects));')
      ->line('av_store(self, 6, newSViv(ctx->keep_alive));')
      ->blank
      ->line('self_ref = newRV_noinc((SV *)self);')
      ->line('sv_bless(self_ref, gv_stashpv("Hypersonic::UA", GV_ADD));')
      ->blank
      ->line('ST(0) = sv_2mortal(self_ref);')
      ->xs_return('1')
      ->xs_end
      ->blank;
}

sub gen_xs_destroy {
    my ($class, $builder) = @_;

    $builder->comment('Destructor')
      ->xs_function('xs_ua_destroy')
      ->xs_preamble
      ->line('if (items != 1 || !SvROK(ST(0))) XSRETURN_EMPTY;')
      ->blank
      ->line('AV *self = (AV *)SvRV(ST(0));')
      ->line('SV **slot_sv = av_fetch(self, 0, 0);')
      ->line('if (!slot_sv || !SvOK(*slot_sv)) XSRETURN_EMPTY;')
      ->blank
      ->line('int slot = SvIV(*slot_sv);')
      ->line('if (slot < 0 || slot >= UA_MAX_INSTANCES) XSRETURN_EMPTY;')
      ->blank
      ->line('UAContext *ctx = &ua_registry[slot];')
      ->blank
      ->comment('Free UA resources')
      ->if('ctx->default_headers')
        ->line('SvREFCNT_dec(ctx->default_headers);')
        ->line('ctx->default_headers = NULL;')
      ->endif
      ->if('ctx->base_url')
        ->line('free(ctx->base_url);')
        ->line('ctx->base_url = NULL;')
      ->endif
      ->blank
      ->line('ua_free_slot(slot);')
      ->xs_return('0')
      ->xs_end
      ->blank;
}

sub gen_xs_parse_url {
    my ($class, $builder) = @_;

    $builder->comment('Parse URL into components: (scheme, host, port, path, query)')
      ->xs_function('xs_ua_parse_url')
      ->xs_preamble
      ->line('if (items != 1) croak("Usage: parse_url(url)");')
      ->blank
      ->line('STRLEN url_len;')
      ->line('const char* url = SvPV(ST(0), url_len);')
      ->blank
      ->comment('Parse scheme')
      ->line('const char* p = url;')
      ->line('const char* scheme_end = strstr(p, "://");')
      ->if('!scheme_end')
        ->line('croak("Invalid URL: missing scheme");')
      ->endif

lib/Hypersonic/UA.pm  view on Meta::CPAN

        ->line('SvPV(body_sv, body_len);')
        ->line('request_size += 20 + body_len;')
      ->endif
      ->blank
      ->line('request_size += 2;')
      ->blank
      ->comment('Allocate and build request')
      ->line('SV* request = newSV(request_size);')
      ->line('SvPOK_on(request);')
      ->line('char* rp = SvPVX(request);')
      ->blank
      ->comment('Request line')
      ->line('memcpy(rp, method, method_len); rp += method_len;')
      ->line('*rp++ = \' \';')
      ->line('memcpy(rp, path, path_len); rp += path_len;')
      ->line('memcpy(rp, " HTTP/1.1\\r\\n", 11); rp += 11;')
      ->blank
      ->comment('Host header')
      ->line('memcpy(rp, "Host: ", 6); rp += 6;')
      ->line('memcpy(rp, host, host_len); rp += host_len;')
      ->line('*rp++ = \'\\r\'; *rp++ = \'\\n\';')
      ->blank
      ->comment('Other headers')
      ->line('hv_iterinit(headers);')
      ->line('while ((entry = hv_iternext(headers)) != NULL) {')
      ->line('    SV* key_sv = hv_iterkeysv(entry);')
      ->line('    SV* val_sv = hv_iterval(headers, entry);')
      ->line('    STRLEN key_len, val_len;')
      ->line('    const char* key = SvPV(key_sv, key_len);')
      ->line('    const char* val = SvPV(val_sv, val_len);')
      ->line('    memcpy(rp, key, key_len); rp += key_len;')
      ->line('    *rp++ = \':\'; *rp++ = \' \';')
      ->line('    memcpy(rp, val, val_len); rp += val_len;')
      ->line('    *rp++ = \'\\r\'; *rp++ = \'\\n\';')
      ->line('}')
      ->blank
      ->comment('Content-Length and body if present')
      ->if('body_len > 0')
        ->line('rp += sprintf(rp, "Content-Length: %zu\\r\\n", body_len);')
      ->endif
      ->blank
      ->comment('End of headers')
      ->line('*rp++ = \'\\r\'; *rp++ = \'\\n\';')
      ->blank
      ->comment('Body')
      ->if('body_len > 0')
        ->line('const char* body = SvPV_nolen(body_sv);')
        ->line('memcpy(rp, body, body_len); rp += body_len;')
      ->endif
      ->blank
      ->line('SvCUR_set(request, rp - SvPVX(request));')
      ->line('ST(0) = sv_2mortal(request);')
      ->xs_return('1')
      ->xs_end
      ->blank;
}

sub gen_xs_get {
    my ($class, $builder) = @_;

    # Inlined HTTP GET with connection pooling and keep-alive
    $builder->comment('GET request - with keep-alive connection pooling')
      ->comment('Usage: $ua->get($url) or $ua->get($url, sub { ... })')
      ->xs_function('xs_ua_get')
      ->xs_preamble
      ->line('if (items < 2) croak("Usage: $ua->get($url, [$cb])");')
      ->blank
      ->line('SV *self_sv = ST(0);')
      ->line('SV *url_sv = ST(1);')
      ->line('SV *cb = (items >= 3 && SvROK(ST(2)) && SvTYPE(SvRV(ST(2))) == SVt_PVCV) ? ST(2) : NULL;')
      ->blank
      ->comment('Parse URL')
      ->line('STRLEN url_len;')
      ->line('const char *url = SvPV(url_sv, url_len);')
      ->blank
      ->line('const char *scheme_end = strstr(url, "://");')
      ->line('if (!scheme_end) croak("Invalid URL");')
      ->blank
      ->line('int is_https = (scheme_end - url == 5 && memcmp(url, "https", 5) == 0);')
      ->line('const char *host_start = scheme_end + 3;')
      ->line('const char *host_end = host_start;')
      ->line('int port = is_https ? 443 : 80;')
      ->blank
      ->line('while (*host_end && *host_end != \':\' && *host_end != \'/\' && *host_end != \'?\') host_end++;')
      ->blank
      ->line('const char *p = host_end;')
      ->if('*host_end == \':\'')
        ->line('port = atoi(host_end + 1);')
        ->line('while (*p && *p != \'/\' && *p != \'?\') p++;')
      ->endif
      ->blank
      ->line('const char *path = (*p == \'/\') ? p : "/";')
      ->blank
      ->line('char host_buf[256];')
      ->line('int host_len = host_end - host_start;')
      ->line('if (host_len > 255) host_len = 255;')
      ->line('memcpy(host_buf, host_start, host_len);')
      ->line('host_buf[host_len] = 0;')
      ->blank
      ->comment('Try to get pooled connection first')
      ->line('int fd = pool_get(host_buf, port);')
      ->line('int pooled = (fd > 0);')
      ->blank
      ->if('fd <= 0')
        ->comment('No pooled connection, create new one')
        ->line('fd = socket(AF_INET, SOCK_STREAM, 0);')
        ->line('if (fd < 0) croak("socket() failed");')
        ->blank
        ->line('struct sockaddr_in addr;')
        ->line('memset(&addr, 0, sizeof(addr));')
        ->line('addr.sin_family = AF_INET;')
        ->line('addr.sin_port = htons(port);')
        ->if('!dns_lookup_cached(host_buf, &addr.sin_addr)')
          ->line('close(fd);')
          ->line('croak("DNS resolution failed for %s", host_buf);')
        ->endif
        ->blank
        ->if('connect(fd, (struct sockaddr *)&addr, sizeof(addr)) < 0')
          ->line('close(fd);')
          ->line('croak("connect() failed");')
        ->endif
      ->endif
      ->blank
      ->comment('Build HTTP GET request with keep-alive')
      ->line('char req_buf[4096];')
      ->line('int req_len = snprintf(req_buf, sizeof(req_buf),')
      ->line('    "GET %s HTTP/1.1\\r\\n"')
      ->line('    "Host: %s\\r\\n"')
      ->line('    "Connection: keep-alive\\r\\n"')
      ->line('    "User-Agent: Hypersonic/1.0\\r\\n"')
      ->line('    "\\r\\n",')
      ->line('    path, host_buf);')
      ->blank
      ->comment('Send request')
      ->line('if (send(fd, req_buf, req_len, 0) < 0) {')
      ->line('    close(fd);')
      ->line('    croak("send() failed");')
      ->line('}')
      ->blank
      ->comment('Receive response - need to parse Content-Length for keep-alive')
      ->line('char resp_buf[65536];')
      ->line('int resp_len = 0;')
      ->line('int headers_end = 0;')
      ->line('int content_length = -1;')
      ->line('int n;')
      ->blank
      ->comment('Read until we have headers')
      ->line('while (!headers_end && resp_len < (int)sizeof(resp_buf) - 1) {')
      ->line('    n = recv(fd, resp_buf + resp_len, sizeof(resp_buf) - resp_len - 1, 0);')
      ->line('    if (n <= 0) break;')
      ->line('    resp_len += n;')
      ->line('    resp_buf[resp_len] = 0;')
      ->line('    char *hdr_end = strstr(resp_buf, "\\r\\n\\r\\n");')
      ->line('    if (hdr_end) {')
      ->line('        headers_end = hdr_end - resp_buf + 4;')
      ->line('        /* Parse Content-Length */')
      ->line('        char *cl = strcasestr(resp_buf, "Content-Length:");')
      ->line('        if (cl && cl < hdr_end) content_length = atoi(cl + 15);')
      ->line('    }')
      ->line('}')
      ->blank
      ->comment('Read remaining body based on Content-Length')
      ->if('content_length > 0')
        ->line('int body_received = resp_len - headers_end;')
        ->line('while (body_received < content_length && resp_len < (int)sizeof(resp_buf) - 1) {')
        ->line('    n = recv(fd, resp_buf + resp_len, sizeof(resp_buf) - resp_len - 1, 0);')
        ->line('    if (n <= 0) break;')
        ->line('    resp_len += n;')
        ->line('    body_received += n;')
        ->line('}')
      ->endif
      ->line('resp_buf[resp_len] = 0;')
      ->blank
      ->comment('Return connection to pool if keep-alive')
      ->line('char *conn_hdr = strcasestr(resp_buf, "Connection:");')
      ->line('int keep_alive = 1;')
      ->if('conn_hdr && headers_end > 0 && conn_hdr < resp_buf + headers_end')
        ->line('if (strncasecmp(conn_hdr + 11, " close", 6) == 0) keep_alive = 0;')
      ->endif
      ->blank
      ->if('keep_alive && content_length >= 0')
        ->line('pool_put(fd, host_buf, port);')
      ->else
        ->line('close(fd);')
      ->endif
      ->blank
      ->comment('Parse response')
      ->line('HV *result = newHV();')
      ->blank
      ->comment('Extract status code')
      ->line('int status = 0;')
      ->line('if (resp_len > 12 && memcmp(resp_buf, "HTTP/1.", 7) == 0) {')
      ->line('    status = atoi(resp_buf + 9);')
      ->line('}')
      ->line('hv_stores(result, "status", newSViv(status));')
      ->blank
      ->comment('Find body')
      ->line('const char *body_start = strstr(resp_buf, "\\r\\n\\r\\n");')
      ->if('body_start')
        ->line('body_start += 4;')
        ->line('hv_stores(result, "body", newSVpv(body_start, resp_len - (body_start - resp_buf)));')
      ->else
        ->line('hv_stores(result, "body", newSVpvs(""));')
      ->endif
      ->blank
      ->comment('Store headers')
      ->line('HV *headers = newHV();')
      ->line('hv_stores(result, "headers", newRV_noinc((SV *)headers));')
      ->blank
      ->comment('If callback provided, call it')
      ->if('cb')
        ->line('SPAGAIN;')
        ->line('ENTER; SAVETMPS;')
        ->line('PUSHMARK(SP);')
        ->line('XPUSHs(sv_2mortal(newRV_noinc((SV *)result)));')
        ->line('PUTBACK;')
        ->line('call_sv(cb, G_DISCARD);')
        ->line('FREETMPS; LEAVE;')
        ->line('XSRETURN_EMPTY;')
      ->endif
      ->blank
      ->line('ST(0) = sv_2mortal(newRV_noinc((SV *)result));')
      ->xs_return('1')
      ->xs_end
      ->blank;
}

sub gen_xs_post {
    my ($class, $builder) = @_;

    $builder->comment('POST request - blocking or callback')
      ->comment('Usage: $ua->post($url, $body) or $ua->post($url, $body, sub { ... })')
      ->xs_function('xs_ua_post')
      ->xs_preamble
      ->line('if (items < 3) croak("Usage: $ua->post($url, $body, [$cb])");')
      ->blank
      ->line('SV *self_sv = ST(0);')
      ->line('SV *url_sv = ST(1);')
      ->line('SV *body_sv = ST(2);')
      ->line('SV *cb = (items >= 4 && SvROK(ST(3)) && SvTYPE(SvRV(ST(3))) == SVt_PVCV) ? ST(3) : NULL;')
      ->blank

lib/Hypersonic/UA.pm  view on Meta::CPAN

    *compile = sub {
        my $result = $orig_compile->(@_);
        __PACKAGE__->_install_stubs() if $result;
        return $result;
    };
}

1;

__END__

=head1 NAME

Hypersonic::UA - High-performance JIT-compiled HTTP user agent

=head1 SYNOPSIS

    use Hypersonic::UA;

    # Compile with minimal features (blocking only)
    Hypersonic::UA->compile();

    # Or compile with async support
    Hypersonic::UA->compile(async => 1);

    # Or compile with all features
    Hypersonic::UA->compile(full => 1);

    # Create UA instance
    my $ua = Hypersonic::UA->new();

    # Blocking requests
    my $res = $ua->get('http://example.com/api');
    my $res = $ua->post('http://example.com/api', '{"data":"value"}');
    my $res = $ua->put('http://example.com/api', $body);
    my $res = $ua->delete('http://example.com/api');

    # Response is a hashref
    print $res->{status};   # 200
    print $res->{body};     # Response body

    # Async requests (requires async => 1)
    # No manual tick() needed - event loop runs automatically!
    my $future = $ua->get_async('http://example.com/api');

    # Chain with callbacks
    $future->then(sub {
        my ($response) = @_;
        print $response;
    });

    # Or fetch multiple URLs concurrently
    my @futures = map { $ua->get_async($_) } @urls;
    my $all = Hypersonic::Future->needs_all(@futures);
    my @results = $all->result;  # Automatically completes all requests

=head1 DESCRIPTION

C<Hypersonic::UA> is a high-performance HTTP client using JIT-compiled XS code.
It supports both blocking and async operations with connection pooling and
keep-alive support.

=head1 COMPILATION OPTIONS

    Hypersonic::UA->compile(%options);

=over 4

=item async => 1

Enable async methods: C<get_async>, C<post_async>, C<tick>, C<run>, C<pending>.

=item parallel => 1

Enable parallel methods: C<parallel>, C<race>. Implies C<async>.

=item tls => 1

Enable HTTPS/TLS support.

=item http2 => 1

Enable HTTP/2 protocol support.

=item compression => 1

Enable gzip/deflate response decompression.

=item cookie_jar => 1

Enable automatic cookie handling.

=item redirects => 1

Enable automatic redirect following.

=item full => 1

Enable all features.

=item cache_dir => $path

Directory for caching compiled XS code.

=back

=head1 METHODS

=head2 new

    my $ua = Hypersonic::UA->new(%options);

Create a new UA instance.

=head2 get

    my $res = $ua->get($url);

Perform a blocking GET request.

=head2 post

lib/Hypersonic/UA.pm  view on Meta::CPAN

    # Just use the Future when you need the result:
    $future->then(sub {
        my ($response) = @_;
        print $response;
    });

Start an async GET request. Returns a Future that resolves with the response.

=head2 post_async

    my $future = $ua->post_async($url, $body);

Start an async POST request. Returns a Future.

=head2 tick

    $ua->tick();

Manually process pending async requests. B<Usually not needed> - the event
loop runs automatically. Returns remaining pending count (0 when all complete).

This method now loops internally until all requests complete or no progress
is made, so a single call is sufficient.

=head2 run

    $ua->run();

Run event loop until all pending requests complete. B<Usually not needed> -
prefer using Futures directly which auto-tick.

=head2 pending

    my $count = $ua->pending();

Return count of pending async requests.

=head1 PARALLEL METHODS

These require C<< parallel => 1 >> at compile time.

=head2 parallel

    my @results = $ua->parallel(@urls);

Fetch multiple URLs in parallel, wait for all to complete.

=head2 race

    my $result = $ua->race(@urls);

Fetch multiple URLs, return first to complete.

=head1 PERFORMANCE

C<Hypersonic::UA> is designed for high-throughput async HTTP operations.
Key performance features:

=head2 Connection Pooling

Connections are automatically pooled and reused with HTTP keep-alive.
This eliminates the TCP handshake overhead for subsequent requests to
the same host, significantly improving throughput.

=head2 Event-Driven I/O

Uses the best available event backend (kqueue on macOS/BSD, epoll on Linux)
for efficient non-blocking I/O with minimal syscall overhead.

=head2 JIT-Compiled XS

All hot paths are JIT-compiled to XS/C code, avoiding Perl interpreter
overhead in the request processing loop.

=head2 Benchmark Example

    use Hypersonic::Future;
    use Hypersonic::UA;
    use Time::HiRes qw(time);

    Hypersonic::Future->compile;
    Hypersonic::UA->compile(async => 1);

    my $ua = Hypersonic::UA->new();
    my @urls = ('http://127.0.0.1:8080/') x 1000;

    my $start = time();

    # Start all requests - event loop runs automatically!
    my @futures = map { $ua->get_async($_) } @urls;

    # Combine futures - when accessed, remaining requests complete
    my $all = Hypersonic::Future->needs_all(@futures);

    # Get results - this triggers any remaining processing
    my @responses = $all->result;

    my $elapsed = time() - $start;
    printf "%d requests in %.3fs (%.0f req/sec)\n",
        scalar(@responses), $elapsed, scalar(@responses) / $elapsed;

Typical results on modern hardware:

=over 4

=item * B<100,000+ requests/sec> to localhost

=item * B<Connection reuse> via keep-alive pooling

=item * B<Minimal memory overhead> with slot-based context management

=back

=head1 AUTHOR

lnation E<lt>email@lnation.orgE<gt>

=head1 LICENSE

This library is free software; you can redistribute it and/or modify it
under the same terms as Perl itself.

=cut



( run in 0.790 second using v1.01-cache-2.11-cpan-a5162978ef8 )