nodo 1.6.1 → 1.6.2

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: c7c6ed5581192d2d679c8d41e72925c58d68aec32831b2a556fdb3630c4357b7
4
- data.tar.gz: 3da48a3ca955dedead870d27dcd73d96d74f62b09b31f18e124e3b4e626bd920
3
+ metadata.gz: 95d8c9823753373c6be5ad0c493e7f14346950b611d4ca0d90c5a96bcabfa5e6
4
+ data.tar.gz: 3ad4b37694f4ef33cfadad03794ce7b4bf2a9fd42dc4e0abb319ec981e529b33
5
5
  SHA512:
6
- metadata.gz: 96168d93c24f3543926536d047785f230a588bb5f555550507d33f555f98cbc606465be9195b9af01fe4301c6c52931ea4ba9fb97f262ab2bfbeba015a3d67a0
7
- data.tar.gz: 0bcf160a3b8a539979372fcc2f4141f4a1d26889053fcfed93e510e19d2009f30c5b9b64d598c460df93447f63bf8589fb908b5601b9fcfeb2a9d5623dbd4033
6
+ metadata.gz: 0e0004a84541cb4edd4cc398250fe17ee5623aed13f1bbb76b31eee531867906cffd87c5e0f71b50c0c72fdae4f26e57d24f654fdd2623bee90b20c60f3ff0b4
7
+ data.tar.gz: 539b75f4a8283f4db872db614158403129651f4b0811d9e7f9ef5dfa1f506bedcf238fa0b93896fe776ae936bbc8eba9cbd44ea86fa8f80b87142e5d69cfe957
data/README.md CHANGED
@@ -217,8 +217,63 @@ Nodo.debug = true
217
217
  before instantiating any worker instances. The debug mode will be active during
218
218
  the current process run.
219
219
 
220
+ To print a debug message from JS code:
220
221
 
221
- ### Clean your Rails root
222
+ ```js
223
+ nodo.debug("Debug message");
224
+ ```
225
+
226
+ ### Evaluation
227
+
228
+ While `Nodo` is mainly function-based, it is possible to evaluate JS code in the
229
+ context of the defined object.
230
+
231
+ ```ruby
232
+ foo = Foo.new.evaluate("3 + 5")
233
+ => 8
234
+ ```
235
+
236
+ Evaluated code can access functions, required dependencies and constants:
237
+
238
+ ```ruby
239
+ class Foo < Nodo::Core
240
+ const :BAR, 'bar'
241
+ require :uuid
242
+ function :hello, code: '() => "world"'
243
+ end
244
+
245
+ foo = Foo.new
246
+
247
+ foo.evaluate('BAR')
248
+ => "bar"
249
+
250
+ foo.evaluate('uuid.v4()')
251
+ => "f258bef3-0d6f-4566-ad39-d8dec973ef6b"
252
+
253
+ foo.evaluate('hello()')
254
+ => "world"
255
+ ```
256
+
257
+ Variables defined by evaluation are local to the current instance:
258
+
259
+ ```ruby
260
+ one = Foo.new
261
+ one.evaluate('a = 1')
262
+ two = Foo.new
263
+ two.evaluate('a = 2')
264
+ one.evaluate('a') => 1
265
+ two.evaluate('a') => 2
266
+ ```
267
+
268
+ ⚠️ Evaluation comes with the usual caveats:
269
+
270
+ - Avoid modifying any of your predefined identifiers. Remember that in JS,
271
+ as in Ruby, constants are not necessarily constant.
272
+ - Never evaluate any code which includes un-checked user data. The Node.js process
273
+ has full read/write access to your filesystem! 💥
274
+
275
+
276
+ ## Clean your Rails root
222
277
 
223
278
  For Rails applications, Nodo enables you to move `node_modules`, `package.json` and
224
279
  `yarn.lock` into your application's `vendor` folder by setting the `NODE_PATH` in
