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.
Contents
Concepts
Cookbook
- Creating A Service Type
- Creating A Request Type
- Creating A Response Type
- Registering A Service
- Fire A Request
- Handling A Request In A Service
- Handling A Response
Code
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.