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 +4 -4
- data/README.md +86 -12
- data/lib/nodo/core.rb +20 -6
- data/lib/nodo/dependency.rb +10 -1
- data/lib/nodo/errors.rb +15 -5
- data/lib/nodo/nodo.js +13 -10
- data/lib/nodo/railtie.rb +2 -1
- data/lib/nodo/version.rb +1 -1
- data/lib/nodo.rb +5 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c7c6ed5581192d2d679c8d41e72925c58d68aec32831b2a556fdb3630c4357b7
|
4
|
+
data.tar.gz: 3da48a3ca955dedead870d27dcd73d96d74f62b09b31f18e124e3b4e626bd920
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 96168d93c24f3543926536d047785f230a588bb5f555550507d33f555f98cbc606465be9195b9af01fe4301c6c52931ea4ba9fb97f262ab2bfbeba015a3d67a0
|
7
|
+
data.tar.gz: 0bcf160a3b8a539979372fcc2f4141f4a1d26889053fcfed93e510e19d2009f30c5b9b64d598c460df93447f63bf8589fb908b5601b9fcfeb2a9d5623dbd4033
|
data/README.md
CHANGED
@@ -1,4 +1,5 @@
|
|
1
|
-
[](http://badge.fury.io/rb/nodo)
|
1
|
+
[](http://badge.fury.io/rb/nodo)
|
2
|
+
[](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
|
-
|
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
|
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
|
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:
|
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
|
-
|
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
|
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
|
-
|
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
|
-
|
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)
|
data/lib/nodo/dependency.rb
CHANGED
@@ -7,7 +7,16 @@ module Nodo
|
|
7
7
|
end
|
8
8
|
|
9
9
|
def to_js
|
10
|
-
|
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
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
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
|
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
|
-
|
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
|
-
|
39
|
+
debug(`Completed 200 OK${timing}\n`);
|
39
40
|
}
|
40
41
|
|
41
|
-
function
|
42
|
-
|
43
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
109
|
+
debug(`server ready, listening on ${socket} (max connections: ${server.maxConnections})`);
|
107
110
|
});
|
108
111
|
},
|
109
112
|
|
110
113
|
close: (finalizer) => {
|
111
|
-
|
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,
|
122
|
+
return { core: core, debug: debug };
|
120
123
|
})();
|
data/lib/nodo/railtie.rb
CHANGED
data/lib/nodo/version.rb
CHANGED
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.
|
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-
|
11
|
+
date: 2021-10-12 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|