not stable.
psionic is a bidirectional rpc system for node, glued together with json-rpc and a promise-based workflow.
to create a websocket-server (using ws):
all these examples use
babel-node
, see the examples folder for config. using babel is optional, but it does make working with promises nicer.
// server
import psionic from 'psionic';
psionic.webSocket.createServer({ port: 3000 }, function (client) {
// you have to call 'describe' once for the client to start.
client.describe({
factor: 'doubling'
// functions can return promises if needed,
// they'll always be promises for the caller.
multiply(x) { return x * 2; }
});
});
and a corresponding client:
// client
import psionic from 'psionic';
(async function () {
let client = await psionic.webSocket.connect('ws://localhost:3000');
let doubled = await client.multiply(5);
console.log(client.factor + '5 gives ' + doubled + '! amazing!');
// doubling 5 gives 10! amazing!
})().catch(ex => console.error(ex.stack));
the client can also call describe to send state to the server.
// client
import psionic from 'psionic';
(async function () {
let client = await psionic.webSocket.connect('ws://localhost:3000', {
// using connect options, your state will be sent right as the server socket is created.
describe: { name: "joe" }
});
let result = await client.test("jdp");
if (!result) {
// you can also call describe later to send state whenever needed,
// which gives a promise to be sure it makes it to the server.
await client.describe({
name: "jdp",
welcome(x) { console.log('Server says: ' + x); }
});
}
console.log(await client.test("jdp"));
})().catch(ex => console.error(ex.stack));
corresponding server:
// server
import psionic from 'psionic';
psionic.webSocket.createServer({ port: 3000 }, function (client) {
// the 'client' objects data is sent from the client,
// it is user input and can't be trusted.
let name = client.name; // joe
client.describe({
async test(testName) {
if (client.welcome instanceof Function) {
await client.welcome('Welcome, ' + client.name);
}
return testName === client.name;
}
});
});
if you need ad-hoc message passing, you can use the event emitter:
// server
import psionic from 'psionic';
psionic.webSocket.createServer({ port: 3000 }, function (client) {
client.describe({}); // describe still has to be called.
var pings = 0;
client.events.on('pong', function (i) {
console.log('client sent a pong: ' + i);
});
setInterval(function () {
client.emit('ping', ++pings);
}, 1000);
});
and a corresponding client:
// client
import psionic from 'psionic';
(async function () {
let client = await psionic.webSocket.connect('ws://localhost:3000');
client.events.on('ping', function (i) {
console.log(i + ' pings since we connected');
// the client can trigger events on the server, as well.
client.emit('pong', i);
});
})().catch(ex => console.error(ex.stack));
So far there is no implementation with fallbacks (using say, socket.io or primus,) so each server/client pair is specific to a single protocol.
Plain Node.js net sockets.
It can be accessed using require('psionic').socket
, or require('psionic/lib/socket')
// server
import psionic from 'psionic';
// all server options are passed to net.createServer
// if a `port` is passed, net.listen will be called during construction.
var opts = { port: 9000 };
var server = psionic.socket.createServer(opts, function (client) { });
// createServer returns the underlying net server
server.listen(9000);
// client
var promise = psionic.socket.connect(
// the first argument is passed to net.connect
{ port: 9000 },
// the second argument is used to configure the psionic client.
{ describe: {} }
);
promise.then(function (client) {
// you can get the underlying socket after connecting.
// note that the client auto-reconnects, so this can change.
var underlyingSocket = client.state.socket;
});
Websockets are created using the websockets/ws library, or natively on the browser.
It can be accessed using require('psionic').webSocket
, or require('psionic/lib/websocket')
// server
import psionic from 'psionic';
// all server options are passed to the ws.Server constructor
var opts = { port: 9000 };
var server = psionic.webSocket.createServer(opts, function (client) { });
// createServer returns the underlying ws server
server.listen(9000);
// client
var promise = psionic.webSocket.connect(
// the first argument is passed to the ws WebSocket constructor
'ws://127.0.0.1:9000',
// the second argument is used to configure the psionic client.
{ describe: {} }
);
promise.then(function (client) {
// you can get the underlying WebSocket after connecting.
// note that the client auto-reconnects, so this can change.
var underlyingSocket = client.state.socket;
});
The WebSocket protocol does not use any polyfills at this time, so it's limited to IE10+. If that's not a problem, you can require('psionic')
with browserify or webpack and use psionic.webSocket.connect
.
The client object is used on both the server- and client-side to send and receive messages.
// Default client structure
client = {
events: EventEmitter,
state: EventEmitter + {
connected: true,
// callId stores the id of the previous rpc-call,
// this is incremented for every call.
callId: 0,
// describe is a reference to the functions that
// can be called from the remote. it is replaced
// by calling `client.describe`.
describe: { ... },
// @emit is called when the remote triggers an event.
"@emit": Function
// @describe is called when the remote is replacing its description.
"@describe": Function
},
// the describe function calls @describe on the remote,
// to tell it which functions are available on this client.
describe: Function,
// emit calls @emit on the remote, which triggers an event.
emit: Function
// any other functions that are added using the describe function
// on the remote are found on this object as well.
}
Client.state has a few events that can be used to detect changes in the connection.
open
- connected, but no service description has been receieveddescribe
- an updated service description has been receivedconnect
- connected, and a service description has been receivedsend
- a message is ready to be sent to the remotemessage
- a message has been received from the remoteresult:{id}
- a remote procedure call has returned a valuedisconnect
- the transport has disconnect, but might reconnectclose
- the transport is shutting down, and will not reconnect
Client.events is only triggered by calling Client.emit(name, args)
, and can have any event names.
When connecting to a server using socket.connect
or webSocket.connect
,
you can supply options (well, option..) to configure the client.
describe
- state object that is sent to the server when connecting, can be used to pass state needed for initializing the remote service.
Psionic uses JSON-RPC messages, with single line JSON. It does not support notification requests at this time, all requests must be responded to.
In order to begin communicating, both client and server must send their description. The client starts this, by calling the "@describe" function.
{"id":1,"name":"@describe","args":[{"test":"psionic!function","example":"value"}]}
This tells the server to create an rpc-function called "test", and an extra value to add to the client
object. The object can be arbitrarily nested.. When the server is ready, it responds to the message and sends its own description.
{"id":1}
{"id":5,"name":"@describe","args":[{"login":"psionic!function"}]}
The client then responds to the message, and calls its login function.
{"id":5}
{"id":2,"name":"logim","args":["username","password"]}
After the server is done processing the request, it will respond with its result:
{"id":2,"error":{"code":-32601,"message":"Function not found: logim"}}
Different error codes are sent based on the JSON-RPC spec. code
and friendlyMessage
are used from any thrown Error objects. If no friendlyMessage is found, "Unhandled error" will be used. Let's try that again..
{"id":3,"name":"login","args":["username","password"]}
There we go, everything's spelled right, now we'll get a response:
{"id":3,"result":true}
And that's it!