@openhps/core
Version:
Open Hybrid Positioning System - Core component
260 lines • 8.89 kB
JavaScript
import { BlobWorker, Pool, spawn, Worker } from 'threads';
import { DataSerializer } from '../data/DataSerializer';
import { DataService } from '../service/DataService';
import { WorkerServiceProxy } from '../service/WorkerServiceProxy';
import { AsyncEventEmitter } from '../_internal/AsyncEventEmitter';
import { DummyDataService } from '../service/DummyDataService';
import { DummyService } from '../service/DummyService';
export class WorkerHandler extends AsyncEventEmitter {
constructor(model, options, config) {
var _a;
super();
this._serviceOutputResponse = new Map();
this.model = model;
this.config = config;
this.options = options;
this.options.timeout = (_a = this.options.timeout) !== null && _a !== void 0 ? _a : 10000;
}
build() {
return new Promise(resolve => {
if (typeof process.env.NODE_ENV === 'undefined') {
// eslint-disable-next-line
const NativeWorker = typeof __non_webpack_require__ === "function" ? __non_webpack_require__("worker_threads").Worker : eval("require")("worker_threads").Worker;
if (NativeWorker) {
// NodeJS
NativeWorker.defaultMaxListeners = 0;
const resolvedPath = require.resolve(this.options.worker);
if (resolvedPath.match(/\.tsx?$/i)) {
// Transpile
this.options.worker = `
require('ts-node').register();
require(${JSON.stringify(resolvedPath)});
`;
this.options.blob = true;
}
}
}
this._pool = Pool(() => this._spawnWorker(), {
size: this.options.poolSize || 4,
concurrency: this.options.poolConcurrency || 2
});
this._pool.events().subscribe(value => {
if (value.type === 'initialized') {
resolve();
}
});
});
}
destroy() {
return new Promise((resolve, reject) => {
if (this._pool === undefined) {
return resolve();
}
const timeout = setTimeout(() => {
this._pool.terminate(true).then(() => {
resolve();
}).catch(ex => {
reject(ex);
});
}, 2500);
this._pool.terminate().then(() => {
clearTimeout(timeout);
resolve();
}).catch(ex => {
clearTimeout(timeout);
reject(ex);
});
});
}
pull(options) {
return new Promise((resolve, reject) => {
// Pass the pull request to the worker
this._pool.queue(worker => {
const pullFn = worker.pull;
return pullFn(options);
}).then(resolve).catch(reject);
});
}
push(frame, options) {
return new Promise((resolve, reject) => {
this._pool.queue(worker => {
const pushFn = worker.push;
return pushFn(DataSerializer.serialize(frame), options);
}).then(() => {
resolve();
}).catch(reject);
});
}
invokeMethod(methodName, ...args) {
return new Promise((resolve, reject) => {
this._pool.queue(worker => {
const invokeMethod = worker.invokeMethod;
return invokeMethod(methodName, ...args.map(a => DataSerializer.serialize(a)));
}).then(result => {
if (result !== undefined) {
resolve(DataSerializer.deserialize(result));
} else {
resolve(result);
}
}).catch(reject);
});
}
createWorker() {
if (this.options.blob) {
const worker = new BlobWorker(this.options.worker, {
type: this.options.type === 'typescript' ? 'classic' : this.options.type
});
return worker;
} else {
const worker = new Worker(this.options.worker, {
type: this.options.type === 'typescript' ? 'classic' : this.options.type
});
return worker;
}
}
/**
* Spawn a single worker
* This method can be called multiple times in a pool
* @returns {Promise<Thread>} Thread spawn promise
*/
_spawnWorker() {
return new Promise((resolve, reject) => {
const worker = this.createWorker();
spawn(worker, {
timeout: this.options.timeout
}).then(thread => {
const init = thread.init;
const pushOutput = thread.pushOutput;
const pullOutput = thread.pullOutput;
const serviceOutputCall = thread.serviceOutputCall;
const serviceInputCall = thread.serviceInputCall;
const eventOutput = thread.eventOutput;
const findAllServices = thread.findAllServices;
const threadId = worker.threadId;
this._serviceOutputResponse.set(threadId, thread.serviceOutputResponse);
// Subscribe to the workers pull, push and service functions
pullOutput().subscribe(this._onWorkerPull.bind(this));
pushOutput().subscribe(this._onWorkerPush.bind(this));
serviceOutputCall().subscribe(this._onWorkerService.bind(this, threadId));
eventOutput().subscribe(this._onWorkerEvent.bind(this));
// Initialize the worker
init(Object.assign({
directory: this.options.directory || __dirname,
services: this._getServices(),
imports: this.options.imports || [],
args: this.options.args || {},
type: this.options.type || 'classic',
methods: this.options.methods ? this.options.methods.map(method => {
return {
name: method.name,
handlerFn: method.handler.toString()
};
}) : []
}, this.config)).then(() => {
return findAllServices();
}).then(services => {
this._addServices(services, serviceInputCall);
resolve(thread);
}).catch(reject);
}).catch(reject);
});
}
/**
* Serialize the services of this model
* @returns {any[]} Services array
*/
_getServices() {
// Serialize this model services to the worker
const services = this.options.services || this.model.findAllServices();
const servicesArray = services.map(service => {
// Services are wrapped in a proxy. Get prototype
const serviceBase = Object.getPrototypeOf(service);
return {
uid: service.uid,
type: serviceBase.constructor.name,
dataType: service instanceof DataService ? service.dataType ? service.dataType.name : undefined : undefined
};
});
return servicesArray;
}
_addServices(services, call) {
const model = this.model;
services.filter(service => {
const internalService = this.model.findService(service.name) || this.model.findDataService(service.name);
return internalService === undefined;
}).forEach(service => {
if (service.dataType) {
const DataType = DataSerializer.findTypeByName(service.dataType);
model.addService(new DummyDataService(service.uid, DataType), new WorkerServiceProxy({
uid: service.uid,
callFunction: call
}));
} else {
model.addService(new DummyService(service.uid), new WorkerServiceProxy({
uid: service.uid,
callFunction: call
}));
}
});
}
_onWorkerService(threadId, value) {
const service = this.model.findDataService(value.serviceUID) || this.model.findService(value.serviceUID);
if (service[value.method]) {
const serializedParams = value.parameters;
const params = [];
serializedParams.forEach(param => {
if (param['__type']) {
params.push(DataSerializer.deserialize(param));
} else {
params.push(param);
}
});
const promise = service[value.method](...params);
Promise.resolve(promise).then(_ => {
if (Array.isArray(_)) {
const result = [];
_.forEach(r => {
result.push(DataSerializer.serialize(r));
});
this._serviceOutputResponse.get(threadId)({
id: value.id,
success: true,
result
});
} else {
const result = DataSerializer.serialize(_);
this._serviceOutputResponse.get(threadId)({
id: value.id,
success: true,
result
});
}
}).catch(ex => {
this._serviceOutputResponse.get(threadId)({
id: value.id,
success: false,
result: ex
});
});
}
}
_onWorkerEvent(value) {
this.emit('event', value);
}
/**
* Triggered for each worker that requests a pull
* @param {PullOptions} options Pull options
*/
_onWorkerPull(options) {
this.emit('pull', options);
}
/**
* Triggered for each worker that pushes data
* @param {any} value Serialized data
* @param {PushOptions} options Push options
*/
_onWorkerPush(value, options) {
const deserializedFrame = DataSerializer.deserialize(value);
this.emit('push', deserializedFrame, options);
}
}