jscall 1.0.1 → 1.1.0

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: 97c7ab8cd5d710cfca8c77f6e906643252b9c54cc5973709763b89aef3830dee
4
- data.tar.gz: e205fad16cc87df640b186ac99190186374f2db821a7e3d499563add9016b097
3
+ metadata.gz: a5fb1072aa12a5e884d9c92537f6ae7a5c632d4940715008865ec9760940f1c2
4
+ data.tar.gz: 597c3199f5ee21f98d80a42b05bf9822b729a22311d7c9f43e2cf45d54594983
5
5
  SHA512:
6
- metadata.gz: f25ebf232e77432bb5c79f511c4fc99de78e2d7861e4d3831edcf3662e747021d5d4a207036c8a7add2fe1822c9fb8fdb7439b1e17e67627abea090c1285662c
7
- data.tar.gz: 8e02fe7a9f3d7f0d82ac70f24d33335c2dffe1261133e2dec4a1c4ff65ec9b87bb9fad8f0049dee3fb681c20e7b3f47d9a97890b45958199f1e78c0b76fd69b5
6
+ metadata.gz: 54c16c0015f2296309523ecf5d966bbf2071303136afef04e0bbdf139aa204c4b3db332a73ea65c0194fce7cea795503db17577805b4110a56edda796fca0a0b
7
+ data.tar.gz: ada3a68d6c19093c22087982e520ed18a0f3f3709584fc2c30978aed7909c1b6b8568eb48e24fde6c57cc759dc235b1a824524b1c0d43053d4d80512767339a8
data/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # Jscall
2
2
 
