gpii-windows
Version:
Components of the GPII personalization infrastructure for use on Microsoft's "Windows" ™
286 lines (255 loc) • 8.39 kB
JavaScript
/* Handles the connection between the service and GPII user process.
*
* Copyright 2018 Raising the Floor - International
*
* Licensed under the New BSD license. You may not use this file except in
* compliance with this License.
*
* The R&D leading to these results received funding from the
* Department of Education - Grant H421A150005 (GPII-APCP). However,
* these results do not necessarily represent the policy of the
* Department of Education, and you should not assume endorsement by the
* Federal Government.
*
* You may obtain a copy of the License at
* https://github.com/GPII/universal/blob/master/LICENSE.txt
*/
/**
* Message protocol:
* packet := size + payload
* size := sizeof(payload) (32-bit uint)
* payload := The message.
*
* Message is a string, JSON, or buffer. Object can look like the following:
*
* requests:
* {
* request: "<request-id>",
* data: { ... }
* }
*
* responses:
* {
* response: "<request-id>",
* data: { ... }
* }
*
* error responses:
* {
* error: "<request-id>",
* data: { ... }
* }
*
*/
"use strict";
var util = require("util"),
EventEmitter = require("events");
var messaging = {};
/**
* Creates a session with the given connected pipe (or socket).
*
* This wraps a pipe, which fires a `message` event when for every JSON object received.
*
* @param {Socket} pipe The pipe.
* @param {String} sessionType [Optional] Initial text that is sent and checked by both ends to ensure both sides are
* compatible.
* @param {Function} requestCallback [Optional] Function to call when a request has been received. The function should
* return the result, or a promise resolving to the result.
* @param {Buffer} initialData [Optional] Initial data.
* @return {Session} The new session instance.
*/
messaging.createSession = function (pipe, sessionType, requestCallback, initialData) {
return new Session(pipe, sessionType, requestCallback, initialData);
};
/**
* Wraps a pipe with a session.
*
* @param {Socket} pipe The pipe.
* @param {String} sessionType [Optional] Initial text that is sent and checked by both ends to ensure both sides are
* compatible.
* @param {Function} requestCallback [Optional] Function to call when a request has been received. The function should
* return the result, or a promise resolving to the result.
* @param {Buffer} initialData [Optional] Initial data.
* @constructor
*/
function Session(pipe, sessionType, requestCallback, initialData) {
this.pipe = pipe;
this.sessionType = sessionType;
this.sessionTypeChecked = !this.sessionType;
if (this.sessionType) {
this.sendMessage(this.sessionType);
}
this.requestCallback = requestCallback;
this.promises = {};
this.buffer = null;
this.payloadLength = null;
var session = this;
// Read the initial data (if any) in the next tick, because it will emit events which aren't yet bound to.
process.nextTick(function () {
if (initialData) {
session.gotData(initialData);
}
pipe.on("data", function (data) {
session.gotData(data);
});
pipe.on("close", function (hadError) {
session.emit("close", hadError);
});
});
}
util.inherits(Session, EventEmitter);
// Number of bytes used for the message length.
messaging.lengthByteCount = 2;
messaging.Session = Session;
/**
* Sends a message to the pipe.
*
* @param {String|Object|Buffer} payload The message payload.
*/
Session.prototype.sendMessage = function (payload) {
var payloadBuf;
if (Buffer.isBuffer(payload)) {
payloadBuf = payload;
} else {
var payloadString;
if (typeof(payload) === "string") {
payloadString = payload;
} else {
payloadString = JSON.stringify(payload);
}
payloadBuf = Buffer.from(payloadString);
}
// <message> = <length> + <payload>
var message = Buffer.alloc(payloadBuf.length + messaging.lengthByteCount);
message.writeUIntBE(payloadBuf.length, 0, messaging.lengthByteCount);
payloadBuf.copy(message, messaging.lengthByteCount);
this.pipe.write(message, "utf8");
};
/**
* Handle the pipe's "data" event.
*
* Messages may not arrive in a single chunk, so the data is added to a buffer until there is enough data to process.
*
* packet := size + payload
* size := sizeof(payload) (32-bit uint)
* payload := The message.
*
* @param {Buffer} data The data.
*/
Session.prototype.gotData = function (data) {
if (data) {
this.buffer = this.buffer ? Buffer.concat([this.buffer, data]) : data;
}
if (!this.payloadLength) {
// The first bytes are the length of the payload.
if (this.buffer.length >= messaging.lengthByteCount) {
this.payloadLength = this.buffer.readUIntBE(0, messaging.lengthByteCount);
this.buffer = this.buffer.slice(messaging.lengthByteCount);
}
}
if (this.payloadLength && this.buffer.length >= this.payloadLength) {
// Got enough data for the payload.
this.gotPacket(this.buffer.toString("utf8", 0, this.payloadLength));
this.buffer = this.buffer.slice(this.payloadLength);
this.payloadLength = 0;
if (this.buffer.length > 0) {
// Still data after the payload, process it now
this.gotData(null);
}
}
};
/**
* Called when a message has been received.
*
* @param {String} packet The JSON string of the message.
*/
Session.prototype.gotPacket = function (packet) {
if (this.sessionTypeChecked) {
var message = JSON.parse(packet);
this.emit("message", message);
if (message.request) {
this.handleRequest(message);
} else if (message.response || message.error) {
this.handleReply(message);
}
} else if (packet === this.sessionType) {
this.sessionTypeChecked = true;
this.emit("ready");
} else {
this.pipe.end();
this.emit("error", new Error("Unexpected client session type " + packet));
}
};
/**
* Call the request callback when a request is received, and sends the response.
*
* @param {Object} message The request message.
*/
Session.prototype.handleRequest = function (message) {
var session = this;
var promise;
try {
var result = this.requestCallback && this.requestCallback(message);
promise = Promise.resolve(result);
} catch (e) {
promise = Promise.reject(e);
}
promise.then(function (result) {
session.sendMessage({
response: message.request,
data: result
});
}, function (err) {
var e = null;
if (err instanceof Error) {
// Error doesn't serialise (nor does it work with Object.assign)
e = {};
Object.getOwnPropertyNames(err).forEach(function (a) {
e[a] = err[a];
});
}
session.sendMessage({
error: message.request,
data: e || err
});
});
};
/**
* Resolves (or rejects) the promise for a request.
*
* @param {Object} message The response object.
*/
Session.prototype.handleReply = function (message) {
// Resolve or reject the promise that is waiting on the result.
var id = message.response || message.error;
var promise = id && this.promises[id];
if (promise) {
if (message.response) {
promise.resolve(message.data);
} else {
promise.reject(message.data);
}
delete this.promises[id];
}
};
/**
* Send a request.
*
* @param {ServiceRequest} request The request data.
* @return {Promise} Resolves when the response has been received, rejects on error.
*/
Session.prototype.sendRequest = function (request) {
var session = this;
return new Promise(function (resolve, reject) {
// Store the resolve/reject methods so they can be called when there's a response.
var requestId = Math.random().toString(36).substring(2);
session.promises[requestId] = {
resolve: resolve,
reject: reject
};
var message = Object.assign({}, request);
message.request = requestId;
session.sendMessage(message);
});
};
module.exports = messaging;