packages/@jsii/runtime/lib/host.ts (154 lines of code) (raw):
import {
api,
Kernel,
JsiiFault,
JsiiError,
RuntimeError,
JsiiErrorType,
} from '@jsii/kernel';
import { EventEmitter } from 'events';
import { Input, IInputOutput } from './in-out';
export class KernelHost {
private readonly kernel = new Kernel(this.callbackHandler.bind(this));
private readonly eventEmitter = new EventEmitter();
public constructor(
private readonly inout: IInputOutput,
private readonly opts: {
debug?: boolean;
debugTiming?: boolean;
noStack?: boolean;
validateAssemblies?: boolean;
} = {},
) {
this.kernel.traceEnabled = opts.debug ?? false;
this.kernel.debugTimingEnabled = opts.debugTiming ?? false;
this.kernel.validateAssemblies = opts.validateAssemblies ?? false;
}
public run() {
const req = this.inout.read();
if (!req || 'exit' in req) {
this.eventEmitter.emit('exit', req?.exit ?? 0);
return; // done
}
this.processRequest(req, () => {
// Schedule the call to run on the next event loop iteration to
// avoid recursion.
setImmediate(() => this.run());
});
}
public once(event: 'exit', listener: (code: number) => void) {
this.eventEmitter.once(event, listener);
}
private callbackHandler(
callback: api.Callback,
): api.CompleteRequest['result'] {
// write a "callback" response, which is a special response that tells
// the client that there's synchronous callback it needs to invoke and
// bring back the result via a "complete" request.
this.inout.write({ callback });
return completeCallback.call(this);
function completeCallback(this: KernelHost): api.CompleteRequest['result'] {
const req = this.inout.read();
if (!req || 'exit' in req) {
throw new JsiiFault('Interrupted before callback returned');
}
// if this is a completion for the current callback, then we can
// finally stop this nonsense and return the result.
const completeReq = req as { complete: api.CompleteRequest };
if (
'complete' in completeReq &&
completeReq.complete.cbid === callback.cbid
) {
if (completeReq.complete.err) {
if (completeReq.complete.name === JsiiErrorType.JSII_FAULT) {
throw new JsiiFault(completeReq.complete.err);
}
throw new RuntimeError(completeReq.complete.err);
}
return completeReq.complete.result;
}
// otherwise, process the request normally, but continue to wait for
// our callback to be completed. sync=true to enforce that `completeCallback`
// will be called synchronously and return value will be chained back so we can
// return it to the callback handler.
return this.processRequest(
req,
completeCallback.bind(this),
/* sync */ true,
);
}
}
/**
* Processes the input request `req` and writes the output response to
* stdout. This method invokes `next` when the request was fully processed.
* This either happens synchronously or asynchronously depending on the api
* (e.g. the "end" api will wait for an async promise to be fulfilled before
* it writes the response)
*
* @param req The input request
* @param next A callback to invoke to continue
* @param sync If this is 'true', "next" must be called synchronously. This means
* that we won't process any async activity (begin/complete). The kernel
* doesn't allow any async operations during a sync callback, so this shouldn't
* happen, so we assert in this case to find bugs.
*/
private processRequest<T>(
req: Input,
next: () => T,
sync = false,
): T | undefined {
if ('callback' in req) {
throw new JsiiFault(
'Unexpected `callback` result. This request should have been processed by a callback handler',
);
}
if (!('api' in req)) {
throw new JsiiFault('Malformed request, "api" field is required');
}
const apiReq = req;
const fn = this.findApi(apiReq.api);
try {
const ret = fn.call(this.kernel, req);
// special case for 'begin' and 'complete' which are on an async
// promise path. in order to allow the kernel to actually fulfill
// the promise, and continue any async flows (which may potentially
// start other promises), we respond only within a setImmediate
// block, which is scheduled in the same micro-tasks queue as
// promises. see the kernel test 'async overrides: two overrides'
// for an example for this use case.
if (apiReq.api === 'begin' || apiReq.api === 'complete') {
checkIfAsyncIsAllowed();
this.debug('processing pending promises before responding');
setImmediate(() => {
this.writeOkay(ret);
next();
});
return undefined;
}
// if this is an async method, return immediately and
// call next only when the promise is fulfilled.
if (this.isPromise(ret)) {
checkIfAsyncIsAllowed();
this.debug('waiting for promise to be fulfilled');
const promise = ret;
promise
.then((val) => {
this.debug('promise succeeded:', val);
this.writeOkay(val);
next();
})
.catch((e) => {
this.debug('promise failed:', e);
this.writeError(e);
next();
});
return undefined;
}
this.writeOkay(ret);
} catch (e: any) {
this.writeError(e);
}
// indicate this request was processed (synchronously).
return next();
function checkIfAsyncIsAllowed() {
if (sync) {
throw new JsiiFault(
'Cannot handle async operations while waiting for a sync callback to return',
);
}
}
}
/**
* Writes an "ok" result to stdout.
*/
private writeOkay(result: any) {
const res = { ok: result };
this.inout.write(res);
}
/**
* Writes an "error" result to stdout.
*/
private writeError(error: JsiiError) {
const res = {
error: error.message,
name: error.name,
stack: this.opts.noStack ? undefined : error.stack,
};
this.inout.write(res);
}
/**
* Returns true if the value is a promise.
*/
private isPromise(v: any): v is Promise<any> {
return typeof v?.then === 'function';
}
/**
* Given a kernel api name, returns the function to invoke.
*/
private findApi(apiName: string): (this: Kernel, arg: Input) => any {
const fn = (this.kernel as any)[apiName];
if (typeof fn !== 'function') {
throw new Error(`Invalid kernel api call: ${apiName}`);
}
return fn;
}
private debug(...args: any[]) {
if (!this.opts.debug) {
return;
}
console.error(...args);
}
}