Services

TIBET Services

wins

  • Simple, reusable interface to local or remote services and content.
  • Integrated via TIBET signaling, minimizing conceptual complexity.
  • Integrates with await / Promises while 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 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 easier to manage asynchronous calls and "AJAX" was born.

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

TIBET addressed the problem of structuring and reusing service invocation logic by introducing the TP.core.Service, TP.sig.Request, and TP.sig.Response types.

Before Promises, before async/await, TIBET provided an easy, reusable way to encapsulate a service endpoint, trigger a request, and process a response. Using Service/Request/Response, the code for each service is organized via objects whose methods can be reused, inherited, and tested.

With the advent of async features in JavaScript, Service/Request/Response now supports await as well as the standard Promise API. When you trigger a Request you receive a thenable Response instance you can upgrade to a full Promise via asPromise or use directly via then or await.

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

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

The Request Type

The TP.sig.Request type is a TIBET signal that carries specific information about requesting an action from a service.

Requests are fired just like other TIBET signal. If a service (typically a subtype of TP.core.Service) is registered for that signal, it will process that request and return a response instance.

After a service is done fulfilling the request, it calls complete() or fail(), depending on whether it succeeded or not. This is analogous to invoking a Promise resolver or rejector to finalize processing.

Handlers on the request or the "requestor" which triggered the request are activated in response to the request's completion, failure, cancellation, or error state.

The Response Type

The TP.sig.Response type is a TIBET signal that is typically paired with a particular Request type. While it contains information about the response that the service produced it can also be augmented with logic specific to a particular service/request interaction.

When a request is fired and accepted by a service for processing the service will query the request for the response type to return. This approach allows requests to designate a 'smart wrapper' for the results of that operation, much like URI maps allow you to specify custom content type wrappers for their resource data.

The Service Type

The TP.core.Service type is a specialized form of signal handler.

In TIBET, services can be defined to handle any kind of request, although the majority are created to interact with remote endpoints such as REST or HTTPS service endpoints.

For instance, rather than exposing all of the 'choreography' involved in advanced scenarios of communicating with a remote service, that can all be abstracted into a subtype of TP.core.Service and triggered by firing a request.

Services are also commonly used in TIBET to interact with the User, the other common "asynchronous data source" web applications often interact with.

cookbook

Creating A Service Type

All TIBET services are ultimately subtypes of one of the TP.core.Service types.

A common root for creating new service endpoints is the TP.core.IOService. In this example we use TP.uri.HTTPService to demonstrate creating a service to handle WebDAV communication requests.

The sample code below is taken from ~lib/src/tibet/services/TP.uri.WebDAVService.js and provides some insight into how services are constructed.

The primary steps for creating a Service are:

  • creating the service subtype,
  • registering one or more triggering signals,
  • implementing handlers for each trigger

The code below shows the construction of the service subtype WebDAVService and registration of triggers, the signals the service will respond to.

//  Define the service type 'WebDAVService' as a subtype of 'HTTPService'.
TP.uri.HTTPService.defineSubtype('WebDAVService');

//  Define triggering signals and origins. In this case, we're listening for
//  'WebDAVRequest' signals coming from any origin. (NOTE the registration here
//  is an array of ordered pairs containing signal origin and signal name).
TP.uri.WebDAVService.Type.defineAttribute('triggers',
    TP.ac(TP.ac(TP.ANY, 'TP.sig.WebDAVRequest')));

//  Register the service with TIBET. This will create a (singleton) instance of
//  WebDAVService and set up listening via TIBET's signaling system.
TP.uri.WebDAVService.register();

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".

Signal origin/name pairing is common TIBET syntax for signaling of any kind. Most Service triggers use the TP.ANY value as their origin since they want to observe the entire system for their TP.sig.Request type(s).

To define multiple signals for a TP.core.Service, we might use:

TP.uri.WebDAVService.Type.defineAttribute('triggers', TP.ac(
    TP.ac(TP.ANY, 'TP.sig.WebDAVRequest'),
    TP.ac('SomeCustomOrigin', 'TP.sig.ACustomDAVRequest')
));

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.

See Handling A Request in the cookbook below for an example.

Creating A Request Type

