@motiadev/core 0.0.19 → 0.0.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +2 -3
- package/dist/index.js +2 -3
- package/dist/src/call-step-file.d.ts +3 -2
- package/dist/src/call-step-file.js +13 -9
- package/dist/src/cron-handler.d.ts +12 -2
- package/dist/src/cron-handler.js +23 -18
- package/dist/src/event-manager.js +9 -4
- package/dist/src/flows-endpoint.d.ts +4 -3
- package/dist/src/flows-endpoint.js +5 -4
- package/dist/src/get-step-config.d.ts +2 -0
- package/dist/src/get-step-config.js +22 -0
- package/dist/src/guards.d.ts +1 -1
- package/dist/src/guards.js +0 -1
- package/dist/src/locked-data.d.ts +26 -0
- package/dist/src/locked-data.js +144 -0
- package/dist/src/logger.d.ts +1 -1
- package/dist/src/node/get-module-export.js +5 -2
- package/dist/src/node/logger.d.ts +11 -6
- package/dist/src/node/logger.js +29 -16
- package/dist/src/node/node-runner.js +3 -4
- package/dist/src/node/rpc.d.ts +2 -0
- package/dist/src/node/rpc.js +11 -1
- package/dist/src/printer.d.ts +17 -0
- package/dist/src/printer.js +74 -0
- package/dist/src/python/logger.py +3 -3
- package/dist/src/python/python-runner.py +21 -16
- package/dist/src/python/rpc.py +26 -15
- package/dist/src/python/rpc_state_manager.py +12 -11
- package/dist/src/ruby/get_config.rb +12 -22
- package/dist/src/ruby/logger.rb +23 -16
- package/dist/src/ruby/rpc.rb +123 -0
- package/dist/src/ruby/rpc_state_manager.rb +25 -0
- package/dist/src/ruby/ruby_runner.rb +42 -31
- package/dist/src/server.d.ts +8 -10
- package/dist/src/server.js +31 -13
- package/dist/src/step-handler-rpc-processor.d.ts +1 -0
- package/dist/src/step-handler-rpc-processor.js +11 -1
- package/dist/src/step-handlers.d.ts +8 -2
- package/dist/src/step-handlers.js +34 -20
- package/dist/src/step-validator.d.ts +14 -0
- package/dist/src/step-validator.js +99 -0
- package/dist/src/types.d.ts +20 -26
- package/dist/src/utils.d.ts +2 -0
- package/dist/src/utils.js +15 -0
- package/package.json +2 -1
- package/dist/src/ruby/state_adapter.rb +0 -62
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.Printer = void 0;
|
|
7
|
+
const colors_1 = __importDefault(require("colors"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const guards_1 = require("./guards");
|
|
10
|
+
const stepTag = colors_1.default.bold(colors_1.default.magenta('Step'));
|
|
11
|
+
const flowTag = colors_1.default.bold(colors_1.default.blue('Flow'));
|
|
12
|
+
const created = colors_1.default.green('➜ [CREATED]');
|
|
13
|
+
const updated = colors_1.default.yellow('➜ [UPDATED]');
|
|
14
|
+
const removed = colors_1.default.red('➜ [REMOVED]');
|
|
15
|
+
const invalidEmit = colors_1.default.red('➜ [INVALID EMIT]');
|
|
16
|
+
const error = colors_1.default.red('[ERROR]');
|
|
17
|
+
class Printer {
|
|
18
|
+
constructor(baseDir) {
|
|
19
|
+
this.baseDir = baseDir;
|
|
20
|
+
}
|
|
21
|
+
printInvalidEmit(step, emit) {
|
|
22
|
+
console.log(`${invalidEmit} ${stepTag} ${this.getStepType(step)} ${this.getStepPath(step)} tried to emit an event not defined in the step config: ${colors_1.default.yellow(emit)}`);
|
|
23
|
+
}
|
|
24
|
+
printStepCreated(step) {
|
|
25
|
+
console.log(`${created} ${stepTag} ${this.getStepType(step)} ${this.getStepPath(step)} created`);
|
|
26
|
+
}
|
|
27
|
+
printStepUpdated(step) {
|
|
28
|
+
console.log(`${updated} ${stepTag} ${this.getStepType(step)} ${this.getStepPath(step)} updated`);
|
|
29
|
+
}
|
|
30
|
+
printStepRemoved(step) {
|
|
31
|
+
console.log(`${removed} ${stepTag} ${this.getStepType(step)} ${this.getStepPath(step)} removed`);
|
|
32
|
+
}
|
|
33
|
+
printFlowCreated(flowName) {
|
|
34
|
+
console.log(`${created} ${flowTag} ${colors_1.default.bold(colors_1.default.cyan(flowName))} created`);
|
|
35
|
+
}
|
|
36
|
+
printFlowUpdated(flowName) {
|
|
37
|
+
console.log(`${updated} ${flowTag} ${colors_1.default.bold(colors_1.default.cyan(flowName))} updated`);
|
|
38
|
+
}
|
|
39
|
+
printFlowRemoved(flowName) {
|
|
40
|
+
console.log(`${removed} ${flowTag} ${colors_1.default.bold(colors_1.default.cyan(flowName))} removed`);
|
|
41
|
+
}
|
|
42
|
+
printValidationError(stepPath, validationError) {
|
|
43
|
+
const relativePath = this.getRelativePath(stepPath);
|
|
44
|
+
console.log(`${error} ${colors_1.default.bold(colors_1.default.cyan(relativePath))}`);
|
|
45
|
+
validationError.errors?.forEach((error) => {
|
|
46
|
+
if (error.path) {
|
|
47
|
+
console.log(`${colors_1.default.red('│')} ${colors_1.default.yellow(`✖ ${error.path}`)}: ${error.message}`);
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
console.log(`${colors_1.default.red('│')} ${colors_1.default.yellow('✖')} ${error.message}`);
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
console.log(`${colors_1.default.red('└─')} ${colors_1.default.red(validationError.error)} `);
|
|
54
|
+
}
|
|
55
|
+
getRelativePath(filePath) {
|
|
56
|
+
return path_1.default.relative(this.baseDir, filePath);
|
|
57
|
+
}
|
|
58
|
+
getStepType(step) {
|
|
59
|
+
if ((0, guards_1.isApiStep)(step))
|
|
60
|
+
return colors_1.default.gray('(API)');
|
|
61
|
+
if ((0, guards_1.isEventStep)(step))
|
|
62
|
+
return colors_1.default.gray('(Event)');
|
|
63
|
+
if ((0, guards_1.isCronStep)(step))
|
|
64
|
+
return colors_1.default.gray('(Cron)');
|
|
65
|
+
if ((0, guards_1.isNoopStep)(step))
|
|
66
|
+
return colors_1.default.gray('(Noop)');
|
|
67
|
+
return colors_1.default.gray('(Unknown)');
|
|
68
|
+
}
|
|
69
|
+
getStepPath(step) {
|
|
70
|
+
const stepPath = this.getRelativePath(step.filePath);
|
|
71
|
+
return colors_1.default.bold(colors_1.default.cyan(stepPath));
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
exports.Printer = Printer;
|
|
@@ -3,11 +3,11 @@ from typing import Any, Dict, Optional
|
|
|
3
3
|
from rpc import RpcSender
|
|
4
4
|
|
|
5
5
|
class Logger:
|
|
6
|
-
def __init__(self, trace_id: str, flows: list[str], file_path: str,
|
|
6
|
+
def __init__(self, trace_id: str, flows: list[str], file_path: str, rpc: RpcSender):
|
|
7
7
|
self.trace_id = trace_id
|
|
8
8
|
self.flows = flows
|
|
9
9
|
self.file_name = file_path.split('/')[-1]
|
|
10
|
-
self.
|
|
10
|
+
self.rpc = rpc
|
|
11
11
|
|
|
12
12
|
def _log(self, level: str, message: str, args: Optional[Dict[str, Any]] = None) -> None:
|
|
13
13
|
log_entry = {
|
|
@@ -27,7 +27,7 @@ class Logger:
|
|
|
27
27
|
args = {"data": args}
|
|
28
28
|
log_entry.update(args)
|
|
29
29
|
|
|
30
|
-
self.
|
|
30
|
+
self.rpc.send_no_wait('log', log_entry)
|
|
31
31
|
|
|
32
32
|
def info(self, message: str, args: Optional[Any] = None) -> None:
|
|
33
33
|
self._log("info", message, args)
|
|
@@ -2,6 +2,7 @@ import sys
|
|
|
2
2
|
import json
|
|
3
3
|
import importlib.util
|
|
4
4
|
import traceback
|
|
5
|
+
import asyncio
|
|
5
6
|
import os
|
|
6
7
|
from logger import Logger
|
|
7
8
|
from rpc import RpcSender
|
|
@@ -19,18 +20,18 @@ def parse_args(arg: str) -> Any:
|
|
|
19
20
|
return arg
|
|
20
21
|
|
|
21
22
|
class Context:
|
|
22
|
-
def __init__(self, args: Any, file_name: str):
|
|
23
|
+
def __init__(self, args: Any, rpc: RpcSender, file_name: str):
|
|
23
24
|
self.trace_id = args.traceId
|
|
24
25
|
self.flows = args.flows
|
|
25
26
|
self.file_name = file_name
|
|
26
|
-
self.
|
|
27
|
-
self.state = RpcStateManager(
|
|
28
|
-
self.logger = Logger(self.trace_id, self.flows, self.file_name,
|
|
27
|
+
self.rpc = rpc
|
|
28
|
+
self.state = RpcStateManager(rpc)
|
|
29
|
+
self.logger = Logger(self.trace_id, self.flows, self.file_name, rpc)
|
|
29
30
|
|
|
30
31
|
async def emit(self, event: Any):
|
|
31
|
-
await self.
|
|
32
|
+
return await self.rpc.send('emit', event)
|
|
32
33
|
|
|
33
|
-
async def run_python_module(file_path: str, args: Any) -> None:
|
|
34
|
+
async def run_python_module(file_path: str, rpc: RpcSender, args: Any) -> None:
|
|
34
35
|
try:
|
|
35
36
|
# Construct path relative to steps directory
|
|
36
37
|
flows_dir = os.path.join(os.getcwd(), 'steps')
|
|
@@ -48,16 +49,16 @@ async def run_python_module(file_path: str, args: Any) -> None:
|
|
|
48
49
|
if not hasattr(module, 'handler'):
|
|
49
50
|
raise AttributeError(f"Function 'handler' not found in module {module_path}")
|
|
50
51
|
|
|
51
|
-
context = Context(args, file_path)
|
|
52
|
-
|
|
53
|
-
|
|
52
|
+
context = Context(args, rpc, file_path)
|
|
53
|
+
|
|
54
54
|
await module.handler(args.data, context)
|
|
55
|
-
|
|
56
|
-
# exit with 0 to indicate success
|
|
57
|
-
sys.exit(0)
|
|
58
|
-
except Exception as error:
|
|
59
|
-
print('Error running Python module:', file=sys.stderr)
|
|
60
55
|
|
|
56
|
+
rpc.close()
|
|
57
|
+
|
|
58
|
+
# We need this to close the process
|
|
59
|
+
rpc.send_no_wait('close', None)
|
|
60
|
+
except Exception as error:
|
|
61
|
+
print(f'Error running Python module: {error}', file=sys.stderr)
|
|
61
62
|
traceback.print_exc(file=sys.stderr)
|
|
62
63
|
sys.exit(1)
|
|
63
64
|
|
|
@@ -69,5 +70,9 @@ if __name__ == "__main__":
|
|
|
69
70
|
file_path = sys.argv[1]
|
|
70
71
|
arg = sys.argv[2] if len(sys.argv) > 2 else None
|
|
71
72
|
|
|
72
|
-
|
|
73
|
-
asyncio.
|
|
73
|
+
rpc = RpcSender()
|
|
74
|
+
loop = asyncio.get_event_loop()
|
|
75
|
+
# Create and gather tasks
|
|
76
|
+
tasks = asyncio.gather(rpc.init(), run_python_module(file_path, rpc, parse_args(arg)))
|
|
77
|
+
# Run until tasks complete
|
|
78
|
+
loop.run_until_complete(tasks)
|
package/dist/src/python/rpc.py
CHANGED
|
@@ -10,6 +10,7 @@ NODEIPCFD = int(os.environ["NODE_CHANNEL_FD"])
|
|
|
10
10
|
|
|
11
11
|
class RpcSender:
|
|
12
12
|
def __init__(self):
|
|
13
|
+
self.executing = True
|
|
13
14
|
self.pending_requests: Dict[str, Tuple[asyncio.Future, str, Any]] = {}
|
|
14
15
|
|
|
15
16
|
def send_no_wait(self, method: str, args: Any) -> None:
|
|
@@ -23,10 +24,10 @@ class RpcSender:
|
|
|
23
24
|
# send message
|
|
24
25
|
os.write(NODEIPCFD, bytesMessage)
|
|
25
26
|
|
|
26
|
-
async def send(self, method: str, args: Any) -> Any:
|
|
27
|
+
async def send(self, method: str, args: Any) -> asyncio.Future[Any]:
|
|
27
28
|
future = asyncio.Future()
|
|
28
29
|
request_id = str(uuid.uuid4())
|
|
29
|
-
self.pending_requests[request_id] =
|
|
30
|
+
self.pending_requests[request_id] = future
|
|
30
31
|
|
|
31
32
|
request = {
|
|
32
33
|
'type': 'rpc_request',
|
|
@@ -39,16 +40,17 @@ class RpcSender:
|
|
|
39
40
|
# send message
|
|
40
41
|
os.write(NODEIPCFD, bytesMessage)
|
|
41
42
|
|
|
42
|
-
return future
|
|
43
|
+
return await future
|
|
43
44
|
|
|
44
|
-
def init(self):
|
|
45
|
+
async def init(self):
|
|
45
46
|
def on_message(msg: Dict[str, Any]):
|
|
46
47
|
if msg.get('type') == 'rpc_response':
|
|
47
48
|
request_id = msg['id']
|
|
48
49
|
|
|
49
50
|
if request_id in self.pending_requests:
|
|
50
|
-
future
|
|
51
|
-
|
|
51
|
+
future = self.pending_requests[request_id]
|
|
52
|
+
del self.pending_requests[request_id]
|
|
53
|
+
|
|
52
54
|
if msg.get('error'):
|
|
53
55
|
future.set_exception(msg['error'])
|
|
54
56
|
elif msg.get('result'):
|
|
@@ -56,15 +58,16 @@ class RpcSender:
|
|
|
56
58
|
else:
|
|
57
59
|
# It's a void response
|
|
58
60
|
future.set_result(None)
|
|
59
|
-
|
|
60
|
-
del self.pending_requests[request_id]
|
|
61
|
-
|
|
61
|
+
|
|
62
62
|
# Read messages from Node IPC file descriptor
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
message =
|
|
63
|
+
while self.executing:
|
|
64
|
+
# Use asyncio to read from fd without blocking
|
|
65
|
+
try:
|
|
66
|
+
message = await asyncio.get_event_loop().run_in_executor(
|
|
67
|
+
None, lambda: os.read(NODEIPCFD, 4096).decode('utf-8')
|
|
68
|
+
)
|
|
67
69
|
if not message:
|
|
70
|
+
await asyncio.sleep(0.01) # Add small delay to prevent busy-waiting
|
|
68
71
|
continue
|
|
69
72
|
|
|
70
73
|
# Parse messages (may be multiple due to buffering)
|
|
@@ -75,6 +78,14 @@ class RpcSender:
|
|
|
75
78
|
on_message(msg)
|
|
76
79
|
except json.JSONDecodeError:
|
|
77
80
|
pass
|
|
81
|
+
except Exception as e:
|
|
82
|
+
print(f"Error reading from IPC: {e}", file=sys.stderr)
|
|
83
|
+
await asyncio.sleep(0.01)
|
|
78
84
|
|
|
79
|
-
|
|
80
|
-
|
|
85
|
+
def close(self) -> None:
|
|
86
|
+
self.executing = False
|
|
87
|
+
outstanding_requests = list(self.pending_requests.values())
|
|
88
|
+
|
|
89
|
+
if len(outstanding_requests) > 0:
|
|
90
|
+
print("Process ended while there are some promises outstanding", file=sys.stderr)
|
|
91
|
+
sys.exit(1)
|
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
from typing import Any
|
|
2
|
-
|
|
2
|
+
import asyncio
|
|
3
|
+
from rpc import RpcSender
|
|
3
4
|
|
|
4
5
|
class RpcStateManager:
|
|
5
|
-
def __init__(self,
|
|
6
|
-
self.
|
|
6
|
+
def __init__(self, rpc: RpcSender):
|
|
7
|
+
self.rpc = rpc
|
|
7
8
|
|
|
8
|
-
async def get(self, trace_id: str, key: str) -> Any:
|
|
9
|
-
return await self.
|
|
9
|
+
async def get(self, trace_id: str, key: str) -> asyncio.Future[Any]:
|
|
10
|
+
return await self.rpc.send('state.get', {'traceId': trace_id, 'key': key})
|
|
10
11
|
|
|
11
|
-
async def set(self, trace_id: str, key: str, value: Any) -> None:
|
|
12
|
-
await self.
|
|
12
|
+
async def set(self, trace_id: str, key: str, value: Any) -> asyncio.Future[None]:
|
|
13
|
+
return await self.rpc.send('state.set', {'traceId': trace_id, 'key': key, 'value': value})
|
|
13
14
|
|
|
14
|
-
async def delete(self, trace_id: str, key: str) -> None:
|
|
15
|
-
await self.
|
|
15
|
+
async def delete(self, trace_id: str, key: str) -> asyncio.Future[None]:
|
|
16
|
+
return await self.rpc.send('state.delete', {'traceId': trace_id, 'key': key})
|
|
16
17
|
|
|
17
|
-
async def clear(self, trace_id: str) -> None:
|
|
18
|
-
await self.
|
|
18
|
+
async def clear(self, trace_id: str) -> asyncio.Future[None]:
|
|
19
|
+
return await self.rpc.send('state.clear', {'traceId': trace_id})
|
|
@@ -19,28 +19,18 @@ end
|
|
|
19
19
|
|
|
20
20
|
def extract_config(file_path)
|
|
21
21
|
begin
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
config
|
|
34
|
-
|
|
35
|
-
# Convert config instance to hash with symbol keys
|
|
36
|
-
{
|
|
37
|
-
type: config.type,
|
|
38
|
-
name: config.name,
|
|
39
|
-
subscribes: config.subscribes,
|
|
40
|
-
emits: config.emits,
|
|
41
|
-
input: config.input,
|
|
42
|
-
flows: config.flows
|
|
43
|
-
}
|
|
22
|
+
unless File.exist?(file_path)
|
|
23
|
+
raise LoadError, "Could not load module from #{file_path}"
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Load the file in a clean context
|
|
27
|
+
load file_path
|
|
28
|
+
|
|
29
|
+
unless defined?(config)
|
|
30
|
+
raise NameError, "Function 'config' not found in module #{file_path}"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
config()
|
|
44
34
|
rescue NameError => e
|
|
45
35
|
raise "Error accessing config: #{e.message}"
|
|
46
36
|
rescue => e
|
package/dist/src/ruby/logger.rb
CHANGED
|
@@ -2,39 +2,46 @@ require 'json'
|
|
|
2
2
|
require 'time'
|
|
3
3
|
|
|
4
4
|
class CustomLogger
|
|
5
|
-
def initialize(trace_id, flows, file_path)
|
|
5
|
+
def initialize(trace_id, flows, file_path, sender)
|
|
6
6
|
@trace_id = trace_id
|
|
7
7
|
@flows = flows
|
|
8
|
-
@file_name =
|
|
8
|
+
@file_name = file_path.split('/').last
|
|
9
|
+
@sender = sender
|
|
9
10
|
end
|
|
10
11
|
|
|
11
12
|
def log(level, message, args = nil)
|
|
12
|
-
#
|
|
13
|
+
# Handle message formatting consistently with Python/Node
|
|
13
14
|
if message.is_a?(String) && message.strip.start_with?('{', '[')
|
|
14
15
|
begin
|
|
15
|
-
message = JSON.parse(message)
|
|
16
|
+
message = JSON.parse(message)
|
|
16
17
|
rescue JSON::ParserError
|
|
17
|
-
# Leave message as is if
|
|
18
|
+
# Leave message as is if not valid JSON
|
|
18
19
|
end
|
|
19
20
|
end
|
|
20
21
|
|
|
21
|
-
# Construct the
|
|
22
|
+
# Construct the log entry to match Python/Node format
|
|
22
23
|
log_entry = {
|
|
24
|
+
level: level,
|
|
25
|
+
time: (Time.now.to_f * 1000).to_i, # Milliseconds since epoch to match Node
|
|
26
|
+
traceId: @trace_id,
|
|
27
|
+
flows: @flows,
|
|
28
|
+
file: @file_name,
|
|
23
29
|
msg: message
|
|
24
30
|
}
|
|
25
31
|
|
|
26
|
-
#
|
|
32
|
+
# Handle additional args consistently with Python/Node
|
|
27
33
|
if args
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
+
case args
|
|
35
|
+
when OpenStruct
|
|
36
|
+
log_entry.merge!(args.to_h)
|
|
37
|
+
when Hash
|
|
38
|
+
log_entry.merge!(args)
|
|
39
|
+
else
|
|
40
|
+
log_entry[:data] = args
|
|
41
|
+
end
|
|
34
42
|
end
|
|
35
43
|
|
|
36
|
-
|
|
37
|
-
puts JSON.dump(log_entry)
|
|
44
|
+
@sender.send_no_wait('log', log_entry)
|
|
38
45
|
end
|
|
39
46
|
|
|
40
47
|
def info(message, args = nil)
|
|
@@ -52,4 +59,4 @@ class CustomLogger
|
|
|
52
59
|
def warn(message, args = nil)
|
|
53
60
|
log('warn', message, args)
|
|
54
61
|
end
|
|
55
|
-
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
require 'securerandom'
|
|
2
|
+
require 'json'
|
|
3
|
+
require 'thread'
|
|
4
|
+
|
|
5
|
+
# Get the file descriptor from ENV
|
|
6
|
+
NODE_CHANNEL_FD = ENV['NODE_CHANNEL_FD'].to_i
|
|
7
|
+
|
|
8
|
+
class RpcSender
|
|
9
|
+
def initialize
|
|
10
|
+
@closed = false
|
|
11
|
+
@writer = IO.new(NODE_CHANNEL_FD, 'w')
|
|
12
|
+
@reader = IO.new(NODE_CHANNEL_FD, 'r')
|
|
13
|
+
@pending_requests = {}
|
|
14
|
+
@mutex = Mutex.new
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def send(method, args)
|
|
18
|
+
return nil if @closed
|
|
19
|
+
|
|
20
|
+
id = SecureRandom.uuid
|
|
21
|
+
promise = Queue.new
|
|
22
|
+
|
|
23
|
+
@mutex.synchronize do
|
|
24
|
+
@pending_requests[id] = promise
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
data = { type: 'rpc_request', id: id, method: method, args: args }
|
|
28
|
+
message = (JSON.dump(data) + "\n").encode('utf-8')
|
|
29
|
+
|
|
30
|
+
@writer.write(message)
|
|
31
|
+
@writer.flush
|
|
32
|
+
|
|
33
|
+
result = promise.pop
|
|
34
|
+
raise result if result.is_a?(StandardError)
|
|
35
|
+
result
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def send_no_wait(method, args)
|
|
39
|
+
return if @closed
|
|
40
|
+
|
|
41
|
+
data = { type: 'rpc_request', method: method, args: args }
|
|
42
|
+
message = (JSON.dump(data) + "\n").encode('utf-8')
|
|
43
|
+
|
|
44
|
+
@writer.write(message)
|
|
45
|
+
@writer.flush
|
|
46
|
+
rescue IOError, Errno::EBADF
|
|
47
|
+
# Ignore errors during shutdown
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def init
|
|
51
|
+
@background_thread = Thread.new do
|
|
52
|
+
until @closed
|
|
53
|
+
begin
|
|
54
|
+
line = @reader.gets
|
|
55
|
+
if line
|
|
56
|
+
msg = JSON.parse(line)
|
|
57
|
+
handle_response(msg) if msg['type'] == 'rpc_response'
|
|
58
|
+
end
|
|
59
|
+
rescue IOError, Errno::EBADF
|
|
60
|
+
break if @closed
|
|
61
|
+
rescue => e
|
|
62
|
+
puts "Error in RPC thread: #{e.message}"
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
@background_thread.abort_on_exception = true
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def handle_response(msg)
|
|
71
|
+
id = msg['id']
|
|
72
|
+
return unless id
|
|
73
|
+
|
|
74
|
+
@mutex.synchronize do
|
|
75
|
+
if promise = @pending_requests.delete(id)
|
|
76
|
+
if msg['error']
|
|
77
|
+
promise.push(StandardError.new(msg['error']))
|
|
78
|
+
else
|
|
79
|
+
promise.push(msg['result'])
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def close
|
|
86
|
+
return if @closed
|
|
87
|
+
@closed = true
|
|
88
|
+
|
|
89
|
+
# Send final close message
|
|
90
|
+
begin
|
|
91
|
+
send_no_wait('close', nil)
|
|
92
|
+
rescue
|
|
93
|
+
# Ignore errors during shutdown
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Clean up background thread
|
|
97
|
+
if @background_thread
|
|
98
|
+
@background_thread.kill rescue nil
|
|
99
|
+
@background_thread.join(1) rescue nil
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Check for pending requests
|
|
103
|
+
@mutex.synchronize do
|
|
104
|
+
if @pending_requests.any?
|
|
105
|
+
puts 'Process ended while there are some promises outstanding'
|
|
106
|
+
exit(1)
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Close file descriptors
|
|
111
|
+
begin
|
|
112
|
+
@writer.close unless @writer.closed?
|
|
113
|
+
rescue
|
|
114
|
+
# Ignore errors during shutdown
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
begin
|
|
118
|
+
@reader.close unless @reader.closed?
|
|
119
|
+
rescue
|
|
120
|
+
# Ignore errors during shutdown
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
class RpcStateManager
|
|
2
|
+
def initialize(sender)
|
|
3
|
+
@sender = sender
|
|
4
|
+
end
|
|
5
|
+
|
|
6
|
+
def get(trace_id, key)
|
|
7
|
+
# Return promise to match Python/Node behavior
|
|
8
|
+
@sender.send('state.get', { traceId: trace_id, key: key })
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def set(trace_id, key, value)
|
|
12
|
+
# Return promise to match Python/Node behavior
|
|
13
|
+
@sender.send('state.set', { traceId: trace_id, key: key, value: value })
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def delete(trace_id, key)
|
|
17
|
+
# Return promise to match Python/Node behavior
|
|
18
|
+
@sender.send('state.delete', { traceId: trace_id, key: key })
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def clear(trace_id)
|
|
22
|
+
# Return promise to match Python/Node behavior
|
|
23
|
+
@sender.send('state.clear', { traceId: trace_id })
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -3,9 +3,9 @@ require 'io/console'
|
|
|
3
3
|
require 'ostruct'
|
|
4
4
|
require 'pathname'
|
|
5
5
|
require_relative 'logger'
|
|
6
|
-
require_relative '
|
|
6
|
+
require_relative 'rpc'
|
|
7
|
+
require_relative 'rpc_state_manager'
|
|
7
8
|
|
|
8
|
-
# Parse arguments as JSON or fallback to raw string
|
|
9
9
|
def parse_args(arg)
|
|
10
10
|
begin
|
|
11
11
|
JSON.parse(arg, object_class: OpenStruct)
|
|
@@ -15,51 +15,61 @@ def parse_args(arg)
|
|
|
15
15
|
end
|
|
16
16
|
end
|
|
17
17
|
|
|
18
|
-
# Get the file descriptor from ENV
|
|
19
|
-
NODE_CHANNEL_FD = ENV['NODE_CHANNEL_FD'].to_i
|
|
20
|
-
|
|
21
|
-
# Context class for managing the execution environment
|
|
22
18
|
class Context
|
|
23
19
|
attr_reader :trace_id, :flows, :file_name, :state, :logger
|
|
24
20
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
21
|
+
def emit(event)
|
|
22
|
+
# Add type field if not present to match Node.js/Python behavior
|
|
23
|
+
event = { type: event[:type] || event['type'], data: event[:data] || event['data'] } unless event.is_a?(String)
|
|
24
|
+
promise = @rpc.send('emit', event)
|
|
25
|
+
promise # Return promise to maintain async pattern
|
|
29
26
|
end
|
|
30
27
|
|
|
31
|
-
def initialize(args,
|
|
28
|
+
def initialize(rpc, args, file_path)
|
|
29
|
+
@rpc = rpc
|
|
32
30
|
@trace_id = args.traceId
|
|
33
31
|
@flows = args.flows
|
|
34
|
-
@file_name =
|
|
35
|
-
@state =
|
|
36
|
-
@logger = CustomLogger.new(@trace_id, @flows, @file_name)
|
|
32
|
+
@file_name = file_path.split('/').last # Consistent with Python/Node
|
|
33
|
+
@state = RpcStateManager.new(rpc)
|
|
34
|
+
@logger = CustomLogger.new(@trace_id, @flows, @file_name, @rpc)
|
|
37
35
|
end
|
|
38
36
|
end
|
|
39
37
|
|
|
40
|
-
# Dynamically load and execute a Ruby script
|
|
41
38
|
def run_ruby_module(file_path, args)
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
39
|
+
rpc = nil
|
|
40
|
+
begin
|
|
41
|
+
unless File.exist?(file_path)
|
|
42
|
+
raise LoadError, "Could not load module from #{file_path}"
|
|
43
|
+
end
|
|
45
44
|
|
|
46
|
-
|
|
47
|
-
|
|
45
|
+
# Load the file in a clean context
|
|
46
|
+
load file_path
|
|
48
47
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
48
|
+
unless defined?(handler)
|
|
49
|
+
raise NameError, "Function 'handler' not found in module #{file_path}"
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
rpc = RpcSender.new
|
|
53
|
+
context = Context.new(rpc, args, file_path)
|
|
54
|
+
rpc.init
|
|
52
55
|
|
|
53
|
-
|
|
56
|
+
# Call handler and wait for any promises
|
|
57
|
+
result = handler(args.data, context)
|
|
58
|
+
|
|
59
|
+
# If handler returns a promise-like object, wait for it
|
|
60
|
+
if result.respond_to?(:value)
|
|
61
|
+
result.value
|
|
62
|
+
end
|
|
54
63
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
64
|
+
rescue => e
|
|
65
|
+
$stderr.puts "Error running Ruby module: #{e.message}"
|
|
66
|
+
$stderr.puts e.backtrace
|
|
67
|
+
raise
|
|
68
|
+
ensure
|
|
69
|
+
rpc&.close if rpc
|
|
70
|
+
end
|
|
60
71
|
end
|
|
61
72
|
|
|
62
|
-
# Entry point
|
|
63
73
|
if __FILE__ == $PROGRAM_NAME
|
|
64
74
|
if ARGV.length < 1
|
|
65
75
|
$stderr.puts 'Usage: ruby ruby-runner.rb <file-path> <arg>'
|
|
@@ -72,9 +82,10 @@ if __FILE__ == $PROGRAM_NAME
|
|
|
72
82
|
begin
|
|
73
83
|
parsed_args = parse_args(arg)
|
|
74
84
|
run_ruby_module(file_path, parsed_args)
|
|
85
|
+
exit 0
|
|
75
86
|
rescue => e
|
|
76
87
|
$stderr.puts "Error: #{e.message}"
|
|
77
88
|
$stderr.puts e.backtrace
|
|
78
89
|
exit 1
|
|
79
90
|
end
|
|
80
|
-
end
|
|
91
|
+
end
|