nodo 1.5.3 → 1.6.0

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: e46009bcd3e3e8e5fd0f0a6f624c3f263caa09c64225878011e557333ffb8e64
4
- data.tar.gz: 98eed61321bae14d89725f6884a16d2c3fccc2eb20f808781b10842e2f22ae25
3
+ metadata.gz: 421dba598b565369d200c90c8b095786f7b0a98de7c97e88c6927b59b64591a0
4
+ data.tar.gz: c6ba40e8873c90713edf4cc0887dcf64c3e7b9ea4629f4d97e3fc3c8c58218b9
5
5
  SHA512:
6
- metadata.gz: ce0e598a9e6cffb816b53ce802a1a412746d195e387424b3362cc765d1fbd03931322c734a3ee6817487151f9969f414e1d99cfc1a4e9bddce99a7d6c7d95e26
7
- data.tar.gz: a628c3765b94bd32600f554c1e973703e90a9dbcb06764b3d3256eb6ceb519997b2241f1f7c02474b8859aa2b4ff0049c863bcf047d38fbeeb8ed1be5e1bedb2
6
+ metadata.gz: 58410d1f53c78f40314ae24ea5f6e7d2aeca99f37aeeb6495ccdf0ee8a1fe903c956ad23462cba48ad38132df5614b3f354ee6c09638f52d47fe78e1e90d3064
7
+ data.tar.gz: 8374444cbbc7257b206fb17abd3470df3488885ae843e414bca3e645cf6c9f9e9d91d6de6f354e5f7fe4a1a98d780f04562df89752286851f8727d86e3c88aa9
data/README.md CHANGED
@@ -1,4 +1,5 @@
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)
1
+ [![Gem Version](https://badge.fury.io/rb/nodo.svg)](http://badge.fury.io/rb/nodo)
2
+ [![build](https://github.com/mtgrosser/nodo/actions/workflows/build.yml/badge.svg)](https://github.com/mtgrosser/nodo/actions/workflows/build.yml)
2
3
 
3
4
  # Nōdo – call Node.js from Ruby
4
5
 
@@ -39,13 +40,13 @@ In Nodo, you define JS functions as you would define Ruby methods:
39
40
 
40
41
  ```ruby
41
42
  class Foo < Nodo::Core
42
-
43
+
43
44
  function :say_hi, <<~JS
44
45
  (name) => {
45
46
  return `Hello ${name}!`;
46
47
  }
47
48
  JS
48
-
49
+
49
50
  end
50
51
 
51
52
  foo = Foo.new
@@ -90,6 +91,14 @@ class FooBar < Nodo::Core
90
91
  end
91
92
  ```
92
93
 
94
+ ### Alternate function definition syntax
95
+
96
+ JS code can also be supplied using the `code:` keyword argument:
97
+
98
+ ```ruby
99
+ function :hello, code: "() => 'world'"
100
+ ```
101
+
93
102
  ### Setting NODE_PATH
94
103
 
95
104
  By default, `./node_modules` is used as the `NODE_PATH`.
@@ -99,13 +108,7 @@ To set a custom path:
99
108
  Nodo.modules_root = 'path/to/node_modules'
100
109
  ```
101
110
 
102
- For Rails applications, it will be set to `vendor/node_modules`.
103
- To use the Rails 6 default of putting `node_modules` to `RAILS_ROOT`:
104
-
105
- ```ruby
106
- # config/initializers/nodo.rb
107
- Nodo.modules_root = Rails.root.join('node_modules')
108
- ```
111
+ Also see: [Clean your Rails root](#Clean-your-Rails-root)
109
112
 
110
113
  ### Defining JS constants
111
114
 
@@ -130,7 +133,8 @@ end
130
133
 
131
134
  ### Inheritance
132
135
 
133
- Subclasses will inherit functions, constants, dependencies and scripts from their superclasses, while only functions can be overwritten.
136
+ Subclasses will inherit functions, constants, dependencies and scripts from
137
+ their superclasses, while only functions can be overwritten.
134
138
 
135
139
  ```ruby
136
140
  class Foo < Nodo::Core
@@ -153,7 +157,8 @@ SubSubFoo.new.bar => "callingsubsubclass"
153
157
  ### Async functions
154
158
 
155
159
  `Nodo` supports calling `async` functions from Ruby.
156
- The Ruby call will happen synchronously, i.e. it will block until the JS function resolves:
160
+ The Ruby call will happen synchronously, i.e. it will block until the JS
161
+ function resolves:
157
162
 
158
163
  ```ruby
159
164
  class SyncFoo < Nodo::Core
@@ -162,3 +167,63 @@ class SyncFoo < Nodo::Core
162
167
  JS
163
168
  end
164
169
  ```
170
+
171
+ ### Limiting function execution time
172
+
173
+ The default timeout for a single JS function call is 60 seconds due to the
174
+ `Net::HTTP` default. It can be overridden on a per-function basis:
175
+
176
+ ```ruby
177
+ class Foo < Nodo::Core
178
+ function :sleep, timeout: 1, code: <<~'JS'
179
+ async (sec) => await new Promise(resolve => setTimeout(resolve, sec * 1000))
180
+ JS
181
+ end
182
+
183
+ Foo.new.sleep(2)
184
+ => Nodo::TimeoutError raised
185
+ ```
186
+
187
+ ### Logging
188
+
189
+ By default, JS errors will be logged to `STDOUT`.
190
+
191
+ To set a custom logger:
192
+
193
+ ```ruby
194
+ Nodo.logger = Logger.new('nodo.log')
195
+ ```
196
+
197
+ In Rails applications, `Rails.logger` will automatically be set.
198
+
199
+
200
+ ### Debugging
201
+
202
+ To get verbose debug output, set
203
+
204
+ ```ruby
205
+ Nodo.debug = true
206
+ ```
207
+
208
+ before instantiating any worker instances. The debug mode will be active during
209
+ the current process run.
210
+
211
+
212
+ #### Clean your Rails root
213
+
214
+ For Rails applications, Nodo enables you to move `node_modules`, `package.json` and
215
+ `yarn.lock` into your application's `vendor` folder by setting the `NODE_PATH` in
216
+ an initializer:
217
+
218
+ ```ruby
219
+ # config/initializers/nodo.rb
220
+ Nodo.modules_root = Rails.root.join('vendor', 'node_modules')
221
+ ```
222
+
223
+ The rationale behind this is NPM modules being external vendor dependencies, which
224
+ should not clutter the application root directory.
225
+
226
+ With this new default, all `yarn` operations should be done after `cd`ing to `vendor`.
227
+
228
+ This repo provides an [adapted version](https://github.com/mtgrosser/nodo/blob/master/install/yarn.rake)
229
+ of the `yarn:install` rake task which will automatically take care of the vendored module location.
data/lib/nodo/core.rb CHANGED
@@ -12,6 +12,8 @@ module Nodo
12
12
  @@mutex = Mutex.new
13
13
 
14
14
  class << self
15
+ extend Forwardable
16
+
15
17
  attr_accessor :class_defined
16
18
 
17
19
  def inherited(subclass)
@@ -24,6 +26,10 @@ module Nodo
24
26
  @instance ||= new
25
27
  end
26
28
 
29
+ def class_function(*methods)
30
+ singleton_class.def_delegators(:instance, *methods)
31
+ end
32
+
27
33
  def class_defined?
28
34
  !!class_defined
29
35
  end
@@ -56,8 +62,13 @@ module Nodo
56
62
  self.dependencies = dependencies + mods.merge(deps).map { |name, package| Dependency.new(name, package) }
57
63
  end
58
64
 
59
- def function(name, code)
60
- 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))
61
72
  define_method(name) { |*args| call_js_method(name, args) }
62
73
  end
63
74
 
@@ -85,7 +96,7 @@ module Nodo
85
96
  nodo.core.close(() => { process.exit(0) });
86
97
  };
87
98
 
88
- process.on('SIGINT', shutdown);
99
+ // process.on('SIGINT', shutdown);
89
100
  process.on('SIGTERM', shutdown);
90
101
 
91
102
  nodo.core.run(socket);
@@ -137,6 +148,11 @@ module Nodo
137
148
  self.class.clsid
138
149
  end
139
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
+
140
156
  def ensure_process_is_spawned
141
157
  return if node_pid
142
158
  spawn_process
@@ -151,6 +167,7 @@ module Nodo
151
167
  def spawn_process
152
168
  @@tmpdir = Pathname.new(Dir.mktmpdir('nodo'))
153
169
  env = Nodo.env.merge('NODE_PATH' => Nodo.modules_root.to_s)
170
+ env['NODO_DEBUG'] = '1' if Nodo.debug
154
171
  @@node_pid = Process.spawn(env, Nodo.binary, '-e', self.class.generate_core_code, '--', socket_path.to_s, err: :out)
155
172
  at_exit do
156
173
  Process.kill(:SIGTERM, node_pid) rescue Errno::ECHILD
@@ -166,7 +183,7 @@ module Nodo
166
183
  begin
167
184
  break if socket = UNIXSocket.new(socket_path)
168
185
  rescue Errno::ENOENT, Errno::ECONNREFUSED, Errno::ENOTDIR
169
- sleep 0.2
186
+ Kernel.sleep(0.2)
170
187
  end
171
188
  end
172
189
  socket.close if socket
@@ -181,12 +198,15 @@ module Nodo
181
198
  request = Net::HTTP::Post.new("/#{clsid}/#{method}", 'Content-Type': 'application/json')
182
199
  request.body = JSON.dump(args)
183
200
  client = Client.new("unix://#{socket_path}")
201
+ client.read_timeout = function.timeout if function
184
202
  response = client.request(request)
185
203
  if response.is_a?(Net::HTTPOK)
186
204
  parse_response(response)
187
205
  else
188
206
  handle_error(response, function)
189
207
  end
208
+ rescue Net::ReadTimeout
209
+ raise TimeoutError, "function call #{self.class}##{method} timed out"
190
210
  rescue Errno::EPIPE, IOError
191
211
  # TODO: restart or something? If this happens the process is completely broken
192
212
  raise Error, 'Node process failed'
@@ -195,13 +215,16 @@ module Nodo
195
215
  def handle_error(response, function)
196
216
  if response.body
197
217
  result = parse_response(response)
198
- 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')
199
219
  end
200
- raise CallError, "Node returned #{response.code}"
220
+ error ||= CallError.new("Node returned #{response.code}")
221
+ log_exception(error)
222
+ raise error
201
223
  end
202
224
 
203
225
  def parse_response(response)
204
- JSON.parse(response.body.force_encoding('UTF-8'))
226
+ data = response.body.force_encoding('UTF-8')
227
+ JSON.parse(data) unless data == ''
205
228
  end
206
229
 
207
230
  def with_tempfile(name)
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
@@ -3,6 +3,7 @@ require 'active_support'
3
3
 
4
4
  class Nodo::Railtie < Rails::Railtie
5
5
  initializer 'nodo' do |app|
6
- Nodo.modules_root = Rails.root.join('vendor', 'node_modules')
6
+ Nodo.env['NODE_ENV'] = Rails.env.to_s
7
+ Nodo.logger = Rails.logger
7
8
  end
8
9
  end
data/lib/nodo/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Nodo
2
- VERSION = '1.5.3'
2
+ VERSION = '1.6.0'
3
3
  end
data/lib/nodo.rb CHANGED
@@ -3,15 +3,19 @@ require 'json'
3
3
  require 'fileutils'
4
4
  require 'tmpdir'
5
5
  require 'tempfile'
6
+ require 'logger'
6
7
  require 'socket'
8
+ require 'forwardable'
7
9
 
8
10
  module Nodo
9
11
  class << self
10
- attr_accessor :modules_root, :env, :binary
12
+ attr_accessor :modules_root, :env, :binary, :logger, :debug
11
13
  end
12
14
  self.modules_root = './node_modules'
13
15
  self.env = {}
14
16
  self.binary = 'node'
17
+ self.logger = Logger.new(STDOUT)
18
+ self.debug = false
15
19
  end
16
20
 
17
21
  require_relative 'nodo/version'
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.3
4
+ version: 1.6.0
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