Services

TIBET Services

wins

  • Simple, reusable interface to remote services and their content.
  • Integrated via TIBET signaling, minimizing conceptual complexity.
  • Integrates with Promises as needed, but offering more flexibility.

concepts

In the beginning there was "form submit". Set a form's target to an iframe, hook the onload handler, and submit the form. The earliest service call API of the web was asynchronous.

With the advent of XMLHttpRequest you could perform synchronous calls, although that's always been a questionable practice from a usability perspective. Thankfully XHRs also made it much easier to manage asynchronous calls and "AJAX" was born.

The thing is, even with XHRs there was no "organizing principle" or structure around which to construct your client-side service layer. Instead most applications ended up with XHR callbacks sprinkled everywhere. There was very little reuse, and a lot of hard-to-debug code.

TIBET addressed the question of how to structure and reuse service invocations by introducing the TP.core.Service, TP.sig.Request, and TP.sig.Response types.

Before Promises, before async/await, TIBET provided easy, reusable ways to construct a service endpoint, trigger a request, and process a responseā€¦all while keeping the code for that service organized via objects whose methods could be reused, inherited, and tested.

With the advent of modern async features in JavaScript the TIBET Service/Request/Response triplet has now been augmented so you can work with a TP.core.Response as a Promise. When you trigger a Request you get back a Response which implements the standard then API and which ultmately gives you a handle to a fully-featured Promise instance.

All code in services, requests, and responses can take advantage of async/await syntax as appropriate, but you retain the organizing behavior of TIBET's "service layer" types.

Why use TIBET's Service/Request/Response rather than promises or raw async/await?

We believe the need for a reusable, well-organized "service layer" still remains, and TIBET's S/R/R trio provides an easy, signal-friendly way to construct and maintain resusable code for accessing all of your enterprise's REST service endpoints.

cookbook

Creating A Service Type

A Service is ultimately a subtype of one of the TP.core.Service types. A common root for creating new service endpoints is the TP.core.IOService.

The sample code below is from ~lib/src/tibet/workflow/TP.core.UserIOService.js and provides some insight into how services are constructed. (If you think about it the "user" is an asynchronous source and sink for data so TIBET models the "user" like any other "service").

The main steps you need for creating a Service are to create the subtype, register its triggering signals, and implement signal handlers for each trigger to match your needs.

The code below shows the construction of the service subtype UserIOService and registration of triggers, essentially the signals which the service will register for and respond to.

//  Construct the service type 'UserIOService'.
TP.core.IOService.defineSubtype('UserIOService');

//  what signals will trigger this resource/service?
TP.core.UserIOService.Type.defineAttribute('triggers',
    TP.ac(TP.ac(TP.ANY, 'TP.sig.UserIORequest')));

NOTE: the triggers are provided as a list of ordered pairs, aka an array of arrays where the ordered pairs consist of a signal "origin" and signal "name". This origin/signal pairing is common TIBET syntax for signaling of any kind, however most Service triggers use TP.ANY as their origin since they want to observe the entire system for their Request type(s).

Once you've created the Service type and registered trigger signals you need to implement the handler functions that will respond to the Requests themselves.

For example, our UserIOService defines handlers for different subtypes of TP.sig.UserIORequest (UserInputRequest and UserOutputRequest in particular).

TP.core.UserIOService.Inst.defineHandler('UserInputRequest',
function(aSignal) {
...
});

Follow the same pattern in your Service types and you'll be all set.

Creating A Request Type

To activate a Service you need to fire a Request. The specific type of Request is normally configured for a particular Service. As a result you'll often want to create a TP.sig.Request subtype if there is special response handling.

To help ensure you match requests, responses, and services effectively you can create specific subtypes of TP.sig.Request that your Service will use when it defines its triggers.

For example, the TP.uri.WebDAVService relies on TP.sig.WebDAVRequest triggers rather than handling more general requests.

We can create the specific Request type with a single line of code:

TP.sig.HTTPRequest.defineSubtype('WebDAVRequest');

If your Request type wants to return a specific Response type you can register that by defining a type attribute of responseType and naming the Response:

TP.sig.WebDAVRequest.Type.defineAttribute('responseType', 'TP.sig.WebDAVResponse');

Creating A Response Type

Depending on the specific Service and Request you may find that you want to provide special result handling logic. The easy way to do that in TIBET is to define a specific Response type.