3
+ [![Ruby](https://github.com/csg-tokyo/jscall/actions/workflows/ruby.yml/badge.svg)](https://github.com/csg-tokyo/jscall/actions/workflows/ruby.yml)
4
+
3
5
  Jscall allows executing a program in JavaScript on node.js or a web browser.
4
6
  By default, node.js is used for the execution.
5
7
  To choose a web browser, call `Jscall.config`.
@@ -19,10 +21,14 @@ This returns `2`. The argument passed to `Jscall.exec` can be
19
21
  multipe lines. It is executed as source code written in JavaScript.
20
22
 
21
23
  `Jscall.exec` returns a resulting value. Numbers, character strings (and symbols), boolean values, and `nil` (and `null`)
22
- are copied when passing between Ruby and JavaScript. An array is also shallow-copied.
24
+ are copied when passing between Ruby and JavaScript. An array is shallow-copied.
23
25
  Other objects are not copied. When they are passed, a remote reference is created at the destination.
26
+ When a `Map` object is returned from JavaScript to Ruby, it is also
27
+ shallow-copied but into a `Hash` object in Ruby.
24
28
 
25
- A remote reference is a proxy object. A method call on a remote reference invokes a method on the corresponding object on the remote site. For example,
29
+ A remote reference is a local reference to a proxy object.
30
+ A method call on a remote reference invokes a method on the corresponding
31
+ object on the remote site. For example,
26
32
 
27
33
  ```
28
34
  js_obj = Jscall.exec '({ foo: (x) => x + 1, bar: 7 })'
@@ -35,10 +41,29 @@ js_obj.baz # 9
35
41
  The `foo` method is executed in JavaScript.
36
42
  Since `bar` is not a function, its value is returned to Ruby as it is.
37
43
 
38
- Setting a object property to a given value is also
44
+ Setting an object property to a given value is also
39
45
  allowed. The expression `js_obj.baz = 9` above sets
40
46
  the object property `baz` to 9.
41
47
 
48
+ An argument to a JavaScript method is copied from Ruby to
49
+ JavaScript unless it is an object. When an argument is a Ruby object,
50
+ a proxy object is created in JavaScript. The rule is the same as the
51
+ rule for returning a value from JavaScript to Ruby. A primitive
52
+ value is copied but an object is not. An array is shallow-copied.
53
+
54
+ A `Hash` object in Ruby is also shallow-copied into JavaScript but a normal
55
+ object is created in JavaScript. Recall that a JavaScript object is
56
+ regarded as an associative array, or a hash table as in Ruby.
57
+ For example, in Ruby,
58
+
59
+ ```
60
+ obj = { a: 2, b: 3 }
61
+ ```
62
+
63
+ when this ruby object is passed to JavaScript as an argument,
64
+ a normal object `{ a: 2, b: 3 }` is created as its copy in JavaScript
65
+ and passed to a JavaScript method.
66
+
42
67
  To call a JavaScript function from Ruby, call a mehtod on `Jscall`.
43
68
  For example,
44
69
 
@@ -54,9 +79,22 @@ Jscall.foo(7) # 8
54
79
  `Jscall.foo(7)` invokes the JavaScript function with the name following `Jscall.`
55
80
  with the given argument. In this case,
56
81
  the `foo` function is executed with the argument `7`.
82
+ Arguments and a return value are passed to/from a function
83
+ as they are passed to/from a method.
84
+
85
+ `Jscall` can be used for obtaining a remote reference to access a global variable
86
+ in JavaScript. For example,
87
+
88
+ ```
89
+ Jscall.console.log('Hello')
90
+ ```
91
+
92
+ This prints `Hello` on a JavaScript console. `Jscall.console` returns a remote
93
+ refererence to the value of `console` in JavaScript. Then, `.log('Hello')`
94
+ calls the `log` method on `console` in JavaScript.
57
95
 
58
96
  When a Ruby object is passed to a JavaScript function/method,
59
- it can call a method on the passed Ruby object.
97
+ you can call a method on the passed Ruby object.
60
98
 
61
99
  ```
62
100
  Jscall.exec <<CODE
@@ -84,10 +122,19 @@ CODE
84
122
  Jscall.foo()
85
123
  ```
86
124
 
87
- `Jscall.foo()` returns the result of evaluating `RUBY_VERSION`
88
- in Ruby.
125
+ `Jscall.foo()` returns the result of evaluating given Ruby code
126
+ `RUBY_VERSION` in Ruby.
89
127
  Don't forget to `await` a call to `Ruby.exec`.
90
128
 
129
+ ### Remote references
130
+
131
+ A remote reference is implemented by a local reference to a proxy
132
+ object representing the remote object that the remote reference refers to.
133
+ When a proxy object is passed as an argument or a return value
134
+ from Ruby to JavaScript (or vice versa), the corresponding JavaScript
135
+ (or Ruby) object is passed to the destination. In other words,
136
+ a remote reference passed is converted back to a local reference.
137
+
91
138
  Remote references will be automatically reclaimed when they are no
92
139
  longer used. To reclaim them immediately, call:
93
140
 
@@ -95,6 +142,25 @@ longer used. To reclaim them immediately, call:
95
142
  Jscall.scavenge_references
96
143
  ```
97
144
 
145
+ As mentioned above, a remote reference is a local reference
146
+ to a proxy object. In Ruby,
147
+ even a proxy object provides a number of methods inherited from `Object` class,
148
+ such as `clone`, `to_s`, and `inspect`. A call to such a method is not
149
+ delegated to the corresponding JavaScript object. To invoke such a method
150
+ on a JavaScript object, call `send` on its proxy object.
151
+ For example,
152
+
153
+ ```
154
+ js_obj = Jscall.exec '({ to_s: (x, y) => x + y })'
155
+ puts js_obj.to_s(3, 4) # error
156
+ puts js_obj.send('to_s', 3, 4) # 7
157
+ ```
158
+
159
+ The `send` method invokes the JavaScript method with the name specified
160
+ by the first argument. The remaining arguments passed to `send` are passed
161
+ to that JavaScript method.
162
+
163
+
98
164
  ## DOM manipulation
99
165
 
100
166
  When JavaScript code is run on a browser, some utility methods
@@ -109,7 +175,16 @@ links `mystyle.css` in the current directory.
109
175
  - `Jscall.dom.print(msg)`
110
176
 
111
177
  This adds a `p` element to the DOM tree.
178
+ Its inner text is the character string passed as `msg`.
112
179
 
180
+ - `Jscall.dom.append_to_body(html_source)`
181
+
182
+ This inserts the given `html_source` at the end of the `body` element.
183
+ It is a shorthand for
184
+
185
+ ```
186
+ Jscall.document.body.insertAdjacentHTML('beforeend', html_source)
187
+ ```
113
188
 
114
189
  ## Variable scope
115
190
 
@@ -142,16 +217,33 @@ for loading a CommonJS module. For example,
142
217
  Jscall.exec "mime = require('./mime.js')"
143
218
  ```
144
219
 
145
- The file `./mime.js` is loaded and the module is bound to a global variable `mime`.
220
+ The file `./mime.js` is loaded and the module is bound to a global variable `mime` in JavaScript.
221
+
222
+ You can directly call `require` on `Jscall` in Ruby.
223
+
224
+ ```
225
+ parser = Jscall.require("@babel/parser")
226
+ ast = parser.parse('const i = 3')
227
+ Jscall.console.log(ast)
228
+ ```
229
+
230
+ `require` will search `./node_modules/` for `@babel/parser`.
231
+ This is equivalent to the following JavaScript code.
146
232
 
147
- You may want to call `Jscall.dyn_import` in Ruby.
233
+ ```
234
+ parser = require("@babel/parser")
235
+ ast = parser.parse('const i = 3')
236
+ console.log(ast)
237
+ ```
238
+
239
+ Dynamic importing is also available. Call `Jscall.dyn_import` in Ruby.
148
240
 
149
241
  ```
150
242
  fs = Jscall.dyn_import('fs')
151
243
  ```
152
244
 
153
245
  This executes dynamic importing in JavaScript.
154
- For node.js, the file name of the imported module should be a full path name. For a web browser, the root directory is a current working directory. So `Jscall.dyn_import('/mine.mjs')` loads the file `./mine.mjs`.
246
+ For node.js, the file name of the imported module should be a full path name. For a web browser, the root directory is the current working directory. So `Jscall.dyn_import('/mine.mjs')` loads the file `./mine.mjs`.
155
247
 
156
248
  `Jscall.dyn_import` takes the second argument. If it is given,
157
249
  a global variable in JavaScript is bound to the loaded module.
@@ -166,49 +258,112 @@ This is quite equivalent to the following JavaScript code:
166
258
  fs_module = await load('fs')
167
259
  ```
168
260
 
261
+ ## Promise
262
+
263
+ If a program attempts to pass a `Promise` object from JavaScript to Ruby,
264
+ it waits until the promise is fullfilled. Then Jscall passes
265
+ the value of that promise from JavaScript to Ruby instead of that
266
+ promise itself (or a remote reference to that promise). When that promise
267
+ is rejected, an error object is passed to Ruby
268
+ so that the error will be raised in Ruby.
269
+ This design reflects the fact that an `async` function in JavaScript
270
+ also returns a `Promise` object but this object must not be returned
271
+ to Ruby as is when that `async` function is called from Ruby.
272
+ Jscall cannot determine whether a promise should be passed as is to Ruby
273
+ or its value must be passed to Ruby after the promise is fullfilled.
274
+
275
+ When enforcing Jscall to pass a `Promise` object from JavaScript to Ruby,
276
+ `.async` must be inserted between a receiver and a method name.
277
+
278
+ ```
279
+ Jscall.exec(<<CODE)
280
+ function make_promise() {
281
+ return { a: Promise.resolve(7) }
282
+ }
283
+ CODE
284
+
285
+ obj = Jscall.make_promise
286
+ result = obj.a # 7
287
+ prom = obj.async.a # promise
288
+ prom.then(->(r) { puts r }) # 7
289
+ ```
290
+
169
291
 
170
292
  ## Configuration
171
293
 
172
- To import JavaScript modules when node.js starts,
294
+ Jscall supports several configuration options.
295
+ Call `Jscall.config` with necessary options.
296
+
297
+ ### module_names:
298
+
299
+ To import JavaScript modules when node.js or a web browser starts,
173
300
 
174
301
  ```
175
- Jscall.config(module_names: [["Foo", "./foo.mjs"], ["Bar", "./bar.mjs"]], options: "--use-strict")
302
+ Jscall.config(module_names: [["Foo", "./js", "/lib/foo.mjs"], ["Bar", "./js", "/lib/bar.mjs"]])
176
303
  ```
177
304
 
178
- This specifies that `./foo.mjs` and `./bar.mjs` are impoted when node.js starts.
305
+ This specifies that `./js/lib/foo.mjs` and `./js/lib/bar.mjs` are imported
306
+ at the beginning.
179
307
  This is equivalent to the following import declarations:
180
308
 
181
309
  ```
182
- import * as "Foo" from "./foo.mjs"
183
- import * as "Bar" from "./bar.mjs"
310
+ import * as "Foo" from "./js/lib/foo.mjs"
311
+ import * as "Bar" from "./js/lib/bar.mjs"
312
+ ```
313
+
314
+ Note that each array element given to `module_names:` is
315
+
316
+ ```
317
+ [<module_name> <root> <path>]
184
318
  ```
185
319
 
186
- `'--use-strict'` is passed to node.js as a command line argument.
320
+ `<path>` must start with `/`. It is used as a part of the URL when a browser
321
+ accesses a module.
322
+ When importing a module for node.js, `<root>` and `<path>` are concatenated
323
+ to form a full path name.
187
324
 
188
- `module_names:` and `options:` are optional arguments to `Jscall.config`.
325
+ `<path>` must not start with `/jscall` or `/cmd`. They are reserved for
326
+ internal use.
189
327
 
190
- To change the name of the node command,
328
+ ### options:
329
+
330
+ To specify a command line argument passed to node.js,
191
331
 
192
332
  ```
193
- Jscall::PipeToJs.node_command = "node.exe"
333
+ Jscall.config(options: '--use-strict')
194
334
  ```
195
335
 
196
- The default command name is `"node"`.
336
+ This call specifies that
337
+ `--use-strict` is passed as a command line argument.
338
+
339
+ ### browser: and port:
197
340
 
198
341
  When running JavaScript code on a web browser,
199
342
 
200
343
  ```
201
- Jscall.config(browser: true, options: {port: 10082})
344
+ Jscall.config(browser: true, port: 10082)
202
345
  ```
203
346
 
204
- `options:` is an optional argument.
347
+ Passing `true` for `browser:` switches the execution engine to a web browser.
348
+ The default engine is node.js.
349
+ To switch the engine back to node.js, pass `false` for `browser:`.
350
+ Call `Jscall.close` to detach the current execution engine.
351
+ A new enigine with a new configuration will be created.
352
+
353
+ `port:` specifies the port number of an http server. It is optional.
205
354
  The example above specifies that Ruby receives http requests
206
355
  sent to http://localhost:10082 from JavaScript on a web browser.
207
356
 
208
- Passing `false` for `browser:` to `Jscall.config` switches
209
- the execution engine to node.js.
210
- Call `Jscall.close` to detach the current execution engine.
211
- A new enigine with a new configuration will be created.
357
+
358
+ ### Other configurations
359
+
360
+ To change the name of the node command,
361
+
362
+ ```
363
+ Jscall::PipeToJs.node_command = "node.exe"
364
+ ```
365
+
366
+ The default command name is `"node"`.
212
367
 
213
368
  To change the command for launching a web browser,
214
369
 
@@ -224,6 +379,12 @@ Jscall launches a web browser by the command like the following:
224
379
  open http://localhost:10082/jscall/jscall.html
225
380
  ```
226
381
 
382
+ Jscall generates a verbose error message if its debug level is more than 0.
383
+
384
+ ```
385
+ Jscall.debug = 1
386
+ ```
387
+
227
388
  ## Installation
228
389
 
229
390
  Add this line to your application's Gemfile:
@@ -2,24 +2,27 @@
2
2
 
3
3
  export class HttpStream {
4
4
  constructor() {
5
- this.send_buffer = ['start']
5
+ this.recv_buffer = []
6
6
  this.send_callback = null
7
+ this.fetching = null
8
+ this.call_queue = []
9
+ this.puts('start')
7
10
  }
8
11
 
9
12
  [Symbol.asyncIterator]() {
10
13
  const http_stream = this
11
14
  return {
12
15
  next() {
13
- let msg = http_stream.send_buffer.shift()
14
- if (msg === undefined)
16
+ let next_data = http_stream.recv_buffer.shift()
17
+ if (next_data === undefined)
15
18
  return new Promise((resolve, reject) => {
16
19
  if (http_stream.send_callback === null)
17
20
  http_stream.send_callback = resolve
18
21
  else
19
22
  throw new Error('(fatal) send_callback is not null!')
20
- }).then(() => this.next())
23
+ })
21
24
  else
22
- return http_stream.do_fetch(msg)
25
+ return next_data
23
26
  }
24
27
  }
25
28
  }
@@ -40,12 +43,30 @@ export class HttpStream {
40
43
  }
41
44
 
42
45
  puts(msg) {
43
- this.send_buffer.push(msg)
44
- if (this.send_callback !== null) {
45
- const callback = this.send_callback
46
- this.send_callback = null
47
- callback()
48
- }
46
+ if (this.fetching === null)
47
+ this.fetching = this.puts0(msg)
48
+ else
49
+ this.call_queue.push(msg)
50
+ }
51
+
52
+ puts0(msg) {
53
+ return this.do_fetch(msg)
54
+ .then((data) => {
55
+ if (this.send_callback === null)
56
+ this.recv_buffer.push(data)
57
+ else {
58
+ const callback = this.send_callback
59
+ this.send_callback = null
60
+ callback(data)
61
+ }
62
+
63
+ if (this.call_queue.length > 0) {
64
+ const msg2 = this.call_queue.shift()
65
+ this.fetching = this.puts0(msg2)
66
+ }
67
+ else
68
+ this.fetching = null
69
+ })
49
70
  }
50
71
 
51
72
  setEncoding(encoding) {}
@@ -64,4 +85,8 @@ export const Jscall = new class {
64
85
  link.href = file_name
65
86
  document.head.append(link)
66
87
  }
88
+
89
+ append_to_body(html_source) {
90
+ document.body.insertAdjacentHTML('beforeend', html_source)
91
+ }
67
92
  }
@@ -8,6 +8,7 @@ module Jscall
8
8
  WEBrick::HTTPUtils::DefaultMimeTypes['mjs'] ||= "application/javascript"
9
9
 
10
10
  class Dom
11
+ # see Jscall class in browser.mjs
11
12
  def method_missing(name, *args)
12
13
  Jscall.__getpipe__.funcall(nil, "Jscall.#{name}", args)
13
14
  end
@@ -23,13 +24,27 @@ module Jscall
23
24
  end
24
25
 
25
26
  class PipeToBrowser < PipeToJs
26
- def startJS(module_names, options)
27
- port = 10081
28
- port = options[:port] if options.is_a?(Hash) && options.has_key?(:port)
29
- @pipe = FetchServer.new(port)
27
+ def startJS(module_names, config)
28
+ urls = {}
29
+ module_names.each_index do |i|
30
+ mod = module_names[i]
31
+ urls[mod[2]] = mod
32
+ end
33
+ port = config[:port] || 10081
34
+ @pipe = FetchServer.new(port, urls)
30
35
  @pipe.open
31
36
  end
32
37
 
38
+ def setup(config)
39
+ if config[:browser]
40
+ module_names = config[:module_names] || []
41
+ module_names.each_index do |i|
42
+ mod = module_names[i]
43
+ Jscall.dyn_import(mod[2], mod[0])
44
+ end
45
+ end
46
+ end
47
+
33
48
  def close
34
49
  @pipe.shutdown
35
50
  sleep(0.5)
@@ -58,7 +73,8 @@ module Jscall
58
73
  @@run_cmd = name
59
74
  end
60
75
 
61
- def initialize(port)
76
+ def initialize(port, urls)
77
+ @url_map = urls
62
78
  @send_buffer = Thread::Queue.new
63
79
  @receive_buffer = Thread::Queue.new
64
80
  @server_running = false
@@ -96,10 +112,16 @@ module Jscall
96
112
  end
97
113
 
98
114
  def read_file(req, res)
99
- if req.path.start_with?('/jscall/')
115
+ path = req.path
116
+ if path.start_with?('/jscall/')
100
117
  root = "#{__dir__}/../"
101
118
  else
102
- root = @server.config[:DocumentRoot]
119
+ value = @url_map[path]
120
+ if value.nil?
121
+ root = @server.config[:DocumentRoot]
122
+ else
123
+ root = value[1]
124
+ end
103
125
  end
104
126
  WEBrick::HTTPServlet::FileHandler.new(@server, root).service(req, res)
105
127
  end
data/lib/jscall/main.mjs CHANGED
@@ -3,10 +3,16 @@
3
3
  const cmd_eval = 1
4
4
  const cmd_call = 2
5
5
  const cmd_reply = 3
6
+ const cmd_async_call = 4
7
+ const cmd_async_eval = 5
8
+ const cmd_retry = 6
9
+ const cmd_reject = 7
10
+
6
11
  const param_array = 0
7
12
  const param_object = 1
8
13
  const param_local_object = 2
9
14
  const param_error = 3
15
+ const param_hash = 4
10
16
 
11
17
  const table_size = 100
12
18
 
@@ -64,8 +70,9 @@ const exported = new class {
64
70
  }
65
71
  }
66
72
 
67
- const RemoteRef = class {
73
+ class RemoteRef extends Function {
68
74
  constructor(id) {
75
+ super()
69
76
  this.id = id
70
77
  }
71
78
  }
@@ -74,12 +81,20 @@ const remoteRefHandler = new class {
74
81
  get(obj, name) {
75
82
  if (name === '__self__')
76
83
  return obj
84
+ else if (name === 'then')
85
+ // to prevent the Promise from handling RemoteRefs as thenable
86
+ // e.g., `Jscall.exec("{ call: (x) => Promise.resolve(x) }").call(obj)' should return that obj itself
87
+ return undefined
77
88
  else
78
89
  return (...args) => {
79
90
  // this returns Promise
80
91
  return funcall_to_ruby(obj.id, name, args)
81
92
  }
82
93
  }
94
+ apply(obj, self, args) {
95
+ // this returns Promise
96
+ return funcall_to_ruby(obj.id, 'call', args)
97
+ }
83
98
  }