To activate a Service you need to fire a Request.

Services are normally configured for a particular set of Request types. As a result you'll typically define a TP.sig.Request subtype to match up with your particular service target.

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

We can create a specific Request subtype 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 response 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, i.e. to get it to observe its triggers, you need to register it. This causes the TIBET signaling system to set up the signal observations.

Registration is typically done in the Service's file just after defining the services trigger signals.

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, 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 constructs, configures, and then fires a TP.sig.WebDAVRequest:

APP.MyApp.Inst.defineMethod('copyFileFromTo',
async function(srcFileName, destFileName) {
    var req;

    req = TP.sig.WebDAVRequest.construct();

    //  Tell the WebDAV service to copy from the source to the destination
    req.atPut('action', 'copy');
    req.atPut('uri', TP.uc(aFileName));             //  Source
    req.atPut('destination', TP.uc(destFileName));  //  Destination

    //  Firing this request returns a TP.sig.WebDAVResponse which, because it's
    //  a subtype of TP.sig.Response, is a 'thenable' and we can await it.
    await req.fire();

    //  OR use TIBET signaling:

    //  We can supply ourself as the 'origin' of the request. When the service
    //  is finished and the response signal is fired, we will receive that
    //  signal and our handler will run (see the 'Handling A Response' example
    //  below).
    req.fire(this);

    return this;
});

In response to the above signal the WebDAVService will encode a proper query to the server to copy the source file to the destination file. The server's response 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));


See the TIBET Signaling documentation for more information on signaling.

Handling a Request In A Service

Handling a request signal is the same as handling any signal in TIBET, define a handler using the defineHandler() method of the target service.

For example, TP.uri.WebDAVService type could define a simple handler for servicing WebDAV requests as follows.

TP.uri.WebDAVService.Inst.defineHandler('WebDAVRequest',
function(aRequest) {

    //  Call on WebDAV server...get some results...or get error...
    //  This can be async/await logic, callback logic, etc.
    ...snipped...

    //  If we got good data tell the request that we're done via 'complete()'
    //  with the result.
    if (TP.isValid(goodResult)) {
        aRequest.complete(goodResult);
        return this;
    }

    //  If we got bad data tell the request we're done via 'fail()' and
    //  the error.
    aRequest.fail(theError);

    //  Return the response. This provides the hook for 'await' or 'then'
    //  regardless of whether this method itself used await or not.
    return aRequest.getResponse();
});

NOTE: TIBET's actual implementation of its WebDAV functionality is quite a bit more sophisticated. The above code is just an example.

It's critical when implementing service request handlers that you use one of the built-in "job control" methods in TIBET to finalize the response. The typical methods you might use are complete, fail, and cancel.

You should always return the request's response object so any callers can use await, then, or asPromise on the response.

Handling a Response

As mentioned in the Creating A Service Type cookbook entry, handlers of a service's trigger signals produce TP.sig.Response objects.

Using async

The simplest way of handling a response is to use await:

response = await request.fire();

result = response.getResult();

Using Promise APIs

request.fire().then(successFunc, errorFunc);

OR

promise = request.fire().asPromise();
...

Using the Request

You can define specific response/error handlers on the Request itself.

For example, HTTP services will check the request for handlers specific to the HTTP result code. You can use this to create specific handlers for things like 404s:

TP.sig.WebDavRequest.Inst.defineHandler('404', function(signal) {
    //  ... snipped ...
});

Using the "requestor"

The first parameter to the fire call accepts a "requestor", an object that will be directly notified when the request completes.

This feature allows you to create an object which can help you organize all the handlers for a range of requests or responses from one or more services.

In the Firing A Request example we supplied an instance of 'MyApp' to the fire() call (i.e. using req.fire(this)). That allows 'MyApp' to then handle the response:

APP.MyApp.Inst.defineHandler('WebDAVResponse',
function(aSignal) {
    //  Handle the response here. The result is in "aSignal.get('result')"
    ... snipped ...
});

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.

~lib/src/tibet/kernel/TIBETJobControl.js defines the TP.core.JobStatus trait which Request/Response mix in via:

TP.sig.WorkflowSignal.addTraits(TP.core.JobStatus);

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.