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 +4 -4
- data/README.md +186 -25
- data/lib/jscall/browser.mjs +36 -11
- data/lib/jscall/browser.rb +29 -7
- data/lib/jscall/main.mjs +186 -54
- data/lib/jscall/version.rb +1 -1
- data/lib/jscall.rb +244 -74
- metadata +6 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a5fb1072aa12a5e884d9c92537f6ae7a5c632d4940715008865ec9760940f1c2
|
4
|
+
data.tar.gz: 597c3199f5ee21f98d80a42b05bf9822b729a22311d7c9f43e2cf45d54594983
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 54c16c0015f2296309523ecf5d966bbf2071303136afef04e0bbdf139aa204c4b3db332a73ea65c0194fce7cea795503db17577805b4110a56edda796fca0a0b
|
7
|
+
data.tar.gz: ada3a68d6c19093c22087982e520ed18a0f3f3709584fc2c30978aed7909c1b6b8568eb48e24fde6c57cc759dc235b1a824524b1c0d43053d4d80512767339a8
|
data/README.md
CHANGED
@@ -1,5 +1,7 @@
|
|
1
1
|
# Jscall
|
2
2
|
|
3
|
+
[](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
|
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
|
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
|
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
|
-
|
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
|
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
|
-
|
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
|
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
|
-
|
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"]]
|
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
|
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
|
-
|
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
|
-
|
325
|
+
`<path>` must not start with `/jscall` or `/cmd`. They are reserved for
|
326
|
+
internal use.
|
189
327
|
|
190
|
-
|
328
|
+
### options:
|
329
|
+
|
330
|
+
To specify a command line argument passed to node.js,
|
191
331
|
|
192
332
|
```
|
193
|
-
Jscall
|
333
|
+
Jscall.config(options: '--use-strict')
|
194
334
|
```
|
195
335
|
|
196
|
-
|
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,
|
344
|
+
Jscall.config(browser: true, port: 10082)
|
202
345
|
```
|
203
346
|
|
204
|
-
`
|
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
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
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:
|
data/lib/jscall/browser.mjs
CHANGED
@@ -2,24 +2,27 @@
|
|
2
2
|
|
3
3
|
export class HttpStream {
|
4
4
|
constructor() {
|
5
|
-
this.
|
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
|
14
|
-
if (
|
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
|
-
})
|
23
|
+
})
|
21
24
|
else
|
22
|
-
return
|
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.
|
44
|
-
|
45
|
-
|
46
|
-
this.
|
47
|
-
|
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
|
}
|
data/lib/jscall/browser.rb
CHANGED
@@ -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,
|
27
|
-
|
28
|
-
|
29
|
-
|
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
|
-
|
115
|
+
path = req.path
|
116
|
+
if path.start_with?('/jscall/')
|
100
117
|
root = "#{__dir__}/../"
|
101
118
|
else
|
102
|
-
|
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
|
-
|
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
|
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
|
-
|
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()
|
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
|
-
|
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[
|
163
|
-
const name = cmd[
|
164
|
-
const args = cmd[
|
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
|
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
|
198
|
-
|
199
|
-
|
200
|
-
|
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[
|
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
|
248
|
-
|
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
|
265
|
-
const
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
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
|
316
|
+
export class MessageReader {
|
317
|
+
static HeaderSize = 6
|
318
|
+
|
273
319
|
constructor(stream) {
|
274
|
-
this.stream
|
275
|
-
this.
|
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 (
|
356
|
+
for await (const data of this.stream) {
|
280
357
|
this.acc += data
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
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
|
-
|
289
|
-
|
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
|
422
|
+
for await (const json_data of new MessageReader(stdin)) {
|
423
|
+
let cmd
|
302
424
|
try {
|
303
|
-
|
425
|
+
cmd = JSON.parse(json_data)
|
304
426
|
|
305
427
|
// scavenge remote references
|
306
|
-
if (cmd.length >
|
307
|
-
cmd[
|
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[
|
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
|
-
|
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
|
}
|
data/lib/jscall/version.rb
CHANGED
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
|
-
|
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
|
-
|
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,
|
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.
|
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
|
272
|
+
if obj[0] == Param_array
|
203
273
|
obj[1].map {|e| decode_obj(e)}
|
204
|
-
elsif
|
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
|
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
|
-
|
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[
|
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
|
-
|
256
|
-
|
257
|
-
|
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
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
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
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
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
|
-
|
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
|
-
|
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
|
-
@
|
297
|
-
@options = ''
|
432
|
+
@configurations = {}
|
298
433
|
@pipeToJsClass = PipeToJs
|
299
434
|
|
300
|
-
def self.config(module_names: [], options: '', browser: false)
|
301
|
-
|
302
|
-
|
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(@
|
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
|
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-
|
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.
|
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: []
|