Refactor the stream sender

This commit is contained in:
NI
2019-09-19 15:04:20 +08:00
parent 2736a3db9d
commit 2d47cd004c
6 changed files with 309 additions and 123 deletions

View File

@@ -187,7 +187,6 @@ class Dial {
throw e; throw e;
} }
}, },
15,
4096 - 64 // Server has a 4096 bytes receive buffer, can be no greater 4096 - 64 // Server has a 4096 bytes receive buffer, can be no greater
); );

View File

@@ -46,3 +46,32 @@ export function getRands(n, min, max) {
return r; return r;
} }
/**
* Separate given buffer to multiple ones based on input max length
*
* @param {Uint8Array} buf Buffer to separate
* @param {number} max Max length of each buffer
*
* @returns {Array<Uint8Array>} Separated buffers
*
*/
export function separateBuffer(buf, max) {
let start = 0,
result = [];
while (start < buf.length) {
let remain = buf.length - start;
if (remain <= max) {
result.push(buf.slice(start, start + remain));
return result;
}
remain = max;
result.push(buf.slice(start, start + remain));
start += remain;
}
}

146
ui/stream/common_test.js Normal file
View File

@@ -0,0 +1,146 @@
// Sshwifty - A Web SSH client
//
// Copyright (C) 2019 Rui NI <nirui@gmx.com>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
import assert from "assert";
import * as common from "./common.js";
describe("Common", () => {
it("separateBuffer", async () => {
let resultArr = [];
const expected = new Uint8Array([
0,
1,
2,
3,
4,
5,
6,
7,
8,
9,
0,
1,
2,
3,
4,
5,
6,
7,
8,
9,
0,
1,
2,
3,
4,
5,
6,
7,
8,
9,
0,
1,
2,
3,
4,
5,
6,
7,
8,
9,
0,
1,
2,
3,
4,
5,
6,
7,
8,
9,
0,
1,
2,
3,
4,
5,
6,
7,
8,
9,
0,
1,
2,
3,
4,
5,
6,
7,
8,
9,
0,
1,
2,
3,
4,
5,
6,
7,
8,
9,
0,
1,
2,
3,
4,
5,
6,
7,
8,
9,
0,
1,
2,
3,
4,
5,
6,
7,
8,
9,
0,
1,
2,
3,
4,
5,
6,
7,
8,
9
]),
sepSeg = common.separateBuffer(expected, 16);
sepSeg.forEach(d => {
resultArr.push(...d);
});
const result = new Uint8Array(resultArr);
assert.deepEqual(result, expected);
});
});

View File

