-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathmessage.js
162 lines (151 loc) · 4.89 KB
/
message.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
import { assert, consoleError } from "./util.js";
/**
* Abstract base implementation for protocols.
*
* Creates a very thin generic wrapper around any messaging passing system
* and allows to make request/response function calls over it.
*
* It implements:
* - The notion of paths to call a specific function on the other end
* - The notion of a response to a call, or an exception
* - async waiting for the response
*
* To expose a function, you `register()` it,
* which allows the other side to call it.
* To call a function on the other side, you use `await makeCall()`.
*
* E.g. server:
* register("/user", arg => {
* if (!name) {
* throw new Error("I don't know what you're talking about");
* }
* return "I've seen " + arg.name + " before, yes";
* })
* client:
* let result = await makeCall("/user", { name: "Fred" });
* result == "I've seen Fred before, yes";
* whereas:
* await makeCall("/user", { license: "4" });
* throws an exception with .message == "I don't know what you're talking about";
*/
export default class MessageCall {
constructor() {
/**
* Obj map ID {string} ->
*/
this._functions = {};
/**
* Obj map ID {string} -> {
* resolve {function(result)} called when the function succeeded
* reject {function(ex)} called when the function had an error or exception on the other end
* }
*/
this._callsWaiting = {};
this._lastID = 0;
// Subclasses: call `this._incomingMessage(messageStr)` when new messages arrive.
}
/**
* @param message {JSON}
*/
send(message) {
throw new Error("Implement this");
}
/**
* @param path {string} URL relative to the root, e.g. "/app/user"
* @param func {async function(arg)} will be called when the path is invoked
* arg {Object} parameter from the caller
* Whatever the function returns will be sent back to the caller.
* If the function throws, the error .message {string} and .code {string} will
* be sent to the caller as exception.
*/
register(path, func) {
assert(path && typeof(path) == "string");
assert(typeof(func) == "function");
this._functions[path] = func;
}
/**
* @param message {JSON or JSON as string}
*/
async _incomingMessage(message) {
try {
if (typeof(message) == "string") {
message = JSON.parse(message);
}
} catch (ex) {
return;
}
if (typeof(message.id) != "number" || !(typeof(message.path) == "string" || typeof(message.success) == "boolean")) {
return; // not for us
}
try {
if (typeof(message.success) == "boolean") {
// We called a function on the other side, and they responded
this._response(message);
return;
}
// The other side is calling a function here
let func = this._functions[message.path];
assert(func, "404 " + message.path + ": No such function registered");
let result = await func(message.arg);
this.send({
id: message.id,
success: true,
result: result,
});
} catch (ex) {
// Error in function called by remote side
console.error(`Error, in function on our side, called by remote side. Function ${message.path}, args ${message.arg || 'none'}, function ${this._functions[message.path]}`);
consoleError(ex);
this.send({
id: message.id,
success: false,
message: ex.message,
code: ex.code,
//exception: ex,
});
}
}
/**
* Calls a function on other side
* @param path {string} like the path component of a HTTP URL.
* E.g. "/contact/call/" or "register".
* Must match the registration on the other side exactly, including leading slash or not.
* @param arg {JSON} arguments for the function call
* @param {Promise} waits until the call returns with a result or Exception
*/
async makeCall(path, arg) {
assert(path && typeof(path) == "string");
assert(!arg || typeof(arg) == "object");
return new Promise((resolve, reject) => {
let id = this._generateID();
this._callsWaiting[id] = {
resolve: resolve,
reject: reject,
};
this.send({
id: id,
path: path,
arg: arg,
});
// message will be processed on the other side
// then they will send us a response with message.result
});
}
_response(message) {
assert(typeof(message.success) == "boolean");
let callWaiting = this._callsWaiting[message.id];
delete this._callsWaiting[message.id];
assert(callWaiting, "Got a response for call ID " + message.id + ", but we did not make such a call, or we already got the response for it");
if (message.success) {
callWaiting.resolve(message.result);
} else {
let ex = new Error();
ex.message = message.message;
ex.code = message.code;
callWaiting.reject(ex);
}
}
_generateID() {
return this._lastID++;
}
}