@@ -229,7 +284,7 @@ an initializer:
229
284
  Nodo.modules_root = Rails.root.join('vendor', 'node_modules')
230
285
  ```
231
286
 
232
- The rationale behind this is NPM modules being external vendor dependencies, which
287
+ The rationale for this is NPM modules being external vendor dependencies, which
233
288
  should not clutter the application root directory.
234
289
 
235
290
  With this new default, all `yarn` operations should be done after `cd`ing to `vendor`.
data/lib/nodo/constant.rb CHANGED
@@ -7,7 +7,7 @@ module Nodo
7
7
  end
8
8
 
9
9
  def to_js
10
- "const #{name} = #{value.to_json};\n"
10
+ "const #{name} = __nodo_klass__.#{name} = #{value.to_json};\n"
11
11
  end
12
12
  end
13
13
  end
data/lib/nodo/core.rb CHANGED
@@ -1,8 +1,11 @@
1
1
  module Nodo
2
2
  class Core
3
3
  SOCKET_NAME = 'nodo.sock'
4
- DEFINE_METHOD = '__nodo_define_class__'
5
- TIMEOUT = 5
4
+ DEFINE_METHOD = '__nodo_define_class__'.freeze
5
+ EVALUATE_METHOD = '__nodo_evaluate__'.freeze
6
+ GC_METHOD = '__nodo_gc__'.freeze
7
+ INTERNAL_METHODS = [DEFINE_METHOD, EVALUATE_METHOD, GC_METHOD].freeze
8
+ LAUNCH_TIMEOUT = 5
6
9
  ARRAY_CLASS_ATTRIBUTES = %i[dependencies constants scripts].freeze
7
10
  HASH_CLASS_ATTRIBUTES = %i[functions].freeze
8
11
  CLASS_ATTRIBUTES = (ARRAY_CLASS_ATTRIBUTES + HASH_CLASS_ATTRIBUTES).freeze
@@ -10,6 +13,7 @@ module Nodo
10
13
  @@node_pid = nil
11
14
  @@tmpdir = nil
12
15
  @@mutex = Mutex.new
16
+ @@exiting = nil
13
17
 
14
18
  class << self
15
19
  extend Forwardable
@@ -21,15 +25,11 @@ module Nodo
21
25
  subclass.send "#{attr}=", send(attr).dup
22
26
  end
23
27
  end
24
-
28
+
25
29
  def instance
26
30
  @instance ||= new
27
31
  end
28
32
 
29
- def class_function(*methods)
30
- singleton_class.def_delegators(:instance, *methods)
31
- end
32
-
33
33
  def class_defined?
34
34
  !!class_defined
35
35
  end
@@ -42,6 +42,7 @@ module Nodo
42
42
  define_method "#{attr}=" do |value|
43
43
  instance_variable_set :"@#{attr}", value
44
44
  end
45
+ protected "#{attr}="
45
46
  end
46
47
 
47
48
  ARRAY_CLASS_ATTRIBUTES.each do |attr|
@@ -55,30 +56,6 @@ module Nodo
55
56
  instance_variable_get(:"@#{attr}") || instance_variable_set(:"@#{attr}", {})
56
57
  end
57
58
  end
58
-
59
- def require(*mods)
60
- deps = mods.last.is_a?(Hash) ? mods.pop : {}
61
- mods = mods.map { |m| [m, m] }.to_h
62
- self.dependencies = dependencies + mods.merge(deps).map { |name, package| Dependency.new(name, package) }
63
- end
64
-
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
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))
72
- define_method(name) { |*args| call_js_method(name, args) }
73
- end
74
-
75
- def const(name, value)
76
- self.constants = constants + [Constant.new(name, value)]
77
- end
78
-
79
- def script(code)
80
- self.scripts = scripts + [Script.new(code)]
81
- end
82
59
 
83
60
  def generate_core_code
84
61
  <<~JS
@@ -106,8 +83,7 @@ module Nodo
106
83
  def generate_class_code
107
84
  <<~JS
108
85
  (() => {
109
- const __nodo_log = nodo.log;
110
- const __nodo_klass__ = {};
86
+ const __nodo_klass__ = { nodo: global.nodo };
111
87
  #{dependencies.map(&:to_js).join}
112
88
  #{constants.map(&:to_js).join}
113
89
  #{functions.values.map(&:to_js).join}
@@ -117,14 +93,57 @@ module Nodo
117
93
  JS
118
94
  end
119
95
 
96
+ protected
97
+
98
+ def finalize_context(context_id)
99
+ proc do
100
+ if not @@exiting and core = Nodo::Core.instance
101
+ core.send(:call_js_method, GC_METHOD, context_id)
102
+ end
103
+ end
104
+ end
105
+
120
106
  private
121
107
 
108
+ def require(*mods)
109
+ deps = mods.last.is_a?(Hash) ? mods.pop : {}
110
+ mods = mods.map { |m| [m, m] }.to_h
111
+ self.dependencies = dependencies + mods.merge(deps).map { |name, package| Dependency.new(name, package) }
112
+ end
113
+
114
+ def function(name, _code = nil, timeout: Nodo.timeout, code: nil)
115
+ raise ArgumentError, "reserved method name #{name.inspect}" if reserved_method_name?(name)
116
+ code = (code ||= _code).strip
117
+ raise ArgumentError, 'function code is required' if '' == code
118
+ loc = caller_locations(1, 1)[0]
119
+ source_location = "#{loc.path}:#{loc.lineno}: in `#{name}'"
120
+ self.functions = functions.merge(name => Function.new(name, _code || code, source_location, timeout))
121
+ define_method(name) { |*args| call_js_method(name, args) }
122
+ end
123
+
124
+ def class_function(*methods)
125
+ singleton_class.def_delegators(:instance, *methods)
126
+ end
127
+
128
+ def const(name, value)
129
+ self.constants = constants + [Constant.new(name, value)]
130
+ end
131
+
132
+ def script(code)
133
+ self.scripts = scripts + [Script.new(code)]
134
+ end
135
+
122
136
  def nodo_js