@@ -17,26 +17,20 @@
import Exception from "./exception.js"; import Exception from "./exception.js";
import * as subscribe from "./subscribe.js"; import * as subscribe from "./subscribe.js";
import * as common from "./common.js";
export class Sender { export class Sender {
/** /**
* constructor * constructor
* *
* @param {function} sender Underlaying sender * @param {function} sender Underlaying sender
* @param {number} bufferDelay in ms
* *
*/ */
constructor(sender, bufferDelay, maxSegSize) { constructor(sender, maxSegSize) {
this.sender = sender; this.sender = sender;
this.delay = bufferDelay;
this.maxSegSize = maxSegSize; this.maxSegSize = maxSegSize;
this.timeout = null;
this.buffered = new Uint8Array(this.maxSegSize);
this.bufferedSize = 0;
this.subscribe = new subscribe.Subscribe(); this.subscribe = new subscribe.Subscribe();
this.sendingPoc = this.sending(); this.sendingPoc = this.sending();
this.resolves = [];
this.rejects = [];
} }
/** /**
@@ -45,9 +39,19 @@ export class Sender {
*/ */
async sending() { async sending() {
for (;;) { for (;;) {
let fetched = await this.subscribe.subscribe(); const fetched = await this.subscribe.subscribe();
await this.sender(fetched); try {
const dataSegs = common.separateBuffer(fetched.data, this.maxSegSize);
for (let i in dataSegs) {
await this.sender(dataSegs[i]);
}
fetched.resolve();
} catch (e) {
fetched.reject(e);
}
} }
} }
@@ -55,7 +59,7 @@ export class Sender {
* Clear everything * Clear everything
* *
*/ */
async clear() { close() {
if (this.timeout !== null) { if (this.timeout !== null) {
clearTimeout(this.timeout); clearTimeout(this.timeout);
this.timeout = null; this.timeout = null;
@@ -64,104 +68,10 @@ export class Sender {
this.buffered = null; this.buffered = null;
this.bufferedSize = 0; this.bufferedSize = 0;
this.subscribe.reject(new Exception("Sender has been closed", false)); this.subscribe.reject(new Exception("Sender has been cleared", false));
this.subscribe.disable();
this.sendingPoc.catch(() => {}); this.sendingPoc.catch(() => {});
this.reject(new Exception("Sending has been cancelled", true));
}
/**
* Call resolves
*
* @param {any} d Data
*/
resolve(d) {
for (let i in this.resolves) {
this.resolves[i](d);
}
this.resolves = [];
this.rejects = [];
}
/**
* Call rejects
*
* @param {any} d Data
*/
reject(d) {
for (let i in this.rejects) {
this.rejects[i](d);
}
this.resolves = [];
this.rejects = [];
}
/**
* Send buffer to the sender
*
*/
flushBuffer() {
if (this.bufferedSize <= 0) {
return;
}
if (this.timeout !== null) {
clearTimeout(this.timeout);
this.timeout = null;
}
this.resolve(true);
let d = this.buffered.slice(0, this.bufferedSize);
this.subscribe.resolve(d);
if (d.length >= this.buffered.length) {
this.buffered = new Uint8Array(this.maxSegSize);
this.bufferedSize = 0;
} else {
this.buffered = this.buffered.slice(d.length, this.buffered.length);
this.bufferedSize = 0;
}
}
/**
* Append buffer to internal data storage
*
* @param {Uint8Array} buf Buffer data
*/
appendBuffer(buf) {
let remain = this.buffered.length - this.bufferedSize;
if (remain <= 0) {
this.flushBuffer();
remain = this.buffered.length - this.bufferedSize;
}
let start = 0,
end = remain;
while (start < buf.length) {
if (end > buf.length) {
end = buf.length;
}
let d = buf.slice(start, end);
this.buffered.set(d, this.bufferedSize);
this.bufferedSize += d.length;
if (this.buffered.length >= this.bufferedSize) {
this.flushBuffer();
}
start += d.length;
end = start + (this.buffered.length - this.bufferedSize);
}
} }
/** /**
@@ -176,21 +86,12 @@ export class Sender {
* *
*/ */
send(data) { send(data) {
let self = this;
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
self.resolves.push(resolve); this.subscribe.resolve({
self.rejects.push(reject); data: data,
resolve: resolve,
this.appendBuffer(data); reject: reject
});
if (this.bufferedSize <= 0) {
return;
}
self.timeout = setTimeout(() => {
self.flushBuffer();
}, self.delay);
}); });
} }
} }

111
ui/stream/sender_test.js Normal file
View File

@@ -0,0 +1,111 @@
// Sshwifty - A Web SSH client
//
// Copyright (C) 2019 Rui NI <nirui@gmx.com>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
import assert from "assert";
import * as sender from "./sender.js";
describe("Sender", () => {
function generateTestData(size) {
let d = new Uint8Array(size);
for (let i = 0; i < d.length; i++) {
d[i] = i % 256;
}
return d;
}
it("Send", async () => {
const maxSegSize = 64;
let result = [];
let sd = new sender.Sender(rawData => {
return new Promise(resolve => {
setTimeout(() => {
for (let i in rawData) {
result.push(rawData[i]);
}
resolve();
}, 5);
});
}, maxSegSize);
let expected = generateTestData(maxSegSize * 16);
sd.send(expected);
let sendCompleted = new Promise(resolve => {
let timer = setInterval(() => {
if (result.length < expected.length) {
return;
}
clearInterval(timer);
timer = null;
resolve();
}, 100);
});
await sendCompleted;
assert.deepEqual(new Uint8Array(result), expected);
});
it("Send (Multiple calls)", async () => {
const maxSegSize = 64;
let result = [];
let sd = new sender.Sender(rawData => {
return new Promise(resolve => {
setTimeout(() => {
for (let i in rawData) {
result.push(rawData[i]);
}
resolve();
}, 10);
});
}, maxSegSize);
let expectedSingle = generateTestData(maxSegSize * 2),
expectedLen = expectedSingle.length * 16,
expected = new Uint8Array(expectedLen);
for (let i = 0; i < expectedLen; i += expectedSingle.length) {
expected.set(expectedSingle, i);
}
for (let i = 0; i < expectedLen; i += expectedSingle.length) {
setTimeout(() => {
sd.send(expectedSingle);
}, 100);
}
let sendCompleted = new Promise(resolve => {
let timer = setInterval(() => {
if (result.length < expectedLen) {
return;
}
clearInterval(timer);
timer = null;
resolve();
}, 100);
});
await sendCompleted;
assert.deepEqual(new Uint8Array(result), expected);
});
});

View File

@@ -139,7 +139,7 @@ export class Streams {
} }
try { try {
this.sender.clear(); this.sender.close();
} catch (e) { } catch (e) {
process.env.NODE_ENV === "development" && console.trace(e); process.env.NODE_ENV === "development" && console.trace(e);
} }