nodo 1.5.0 → 1.5.5

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8f997b2c506f2cec9bfd235c68cb0db93fcb103fa7f9b40f03740ca27b59ffb4
4
- data.tar.gz: 8a1ba6ae691ee2a992b2af5a127c43c70178cc403134db04a08f9597dd8588b3
3
+ metadata.gz: 3cef66bc4b1d53b20500110ac7acbc226ec361b6aaed569874ce8568a6718242
4
+ data.tar.gz: ab2cae6eef01d53ebc1c471a5e08edfd37a263178adc49aa459c98dfcea4e92f
5
5
  SHA512:
6
- metadata.gz: 77c7a96627569da1b4093eb0fbfb6ab94c0c7dae4e425b019d52e06183f098daac57b88f68924fbe117afc1a0cf8be3aec143fd8e6935c839ebd573dde5afd70
7
- data.tar.gz: 8057351d0968d8bc357af3926b805f82ce6e05949ee54efb9df40293ba4faf0ce8d337550318de0114c7f9dc2df05958ea481dc35aa2981a0d95501c9cb18c64
6
+ metadata.gz: 9bf90e8b56366f2e42039026575db43a0c5a9808a677a533c3e211c9ae211a8f06b2ea0890550e22fca259f9fa416c0e96017a676538a3eb673f2a66caa0996a
7
+ data.tar.gz: 2573e2dfd199bb9f8e25dc11c5dcb49e0ba6f29796aaa523656fa1020e0d8ce325ab411cf2d60413ac0557407fec4a132ba51d75075585a0e7b56d76bfc42811
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`.
@@ -117,12 +129,62 @@ end
117
129
  class BarFoo < Nodo::Core
118
130
 
119
131
  script <<~JS
120
- // some custom JS
121
- // to be executed during initialization
132
+ // custom JS to be executed during initialization
133
+ // things defined here can later be used inside functions
134
+ const bigThing = someLib.init();
122
135
  JS
123
136
  end
124
137
  ```
125
138
 
126
139
  ### Inheritance
127
140
 
128
- 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.
143
+
144
+ ```ruby
145
+ class Foo < Nodo::Core
146
+ function :foo, "() => 'superclass'"
147
+ end
148
+
149
+ class SubFoo < Foo
150
+ function :bar, "() => { return 'calling' + foo() }"
151
+ end
152
+
153
+ class SubSubFoo < SubFoo
154
+ function :foo, "() => 'subsubclass'"
155
+ end
156
+
157
+ Foo.new.foo => "superclass"
158
+ SubFoo.new.bar => "callingsuperclass"
159
+ SubSubFoo.new.bar => "callingsubsubclass"
160
+ ```
161
+
162
+ ### Async functions
163
+
164
+ `Nodo` supports calling `async` functions from Ruby.
165
+ The Ruby call will happen synchronously, i.e. it will block until the JS
166
+ function resolves:
167
+
168
+ ```ruby
169
+ class SyncFoo < Nodo::Core
170
+ function :do_something, <<~JS
171
+ async () => { return await asyncFunc(); }
172
+ JS
173
+ end
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
+ ```
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,11 @@ 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)
67
+ code = (code ||= _code).strip
68
+ raise ArgumentError, 'function code is required' if '' == code
69
+ self.functions = functions.merge(name => Function.new(name, _code || code, caller.first, timeout))
62
70
  define_method(name) { |*args| call_js_method(name, args) }
63
71
  end
64
72
 
@@ -80,6 +88,8 @@ module Nodo
80
88
  process.exit(1);
81
89
  }
82
90
 
91
+ process.title = `nodo-core ${socket}`;
92
+
83
93
  const shutdown = () => {
84
94
  nodo.core.close(() => { process.exit(0) });
85
95
  };
@@ -104,16 +114,6 @@ module Nodo
104
114
  })()
105
115
  JS
106
116
  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
117
 
118
118
  private
119
119
 
@@ -155,23 +155,31 @@ module Nodo
155
155
  return if self.class.class_defined?
156
156
  call_js_method(DEFINE_METHOD, self.class.generate_class_code)
157
157
  self.class.class_defined = true
158
- # rescue => e
159
- # raise Error, e.message
160
158
  end
161
-
159
+
162
160
  def spawn_process
163
161
  @@tmpdir = Pathname.new(Dir.mktmpdir('nodo'))