123
137
  Pathname.new(__FILE__).dirname.join('nodo.js').to_s.to_json
124
138
  end
139
+
140
+ def reserved_method_name?(name)
141
+ Nodo::Core.method_defined?(name, false) || Nodo::Core.private_method_defined?(name, false) || name.to_s == DEFINE_METHOD
142
+ end
125
143
  end
126
144
 
127
145
  def initialize
146
+ raise ClassError, :new if self.class == Nodo::Core
128
147
  @@mutex.synchronize do
129
148
  ensure_process_is_spawned
130
149
  wait_for_socket
@@ -132,6 +151,13 @@ module Nodo
132
151
  end
133
152
  end
134
153
 
154
+ def evaluate(code)
155
+ ensure_context_is_defined
156
+ call_js_method(EVALUATE_METHOD, code)
157
+ end
158
+
159
+ private
160
+
135
161
  def node_pid
136
162
  @@node_pid
137
163
  end
@@ -148,9 +174,15 @@ module Nodo
148
174
  self.class.clsid
149
175
  end
150
176
 
177
+ def context_defined?
178
+ @context_defined
179
+ end
180
+
151
181
  def log_exception(e)
152
182
  return unless logger = Nodo.logger
153
- logger.error "\n#{e.class} (#{e.message}):\n\n#{e.backtrace.join("\n")}"
183
+ message = "\n#{e.class} (#{e.message})"
184
+ message << ":\n\n#{e.backtrace.join("\n")}" if e.backtrace
185
+ logger.error message
154
186
  end
155
187
 
156
188
  def ensure_process_is_spawned
@@ -164,12 +196,22 @@ module Nodo
164
196
  self.class.class_defined = true
