node-srt/src/async.js
Stephan Hesse 074da05097
Worker-based Async API with await/Promise support (or plain callbacks) (#6)
* fix lint errors in eslint config :)

* index.d.s: add /* eslint-disable @typescript-eslint/triple-slash-reference */

* improve eslint calling script

* add async-worker version of the SRT JS API with await/Promise support
+ potentially out-of-order RPC-back/result-dequeuing is possible
with a type of call-ID generator and callback-map that we prototyped
(but not used atm since the worker is only doing sync/blocking internally
with the current SRT lib binding).

* add various examples for async API ("classic" callbacks / Promise / await)

* add .eslintignore (should go with commit where we just call "eslint .")

* index.js: add missing semi

* async.js: fix method name litteral in epollUWait

* async.js: allow for accept method to use a timeout opt (defaults to false),
and a custom timeout value option (default to default timeout),
which can potentially be set differently than the general default timeout.
+ make the timeout value a static class property so that it can be
user-defined module-load wide. defaults to constant in module top scope.

* async.js: fix for a rejected promise, make sure we don't resolve anymore
+ add a custom timeout value argument to _createAsyncWorkPromise

* async.js: rm an experiment

* fix a lint error in example

* add types for async api and fix some details on binding API types

* export async API to index

* async.js: add some missing docs for private methods

* include async types in index
2020-08-03 09:54:26 +02:00

242 lines
5.9 KiB
JavaScript

const {
Worker, isMainThread, parentPort, workerData
} = require('worker_threads');
const path = require('path');
const {performance} = require("perf_hooks");
const DEFAULT_PROMISE_TIMEOUT_MS = 3000;
/*
const WORK_ID_GEN_MOD = 0xFFF;
*/
class AsyncSRT {
/**
* @static
* @type {number} Promise-timeout in millis
*/
static TimeoutMs = DEFAULT_PROMISE_TIMEOUT_MS;
constructor() {
this._worker = new Worker(path.resolve(__dirname, './async-worker.js'));
this._worker.on('message', this._onWorkerMessage.bind(this));
/*
this._workIdGen = 0;
this._workCbMap = new Map();
*/
this._workCbQueue = [];
}
/**
* @private
* @param {*} data
*/
_onWorkerMessage(data) {
const resolveTime = performance.now();
const {timestamp, result, workId} = data;
const callback = this._workCbQueue.shift();
callback(result);
}
/**
* @private
* @param {string} method
* @param {Array<any>} args
* @param {Function} callback
*/
_postAsyncWork(method, args, callback) {
const timestamp = performance.now();
// not really needed atm,
// only if the worker spawns async jobs itself internally
// and thus the queuing order of jobs would not be preserved
// across here and the worker side.
/*
if (this._workCbMap.size >= WORK_ID_GEN_MOD - 1) {
throw new Error('Can`t post more async work: Too many awaited callbacks unanswered in queue');
}
const workId = this._workIdGen;
this._workIdGen = (this._workIdGen + 1) % WORK_ID_GEN_MOD;
this._workCbMap.set(workId, callback);
*/
this._workCbQueue.push(callback);
this._worker.postMessage({method, args, /*workId,*/ timestamp});
}
/**
* @private
* @param {string} method
* @param {Array<any>} args optional
* @param {Function} callback optional
*/
_createAsyncWorkPromise(method, args = [], callback = null, useTimeout = true, timeoutMs = AsyncSRT.TimeoutMs) {
return new Promise((resolve, reject) => {
let timeout;
let rejected = false;
const onResult = (result) => {
// Q: signal somehow to app that timed-out call has had result after all? (only in case of using Promise..?)
if (rejected) {
// The reject thing only makes sense for Promise,
// and users can manage this aspect themselves when using plain callbacks.
if (callback) callback(result);
return;
} else if (useTimeout) clearTimeout(timeout);
resolve(result);
if (callback) callback(result); // NOTE: the order doesn't matter for us,
// but intuitively the promise result should probably be resolved first.
};
if (useTimeout) {
timeout = setTimeout(() => {
reject(new Error('Timeout exceeded while awaiting result from worker running native-addon module functions'));
rejected = true;
}, timeoutMs);
}
this._postAsyncWork(method, args, onResult);
});
}
/**
*
* @param {boolean} sender
* @returns SRTSOCKET identifier (integer value) or -1 (SRT_ERROR)
*/
createSocket(sender, callback) {
return this._createAsyncWorkPromise("createSocket", [sender], callback);
}
/**
*
* @param socket
* @param address
* @param port
*/
bind(socket, address, port, callback) {
return this._createAsyncWorkPromise("bind", [socket, address, port], callback);
}
/**
*
* @param socket
* @param backlog
*/
listen(socket, backlog, callback) {
return this._createAsyncWorkPromise("listen", [socket, backlog], callback);
}
/**
*
* @param socket
* @param host
* @param port
*/
connect(socket, host, port, callback) {
return this._createAsyncWorkPromise("connect", [socket, host, port], callback);
}
/**
*
* @param socket
* @returns File descriptor of incoming connection pipe
*/
accept(socket, callback, useTimeout = false, timeoutMs = AsyncSRT.TimeoutMs) {
return this._createAsyncWorkPromise("accept", [socket], callback, useTimeout, timeoutMs);
}
/**
*
* @param socket
*/
close(socket, callback) {
return this._createAsyncWorkPromise("close", [socket], callback);
}
/**
*
* @param socket
* @param chunkSize
* @returns {Promise<Buffer>}
*/
read(socket, chunkSize, callback) {
return this._createAsyncWorkPromise("read", [socket, chunkSize], callback);
}
/**
*
* @param socket
* @param {Buffer} chunk
*/
write(socket, chunk, callback) {
// TODO: see if we can do this using SharedArrayBuffer for example,
// or just leveraging Transferable objects capabilities ... ?
// FIXME: Performance ... ?
const buf = Buffer.allocUnsafe(chunk.length);
chunk.copy(buf);
chunk = buf;
return this._createAsyncWorkPromise("write", [socket, chunk], callback);
}
/**
*
* @param socket
* @param option
* @param value
*/
setSockOpt(socket, option, value, callback) {
return this._createAsyncWorkPromise("setSockOpt", [socket, option, value], callback);
}
/**
*
* @param socket
* @param option
*/
getSockOpt(socket, option, callback) {
return this._createAsyncWorkPromise("getSockOpt", [socket, option], callback);
}
/**
*
* @param socket
*/
getSockState(socket, callback) {
return this._createAsyncWorkPromise("getSockState", [socket], callback);
}
/**
* @returns epid
*/
epollCreate(callback) {
return this._createAsyncWorkPromise("epollCreate", [], callback);
}
/**
*
* @param epid
* @param socket
* @param events
*/
epollAddUsock(epid, socket, events, callback) {
return this._createAsyncWorkPromise("epollAddUsock", [epid, socket, events], callback);
}
/**
*
* @param epid
* @param msTimeOut
*/
epollUWait(epid, msTimeOut, callback) {
return this._createAsyncWorkPromise("epollUWait", [epid, msTimeOut], callback);
}
}
module.exports = {AsyncSRT};