nodo 1.5.4 → 1.6.1

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: 68a024656d2aaacddb3f52790316307d06acd432c0e5f43d149ad6c716888899
4
- data.tar.gz: 07020ab3ca97f25f90e2e0f2b26ecdf43f78312a308852e6f5f0a50e6ad57d73
3
+ metadata.gz: c7c6ed5581192d2d679c8d41e72925c58d68aec32831b2a556fdb3630c4357b7
4
+ data.tar.gz: 3da48a3ca955dedead870d27dcd73d96d74f62b09b31f18e124e3b4e626bd920
5
5
  SHA512:
6
- metadata.gz: 89c52ca6b172f94b4bc55730cad6d9934fe4c1fc914b0236665202ec26f5d98b77790336aed67af275fd2b4d5050dce4675b43d02639a9a5911d2836b651e6ae
7
- data.tar.gz: 9a7fe1128ed3198d2ae4a936eda189d806f3a5e3221efb745d71b7b2a92dff546855e7f06a457648241860e1515ec0aae6ea8bc63703d64b0a9ba87a298174c5
6
+ metadata.gz: 96168d93c24f3543926536d047785f230a588bb5f555550507d33f555f98cbc606465be9195b9af01fe4301c6c52931ea4ba9fb97f262ab2bfbeba015a3d67a0
7
+ data.tar.gz: 0bcf160a3b8a539979372fcc2f4141f4a1d26889053fcfed93e510e19d2009f30c5b9b64d598c460df93447f63bf8589fb908b5601b9fcfeb2a9d5623dbd4033
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,72 @@ 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 and can be
174
+ set globally:
175
+
176
+ ```ruby
177
+ Nodo.timeout = 5
178
+ ```
179
+
180
+ If the execution of a single function call exceeds the timeout, `Nodo::TimeoutError`
181
+ is raised.
182
+
183
+ The timeout can also be set on a per-function basis:
184
+
185
+ ```ruby
186
+ class Foo < Nodo::Core
187
+ function :sleep, timeout: 1, code: <<~'JS'
188
+ async (sec) => await new Promise(resolve => setTimeout(resolve, sec * 1000))
189
+ JS
190
+ end
191
+
192
+ Foo.new.sleep(2)
193
+ => Nodo::TimeoutError raised
194
+ ```
195
+
196
+ ### Logging
197
+
198
+ By default, JS errors will be logged to `STDOUT`.
199
+
200
+ To set a custom logger:
201
+
202
+ ```ruby
203
+ Nodo.logger = Logger.new('nodo.log')
204
+ ```
205
+
206
+ In Rails applications, `Rails.logger` will automatically be set.
207
+
208
+
209
+ ### Debugging
210
+
211
+ To get verbose debug output, set
212
+
213
+ ```ruby
214
+ Nodo.debug = true
215
+ ```
216
+
217
+ before instantiating any worker instances. The debug mode will be active during
218
+ the current process run.
219
+
220
+
221
+ ### Clean your Rails root
222
+
223
+ For Rails applications, Nodo enables you to move `node_modules`, `package.json` and
224
+ `yarn.lock` into your application's `vendor` folder by setting the `NODE_PATH` in
225
+ an initializer:
226
+
227
+ ```ruby
228
+ # config/initializers/nodo.rb
229
+ Nodo.modules_root = Rails.root.join('vendor', 'node_modules')
230
+ ```
231
+
232
+ The rationale behind this is NPM modules being external vendor dependencies, which
233
+ should not clutter the application root directory.
234
+
235
+ With this new default, all `yarn` operations should be done after `cd`ing to `vendor`.
236
+
237
+ This repo provides an [adapted version](https://github.com/mtgrosser/nodo/blob/master/install/yarn.rake)
238
+ of the `yarn:install` rake task which will automatically take care of the vendored module location.
data/lib/nodo/core.rb CHANGED
@@ -62,10 +62,13 @@ module Nodo
62
62
  self.dependencies = dependencies + mods.merge(deps).map { |name, package| Dependency.new(name, package) }
63
63
  end
64
64
 
65
- def function(name, _code = nil, timeout: 60, code: nil)
65
+ def function(name, _code = nil, timeout: Nodo.timeout, code: nil)
66
+ raise ArgumentError, "reserved method name #{name.inspect}" if Nodo::Core.method_defined?(name) || name.to_s == DEFINE_METHOD
66
67
  code = (code ||= _code).strip
67
68
  raise ArgumentError, 'function code is required' if '' == code
68
- self.functions = functions.merge(name => Function.new(name, _code || code, caller.first, timeout))
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))
69
72
  define_method(name) { |*args| call_js_method(name, args) }
70
73
  end
71
74
 
@@ -93,7 +96,7 @@ module Nodo
93
96
  nodo.core.close(() => { process.exit(0) });
94
97
  };
