nodo 1.6.1 → 1.6.2

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