1 | /**
|
2 | * Kettle Request for use with WebSockets
|
3 | *
|
4 | * Copyright 2012-2013 OCAD University
|
5 | * Copyright 2015 Raising the Floor (International)
|
6 | *
|
7 | * Licensed under the New BSD license. You may not use this file except in
|
8 | * compliance with this License.
|
9 | *
|
10 | * You may obtain a copy of the License at
|
11 | * https://github.com/fluid-project/kettle/blob/master/LICENSE.txt
|
12 | */
|
13 |
|
14 | ;
|
15 |
|
16 | var fluid = require("infusion"),
|
17 | kettle = fluid.registerNamespace("kettle");
|
18 |
|
19 | /*
|
20 | * Refinement of the `kettle.request` grade to handle WebSockets connections. Note that unlike `kettle.request.http`
|
21 | * components, these requests are long-lived and will process numerous messages whilst the WebSockets bus remains
|
22 | * connected. They participate in the Kettle middleware chain only during the process of initial negotiation up
|
23 | * from the base HTTP connection.
|
24 | */
|
25 | fluid.defaults("kettle.request.ws", {
|
26 | gradeNames: ["kettle.request"],
|
27 | members: {
|
28 | // ws: null // arrives on the `connection` message handled by the kettle.server.ws
|
29 | },
|
30 | events: {
|
31 | onBindWs: null,
|
32 | onReceiveMessage: null,
|
33 | onSendMessage: null
|
34 | },
|
35 | receiveMessageJSON: true, // deserialize all received data as JSON
|
36 | sendMessageJSON: true, // serialize all sent data as JSON
|
37 | listeners: {
|
38 | "onBindWs.ensureResponseDisposes": {
|
39 | funcName: "kettle.request.ws.ensureResponseDisposes",
|
40 | priority: "before:handleRequest"
|
41 | },
|
42 | "onBindWs.listen": {
|
43 | funcName: "kettle.request.ws.listen",
|
44 | priority: "after:ensureResponseDisposes"
|
45 | },
|
46 | "onSendMessage.encode": {
|
47 | funcName: "kettle.request.ws.sendEncode",
|
48 | args: ["{that}.options.sendMessageJSON", "{arguments}.0"],
|
49 | priority: "before:send"
|
50 | },
|
51 | "onSendMessage.send": {
|
52 | funcName: "kettle.request.ws.sendMessageImpl",
|
53 | args: ["{that}", "{arguments}.0"]
|
54 | }
|
55 | },
|
56 | invokers: {
|
57 | sendMessage: {
|
58 | funcName: "kettle.request.ws.sendMessage",
|
59 | args: ["{that}", "{arguments}.0"] // message
|
60 | },
|
61 | sendTypedMessage: {
|
62 | funcName: "kettle.request.ws.sendTypedMessage",
|
63 | args: ["{that}", "{arguments}.0", "{arguments}.1"] // type, payload
|
64 | },
|
65 | handleRequest: "{request}.handlerPromise.resolve()", // by default we simply proceed
|
66 | handleFullRequest: "kettle.request.ws.handleFullRequest"
|
67 | }
|
68 | });
|
69 |
|
70 | fluid.defaults("kettle.request.ws.mismatch", {
|
71 | gradeNames: ["kettle.request.ws", "kettle.request.mismatch"]
|
72 | });
|
73 |
|
74 | // This is handed the verifyClient callback from ws
|
75 | kettle.request.ws.handleFullRequest = function (request, fullRequestPromise, verifyCallback) {
|
76 | fullRequestPromise.then(function () {
|
77 | request.events.onRequestSuccess.fire();
|
78 | verifyCallback(true);
|
79 | }, function (err) {
|
80 | // note that these onRequestXxxx events by default have no listeners on a ws request
|
81 | request.events.onRequestError.fire(err);
|
82 | // note that this message cannot be read by the standard client, but we send it anyway. The status code can be read ok
|
83 | verifyCallback(false, err.statusCode, err.message);
|
84 | });
|
85 | };
|
86 |
|
87 | /** Begins listening for firings of the underlying ws `message` event and converts them into firings of the request
|
88 | * component's `onReceiveMessage` event with appropriate request marking.
|
89 | * @param {kettle.request.ws} that - The request component for which should start listening
|
90 | */
|
91 | kettle.request.ws.listen = function (that) {
|
92 | that.ws.on("message", function (message) {
|
93 | kettle.withRequest(that, function () {
|
94 | message = that.options.receiveMessageJSON ? kettle.JSON.parse(message) : message;
|
95 | that.events.onReceiveMessage.fire(that, message);
|
96 | })();
|
97 | });
|
98 | };
|
99 |
|
100 | /**
|
101 | * Ensure that the request object is cleared on socket disconnect/close.
|
102 | * @param {kettle.request.ws} that - The request component for which we will listen for socket closure and convert this
|
103 | * into a firing of the `onRequestEnd` event followed by destruction of the component, if this has not already occurred
|
104 | */
|
105 | kettle.request.ws.ensureResponseDisposes = function (that) {
|
106 | that.ws.on("close", kettle.withRequest(that, function () {
|
107 | if (!fluid.isDestroyed(that)) {
|
108 | that.events.onRequestEnd.fire();
|
109 | that.destroy();
|
110 | }
|
111 | }));
|
112 | };
|
113 |
|
114 | /** Utility function to encode a conditionally encode a payload as JSON depending on a flag value
|
115 | * @param {Booleanish} encode - If truthy, the supplied payloed will be encoded as JSON
|
116 | * @param {Any} data - The payload value
|
117 | * @return {Any} Either the original payload, or the payload encoded as JSON
|
118 | */
|
119 | kettle.request.ws.sendEncode = function (encode, data) {
|
120 | return encode ? JSON.stringify(data) : data;
|
121 | };
|
122 |
|
123 | /** Invokes the transform chain pseudoevent for `onSendMessage`, given an initial message value
|
124 | * @param {kettle.request.ws} that - The request component holding the pseudoevent
|
125 | * @param {Any} message - The message to be sent as initial chain argument
|
126 | * @return {Promise} The resolved value of the `onSendMessage` transform chain, indicating whether the
|
127 | * payload was successfuly sent.
|
128 | */
|
129 | kettle.request.ws.sendMessage = function (that, message) {
|
130 | var options = {}; // none currently supported
|
131 | var promise = fluid.promise.fireTransformEvent(that.events.onSendMessage, message, options);
|
132 | return promise;
|
133 | };
|
134 |
|
135 | /** The member of the `onSendMessage` transform chain responsible for actually dispatching the appropriately encoded
|
136 | * payload over the WebSockets bus.
|
137 | * @param {kettle.request.ws} that - The request component holding the WebSockets object
|
138 | * @param {Any} message - The message to be sent
|
139 | * @return {Promise} A promise for the disposition of sending the payload
|
140 | */
|
141 | kettle.request.ws.sendMessageImpl = function (that, message) {
|
142 | var promise = fluid.promise();
|
143 | that.ws.send(message, function (err) {
|
144 | promise[err ? "reject" : "resolve"](err);
|
145 | });
|
146 | return promise;
|
147 | };
|
148 |
|
149 | /** A very simple utility facilitating the sending of "typed messages" which are payloads framed by a top-level
|
150 | * String member `type` and the payload itself into the member `payload`
|
151 | * @param {kettle.request.ws} that - The request over which the message is to be sent.
|
152 | * @param {String} type - The type to be ascribed to the payload
|
153 | * @param {JSONable} payload - The payload to be sent as the message
|
154 | * @return {Promise} A promise for the disposition of the message send
|
155 | */
|
156 | kettle.request.ws.sendTypedMessage = function (that, type, payload) {
|
157 | return that.sendMessage({
|
158 | type: type,
|
159 | payload: payload
|
160 | });
|
161 | };
|