jscall 1.0.1 → 1.1.0

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