nodo 1.5.0 → 1.5.5

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: 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: []