95
98
 
96
- process.on('SIGINT', shutdown);
99
+ // process.on('SIGINT', shutdown);
97
100
  process.on('SIGTERM', shutdown);
98
101
 
99
102
  nodo.core.run(socket);
@@ -145,6 +148,11 @@ module Nodo
145
148
  self.class.clsid
146
149
  end
147
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
+
148
156
  def ensure_process_is_spawned
149
157
  return if node_pid
150
158
  spawn_process
@@ -159,6 +167,7 @@ module Nodo
159
167
  def spawn_process
160
168
  @@tmpdir = Pathname.new(Dir.mktmpdir('nodo'))
161
169
  env = Nodo.env.merge('NODE_PATH' => Nodo.modules_root.to_s)
170
+ env['NODO_DEBUG'] = '1' if Nodo.debug
162
171
  @@node_pid = Process.spawn(env, Nodo.binary, '-e', self.class.generate_core_code, '--', socket_path.to_s, err: :out)
163
172
  at_exit do
164
173
  Process.kill(:SIGTERM, node_pid) rescue Errno::ECHILD
@@ -174,7 +183,7 @@ module Nodo
174
183
  begin
175
184
  break if socket = UNIXSocket.new(socket_path)
176
185
  rescue Errno::ENOENT, Errno::ECONNREFUSED, Errno::ENOTDIR
177
- sleep 0.2
186
+ Kernel.sleep(0.2)
178
187
  end
179
188
  end
180
189
  socket.close if socket
@@ -206,9 +215,14 @@ module Nodo
206
215
  def handle_error(response, function)
207
216
  if response.body
208
217
  result = parse_response(response)
209
- raise JavaScriptError.new(result['error'], function) if result.is_a?(Hash) && result.key?('error')
218
+ error = if result.is_a?(Hash) && result['error'].is_a?(Hash)
219
+ attrs = result['error']
220
+ (attrs['nodo_dependency'] ? DependencyError : JavaScriptError).new(attrs, function)
221
+ end
210
222
  end
211
- raise CallError, "Node returned #{response.code}"
223
+ error ||= CallError.new("Node returned #{response.code}")
224
+ log_exception(error)
225
+ raise error
212
226
  end
213
227
 
214
228
  def parse_response(response)
@@ -7,7 +7,16 @@ module Nodo
7
7
  end
8
8
 
9
9
  def to_js
10
- "const #{name} = require(#{package.to_json});\n"
10
+ <<~JS
11
+ const #{name} = (() => {
12
+ try {
13
+ return require(#{package.to_json});
14
+ } catch(e) {
15
+ e.nodo_dependency = #{package.to_json};
16
+ throw e;
17
+ }
18
+ })();
19
+ JS
11
20
  end
12
21
  end
13
22
  end
data/lib/nodo/errors.rb CHANGED
@@ -38,12 +38,22 @@ module Nodo
38
38
 
39
39
  def generate_message
40
40
  message = "#{attributes['message'] || 'Unknown error'}"
41
- if loc = attributes['loc']
42
- message << loc.inject(' in') { |s, (key, value)| s << " #{key}: #{value}" }
43
- end
44
- message
41
+ message << format_location(attributes['loc'])
42
+ end
43
+
44
+ def format_location(loc)
45
+ return '' unless loc
46
+ loc.inject(' in') { |s, (key, value)| s << " #{key}: #{value}" }
45
47
  end
46
48
  end
47
49
 
48
- class DependencyError < JavaScriptError; end
50
+ class DependencyError < JavaScriptError
51
+ private
52
+
53
+ def generate_message
54
+ message = "#{attributes['message'] || 'Dependency error'}\n"
55
+ message << "The specified dependency '#{attributes['nodo_dependency']}' could not be loaded. "
56
+ message << "Run 'yarn add #{attributes['nodo_dependency']}' to install it.\n"
57
+ end
58
+ end
49
59
  end
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.4'
2
+ VERSION = '1.6.1'
3
3
  end
data/lib/nodo.rb CHANGED
@@ -3,16 +3,20 @@ require 'json'
3
3
  require 'fileutils'
4
4
  require 'tmpdir'
5
5
  require 'tempfile'
6
+ require 'logger'
6
7
  require 'socket'
7
8
  require 'forwardable'
8
9
 
9
10
  module Nodo
10
11
  class << self
11
- attr_accessor :modules_root, :env, :binary
12
+ attr_accessor :modules_root, :env, :binary, :logger, :debug, :timeout
12
13
  end
13
14
  self.modules_root = './node_modules'
14
15
  self.env = {}
15
16
  self.binary = 'node'
17
+ self.logger = Logger.new(STDOUT)
18
+ self.debug = false
19
+ self.timeout = 60
16
20
  end
17
21
 
18
22
  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.4
4
+ version: 1.6.1
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-06 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