@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.
Files changed (46) hide show
  1. package/dist/index.d.ts +2 -3
  2. package/dist/index.js +2 -3
  3. package/dist/src/call-step-file.d.ts +3 -2
  4. package/dist/src/call-step-file.js +13 -9
  5. package/dist/src/cron-handler.d.ts +12 -2
  6. package/dist/src/cron-handler.js +23 -18
  7. package/dist/src/event-manager.js +9 -4
  8. package/dist/src/flows-endpoint.d.ts +4 -3
  9. package/dist/src/flows-endpoint.js +5 -4
  10. package/dist/src/get-step-config.d.ts +2 -0
  11. package/dist/src/get-step-config.js +22 -0
  12. package/dist/src/guards.d.ts +1 -1
  13. package/dist/src/guards.js +0 -1
  14. package/dist/src/locked-data.d.ts +26 -0
  15. package/dist/src/locked-data.js +144 -0
  16. package/dist/src/logger.d.ts +1 -1
  17. package/dist/src/node/get-module-export.js +5 -2
  18. package/dist/src/node/logger.d.ts +11 -6
  19. package/dist/src/node/logger.js +29 -16
  20. package/dist/src/node/node-runner.js +3 -4
  21. package/dist/src/node/rpc.d.ts +2 -0
  22. package/dist/src/node/rpc.js +11 -1
  23. package/dist/src/printer.d.ts +17 -0
  24. package/dist/src/printer.js +74 -0
  25. package/dist/src/python/logger.py +3 -3
  26. package/dist/src/python/python-runner.py +21 -16
  27. package/dist/src/python/rpc.py +26 -15
  28. package/dist/src/python/rpc_state_manager.py +12 -11
  29. package/dist/src/ruby/get_config.rb +12 -22
  30. package/dist/src/ruby/logger.rb +23 -16
  31. package/dist/src/ruby/rpc.rb +123 -0
  32. package/dist/src/ruby/rpc_state_manager.rb +25 -0
  33. package/dist/src/ruby/ruby_runner.rb +42 -31
  34. package/dist/src/server.d.ts +8 -10
  35. package/dist/src/server.js +31 -13
  36. package/dist/src/step-handler-rpc-processor.d.ts +1 -0
  37. package/dist/src/step-handler-rpc-processor.js +11 -1
  38. package/dist/src/step-handlers.d.ts +8 -2
  39. package/dist/src/step-handlers.js +34 -20
  40. package/dist/src/step-validator.d.ts +14 -0
  41. package/dist/src/step-validator.js +99 -0
  42. package/dist/src/types.d.ts +20 -26
  43. package/dist/src/utils.d.ts +2 -0
  44. package/dist/src/utils.js +15 -0
  45. package/package.json +2 -1
  46. 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, sender: RpcSender):
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.sender = sender
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.sender.send_no_wait('log', log_entry)
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.sender = RpcSender()
27
- self.state = RpcStateManager(self.sender)
28
- self.logger = Logger(self.trace_id, self.flows, self.file_name, self.sender)
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.sender.send('emit', event)
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
- context.sender.init()
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
- import asyncio
73
- asyncio.run(run_python_module(file_path, parse_args(arg)))
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)
@@ -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] = (future, method, args)
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, _, _ = self.pending_requests[request_id]
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
- async def read_messages():
64
- while True:
65
- # Read message from pipe
66
- message = os.read(NODEIPCFD, 4096).decode('utf-8')
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
- # Start message reading loop
80
- asyncio.create_task(read_messages())
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, sender):
6
- self.sender = sender
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.sender.send('state.get', {'traceId': trace_id, 'key': key})
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.sender.send('state.set', {'traceId': trace_id, 'key': key, 'value': value})
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.sender.send('state.delete', {'traceId': trace_id, 'key': key})
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.sender.send('state.clear', {'traceId': trace_id})
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
- # Remove previous Config class if it exists
23
- Object.send(:remove_const, :Config) if Object.const_defined?(:Config)
24
-
25
- # Create a new binding for evaluation
26
- evaluation_binding = binding
27
-
28
- # Load and evaluate the file content in our binding
29
- file_content = File.read(file_path)
30
- evaluation_binding.eval(file_content)
31
-
32
- # Get the config variable from our binding
33
- config = evaluation_binding.eval('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
@@ -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 = File.basename(file_path)
8
+ @file_name = file_path.split('/').last
9
+ @sender = sender
9
10
  end
10
11
 
11
12
  def log(level, message, args = nil)
12
- # Ensure message is not nested JSON or a stringified JSON object
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) # Parse if valid JSON
16
+ message = JSON.parse(message)
16
17
  rescue JSON::ParserError
17
- # Leave message as is if it's not valid JSON
18
+ # Leave message as is if not valid JSON
18
19
  end
19
20
  end
20
21
 
21
- # Construct the base log entry
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
- # Merge additional arguments if provided
32
+ # Handle additional args consistently with Python/Node
27
33
  if args
28
- args = case args
29
- when OpenStruct then args.to_h
30
- when Hash then args
31
- else { data: args }
32
- end
33
- log_entry.merge!(args)
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
- # Generate JSON output
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 'state_adapter'
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
- # Emit a message to the parent process via Node IPC
26
- def emit(text)
27
- message = (JSON.dump(text) + "\n").encode('utf-8')
28
- IO.new(NODE_CHANNEL_FD, 'w').write(message)
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, file_name)
28
+ def initialize(rpc, args, file_path)
29
+ @rpc = rpc
32
30
  @trace_id = args.traceId
33
31
  @flows = args.flows
34
- @file_name = file_name
35
- @state = create_internal_state_manager(state_manager_url: args[:stateConfig]&.dig(:stateManagerUrl))
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
- unless File.exist?(file_path)
43
- raise LoadError, "Could not load module from #{file_path}"
44
- end
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
- # Load the file dynamically
47
- load file_path
45
+ # Load the file in a clean context
46
+ load file_path
48
47
 
49
- unless defined?(executor)
50
- raise NameError, "Function 'executor' not found in module #{file_path}"
51
- end
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
- context = Context.new(args, file_path)
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
- executor(args.data, context)
56
- rescue => e
57
- $stderr.puts "Error running Ruby module: #{e.message}"
58
- $stderr.puts e.backtrace
59
- exit 1
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