Initial commit
This commit is contained in:
48
ui/stream/common.js
Normal file
48
ui/stream/common.js
Normal file
@@ -0,0 +1,48 @@
|
||||
// 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/>.
|
||||
|
||||
/**
|
||||
* Get one unsafe random number
|
||||
*
|
||||
* @param {number} min Min value (included)
|
||||
* @param {number} max Max value (not included)
|
||||
*
|
||||
* @returns {number} Get random number
|
||||
*
|
||||
*/
|
||||
export function getRand(min, max) {
|
||||
return Math.floor(Math.random() * (max - min + 1) + min);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a group of random number
|
||||
*
|
||||
* @param {number} n How many number to get
|
||||
* @param {number} min Min value (included)
|
||||
* @param {number} max Max value (not included)
|
||||
*
|
||||
* @returns {Array<number>} A group of random number
|
||||
*/
|
||||
export function getRands(n, min, max) {
|
||||
let r = [];
|
||||
|
||||
for (let i = 0; i < n; i++) {
|
||||
r.push(getRand(min, max));
|
||||
}
|
||||
|
||||
return r;
|
||||
}
|
||||
40
ui/stream/exception.js
Normal file
40
ui/stream/exception.js
Normal file
@@ -0,0 +1,40 @@
|
||||
// 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/>.
|
||||
|
||||
export default class Exception {
|
||||
/**
|
||||
* constructor
|
||||
*
|
||||
* @param {string} message error message
|
||||
* @param {boolean} temporary whether or not the error is temporary
|
||||
*
|
||||
*/
|
||||
constructor(message, temporary) {
|
||||
this.message = message;
|
||||
this.temporary = temporary;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the error string
|
||||
*
|
||||
* @returns {string} Error message
|
||||
*
|
||||
*/
|
||||
toString() {
|
||||
return this.message;
|
||||
}
|
||||
}
|
||||
264
ui/stream/header.js
Normal file
264
ui/stream/header.js
Normal file
@@ -0,0 +1,264 @@
|
||||
// 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 Exception from "./exception.js";
|
||||
|
||||
export const CONTROL = 0x00;
|
||||
export const STREAM = 0x40;
|
||||
export const CLOSE = 0x80;
|
||||
export const COMPLETED = 0xc0;
|
||||
|
||||
export const CONTROL_ECHO = 0x00;
|
||||
export const CONTROL_PAUSESTREAM = 0x01;
|
||||
export const CONTROL_RESUMESTREAM = 0x02;
|
||||
|
||||
const headerHeaderCutter = 0xc0;
|
||||
const headerDataCutter = 0x3f;
|
||||
|
||||
export const HEADER_MAX_DATA = headerDataCutter;
|
||||
|
||||
export class Header {
|
||||
/**
|
||||
* constructor
|
||||
*
|
||||
* @param {number} headerByte one byte data of the header
|
||||
*/
|
||||
constructor(headerByte) {
|
||||
this.headerByte = headerByte;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the header type
|
||||
*
|
||||
* @returns {number} Type number
|
||||
*
|
||||
*/
|
||||
type() {
|
||||
return this.headerByte & headerHeaderCutter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the header data
|
||||
*
|
||||
* @returns {number} Data number
|
||||
*
|
||||
*/
|
||||
data() {
|
||||
return this.headerByte & headerDataCutter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the reader data
|
||||
*
|
||||
* @param {number} data
|
||||
*/
|
||||
set(data) {
|
||||
if (data > headerDataCutter) {
|
||||
throw new Exception("data must not be greater than 0x3f", false);
|
||||
}
|
||||
|
||||
this.headerByte |= headerDataCutter & data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the header value
|
||||
*
|
||||
* @returns {number} Header byte data
|
||||
*
|
||||
*/
|
||||
value() {
|
||||
return this.headerByte;
|
||||
}
|
||||
}
|
||||
|
||||
export const STREAM_HEADER_BYTE_LENGTH = 2;
|
||||
export const STREAM_MAX_LENGTH = 0x1fff;
|
||||
export const STREAM_MAX_MARKER = 0x07;
|
||||
|
||||
const streamHeaderLengthFirstByteCutter = 0x1f;
|
||||
|
||||
export class Stream {
|
||||
/**
|
||||
* constructor
|
||||
*
|
||||
* @param {number} headerByte1 First header byte
|
||||
* @param {number} headerByte2 Second header byte
|
||||
*
|
||||
*/
|
||||
constructor(headerByte1, headerByte2) {
|
||||
this.headerByte1 = headerByte1;
|
||||
this.headerByte2 = headerByte2;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the marker data
|
||||
*
|
||||
* @returns {number} the marker
|
||||
*
|
||||
*/
|
||||
marker() {
|
||||
return this.headerByte1 >> 5;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the stream data length
|
||||
*
|
||||
* @returns {number} Length of the stream data
|
||||
*
|
||||
*/
|
||||
length() {
|
||||
let r = 0;
|
||||
|
||||
r |= this.headerByte1 & streamHeaderLengthFirstByteCutter;
|
||||
r <<= 8;
|
||||
r |= this.headerByte2;
|
||||
|
||||
return r;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the header
|
||||
*
|
||||
* @param {number} marker Header marker
|
||||
* @param {number} length Stream data length
|
||||
*
|
||||
*/
|
||||
set(marker, length) {
|
||||
if (marker > STREAM_MAX_MARKER) {
|
||||
throw new Exception("marker must not be greater than 0x07", false);
|
||||
}
|
||||
|
||||
if (length > STREAM_MAX_LENGTH) {
|
||||
throw new Exception("n must not be greater than 0x1fff", false);
|
||||
}
|
||||
|
||||
this.headerByte1 =
|
||||
(marker << 5) | ((length >> 8) & streamHeaderLengthFirstByteCutter);
|
||||
this.headerByte2 = length & 0xff;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the header data
|
||||
*
|
||||
* @returns {Uint8Array} Header data
|
||||
*
|
||||
*/
|
||||
buffer() {
|
||||
return new Uint8Array([this.headerByte1, this.headerByte2]);
|
||||
}
|
||||
}
|
||||
|
||||
export class InitialStream extends Stream {
|
||||
/**
|
||||
* Return how large the data can be
|
||||
*
|
||||
* @returns {number} Max data size
|
||||
*
|
||||
*/
|
||||
static maxDataSize() {
|
||||
return 0x07ff;
|
||||
}
|
||||
|
||||
/**
|
||||
* constructor
|
||||
*
|
||||
* @param {number} headerByte1 First header byte
|
||||
* @param {number} headerByte2 Second header byte
|
||||
*
|
||||
*/
|
||||
constructor(headerByte1, headerByte2) {
|
||||
super(headerByte1, headerByte2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return command ID
|
||||
*
|
||||
* @returns {number} Command ID
|
||||
*
|
||||
*/
|
||||
command() {
|
||||
return this.headerByte1 >> 4;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return data
|
||||
*
|
||||
* @returns {number} Data
|
||||
*
|
||||
*/
|
||||
data() {
|
||||
let r = 0;
|
||||
|
||||
r |= this.headerByte1 & 0x07;
|
||||
r <<= 8;
|
||||
r |= this.headerByte2 & 0xff;
|
||||
|
||||
return r;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return whether or not the respond is success
|
||||
*
|
||||
* @returns {boolean} True when the request is successful, false otherwise
|
||||
*
|
||||
*/
|
||||
success() {
|
||||
return (this.headerByte1 & 0x08) != 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the header
|
||||
*
|
||||
* @param {number} commandID Command ID
|
||||
* @param {number} data Stream data
|
||||
* @param {boolean} success Whether or not the request is successful
|
||||
*
|
||||
*/
|
||||
set(commandID, data, success) {
|
||||
if (commandID > 0x0f) {
|
||||
throw new Exception("Command ID must not greater than 0x0f", false);
|
||||
}
|
||||
|
||||
if (data > InitialStream.maxDataSize()) {
|
||||
throw new Exception("Data must not greater than 0x07ff", false);
|
||||
}
|
||||
|
||||
let dd = data & InitialStream.maxDataSize();
|
||||
|
||||
if (success) {
|
||||
dd |= 0x0800;
|
||||
}
|
||||
|
||||
this.headerByte1 = 0;
|
||||
this.headerByte1 |= commandID << 4;
|
||||
this.headerByte1 |= dd >> 8;
|
||||
this.headerByte2 = 0;
|
||||
this.headerByte2 |= dd & 0xff;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a new Header
|
||||
*
|
||||
* @param {number} h Header number
|
||||
*
|
||||
* @returns {Header} The header which been built
|
||||
*
|
||||
*/
|
||||
export function header(h) {
|
||||
return new Header(h);
|
||||
}
|
||||
56
ui/stream/header_test.js
Normal file
56
ui/stream/header_test.js
Normal file
@@ -0,0 +1,56 @@
|
||||
// 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 * as header from "./header.js";
|
||||
import assert from "assert";
|
||||
|
||||
describe("Header", () => {
|
||||
it("Header", () => {
|
||||
let h = new header.Header(header.ECHO);
|
||||
|
||||
h.set(63);
|
||||
|
||||
let n = new header.Header(h.value());
|
||||
|
||||
assert.equal(h.type(), n.type());
|
||||
assert.equal(h.data(), n.data());
|
||||
assert.equal(n.type(), header.CONTROL);
|
||||
assert.equal(n.data(), 63);
|
||||
});
|
||||
|
||||
it("Stream", () => {
|
||||
let h = new header.Stream(0, 0);
|
||||
|
||||
h.set(header.STREAM_MAX_MARKER, header.STREAM_MAX_LENGTH);
|
||||
|
||||
assert.equal(h.marker(), header.STREAM_MAX_MARKER);
|
||||
assert.equal(h.length(), header.STREAM_MAX_LENGTH);
|
||||
|
||||
assert.equal(h.headerByte1, 0xff);
|
||||
assert.equal(h.headerByte2, 0xff);
|
||||
});
|
||||
|
||||
it("InitialStream", () => {
|
||||
let h = new header.InitialStream(0, 0);
|
||||
|
||||
h.set(15, 128, true);
|
||||
|
||||
assert.equal(h.command(), 15);
|
||||
assert.equal(h.data(), 128);
|
||||
assert.equal(h.success(), true);
|
||||
});
|
||||
});
|
||||
570
ui/stream/reader.js
Normal file
570
ui/stream/reader.js
Normal file
@@ -0,0 +1,570 @@
|
||||
// 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 Exception from "./exception.js";
|
||||
import * as subscribe from "./subscribe.js";
|
||||
|
||||
export class Buffer {
|
||||
/**
|
||||
* constructor
|
||||
*
|
||||
* @param {Uint8Array} buffer Array buffer
|
||||
* @param {function} depleted Callback that will be called when the buffer
|
||||
* is depleted
|
||||
*/
|
||||
constructor(buffer, depleted) {
|
||||
this.buffer = buffer;
|
||||
this.used = 0;
|
||||
this.onDepleted = depleted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the index of given byte inside current available (unused) read
|
||||
* buffer
|
||||
*
|
||||
* @param {number} byteData Target data
|
||||
* @param {number} maxLen Max search length
|
||||
*
|
||||
* @returns {number} Return number >= 0 when found, -1 when not
|
||||
*/
|
||||
searchBuffer(byteData, maxLen) {
|
||||
let searchLen = this.remains();
|
||||
|
||||
if (searchLen > maxLen) {
|
||||
searchLen = maxLen;
|
||||
}
|
||||
|
||||
for (let i = 0; i < searchLen; i++) {
|
||||
if (this.buffer[i + this.used] !== byteData) {
|
||||
continue;
|
||||
}
|
||||
|
||||
return i;
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the index of given byte inside current available (unused) read
|
||||
* buffer
|
||||
*
|
||||
* @param {number} byteData Target data
|
||||
*
|
||||
* @returns {number} Return number >= 0 when found, -1 when not
|
||||
*/
|
||||
indexOf(byteData) {
|
||||
return this.searchBuffer(byteData, this.remains());
|
||||
}
|
||||
|
||||
/**
|
||||
* Return how many bytes in the source + buffer is still available to be
|
||||
* read, return 0 when reader is depleted and thus can be ditched
|
||||
*
|
||||
* @returns {number} Remaining size
|
||||
*
|
||||
*/
|
||||
remains() {
|
||||
return this.buffer.length - this.used;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return how many bytes is still availale in the buffer.
|
||||
*
|
||||
* Note: This reader don't have renewable data source, so when buffer
|
||||
* depletes, the reader is done
|
||||
*
|
||||
* @returns {number} Remaining size
|
||||
*
|
||||
*/
|
||||
buffered() {
|
||||
return this.remains();
|
||||
}
|
||||
|
||||
/**
|
||||
* Export max n bytes from current buffer
|
||||
*
|
||||
* @param {number} n suggested max byte length, set to 0 to refresh buffer
|
||||
* if current buffer is deplated
|
||||
*
|
||||
* @returns {Uint8Array} Exported data
|
||||
*
|
||||
* @throws {Exception} When reader has been depleted
|
||||
*
|
||||
*/
|
||||
export(n) {
|
||||
let remain = this.remains();
|
||||
|
||||
if (remain <= 0) {
|
||||
throw new Exception("Reader has been depleted", false);
|
||||
}
|
||||
|
||||
if (remain > n) {
|
||||
remain = n;
|
||||
}
|
||||
|
||||
let exported = this.buffer.slice(this.used, this.used + remain);
|
||||
this.used += exported.length;
|
||||
|
||||
if (this.remains() <= 0) {
|
||||
this.onDepleted();
|
||||
}
|
||||
|
||||
return exported;
|
||||
}
|
||||
}
|
||||
|
||||
export class Multiple {
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param {function} depleted Callback will be called when all reader is
|
||||
* depleted
|
||||
*
|
||||
*/
|
||||
constructor(depleted) {
|
||||
this.reader = null;
|
||||
this.depleted = depleted;
|
||||
this.subscribe = new subscribe.Subscribe();
|
||||
this.closed = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add new reader as sub reader
|
||||
*
|
||||
* @param {Buffer} reader
|
||||
* @param {function} depleted Callback that will be called when given reader
|
||||
* is depleted
|
||||
*
|
||||
* @throws {Exception} When the reader is closed
|
||||
*
|
||||
*/
|
||||
feed(reader, depleted) {
|
||||
if (this.closed) {
|
||||
throw new Exception("Reader is closed", false);
|
||||
}
|
||||
|
||||
if (this.reader === null && this.subscribe.pendings() <= 0) {
|
||||
this.reader = {
|
||||
reader: reader,
|
||||
depleted: depleted
|
||||
};
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.subscribe.resolve({
|
||||
reader: reader,
|
||||
depleted: depleted
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the index of given byte inside current available (unused) read
|
||||
* buffer
|
||||
*
|
||||
* @param {number} byteData Target data
|
||||
* @param {number} maxLen Max search length
|
||||
*
|
||||
* @returns {number} Return number >= 0 when found, -1 when not
|
||||
*
|
||||
*/
|
||||
searchBuffer(byteData, maxLen) {
|
||||
if (this.reader === null) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return this.reader.reader.searchBuffer(byteData, maxLen);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the index of given byte inside current available (unused) read
|
||||
* buffer
|
||||
*
|
||||
* @param {number} byteData Target data
|
||||
*
|
||||
* @returns {number} Return number >= 0 when found, -1 when not
|
||||
*/
|
||||
indexOf(byteData) {
|
||||
return this.searchBuffer(byteData, this.buffered());
|
||||
}
|
||||
|
||||
/**
|
||||
* Return how many bytes still available in the buffer (How many bytes of
|
||||
* buffer is left for read before reloading from data source)
|
||||
*
|
||||
* @returns {number} How many bytes left in the current buffer
|
||||
*/
|
||||
buffered() {
|
||||
if (this.reader == null) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return this.reader.reader.buffered();
|
||||
}
|
||||
|
||||
/**
|
||||
* close current reading
|
||||
*
|
||||
*/
|
||||
close() {
|
||||
if (this.closed) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.closed = true;
|
||||
this.subscribe.reject(new Exception("Reader is closed", false));
|
||||
}
|
||||
|
||||
/**
|
||||
* Export max n bytes from current buffer
|
||||
*
|
||||
* @param {number} n suggested max byte length, set to 0 to refresh buffer
|
||||
* if current buffer is deplated
|
||||
*
|
||||
* @returns {Uint8Array} Exported data
|
||||
*
|
||||
*/
|
||||
async export(n) {
|
||||
for (;;) {
|
||||
if (this.reader !== null) {
|
||||
let exported = await this.reader.reader.export(n);
|
||||
|
||||
if (this.reader.reader.remains() <= 0) {
|
||||
this.reader.depleted();
|
||||
|
||||
this.reader = null;
|
||||
}
|
||||
|
||||
return exported;
|
||||
}
|
||||
|
||||
this.depleted(this);
|
||||
|
||||
this.reader = await this.subscribe.subscribe();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class Reader {
|
||||
/**
|
||||
* constructor
|
||||
*
|
||||
* @param {Multiple} multiple Source reader
|
||||
* @param {function} bufferConverter Function convert
|
||||
*
|
||||
*/
|
||||
constructor(multiple, bufferConverter) {
|
||||
this.multiple = multiple;
|
||||
this.buffers = new subscribe.Subscribe();
|
||||
this.bufferConverter =
|
||||
bufferConverter ||
|
||||
(d => {
|
||||
return d;
|
||||
});
|
||||
this.closed = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add buffer into current reader
|
||||
*
|
||||
* @param {Uint8Array} buffer buffer to add
|
||||
*
|
||||
* @throws {Exception} When the reader is closed
|
||||
*
|
||||
*/
|
||||
feed(buffer) {
|
||||
if (this.closed) {
|
||||
throw new Exception("Reader is closed, new data has been deined", false);
|
||||
}
|
||||
|
||||
this.buffers.resolve(buffer);
|
||||
}
|
||||
|
||||
async reader() {
|
||||
if (this.closed) {
|
||||
throw new Exception("Reader is closed, unable to read", false);
|
||||
}
|
||||
|
||||
if (this.multiple.buffered() > 0) {
|
||||
return this.multiple;
|
||||
}
|
||||
|
||||
let self = this,
|
||||
converted = await this.bufferConverter(await self.buffers.subscribe());
|
||||
|
||||
this.multiple.feed(new Buffer(converted, () => {}), () => {});
|
||||
|
||||
return this.multiple;
|
||||
}
|
||||
|
||||
/**
|
||||
* close current reading
|
||||
*
|
||||
*/
|
||||
close() {
|
||||
if (this.closed) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.closed = true;
|
||||
this.buffers.reject(
|
||||
new Exception(
|
||||
"Reader is closed, and thus " + "cannot be operated on",
|
||||
false
|
||||
)
|
||||
);
|
||||
|
||||
return this.multiple.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the index of given byte inside current available (unused) read
|
||||
* buffer
|
||||
*
|
||||
* @param {number} byteData Target data
|
||||
* @param {number} maxLen Max search length
|
||||
*
|
||||
* @returns {number} Return number >= 0 when found, -1 when not
|
||||
*/
|
||||
async searchBuffer(byteData, maxLen) {
|
||||
return (await this.reader()).searchBuffer(byteData, maxLen);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the index of given byte inside current available (unused) read
|
||||
* buffer
|
||||
*
|
||||
* @param {number} byteData Target data
|
||||
*
|
||||
* @returns {number} Return number >= 0 when found, -1 when not
|
||||
*/
|
||||
async indexOf(byteData) {
|
||||
return (await this.reader()).indexOf(byteData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return how many bytes still available in the buffer (How many bytes of
|
||||
* buffer is left for read before reloading from data source)
|
||||
*
|
||||
* @returns {number} How many bytes left in the current buffer
|
||||
*/
|
||||
async buffered() {
|
||||
return (await this.reader()).buffered();
|
||||
}
|
||||
|
||||
/**
|
||||
* Export max n bytes from current buffer
|
||||
*
|
||||
* @param {number} n suggested max byte length, set to 0 to refresh buffer
|
||||
* if current buffer is deplated
|
||||
*
|
||||
* @returns {Uint8Array} Exported data
|
||||
*
|
||||
*/
|
||||
async export(n) {
|
||||
return (await this.reader()).export(n);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read exactly one bytes from the reader
|
||||
*
|
||||
* @param {Reader} reader the source reader
|
||||
*
|
||||
* @returns {Uint8Array} Exported data
|
||||
*
|
||||
*/
|
||||
export async function readOne(reader) {
|
||||
for (;;) {
|
||||
let d = await reader.export(1);
|
||||
|
||||
if (d.length <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
return d;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read exactly n bytes from the reader
|
||||
*
|
||||
* @param {Reader} reader the source reader
|
||||
* @param {number} n length to read
|
||||
*
|
||||
* @returns {Uint8Array} Exported data
|
||||
*
|
||||
*/
|
||||
export async function readN(reader, n) {
|
||||
let readed = 0,
|
||||
result = new Uint8Array(n);
|
||||
|
||||
while (readed < n) {
|
||||
let exported = await reader.export(n - readed);
|
||||
|
||||
result.set(exported, readed);
|
||||
readed += exported.length;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export class Limited {
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param {Reader} reader the source reader
|
||||
* @param {number} maxN max bytes to read
|
||||
*
|
||||
* @returns {boolean} true when the reader is completed, false otherwise
|
||||
*
|
||||
*/
|
||||
constructor(reader, maxN) {
|
||||
this.reader = reader;
|
||||
this.remain = maxN;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate whether or not the current reader is completed
|
||||
*
|
||||
* @returns {boolean} true when the reader is completed, false otherwise
|
||||
*
|
||||
*/
|
||||
completed() {
|
||||
return this.remain <= 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the index of given byte inside current available (unused) read
|
||||
* buffer
|
||||
*
|
||||
* @param {number} byteData Target data
|
||||
* @param {number} maxLen Max search length
|
||||
*
|
||||
* @returns {number} Return number >= 0 when found, -1 when not
|
||||
*
|
||||
*/
|
||||
searchBuffer(byteData, maxLen) {
|
||||
return this.reader.searchBuffer(
|
||||
byteData,
|
||||
maxLen > this.remain ? this.remain : maxLen
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the index of given byte inside current read buffer
|
||||
*
|
||||
* @param {number} byteData Target data
|
||||
*
|
||||
* @returns {number} Return number >= 0 when found, -1 when not
|
||||
*/
|
||||
indexOf(byteData) {
|
||||
return this.reader.searchBuffer(byteData, this.remain);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return how many bytes still available to be read
|
||||
*
|
||||
* @returns {number} Remaining size
|
||||
*
|
||||
*/
|
||||
remains() {
|
||||
return this.remain;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return how many bytes still available in the buffer (How many bytes of
|
||||
* buffer is left for read before reloading from data source)
|
||||
*
|
||||
* @returns {number} Remaining size
|
||||
*
|
||||
*/
|
||||
buffered() {
|
||||
let buf = this.reader.buffered();
|
||||
|
||||
return buf > this.remain ? this.remain : buf;
|
||||
}
|
||||
|
||||
/**
|
||||
* Export max n bytes from current buffer
|
||||
*
|
||||
* @param {number} n suggested max length
|
||||
*
|
||||
* @throws {Exception} when reading already completed
|
||||
*
|
||||
* @returns {Uint8Array} Exported data
|
||||
*
|
||||
*/
|
||||
async export(n) {
|
||||
if (this.completed()) {
|
||||
throw new Exception("Reader already completed", false);
|
||||
}
|
||||
|
||||
let toRead = n > this.remain ? this.remain : n,
|
||||
exported = await this.reader.export(toRead);
|
||||
|
||||
this.remain -= exported.length;
|
||||
|
||||
return exported;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the whole Limited reader and return the result
|
||||
*
|
||||
* @param {Limited} limited the Limited reader
|
||||
*
|
||||
* @returns {Uint8Array} Exported data
|
||||
*
|
||||
*/
|
||||
export async function readCompletely(limited) {
|
||||
return await readN(limited, limited.remains());
|
||||
}
|
||||
|
||||
/**
|
||||
* Read until given byteData is reached. This function is guaranteed to spit
|
||||
* out at least one byte
|
||||
*
|
||||
* @param {Reader} indexOfReader
|
||||
* @param {number} byteData
|
||||
*/
|
||||
export async function readUntil(indexOfReader, byteData) {
|
||||
let pos = await indexOfReader.indexOf(byteData),
|
||||
buffered = await indexOfReader.buffered();
|
||||
|
||||
if (pos >= 0) {
|
||||
return {
|
||||
data: await readN(indexOfReader, pos + 1),
|
||||
found: true
|
||||
};
|
||||
}
|
||||
|
||||
if (buffered <= 0) {
|
||||
let d = await readOne(indexOfReader);
|
||||
|
||||
return {
|
||||
data: d,
|
||||
found: d[0] === byteData
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
data: await readN(indexOfReader, buffered),
|
||||
found: false
|
||||
};
|
||||
}
|
||||
220
ui/stream/reader_test.js
Normal file
220
ui/stream/reader_test.js
Normal file
@@ -0,0 +1,220 @@
|
||||
// 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 reader from "./reader.js";
|
||||
|
||||
describe("Reader", () => {
|
||||
it("Buffer", async () => {
|
||||
let buf = new reader.Buffer(
|
||||
new Uint8Array([0, 1, 2, 3, 4, 5, 6, 7]),
|
||||
() => {}
|
||||
);
|
||||
|
||||
let ex = buf.export(1);
|
||||
|
||||
assert.equal(ex.length, 1);
|
||||
assert.equal(ex[0], 0);
|
||||
assert.equal(buf.remains(), 7);
|
||||
|
||||
ex = await reader.readCompletely(buf);
|
||||
|
||||
assert.equal(ex.length, 7);
|
||||
assert.deepEqual(ex, new Uint8Array([1, 2, 3, 4, 5, 6, 7]));
|
||||
assert.equal(buf.remains(), 0);
|
||||
});
|
||||
|
||||
it("Reader", async () => {
|
||||
const maxTests = 3;
|
||||
let IntvCount = 0,
|
||||
r = new reader.Reader(new reader.Multiple(() => {}), data => {
|
||||
return data;
|
||||
}),
|
||||
expected = [
|
||||
0,
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
5,
|
||||
6,
|
||||
7,
|
||||
0,
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
5,
|
||||
6,
|
||||
7,
|
||||
0,
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
5,
|
||||
6,
|
||||
7
|
||||
],
|
||||
feedIntv = setInterval(() => {
|
||||
r.feed(Uint8Array.from(expected.slice(0, 8)));
|
||||
|
||||
IntvCount++;
|
||||
|
||||
if (IntvCount < maxTests) {
|
||||
return;
|
||||
}
|
||||
|
||||
clearInterval(feedIntv);
|
||||
}, 300);
|
||||
|
||||
let result = [];
|
||||
|
||||
while (result.length < expected.length) {
|
||||
result.push((await r.export(1))[0]);
|
||||
}
|
||||
|
||||
assert.deepEqual(result, expected);
|
||||
});
|
||||
|
||||
it("readOne", async () => {
|
||||
let r = new reader.Reader(new reader.Multiple(() => {}), data => {
|
||||
return data;
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
r.feed(Uint8Array.from([0, 1, 2, 3, 4, 5, 7]));
|
||||
}, 100);
|
||||
|
||||
let rr = await reader.readOne(r);
|
||||
|
||||
assert.deepEqual(rr, [0]);
|
||||
|
||||
rr = await reader.readOne(r);
|
||||
|
||||
assert.deepEqual(rr, [1]);
|
||||
});
|
||||
|
||||
it("readN", async () => {
|
||||
let r = new reader.Reader(new reader.Multiple(() => {}), data => {
|
||||
return data;
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
r.feed(Uint8Array.from([0, 1, 2, 3, 4, 5, 7]));
|
||||
}, 100);
|
||||
|
||||
let rr = await reader.readN(r, 3);
|
||||
|
||||
assert.deepEqual(rr, [0, 1, 2]);
|
||||
|
||||
rr = await reader.readN(r, 3);
|
||||
|
||||
assert.deepEqual(rr, [3, 4, 5]);
|
||||
});
|
||||
|
||||
it("Limited", async () => {
|
||||
const maxTests = 3;
|
||||
let IntvCount = 0,
|
||||
r = new reader.Reader(new reader.Multiple(() => {}), data => {
|
||||
return data;
|
||||
}),
|
||||
expected = [0, 1, 2, 3, 4, 5, 6, 7, 0, 1],
|
||||
limited = new reader.Limited(r, 10),
|
||||
feedIntv = setInterval(() => {
|
||||
r.feed(Uint8Array.from(expected.slice(0, 8)));
|
||||
|
||||
IntvCount++;
|
||||
|
||||
if (IntvCount < maxTests) {
|
||||
return;
|
||||
}
|
||||
|
||||
clearInterval(feedIntv);
|
||||
}, 300);
|
||||
|
||||
let result = [];
|
||||
|
||||
while (!limited.completed()) {
|
||||
result.push((await limited.export(1))[0]);
|
||||
}
|
||||
|
||||
assert.equal(limited.completed(), true);
|
||||
assert.deepEqual(result, expected);
|
||||
});
|
||||
|
||||
it("readCompletely", async () => {
|
||||
const maxTests = 3;
|
||||
let IntvCount = 0,
|
||||
r = new reader.Reader(new reader.Multiple(() => {}), data => {
|
||||
return data;
|
||||
}),
|
||||
expected = [0, 1, 2, 3, 4, 5, 6, 7, 0, 1],
|
||||
limited = new reader.Limited(r, 10),
|
||||
feedIntv = setInterval(() => {
|
||||
r.feed(Uint8Array.from(expected.slice(0, 8)));
|
||||
|
||||
IntvCount++;
|
||||
|
||||
if (IntvCount < maxTests) {
|
||||
return;
|
||||
}
|
||||
|
||||
clearInterval(feedIntv);
|
||||
}, 300);
|
||||
|
||||
let result = await reader.readCompletely(limited);
|
||||
|
||||
assert.equal(limited.completed(), true);
|
||||
assert.deepEqual(result, expected);
|
||||
});
|
||||
|
||||
it("readUntil", async () => {
|
||||
const maxTests = 3;
|
||||
let IntvCount = 0,
|
||||
r = new reader.Reader(new reader.Multiple(() => {}), data => {
|
||||
return data;
|
||||
}),
|
||||
sample = [0, 1, 2, 3, 4, 5, 6, 7, 0, 1],
|
||||
expected1 = new Uint8Array([0, 1, 2, 3, 4, 5, 6, 7]),
|
||||
expected2 = new Uint8Array([0, 1]),
|
||||
limited = new reader.Limited(r, 10),
|
||||
feedIntv = setInterval(() => {
|
||||
r.feed(Uint8Array.from(sample));
|
||||
|
||||
IntvCount++;
|
||||
|
||||
if (IntvCount < maxTests) {
|
||||
return;
|
||||
}
|
||||
|
||||
clearInterval(feedIntv);
|
||||
}, 300);
|
||||
|
||||
let result = await reader.readUntil(limited, 7);
|
||||
|
||||
assert.equal(limited.completed(), false);
|
||||
assert.deepEqual(result.data, expected1);
|
||||
assert.deepEqual(result.found, true);
|
||||
|
||||
result = await reader.readUntil(limited, 7);
|
||||
|
||||
assert.equal(limited.completed(), true);
|
||||
assert.deepEqual(result.data, expected2);
|
||||
assert.deepEqual(result.found, false);
|
||||
});
|
||||
});
|
||||
200
ui/stream/sender.js
Normal file
200
ui/stream/sender.js
Normal file
@@ -0,0 +1,200 @@
|
||||
// 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 Exception from "./exception.js";
|
||||
import * as subscribe from "./subscribe.js";
|
||||
|
||||
export class Sender {
|
||||
/**
|
||||
* constructor
|
||||
*
|
||||
* @param {function} sender Underlaying sender
|
||||
* @param {number} bufferDelay in ms
|
||||
*
|
||||
*/
|
||||
constructor(sender, bufferDelay, maxSegSize) {
|
||||
this.sender = sender;
|
||||
this.delay = bufferDelay;
|
||||
this.maxSegSize = maxSegSize;
|
||||
this.timeout = null;
|
||||
this.buffered = new Uint8Array(this.maxSegSize);
|
||||
this.bufferedSize = 0;
|
||||
this.subscribe = new subscribe.Subscribe();
|
||||
this.sendingPoc = this.sending();
|
||||
this.resolves = [];
|
||||
this.rejects = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Sender proc
|
||||
*
|
||||
*/
|
||||
async sending() {
|
||||
for (;;) {
|
||||
let fetched = await this.subscribe.subscribe();
|
||||
|
||||
await this.sender(fetched);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear everything
|
||||
*
|
||||
*/
|
||||
async clear() {
|
||||
if (this.timeout !== null) {
|
||||
clearTimeout(this.timeout);
|
||||
this.timeout = null;
|
||||
}
|
||||
|
||||
this.buffered = null;
|
||||
this.bufferedSize = 0;
|
||||
|
||||
this.subscribe.reject(new Exception("Sender has been closed", false));
|
||||
|
||||
try {
|
||||
await this.sendingPoc;
|
||||
} catch (e) {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send data
|
||||
*
|
||||
* @param {Uint8Array} data data to send
|
||||
*
|
||||
* @throws {Exception} when sending has been cancelled
|
||||
*
|
||||
* @returns {Promise} will be resolved when the data is send and will be
|
||||
* rejected when the data is not
|
||||
*
|
||||
*/
|
||||
send(data) {
|
||||
let self = this;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
self.resolves.push(resolve);
|
||||
self.rejects.push(reject);
|
||||
|
||||
this.appendBuffer(data);
|
||||
|
||||
if (this.bufferedSize <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
self.timeout = setTimeout(() => {
|
||||
self.flushBuffer();
|
||||
}, self.delay);
|
||||
});
|
||||
}
|
||||
}
|
||||
325
ui/stream/stream.js
Normal file
325
ui/stream/stream.js
Normal file
@@ -0,0 +1,325 @@
|
||||
// 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 Exception from "./exception.js";
|
||||
import * as header from "./header.js";
|
||||
import * as reader from "./reader.js";
|
||||
import * as sender from "./sender.js";
|
||||
|
||||
export class Sender {
|
||||
/**
|
||||
* constructor
|
||||
*
|
||||
* @param {number} id ID of the stream
|
||||
* @param {sender.Sender} sd The data sender
|
||||
*
|
||||
*/
|
||||
constructor(id, sd) {
|
||||
this.id = id;
|
||||
this.sender = sd;
|
||||
this.closed = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends data to remote
|
||||
*
|
||||
* @param {number} marker binary marker
|
||||
* @param {Uint8Array} data data to be sent
|
||||
*
|
||||
* @throws {Exception} When the sender already been closed
|
||||
*
|
||||
*/
|
||||
send(marker, data) {
|
||||
if (this.closed) {
|
||||
throw new Exception(
|
||||
"Sender already been closed. No data can be send",
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
let reqHeader = new header.Header(header.STREAM),
|
||||
stHeader = new header.Stream(0, 0),
|
||||
d = new Uint8Array(data.length + 3);
|
||||
|
||||
reqHeader.set(this.id);
|
||||
stHeader.set(marker, data.length);
|
||||
|
||||
d[0] = reqHeader.value();
|
||||
d.set(stHeader.buffer(), 1);
|
||||
d.set(data, 3);
|
||||
|
||||
return this.sender.send(d);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send stream signals
|
||||
*
|
||||
* @param {number} signal Signal value
|
||||
*
|
||||
* @throws {Exception} When the sender already been closed
|
||||
*
|
||||
*/
|
||||
signal(signal) {
|
||||
if (this.closed) {
|
||||
throw new Exception(
|
||||
"Sender already been closed. No signal can be send",
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
let reqHeader = new header.Header(signal);
|
||||
|
||||
reqHeader.set(this.id);
|
||||
|
||||
return this.sender.send(new Uint8Array([reqHeader.value()]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Send close signal and close current sender
|
||||
*
|
||||
*/
|
||||
close() {
|
||||
if (this.closed) {
|
||||
return;
|
||||
}
|
||||
|
||||
let r = this.signal(header.CLOSE);
|
||||
|
||||
this.closed = true;
|
||||
|
||||
return r;
|
||||
}
|
||||
}
|
||||
|
||||
export class InitialSender {
|
||||
/**
|
||||
* constructor
|
||||
*
|
||||
* @param {number} id ID of the stream
|
||||
* @param {number} commandID ID of the command
|
||||
* @param {sender.Sender} sd The data sender
|
||||
*
|
||||
*/
|
||||
constructor(id, commandID, sd) {
|
||||
this.id = id;
|
||||
this.command = commandID;
|
||||
this.sender = sd;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return how large the data can be
|
||||
*
|
||||
* @returns {number} Max data size
|
||||
*
|
||||
*/
|
||||
static maxDataLength() {
|
||||
return header.InitialStream.maxDataSize();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends data to remote
|
||||
*
|
||||
* @param {Uint8Array} data data to be sent
|
||||
*
|
||||
*/
|
||||
send(data) {
|
||||
let reqHeader = new header.Header(header.STREAM),
|
||||
stHeader = new header.InitialStream(0, 0),
|
||||
d = new Uint8Array(data.length + 3);
|
||||
|
||||
reqHeader.set(this.id);
|
||||
stHeader.set(this.command, data.length, true);
|
||||
|
||||
d[0] = reqHeader.value();
|
||||
d.set(stHeader.buffer(), 1);
|
||||
d.set(data, 3);
|
||||
|
||||
return this.sender.send(d);
|
||||
}
|
||||
}
|
||||
|
||||
export class Stream {
|
||||
/**
|
||||
* constructor
|
||||
*
|
||||
* @param {number} id ID of the stream
|
||||
*
|
||||
*/
|
||||
constructor(id) {
|
||||
this.id = id;
|
||||
this.command = null;
|
||||
this.isInitializing = false;
|
||||
this.isShuttingDown = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether or not current stream is running
|
||||
*
|
||||
* @returns {boolean} True when it's running, false otherwise
|
||||
*
|
||||
*/
|
||||
running() {
|
||||
return this.command !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether or not current stream is initializing
|
||||
*
|
||||
* @returns {boolean} True when it's initializing, false otherwise
|
||||
*
|
||||
*/
|
||||
initializing() {
|
||||
return this.isInitializing;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsets current stream
|
||||
*
|
||||
*/
|
||||
clear() {
|
||||
this.command = null;
|
||||
this.isInitializing = false;
|
||||
this.isShuttingDown = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Request the stream for a new command
|
||||
*
|
||||
* @param {number} commandID Command ID
|
||||
* @param {function} commandBuilder Function that returns a command
|
||||
* @param {sender.Sender} sd Data sender
|
||||
*
|
||||
* @throws {Exception} when stream already running
|
||||
*
|
||||
*/
|
||||
run(commandID, commandBuilder, sd) {
|
||||
if (this.running()) {
|
||||
throw new Exception(
|
||||
"Stream already running, cannot accept new commands",
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
this.isInitializing = true;
|
||||
this.command = commandBuilder(new Sender(this.id, sd));
|
||||
|
||||
return this.command.run(new InitialSender(this.id, commandID, sd));
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when initialization respond has been received
|
||||
*
|
||||
* @param {header.InitialStream} streamInitialHeader Stream Initial header
|
||||
*
|
||||
* @throws {Exception} When the stream is not running, or been shutting down
|
||||
*
|
||||
*/
|
||||
initialize(hd) {
|
||||
if (!this.running()) {
|
||||
throw new Exception(
|
||||
"Cannot initialize a stream that is not running",
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
if (this.isShuttingDown) {
|
||||
throw new Exception(
|
||||
"Cannot initialize a stream that is about to shutdown",
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
this.command.initialize(hd);
|
||||
|
||||
if (!hd.success()) {
|
||||
this.clear();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.isInitializing = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when Stream data has been received
|
||||
*
|
||||
* @param {header.Stream} streamHeader Stream header
|
||||
* @param {reader.Limited} rd Data reader
|
||||
*
|
||||
* @throws {Exception} When the stream is not running, or shutting down
|
||||
*
|
||||
*/
|
||||
tick(streamHeader, rd) {
|
||||
if (!this.running()) {
|
||||
throw new Exception("Cannot tick a stream that is not running", false);
|
||||
}
|
||||
|
||||
if (this.isShuttingDown) {
|
||||
throw new Exception(
|
||||
"Cannot tick a stream that is about to shutdown",
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
return this.command.tick(streamHeader, rd);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when stream close request has been received
|
||||
*
|
||||
* @throws {Exception} When the stream is not running, or shutting down
|
||||
*
|
||||
*/
|
||||
close() {
|
||||
if (!this.running()) {
|
||||
throw new Exception("Cannot close a stream that is not running", false);
|
||||
}
|
||||
|
||||
if (this.isShuttingDown) {
|
||||
throw new Exception(
|
||||
"Cannot close a stream that is about to shutdown",
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
this.isShuttingDown = true;
|
||||
this.command.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when stream completed respond has been received
|
||||
*
|
||||
* @throws {Exception} When stream isn't running, or not shutting down
|
||||
*
|
||||
*/
|
||||
completed() {
|
||||
if (!this.running()) {
|
||||
throw new Exception("Cannot close a stream that is not running", false);
|
||||
}
|
||||
|
||||
if (!this.isShuttingDown) {
|
||||
throw new Exception(
|
||||
"Can't complete current stream because Close " +
|
||||
"signal is not received",
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
this.command.completed();
|
||||
this.clear();
|
||||
}
|
||||
}
|
||||
436
ui/stream/streams.js
Normal file
436
ui/stream/streams.js
Normal file
@@ -0,0 +1,436 @@
|
||||
// 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 Exception from "./exception.js";
|
||||
import * as header from "./header.js";
|
||||
import * as stream from "./stream.js";
|
||||
import * as reader from "./reader.js";
|
||||
import * as sender from "./sender.js";
|
||||
import * as common from "./common.js";
|
||||
|
||||
export const ECHO_FAILED = -1;
|
||||
|
||||
export class Requested {
|
||||
/**
|
||||
* constructor
|
||||
*
|
||||
* @param {stream.Stream} stream The selected stream
|
||||
* @param {any} result Result of the run
|
||||
*
|
||||
*/
|
||||
constructor(stream, result) {
|
||||
this.stream = stream;
|
||||
this.result = result;
|
||||
}
|
||||
}
|
||||
|
||||
export class Streams {
|
||||
/**
|
||||
* constructor
|
||||
*
|
||||
* @param {reader.Reader} reader The data reader
|
||||
* @param {sender.Sender} sender The data sender
|
||||
* @param {object} config Configuration
|
||||
*/
|
||||
constructor(reader, sender, config) {
|
||||
this.reader = reader;
|
||||
this.sender = sender;
|
||||
this.config = config;
|
||||
this.echoTimer = null;
|
||||
this.lastEchoTime = null;
|
||||
this.lastEchoData = null;
|
||||
this.stop = false;
|
||||
|
||||
this.streams = [];
|
||||
|
||||
for (let i = 0; i <= header.HEADER_MAX_DATA; i++) {
|
||||
this.streams.push(new stream.Stream(i));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts stream proccessing
|
||||
*
|
||||
* @returns {Promise<true>} When service is completed
|
||||
*
|
||||
* @throws {Exception} When the process already started
|
||||
*
|
||||
*/
|
||||
async serve() {
|
||||
if (this.echoTimer !== null) {
|
||||
throw new Exception("Already started", false);
|
||||
}
|
||||
|
||||
this.echoTimer = setInterval(() => {
|
||||
this.sendEcho();
|
||||
}, this.config.echoInterval);
|
||||
|
||||
this.stop = false;
|
||||
|
||||
this.sendEcho();
|
||||
|
||||
let ee = null;
|
||||
|
||||
while (!this.stop && ee === null) {
|
||||
try {
|
||||
await this.tick();
|
||||
} catch (e) {
|
||||
if (!e.temporary) {
|
||||
ee = e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.clear(ee);
|
||||
|
||||
if (ee !== null) {
|
||||
throw new Exception("Streams is closed: " + ee, false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear current proccess
|
||||
*
|
||||
* @param {Exception} e An error caused this clear. Null when no error
|
||||
*
|
||||
*/
|
||||
clear(e) {
|
||||
if (this.stop) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.stop = true;
|
||||
|
||||
if (this.echoTimer != null) {
|
||||
clearInterval(this.echoTimer);
|
||||
this.echoTimer = null;
|
||||
}
|
||||
|
||||
for (let i in this.streams) {
|
||||
if (!this.streams[i].running()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
this.streams[i].close();
|
||||
} catch (e) {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
try {
|
||||
this.streams[i].completed();
|
||||
} catch (e) {
|
||||
//Do nothing
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
this.sender.clear();
|
||||
} catch (e) {
|
||||
process.env.NODE_ENV === "development" && console.trace(e);
|
||||
}
|
||||
|
||||
try {
|
||||
this.reader.close();
|
||||
} catch (e) {
|
||||
process.env.NODE_ENV === "development" && console.trace(e);
|
||||
}
|
||||
|
||||
this.config.cleared(e);
|
||||
}
|
||||
|
||||
/**
|
||||
* Request remote to pause stream sending
|
||||
*
|
||||
*/
|
||||
pause() {
|
||||
let pauseHeader = header.header(header.CONTROL);
|
||||
|
||||
pauseHeader.set(1);
|
||||
|
||||
return this.sender.send(
|
||||
new Uint8Array([pauseHeader.value(), header.CONTROL_PAUSESTREAM])
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Request remote to resume stream sending
|
||||
*
|
||||
*/
|
||||
resume() {
|
||||
let pauseHeader = header.header(header.CONTROL);
|
||||
|
||||
pauseHeader.set(1);
|
||||
|
||||
return this.sender.send(
|
||||
new Uint8Array([pauseHeader.value(), header.CONTROL_RESUMESTREAM])
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Request stream for given command
|
||||
*
|
||||
* @param {number} commandID Command ID
|
||||
* @param {function} commandBuilder Command builder
|
||||
*
|
||||
* @returns {Requested} The result of the stream command
|
||||
*
|
||||
*/
|
||||
request(commandID, commandBuilder) {
|
||||
try {
|
||||
for (let i in this.streams) {
|
||||
if (this.streams[i].running()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
return new Requested(
|
||||
this.streams[i],
|
||||
this.streams[i].run(commandID, commandBuilder, this.sender)
|
||||
);
|
||||
}
|
||||
|
||||
throw new Exception("No stream is currently available", true);
|
||||
} catch (e) {
|
||||
throw new Exception("Stream request has failed: " + e, true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send echo request
|
||||
*
|
||||
*/
|
||||
sendEcho() {
|
||||
let echoHeader = header.header(header.CONTROL),
|
||||
randomNum = new Uint8Array(common.getRands(8, 0, 255));
|
||||
|
||||
echoHeader.set(randomNum.length - 1);
|
||||
|
||||
randomNum[0] = echoHeader.value();
|
||||
randomNum[1] = header.CONTROL_ECHO;
|
||||
|
||||
this.sender.send(randomNum).then(() => {
|
||||
if (this.lastEchoTime !== null || this.lastEchoData !== null) {
|
||||
this.lastEchoTime = null;
|
||||
this.lastEchoData = null;
|
||||
|
||||
this.config.echoUpdater(ECHO_FAILED);
|
||||
}
|
||||
|
||||
this.lastEchoTime = new Date();
|
||||
this.lastEchoData = randomNum.slice(2, randomNum.length);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* handle received control request
|
||||
*
|
||||
* @param {reader.Reader} rd The reader
|
||||
*
|
||||
*/
|
||||
async handleControl(rd) {
|
||||
let controlType = await reader.readOne(rd),
|
||||
delay = 0,
|
||||
echoBytes = null;
|
||||
|
||||
switch (controlType[0]) {
|
||||
case header.CONTROL_ECHO:
|
||||
echoBytes = await reader.readCompletely(rd);
|
||||
|
||||
if (this.lastEchoTime === null || this.lastEchoData === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.lastEchoData.length !== echoBytes.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (let i in this.lastEchoData) {
|
||||
if (this.lastEchoData[i] == echoBytes[i]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
this.lastEchoTime = null;
|
||||
this.lastEchoData = null;
|
||||
|
||||
this.config.echoUpdater(ECHO_FAILED);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
delay = new Date().getTime() - this.lastEchoTime.getTime();
|
||||
|
||||
if (delay < 0) {
|
||||
delay = 0;
|
||||
}
|
||||
|
||||
this.lastEchoTime = null;
|
||||
this.lastEchoData = null;
|
||||
|
||||
this.config.echoUpdater(delay);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
await reader.readCompletely(rd);
|
||||
|
||||
throw new Exception("Unknown control signal: " + controlType);
|
||||
}
|
||||
|
||||
/**
|
||||
* handle received stream respond
|
||||
*
|
||||
* @param {header.Header} hd The header
|
||||
* @param {reader.Reader} rd The reader
|
||||
*
|
||||
* @throws {Exception} when given stream is not running
|
||||
*
|
||||
*/
|
||||
async handleStream(hd, rd) {
|
||||
if (hd.data() >= this.streams.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
let stream = this.streams[hd.data()];
|
||||
|
||||
if (!stream.running()) {
|
||||
// WARNING: Connection must be reset at this point because we cannot
|
||||
// determine how many bytes to read
|
||||
throw new Exception(
|
||||
'Remote is requesting for stream "' +
|
||||
hd.data() +
|
||||
'" which is not running',
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
let initialHeaderBytes = await reader.readN(rd, 2);
|
||||
|
||||
// WARNING: It's the stream's responsibility to ensure stream data is
|
||||
// completely readed before return
|
||||
if (stream.initializing()) {
|
||||
let streamHeader = new header.InitialStream(
|
||||
initialHeaderBytes[0],
|
||||
initialHeaderBytes[1]
|
||||
);
|
||||
|
||||
return stream.initialize(streamHeader);
|
||||
}
|
||||
|
||||
let streamHeader = new header.Stream(
|
||||
initialHeaderBytes[0],
|
||||
initialHeaderBytes[1]
|
||||
),
|
||||
streamReader = new reader.Limited(rd, streamHeader.length());
|
||||
|
||||
let tickResult = await stream.tick(streamHeader, streamReader);
|
||||
|
||||
await reader.readCompletely(streamReader);
|
||||
|
||||
return tickResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* handle received close respond
|
||||
*
|
||||
* @param {header.Header} hd The header
|
||||
*
|
||||
* @throws {Exception} when given stream is not running
|
||||
*
|
||||
*/
|
||||
async handleClose(hd) {
|
||||
if (hd.data() >= this.streams.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
let stream = this.streams[hd.data()];
|
||||
|
||||
if (!stream.running()) {
|
||||
// WARNING: Connection must be reset at this point because we cannot
|
||||
// determine how many bytes to read
|
||||
throw new Exception(
|
||||
'Remote is requesting for stream "' +
|
||||
hd.data() +
|
||||
'" to be closed, but the stream is not running',
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
let cResult = await stream.close();
|
||||
|
||||
let completedHeader = new header.Header(header.COMPLETED);
|
||||
completedHeader.set(hd.data());
|
||||
this.sender.send(new Uint8Array([completedHeader.value()]));
|
||||
|
||||
return cResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* handle received close respond
|
||||
*
|
||||
* @param {header.Header} hd The header
|
||||
*
|
||||
* @throws {Exception} when given stream is not running
|
||||
*
|
||||
*/
|
||||
async handleCompleted(hd) {
|
||||
if (hd.data() >= this.streams.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
let stream = this.streams[hd.data()];
|
||||
|
||||
if (!stream.running()) {
|
||||
// WARNING: Connection must be reset at this point because we cannot
|
||||
// determine how many bytes to read
|
||||
throw new Exception(
|
||||
'Remote is requesting for stream "' +
|
||||
hd.data() +
|
||||
'" to be completed, but the stream is not running',
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
return stream.completed();
|
||||
}
|
||||
|
||||
/**
|
||||
* Main proccess loop
|
||||
*
|
||||
* @throws {Exception} when encountered an unknown header
|
||||
*/
|
||||
async tick() {
|
||||
let headerBytes = await reader.readOne(this.reader),
|
||||
hd = new header.Header(headerBytes[0]);
|
||||
|
||||
switch (hd.type()) {
|
||||
case header.CONTROL:
|
||||
return this.handleControl(new reader.Limited(this.reader, hd.data()));
|
||||
|
||||
case header.STREAM:
|
||||
return this.handleStream(hd, this.reader);
|
||||
|
||||
case header.CLOSE:
|
||||
return this.handleClose(hd);
|
||||
|
||||
case header.COMPLETED:
|
||||
return this.handleCompleted(hd);
|
||||
|
||||
default:
|
||||
throw new Exception("Unknown header", false);
|
||||
}
|
||||
}
|
||||
}
|
||||
22
ui/stream/streams_test.js
Normal file
22
ui/stream/streams_test.js
Normal file
@@ -0,0 +1,22 @@
|
||||
// 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";
|
||||
|
||||
describe("Streams", () => {
|
||||
it("Header", () => {});
|
||||
});
|
||||
114
ui/stream/subscribe.js
Normal file
114
ui/stream/subscribe.js
Normal file
@@ -0,0 +1,114 @@
|
||||
// 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 Exception from "./exception.js";
|
||||
|
||||
const typeReject = 0;
|
||||
const typeResolve = 1;
|
||||
|
||||
export class Subscribe {
|
||||
/**
|
||||
* constructor
|
||||
*
|
||||
*/
|
||||
constructor() {
|
||||
this.res = null;
|
||||
this.rej = null;
|
||||
this.pending = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns how many resolve/reject in the pending
|
||||
*/
|
||||
pendings() {
|
||||
return (
|
||||
this.pending.length + (this.rej !== null || this.res !== null ? 1 : 0)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the subscribe waiter
|
||||
*
|
||||
* @param {any} d Resolve data which will be send to the subscriber
|
||||
*/
|
||||
resolve(d) {
|
||||
if (this.res === null) {
|
||||
this.pending.push([typeResolve, d]);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.res(d);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reject the subscribe waiter
|
||||
*
|
||||
* @param {any} e Error message that will be send to the subscriber
|
||||
*
|
||||
*/
|
||||
reject(e) {
|
||||
if (this.rej === null) {
|
||||
this.pending.push([typeReject, e]);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.rej(e);
|
||||
}
|
||||
|
||||
/**
|
||||
* Waiting and receive subscribe data
|
||||
*
|
||||
* @returns {Promise<any>} Data receiver
|
||||
*
|
||||
*/
|
||||
subscribe() {
|
||||
if (this.pending.length > 0) {
|
||||
let p = this.pending.shift();
|
||||
|
||||
switch (p[0]) {
|
||||
case typeReject:
|
||||
throw p[1];
|
||||
|
||||
case typeResolve:
|
||||
return p[1];
|
||||
|
||||
default:
|
||||
throw new Exception("Unknown pending type", false);
|
||||
}
|
||||
}
|
||||
|
||||
let self = this;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
self.res = d => {
|
||||
self.res = null;
|
||||
self.rej = null;
|
||||
|
||||
resolve(d);
|
||||
};
|
||||
|
||||
self.rej = e => {
|
||||
self.res = null;
|
||||
self.rej = null;
|
||||
|
||||
reject(e);
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user