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 +4 -4
- data/README.md +77 -12
- data/lib/nodo/core.rb +30 -7
- data/lib/nodo/function.rb +3 -3
- 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: 421dba598b565369d200c90c8b095786f7b0a98de7c97e88c6927b59b64591a0
|
4
|
+
data.tar.gz: c6ba40e8873c90713edf4cc0887dcf64c3e7b9ea4629f4d97e3fc3c8c58218b9
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 58410d1f53c78f40314ae24ea5f6e7d2aeca99f37aeeb6495ccdf0ee8a1fe903c956ad23462cba48ad38132df5614b3f354ee6c09638f52d47fe78e1e90d3064
|
7
|
+
data.tar.gz: 8374444cbbc7257b206fb17abd3470df3488885ae843e414bca3e645cf6c9f9e9d91d6de6f354e5f7fe4a1a98d780f04562df89752286851f8727d86e3c88aa9
|
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,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
|
-
|
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
|
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
|
-
|
218
|
+
error = JavaScriptError.new(result['error'], function) if result.is_a?(Hash) && result.key?('error')
|
199
219
|
end
|
200
|
-
|
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
|
-
|
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
|
-
|
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,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.
|
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-
|
11
|
+
date: 2021-10-12 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|