nodo 1.6.0 → 1.6.4
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 +133 -4
- data/lib/nodo/constant.rb +1 -1
- data/lib/nodo/core.rb +89 -40
- data/lib/nodo/dependency.rb +10 -1
- data/lib/nodo/errors.rb +21 -6
- data/lib/nodo/function.rb +8 -3
- data/lib/nodo/nodo.js +30 -10
- data/lib/nodo/script.rb +5 -3
- data/lib/nodo/version.rb +1 -1
- data/lib/nodo.rb +2 -1
- metadata +1 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a4870340520e40bf14cf0e9261711983d5332eaf04419ff1281626ed5b4716e1
|
4
|
+
data.tar.gz: 162b5e4f6739728c91f64422acb579db9c004429f92402db5f393cfb76ffa675
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: dbe6fb8dce3f39968b26800a28f50b7d2c1ac49ffc60537a50950af598bc329b6ae59fae2db8285b16167238448ef370502d7b6f736c4dc4e66563ae0050ccfd
|
7
|
+
data.tar.gz: 37dd504e71489566a9b57df0e217c2f25d09ce3d511dab2ac6daf849829b90f1293ed07dd4e320eb70ef573da7b466263c704bb99cf46cb555449a8253909089
|
data/README.md
CHANGED
@@ -91,6 +91,23 @@ class FooBar < Nodo::Core
|
|
91
91
|
end
|
92
92
|
```
|
93
93
|
|
94
|
+
### Dynamic ESM imports
|
95
|
+
|
96
|
+
ES modules can be imported dynamically using `nodo.import()`:
|
97
|
+
|
98
|
+
```ruby
|
99
|
+
class DynamicFoo < Nodo::Core
|
100
|
+
function :v4, <<~JS
|
101
|
+
async () => {
|
102
|
+
const uuid = await nodo.import('uuid');
|
103
|
+
return await uuid.v4()
|
104
|
+
}
|
105
|
+
JS
|
106
|
+
end
|
107
|
+
```
|
108
|
+
|
109
|
+
Note that the availability of dynamic imports depends on your Node version.
|
110
|
+
|
94
111
|
### Alternate function definition syntax
|
95
112
|
|
96
113
|
JS code can also be supplied using the `code:` keyword argument:
|
@@ -131,6 +148,23 @@ class BarFoo < Nodo::Core
|
|
131
148
|
end
|
132
149
|
```
|
133
150
|
|
151
|
+
With the above syntax, the script code will be generated during class definition
|
152
|
+
time. In order to have the code generated when the first instance is created, the
|
153
|
+
code can be defined inside a block:
|
154
|
+
|
155
|
+
```ruby
|
156
|
+
class Foo < Nodo::Core
|
157
|
+
script do
|
158
|
+
<<~JS
|
159
|
+
var definitionTime = #{Time.now.to_json};
|
160
|
+
JS
|
161
|
+
end
|
162
|
+
end
|
163
|
+
```
|
164
|
+
|
165
|
+
Note that the script will still be executed only once, when the first instance
|
166
|
+
of class is created.
|
167
|
+
|
134
168
|
### Inheritance
|
135
169
|
|
136
170
|
Subclasses will inherit functions, constants, dependencies and scripts from
|
@@ -168,10 +202,50 @@ class SyncFoo < Nodo::Core
|
|
168
202
|
end
|
169
203
|
```
|
170
204
|
|
205
|
+
### Deferred function definition
|
206
|
+
|
207
|
+
By default, the function code string literal is created when the class
|
208
|
+
is defined. Therefore any string interpolation inside the code will take
|
209
|
+
place at definition time.
|
210
|
+
|
211
|
+
In order to defer the code generation until the first object instantiation,
|
212
|
+
the function code can be given inside a block:
|
213
|
+
|
214
|
+
```ruby
|
215
|
+
class Deferred < Nodo::Core
|
216
|
+
function :now, <<~JS
|
217
|
+
() => { return #{Time.now.to_json}; }
|
218
|
+
JS
|
219
|
+
|
220
|
+
function :later do
|
221
|
+
<<~JS
|
222
|
+
() => { return #{Time.now.to_json}; }
|
223
|
+
JS
|
224
|
+
end
|
225
|
+
end
|
226
|
+
|
227
|
+
instance = Deferred.new
|
228
|
+
sleep 5
|
229
|
+
instance.now => "2021-10-28 20:30:00 +0200"
|
230
|
+
instance.later => "2021-10-28 20:30:05 +0200"
|
231
|
+
```
|
232
|
+
|
233
|
+
The block will be invoked when the first instance is created. As with deferred
|
234
|
+
scripts, it will only be invoked once.
|
235
|
+
|
171
236
|
### Limiting function execution time
|
172
237
|
|
173
|
-
The default timeout for a single JS function call is 60 seconds
|
174
|
-
|
238
|
+
The default timeout for a single JS function call is 60 seconds and can be
|
239
|
+
set globally:
|
240
|
+
|
241
|
+
```ruby
|
242
|
+
Nodo.timeout = 5
|
243
|
+
```
|
244
|
+
|
245
|
+
If the execution of a single function call exceeds the timeout, `Nodo::TimeoutError`
|
246
|
+
is raised.
|
247
|
+
|
248
|
+
The timeout can also be set on a per-function basis:
|
175
249
|
|
176
250
|
```ruby
|
177
251
|
class Foo < Nodo::Core
|
@@ -208,8 +282,63 @@ Nodo.debug = true
|
|
208
282
|
before instantiating any worker instances. The debug mode will be active during
|
209
283
|
the current process run.
|
210
284
|
|
285
|
+
To print a debug message from JS code:
|
286
|
+
|
287
|
+
```js
|
288
|
+
nodo.debug("Debug message");
|
289
|
+
```
|
290
|
+
|
291
|
+
### Evaluation
|
292
|
+
|
293
|
+
While `Nodo` is mainly function-based, it is possible to evaluate JS code in the
|
294
|
+
context of the defined object.
|
295
|
+
|
296
|
+
```ruby
|
297
|
+
foo = Foo.new.evaluate("3 + 5")
|
298
|
+
=> 8
|
299
|
+
```
|
300
|
+
|
301
|
+
Evaluated code can access functions, required dependencies and constants:
|
302
|
+
|
303
|
+
```ruby
|
304
|
+
class Foo < Nodo::Core
|
305
|
+
const :BAR, 'bar'
|
306
|
+
require :uuid
|
307
|
+
function :hello, code: '() => "world"'
|
308
|
+
end
|
309
|
+
|
310
|
+
foo = Foo.new
|
311
|
+
|
312
|
+
foo.evaluate('BAR')
|
313
|
+
=> "bar"
|
314
|
+
|
315
|
+
foo.evaluate('uuid.v4()')
|
316
|
+
=> "f258bef3-0d6f-4566-ad39-d8dec973ef6b"
|
317
|
+
|
318
|
+
foo.evaluate('hello()')
|
319
|
+
=> "world"
|
320
|
+
```
|
321
|
+
|
322
|
+
Variables defined by evaluation are local to the current instance:
|
323
|
+
|
324
|
+
```ruby
|
325
|
+
one = Foo.new
|
326
|
+
one.evaluate('a = 1')
|
327
|
+
two = Foo.new
|
328
|
+
two.evaluate('a = 2')
|
329
|
+
one.evaluate('a') => 1
|
330
|
+
two.evaluate('a') => 2
|
331
|
+
```
|
332
|
+
|
333
|
+
⚠️ Evaluation comes with the usual caveats:
|
334
|
+
|
335
|
+
- Avoid modifying any of your predefined identifiers. Remember that in JS,
|
336
|
+
as in Ruby, constants are not necessarily constant.
|
337
|
+
- Never evaluate any code which includes un-checked user data. The Node.js process
|
338
|
+
has full read/write access to your filesystem! 💥
|
339
|
+
|
211
340
|
|
212
|
-
|
341
|
+
## Clean your Rails root
|
213
342
|
|
214
343
|
For Rails applications, Nodo enables you to move `node_modules`, `package.json` and
|
215
344
|
`yarn.lock` into your application's `vendor` folder by setting the `NODE_PATH` in
|
@@ -220,7 +349,7 @@ an initializer:
|
|
220
349
|
Nodo.modules_root = Rails.root.join('vendor', 'node_modules')
|
221
350
|
```
|
222
351
|
|
223
|
-
The rationale
|
352
|
+
The rationale for this is NPM modules being external vendor dependencies, which
|
224
353
|
should not clutter the application root directory.
|
225
354
|
|
226
355
|
With this new default, all `yarn` operations should be done after `cd`ing to `vendor`.
|
data/lib/nodo/constant.rb
CHANGED
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
|
-
|
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: 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))
|
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
|
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,55 @@ 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, &block)
|
115
|
+
raise ArgumentError, "reserved method name #{name.inspect}" if reserved_method_name?(name)
|
116
|
+
loc = caller_locations(1, 1)[0]
|
117
|
+
source_location = "#{loc.path}:#{loc.lineno}: in `#{name}'"
|
118
|
+
self.functions = functions.merge(name => Function.new(name, _code || code, source_location, timeout, &block))
|
119
|
+
define_method(name) { |*args| call_js_method(name, args) }
|
120
|
+
end
|
121
|
+
|
122
|
+
def class_function(*methods)
|
123
|
+
singleton_class.def_delegators(:instance, *methods)
|
124
|
+
end
|
125
|
+
|
126
|
+
def const(name, value)
|
127
|
+
self.constants = constants + [Constant.new(name, value)]
|
128
|
+
end
|
129
|
+
|
130
|
+
def script(code = nil, &block)
|
131
|
+
self.scripts = scripts + [Script.new(code, &block)]
|
132
|
+
end
|
133
|
+
|
122
134
|
def nodo_js
|
123
135
|
Pathname.new(__FILE__).dirname.join('nodo.js').to_s.to_json
|
124
136
|
end
|
137
|
+
|
138
|
+
def reserved_method_name?(name)
|
139
|
+
Nodo::Core.method_defined?(name, false) || Nodo::Core.private_method_defined?(name, false) || name.to_s == DEFINE_METHOD
|
140
|
+
end
|
125
141
|
end
|
126
142
|
|
127
143
|
def initialize
|
144
|
+
raise ClassError, :new if self.class == Nodo::Core
|
128
145
|
@@mutex.synchronize do
|
129
146
|
ensure_process_is_spawned
|
130
147
|
wait_for_socket
|
@@ -132,6 +149,13 @@ module Nodo
|
|
132
149
|
end
|
133
150
|
end
|
134
151
|
|
152
|
+
def evaluate(code)
|
153
|
+
ensure_context_is_defined
|
154
|
+
call_js_method(EVALUATE_METHOD, code)
|
155
|
+
end
|
156
|
+
|
157
|
+
private
|
158
|
+
|
135
159
|
def node_pid
|
136
160
|
@@node_pid
|
137
161
|
end
|
@@ -148,9 +172,15 @@ module Nodo
|
|
148
172
|
self.class.clsid
|
149
173
|
end
|
150
174
|
|
175
|
+
def context_defined?
|
176
|
+
@context_defined
|
177
|
+
end
|
178
|
+
|
151
179
|
def log_exception(e)
|
152
180
|
return unless logger = Nodo.logger
|
153
|
-
|
181
|
+
message = "\n#{e.class} (#{e.message})"
|
182
|
+
message << ":\n\n#{e.backtrace.join("\n")}" if e.backtrace
|
183
|
+
logger.error message
|
154
184
|
end
|
155
185
|
|
156
186
|
def ensure_process_is_spawned
|
@@ -164,12 +194,22 @@ module Nodo
|
|
164
194
|
self.class.class_defined = true
|
165
195
|
end
|
166
196
|
|
197
|
+
def ensure_context_is_defined
|
198
|
+
return if context_defined?
|
199
|
+
@@mutex.synchronize do
|
200
|
+
call_js_method(EVALUATE_METHOD, '')
|
201
|
+
ObjectSpace.define_finalizer(self, self.class.send(:finalize_context, self.object_id))
|
202
|
+
@context_defined = true
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
167
206
|
def spawn_process
|
168
207
|
@@tmpdir = Pathname.new(Dir.mktmpdir('nodo'))
|
169
208
|
env = Nodo.env.merge('NODE_PATH' => Nodo.modules_root.to_s)
|
170
209
|
env['NODO_DEBUG'] = '1' if Nodo.debug
|
171
210
|
@@node_pid = Process.spawn(env, Nodo.binary, '-e', self.class.generate_core_code, '--', socket_path.to_s, err: :out)
|
172
211
|
at_exit do
|
212
|
+
@@exiting = true
|
173
213
|
Process.kill(:SIGTERM, node_pid) rescue Errno::ECHILD
|
174
214
|
Process.wait(node_pid) rescue Errno::ECHILD
|
175
215
|
FileUtils.remove_entry(tmpdir) if File.directory?(tmpdir)
|
@@ -179,7 +219,7 @@ module Nodo
|
|
179
219
|
def wait_for_socket
|
180
220
|
start = Time.now
|
181
221
|
socket = nil
|
182
|
-
while Time.now - start <
|
222
|
+
while Time.now - start < LAUNCH_TIMEOUT
|
183
223
|
begin
|
184
224
|
break if socket = UNIXSocket.new(socket_path)
|
185
225
|
rescue Errno::ENOENT, Errno::ECONNREFUSED, Errno::ENOTDIR
|
@@ -192,13 +232,19 @@ module Nodo
|
|
192
232
|
|
193
233
|
def call_js_method(method, args)
|
194
234
|
raise CallError, 'Node process not ready' unless node_pid
|
195
|
-
raise CallError, "Class #{clsid} not defined" unless self.class.class_defined? || method
|
235
|
+
raise CallError, "Class #{clsid} not defined" unless self.class.class_defined? || INTERNAL_METHODS.include?(method)
|
196
236
|
function = self.class.functions[method]
|
197
|
-
raise NameError, "undefined function `#{method}' for #{self.class}" unless function || method
|
198
|
-
|
237
|
+
raise NameError, "undefined function `#{method}' for #{self.class}" unless function || INTERNAL_METHODS.include?(method)
|
238
|
+
context_id = case method
|
239
|
+
when DEFINE_METHOD then 0
|
240
|
+
when GC_METHOD then args.first
|
241
|
+
else
|
242
|
+
object_id
|
243
|
+
end
|
244
|
+
request = Net::HTTP::Post.new("/#{clsid}/#{context_id}/#{method}", 'Content-Type': 'application/json')
|
199
245
|
request.body = JSON.dump(args)
|
200
246
|
client = Client.new("unix://#{socket_path}")
|
201
|
-
client.read_timeout = function
|
247
|
+
client.read_timeout = function&.timeout || Nodo.timeout
|
202
248
|
response = client.request(request)
|
203
249
|
if response.is_a?(Net::HTTPOK)
|
204
250
|
parse_response(response)
|
@@ -215,7 +261,10 @@ module Nodo
|
|
215
261
|
def handle_error(response, function)
|
216
262
|
if response.body
|
217
263
|
result = parse_response(response)
|
218
|
-
error =
|
264
|
+
error = if result.is_a?(Hash) && result['error'].is_a?(Hash)
|
265
|
+
attrs = result['error']
|
266
|
+
(attrs['nodo_dependency'] ? DependencyError : JavaScriptError).new(attrs, function)
|
267
|
+
end
|
219
268
|
end
|
220
269
|
error ||= CallError.new("Node returned #{response.code}")
|
221
270
|
log_exception(error)
|
data/lib/nodo/dependency.rb
CHANGED
@@ -7,7 +7,16 @@ module Nodo
|
|
7
7
|
end
|
8
8
|
|
9
9
|
def to_js
|
10
|
-
|
10
|
+
<<~JS
|
11
|
+
const #{name} = __nodo_klass__.#{name} = (() => {
|
12
|
+
try {
|
13
|
+
return require(#{package.to_json});
|
14
|
+
} catch(e) {
|
15
|
+
e.nodo_dependency = #{package.to_json};
|
16
|
+
throw e;
|
17
|
+
}
|
18
|
+
})();
|
19
|
+
JS
|
11
20
|
end
|
12
21
|
end
|
13
22
|
end
|
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,13 +42,23 @@ module Nodo
|
|
37
42
|
end
|
38
43
|
|
39
44
|
def generate_message
|
40
|
-
message = "#{attributes['message'] || 'Unknown error'}"
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
+
message = "#{attributes['message'] || attributes['name'] || 'Unknown error'}"
|
46
|
+
message << format_location(attributes['loc'])
|
47
|
+
end
|
48
|
+
|
49
|
+
def format_location(loc)
|
50
|
+
return '' unless loc
|
51
|
+
loc.inject(' in') { |s, (key, value)| s << " #{key}: #{value}" }
|
45
52
|
end
|
46
53
|
end
|
47
54
|
|
48
|
-
class DependencyError < JavaScriptError
|
55
|
+
class DependencyError < JavaScriptError
|
56
|
+
private
|
57
|
+
|
58
|
+
def generate_message
|
59
|
+
message = "#{attributes['message'] || attributes['name'] || 'Dependency error'}\n"
|
60
|
+
message << "The specified dependency '#{attributes['nodo_dependency']}' could not be loaded. "
|
61
|
+
message << "Run 'yarn add #{attributes['nodo_dependency']}' to install it.\n"
|
62
|
+
end
|
63
|
+
end
|
49
64
|
end
|
data/lib/nodo/function.rb
CHANGED
@@ -2,12 +2,17 @@ module Nodo
|
|
2
2
|
class Function
|
3
3
|
attr_reader :name, :code, :source_location, :timeout
|
4
4
|
|
5
|
-
def initialize(name, code, source_location, timeout)
|
6
|
-
|
5
|
+
def initialize(name, code, source_location, timeout, &block)
|
6
|
+
raise ArgumentError, 'cannot give code when block is given' if code && block
|
7
|
+
code = code.strip if code
|
8
|
+
raise ArgumentError, 'function code is required' if '' == code
|
9
|
+
raise ArgumentError, 'code is required' unless code || block
|
10
|
+
@name, @code, @source_location, @timeout = name, code || block, source_location, timeout
|
7
11
|
end
|
8
12
|
|
9
13
|
def to_js
|
10
|
-
|
14
|
+
js = code.respond_to?(:call) ? code.call.strip : code
|
15
|
+
"const #{name} = __nodo_klass__.#{name} = (#{js});\n"
|
11
16
|
end
|
12
17
|
end
|
13
18
|
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');
|
@@ -7,9 +9,10 @@ module.exports = (function() {
|
|
7
9
|
const path = require('path');
|
8
10
|
const fs = require('fs');
|
9
11
|
const performance = require('perf_hooks').performance;
|
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 = {};
|
@@ -45,7 +48,11 @@ module.exports = (function() {
|
|
45
48
|
console.log(`[Nodo] ${message}`);
|
46
49
|
}
|
47
50
|
}
|
48
|
-
|
51
|
+
|
52
|
+
async function import_module(specifier) {
|
53
|
+
return await import(specifier);
|
54
|
+
}
|
55
|
+
|
49
56
|
const core = {
|
50
57
|
run: (socket) => {
|
51
58
|
debug('Starting up...');
|
@@ -60,12 +67,17 @@ module.exports = (function() {
|
|
60
67
|
}
|
61
68
|
|
62
69
|
const url = req.url.substring(1);
|
63
|
-
const [class_name, method] = url.split('/');
|
64
|
-
let klass;
|
70
|
+
const [class_name, object_id, method] = url.split('/');
|
71
|
+
let klass, context;
|
65
72
|
|
66
73
|
if (classes.hasOwnProperty(class_name)) {
|
67
74
|
klass = classes[class_name];
|
68
|
-
if (
|
75
|
+
if (EVALUATE_METHOD == method) {
|
76
|
+
if (!contexts.hasOwnProperty(object_id)) {
|
77
|
+
contexts[object_id] = vm.createContext({ require: require, ...klass });
|
78
|
+
}
|
79
|
+
context = contexts[object_id];
|
80
|
+
} else if (!klass.hasOwnProperty(method) && !GC_METHOD == method) {
|
69
81
|
return respond_with_error(res, 404, `Method ${class_name}#${method} not found`);
|
70
82
|
}
|
71
83
|
} else if (DEFINE_METHOD != method) {
|
@@ -87,13 +99,21 @@ module.exports = (function() {
|
|
87
99
|
|
88
100
|
try {
|
89
101
|
if (DEFINE_METHOD == method) {
|
90
|
-
|
91
|
-
classes[class_name] = new_class;
|
102
|
+
classes[class_name] = vm.runInThisContext(input, class_name);
|
92
103
|
respond_with_data(res, class_name, start);
|
104
|
+
} else if (EVALUATE_METHOD == method) {
|
105
|
+
Promise.resolve(vm.runInNewContext(input, context)).then((result) => {
|
106
|
+
respond_with_data(res, result, start);
|
107
|
+
}).catch((error) => {
|
108
|
+
respond_with_error(res, 500, error);
|
109
|
+
});
|
110
|
+
} else if (GC_METHOD == method) {
|
111
|
+
delete contexts[object_id];
|
112
|
+
respond_with_data(res, true, start);
|
93
113
|
} else {
|
94
|
-
Promise.resolve(klass[method].apply(null, input)).then(
|
114
|
+
Promise.resolve(klass[method].apply(null, input)).then((result) => {
|
95
115
|
respond_with_data(res, result, start);
|
96
|
-
}).catch(
|
116
|
+
}).catch((error) => {
|
97
117
|
respond_with_error(res, 500, error);
|
98
118
|
});
|
99
119
|
}
|
@@ -119,5 +139,5 @@ module.exports = (function() {
|
|
119
139
|
}
|
120
140
|
};
|
121
141
|
|
122
|
-
return { core: core, debug: debug };
|
142
|
+
return { core: core, debug: debug, import: import_module };
|
123
143
|
})();
|
data/lib/nodo/script.rb
CHANGED
@@ -2,12 +2,14 @@ module Nodo
|
|
2
2
|
class Script
|
3
3
|
attr_reader :code
|
4
4
|
|
5
|
-
def initialize(code)
|
6
|
-
|
5
|
+
def initialize(code = nil, &block)
|
6
|
+
raise ArgumentError, 'cannot give code when block is given' if code && block
|
7
|
+
@code = code || block
|
7
8
|
end
|
8
9
|
|
9
10
|
def to_js
|
10
|
-
|
11
|
+
js = code.respond_to?(:call) ? code.call : code
|
12
|
+
"#{js}\n"
|
11
13
|
end
|
12
14
|
end
|
13
15
|
end
|
data/lib/nodo/version.rb
CHANGED
data/lib/nodo.rb
CHANGED
@@ -9,13 +9,14 @@ require 'forwardable'
|
|
9
9
|
|
10
10
|
module Nodo
|
11
11
|
class << self
|
12
|
-
attr_accessor :modules_root, :env, :binary, :logger, :debug
|
12
|
+
attr_accessor :modules_root, :env, :binary, :logger, :debug, :timeout
|
13
13
|
end
|
14
14
|
self.modules_root = './node_modules'
|
15
15
|
self.env = {}
|
16
16
|
self.binary = 'node'
|
17
17
|
self.logger = Logger.new(STDOUT)
|
18
18
|
self.debug = false
|
19
|
+
self.timeout = 60
|
19
20
|
end
|
20
21
|
|
21
22
|
require_relative 'nodo/version'
|