├── .npmignore
├── .gitignore
├── test
├── run.sh
├── overflow
│ ├── run.sh
│ ├── client.js
│ └── server.js
├── boundary
│ ├── run.sh
│ ├── client.js
│ └── server.js
└── mix
│ ├── client-browser.js
│ ├── client-node.js
│ ├── index.html
│ ├── run.sh
│ ├── client.js
│ └── server.js
├── lib
├── browser
│ ├── split.js
│ ├── upgrade-middleware.js
│ ├── request-middleware.js
│ └── emitter.js
├── standalone-promise.js
├── emitter.js
├── receptor.js
├── node
│ ├── connection-listener.js
│ ├── messaging.js
│ └── emitter.js
└── mock
│ └── emitter.js
├── package.json
├── LICENSE
└── README.md
/.npmignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | /node_modules/
3 | /test/
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | /node_modules/
3 | /test/client-browser-bundle.js
--------------------------------------------------------------------------------
/test/run.sh:
--------------------------------------------------------------------------------
1 | cd overflow
2 | sh run.sh
3 | cd ../boundary
4 | sh run.sh
5 | cd ../mix
6 | sh run.sh
7 | cd ../
--------------------------------------------------------------------------------
/test/overflow/run.sh:
--------------------------------------------------------------------------------
1 | rm /tmp/antena-test-node-large.sock
2 | node server.js /tmp/antena-test-node-large.sock &
3 | sleep 1
4 | node client.js /tmp/antena-test-node-large.sock
5 | wait $!
--------------------------------------------------------------------------------
/test/boundary/run.sh:
--------------------------------------------------------------------------------
1 | rm /tmp/antena-test-node-boundary.sock ; node server.js /tmp/antena-test-node-boundary.sock &
2 | sleep 1
3 | node client.js /tmp/antena-test-node-boundary.sock
4 | wait $!
--------------------------------------------------------------------------------
/test/mix/client-browser.js:
--------------------------------------------------------------------------------
1 | const Client = require("./client.js");
2 | Client("antena-traffic", "browser-session", (error) => {
3 | if (error)
4 | throw error;
5 | alert("ok");
6 | });
--------------------------------------------------------------------------------
/test/mix/client-node.js:
--------------------------------------------------------------------------------
1 | const Client = require("./client.js");
2 | Client(process.argv[2], "node-session-"+String(process.argv[2]), (error) => {
3 | if (error)
4 | throw error;
5 | console.log("ok");
6 | });
--------------------------------------------------------------------------------
/test/mix/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Antena Browser
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/lib/browser/split.js:
--------------------------------------------------------------------------------
1 | module.exports = (url, splitter) => {
2 | if (url.startsWith("http://"))
3 | url = url.substring(url.indexOf("/", 7));
4 | if (url.startsWith("/") && url.indexOf(splitter) === 1)
5 | return url.substring(splitter.length + 2);
6 | return null;
7 | };
--------------------------------------------------------------------------------
/test/mix/run.sh:
--------------------------------------------------------------------------------
1 | browserify client-browser.js > client-browser-bundle.js
2 | rm /tmp/antena-test.sock
3 | node server.js /tmp/antena-test.sock 8000 8080 &
4 | sleep 1
5 | node client-node.js /tmp/antena-test.sock
6 | node client-node.js 8000
7 | node client-node.js [::1]:8000
8 | node client-node.js 127.0.0.1:8000
9 | open http://localhost:8080/index.html
10 | sleep 1
11 | rm client-browser-bundle.js
12 | wait $!
--------------------------------------------------------------------------------
/lib/standalone-promise.js:
--------------------------------------------------------------------------------
1 |
2 | // https://tc39.github.io/ecma262/#sec-properties-of-promise-instances
3 |
4 | module.exports = () => {
5 | let reject, resolve;
6 | const promise = new Promise((closure1, closure2) => {
7 | resolve = closure1;
8 | reject = closure2;
9 | });
10 | promise._pending = true;
11 | promise._reject = reject;
12 | promise._resolve = resolve;
13 | return promise;
14 | };
15 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "antena",
3 | "description": "Isomorphic communication library for node and browsers",
4 | "version": "4.0.10",
5 | "author": {
6 | "name": "Laurent Christophe",
7 | "email": "lachrist@vub.ac.be"
8 | },
9 | "repository": "lachrist/antena",
10 | "license": "MIT",
11 | "dependencies": {
12 | "posix-socket": "0.0.11",
13 | "ws": "^6.2.0"
14 | },
15 | "keywords": [
16 | "Request",
17 | "Connect",
18 | "Http",
19 | "WebSocket",
20 | "Upgrade"
21 | ]
22 | }
23 |
--------------------------------------------------------------------------------
/test/boundary/client.js:
--------------------------------------------------------------------------------
1 | const Emitter = require("../../lib/emitter.js");
2 | const session = "overflow";
3 | Emitter(process.argv[process.argv.length - 1], session, (error, emitter) => {
4 | if (error)
5 | throw error;
6 | let state = true;
7 | emitter.onpush = (message) => {
8 | console.log("onpush", message);
9 | if (state) {
10 | state = false;
11 | if (message !== "ab") throw new Error("Wrong first message");
12 | } else {
13 | if (message !== "cd") throw new Error("Wrong second message");
14 | emitter.terminate();
15 | }
16 | };
17 | });
--------------------------------------------------------------------------------
/test/mix/client.js:
--------------------------------------------------------------------------------
1 |
2 | const Emitter = require("../../lib/emitter.js")
3 |
4 | module.exports = (address, session, callback) => {
5 | console.log(session+"...");
6 | Emitter(address, session, (error, emitter) => {
7 | if (error)
8 | return callback(error);
9 | emitter.then(callback, callback);
10 | emitter.onpush = (message) => {
11 | console.assert(message === "barbar", "Wrong post -> push");
12 | emitter.terminate();
13 | };
14 | emitter.onterminate = () => {
15 | console.assert(emitter.pull("qux") === "quxqux", "Wrong termination pull");
16 | };
17 | console.assert(emitter.pull("foo") === "foofoo", "Wrong pull");
18 | emitter.post("bar");
19 | });
20 | };
21 |
--------------------------------------------------------------------------------
/test/overflow/client.js:
--------------------------------------------------------------------------------
1 | const Emitter = require("../../lib/emitter.js");
2 | const session = "overflow";
3 | Emitter(process.argv[process.argv.length - 1], session, (error, emitter) => {
4 | if (error)
5 | throw error;
6 | emitter.then(() => {}, (error) => { throw error });
7 | console.log("pull", session);
8 | const response = emitter.pull("a".repeat(100000));
9 | if (response !== "b".repeat(100000))
10 | throw new Error("Pull mismatch");
11 | console.log("post", session);
12 | emitter.post("c".repeat(100000));
13 | emitter.onpush = (message) => {
14 | console.log("onpush", session);
15 | if (message !== "d".repeat(100000))
16 | throw new Error("Push mismatch");
17 | emitter.terminate(0);
18 | };
19 | });
--------------------------------------------------------------------------------
/test/overflow/server.js:
--------------------------------------------------------------------------------
1 |
2 | const Net = require("net");
3 | const Receptor = require("../../lib/receptor.js");
4 |
5 | const server = Net.createServer();
6 | server.listen(process.argv[process.argv.length - 1]);
7 |
8 | const receptor = Receptor();
9 | server.on("connection", receptor.ConnectionListener());
10 |
11 | receptor.onpull = (session, message, callback) => {
12 | console.log("onpull", session);
13 | if (message !== "a".repeat(100000))
14 | throw new Error("Pull missmatch");
15 | callback("b".repeat(100000));
16 | };
17 |
18 | receptor.onpost = (session, message) => {
19 | console.log("onpost", session);
20 | if (message !== "c".repeat(100000))
21 | throw new Error("Post mismatch");
22 | console.log("push", session);
23 | receptor.push(session, "d".repeat(100000));
24 | };
25 |
26 | setTimeout(() => { server.close() }, 2000);
27 |
--------------------------------------------------------------------------------
/test/boundary/server.js:
--------------------------------------------------------------------------------
1 |
2 | const Net = require("net");
3 | const Receptor = require("../../lib/receptor.js");
4 |
5 | const server = Net.createServer();
6 | server.listen(process.argv[process.argv.length - 1]);
7 |
8 | const receptor = Receptor();
9 | const listener = receptor.ConnectionListener();
10 | server.on("connection", (socket) => {
11 | listener(socket);
12 | setTimeout(() => {
13 | const buffer = Buffer.alloc(5);
14 | buffer.writeUInt32LE(6, 0);
15 | buffer.write("a", 4, 1, "utf8");
16 | socket.write(buffer);
17 | setTimeout(() => {
18 | const buffer = Buffer.alloc(7);
19 | buffer.write("b", 0, 1, "utf8");
20 | buffer.writeUInt32LE(6, 1);
21 | buffer.write("cd", 5, 2, "utf8");
22 | socket.write(buffer);
23 | }, 1000);
24 | }, 1000);
25 | });
26 |
27 | setTimeout(() => { server.close() }, 2000);
28 |
--------------------------------------------------------------------------------
/lib/emitter.js:
--------------------------------------------------------------------------------
1 | const process = (function () { return this } ()).process;
2 | // https://github.com/iliakan/detect-node
3 | if (Object.prototype.toString.call(process) === "[object process]") {
4 | const browserify_do_no_require1 = __dirname + "/mock/emitter.js";
5 | const browserify_do_no_require2 = __dirname + "/node/emitter.js";
6 | // In case antena is browserified and then fed to a node-based headless browser,
7 | // we want to use the real node require and not the shim provided by Browserify.
8 | const MockEmitter = process.mainModule.require(browserify_do_no_require1);
9 | const NodeEmitter = process.mainModule.require(browserify_do_no_require2);
10 | module.exports = (address, session, callback) => (typeof address === "object" ? MockEmitter : NodeEmitter)(address, session, callback);
11 | } else {
12 | module.exports = require("./browser/emitter.js");
13 | }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 Laurent Christophe
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/lib/browser/upgrade-middleware.js:
--------------------------------------------------------------------------------
1 |
2 | const Ws = require("ws");
3 | const Split = require("./split.js");
4 |
5 | module.exports = function (splitter = "__antena__") {
6 | const wss = new Ws.Server({noServer:true});
7 | return (request, socket, head, next) => {
8 | const session = Split(request.url, splitter);
9 | if (session) {
10 | wss.handleUpgrade(request, socket, head, (websocket) => {
11 | websocket._antena_push = push;
12 | websocket._antena_session = session;
13 | websocket._antena_receptor = this;
14 | if (this._connect(session, websocket)) {
15 | websocket.on("close", onclose);
16 | websocket.on("message", onmessage);
17 | } else {
18 | websocket.close(1002, "Already Connected");
19 | this.onerror(session, new Error("Websocket already connected"));
20 | }
21 | });
22 | } else if (next) {
23 | next();
24 | }
25 | return Boolean(session);
26 | };
27 | };
28 |
29 | function push (message) {
30 | this.send(message);
31 | }
32 |
33 | function onclose (code, reason) {
34 | this._antena_receptor._disconnect(this._antena_session, this);
35 | }
36 |
37 | function onmessage (message) {
38 | this._antena_receptor.onpost(this._antena_session, message);
39 | }
40 |
--------------------------------------------------------------------------------
/test/mix/server.js:
--------------------------------------------------------------------------------
1 |
2 | const Fs = require("fs");
3 | const Net = require("net");
4 | const Http = require("http");
5 | const Path = require("path");
6 | const Client = require("./client.js");
7 | const Receptor = require("../../lib/receptor.js");
8 |
9 | const server1 = Net.createServer();
10 | const server2 = Net.createServer();
11 | const server3 = Http.createServer();
12 |
13 | server1.listen(process.argv[2]);
14 | server2.listen(Number(process.argv[3]));
15 | server3.listen(Number(process.argv[4]));
16 |
17 | const receptor = Receptor();
18 |
19 | receptor.onpost = (session, message) => {
20 | receptor.push(session, message + message);
21 | };
22 |
23 | receptor.onpull = (session, message, callback) => {
24 | callback(message + message);
25 | };
26 |
27 | Client(receptor, "mock-session", (error) => {
28 | if (error)
29 | throw error;
30 | console.log("ok");
31 | });
32 |
33 | server1.on("connection", receptor.ConnectionListener());
34 |
35 | server2.on("connection", receptor.ConnectionListener());
36 |
37 | const request_middleware = receptor.RequestMiddleware("antena-traffic");
38 | server3.on("request", (request, response) => {
39 | if (!request_middleware(request, response)) {
40 | if (request.url === "/index.html" || request.url === "/client-browser-bundle.js") {
41 | Fs.createReadStream(Path.join(__dirname, request.url)).pipe(response);
42 | } else {
43 | response.writeHead(404);
44 | response.end();
45 | }
46 | }
47 | });
48 |
49 | const upgrade_middleware = receptor.UpgradeMiddleware("antena-traffic");
50 | server3.on("upgrade", (request, socket, head) => {
51 | if (!upgrade_middleware(request, socket, head)) {
52 | throw new Error("Upgrade request not handled");
53 | }
54 | });
55 |
56 | setTimeout(() => {
57 | server1.close(() => {
58 | console.log("server1 closed");
59 | });
60 | server2.close(() => {
61 | console.log("server2 closed");
62 | });
63 | server3.close(() => {
64 | console.log("server3 closed");
65 | });
66 | }, 4000);
67 |
--------------------------------------------------------------------------------
/lib/browser/request-middleware.js:
--------------------------------------------------------------------------------
1 |
2 | const Split = require("./split.js");
3 |
4 | module.exports = function (splitter = "__antena__") {
5 | return (request, response, next) => {
6 | const prefix = Split(request.url, splitter);
7 | if (prefix) {
8 | const token = prefix.substring(1);
9 | const session = this._authentify(token);
10 | response.sendDate = false;
11 | response.setHeader("Content-Type", "text/plain;charset=UTF-8");
12 | if (session) {
13 | if (request.method !== "GET") {
14 | request._antena_session = session;
15 | request._antena_message = "";
16 | request._antena_receptor = this;
17 | request.on("data", ondata);
18 | }
19 | if (prefix[0] === "+") {
20 | if (request.method === "GET") {
21 | this.onpull(session, "", (message) => {
22 | response.end(message, "utf8");
23 | });
24 | } else {
25 | request._antena_response = response;
26 | request.on("end", onend1);
27 | }
28 | } else if (prefix[0] === "*") {
29 | request.on("end", onend2);
30 | response.end();
31 | } else if (prefix[0] === ".") {
32 | if (!this._revoke(token))
33 | response.writeHead(400, "Fail to revoke token");
34 | response.end();
35 | } else {
36 | response.writeHead(400, "Invalid message");
37 | response.end();
38 | }
39 | } else {
40 | response.writeHead(400, "Token Revoked");
41 | response.end();
42 | }
43 | } else if (next) {
44 | next();
45 | }
46 | return Boolean(prefix);
47 | }
48 | };
49 |
50 | function ondata (data) {
51 | this._antena_message += data;
52 | }
53 |
54 | function onend1 () {
55 | this._antena_receptor.onpull(this._antena_session, this._antena_message, (message) => {
56 | this._antena_response.end(message, "utf8");
57 | });
58 | }
59 |
60 | function onend2 () {
61 | this._antena_receptor.onpost(this._antena_session, this._antena_message);
62 | }
63 |
--------------------------------------------------------------------------------
/lib/receptor.js:
--------------------------------------------------------------------------------
1 |
2 | const Crypto = require("crypto");
3 |
4 | const ConnectionListener = require("./node/connection-listener.js");
5 | const RequestMiddleware = require("./browser/request-middleware.js");
6 | const UpgradeMiddleware = require("./browser/upgrade-middleware.js");
7 |
8 | const onpost = (session, message) => {
9 | throw new Error("Post lost from "+session+": "+message);
10 | };
11 |
12 | const onpull = (session, message) => {
13 | throw new Error("Pull lost from "+session+": "+message);
14 | };
15 |
16 | module.exports = () => ({
17 | _connections: {__proto__:null},
18 | _sessions: {__proto__:null},
19 | _messagess: {__proto__:null},
20 | _connect: connect,
21 | _disconnect: disconnect,
22 | _authentify: authentify,
23 | _revoke: revoke,
24 | push,
25 | onpost,
26 | onpull,
27 | ConnectionListener,
28 | RequestMiddleware,
29 | UpgradeMiddleware,
30 | });
31 |
32 | function authentify (token) {
33 | return this._sessions[token];
34 | }
35 |
36 | function revoke (token) {
37 | const session = this._sessions[token];
38 | if (session)
39 | this._sessions[token] = undefined;
40 | return Boolean(session);
41 | }
42 |
43 | function connect (session, connection) {
44 | if (session in this._connections)
45 | return false;
46 | for (let token in this._sessions) {
47 | if (this._sessions[token] === session) {
48 | this._sessions[token] = undefined;
49 | }
50 | }
51 | let token
52 | do {
53 | token = Crypto.randomBytes(6).toString("base64").replace(/\+/g, "-").replace(/\//g, "_");
54 | } while (token in this._sessions);
55 | this._sessions[token] = session;
56 | this._connections[session] = connection;
57 | connection._antena_push(token);
58 | if (session in this._messagess) {
59 | const messages = this._messagess[session];
60 | for (let index = 0; index < messages.lenght; index++)
61 | connection._antena_push(messages[index]);
62 | delete this._messagess[session];
63 | }
64 | return true;
65 | }
66 |
67 | function disconnect (session, connection) {
68 | if (!this._connections[session])
69 | return false;
70 | if (this._connections[session] !== connection)
71 | return false;
72 | delete this._connections[session];
73 | return true;
74 | }
75 |
76 | function push (session, message) {
77 | if (session in this._connections) {
78 | this._connections[session]._antena_push(message);
79 | } else if (session in this._messagess) {
80 | this._messagess[session].push(message);
81 | } else {
82 | this._messagess[session] = [message];
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/lib/node/connection-listener.js:
--------------------------------------------------------------------------------
1 |
2 | const Messaging = require("./messaging.js");
3 |
4 | module.exports = function () {
5 | return (socket) => {
6 | socket.setNoDelay(true);
7 | Messaging(socket);
8 | socket._antena_receptor = this;
9 | socket._antena_receive = receive0;
10 | socket._antena_session = null;
11 | socket._antena_token = null;
12 | socket._antena_push = null;
13 | socket.on("close", onclose0);
14 | socket.on("end", onend);
15 | };
16 | };
17 |
18 | function onclose0 () {
19 | this.destroy(new Error("'close' before session message"));
20 | }
21 |
22 | function onerror1 (error) {
23 | this._antena_receptor._disconnect(this._antena_session, this);
24 | }
25 |
26 | function onend () {
27 | if (this.writable) {
28 | this.end();
29 | }
30 | }
31 |
32 | function onend1 () {
33 | this._antena_receptor._disconnect(this._antena_session, this);
34 | }
35 |
36 | function receive0 (message) {
37 | this.removeListener("close", onclose0);
38 | if (message[0] === "@") {
39 | const session = message.substring(1);
40 | this._antena_session = session;
41 | this._antena_push = this._antena_send;
42 | if (this._antena_receptor._connect(session, this)) {
43 | this.on("end", onend1);
44 | this.on("error", onerror1);
45 | this._antena_receive = receive1;
46 | } else {
47 | this.destroy(new Error("Already connected"));
48 | }
49 | } else if (message[0] === "$") {
50 | this._antena_token = message.substring(1);
51 | this._antena_session = this._antena_receptor._authentify(this._antena_token);
52 | if (this._antena_session) {
53 | this._antena_receive = receive2;
54 | } else {
55 | this.destroy(new Error("Invalid token"));
56 | }
57 | } else {
58 | this.destroy(new Error("Invalid session message"));
59 | }
60 | }
61 |
62 | function receive1 (message) {
63 | this.destroy(new Error("Synchronous node socket should not receive any message"));
64 | }
65 |
66 | function receive2 (message) {
67 | if (this._antena_receptor._authentify(this._antena_token) === this._antena_session) {
68 | if (message[0] === "?") {
69 | this._antena_receptor.onpull(this._antena_session, message.substring(1), (message) => {
70 | this._antena_send(message);
71 | });
72 | } else if (message[0] === "!") {
73 | this._antena_receptor.onpost(this._antena_session, message.substring(1));
74 | } else if (message === ".") {
75 | this._antena_receptor._revoke(this._antena_token);
76 | } else {
77 | this.destroy(new Error("Asynchronous node socket got an invalid message"));
78 | }
79 | } else {
80 | this.destroy(this, new Error("Asynchronous node socket used a rvoked token"));
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/lib/node/messaging.js:
--------------------------------------------------------------------------------
1 |
2 | // https://github.com/nodejs/node/issues/26607
3 | // let BUFFER = Buffer.allocUnsafe(1024);
4 | // function send (message) {
5 | // let bytelength = BUFFER.write(message, 4, "utf8") + 4;
6 | // if (bytelength > BUFFER.length - 8) {
7 | // bytelength = Buffer.byteLength(message, "utf8") + 4;
8 | // BUFFER = Buffer.allocUnsafe(bytelength + 8);
9 | // BUFFER.write(message, 4, "utf8");
10 | // }
11 | // BUFFER.writeUInt32LE(bytelength, 0);
12 | // this.write(BUFFER.slice(0, bytelength));
13 | // };
14 |
15 | module.exports = (socket) => {
16 | socket._antena_buffer = Buffer.allocUnsafe(1024);
17 | socket._antena_length = 0;
18 | socket._antena_send = send;
19 | socket.on("data", ondata);
20 | }
21 |
22 | function send (message) {
23 | const body = Buffer.from(message, "utf8");
24 | const head = Buffer.allocUnsafe(4);
25 | head.writeUInt32LE(body.length + 4, 0);
26 | this.write(head);
27 | this.write(body);
28 | }
29 |
30 | function ondata (buffer) {
31 | while (buffer.length) {
32 | // Not enough data to compute the message's bytelength
33 | if (this._antena_length + buffer.length < 4) {
34 | buffer.copy(this._antena_buffer, this._antena_length);
35 | this._antena_length += buffer.length;
36 | break;
37 | }
38 | // Optimization when the antena buffer is empty
39 | if (this._antena_length === 0) {
40 | const target = buffer.readUInt32LE(0);
41 | if (buffer.length >= target) {
42 | this._antena_receive(buffer.toString("utf8", 4, target));
43 | buffer = buffer.slice(target);
44 | continue;
45 | }
46 | }
47 | // Make sure the antena buffer has enough byte to read the message's bytelength
48 | if (this._antena_length < 4) {
49 | buffer.copy(this._antena_buffer, this._antena_length, 0, 4 - this._antena_length);
50 | buffer = buffer.slice(4 - this._antena_length);
51 | this._antena_length = 4;
52 | }
53 | // Read the message's bytelength
54 | const target = this._antena_buffer.readUInt32LE(0);
55 | // Copy the part of the input buffer that is still in the current message boundary
56 | const tocopy = Math.min(buffer.length, target - this._antena_length);
57 | if (this._antena_buffer.length < target + tocopy) {
58 | const temporary = this._antena_buffer;
59 | this._antena_buffer = Buffer.allocUnsafe(target + tocopy);
60 | temporary.copy(this._antena_buffer, 0, 0, this._antena_length);
61 | }
62 | buffer.copy(this._antena_buffer, this._antena_length, 0, tocopy);
63 | buffer = buffer.slice(tocopy);
64 | this._antena_length += tocopy;
65 | // We reached the message boundary
66 | if (this._antena_length === target) {
67 | this._antena_receive(this._antena_buffer.toString("utf8", 4, target));
68 | this._antena_length = 0;
69 | }
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/lib/mock/emitter.js:
--------------------------------------------------------------------------------
1 |
2 | const StandalonePromise = require("../standalone-promise.js")
3 |
4 | module.exports = (receptor, session, callback) => {
5 | const emitter = StandalonePromise();
6 | emitter._receptor = receptor;
7 | emitter._token = null;
8 | emitter._terminated = false;
9 | emitter._antena_push = push;
10 | emitter.session = session;
11 | emitter.pull = pull;
12 | emitter.post = post;
13 | emitter.terminate = terminate;
14 | emitter.destroy = destroy;
15 | emitter.onpush = onpush;
16 | emitter.onterminate = onterminate;
17 | if (receptor._connect(session, emitter)) {
18 | callback(null, emitter);
19 | } else {
20 | callback(new Error("Emitter already connected"));
21 | }
22 | };
23 |
24 | ////////////
25 | // Helper //
26 | ////////////
27 |
28 | const failure = (emitter, error) => {
29 | emitter._receptor._disconnect(emitter.session, emitter);
30 | emitter._pending = false;
31 | emitter._reject(error);
32 | };
33 |
34 | const success = (emitter) => {
35 | emitter.onterminate();
36 | emitter._receptor._revoke(emitter._token);
37 | emitter._pending = false;
38 | emitter._resolve(null);
39 | };
40 |
41 | const pushcb = (emitter, message) => {
42 | emitter.onpush(message);
43 | };
44 |
45 | const postcb = (emitter, message) => {
46 | emitter._receptor.onpost(emitter.session, message);
47 | };
48 |
49 | const terminatecb = (emitter) => {
50 | if (emitter._receptor._disconnect(emitter.session, emitter)) {
51 | setImmediate(success, emitter);
52 | }
53 | };
54 |
55 | ////////////////
56 | // Connection //
57 | ////////////////
58 |
59 | function push (message) {
60 | if (this._token === null) {
61 | this._token = message;
62 | } else {
63 | setImmediate(pushcb, this, message);
64 | }
65 | }
66 |
67 | /////////////
68 | // Emitter //
69 | /////////////
70 |
71 | const onterminate = () => {};
72 |
73 | const onpush = (message) => {
74 | throw new Error("Lost push message: "+message);
75 | };
76 |
77 | function pull (message) {
78 | if (this._pending) {
79 | let result = null;
80 | let done = false;
81 | this._receptor.onpull(this.session, message, (argument0) => {
82 | done = true;
83 | result = argument0;
84 | });
85 | if (done)
86 | return result;
87 | failure(this, new Error("Callback not synchronously called"));
88 | }
89 | return null;
90 | }
91 |
92 | function terminate () {
93 | if (!this._pending || this._terminated)
94 | return false;
95 | this._terminated = true;
96 | setImmediate(terminatecb, this);
97 | return true;
98 | }
99 |
100 | function post (message) {
101 | if (this._pending)
102 | setImmediate(postcb, this, message);
103 | return this._pending;
104 | };
105 |
106 | function destroy () {
107 | if (!this._pending)
108 | return false;
109 | failure(this, new Error("Emitter destroyed by the user"));
110 | return true;
111 | };
112 |
--------------------------------------------------------------------------------
/lib/browser/emitter.js:
--------------------------------------------------------------------------------
1 |
2 | const StandalonePromise = require("../standalone-promise.js")
3 |
4 | const CONNECTING = 0;
5 | const OPEN = 1;
6 | const CLOSING = 2;
7 | const CLOSED = 3;
8 |
9 | module.exports = (options = {}, session, callback) => {
10 | if (typeof options === "string")
11 | options = {splitter:options};
12 | const secure = options.secure || location.protocol === "https:" ? "s" : "";
13 | const hostname = options.hostname || location.hostname;
14 | const port = (options.port||location.port) ? ":" + (options.port||location.port) : "";
15 | const splitter = options.splitter || "__antena__";
16 | const websocket = new WebSocket("ws"+secure+"://"+hostname+port+"/"+splitter+"/"+session);
17 | websocket.onclose = (event) => {
18 | callback(new Error("Websocket closed: "+event.code+" "+event.reason));
19 | };
20 | websocket.onmessage = ({data:token}) => {
21 | const emitter = StandalonePromise();
22 | emitter._pending = true;
23 | emitter._url1 = "http"+secure+"://"+hostname+port+"/"+splitter+"/+"+token;
24 | emitter._url2 = "http"+secure+"://"+hostname+port+"/"+splitter+"/*"+token;
25 | emitter._url3 = "http"+secure+"://"+hostname+port+"/"+splitter+"/."+token;
26 | emitter._websocket = websocket;
27 | emitter.destroy = destroy;
28 | emitter.terminate = terminate;
29 | emitter.pull = pull;
30 | emitter.post = post;
31 | emitter.onpush = onpush;
32 | emitter.onterminate = onterminate;
33 | websocket._antena_emitter = emitter;
34 | websocket.onmessage = onmessage;
35 | websocket.onclose = onclose;
36 | callback(null, emitter);
37 | };
38 | };
39 |
40 | ////////////
41 | // Helper //
42 | ////////////
43 |
44 | const failure = (emitter, error) => {
45 | if (emitter._pending) {
46 | if (emitter._websocket.readyState !== OPEN)
47 | emitter._websocket.close(1001);
48 | emitter._pending = false;
49 | emitter._reject(error);
50 | }
51 | };
52 |
53 | const success = (emitter) => {
54 | if (emitter._pending) {
55 | emitter.onterminate();
56 | if (emitter._pending) {
57 | const request = new XMLHttpRequest();
58 | request.open("GET", emitter._url3, false);
59 | request.setRequestHeader("Cache-Control", "no-cache");
60 | request.setRequestHeader("User-Agent", "*");
61 | try {
62 | request.send();
63 | } catch (error) {
64 | return failure(emitter, error);
65 | }
66 | if (request.status !== 200)
67 | return failure(emitter, new Error("HTTP Error: "+request.status+" "+request.statusText));
68 | emitter._pending = false;
69 | emitter._resolve(null);
70 | }
71 | }
72 | };
73 |
74 | ///////////////
75 | // WebSocket //
76 | ///////////////
77 |
78 | function onmessage ({data}) {
79 | this._antena_emitter.onpush(data);
80 | }
81 |
82 | function onclose ({wasClean, code, reason}) {
83 | if (wasClean) {
84 | if (code === 1000) {
85 | success(this._antena_emitter)
86 | } else {
87 | failure(this._antena_emitter, new Error("Abnormal websocket closure: "+code+" "+reason));
88 | }
89 | } else {
90 | failure(this._antena_emitter, new Error("Unclean websocket closure"));
91 | }
92 | }
93 |
94 | //////////////
95 | // Emitter //
96 | //////////////
97 |
98 | const onpush = (message) => {
99 | throw new Error("Lost a push message: "+message);
100 | };
101 |
102 | const onterminate = () => {};
103 |
104 | function terminate () {
105 | if (this._websocket.readyState !== OPEN)
106 | return false;
107 | this._websocket.close(1000);
108 | return true;
109 | }
110 |
111 | function destroy () {
112 | if (!this._url1)
113 | return false;
114 | failure(this, new Error("Destroyed by the user"));
115 | return true;
116 | }
117 |
118 | function post (message) {
119 | if (this._pending) {
120 | if (this._websocket.readyState === OPEN) {
121 | this._websocket.send(message);
122 | } else {
123 | const request = new XMLHttpRequest();
124 | request.open("PUT", this._url2, true);
125 | request.setRequestHeader("User-Agent", "*");
126 | try {
127 | request.send(message);
128 | } catch (error) {
129 | failure(this, error);
130 | }
131 | }
132 | }
133 | return this._pending;
134 | }
135 |
136 | function pull (message) {
137 | if (this._pending) {
138 | const request = new XMLHttpRequest();
139 | if (message) {
140 | request.open("PUT", this._url1, false);
141 | } else {
142 | request.open("GET", this._url1, false);
143 | request.setRequestHeader("Cache-Control", "no-cache");
144 | }
145 | request.setRequestHeader("User-Agent", "*");
146 | try {
147 | request.send(message);
148 | } catch (error) {
149 | failure(this, error);
150 | return null;
151 | }
152 | if (request.status === 200)
153 | return request.responseText;
154 | failure(this, new Error("HTTP Error: "+request.status+" "+request.statusText));
155 | }
156 | return null;
157 | }
158 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Antena
2 |
3 | Antena is yet an other JavaScript communication library.
4 | Antena normalises the server-client model for node and browsers.
5 | In Antena, the server is called receptor and its clients are called emitters.
6 | Both receptors and emitters can perform push notifications but only emitters can perform synchronous pull requests.
7 | For node emitters, synchronous pull requests are implemented using [posix-socket](https://www.npmjs.com/package/posix-socket) which is much faster than using the synchronous methods of `child_process` or `fs`.
8 |
9 | ```js
10 | const AntenaReceptor = require("antena/lib/receptor");
11 | const receptor = AntenaReceptor();
12 | receptor.onpull = (session, message, callback) => {
13 | console.assert(session === "antena-rocks");
14 | callback(message+"World");
15 | };
16 | receptor.onpost = (session, message) => {
17 | console.assert(session === "antena-rocks");
18 | receptor.push(session, message+"Bar");
19 | };
20 | receptor.terminate = (session, (error) => {
21 | // Politely
22 | });
23 | // For Node Emitters
24 | const server1 = require("net").createServer(8080);
25 | const onconnection = receptor.ConnectionListener()
26 | server1.on("connection", onconnection);
27 | // For Browser Emitters
28 | const server2 = require("http").createServer(8000);
29 | const onrequest = receptor.RequestMiddleware("_ANTENA_");
30 | server2.on("request", (request, response) => {
31 | onrequest(request, response, () => {
32 | // Request not handled by antena
33 | });
34 | });
35 | const onupgrade = receptor.UpgradeMiddleware("_ANTENA_");
36 | server2.on("upgrade", (request, socket, head) => {
37 | onupgrade(request, response, () => {
38 | // Upgrade not handled by Antena
39 | });
40 | });
41 | ```
42 |
43 | ```js
44 | const AntenaEmitter = require("antena/lib/emitter");
45 | const callback = (error, emitter) => {
46 | if (error)
47 | throw error;
48 | emitter.then(() => {
49 | // The emitter closed normally.
50 | // The emitter can no longer be used.
51 | }, (error) => {
52 | // An (a)synchronous error occurred.
53 | // The emitter can no longer be used.
54 | });
55 | console.assert(emitter.pull("Hello") === "HelloWorld");
56 | emitter.push("Foo");
57 | emitter.onpush = (message) => {
58 | console.assert(messsage === "FooBar");
59 | emitter.terminate();
60 | // Emitter is still fully usable after calling terminate.
61 | };
62 | emitter.onterminate = () => {
63 | // Emitter will no longer receive push events.
64 | // Post an pull can still be performed in this callback.
65 | // After returning, the emitter will be resolved.
66 | };
67 | };
68 | // Mock Emitter
69 | AntenaEmitter(receptor, "antena-rocks", callback);
70 | // Node Emitter
71 | AntenaEmitter(8080, "antena-rocks", callback);
72 | // Browser Emitter
73 | AntenaEmitter("_ANTENA_", "antena-rocks", callback);
74 | ```
75 |
76 | ## Receptor
77 |
78 | ### `receptor = require("antena/receptor")()`
79 |
80 | Create a new receptor.
81 |
82 | * `receptor :: antena.Receptor`
83 |
84 | ### `receptor.push(session, message)`
85 |
86 | Push a message to an emitter identified by its session.
87 |
88 | * `session :: string`
89 | * `message :: string`
90 |
91 | ### `receptor.onpost = (session, message) => { ... }`
92 |
93 | Handler for `emitter.post(message)`.
94 |
95 | * `session :: string`
96 | * `message :: string`
97 |
98 | ### `receptor.onpull = (session, message, callback) => { ... }`
99 |
100 | Handler for `emitter.pull(message)`.
101 |
102 | * `receptor :: antena.Receptor`
103 | * `session :: string`
104 | * `message :: string`
105 | * `callback(message)`
106 | * `message :: String`
107 |
108 | ### `onconnection = receptor.ConnectionListener()`
109 |
110 | Create a listener for the `connection` event of a `net.Server`.
111 |
112 | * `receptor :: antena.Receptor`
113 | * `onconnection(socket)`
114 | * `socket :: net.Socket`
115 |
116 | ### `onrequest = receptor.RequestMiddleware([splitter])`
117 |
118 | Create a middleware for the `request` event of a `http(s).Server`.
119 |
120 | * `receptor :: antena.Receptor`
121 | * `splitter :: string`, default: `"__antena__"`
122 | A string used to single out the traffic from an emitter.
123 | * `handled = onrequest(request, response, [next])`
124 | * `request :: (http|https).IncomingMessage`
125 | * `response :: (http|https).ServerResponse`
126 | * `next()`:
127 | Called if the request was not handled by Antena (if defined).
128 | * `handled :: boolean`:
129 | Indicates whether the request was handled by Antena.
130 |
131 | ### `onupgrade = receptor.UpgradeMiddleware([splitter])`
132 |
133 | Create a middleware for the `upgrade` event of a `http(s).Server`.
134 |
135 | * `receptor :: antena.Receptor`
136 | * `splitter :: string`, default: `"__antena__"`
137 | A string used to single out the traffic from an emitter.
138 | * `handled = onupgrage(request, socket, head, [next])`
139 | * `request :: http.IncomingMessage`
140 | * `socket :: (net|tls).Socket`
141 | * `head :: Buffer`
142 | * `next()`:
143 | Called if the upgrade request was not handled by Antena (if defined).
144 | * `handled :: boolean`
145 | Indicate whether the upgrade request was handled by Antena.
146 |
147 | ## Emitter
148 |
149 | Emitter are [promises](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) which represents the lifetime of the connection.
150 |
151 | ### `emitter = require("antena/emitter")(address, session)`
152 |
153 | * `address :: object | string | number | antena.Receptor`:
154 | Antena will choose one of the below three mode:
155 | * If `window` is defined, Antena will perform a browser connection.
156 | The address should then be:
157 | * `object`:
158 | An option object with the following fields:
159 | * `secure :: boolean`, default: `location.protocol === "https:`
160 | * `hostname :: string`, default: `location.hostname`
161 | * `port :: number`, default: `location.port`
162 | * `splitter :: string`, default: `"__antena__"`
163 | * `string`, alias for `{splitter:address}`.
164 | * Else, if `address` is not an object, Antena will perform a node connection.
165 | The address should then be:
166 | * `string`
167 | * Port string; eg `"8080"`: alias for `"[::1]:8080"`
168 | * Local socket address; eg `/tmp/antena.sock`
169 | * IPv4 and port; eg `127.0.0.1:8080`
170 | * IPv6 and port: eg `[::1]:8080`
171 | * `number`, alias for `"[::1]:"+address`
172 | * Else, the address should be `antena.Receptor` and Antena will perform a local mock connection.
173 | * `session :: string`:
174 | All subsequent push/pull requests will be tagged with this string.
175 |
176 | ### `emitter.session :: string`
177 |
178 | ### `emitter.terminate()`
179 |
180 | Attempt to perform a graceful shutdown, the emitter is still fully usable.
181 |
182 | * `callback(error)`
183 | * `error :: Error`
184 |
185 | ### `emitter.destroy()`
186 |
187 | Immediately destroy the emitter, messages might be lost.
188 |
189 | ### `emitter.post(message)`
190 |
191 | Immediately send a message to the receptor, may throw an error.
192 |
193 | * `message :: string`
194 |
195 | ### `result = emitter.pull(query)`
196 |
197 | Perform a synchronous request to the receptor, may throw an error.
198 |
199 | * `query :: string`
200 | * `result :: string`
201 |
202 | ### `emitter.onpush = (message) => { ... }`
203 |
204 | * `message :: string`
205 |
--------------------------------------------------------------------------------
/lib/node/emitter.js:
--------------------------------------------------------------------------------
1 |
2 | const Net = require("net");
3 | const PosixSocket = require("posix-socket");
4 | const Messaging = require("./messaging.js");
5 | const StandalonePromise = require("../standalone-promise.js")
6 |
7 | let BUFFER = Buffer.from(new ArrayBuffer(1024));
8 |
9 | const noop = () => {};
10 |
11 | module.exports = (address, session, callback) => {
12 | address = convert(address);
13 | let sockfd;
14 | const socket = Net.connect(address.net);
15 | const cleanup = (error) => {
16 | if (callback) {
17 | if (error === false)
18 | error = new Error("Could not receive token before closure");
19 | socket.destroy();
20 | if (sockfd) {
21 | try {
22 | PosixSocket.close(sockfd);
23 | } catch (e) {
24 | error = new Error(error.message + " AND " + e.message);
25 | }
26 | }
27 | process.nextTick(callback, error);
28 | callback = null;
29 | }
30 | };
31 | socket.once("error", cleanup);
32 | socket.once("close", cleanup);
33 | socket.once("connect", () => {
34 | Messaging(socket);
35 | socket._antena_send("@"+session);
36 | socket._antena_receive = (token) => {
37 | try {
38 | sockfd = PosixSocket.socket(address.domain, PosixSocket.SOCK_STREAM, 0);
39 | if (address.domain !== PosixSocket.AF_LOCAL)
40 | PosixSocket.setsockopt(sockfd, PosixSocket.IPPROTO_TCP, PosixSocket.TCP_NODELAY, 1);
41 | PosixSocket.connect(sockfd, address.posix);
42 | output(sockfd, "$"+token);
43 | } catch (error) {
44 | return cleanup(error);
45 | }
46 | const emitter = StandalonePromise();
47 | emitter._sockfd = sockfd;
48 | emitter._socket = socket;
49 | emitter.onpush = onpush;
50 | emitter.onterminate = onterminate;
51 | emitter.session = session;
52 | emitter.destroy = destroy;
53 | emitter.terminate = terminate;
54 | emitter.post = post;
55 | emitter.pull = pull;
56 | socket.removeListener("error", cleanup);
57 | socket.removeListener("close", cleanup);
58 | socket._antena_emitter = emitter;
59 | socket._antena_receive = receive;
60 | socket.on("error", onerror);
61 | socket.on("close", onclose);
62 | callback(null, emitter);
63 | };
64 | });
65 | };
66 |
67 | ////////////
68 | // Helper //
69 | ////////////
70 |
71 | const convert = (address) => {
72 | if (typeof address === "number" || /^[0-9]+$/.test(address))
73 | address = "[::1]:"+address;
74 | const ipv4 = /^([0-9.]+)\:([0-9]+)$/.exec(address);
75 | if (ipv4) {
76 | return {
77 | domain: PosixSocket.AF_INET,
78 | posix: {
79 | sin_family: PosixSocket.AF_INET,
80 | sin_port: parseInt(ipv4[2]),
81 | sin_addr: ipv4[1]
82 | },
83 | net: {
84 | port: parseInt(ipv4[2]),
85 | host: ipv4[1],
86 | family: 4
87 | }
88 | };
89 | }
90 | const ipv6 = /^\[([0-9a-fA-F:]+)\]\:([0-9]+)$/.exec(address);
91 | if (ipv6) {
92 | return {
93 | domain: PosixSocket.AF_INET6,
94 | posix: {
95 | sin6_family: PosixSocket.AF_INET6,
96 | sin6_port: parseInt(ipv6[2]),
97 | sin6_flowinfo: 0,
98 | sin6_addr: ipv6[1],
99 | sin6_scope_id: 0
100 | },
101 | net: {
102 | port: parseInt(ipv6[2]),
103 | host: ipv6[1],
104 | family: 6
105 | }
106 | };
107 | }
108 | return {
109 | domain: PosixSocket.AF_LOCAL,
110 | posix: {
111 | sun_family: PosixSocket.AF_LOCAL,
112 | sun_path: address
113 | },
114 | net: {
115 | path: address
116 | }
117 | };
118 | };
119 |
120 | const output = (sockfd, message) => {
121 | let bytelength = BUFFER.write(message, 4, "utf8") + 4;
122 | if (bytelength > BUFFER.length - 8) {
123 | bytelength = Buffer.byteLength(message, "utf8") + 4;
124 | BUFFER = Buffer.from(new ArrayBuffer(bytelength + 8));
125 | BUFFER.write(message, 4, "utf8");
126 | }
127 | BUFFER.writeUInt32LE(bytelength, 0);
128 | if (PosixSocket.send(sockfd, BUFFER.buffer, bytelength, 0) < bytelength) {
129 | throw new Error("Could not send the entire message ("+bytelength+" bytes)");
130 | }
131 | };
132 |
133 | const success = (emitter) => {
134 | if (emitter._pending) {
135 | emitter.onterminate();
136 | if (emitter._pending) {
137 | try {
138 | output(emitter._sockfd, ".");
139 | PosixSocket.shutdown(emitter._sockfd, PosixSocket.SHUT_WR);
140 | if (PosixSocket.recv(emitter._sockfd, BUFFER.buffer, 1, 0) !== 0) {
141 | throw new Error("Received some data instead of a FIN packet");
142 | }
143 | } catch (error) {
144 | return failure(emitter, error);
145 | }
146 | try {
147 | PosixSocket.close(emitter._sockfd);
148 | } catch (error) {
149 | emitter._sockfd = null;
150 | return failure(emitter, error);
151 | }
152 | emitter._pending = false;
153 | emitter._resolve(null);
154 | }
155 | }
156 | };
157 |
158 | const failure = (emitter, error) => {
159 | if (emitter.pending) {
160 | if (emitter._sockfd) {
161 | try {
162 | PosixSocket.close(emitter._sockfd);
163 | } catch (caught) {
164 | error = new Error(error.message + " AND " + caught.message);
165 | }
166 | }
167 | if (!emitter._socket.destroyed)
168 | emitter._socket.destroy();
169 | emitter._pending = false;
170 | emitter._reject(error);
171 | }
172 | };
173 |
174 | ////////////////
175 | // net.Socket //
176 | ////////////////
177 |
178 | function receive (message) {
179 | this._antena_emitter.onpush(message);
180 | }
181 |
182 | function onerror (error) {
183 | failure(this._antena_emitter, error);
184 | }
185 |
186 | function onclose () {
187 | success(this._antena_emitter);
188 | }
189 |
190 | /////////////
191 | // Emitter //
192 | /////////////
193 |
194 | const onpush = (message) => {
195 | throw new Error("Lost push message: "+message);
196 | };
197 |
198 | const onterminate = () => {};
199 |
200 | function destroy () {
201 | if (!this._pending)
202 | return false;
203 | failure(this, new Error("Destroyed by the user"));
204 | return true;
205 | }
206 |
207 | function terminate () {
208 | if (!this._socket.writable)
209 | return false;
210 | this._socket.end();
211 | return true;
212 | }
213 |
214 | function post (message) {
215 | if (this._pending) {
216 | try {
217 | output(this._sockfd, "!"+message);
218 | } catch (error) {
219 | failure(this, error);
220 | }
221 | }
222 | return this._pending;
223 | }
224 |
225 | function pull (string) {
226 | if (this._pending) {
227 | try {
228 | output(this._sockfd, "?" + string);
229 | if (PosixSocket.recv(this._sockfd, BUFFER.buffer, 4, PosixSocket.MSG_WAITALL) < 4)
230 | throw new Error("Could not recv the head of the pull response (4 bytes)");
231 | const bytelength = BUFFER.readUInt32LE(0) - 4;
232 | if (bytelength > BUFFER.byteLength)
233 | BUFFER = Buffer.from(new ArrayBuffer(bytelength));
234 | if (PosixSocket.recv(this._sockfd, BUFFER.buffer, bytelength, PosixSocket.MSG_WAITALL) < bytelength)
235 | throw new Error("Could not recv the body of the pull response ("+bytelength+" bytes)");
236 | return BUFFER.toString("utf8", 0, bytelength);
237 | } catch (error) {
238 | failure(this, error);
239 | }
240 | }
241 | return null;
242 | }
243 |
--------------------------------------------------------------------------------