84
99
 
85
100
  const imported = new class {
@@ -94,7 +109,8 @@ const imported = new class {
94
109
  if (obj !== null && obj !== undefined)
95
110
  return obj
96
111
  else {
97
- const rref = new Proxy(new RemoteRef(index), remoteRefHandler)
112
+ const ref = new RemoteRef(index)
113
+ const rref = new Proxy(ref, remoteRefHandler)
98
114
  this.objects[index] = new WeakRef(rref)
99
115
  return rref
100
116
  }
@@ -104,14 +120,15 @@ const imported = new class {
104
120
 
105
121
  // collect reclaimed RemoteRef objects.
106
122
  dead_references() {
107
- if (this.canary === null || this.canary.deref() === undefined)
123
+ // In Safari, deref() may return null
124
+ if (this.canary === null || this.canary.deref() == undefined)
108
125
  this.canary = new WeakRef(new RemoteRef(-1))
109
126
  else
110
127
  return [] // the canary is alive, so no GC has happened yet.
111
128
 
112
129
  const deads = []
113
130
  this.objects.forEach((wref, index) => {
114
- if (wref !== null && wref !== undefined && wref.deref() === undefined) {
131
+ if (wref !== null && wref !== undefined && wref.deref() == undefined) {
115
132
  this.objects[index] = null
116
133
  deads.push(index)
117
134
  }
@@ -127,15 +144,21 @@ const encode_obj = obj => {
127
144
  return null
128
145
  else if (obj.constructor === Array)
129
146
  return [param_array, obj.map(e => encode_obj(e))]
147
+ else if (obj instanceof Map) {
148
+ const hash = {}
149
+ for (const [key, value] of obj.entries())
150
+ hash[key] = value
151
+ return [param_hash, hash]
152
+ }
130
153
  else if (obj instanceof RemoteRef)
131
154
  return [param_local_object, exported.export_remoteref(obj)]
132
155
  else
133
156
  return [param_object, exported.export(obj)]
134
157
  }
135
158
 
136
- const encode_error = msg => [param_error, msg]
159
+ const encode_error = msg => [param_error, msg.toString()]
137
160
 
138
- const RubyError = class {
161
+ class RubyError {
139
162
  constructor(msg) { this.message = msg }
140
163
  get() { return 'RubyError: ' + this.message }
141
164
  }
@@ -146,6 +169,12 @@ const decode_obj = obj => {
146
169
  else if (obj.constructor === Array && obj.length == 2)
147
170
  if (obj[0] == param_array)
148
171
  return obj[1].map(e => decode_obj(e))
172
+ else if (obj[0] == param_hash) {
173
+ const hash = {}
174
+ for (const [key, value] of Object.entries(obj[1]))
175
+ hash[key] = decode_obj(value)
176
+ return hash
177
+ }
149
178
  else if (obj[0] == param_object)
150
179
  return imported.import(obj[1])
151
180
  else if (obj[0] == param_local_object)
@@ -159,9 +188,9 @@ const decode_obj = obj => {
159
188
  const js_eval = eval
160
189
 
161
190
  const funcall_from_ruby = cmd => {
162
- const receiver = decode_obj(cmd[1])
163
- const name = cmd[2]
164
- const args = cmd[3].map(e => decode_obj(e))
191
+ const receiver = decode_obj(cmd[2])
192
+ const name = cmd[3]
193
+ const args = cmd[4].map(e => decode_obj(e))
165
194
  if (name.endsWith('=')) {
166
195
  const name2 = name.substring(0, name.length - 1)
167
196
  if (receiver === null)
@@ -189,34 +218,41 @@ const funcall_from_ruby = cmd => {
189
218
  return f // obtain a propety
190
219
  }
191
220
 
192
- throw `unknown function/method was called, ${name}`
221
+ throw `unknown JS function/method was called: ${name} on <${receiver}>`
193
222
  }
194
223
 
195
224
  let stdout_puts = console.log
225
+ let num_generated_ids = 0
196
226
 
197
- const reply = value => {
198
- if (value instanceof Promise)
199
- value.then(result => { reply(result) })
200
- .catch(err => reply_error(err))
201
- else {
202
- if (last_callback_stack_depth < callback_stack.length)
203
- throw 'Ruby code was called without await. Call Jscall.close for recovery'
227
+ const fresh_id = () => {
228
+ num_generated_ids += 1
229
+ return num_generated_ids
230
+ }
204
231
 
232
+ const reply = (message_id, value, sync_mode) => {
233
+ if (sync_mode && value instanceof Promise)
234
+ value.then(result => { reply(message_id, result, true) })
235
+ .catch(err => reply_error(message_id, err))
236
+ else {
205
237
  try {
206
- const cmd = reply_with_piggyback([cmd_reply, encode_obj(value)])
238
+ const cmd = reply_with_piggyback([cmd_reply, message_id, encode_obj(value)])
207
239
  const data = JSON.stringify(cmd)
208
240
  stdout_puts(data)
209
241
  } catch (e) {
210
- reply_error(e)
242
+ reply_error(message_id, e)
211
243
  }
212
244
  }
213
245
  }
214
246
 
215
- const reply_error = e => {
216
- const cmd = reply_with_piggyback([cmd_reply, encode_error(e)])
247
+ const reply_error = (message_id, e) => {
248
+ const cmd = reply_with_piggyback([cmd_reply, message_id, encode_error(e)])
217
249
  stdout_puts(JSON.stringify(cmd))
218
250
  }
219
251
 
252
+ const puts_retry_cmd = msg_id => {
253
+ stdout_puts(JSON.stringify([cmd_retry, msg_id, encode_obj(false)]))
254
+ }
255
+
220
256
  export const scavenge_references = () => {
221
257
  reply_counter = 200
222
258
  imported.kill_canary()
@@ -230,7 +266,7 @@ const reply_with_piggyback = cmd => {
230
266
  const dead_refs = imported.dead_references()
231
267
  if (dead_refs.length > 0) {
232
268
  const cmd2 = cmd.concat()
233
- cmd2[4] = dead_refs
269
+ cmd2[5] = dead_refs
234
270
  return cmd2
235
271
  }
236
272
  }
@@ -239,55 +275,140 @@ const reply_with_piggyback = cmd => {
239
275
  }
240
276
 
241
277
  const callback_stack = []
242
- let last_callback_stack_depth = 0
243
278
  let reply_counter = 0
244
279
 
245
280
  export const exec = src => {
246
281
  return new Promise((resolve, reject) => {
247
- const cmd = reply_with_piggyback([cmd_eval, src])
248
- callback_stack.push([resolve, reject])
282
+ const message_id = fresh_id()
283
+ const cmd = reply_with_piggyback([cmd_eval, message_id, src])
284
+ callback_stack.push([message_id, resolve, reject])
249
285
  stdout_puts(JSON.stringify(cmd))
250
286
  })
251
287
  }
252
288
 
253
289
  const funcall_to_ruby = (receiver_id, name, args) => {
254
290
  return new Promise((resolve, reject) => {
291
+ const message_id = fresh_id()
255
292
  const receiver = [param_local_object, receiver_id]
256
293
  const encoded_args = args.map(e => encode_obj(e))
257
- const cmd = reply_with_piggyback([cmd_call, receiver, name, encoded_args])
258
- callback_stack.push([resolve, reject])
294
+ const cmd = reply_with_piggyback([cmd_call, message_id, receiver, name, encoded_args])
295
+ callback_stack.push([message_id, resolve, reject])
259
296
  stdout_puts(JSON.stringify(cmd))
260
297
  })
261
298
  }
262
299
 
263
300
  const returned_from_callback = cmd => {
264
- const result = decode_obj(cmd[1])
265
- const callback = callback_stack.pop()
266
- if (result instanceof RubyError)
267
- callback[1](result.get())
268
- else
269
- callback[0](result)
301
+ const message_id = cmd[1]
302
+ const result = decode_obj(cmd[2])
303
+ for (let i = callback_stack.length - 1; i >= 0; i--) {
304
+ // check the most recent element first since callbacks are
305
+ // assumed to be synchronously executed
306
+ if (callback_stack[i][0] === message_id) {
307
+ const [[_, resolve, reject]] = callback_stack.splice(i, 1)
308
+ if (result instanceof RubyError)
309
+ reject(result.get())
310
+ else
311
+ resolve(result)
312
+ }
313
+ }
270
314
  }
271
315
 
272
- class JsonReader {
316
+ export class MessageReader {
317
+ static HeaderSize = 6
318
+
273
319
  constructor(stream) {
274
- this.stream = stream
275
- this.acc = ""
320
+ this.stream = stream
321
+ this.state = "header"
322
+ this.acc = ""
323
+ this.bodySize = 0
324
+ }
325
+
326
+ parseHeader(pos) {
327
+ // skip leading whitespace characters as a countermeasure against leftover "\n"
328
+ while (pos < this.acc.length && /\s/.test(this.acc[pos]))
329
+ pos++
330
+
331
+ if (this.acc.length >= MessageReader.HeaderSize) {
332
+ const start = pos
333
+ pos += MessageReader.HeaderSize
334
+ return [parseInt(this.acc.slice(start, pos), 16), pos]
335
+ }
336
+ else
337
+ return undefined
338
+ }
339
+
340
+ parseBody(pos) {
341
+ if (this.acc.length >= this.bodySize) {
342
+ const start = pos
343
+ pos += this.bodySize
344
+ return [this.acc.slice(start, pos), pos]
345
+ }
346
+ else
347
+ return undefined
348
+ }
349
+
350
+ consume(pos) {
351
+ if (pos > 0)
352
+ this.acc = this.acc.slice(pos)
276
353
  }
277
354
 
278
355
  async *[Symbol.asyncIterator]() {
279
- for await (let data of this.stream) {
356
+ for await (const data of this.stream) {
280
357
  this.acc += data
281
- try {
282
- yield JSON.parse(this.acc)
283
- this.acc = ""
284
- } catch {
285
- // keep data in this.acc
358
+ let pos = 0
359
+ while (true) {
360
+ const result = this.iteratorBody(pos)
361
+ if (result[0] === false)
362
+ break
363
+ else if (result[0] !== true)
364
+ yield result[0] // result[0] is a string
365
+
366
+ pos = result[1]
367
+ if (this.checkEmptiness(pos))
368
+ break
369
+ }
370
+ }
371
+ this.checkEOS()
372
+ }
373
+
374
+ iteratorBody(pos) {
375
+ if (this.state === "header") {
376
+ const header = this.parseHeader(pos)
377
+ if (header === undefined) {
378
+ this.consume(pos)
379
+ return [false, pos]
380
+ } else {
381
+ this.bodySize = header[0]
382
+ pos = header[1]
383
+ this.state = "body"
384
+ }
385
+ }
386
+ if (this.state === "body") {
387
+ const body = this.parseBody(pos)
388
+ if (body === undefined) {
389
+ this.consume(pos)
390
+ return [false, pos]
391
+ } else {
392
+ this.state = "header"
393
+ return body
286
394
  }
287
395
  }
288
- if (this.acc != "") {
289
- yield JSON.parse(this.acc)
396
+ return [true, pos]
397
+ }
398
+
399
+ checkEmptiness(pos) {
400
+ if (pos == this.acc.length || (pos == this.acc.length - 1
401
+ && this.acc[this.acc.length - 1] === "\n")) {
402
+ this.acc = ""
403
+ return true
290
404
  }
405
+ else
406
+ return false
407
+ }
408
+
409
+ checkEOS() {
410
+ if (this.acc.length > 0)
411
+ throw new Error("The pipe closed after receiving an incomplete message")
291
412
  }
292
413
  }
293
414
 
@@ -298,30 +419,41 @@ export const start = async (stdin, use_stdout) => {
298
419
  stdout_puts = (m) => stdin.puts(m) // on browser
299
420
 
300
421
  stdin.setEncoding('utf8')
301
- for await (const cmd of new JsonReader(stdin)) {
422
+ for await (const json_data of new MessageReader(stdin)) {
423
+ let cmd
302
424
  try {
303
- last_callback_stack_depth = callback_stack.length
425
+ cmd = JSON.parse(json_data)
304
426
 
305
427
  // scavenge remote references
306
- if (cmd.length > 4)
307
- cmd[4].forEach(i => exported.remove(i))
428
+ if (cmd.length > 5)
429
+ cmd[5].forEach(i => exported.remove(i))
308
430
 
309
431
  if (cmd[0] == cmd_eval) {
310
- const result = js_eval(cmd[1])
311
- reply(result)
432
+ const result = js_eval(cmd[2])
433
+ reply(cmd[1], result, true)
312
434
  }
313
435
  else if (cmd[0] == cmd_call) {
314
436
  const result = funcall_from_ruby(cmd)
315
- reply(result)
437
+ reply(cmd[1], result, true)
316
438
  }
317
439
  else if (cmd[0] == cmd_reply)
318
440
  returned_from_callback(cmd)
319
- else
320
- reply_error('invalid command')
441
+ else if (cmd[0] == cmd_async_call) {
442
+ const result = funcall_from_ruby(cmd)
443
+ reply(cmd[1], result, false)
444
+ }
445
+ else if (cmd[0] == cmd_async_eval) {
446
+ const result = js_eval(cmd[2])
447
+ reply(cmd[1], result, false)
448
+ }
449
+ else if (cmd[0] == cmd_reject)
450
+ puts_retry_cmd(cmd[1])
451
+ else // cmd_retry and other unknown commands
452
+ reply_error(cmd[1], `invalid command ${cmd[0]}`)
321
453
  } catch (error) {
322
454
  const msg = typeof error === 'string' ? error : error.toString() +
323
455
  '\n ---\n' + error.stack
324
- reply_error(msg)
456
+ reply_error(cmd[1], msg)
325
457
  }
326
458
  }
327
459
  }
@@ -3,5 +3,5 @@
3
3
  # Copyright (C) 2022- Shigeru Chiba. All rights reserved.
4
4
 
5
5
  module Jscall
6
- VERSION = "1.0.1"
6
+ VERSION = "1.1.0"
7
7
  end
data/lib/jscall.rb CHANGED
@@ -9,6 +9,16 @@ require_relative "jscall/version"
9
9
  require_relative "jscall/browser"
10
10
 
11
11
  module Jscall
12
+ @@debug = 0
13
+
14
+ # Current debug level (>= 0)
15
+ def self.debug() @@debug end
16
+
17
+ # Sets the current debug level.
18
+ def self.debug=(level)
19
+ @@debug = level
20
+ end
21
+
12
22
  TableSize = 100
13
23
 
14
24
  class JavaScriptError < RuntimeError
@@ -77,17 +87,50 @@ module Jscall
77
87
  end
78
88
 
79
89
  class RemoteRef
80
- attr_reader :id
81
-
82
90
  def initialize(id)
83
91
  @id = id
84
92
  end
85
93
 
94
+ def __get_id
95
+ @id
96
+ end
97
+
98
+ def async
99
+ AsyncRemoteRef.new(@id)
100
+ end
101
+
102
+ # override Object#then
103
+ def then(*args)
104
+ send('then', *args)
105
+ end
106
+
107
+ # puts() calls this
108
+ def to_ary
109
+ ["#<RemoteRef @id=#{@id}>"]
110
+ end
111
+
112
+ # override Object#send
113
+ def send(name, *args)
114
+ Jscall.__getpipe__.funcall(self, name, args)
115
+ end
116
+
117
+ def async_send(name, *args)
118
+ Jscall.__getpipe__.async_funcall(self, name, args)
119
+ end
120
+
86
121
  def method_missing(name, *args)
87
122
  Jscall.__getpipe__.funcall(self, name, args)
88
123
  end
89
124
  end
90
125
 
126
+ class AsyncRemoteRef < RemoteRef
127
+ alias send async_send
128
+
129
+ def method_missing(name, *args)
130
+ Jscall.__getpipe__.async_funcall(self, name, args)
131
+ end
132
+ end
133
+
91
134
  class Imported # outbound references from Ruby to Node
92
135
  attr_reader :objects
93
136
 
@@ -137,10 +180,19 @@ module Jscall
137
180
  CMD_EVAL = 1
138
181
  CMD_CALL = 2
139
182
  CMD_REPLY = 3
183
+ CMD_ASYNC_CALL = 4
184
+ CMD_ASYNC_EVAL = 5
185
+ CMD_RETRY = 6
186
+ CMD_REJECT = 7
187
+
140
188
  Param_array = 0
141
189
  Param_object = 1
142
190
  Param_local_object = 2
143
191
  Param_error = 3
192
+ Param_hash = 4
193
+
194
+ Header_size = 6
195
+ Header_format = '%%0%dx' % Header_size
144
196
 
145
197
  @@node_cmd = 'node'
146
198
 
@@ -148,21 +200,35 @@ module Jscall
148
200
  @@node_cmd = cmd
149
201
  end
150
202
 
151
- # do import * as 'module_names[i][0]' from 'module_names[i][1]'
152
- #
153
- def initialize(module_names=[], options='')
154
- startJS(module_names, options)
203
+ def initialize(config)
155
204
  @exported = Exported.new
156
205
  @imported = Imported.new
157
206
  @send_counter = 0
207
+ @num_generated_ids = 0
208
+ @pending_replies = {}
209
+ module_names = config[:module_names] || []
210
+ startJS(module_names, config)
158
211
  end
159
212
 
160
- # module_names: an array of [module_name, module_file_name]
213
+ def setup(config)
214
+ # called just after executing new PipeToJs(config)
215
+ end
216
+
217
+ # Config options.
218
+ #
219
+ # module_names: an array of [module_name, module_root, module_file_name]
220
+ # For example,
221
+ # [['Foo', '/home/jscall', '/lib/foo.mjs']]
222
+ # this does
223
+ # import * as Foo from "/home/jscall/lib/foo.mjs"
224
+ #
225
+ # options: options passed to node.js
161
226
  #
162
- def startJS(module_names, options)
227
+ def startJS(module_names, config)
228
+ options = config[:options] || ''
163
229
  script2 = ''
164
230
  module_names.each_index do |i|
165
- script2 += "import * as m#{i + 2} from \"#{module_names[i][1]}\"; globalThis.#{module_names[i][0]} = m#{i + 2}; "
231
+ script2 += "import * as m#{i + 2} from \"#{module_names[i][1]}#{module_names[i][2]}\"; globalThis.#{module_names[i][0]} = m#{i + 2}; "
166
232
  end
167
233
  script2 += "import { createRequire } from \"node:module\"; globalThis.require = createRequire(\"file://#{Dir.pwd}/\");"
168
234
  script = "'import * as m1 from \"#{__dir__}/jscall/main.mjs\"; globalThis.Ruby = m1; #{script2}; Ruby.start(process.stdin, true)'"
@@ -184,8 +250,12 @@ module Jscall
184
250
  obj
185
251
  elsif obj.is_a?(Array)
186
252
  [Param_array, obj.map {|e| encode_obj(e)}]
253
+ elsif obj.is_a?(Hash)
254
+ hash2 = {}
255
+ obj.each {|key, value| hash2[key] = encode_obj(value) }
256
+ [Param_hash, hash2]
187
257
  elsif obj.is_a?(RemoteRef)
188
- [Param_local_object, obj.id]
258
+ [Param_local_object, obj.__get_id]
189
259
  else
190
260
  [Param_object, @exported.export(obj)]
191
261
  end
@@ -199,11 +269,15 @@ module Jscall
199
269
  if obj.is_a?(Numeric) || obj.is_a?(String) || obj == true || obj == false || obj == nil
200
270
  obj
201
271
  elsif obj.is_a?(Array) && obj.size == 2
202
- if (obj[0] == Param_array)
272
+ if obj[0] == Param_array
203
273
  obj[1].map {|e| decode_obj(e)}
204
- elsif (obj[0] == Param_object)
274
+ elsif obj[0] == Param_hash
275
+ hash = {}
276
+ obj[1].each {|key, value| hash[key] = decode_obj(value)}
277
+ hash
278
+ elsif obj[0] == Param_object
205
279
  @imported.import(obj[1])
206
- elsif (obj[0] == Param_local_object)
280
+ elsif obj[0] == Param_local_object
207
281
  @exported.find(obj[1])
208
282
  else # if Param_error
209
283
  JavaScriptError.new(obj[1])
@@ -213,20 +287,38 @@ module Jscall
213
287
  end
214
288
  end
215
289
 
290
+ def fresh_id
291
+ @num_generated_ids += 1
292
+ end
293
+
216
294
  def funcall(receiver, name, args)
217
- cmd = [CMD_CALL, encode_obj(receiver), name, args.map {|e| encode_obj(e)}]
295
+ cmd = [CMD_CALL, nil, encode_obj(receiver), name, args.map {|e| encode_obj(e)}]
296
+ send_command(cmd)
297
+ end
298
+
299
+ def async_funcall(receiver, name, args)
300
+ cmd = [CMD_ASYNC_CALL, nil, encode_obj(receiver), name, args.map {|e| encode_obj(e)}]
218
301
  send_command(cmd)
219
302
  end
220
303
 
221
304
  def exec(src)
222
- cmd = [CMD_EVAL, src]
305
+ cmd = [CMD_EVAL, nil, src]
306
+ send_command(cmd)
307
+ end
308
+
309
+ def async_exec(src)
310
+ cmd = [CMD_ASYNC_EVAL, nil, src]
223
311
  send_command(cmd)
224
312
  end
225
313
 
226
314
  def encode_eval_error(e)
227
315
  traces = e.backtrace
228
316
  location = if traces.size > 0 then traces[0] else '' end
229
- encode_error(location + ' ' + e.to_s)
317
+ if Jscall.debug > 0
318
+ encode_error("\n#{e.full_message}")
319
+ else
320
+ encode_error(location + ' ' + e.to_s)
321
+ end
230
322
  end
231
323
 
232
324
  def scavenge
@@ -243,7 +335,7 @@ module Jscall
243
335
  dead_refs = @imported.dead_references()
244
336
  if (dead_refs.length > 0)
245
337
  cmd2 = cmd.dup
246
- cmd2[4] = dead_refs
338
+ cmd2[5] = dead_refs
247
339
  return cmd2
248
340
  end
249
341
  end
@@ -251,55 +343,103 @@ module Jscall
251
343
  end
252
344
 
253
345
  def send_command(cmd)
346
+ message_id = (cmd[1] ||= fresh_id)
254
347
  json_data = JSON.generate(send_with_piggyback(cmd))
255
- @pipe.puts(json_data)
256
- reply_data = @pipe.gets
257
- reply = JSON.parse(reply_data || '[]')
258
- if reply.length > 4
259
- reply[4].each {|idx| @exported.remove(idx) }
348
+ header = (Header_format % json_data.length)
349
+ if header.length != Header_size
350
+ raise "message length limit exceeded"
260
351
  end
261
- if @pipe.closed?
262
- raise RuntimeError.new("connection closed: #{reply}")
263
- elsif reply[0] == CMD_REPLY
264
- result = decode_obj(reply[1])
265
- if result.is_a?(JavaScriptError)
266
- raise result
267
- else
268
- return result
269
- end
270
- elsif reply[0] == CMD_EVAL
271
- begin
272
- result = Object::TOPLEVEL_BINDING.eval(reply[1])
273
- encoded = encode_obj(result)
274
- rescue => e
275
- encoded = encode_eval_error(e)
352
+ json_data_with_header = header + json_data
353
+ @pipe.puts(json_data_with_header)
354
+
355
+ while true
356
+ reply_data = @pipe.gets
357
+ reply = JSON.parse(reply_data || '[]')
358
+ if reply.length > 5
359
+ reply[5].each {|idx| @exported.remove(idx) }
360
+ reply[5] = nil
276
361
  end
277
- send_command([CMD_REPLY, encoded])
278
- elsif reply[0] == CMD_CALL
279
- begin
280
- receiver = decode_obj(reply[1])
281
- name = reply[2]
282
- args = reply[3].map {|e| decode_obj(e)}
283
- result = receiver.public_send(name, *args)
284
- encoded = encode_obj(result)
285
- rescue => e
286
- encoded = encode_eval_error(e)
362
+ if @pipe.closed?
363
+ raise RuntimeError.new("connection closed: #{reply}")
364
+ elsif reply[0] == CMD_REPLY
365
+ result = decode_obj(reply[2])
366
+ if reply[1] != message_id
367
+ @pending_replies[reply[1]] = result
368
+ send_reply(reply[1], nil, false, CMD_REJECT)
369
+ elsif result.is_a?(JavaScriptError)
370
+ raise result
371
+ else
372
+ return result
373
+ end
374
+ elsif reply[0] == CMD_EVAL
375
+ begin
376
+ result = Object::TOPLEVEL_BINDING.eval(reply[2])
377
+ send_reply(reply[1], result)
378
+ rescue => e
379
+ send_error(reply[1], e)
380
+ end
381
+ elsif reply[0] == CMD_CALL
382
+ begin
383
+ receiver = decode_obj(reply[2])
384
+ name = reply[3]
385
+ args = reply[4].map {|e| decode_obj(e)}
386
+ result = receiver.public_send(name, *args)
387
+ send_reply(reply[1], result)
388
+ rescue => e
389
+ send_error(reply[1], e)
390
+ end
391
+ elsif reply[0] == CMD_RETRY
392
+ if reply[1] != message_id
393
+ send_reply(reply[1], nil, false, CMD_REJECT)
394
+ else
395
+ result = @pending_replies.delete(message_id)
396
+ if result.nil?
397
+ raise RuntimeError.new("bad CMD_RETRY: #{reply}")
398
+ elsif result.is_a?(JavaScriptError)
399
+ raise result
400
+ else
401
+ return result
402
+ end
403
+ end
404
+ else
405
+ # CMD_REJECT and other unknown commands
406
+ raise RuntimeError.new("bad message: #{reply}")
287
407
  end
288
- send_command([CMD_REPLY, encoded])
408
+ end
409
+ end
410
+
411
+ def send_reply(message_id, value, erroneous = false, cmd_id=CMD_REPLY)
412
+ if erroneous
413
+ encoded = encode_eval_error(value)
289
414
  else
290
- raise RuntimeError.new("bad reply: #{reply}")
415
+ encoded = encode_obj(value)
416
+ end
417
+ json_data = JSON.generate(send_with_piggyback([cmd_id, message_id, encoded]))
418
+ header = (Header_format % json_data.length)
419
+ if header.length != Header_size
420
+ raise "message length limit exceeded"
291
421
  end
422
+ json_data_with_header = header + json_data
423
+ @pipe.puts(json_data_with_header)
424
+ end
425
+
426
+ def send_error(message_id, e)
427
+ send_reply(message_id, e, true)
292
428
  end
293
429
  end
294
430
 
295
431
  @pipe = nil
296
- @module_names = []
297
- @options = ''
432
+ @configurations = {}
298
433
  @pipeToJsClass = PipeToJs
299
434
 
300
- def self.config(module_names: [], options: '', browser: false)
301
- @module_names = module_names
302
- @options = options
435
+ #def self.config(module_names: [], options: '', browser: false)
436
+ def self.config(**kw)
437
+ if kw.nil? || kw == {}
438
+ @configurations = {}
439
+ else
440
+ @configurations = @configurations.merge!(kw)
441
+ end
442
+ browser = @configurations[:browser]
303
443
  @pipeToJsClass = if browser then PipeToBrowser else PipeToJs end
304
444
  nil
305
445
  end
@@ -310,35 +450,65 @@ module Jscall
310
450
 
311
451
  Signal.trap(0) { self.close } # close before termination
312
452
 
313
- def self.exec(src)
314
- __getpipe__.exec(src)
315
- end
316
-
317
- def self.dyn_import(name, var_name=nil)
318
- __getpipe__.funcall(nil, 'Ruby.dyn_import', [name, var_name])
319
- end
320
-
321
- # name is a string object.
322
- # Evaluating this string in JavaScript results in a JavaScript function.
323
- #
324
- def self.funcall(name, *args)
325
- __getpipe__.funcall(nil, name, args)
326
- end
327
-
328
453
  # reclaim unused remote references.
329
454
  #
330
455
  def self.scavenge_references
331
456
  __getpipe__.scavenge
332
457
  end
333
458
 
334
- def self.method_missing(name, *args)
335
- __getpipe__.funcall(nil, name, args)
336
- end
337
-
338
459
  def self.__getpipe__
339
460
  if @pipe.nil?
340
- @pipe = @pipeToJsClass.new(@module_names, @options)
461
+ @pipe = @pipeToJsClass.new(@configurations)
462
+ @pipe.setup(@configurations)
341
463
  end
342
464
  @pipe
343
465
  end
466
+
467
+ module Interface
468
+ def exec(src)
469
+ __getpipe__.exec(src)
470
+ end
471
+
472
+ def async_exec(src)
473
+ __getpipe__.async_exec(src)
474
+ end
475
+
476
+ # name is a string object.
477
+ # Evaluating this string in JavaScript results in a JavaScript function.
478
+ #
479
+ def funcall(name, *args)
480
+ __getpipe__.funcall(nil, name, args)
481
+ end
482
+
483
+ def async_funcall(name, *args)
484
+ __getpipe__.async_funcall(nil, name, args)
485
+ end
486
+
487
+ def dyn_import(name, var_name=nil)
488
+ funcall('Ruby.dyn_import', name, var_name)
489
+ end
490
+
491
+ def method_missing(name, *args)
492
+ funcall(name, *args)
493
+ end
494
+ end
495
+
496
+ extend Interface
497
+
498
+ module AsyncInterface
499
+ include Interface
500
+
501
+ alias exec async_exec
502
+ alias funcall async_funcall
503
+ end
504
+
505
+ def self.async
506
+ @async ||= Class.new do
507
+ def __getpipe__
508
+ Jscall.__getpipe__
509
+ end
510
+
511
+ include AsyncInterface
512
+ end.new
513
+ end
344
514
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: jscall
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.1
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Shigeru Chiba
8
- autorequire:
8
+ autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2022-07-05 00:00:00.000000000 Z
11
+ date: 2022-08-11 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: webrick
@@ -50,7 +50,7 @@ licenses:
50
50
  metadata:
51
51
  homepage_uri: https://github.com/csg-tokyo/jscall
52
52
  source_code_uri: https://github.com/csg-tokyo/jscall
53
- post_install_message:
53
+ post_install_message:
54
54
  rdoc_options: []
55
55
  require_paths:
56
56
  - lib
@@ -65,8 +65,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
65
65
  - !ruby/object:Gem::Version
66
66
  version: '0'
67
67
  requirements: []
68
- rubygems_version: 3.1.6
69
- signing_key:
68
+ rubygems_version: 3.3.3
69
+ signing_key:
70
70
  specification_version: 4
71
71
  summary: a library for calling JavaScript functions
72
72
  test_files: []