165
197
  end
166
198
 
199
+ def ensure_context_is_defined
200
+ return if context_defined?
201
+ @@mutex.synchronize do
202
+ call_js_method(EVALUATE_METHOD, '')
203
+ ObjectSpace.define_finalizer(self, self.class.send(:finalize_context, self.object_id))
204
+ @context_defined = true
205
+ end
206
+ end
207
+
167
208
  def spawn_process
168
209
  @@tmpdir = Pathname.new(Dir.mktmpdir('nodo'))
169
210
  env = Nodo.env.merge('NODE_PATH' => Nodo.modules_root.to_s)
170
211
  env['NODO_DEBUG'] = '1' if Nodo.debug
171
212
  @@node_pid = Process.spawn(env, Nodo.binary, '-e', self.class.generate_core_code, '--', socket_path.to_s, err: :out)
172
213
  at_exit do
214
+ @@exiting = true
173
215
  Process.kill(:SIGTERM, node_pid) rescue Errno::ECHILD
174
216
  Process.wait(node_pid) rescue Errno::ECHILD
175
217
  FileUtils.remove_entry(tmpdir) if File.directory?(tmpdir)
@@ -179,7 +221,7 @@ module Nodo
179
221
  def wait_for_socket
180
222
  start = Time.now
181
223
  socket = nil
182
- while Time.now - start < TIMEOUT
224
+ while Time.now - start < LAUNCH_TIMEOUT
183
225
  begin
184
226
  break if socket = UNIXSocket.new(socket_path)
185
227
  rescue Errno::ENOENT, Errno::ECONNREFUSED, Errno::ENOTDIR
@@ -192,10 +234,16 @@ module Nodo
192
234
 
193
235
  def call_js_method(method, args)
194
236
  raise CallError, 'Node process not ready' unless node_pid
195
- raise CallError, "Class #{clsid} not defined" unless self.class.class_defined? || method == DEFINE_METHOD
237
+ raise CallError, "Class #{clsid} not defined" unless self.class.class_defined? || INTERNAL_METHODS.include?(method)
196
238
  function = self.class.functions[method]
197
- raise NameError, "undefined function `#{method}' for #{self.class}" unless function || method == DEFINE_METHOD
198
- request = Net::HTTP::Post.new("/#{clsid}/#{method}", 'Content-Type': 'application/json')
239
+ raise NameError, "undefined function `#{method}' for #{self.class}" unless function || INTERNAL_METHODS.include?(method)
240
+ context_id = case method
241
+ when DEFINE_METHOD then 0
242
+ when GC_METHOD then args.first
243
+ else
244
+ object_id
245
+ end
246
+ request = Net::HTTP::Post.new("/#{clsid}/#{context_id}/#{method}", 'Content-Type': 'application/json')
199
247
  request.body = JSON.dump(args)
200
248
  client = Client.new("unix://#{socket_path}")
201
249
  client.read_timeout = function.timeout if function
@@ -8,7 +8,7 @@ module Nodo
8
8
 
9
9
  def to_js
10
10
  <<~JS