Creating a new response type is easy, just defineSubtype with your new type name using the desired parent response type. Since WebDAV is ultimately an HTTP-based protocol we'll use TP.sig.HTTPResponse in the sample below:

TP.sig.HTTPResponse.defineSubtype('WebDAVResponse');

Once you have your response type you can implement whatever methods you require to meet your needs. See the TP.sig.SOAPResponse type for an example.

Registering A Service

To activate a Service, essentially to get it to observe its triggers, you need to register it. This is typically done in the Service's file just after defining the triggers.

The code below is from ~lib/src/tibet/services/webdav/TP.uri.WebDAVService.js:

TP.uri.WebDAVService.register();

Once your service is registered it will begin handling any requests which match the origin/signal sets you provided in the triggers attribute.

Firing A Request

Firing a Request is as simple as firing any signal in TIBET.

If you have a specific Request subtype you will typically construct an instance prior to firing it, providing that instance with any properties you want it to include as part of the request.

For example, the following code is from the TP.shell.Shell type and is used to construct, configure, and then fire a TP.sig.UserInputRequest:

TP.shell.Shell.Inst.defineMethod('stdin',
function(aQuery, aDefault, aRequest) {
    var req;

    if (TP.notValid(aRequest)) {
        req = TP.sig.UserInputRequest.construct();
        req.set('requestor', this);
    } else if (TP.isKindOf(aRequest, TP.sig.UserInputRequest)) {
        req = aRequest;
    } else {
        //  convert from collection form to UIR form
        req = TP.sig.UserInputRequest.construct(aRequest);
        req.set('requestor', this);
    }

    req.atPut('query', aQuery);
    req.atPut('default', aDefault);

    req.fire(this);

    return this;
});

In response to the above signal the Shell will use any query value to render a prompt for the user along with any default value. The user's answer will be captured and provided back to the Service in the Response.

For simpler cases you don't need to configure actual Request instances. You can also simply "fire" the string version if you don't require a specific instance of a specific request type:

"TP.sig.FooRequest".fire();

// or

"TP.sig.FooRequest".fire(TP.ANY);

// or

"TP.sig.FooRequest".fire(TP.ANY, TP.hc('foo', 1, 'bar', 2));

Handling A Response

As mentioned in the Creating A Service Type cookbook entry Response data is handled by methods on the service you define. For example, our TP.sig.UserInputRequest ultimately ends with signaling of a TP.sig.UserInput response. The TP.shell.Shell type defines the following handler for that response type:

TP.shell.Shell.Inst.defineHandler('UserInput',
function(aSignal) {

    /**
     * @method handleUserInput
     * @summary Responds to TP.sig.UserInput signals, which are sent in
     *     response to a previous TP.sig.UserInputRequest from the shell itself.
     * @description The implication is that this method is only invoked when the
     *     shell had a prior request in the queue. The request itself will have
     *     been bound to the input response. The shell's response is to invoke
     *     the input as if it were a standard shell request.
     *     TP.sig.UserInputRequests originated by commands or other objects are
     *     presumed to "do the right thing" in response to the input they are
     *     provided.
     * @param {TP.sig.UserInput} aSignal The signal instance that triggered this
     *     handler.
     * @returns {TP.shell.ShellResponse} The response produced when processing
     *     the TP.sig.ShellRequest constructed by this method.
     */

    var request,
        responder,

        req,

        response;

    //  capture the responder so we can let it know when we're done. this
    //  will typically be something like the TP.tdp.Console or a similar UI.
    request = aSignal.getRequest();
    request.isActive(true);

    responder = aSignal.getRequest().get('responder');

    //  construct a request we can process to accomplish the new task
    req = TP.sig.ShellRequest.construct(TP.hc('cmd', aSignal.getResult()));
    req.set('requestor', this);

    try {
        TP.handle(this, req);
    } finally {
        //  if there was a responder let it know we're done
        if (TP.isValid(responder)) {
            aSignal.getRequestID().signal('TP.sig.RequestCompleted');
        }
    }

    response = aSignal.getResponse();

    //  TODO:   why both?
    response.complete();
    request.complete();

    return response;
});

code

Code for the service layer is in: ~lib/src/tibet/kernel/TIBETWorkflowTypes.js. Look for the Service, Request, and Response types but also check out the TP.core.Resource type.

Sample service/request/response code is in: ~/lib/src/tibet/services/*.

The various subdirectories in the services tree offer a lot of sample code on how to create service, request, and response types for specific endpoints or data formats.