The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.

NAME

RPC::ExtDirect::Client::Async - Asynchronous Ext.Direct client in Perl

SYNOPSIS

    use RPC::ExtDirect::Client::Async;
    
    my $client = RPC::ExtDirect::Client::Async->new(host => 'localhost');
    
    $client->call_async(
        action   => 'Action',
        method   => 'Method',
        arg      => [ 'foo', 'bar' ],
        cookies  => { foo => 'bar' },
        metadata => { baz => 'qux' },
        cb       => sub {
            my ($result, $success, $error) = @_;
            
            if ( $success ) {
                # Do something with $result
                ...
            }
            else {
                # Handle $error
                ...
            }
        },
    );

DESCRIPTION

This module implements a fully asynchronous Ext.Direct client based on AnyEvent::HTTP. It can be used with any event loop supported by AnyEvent.

If you are not familiar with Ext.Direct, start with RPC::ExtDirect::Intro before going further.

CAVEATS

API initialization

This client is fully asynchronous, and does not block on any operation. However before a client object can run Ext.Direct requests, it will need an instance of RPC::ExtDirect::API that holds the available API declaration. This object can be initialized in two ways: either remotely from a server, or locally by passing the api argument to the Client constructor:

    my $api = RPC::ExtDirect::API->new_from_hashref({
        api_href => { ... },
    });
    
    my $client = RPC::ExtDirect::Client::Async->new(
        host => 'localhost',
        port => 8080,
        api  => $api,
    );

If no local API instance is provided, the client object will attempt to retrieve it from the server. This operation is non-blocking; constructor will return the client object immediately and it can be used to make calls without waiting for the API to be initialized. However any requests made to the client before its API object is available will be queued internally by the client object, and dispatched only when API is retrieved. This is done transparently to the caller.

You can pass an option callback to be fired when the API is available via "api_cb" option:

    my $client = RPC::ExtDirect::Client::Async->new(
        ...
        api_cb => sub {
            my ($client, $success, $error) = @_;
            
            if ( !$success ) {
                # $error is defined
                ...
            }
        },
    );

This callback will be fired when a local API instance is provided, too.

If the remote API cannot be retrieved for some reason, the client object will set the error internally as fatal exception. Every request that was queued before the API retrieval failed will have its callback fired as if the request itself has failed, with the $error argument set to the API retrieval exception. Every subsequent request will have its callback sub fired immediately with an error.

Error handling

In a cooperative multi-tasking environment such as AnyEvent it is very hard, if at all possible, to guarantee that an exception thrown in the module code will be caught properly in the calling code. Because of that, Async client will not die upon encountering errors, except when the error is fatal. Instead, the error will be passed to the callback:

    $client->call_async(
        action => 'foo',
        method => 'bar',
        ...
        cb => sub {
            my ($result, $success, $error) = @_;
            
            if ( $success ) {
                # $result is defined, $error is undefined
            }
            else {
                # vice versa: $result is undefined, $error is set
            }
        },
    );

There are two exceptions to this rule: fatal errors encountered in Client constructor, and missing callback argument in "call_async", "submit_async", or "poll_async". This kind of errors usually means that there is a typo somewhere in the calling code, and should be caught early on.

Condition variables

Async client supports AnyEvent's condition variables to signal beginning and finishing a request, if such variable is provided. This can be used to block until several requests are finished:

    my $cv = AnyEvent->condvar;
    
    $client->call_async(   ..., cv => $cv );
    $client->upload_async( ..., cv => $cv );
    
    # This will return only when both requests are done
    $cv->recv;

Condition variable can also be used instead of a callback sub:

    my $cv = AnyEvent->condvar;
    
    $client->call_async( ..., cb => $cv );
    
    # This will block until the request has finished
    my $result = $cv->recv;
    
    # Handle the error, if any
    if ( not defined $result ) {
        my $error = ($cv->recv)[2];
        
        ...
    }

See "CONDITION VARIABLES" in AnyEvent for more information.

CLIENT OBJECT INTERFACE

RPC::ExtDirect::Client::Async provides several public methods:

new

Constructor. Returns a new Client instance and initializes an Ext.Direct API instance from the server, unless "api" parameter is provided. Accepts named arguments in a hash.

Parameters:

api

Optional RPC::ExtDirect::API instance to use. If not given, a new instance will be created from the remote API provided by the server.

config

Optional RPC::ExtDirect::Config instance to use. If not provided, a new Config instance will be created unless "api" parameter is provided, in which case the Config instance in the API object will be used instead.

host

Server's host name or IP address. This parameter is mandatory.

port

Optional port number to use when connecting to the server. Defaults to 80.

api_cb

Optional callback to be fired when API retrieval operation is finished. This function will receive three positional arguments: the Client reference, success flag, and the error string if it occured. An example:

    my $client = RPC::ExtDirect::Client::Async->new(
        ...
        api_cb => sub {
            my ($client, $success, $error) = @_;
            
            # $error is only defined when API retrieval failed
            if ( !$success ) {
                ...
            }
        },
    );
cv

Optional condition variable to be signaled when API retrieval finishes. See "Condition variables" for more detail.

cookies

Cookies to set when calling server side; can be either HTTP::Cookies object or a hashref containing key-value pairs. Setting this in constructor will pass the same cookies to all subsequent client calls.

...

All other arguments are stored as options and are applied to HTTP requests. See "http_request" in AnyEvent::HTTP for more detail.

Instance method. Returns an "api_class_client" object with the Ext.Direct API declaration published by the server.

Accepts one mandatory positional argument, API type, that can be either 'remoting' or 'polling' to retrieve the corresponding API object.

call_async

Instance method. Calls the specified Ext.Direct Method on the server side and passes the execution Result or Exception to the callback function specified in arguments. Accepts named arguments in a hash.