164
162
  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))
163
+ @@node_pid = Process.spawn(env, Nodo.binary, '-e', self.class.generate_core_code, '--', socket_path.to_s, err: :out)
164
+ at_exit do
165
+ Process.kill(:SIGTERM, node_pid) rescue Errno::ECHILD
166
+ Process.wait(node_pid) rescue Errno::ECHILD
167
+ FileUtils.remove_entry(tmpdir) if File.directory?(tmpdir)
168
+ end
167
169
  end
168
170
 
169
171
  def wait_for_socket
170
172
  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)
173
+ socket = nil
174
+ while Time.now - start < TIMEOUT
175
+ begin
176
+ break if socket = UNIXSocket.new(socket_path)
177
+ rescue Errno::ENOENT, Errno::ECONNREFUSED, Errno::ENOTDIR
178
+ Kernel.sleep(0.2)
179
+ end
174
180
  end
181
+ socket.close if socket
182
+ raise TimeoutError, "could not connect to socket #{socket_path}" unless socket
175
183
  end
176
184
 
177
185
  def call_js_method(method, args)
@@ -182,12 +190,15 @@ module Nodo
182
190
  request = Net::HTTP::Post.new("/#{clsid}/#{method}", 'Content-Type': 'application/json')
183
191
  request.body = JSON.dump(args)
184
192
  client = Client.new("unix://#{socket_path}")
193
+ client.read_timeout = function.timeout if function
185
194
  response = client.request(request)
186
195
  if response.is_a?(Net::HTTPOK)
187
196
  parse_response(response)
188
197
  else
189
198
  handle_error(response, function)
190
199
  end
200
+ rescue Net::ReadTimeout
201
+ raise TimeoutError, "function call #{self.class}##{method} timed out"
191
202
  rescue Errno::EPIPE, IOError
192
203
  # TODO: restart or something? If this happens the process is completely broken
193
204
  raise Error, 'Node process failed'
@@ -202,7 +213,17 @@ module Nodo
202
213
  end
203
214
 
204
215
  def parse_response(response)
205
- JSON.parse(response.body.force_encoding('UTF-8'))
216
+ data = response.body.force_encoding('UTF-8')
217
+ JSON.parse(data) unless data == ''
218
+ end
219
+
220
+ def with_tempfile(name)
221
+ ext = File.extname(name)
222
+ result = nil
223
+ Tempfile.create([File.basename(name, ext), ext], tmpdir) do |file|
224
+ result = yield(file)
225
+ end
226
+ result
206
227
  end
207
228
 
208
229
  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 ADDED
