nodo 1.5.1 → 1.5.6

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c2a8ef54e67cf745b78353e740a440edf45ee300e97d50d7e0edd93abf33bf62
4
- data.tar.gz: 4c5196d63edf85242640050487a86b582fb2fe9d373b79408750973b88622a68
3
+ metadata.gz: 650d220075d68a34268a03f2e68587b1caab13f47707c8b44f7cb7d05f5a5588
4
+ data.tar.gz: 0b73f0cd827a1ab364975d3559f64f2d9c49afe265759715f92d23f133bab10c
5
5
  SHA512:
6
- metadata.gz: aad587338156426fd70ba262b01086056e70730aeff6774e70272c01898c9ba81e2937121226304dc00cd38f83917a788da5507160577747829a595a03a37e13
7
- data.tar.gz: 14abb31d37eb578bba1233afa22c6f1d5c79ea28d9cb48eefa052604a3d148494f58c7af019b6ec85682348d357fdef79d14fea2e4d90cb30de96a6222b5935f
6
+ metadata.gz: 0c1049cd1e574c0a7ec503ef01ae735d7b3ca33d21695a1a76c4c8f5c92f0914db58b59136a7ea0bcf17b83f9c6776817b8b4890061e6c442e414c5476d80037
7
+ data.tar.gz: d5fd39f74f9ebfabaec0463dbf7234e6da34a73b3167f2580fb6ac276f52f6aef433156ef0917b4a5c9c344f1dfc6d0f28e7c1af1bfe329829c5c87068d932dd
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- [![Gem Version](https://badge.fury.io/rb/nodo.svg)](http://badge.fury.io/rb/nodo)
1
+ [![Gem Version](https://badge.fury.io/rb/nodo.svg)](http://badge.fury.io/rb/nodo) [![build](https://github.com/mtgrosser/nodo/actions/workflows/build.yml/badge.svg)](https://github.com/mtgrosser/nodo/actions/workflows/build.yml)
2
2
 
3
3
  # Nōdo – call Node.js from Ruby
4
4
 
@@ -39,13 +39,13 @@ In Nodo, you define JS functions as you would define Ruby methods:
39
39
 
40
40
  ```ruby
41
41
  class Foo < Nodo::Core
42
-
42
+
43
43
  function :say_hi, <<~JS
44
44
  (name) => {
45
45
  return `Hello ${name}!`;
46
46
  }
47
47
  JS
48
-
48
+
49
49
  end
50
50
 
51
51
  foo = Foo.new
@@ -61,7 +61,7 @@ Install your modules to `node_modules`:
61
61
  $ yarn add uuid
62
62
  ```
63
63
 
64
- Then `require` your dependencies:
64
+ `require`ing your dependencies will make the library available as a `const` with the same name:
65
65
 
66
66
  ```ruby
67
67
  class Bar < Nodo::Core
@@ -78,14 +78,26 @@ bar = Bar.new
78
78
  bar.v4 => "b305f5c4-db9a-4504-b0c3-4e097a5ec8b9"
79
79
  ```
80
80
 
81
+
81
82
  ### Aliasing requires
82
83
 
84
+ If the library name cannot be used as name of the constant, the `const` name
85
+ can be given using hash syntax:
86
+
83
87
  ```ruby
84
88
  class FooBar < Nodo::Core
85
89
  require commonjs: '@rollup/plugin-commonjs'
86
90
  end
87
91
  ```
88
92
 
93
+ ### Alternate function definition syntax
94
+
95
+ JS code can also be supplied using the `code:` keyword argument:
96
+
97
+ ```ruby
98
+ function :hello, code: "() => 'world'"
99
+ ```
100
+
89
101
  ### Setting NODE_PATH
90
102
 
91
103
  By default, `./node_modules` is used as the `NODE_PATH`.
@@ -126,7 +138,8 @@ end
126
138
 
127
139
  ### Inheritance
128
140
 
129
- Subclasses will inherit functions, constants, dependencies and scripts from their superclasses, while only functions can be overwritten.
141
+ Subclasses will inherit functions, constants, dependencies and scripts from
142
+ their superclasses, while only functions can be overwritten.
130
143
 
131
144
  ```ruby
132
145
  class Foo < Nodo::Core
@@ -149,7 +162,8 @@ SubSubFoo.new.bar => "callingsubsubclass"
149
162
  ### Async functions
150
163
 
151
164
  `Nodo` supports calling `async` functions from Ruby.
152
- The Ruby call will happen synchronously, i.e. it will block until the JS function resolves:
165
+ The Ruby call will happen synchronously, i.e. it will block until the JS
166
+ function resolves:
153
167
 
154
168
  ```ruby
155
169
  class SyncFoo < Nodo::Core
@@ -158,3 +172,42 @@ class SyncFoo < Nodo::Core
158
172
  JS
159
173
  end
160
174
  ```
175
+
176
+ ### Limiting function execution time
177
+
178
+ The default timeout for a single JS function call is 60 seconds due to the
179
+ `Net::HTTP` default. It can be overridden on a per-function basis:
180
+
181
+ ```ruby
182
+ class Foo < Nodo::Core
183
+ function :sleep, timeout: 1, code: <<~'JS'
184
+ async (sec) => await new Promise(resolve => setTimeout(resolve, sec * 1000))
185
+ JS
186
+ end
187
+
188
+ Foo.new.sleep(2)
189
+ => Nodo::TimeoutError raised
190
+ ```
191
+
192
+ ### Logging
193
+
194
+ By default, JS errors will be logged to `STDOUT`.
195
+
196
+ To set a custom logger:
197
+
198
+ ```ruby
199
+ Nodo.logger = Logger.new('nodo.log')
200
+ ```
201
+
202
+ In Rails applications, `Rails.logger` will automatically be set.
203
+
204
+ ### Debugging
205
+
206
+ To get verbose debug output, set
207
+
208
+ ```ruby
209
+ Nodo.debug = true
210
+ ```
211
+
212
+ before instantiating any worker instances. The debug mode will be active during
213
+ the current process run.
data/lib/nodo/client.rb CHANGED
@@ -28,10 +28,10 @@ module Nodo
28
28
 
29
29
  def connect_unix
30
30
  s = Timeout.timeout(@open_timeout) { UNIXSocket.open(@socket_path) }
31
- @socket = Net::BufferedIO.new(s)
32
- @socket.read_timeout = @read_timeout
33
- @socket.continue_timeout = @continue_timeout
34
- @socket.debug_output = @debug_output
31
+ @socket = Net::BufferedIO.new(s, read_timeout: @read_timeout,
32
+ write_timeout: @write_timeout,
33
+ continue_timeout: @continue_timeout,
34
+ debug_output: @debug_output)
35
35
  on_connect
36
36
  end
37
37
  end
data/lib/nodo/core.rb CHANGED
@@ -12,6 +12,7 @@ module Nodo
12
12
  @@mutex = Mutex.new
13
13
 
14
14
  class << self
15
+ extend Forwardable
15
16
 
16
17
  attr_accessor :class_defined
17
18
 
@@ -25,6 +26,10 @@ module Nodo
25
26
  @instance ||= new
26
27
  end
27
28
 
29
+ def class_function(*methods)
30
+ singleton_class.def_delegators(:instance, *methods)
31
+ end
32
+
28
33
  def class_defined?
29
34
  !!class_defined
30
35
  end
@@ -57,8 +62,13 @@ module Nodo
57
62
  self.dependencies = dependencies + mods.merge(deps).map { |name, package| Dependency.new(name, package) }
58
63
  end
59
64
 
60
- def function(name, code)
61
- self.functions = functions.merge(name => Function.new(name, code, caller.first))
65
+ def function(name, _code = nil, timeout: 60, code: nil)
66
+ raise ArgumentError, "reserved method name #{name.inspect}" if Nodo::Core.method_defined?(name) || name.to_s == DEFINE_METHOD
67
+ code = (code ||= _code).strip
68
+ raise ArgumentError, 'function code is required' if '' == code
69
+ loc = caller_locations(1, 1)[0]
70
+ source_location = "#{loc.path}:#{loc.lineno}: in `#{name}'"
71
+ self.functions = functions.merge(name => Function.new(name, _code || code, source_location, timeout))
62
72
  define_method(name) { |*args| call_js_method(name, args) }
63
73
  end
64
74
 
@@ -80,11 +90,13 @@ module Nodo
80
90
  process.exit(1);
81
91
  }
82
92
 
93
+ process.title = `nodo-core ${socket}`;
94
+
83
95
  const shutdown = () => {
84
96
  nodo.core.close(() => { process.exit(0) });
85
97
  };
86
98
 
87
- process.on('SIGINT', shutdown);
99
+ // process.on('SIGINT', shutdown);
88
100
  process.on('SIGTERM', shutdown);
89
101
 
90
102
  nodo.core.run(socket);
@@ -104,16 +116,6 @@ module Nodo
104
116
  })()
105
117
  JS
106
118
  end
107
-
108
- protected
109
-
110
- def finalize(pid, tmpdir)
111
- proc do
112
- Process.kill(:SIGTERM, pid)
113
- Process.wait(pid)
114
- FileUtils.remove_entry(tmpdir) if File.directory?(tmpdir)
115
- end
116
- end
117
119
 
118
120
  private
119
121
 
@@ -146,6 +148,11 @@ module Nodo
146
148
  self.class.clsid
147
149
  end
148
150
 
151
+ def log_exception(e)
152
+ return unless logger = Nodo.logger
153
+ logger.error "\n#{e.class} (#{e.message}):\n\n#{e.backtrace.join("\n")}"
154
+ end
155
+
149
156
  def ensure_process_is_spawned
150
157
  return if node_pid
151
158
  spawn_process
@@ -155,23 +162,32 @@ module Nodo
155
162
  return if self.class.class_defined?
156
163
  call_js_method(DEFINE_METHOD, self.class.generate_class_code)
157
164
  self.class.class_defined = true
158
- # rescue => e
159
- # raise Error, e.message
160
165
  end
161
-
166
+
162
167
  def spawn_process
163
168
  @@tmpdir = Pathname.new(Dir.mktmpdir('nodo'))
164
169
  env = Nodo.env.merge('NODE_PATH' => Nodo.modules_root.to_s)
165
- @@node_pid = Process.spawn(env, Nodo.binary, '-e', self.class.generate_core_code, '--', socket_path.to_s)
166
- ObjectSpace.define_finalizer(self, self.class.send(:finalize, node_pid, tmpdir))
170
+ env['NODO_DEBUG'] = '1' if Nodo.debug
171
+ @@node_pid = Process.spawn(env, Nodo.binary, '-e', self.class.generate_core_code, '--', socket_path.to_s, err: :out)
172
+ at_exit do
173
+ Process.kill(:SIGTERM, node_pid) rescue Errno::ECHILD
174
+ Process.wait(node_pid) rescue Errno::ECHILD
175
+ FileUtils.remove_entry(tmpdir) if File.directory?(tmpdir)
176
+ end
167
177
  end
168
178
 
169
179
  def wait_for_socket
170
180
  start = Time.now
171
- until socket_path.exist?
172
- raise TimeoutError, "socket #{socket_path} not found" if Time.now - start > TIMEOUT
173
- sleep(0.2)
181
+ socket = nil
182
+ while Time.now - start < TIMEOUT
183
+ begin
184
+ break if socket = UNIXSocket.new(socket_path)
185
+ rescue Errno::ENOENT, Errno::ECONNREFUSED, Errno::ENOTDIR
186
+ Kernel.sleep(0.2)
187
+ end
174
188
  end
189
+ socket.close if socket
190
+ raise TimeoutError, "could not connect to socket #{socket_path}" unless socket
175
191
  end
176
192
 
177
193
  def call_js_method(method, args)
@@ -182,12 +198,15 @@ module Nodo
182
198
  request = Net::HTTP::Post.new("/#{clsid}/#{method}", 'Content-Type': 'application/json')
183
199
  request.body = JSON.dump(args)
184
200
  client = Client.new("unix://#{socket_path}")
201
+ client.read_timeout = function.timeout if function
185
202
  response = client.request(request)
186
203
  if response.is_a?(Net::HTTPOK)
187
204
  parse_response(response)
188
205
  else
189
206
  handle_error(response, function)
190
207
  end
208
+ rescue Net::ReadTimeout
209
+ raise TimeoutError, "function call #{self.class}##{method} timed out"
191
210
  rescue Errno::EPIPE, IOError
192
211
  # TODO: restart or something? If this happens the process is completely broken
193
212
  raise Error, 'Node process failed'
@@ -196,13 +215,25 @@ module Nodo
196
215
  def handle_error(response, function)
197
216
  if response.body
198
217
  result = parse_response(response)
199
- raise JavaScriptError.new(result['error'], function) if result.is_a?(Hash) && result.key?('error')
218
+ error = JavaScriptError.new(result['error'], function) if result.is_a?(Hash) && result.key?('error')
200
219
  end
201
- raise CallError, "Node returned #{response.code}"
220
+ error ||= CallError.new("Node returned #{response.code}")
221
+ log_exception(error)
222
+ raise error
202
223
  end
203
224
 
204
225
  def parse_response(response)
205
- JSON.parse(response.body.force_encoding('UTF-8'))
226
+ data = response.body.force_encoding('UTF-8')
227
+ JSON.parse(data) unless data == ''
228
+ end
229
+
230
+ def with_tempfile(name)
231
+ ext = File.extname(name)
232
+ result = nil
233
+ Tempfile.create([File.basename(name, ext), ext], tmpdir) do |file|
234
+ result = yield(file)
235
+ end
236
+ result
206
237
  end
207
238
 
208
239
  end
data/lib/nodo/function.rb CHANGED
@@ -1,9 +1,9 @@
1
1
  module Nodo
2
2
  class Function
3
- attr_reader :name, :code, :source_location
3
+ attr_reader :name, :code, :source_location, :timeout
4
4
 
5
- def initialize(name, code, source_location)
6
- @name, @code, @source_location = name, code, source_location
5
+ def initialize(name, code, source_location, timeout)
6
+ @name, @code, @source_location, @timeout = name, code, source_location, timeout
7
7
  end
8
8
 
9
9
  def to_js
data/lib/nodo/nodo.js CHANGED
@@ -1,5 +1,6 @@
1
1
  module.exports = (function() {
2
2
  const DEFINE_METHOD = '__nodo_define_class__';
3
+ const DEBUG = process.env.NODO_DEBUG;
3
4
 
4
5
  const vm = require('vm');
5
6
  const http = require('http');
@@ -24,7 +25,7 @@ module.exports = (function() {
24
25
  function respond_with_error(res, code, name) {
25
26
  res.statusCode = code;
26
27
  const rendered = render_error(name);
27
- log(`Error ${code} ${rendered}`);
28
+ debug(`Error ${code} ${rendered}`);
28
29
  res.end(rendered, 'utf8');
29
30
  }
30
31
 
@@ -35,22 +36,24 @@ module.exports = (function() {
35
36
  if (start) {
36
37
  timing = ` in ${(performance.now() - start).toFixed(2)}ms`;
37
38
  }
38
- log(`Completed 200 OK${timing}`);
39
+ debug(`Completed 200 OK${timing}\n`);
39
40
  }
40
41
 
41
- function log(message) {
42
- // fs.appendFileSync('log/nodo.log', `${message}\n`);
43
- // console.log(`[Nodo] ${message}`);
42
+ function debug(message) {
43
+ if (DEBUG) {
44
+ // fs.appendFileSync('log/nodo.log', `${message}\n`);
45
+ console.log(`[Nodo] ${message}`);
46
+ }
44
47
  }
45
48
 
46
49
  const core = {
47
50
  run: (socket) => {
48
- log('Starting up...');
51
+ debug('Starting up...');
49
52
  server = http.createServer((req, res) => {
50
53
  const start = performance.now();
51
54
 
52
55
  res.setHeader('Content-Type', 'application/json');
53
- log(`${req.method} ${req.url}`);
56
+ debug(`${req.method} ${req.url}`);
54
57
 
55
58
  if (req.method !== 'POST' || !req.url.startsWith('/')) {
56
59
  return respond_with_error(res, 405, 'Method Not Allowed');
@@ -103,12 +106,12 @@ module.exports = (function() {
103
106
 
104
107
  //server.maxConnections = 64;
105
108
  server.listen(socket, () => {
106
- log(`server ready, listening on ${socket} (max connections: ${server.maxConnections})`);
109
+ debug(`server ready, listening on ${socket} (max connections: ${server.maxConnections})`);
107
110
  });
108
111
  },
109
112
 
110
113
  close: (finalizer) => {
111
- log("Shutting down");
114
+ debug("Shutting down");
112
115
  if (!closing) {
113
116
  closing = true;
114
117
  server.close(finalizer);
@@ -116,5 +119,5 @@ module.exports = (function() {
116
119
  }
117
120
  };
118
121
 
119
- return { core: core, log: log };
122
+ return { core: core, debug: debug };
120
123
  })();
data/lib/nodo/railtie.rb CHANGED
@@ -4,5 +4,7 @@ require 'active_support'
4
4
  class Nodo::Railtie < Rails::Railtie
5
5
  initializer 'nodo' do |app|
6
6
  Nodo.modules_root = Rails.root.join('vendor', 'node_modules')
7
+ Nodo.env['NODE_ENV'] = Rails.env.to_s
8
+ Nodo.logger = Rails.logger
7
9
  end
8
10
  end
data/lib/nodo/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Nodo
2
- VERSION = '1.5.1'
2
+ VERSION = '1.5.6'
3
3
  end
data/lib/nodo.rb CHANGED
@@ -2,14 +2,20 @@ require 'pathname'
2
2
  require 'json'
3
3
  require 'fileutils'
4
4
  require 'tmpdir'
5
+ require 'tempfile'
6
+ require 'logger'
7
+ require 'socket'
8
+ require 'forwardable'
5
9
 
6
10
  module Nodo
7
11
  class << self
8
- attr_accessor :modules_root, :env, :binary
12
+ attr_accessor :modules_root, :env, :binary, :logger, :debug
9
13
  end
10
14
  self.modules_root = './node_modules'
11
15
  self.env = {}
12
16
  self.binary = 'node'
17
+ self.logger = Logger.new(STDOUT)
18
+ self.debug = false
13
19
  end
14
20
 
15
21
  require_relative 'nodo/version'
@@ -21,4 +27,4 @@ require_relative 'nodo/constant'
21
27
  require_relative 'nodo/client'
22
28
  require_relative 'nodo/core'
23
29
 
24
- require_relative 'nodo/railtie' if defined?(Rails)
30
+ require_relative 'nodo/railtie' if defined?(Rails::Railtie)
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: nodo
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.5.1
4
+ version: 1.5.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Matthias Grosser
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-10-03 00:00:00.000000000 Z
11
+ date: 2021-10-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -105,7 +105,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
105
105
  - !ruby/object:Gem::Version
106
106
  version: '0'
107
107
  requirements: []
108
- rubygems_version: 3.0.3
108
+ rubygems_version: 3.2.22
109
109
  signing_key:
110
110
  specification_version: 4
111
111
  summary: Call Node.js from Ruby