microvium
Version:
A compact, embeddable scripting engine for microcontrollers for executing small scripts written in a subset of JavaScript.
182 lines • 8.2 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.SynchronousWebSocketServer = void 0;
const worker_threads_1 = require("worker_threads");
const util_1 = require("util");
const events_1 = require("events");
const shared_definitions_1 = require("./shared-definitions");
const utils_1 = require("../utils");
class SynchronousWebSocketServer extends events_1.EventEmitter {
constructor(port, opts = {}) {
super();
// First int is SignalState, second int is message length if a message is
// waiting, 3rd and 4th int are read and write pointers for the
// workerStateTrace. 5th int is a flag to indicate workerStateTrace is dirty.
this.signalingArray = new Int32Array(new SharedArrayBuffer(5 * 4));
this.workerStateTrace = new Int32Array(new SharedArrayBuffer(shared_definitions_1.WORKER_THREAD_STATE_TRACE_SIZE * 4));
this.textDecoder = new util_1.TextDecoder();
this.verboseLogging = false;
this.state = 'initial';
this._workerThreadState = shared_definitions_1.WorkerThreadState.CREATING;
const maxMessageSize = opts.maxMessageSize || 100000;
const timeout = opts.timeout || undefined;
this.verboseLogging = opts.verboseLogging || false;
this.signalingArray[0] = shared_definitions_1.SignalState.SIGNAL_NO_MESSAGE;
this.signalingArray[1] = 0;
this.signalingArray[2] = 0;
this.signalingArray[3] = 0;
this.signalingArray[4] = 0;
this.messageDataArray = new Uint8Array(new SharedArrayBuffer(maxMessageSize));
this.timeout = timeout;
const workerData = {
signalingArray: this.signalingArray,
messageDataArray: this.messageDataArray,
workerStateTrace: this.workerStateTrace,
port,
maxMessageSize,
timeout
};
this.worker = new worker_threads_1.Worker(require.resolve('./worker-thread'), { workerData });
this.worker.on('message', () => this.pullWorkerThreadState());
// The worker will respond with either "listening" or an error, and is also
// allowed to produce log messages.
const message = this.receiveFromWorker();
if (message.type === 'listening') {
this.state = 'listening';
}
else {
// The only thing the worker should respond with is to say it's listening.
// If it's not listening, it should throw an error before it gets here.
return (0, utils_1.unexpected)();
}
}
get isConnected() { return this.state === 'connected'; }
get workerThreadState() {
this.pullWorkerThreadState();
return shared_definitions_1.WorkerThreadState[this._workerThreadState];
}
pullWorkerThreadState() {
// Clear the dirty flag
Atomics.store(this.signalingArray, 4, 0);
// While read pointer is not equal to write pointer
let readIndex = Atomics.load(this.signalingArray, 2);
let writeIndex = Atomics.load(this.signalingArray, 3);
while (readIndex !== writeIndex) {
this._workerThreadState = Atomics.load(this.workerStateTrace, readIndex);
if (this.verboseLogging) {
console.log('WS worker state: ' + shared_definitions_1.WorkerThreadState[this._workerThreadState]);
}
readIndex = (readIndex + 1) % shared_definitions_1.WORKER_THREAD_STATE_TRACE_SIZE;
Atomics.store(this.signalingArray, 2, readIndex);
readIndex = Atomics.load(this.signalingArray, 2);
writeIndex = Atomics.load(this.signalingArray, 3);
}
}
waitForConnection() {
if (this.state !== 'listening') {
throw new Error('Can only call `waitForConnection` when socket is listening');
}
while (this.state === 'listening') {
const message = this.receiveFromWorker();
if (message.type === 'client-connected') {
this.state = 'connected';
}
}
}
*receiveSocketEvents() {
while (this.state === 'connected') {
const message = this.receiveSocketEvent();
if (message)
yield message;
else
return;
}
}
receiveSocketEvent() {
if (this.state === 'listening') {
this.waitForConnection();
}
if (this.state !== 'connected') {
throw new Error(`Cannot receive messages while socket is in state ${this.state}`);
}
while (this.state === 'connected') {
const message = this.receiveFromWorker();
// These message types aren't handled here or shouldn't be received in this state
if (message.type !== 'socket-event') {
return (0, utils_1.unexpected)();
}
this.emit(message.event, message.data);
if (message.event === 'message') {
return message.data;
}
if (message.event === 'close') {
this.close();
return undefined;
}
if (message.event === 'error') {
throw new Error(message.event);
}
}
return undefined;
}
receiveFromWorker() {
while (true) {
this.pullWorkerThreadState();
// Receiving blocks the thread, so it can't be done using worker events
// Wait until there's a message available
const result = Atomics.wait(this.signalingArray, 0, shared_definitions_1.SignalState.SIGNAL_NO_MESSAGE, this.timeout);
this.pullWorkerThreadState();
if (result === 'timed-out') {
this.close();
throw new Error('Timed out waiting for next message');
}
// The flag should change to a `1` to indicate that there is a message
// waiting in the buffer.
if (this.signalingArray[0] !== shared_definitions_1.SignalState.SIGNAL_MESSAGE_WAITING) {
console.warn('Signalling error');
continue;
}
const messageLength = this.signalingArray[1];
const messageJSON = this.textDecoder.decode(this.messageDataArray.slice(0, messageLength));
// Set the flag back to zero to indicate that the messageDataArray is
// available for use for the next message
Atomics.store(this.signalingArray, 0, shared_definitions_1.SignalState.SIGNAL_NO_MESSAGE);
Atomics.store(this.signalingArray, 1, 0); // Set length back to zero
Atomics.notify(this.signalingArray, 0, 1); // Notify the worker thread that the buffer is vacant again
if (this.verboseLogging) {
console.log('Message from worker: ' + messageJSON);
}
// Parse the message
const message = JSON.parse(messageJSON);
switch (message.type) {
case 'error': throw new Error(message.error);
case 'debug-log':
console.log('Log from socket worker', ...message.args);
break;
default: return message;
}
}
}
send(message) {
if (this.verboseLogging) {
console.log('WS about to send:', message);
}
this.postMessageToWorker({ action: 'send', message });
}
close() {
if (['closing', 'closed', 'error'].includes(this.state))
return;
if (this.verboseLogging) {
console.log('WS closing');
}
this.state = 'closing';
this.postMessageToWorker({ action: 'close' });
this.worker.terminate();
this.state = 'closed';
}
postMessageToWorker(message) {
this.worker.postMessage(message);
}
}
exports.SynchronousWebSocketServer = SynchronousWebSocketServer;
//# sourceMappingURL=index.js.map