@jambonz/node-client-ws
v0.1.66
Published
A simple Node.js library for building websocket server applications for [jambonz](https://jambonz.org)
Downloads
676
Readme
@jambonz/node-client-ws
A simple Node.js library for building websocket server applications for jambonz
Installing
npm install --save @jambonz/node-client-ws
Alternatively, to scaffold a full websocket application use the create-jambonz-ws-app utility:
npx create-jambonz-ws-app
Usage
Overview
jambonz can interact with applications using either HTTP webhooks or a websocket connection. This library provides support for the latter method of operation (see @jambonz/node-client for the companion Node.js library to use for building applications using HTTP webhooks).
To use this library, an application must first create an http(s) server. This library exposes a function, createEndpoint
that is then invoked to attach a websocket server to that http server.
Doing so exposes a makeService
function that can then be called one or more times to create different services corresponding to different request paths (e.g. '/hello-world' would be one service that routes to one application, '/echo' would be a different service that routes to different logic).
Calling makeService
with a path returns a service client associated with that path. The client emits a session:new
event when an incoming call has been received for the specified path, providing the application with a Session
object which is then used to act on that call.
Sounds a bit involved but it is pretty simple as this sample code should make clear.
/* create an http/s server in your application however you like */
const {createServer} = require('http');
const server = createServer();
server.listen(3000);
/* require the library and call the returned function with your server */
const {createEndpoint} = require('@jambonz/node-client-ws');
const makeService = createEndpoint({server});
/* create a jambonz application listeng for requests with URL path '/hello-world' */
const svc = makeService({path: '/hello-world'});
/* listen for new calls to that service */
svc.on('session:new', (session) => {
/* the 'session' object has all of the properties of the incoming call */
console.log({session}, `new incoming call: ${session.call_sid}`);
/* set up some event handlers for this session */
session
.on('close', onClose.bind(null, session))
.on('error', onError.bind(null, session));
/* all of the jambonz verbs are available as methods on the session object
https://www.jambonz.org/docs/webhooks/overview/
*/
session
.pause({length: 1.5})
.say({text})
.pause({length: 0.5})
.hangup()
.send(); // sends the queued verbs to jambonz
});
const onClose = (session, code, reason) => {
console.log({session, code, reason}, `session ${session.call_sid} closed`);
};
const onError = (session, err) => {
console.log({err}, `session ${session.call_sid} received error`);
};
A word on actionHooks
Many jambonz verbs provide asynchronous notification of events; e.g. the gather
verb sends a notification when a speech transcript is received. When building the app using webhooks these events are sent as individual HTTP requests, but how is this handled in the case of a websocket application?
Very similarly, is the answer. You still define actionHooks and eventHooks in the same way, but now instead of a new http request you get a corresponding event emitted by the session
object.
session
.pause({length: 1.5})
.gather({
say: {text: 'Please say something and we will echo it back to you.'},
input: ['speech'],
actionHook: '/transcript',
timeout: 15
})
.send();
session.on('/transcript', (evt) => {/* respond to transcript here... */});
replying to actionHooks
When receiving actionHooks or eventHooks from jambonz it is possible to respond with a new set of verbs for jambonz to begin executing. For instance, based on the transcript returned from a gather
verb, your app may decide say something back to the user.
When working with webhooks this is accomplished by including a json payload in the 200 OK response of the HTTP webhook. In this library, we use session.reply()
instead.
If you want to response to an actionHook by sending new jambonz verbs to execute, simply do this in your event handler for the actionHook:
session
.say({text: 'goodbye'})
.hangup()
.reply();
However, even if you do not want to supply any new verbs, you must reply, e.g.
session.reply();
This is necessary so that jambonz does not block on its end of the websocket to see if you have any new verbs before it continues with execution of the current stack of verbs.
If you do not reply() to an actionHook or eventHook you may notice a 5-second delay before jambonz continues executing.
send
versus reply
There are two main ways for your webhook to send commands to jambonz:
session.send()
session.reply()
The rule of thumb is that when responding to an actionHook or an eventHook (i.e. in a session callback for an event), use reply()
, otherwise use send()
.
If within an actionHook event handler you first want to reply with a new set of verbs, then later in the same handler want to send yet another set of new verbs use
send()
for the second set. Only callreply()
once per event callback.
Multiple servers sharing a single HTTP(s) server
Typically this module is used in a single-purpose application that is a jambonz websocket application. However, perhaps you want to handle other sorts of websocket traffic, outside of jambonz, through the same server. You can do so by passing an array of "external websocket servers" to the createEndpoint
method, as show in the example below.
Simply pass an array of objects in the optional externalWss
property, where each element contains a path
and a WebSocketServer instance. When a connection is made to the path, your code will receive the connect event and the messages on that websocket and is responsible for responding.
Here is a simple example of an application that handles /foo
and /bar
in separate WebSocketServers from that used for the messaging with jambonz.
const {createServer} = require('http');
const {createEndpoint} = require('@jambonz/node-client-ws');
const server = createServer();
const { WebSocketServer } = require('ws');
// create two external websocket servers on this http server
const wssFoo = new WebSocketServer({ noServer: true });
const wssBar = new WebSocketServer({ noServer: true });
// paths /foo and /bar should come to us, node-client-ws will handle the rest
const makeService = createEndpoint({
server,
externalWss: [
{
path: '/foo',
wss: wssFoo
},
{
path: '/bar',
wss: wssBar
}
]
});
const logger = require('pino')({level: process.env.LOGLEVEL || 'info'});
const port = process.env.WS_PORT || 3000;
require('./lib/routes')({logger, makeService});
server.listen(port, () => {
logger.info(`jambonz websocket server listening at http://localhost:${port}`);
});
// Handle connections and messages for /foo WebSocket server
wssFoo.on('connection', (ws) => {
logger.info('connection to /foo');
ws.on('message', (message) => {
logger.info(`received message on /foo: ${message}`);
ws.send('foo'); // Reply with 'foo' text frame
});
});
// Handle connections and messages for /bar WebSocket server
wssBar.on('connection', (ws) => {
logger.info('connection to /bar');
ws.on('message', (message) => {
logger.info(`received message on /bar: ${message}`);
ws.send('bar'); // Reply with 'bar' text frame
});
});
API
Function: createEndpoint
This function is the main export of the library.
createEndpoint({server, port, logger})
server
{http.Server} A pre-created Node.js HTTP/S server.port
{Number} Optional, the port to listen on. If not provided it is assumed the application will calllisten
on the server objectlogger
{pinoLogger} Optional, a pino to use for logging.externalWss
{Array} Optional, an array of external WebSocket servers that you want to attach to this HTTP server (see example above)
This function returns a function makeService
which the application can use to associate different services within the websocket server to different request URL paths.
Function: makeService
Creates a jambonz service, aka application.
makeService({path})
path
{String} Identifies a request URL path for incoming connections which should route to this service.
This function returns an instance of a Client (short for Service Client).
Class: Client
This class represents a service client, or more generally a jambonz application.
Event: 'session:new'
Emitted when a new incoming call for this service has arrived.
session
{Session} a single call or session that is being controlled on the jambonz serverpath
- {String} the request URL path of the incoming HTTP request
Class: Session
This class represents a unique call in progress on the jambonz server.
Event: 'close'
Emitted when the websocket has been closed from the jambonz end.
code
{Number} the websocket close codereason
- {String} the reason, if supplied
Event: 'error'
Emitted when the underlying websocket connection has an error.
err
{Error} the websocket error
Event: action hooks
As described above, each webhook that you configure in verbs that you send to jambonz will be emitted as events
evt
{Object} event data
session.[verb](data)
All of the jambonz verbs are available as methods on the Session
class. Please review the documentation for each verb on the jambonz.org website.
Calling these methods does not immediately send that verb to jambonz. The verbs are queued and sent only when send()
or reply()
is called.
session.send({execImmediate})
execImmediate
{Boolean} When true, this instructs jambonz to begin executing these verbs immediately, flushing any verbs that were already queued or in progress. When false, the new verbs are appended to the current execution stack within jambonz. Default: true.
Sends currently queued verbs to jambonz.
session.reply()
Sends currently queued verbs to jambonz, associating it as an ack response to the last received actionHook.