mirror of
https://github.com/The-Low-Code-Foundation/OpenNoodl.git
synced 2026-03-08 01:53:30 +01:00
feat(local-backend): add WorkflowRunner for visual workflow execution
TASK-007C: Workflow Runtime Integration
- Add WorkflowRunner class to manage CloudRunner instances
- Integrate WorkflowRunner with LocalBackendServer
- Add workflow IPC handlers to BackendManager:
- backend:update-workflow - Deploy/update a workflow
- backend:reload-workflows - Hot reload all workflows
- backend:workflow-status - Get workflow status
- LocalBackendServer now handles /functions/:name endpoints via WorkflowRunner
- WorkflowRunner loads .workflow.json files from backends/{id}/workflows/
This commit is contained in:
@@ -100,6 +100,19 @@ class BackendManager {
|
|||||||
return this.exportSchema(id, format);
|
return this.exportSchema(id, format);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Workflow management
|
||||||
|
ipcMain.handle('backend:update-workflow', async (_, args) => {
|
||||||
|
return this.updateWorkflow(args.backendId, args.name, args.workflow);
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('backend:reload-workflows', async (_, id) => {
|
||||||
|
return this.reloadWorkflows(id);
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('backend:workflow-status', async (_, id) => {
|
||||||
|
return this.getWorkflowStatus(id);
|
||||||
|
});
|
||||||
|
|
||||||
this.ipcHandlersSetup = true;
|
this.ipcHandlersSetup = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -234,6 +247,7 @@ class BackendManager {
|
|||||||
id: config.id,
|
id: config.id,
|
||||||
name: config.name,
|
name: config.name,
|
||||||
dbPath: path.join(backendPath, 'data', 'local.db'),
|
dbPath: path.join(backendPath, 'data', 'local.db'),
|
||||||
|
workflowsPath: path.join(backendPath, 'workflows'),
|
||||||
port: config.port
|
port: config.port
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -346,6 +360,51 @@ class BackendManager {
|
|||||||
|
|
||||||
this.runningBackends.clear();
|
this.runningBackends.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==========================================================================
|
||||||
|
// WORKFLOW MANAGEMENT
|
||||||
|
// ==========================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update/deploy a workflow to a backend
|
||||||
|
* @param {string} backendId - Backend ID
|
||||||
|
* @param {string} name - Workflow name
|
||||||
|
* @param {Object} workflow - Workflow export data
|
||||||
|
*/
|
||||||
|
async updateWorkflow(backendId, name, workflow) {
|
||||||
|
const server = this.runningBackends.get(backendId);
|
||||||
|
if (!server) {
|
||||||
|
throw new Error('Backend must be running to update workflows');
|
||||||
|
}
|
||||||
|
|
||||||
|
return server.updateWorkflow(name, workflow);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reload all workflows for a backend
|
||||||
|
* @param {string} backendId - Backend ID
|
||||||
|
*/
|
||||||
|
async reloadWorkflows(backendId) {
|
||||||
|
const server = this.runningBackends.get(backendId);
|
||||||
|
if (!server) {
|
||||||
|
throw new Error('Backend must be running to reload workflows');
|
||||||
|
}
|
||||||
|
|
||||||
|
return server.reloadWorkflows();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get workflow status for a backend
|
||||||
|
* @param {string} backendId - Backend ID
|
||||||
|
*/
|
||||||
|
getWorkflowStatus(backendId) {
|
||||||
|
const server = this.runningBackends.get(backendId);
|
||||||
|
if (!server) {
|
||||||
|
return { initialized: false, workflowCount: 0, functions: [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
return server.getWorkflowStatus();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Singleton instance
|
// Singleton instance
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ const http = require('http');
|
|||||||
const path = require('path');
|
const path = require('path');
|
||||||
const EventEmitter = require('events');
|
const EventEmitter = require('events');
|
||||||
|
|
||||||
|
const { WorkflowRunner } = require('./WorkflowRunner');
|
||||||
|
|
||||||
// Using native http.IncomingMessage handling instead of Express for lighter weight
|
// Using native http.IncomingMessage handling instead of Express for lighter weight
|
||||||
// This keeps the main process simple and avoids additional dependencies
|
// This keeps the main process simple and avoids additional dependencies
|
||||||
|
|
||||||
@@ -46,11 +48,13 @@ class LocalBackendServer {
|
|||||||
* @param {string} config.name - Backend display name
|
* @param {string} config.name - Backend display name
|
||||||
* @param {string} config.dbPath - Path to SQLite database
|
* @param {string} config.dbPath - Path to SQLite database
|
||||||
* @param {number} config.port - Port to listen on
|
* @param {number} config.port - Port to listen on
|
||||||
|
* @param {string} config.workflowsPath - Path to workflows directory
|
||||||
*/
|
*/
|
||||||
constructor(config) {
|
constructor(config) {
|
||||||
this.config = config;
|
this.config = config;
|
||||||
this.server = null;
|
this.server = null;
|
||||||
this.adapter = null;
|
this.adapter = null;
|
||||||
|
this.workflowRunner = null;
|
||||||
this.events = new EventEmitter();
|
this.events = new EventEmitter();
|
||||||
this.wsClients = new Set();
|
this.wsClients = new Set();
|
||||||
}
|
}
|
||||||
@@ -412,12 +416,34 @@ class LocalBackendServer {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* POST /functions/:name - Execute cloud function
|
* POST /functions/:name - Execute cloud function
|
||||||
* Placeholder - will be implemented with CloudRunner
|
* Executes a visual workflow via the WorkflowRunner
|
||||||
*/
|
*/
|
||||||
async handleFunction(res, functionName, body, headers) {
|
async handleFunction(res, functionName, body, headers) {
|
||||||
// TODO: Integrate with CloudRunner when TASK-007C is complete
|
if (!this.workflowRunner) {
|
||||||
|
return this.sendError(res, 501, 'Workflows not initialized');
|
||||||
|
}
|
||||||
|
|
||||||
safeLog(`Cloud function called: ${functionName}`);
|
safeLog(`Cloud function called: ${functionName}`);
|
||||||
this.sendError(res, 501, 'Cloud functions not yet implemented');
|
|
||||||
|
// Build request object matching CloudRunner's expected format
|
||||||
|
const request = {
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
headers: headers
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await this.workflowRunner.run(functionName, request);
|
||||||
|
res.writeHead(response.statusCode, {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Access-Control-Allow-Origin': '*',
|
||||||
|
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
|
||||||
|
'Access-Control-Allow-Headers': '*'
|
||||||
|
});
|
||||||
|
res.end(response.body);
|
||||||
|
} catch (error) {
|
||||||
|
safeLog('Function execution error:', error);
|
||||||
|
this.sendError(res, 500, error.message);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==========================================================================
|
// ==========================================================================
|
||||||
@@ -455,6 +481,24 @@ class LocalBackendServer {
|
|||||||
// Initialize adapter
|
// Initialize adapter
|
||||||
await this.initAdapter();
|
await this.initAdapter();
|
||||||
|
|
||||||
|
// Initialize WorkflowRunner if workflows path is provided
|
||||||
|
if (this.config.workflowsPath) {
|
||||||
|
this.workflowRunner = new WorkflowRunner({
|
||||||
|
workflowsPath: this.config.workflowsPath,
|
||||||
|
adapter: this.adapter,
|
||||||
|
enableDebugInspectors: false
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.workflowRunner.initialize();
|
||||||
|
await this.workflowRunner.loadWorkflows();
|
||||||
|
safeLog(`WorkflowRunner initialized with ${this.workflowRunner.getAvailableFunctions().length} functions`);
|
||||||
|
} catch (e) {
|
||||||
|
safeLog('WorkflowRunner initialization failed (workflows disabled):', e.message);
|
||||||
|
// Don't fail server startup if workflows can't be initialized
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Create HTTP server
|
// Create HTTP server
|
||||||
this.server = http.createServer((req, res) => {
|
this.server = http.createServer((req, res) => {
|
||||||
this.handleRequest(req, res);
|
this.handleRequest(req, res);
|
||||||
@@ -507,6 +551,45 @@ class LocalBackendServer {
|
|||||||
getAdapter() {
|
getAdapter() {
|
||||||
return this.adapter;
|
return this.adapter;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get WorkflowRunner for direct access
|
||||||
|
*/
|
||||||
|
getWorkflowRunner() {
|
||||||
|
return this.workflowRunner;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update a workflow (hot reload)
|
||||||
|
* @param {string} name - Workflow name
|
||||||
|
* @param {Object} exportData - Workflow export data
|
||||||
|
*/
|
||||||
|
async updateWorkflow(name, exportData) {
|
||||||
|
if (!this.workflowRunner) {
|
||||||
|
return { success: false, error: 'Workflows not initialized' };
|
||||||
|
}
|
||||||
|
return this.workflowRunner.loadWorkflow(name, exportData);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reload all workflows
|
||||||
|
*/
|
||||||
|
async reloadWorkflows() {
|
||||||
|
if (!this.workflowRunner) {
|
||||||
|
return { success: false, error: 'Workflows not initialized' };
|
||||||
|
}
|
||||||
|
return this.workflowRunner.reloadWorkflows();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get workflow status
|
||||||
|
*/
|
||||||
|
getWorkflowStatus() {
|
||||||
|
if (!this.workflowRunner) {
|
||||||
|
return { initialized: false, workflowCount: 0, functions: [] };
|
||||||
|
}
|
||||||
|
return this.workflowRunner.getStatus();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { LocalBackendServer, generateObjectId };
|
module.exports = { LocalBackendServer, generateObjectId };
|
||||||
|
|||||||
@@ -0,0 +1,400 @@
|
|||||||
|
/**
|
||||||
|
* WorkflowRunner
|
||||||
|
*
|
||||||
|
* Manages CloudRunner instances for executing visual workflows.
|
||||||
|
* This integrates the noodl-viewer-cloud CloudRunner with the LocalBackendServer,
|
||||||
|
* providing database access to workflow nodes via the LocalSQLAdapter.
|
||||||
|
*
|
||||||
|
* @module local-backend/WorkflowRunner
|
||||||
|
*/
|
||||||
|
|
||||||
|
const fs = require('fs').promises;
|
||||||
|
const path = require('path');
|
||||||
|
const EventEmitter = require('events');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Safe console.log wrapper to prevent EPIPE errors
|
||||||
|
*/
|
||||||
|
function safeLog(...args) {
|
||||||
|
try {
|
||||||
|
console.log('[WorkflowRunner]', ...args);
|
||||||
|
} catch (e) {
|
||||||
|
// Ignore EPIPE errors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* WorkflowRunner class
|
||||||
|
*
|
||||||
|
* Loads and executes visual workflows using the CloudRunner from noodl-viewer-cloud.
|
||||||
|
* Workflows are JSON exports from the editor that contain cloud function components.
|
||||||
|
*/
|
||||||
|
class WorkflowRunner {
|
||||||
|
/**
|
||||||
|
* @param {Object} options
|
||||||
|
* @param {string} options.workflowsPath - Path to workflows directory
|
||||||
|
* @param {Object} options.adapter - LocalSQLAdapter instance for database access
|
||||||
|
* @param {boolean} [options.enableDebugInspectors=false] - Enable debug inspectors
|
||||||
|
*/
|
||||||
|
constructor(options) {
|
||||||
|
this.workflowsPath = options.workflowsPath;
|
||||||
|
this.adapter = options.adapter;
|
||||||
|
this.enableDebugInspectors = options.enableDebugInspectors || false;
|
||||||
|
|
||||||
|
this.cloudRunner = null;
|
||||||
|
this.loadedWorkflows = new Map(); // name -> export data
|
||||||
|
this.events = new EventEmitter();
|
||||||
|
this.isInitialized = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the CloudRunner instance
|
||||||
|
*/
|
||||||
|
async initialize() {
|
||||||
|
if (this.isInitialized) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Dynamically import CloudRunner from noodl-viewer-cloud
|
||||||
|
// This package may not be available in all builds, so we handle the error gracefully
|
||||||
|
const viewerCloudPath = this.findViewerCloudPath();
|
||||||
|
|
||||||
|
if (viewerCloudPath) {
|
||||||
|
const { CloudRunner } = require(viewerCloudPath);
|
||||||
|
|
||||||
|
this.cloudRunner = new CloudRunner({
|
||||||
|
enableDebugInspectors: this.enableDebugInspectors,
|
||||||
|
connectToEditor: false // We're running standalone
|
||||||
|
});
|
||||||
|
|
||||||
|
// Inject our adapter into the runtime context
|
||||||
|
// This allows workflow nodes to access the database
|
||||||
|
this.injectAdapterIntoContext();
|
||||||
|
|
||||||
|
safeLog('CloudRunner initialized');
|
||||||
|
this.isInitialized = true;
|
||||||
|
} else {
|
||||||
|
safeLog('noodl-viewer-cloud not found, workflows disabled');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
safeLog('Failed to initialize CloudRunner:', error.message);
|
||||||
|
// Don't throw - workflow support is optional
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find the path to noodl-viewer-cloud package
|
||||||
|
*/
|
||||||
|
findViewerCloudPath() {
|
||||||
|
const possiblePaths = [
|
||||||
|
// Development: relative path from editor
|
||||||
|
path.resolve(__dirname, '..', '..', '..', '..', '..', 'noodl-viewer-cloud', 'src', 'index.ts'),
|
||||||
|
// Built: node_modules
|
||||||
|
'@noodl/viewer-cloud',
|
||||||
|
// Alternative built path
|
||||||
|
path.resolve(__dirname, '..', '..', '..', '..', '..', 'noodl-viewer-cloud', 'dist', 'index.js')
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const p of possiblePaths) {
|
||||||
|
try {
|
||||||
|
require.resolve(p);
|
||||||
|
return p;
|
||||||
|
} catch (e) {
|
||||||
|
// Path not available, try next
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inject the LocalSQLAdapter into the CloudRunner context
|
||||||
|
* This allows workflow nodes to perform database operations
|
||||||
|
*/
|
||||||
|
injectAdapterIntoContext() {
|
||||||
|
if (!this.cloudRunner) return;
|
||||||
|
|
||||||
|
// Access the runtime context and inject our adapter
|
||||||
|
// The runtime has a context property that nodes can access
|
||||||
|
const runtime = this.cloudRunner.runtime;
|
||||||
|
|
||||||
|
if (runtime && runtime.context) {
|
||||||
|
// Add a method to get the local adapter
|
||||||
|
runtime.context.getLocalAdapter = () => this.adapter;
|
||||||
|
|
||||||
|
// Also inject into Services if needed
|
||||||
|
if (!runtime.Services) {
|
||||||
|
runtime.Services = {};
|
||||||
|
}
|
||||||
|
runtime.Services.LocalBackend = {
|
||||||
|
getAdapter: () => this.adapter
|
||||||
|
};
|
||||||
|
|
||||||
|
safeLog('Adapter injected into runtime context');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load all workflows from the workflows directory
|
||||||
|
*/
|
||||||
|
async loadWorkflows() {
|
||||||
|
if (!this.cloudRunner) {
|
||||||
|
safeLog('CloudRunner not initialized, skipping workflow load');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Ensure directory exists
|
||||||
|
await fs.mkdir(this.workflowsPath, { recursive: true });
|
||||||
|
|
||||||
|
const files = await fs.readdir(this.workflowsPath);
|
||||||
|
const workflowFiles = files.filter((f) => f.endsWith('.workflow.json'));
|
||||||
|
|
||||||
|
safeLog(`Found ${workflowFiles.length} workflow files`);
|
||||||
|
|
||||||
|
// Load each workflow
|
||||||
|
for (const file of workflowFiles) {
|
||||||
|
try {
|
||||||
|
const filePath = path.join(this.workflowsPath, file);
|
||||||
|
const content = await fs.readFile(filePath, 'utf-8');
|
||||||
|
const exportData = JSON.parse(content);
|
||||||
|
|
||||||
|
// Extract workflow name from file (without .workflow.json)
|
||||||
|
const workflowName = file.replace('.workflow.json', '');
|
||||||
|
|
||||||
|
// Store for later reference
|
||||||
|
this.loadedWorkflows.set(workflowName, exportData);
|
||||||
|
|
||||||
|
// Load into CloudRunner
|
||||||
|
await this.cloudRunner.load(exportData);
|
||||||
|
|
||||||
|
safeLog(`Loaded workflow: ${workflowName}`);
|
||||||
|
} catch (e) {
|
||||||
|
safeLog(`Failed to load workflow ${file}:`, e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.events.emit('workflowsLoaded', {
|
||||||
|
count: this.loadedWorkflows.size,
|
||||||
|
names: Array.from(this.loadedWorkflows.keys())
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
if (error.code === 'ENOENT') {
|
||||||
|
safeLog('Workflows directory does not exist yet');
|
||||||
|
} else {
|
||||||
|
safeLog('Error loading workflows:', error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load or update a single workflow
|
||||||
|
* @param {string} name - Workflow name (without .workflow.json extension)
|
||||||
|
* @param {Object} exportData - The workflow export data
|
||||||
|
*/
|
||||||
|
async loadWorkflow(name, exportData) {
|
||||||
|
if (!this.cloudRunner) {
|
||||||
|
return { success: false, error: 'CloudRunner not initialized' };
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Save to file
|
||||||
|
const filePath = path.join(this.workflowsPath, `${name}.workflow.json`);
|
||||||
|
await fs.mkdir(this.workflowsPath, { recursive: true });
|
||||||
|
await fs.writeFile(filePath, JSON.stringify(exportData, null, 2));
|
||||||
|
|
||||||
|
// Update in-memory
|
||||||
|
this.loadedWorkflows.set(name, exportData);
|
||||||
|
|
||||||
|
// Reload into CloudRunner
|
||||||
|
await this.cloudRunner.load(exportData);
|
||||||
|
|
||||||
|
safeLog(`Workflow updated: ${name}`);
|
||||||
|
this.events.emit('workflowUpdated', { name });
|
||||||
|
|
||||||
|
return { success: true };
|
||||||
|
} catch (error) {
|
||||||
|
safeLog(`Failed to update workflow ${name}:`, error.message);
|
||||||
|
return { success: false, error: error.message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a workflow
|
||||||
|
* @param {string} name - Workflow name
|
||||||
|
*/
|
||||||
|
async deleteWorkflow(name) {
|
||||||
|
try {
|
||||||
|
const filePath = path.join(this.workflowsPath, `${name}.workflow.json`);
|
||||||
|
await fs.unlink(filePath);
|
||||||
|
this.loadedWorkflows.delete(name);
|
||||||
|
|
||||||
|
safeLog(`Workflow deleted: ${name}`);
|
||||||
|
this.events.emit('workflowDeleted', { name });
|
||||||
|
|
||||||
|
return { success: true };
|
||||||
|
} catch (error) {
|
||||||
|
safeLog(`Failed to delete workflow ${name}:`, error.message);
|
||||||
|
return { success: false, error: error.message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reload all workflows (hot reload)
|
||||||
|
*/
|
||||||
|
async reloadWorkflows() {
|
||||||
|
// Clear existing workflows
|
||||||
|
this.loadedWorkflows.clear();
|
||||||
|
|
||||||
|
// Re-initialize CloudRunner
|
||||||
|
if (this.cloudRunner) {
|
||||||
|
// Create new instance to clear state
|
||||||
|
const viewerCloudPath = this.findViewerCloudPath();
|
||||||
|
if (viewerCloudPath) {
|
||||||
|
const { CloudRunner } = require(viewerCloudPath);
|
||||||
|
this.cloudRunner = new CloudRunner({
|
||||||
|
enableDebugInspectors: this.enableDebugInspectors,
|
||||||
|
connectToEditor: false
|
||||||
|
});
|
||||||
|
this.injectAdapterIntoContext();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load all workflows again
|
||||||
|
await this.loadWorkflows();
|
||||||
|
|
||||||
|
safeLog('Workflows reloaded');
|
||||||
|
this.events.emit('workflowsReloaded', {
|
||||||
|
count: this.loadedWorkflows.size
|
||||||
|
});
|
||||||
|
|
||||||
|
return { success: true, count: this.loadedWorkflows.size };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute a workflow/cloud function
|
||||||
|
* @param {string} functionName - Name of the function to execute
|
||||||
|
* @param {Object} request - Request object with body and headers
|
||||||
|
* @returns {Promise<{statusCode: number, body: string}>}
|
||||||
|
*/
|
||||||
|
async run(functionName, request) {
|
||||||
|
if (!this.cloudRunner) {
|
||||||
|
return {
|
||||||
|
statusCode: 501,
|
||||||
|
body: JSON.stringify({ error: 'Workflows not enabled (CloudRunner not available)' })
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we have this function loaded
|
||||||
|
if (!this.hasFunction(functionName)) {
|
||||||
|
return {
|
||||||
|
statusCode: 404,
|
||||||
|
body: JSON.stringify({ error: `Function '${functionName}' not found` })
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const startTime = Date.now();
|
||||||
|
|
||||||
|
try {
|
||||||
|
safeLog(`Executing function: ${functionName}`);
|
||||||
|
|
||||||
|
// Execute via CloudRunner
|
||||||
|
const response = await this.cloudRunner.run(functionName, request);
|
||||||
|
|
||||||
|
const duration = Date.now() - startTime;
|
||||||
|
safeLog(`Function ${functionName} completed in ${duration}ms`);
|
||||||
|
|
||||||
|
// Emit execution event for logging/debugging
|
||||||
|
this.events.emit('functionExecuted', {
|
||||||
|
functionName,
|
||||||
|
duration,
|
||||||
|
statusCode: response.statusCode,
|
||||||
|
success: response.statusCode >= 200 && response.statusCode < 300
|
||||||
|
});
|
||||||
|
|
||||||
|
return response;
|
||||||
|
} catch (error) {
|
||||||
|
const duration = Date.now() - startTime;
|
||||||
|
safeLog(`Function ${functionName} failed after ${duration}ms:`, error.message);
|
||||||
|
|
||||||
|
this.events.emit('functionExecuted', {
|
||||||
|
functionName,
|
||||||
|
duration,
|
||||||
|
statusCode: 500,
|
||||||
|
success: false,
|
||||||
|
error: error.message
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
statusCode: 500,
|
||||||
|
body: JSON.stringify({ error: error.message })
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a function is available
|
||||||
|
* @param {string} functionName
|
||||||
|
*/
|
||||||
|
hasFunction(functionName) {
|
||||||
|
// CloudRunner looks for components starting with /#__cloud__/
|
||||||
|
// Check if any loaded workflow has this component
|
||||||
|
for (const [, exportData] of this.loadedWorkflows) {
|
||||||
|
if (exportData.components) {
|
||||||
|
const fullName = `/#__cloud__/${functionName}`;
|
||||||
|
if (exportData.components.some((c) => c.name === fullName)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get list of available functions
|
||||||
|
*/
|
||||||
|
getAvailableFunctions() {
|
||||||
|
const functions = [];
|
||||||
|
|
||||||
|
for (const [workflowName, exportData] of this.loadedWorkflows) {
|
||||||
|
if (exportData.components) {
|
||||||
|
for (const component of exportData.components) {
|
||||||
|
if (component.name.startsWith('/#__cloud__/')) {
|
||||||
|
functions.push({
|
||||||
|
name: component.name.replace('/#__cloud__/', ''),
|
||||||
|
workflow: workflowName
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return functions;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get status information
|
||||||
|
*/
|
||||||
|
getStatus() {
|
||||||
|
return {
|
||||||
|
initialized: this.isInitialized,
|
||||||
|
cloudRunnerAvailable: !!this.cloudRunner,
|
||||||
|
workflowCount: this.loadedWorkflows.size,
|
||||||
|
functions: this.getAvailableFunctions()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subscribe to events
|
||||||
|
*/
|
||||||
|
on(event, handler) {
|
||||||
|
this.events.on(event, handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
off(event, handler) {
|
||||||
|
this.events.off(event, handler);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { WorkflowRunner };
|
||||||
@@ -18,11 +18,13 @@
|
|||||||
|
|
||||||
const { LocalBackendServer, generateObjectId } = require('./LocalBackendServer');
|
const { LocalBackendServer, generateObjectId } = require('./LocalBackendServer');
|
||||||
const { BackendManager, backendManager, setupBackendIPC } = require('./BackendManager');
|
const { BackendManager, backendManager, setupBackendIPC } = require('./BackendManager');
|
||||||
|
const { WorkflowRunner } = require('./WorkflowRunner');
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
// Classes
|
// Classes
|
||||||
LocalBackendServer,
|
LocalBackendServer,
|
||||||
BackendManager,
|
BackendManager,
|
||||||
|
WorkflowRunner,
|
||||||
|
|
||||||
// Singleton instance
|
// Singleton instance
|
||||||
backendManager,
|
backendManager,
|
||||||
|
|||||||
Reference in New Issue
Block a user