@comake/skl-js-engine
Version:
Standard Knowledge Language Javascript Engine
161 lines • 6.05 kB
JavaScript
;
/* eslint-disable @typescript-eslint/naming-convention */
/* eslint-disable callback-return */
Object.defineProperty(exports, "__esModule", { value: true });
exports.JSExecutor = void 0;
/* eslint-disable func-style */
const errors_1 = require("./errors");
const transport_1 = require("./transport");
/**
* JSExecutor V2 with enhanced abort control for immediate shutdown
*
* @example Writing abort-aware callbacks:
* ```typescript
* const jsExecutor = new JSExecutorV2(scriptPath, {
* async myCallback(): Promise<string> {
* return new Promise((resolve, reject) => {
* const timeoutId = setTimeout(() => resolve('done'), 5000);
*
* // Listen to the global abort signal to clean up resources
* const abortSignal = JSExecutorV2.getGlobalAbortSignal();
* const onAbort = () => {
* clearTimeout(timeoutId);
* reject(new Error('Operation aborted'));
* };
*
* if (abortSignal.aborted) {
* onAbort();
* return;
* }
*
* abortSignal.addEventListener('abort', onAbort);
* });
* }
* });
* ```
*/
class JSExecutor {
constructor(executorScriptPath, callbacks) {
this.executorScriptPath = executorScriptPath;
this.callbacks = callbacks;
this.isInitialized = false;
this.abortController = new AbortController();
}
/**
* Static method to check if any JSExecutor instance is being shut down
* Callbacks can use this to check if they should abort their operations
*/
static isShuttingDown() {
return JSExecutor.globalAbortController.signal.aborted;
}
/**
* Get the global abort signal that callbacks can listen to
* This allows callbacks to immediately abort when any JSExecutor shuts down
*/
static getGlobalAbortSignal() {
return JSExecutor.globalAbortController.signal;
}
async initialize(config, executionOptions) {
if (this.isInitialized) {
return;
}
this.server = new transport_1.JsonRpcServer({
// 1 hour
requestTimeout: 60 * 60 * 1000
});
// Register callbacks with the server
Object.entries(this.callbacks).forEach(([methodName, callback]) => {
this.server.registerMethod(methodName, this.wrapCallbackWithAbort(callback));
});
// Create and initialize the transport once
this.transport = new transport_1.ParentStdioTransport(this.executorScriptPath, this.server);
await this.transport.initialize(config, executionOptions);
this.isInitialized = true;
}
async shutdown(options) {
// Clear any existing shutdown timeout
if (this.shutdownTimeout) {
clearTimeout(this.shutdownTimeout);
this.shutdownTimeout = undefined;
}
if (options?.afterTimeout) {
this.shutdownTimeout = setTimeout(async () => {
await this.shutdownCore();
}, options.afterTimeout);
}
else {
await this.shutdownCore();
}
}
async shutdownCore() {
// // Signal global abort first
// JSExecutor.globalAbortController.abort();
// Then signal this instance's abort
this.abortController.abort();
if (this.transport) {
await this.transport.close();
this.transport = undefined;
}
this.isInitialized = false;
this.server = undefined;
}
async rpcComplaintCallback(callback, args) {
const result = await callback(...args);
// RPC expects a result object
return result ?? {};
}
/**
* Wrap a callback with abort functionality
* The callback will be automatically aborted if JSExecutor is shut down
*/
wrapCallbackWithAbort(callback) {
return ((...args) => {
// Check if we're shutting down before executing
if (this.abortController.signal.aborted || JSExecutor.globalAbortController.signal.aborted) {
return Promise.reject(new Error('JSExecutor is shutting down'));
}
const callbackPromise = Promise.resolve(this.rpcComplaintCallback(callback, args));
// Create an abort promise
const abortPromise = new Promise((_, reject) => {
const onAbort = () => {
reject(new Error('JSExecutor is shutting down'));
};
// Listen to both instance and global abort signals
this.abortController.signal.addEventListener('abort', onAbort, { once: true });
JSExecutor.globalAbortController.signal.addEventListener('abort', onAbort, { once: true });
});
// Race the callback against the abort signal
return Promise.race([callbackPromise, abortPromise]);
});
}
async execute(code, args, executionOptions) {
if (!this.isInitialized) {
throw new errors_1.NotInitializedError('JSExecutor is not initialized, please call initialize() first');
}
if (!this.transport || !this.transport.isReady()) {
throw new Error('Transport is not ready');
}
const response = await this.transport.request(transport_1.STANDARD_METHODS.executeCode, {
code,
args,
functionName: executionOptions.functionName
}, {
timeout: executionOptions.timeout,
retries: executionOptions.retries ?? 0
});
return response;
}
/**
* Legacy method for backward compatibility
* @deprecated Use execute() instead
*/
async call(methodName, ...args) {
if (!this.transport || !this.transport.isReady()) {
throw new Error('Transport is not ready');
}
return this.transport.request(methodName, args);
}
}
exports.JSExecutor = JSExecutor;
JSExecutor.globalAbortController = new AbortController();
//# sourceMappingURL=jsExecutor.js.map