11
- const #{name} = (() => {
11
+ const #{name} = __nodo_klass__.#{name} = (() => {
12
12
  try {
13
13
  return require(#{package.to_json});
14
14
  } catch(e) {
data/lib/nodo/errors.rb CHANGED
@@ -2,6 +2,11 @@ module Nodo
2
2
  class Error < StandardError; end
3
3
  class TimeoutError < Error; end
4
4
  class CallError < Error; end
5
+ class ClassError < Error
6
+ def initialize(method = nil)
7
+ super("Cannot call method `#{method}' on Nodo::Core, use subclass instead")
8
+ end
9
+ end
5
10
 
6
11
  class JavaScriptError < Error
7
12
  attr_reader :attributes
@@ -37,7 +42,7 @@ module Nodo
37
42
  end
38
43
 
39
44
  def generate_message
40
- message = "#{attributes['message'] || 'Unknown error'}"
45
+ message = "#{attributes['message'] || attributes['name'] || 'Unknown error'}"
41
46
  message << format_location(attributes['loc'])
42
47
  end
43
48
 
@@ -51,7 +56,7 @@ module Nodo
51
56
  private
52
57
 
53
58
  def generate_message
54
- message = "#{attributes['message'] || 'Dependency error'}\n"
59
+ message = "#{attributes['message'] || attributes['name'] || 'Dependency error'}\n"
55
60
  message << "The specified dependency '#{attributes['nodo_dependency']}' could not be loaded. "
56
61
  message << "Run 'yarn add #{attributes['nodo_dependency']}' to install it.\n"
57
62
  end
data/lib/nodo/nodo.js CHANGED
@@ -1,5 +1,7 @@
1
1
  module.exports = (function() {
2
2
  const DEFINE_METHOD = '__nodo_define_class__';
3
+ const EVALUATE_METHOD = '__nodo_evaluate__';
4
+ const GC_METHOD = '__nodo_gc__';
3
5
  const DEBUG = process.env.NODO_DEBUG;
4
6
 
5
7
  const vm = require('vm');
@@ -10,6 +12,7 @@ module.exports = (function() {
10
12
 
11
13
  let server, closing;
12
14
  const classes = {};
15
+ const contexts = {};
13
16
 
14
17
  function render_error(e) {
15
18
  let errInfo = {};
@@ -60,12 +63,17 @@ module.exports = (function() {
60
63
  }
61
64
 
62
65
  const url = req.url.substring(1);
63
- const [class_name, method] = url.split('/');
64
- let klass;
66
+ const [class_name, object_id, method] = url.split('/');
67
+ let klass, context;
65
68
 
66
69
  if (classes.hasOwnProperty(class_name)) {
67
70
  klass = classes[class_name];
68
- if (!klass.hasOwnProperty(method)) {
71
+ if (EVALUATE_METHOD == method) {
72
+ if (!contexts.hasOwnProperty(object_id)) {
73
+ contexts[object_id] = vm.createContext({ require: require, ...klass });
74
+ }
75
+ context = contexts[object_id];
76
+ } else if (!klass.hasOwnProperty(method) && !GC_METHOD == method) {
69
77
  return respond_with_error(res, 404, `Method ${class_name}#${method} not found`);
70
78
  }
71
79
  } else if (DEFINE_METHOD != method) {
@@ -87,13 +95,21 @@ module.exports = (function() {
87
95
 
88
96
  try {
89
97
  if (DEFINE_METHOD == method) {
90
- let new_class = vm.runInThisContext(input, class_name);
91
- classes[class_name] = new_class;
98
+ classes[class_name] = vm.runInThisContext(input, class_name);
92
99
  respond_with_data(res, class_name, start);
100
+ } else if (EVALUATE_METHOD == method) {
101
+ Promise.resolve(vm.runInNewContext(input, context)).then((result) => {
102
+ respond_with_data(res, result, start);
103
+ }).catch((error) => {
104
+ respond_with_error(res, 500, error);
105
+ });
106
+ } else if (GC_METHOD == method) {
107
+ delete contexts[object_id];
108
+ respond_with_data(res, true, start);
93
109
  } else {
94
- Promise.resolve(klass[method].apply(null, input)).then(function (result) {
110
+ Promise.resolve(klass[method].apply(null, input)).then((result) => {
95
111
  respond_with_data(res, result, start);
96
- }).catch(function(error) {
112
+ }).catch((error) => {
97
113
  respond_with_error(res, 500, error);
98
114
  });
99
115
  }
data/lib/nodo/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Nodo
2
- VERSION = '1.6.1'
2
+ VERSION = '1.6.2'
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: nodo
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.6.1
4
+ version: 1.6.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Matthias Grosser