Parameters:

action

Ext.Direct Action (class) name. This parameter is mandatory.

method

Ext.Direct Method name to call. This parameter is mandatory.

arg

Ext.Direct Method arguments; use arrayref for methods that accept ordered parameters or hashref for named parameters. This parameter is mandatory even for Methods that accept no arguments; in such case, pass an empty arrayref.

metadata

Ext.Direct Method metadata. Use arrayref for methods that accept ordered metadata, or hashref for named metadata. This parameter is always optional, as metadata itself is optional.

cb

Mandatory callback function that will be fired when request finishes. This function will receive three positional arguments: the execution Result (or Exception), success flag, and error string if an error has occured:

    $client->call_async(
        ...
        cb => sub {
            my ($result, $success, $error) = @_;
            
            if ( $success ) {
                # Do something with $result
                ...
            }
            else {
                # Handle the $error
                ...
            }
        },
    );
cv

Optional condition variable to be signaled when the request is finished. See "Condition variables" for more detail.

cookies

Optional set of cookies for this particular call only. Cookies should be in the same format as for constructor, see "new".

...

Any other arguments are passed on to http_request function. See "http_request" in AnyEvent::HTTP.

submit_async

Instance method. Submits an HTML form request to a Form Handler method and passes the execution Result or Exception to the callback function specified in arguments. Accepts named arguments in a hash.

Parameters:

action

Ext.Direct Action (class) name. This parameter is mandatory.

method

Ext.Direct Method name to call. This parameter is mandatory.

arg

A hashref of the Method arguments. This parameter is mandatory, unless "upload" is specified.

metadata

Metadata array- or hashref for this Method invocation. This parameter is always optional.

upload

An optional arrayref of file names to upload. Files should be readable by the current process, or "submit" will die with an error.

cb

Mandatory callback function that will be fired when request finishes. See "call_async".

cv

Optional condition variable to be signaled when the request is finished. See "Condition variables" for more detail.

cookies

Optional set of cookies for this particular call only. Cookies should be in the same format as for constructor, see "new".

upload_async

A shortcut for "submit_async", for better readability when uploading files.

poll_async

Instance method. Polls server side for Ext.Direct Events, and passes the result to the callback function specified in arguments. Accepts named arguments in a hash.

In case of successful poll, i.e. when no errors have occured, the callback will receive an arrayref of event hashrefs:

    $client->poll_async(
        ...
        cb => sub {
            my ($events, $success, $error) = @_;
            
            if ( $success ) {
                # Do something with event data
                for my $event ( @$events ) {
                    ...
                }
            }
        }
    );

A poll may return an empty event list, in which case an empty arrayref will be passed to the callback. This is not an error.

Parameters:

cb

Mandatory callback function that will be fired when the poll finishes. See above.

cv

Optional condition variable to be signaled when the request is finished. See "Condition variables" for more detail.

cookies

Optional set of cookies for this particular call only. Cookies should be in the same format as for constructor, see "new".

CONFIGURATION OPTIONS

RPC::ExtDirect::Client::Async adds the following option specific to the Client, and sets it in the RPC::ExtDirect::Config instance it uses:

api_class_client

Class name to use when instantiating API objects from remote server JavaScript response. Default is RPC::ExtDirect::Client::API; use subclass name if you need to augment the stock API class behavior.

ACCESSOR METHODS

For RPC::ExtDirect::Client::Async, the following accessor methods are provided:

config

Return the current RPC::ExtDirect::Config instance held in the client object, or set a new one.

host

Return the current host name or IP address of the server, or set a new one.

port

Return the current port used to connect to the server, or set a new one.

cookies

Return the set of cookies to use with every Ext.Direct request, or set a new one. See "new" for more information.

http_params

Return a hashref with HTTP parameters to be used with every Ext.Direct request, or set a new one. These parameters will be passed to the HTTP request, see "http_request" in AnyEvent::HTTP.

By default this hashref will be populated with all "extra" arguments passed to "new" and does not need to be manipulated directly.

api_cb

Return the optional API retrieval callback function, or set a new one. See "API initialization" for more information.

api_ready

Will return truthy value if API has been successfully initialized, or falsy if it has not been initialized yet, or an error has occured while retrieving remote API declaration. See "exception" below.

exception

Will return the error that API retrieval resulted in, if any. The same error will be passed to the callback functions of all subsequent requests made through the Client object.

SEE ALSO

For more information on using Ext.Direct with Perl, see RPC::ExtDirect. For a blocking synchronous client, see RPC::ExtDirect::Client.

KNOWN ISSUES

AnyEvent::HTTP tries to reuse existing connection for idempotent requests, but for some obscure reason this does not always work as expected. As a consequence, first GET request (Ext.Direct API declaration retrieval) succeeds, while subsequent GET requests, e.g. event polling, will fail with 'Connection reset by peer' error. Which is not very helpful, to say the least.

To work around this issue, persistent connections has been disabled by default for all types of requests, including idempotent ones. If you are sure that this issue will not be a problem for you and want to use persistent connections, pass persistent => 1 parameter either to Client constructor, or to the individual "poll_async" calls.

Note that this issue does not apply to "call_async" or "submit_async" calls, since both of these are translated into HTTP POST requests which are not affected.

ACKNOWLEDGEMENTS

I would like to thank IntelliSurvey, Inc for sponsoring my work on this module.

BUGS AND LIMITATIONS

At this time there are no known bugs in this module. Please report problems to the author, patches are always welcome.

Use Github tracker to open bug reports, this is the easiest and quickest way to get your issue fixed.

COPYRIGHT AND LICENSE

Copyright (c) 2013-2016 Alex Tokarev <tokarev@cpan.org>.

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