@@ -0,0 +1,120 @@
1
+ module.exports = (function() {
2
+ const DEFINE_METHOD = '__nodo_define_class__';
3
+
4
+ const vm = require('vm');
5
+ const http = require('http');
6
+ const path = require('path');
7
+ const fs = require('fs');
8
+ const performance = require('perf_hooks').performance;
9
+
10
+ let server, closing;
11
+ const classes = {};
12
+
13
+ function render_error(e) {
14
+ let errInfo = {};
15
+ if (e instanceof Error) {
16
+ errInfo.name = e.name;
17
+ Object.getOwnPropertyNames(e).reduce((obj, prop) => { obj[prop] = e[prop]; return obj }, errInfo);
18
+ } else {
19
+ errInfo.name = e.toString();
20
+ }
21
+ return JSON.stringify({ error: errInfo });
22
+ }
23
+
24
+ function respond_with_error(res, code, name) {
25
+ res.statusCode = code;
26
+ const rendered = render_error(name);
27
+ log(`Error ${code} ${rendered}`);
28
+ res.end(rendered, 'utf8');
29
+ }
30
+
31
+ function respond_with_data(res, data, start) {
32
+ let timing;
33
+ res.statusCode = 200;
34
+ res.end(JSON.stringify(data), 'utf8');
35
+ if (start) {
36
+ timing = ` in ${(performance.now() - start).toFixed(2)}ms`;
37
+ }
38
+ log(`Completed 200 OK${timing}`);
39
+ }
40
+
41
+ function log(message) {
42
+ // fs.appendFileSync('log/nodo.log', `${message}\n`);
43
+ // console.log(`[Nodo] ${message}`);
44
+ }
45
+
46
+ const core = {
47
+ run: (socket) => {
48
+ log('Starting up...');
49
+ server = http.createServer((req, res) => {
50
+ const start = performance.now();
51
+
52
+ res.setHeader('Content-Type', 'application/json');
53
+ log(`${req.method} ${req.url}`);
54
+
55
+ if (req.method !== 'POST' || !req.url.startsWith('/')) {
56
+ return respond_with_error(res, 405, 'Method Not Allowed');
57
+ }
58
+
59
+ const url = req.url.substring(1);
60
+ const [class_name, method] = url.split('/');
61
+ let klass;
62
+
63
+ if (classes.hasOwnProperty(class_name)) {
64
+ klass = classes[class_name];
65
+ if (!klass.hasOwnProperty(method)) {
66
+ return respond_with_error(res, 404, `Method ${class_name}#${method} not found`);
67
+ }
68
+ } else if (DEFINE_METHOD != method) {
69
+ return respond_with_error(res, 404, `Class ${class_name} not defined`);
70
+ }
71
+
72
+ let body = '';
73
+
74
+ req.on('data', (data) => { body += data; });
75
+
76
+ req.on('end', () => {
77
+ let input, result;
78
+
79
+ try {
80
+ input = JSON.parse(body);
81
+ } catch (e) {
82
+ return respond_with_error(res, 400, 'Bad Request');
83
+ }
84
+
85
+ try {
86
+ if (DEFINE_METHOD == method) {
87
+ let new_class = vm.runInThisContext(input, class_name);
88
+ classes[class_name] = new_class;
89
+ respond_with_data(res, class_name, start);
90
+ } else {
91
+ Promise.resolve(klass[method].apply(null, input)).then(function (result) {
92
+ respond_with_data(res, result, start);
93
+ }).catch(function(error) {
94
+ respond_with_error(res, 500, error);
95
+ });
96
+ }
97
+ } catch(error) {
98
+ return respond_with_error(res, 500, error);
99
+ }
100
+
101
+ });
102
+ });
103
+
104
+ //server.maxConnections = 64;
105
+ server.listen(socket, () => {
106
+ log(`server ready, listening on ${socket} (max connections: ${server.maxConnections})`);
107
+ });
108
+ },
109
+
110
+ close: (finalizer) => {
111
+ log("Shutting down");
112
+ if (!closing) {
113
+ closing = true;
114
+ server.close(finalizer);
115
+ }
116
+ }
117
+ };
118
+
119
+ return { core: core, log: log };
120
+ })();
data/lib/nodo/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Nodo
2
- VERSION = '1.5.0'
2
+ VERSION = '1.5.5'
3
3
  end
data/lib/nodo.rb CHANGED
@@ -2,6 +2,9 @@ require 'pathname'
2
2
  require 'json'
3
3
  require 'fileutils'
4
4
  require 'tmpdir'
5
+ require 'tempfile'
6
+ require 'socket'
7
+ require 'forwardable'
5
8
 
6
9
  module Nodo
7
10
  class << self
@@ -21,4 +24,4 @@ require_relative 'nodo/constant'
21
24
  require_relative 'nodo/client'
22
25
  require_relative 'nodo/core'
23
26
 
24
- require_relative 'nodo/railtie' if defined?(Rails)
27
+ 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.0
4
+ version: 1.5.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Matthias Grosser
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-10-02 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
@@ -82,6 +82,7 @@ files:
82
82
  - lib/nodo/dependency.rb
83
83
  - lib/nodo/errors.rb
84
84
  - lib/nodo/function.rb
85
+ - lib/nodo/nodo.js
85
86
  - lib/nodo/railtie.rb
86
87
  - lib/nodo/script.rb
87
88
  - lib/nodo/version.rb
@@ -89,7 +90,7 @@ homepage: https://github.com/mtgrosser/nodo
89
90
  licenses:
90
91
  - MIT
91
92
  metadata: {}
92
- post_install_message:
93
+ post_install_message:
93
94
  rdoc_options: []
94
95
  require_paths:
95
96
  - lib
@@ -104,8 +105,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
104
105
  - !ruby/object:Gem::Version
105
106
  version: '0'
106
107
  requirements: []
107
- rubygems_version: 3.0.3
108
- signing_key:
108
+ rubygems_version: 3.2.22
109
+ signing_key:
109
110
  specification_version: 4
110
111
  summary: Call Node.js from Ruby
111
112
  test_files: []