jscall 1.0.1 → 1.2.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.
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,24 +200,39 @@ 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
- script = "'import * as m1 from \"#{__dir__}/jscall/main.mjs\"; globalThis.Ruby = m1; #{script2}; Ruby.start(process.stdin, true)'"
234
+ main_js_file = if config[:sync] then "synch.mjs" else "main.mjs" end
235
+ script = "'import * as m1 from \"#{__dir__}/jscall/#{main_js_file}\"; globalThis.Ruby = m1; #{script2}; Ruby.start(process.stdin, true)'"
169
236
  @pipe = IO.popen("#{@@node_cmd} #{options} --input-type 'module' -e #{script}", "r+t")
170
237
  @pipe.autoclose = true
171
238
  end
@@ -184,8 +251,12 @@ module Jscall
184
251
  obj
185
252
  elsif obj.is_a?(Array)
186
253
  [Param_array, obj.map {|e| encode_obj(e)}]
254
+ elsif obj.is_a?(Hash)
255
+ hash2 = {}
256
+ obj.each {|key, value| hash2[key] = encode_obj(value) }
257
+ [Param_hash, hash2]
187
258
  elsif obj.is_a?(RemoteRef)
188
- [Param_local_object, obj.id]
259
+ [Param_local_object, obj.__get_id]
189
260
  else
190
261
  [Param_object, @exported.export(obj)]
191
262
  end
@@ -199,11 +270,15 @@ module Jscall
199
270
  if obj.is_a?(Numeric) || obj.is_a?(String) || obj == true || obj == false || obj == nil
200
271
  obj
201
272
  elsif obj.is_a?(Array) && obj.size == 2
202
- if (obj[0] == Param_array)
273
+ if obj[0] == Param_array
203
274
  obj[1].map {|e| decode_obj(e)}
204
- elsif (obj[0] == Param_object)
275
+ elsif obj[0] == Param_hash
276
+ hash = {}
277
+ obj[1].each {|key, value| hash[key] = decode_obj(value)}
278
+ hash
279
+ elsif obj[0] == Param_object
205
280
  @imported.import(obj[1])
206
- elsif (obj[0] == Param_local_object)
281
+ elsif obj[0] == Param_local_object
207
282
  @exported.find(obj[1])
208
283
  else # if Param_error
209
284
  JavaScriptError.new(obj[1])
@@ -213,20 +288,38 @@ module Jscall
213
288
  end
214
289
  end
215
290
 
291
+ def fresh_id
292
+ @num_generated_ids += 1
293
+ end
294
+
216
295
  def funcall(receiver, name, args)
217
- cmd = [CMD_CALL, encode_obj(receiver), name, args.map {|e| encode_obj(e)}]
296
+ cmd = [CMD_CALL, nil, encode_obj(receiver), name, args.map {|e| encode_obj(e)}]
297
+ send_command(cmd)
298
+ end
299
+
300
+ def async_funcall(receiver, name, args)
301
+ cmd = [CMD_ASYNC_CALL, nil, encode_obj(receiver), name, args.map {|e| encode_obj(e)}]
218
302
  send_command(cmd)
219
303
  end
220
304
 
221
305
  def exec(src)
222
- cmd = [CMD_EVAL, src]
306
+ cmd = [CMD_EVAL, nil, src]
307
+ send_command(cmd)
308
+ end
309
+
310
+ def async_exec(src)
311
+ cmd = [CMD_ASYNC_EVAL, nil, src]
223
312
  send_command(cmd)
224
313
  end
225
314
 
226
315
  def encode_eval_error(e)
227
316
  traces = e.backtrace
228
317
  location = if traces.size > 0 then traces[0] else '' end
229
- encode_error(location + ' ' + e.to_s)
318
+ if Jscall.debug > 0
319
+ encode_error("\n#{e.full_message}")
320
+ else
321
+ encode_error(location + ' ' + e.to_s)
322
+ end
230
323
  end
231
324
 
232
325
  def scavenge
@@ -243,7 +336,7 @@ module Jscall
243
336
  dead_refs = @imported.dead_references()
244
337
  if (dead_refs.length > 0)
245
338
  cmd2 = cmd.dup
246
- cmd2[4] = dead_refs
339
+ cmd2[5] = dead_refs
247
340
  return cmd2
248
341
  end
249
342
  end
@@ -251,55 +344,103 @@ module Jscall
251
344
  end
252
345
 
253
346
  def send_command(cmd)
347
+ message_id = (cmd[1] ||= fresh_id)
254
348
  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) }
349
+ header = (Header_format % json_data.length)
350
+ if header.length != Header_size
351
+ raise "message length limit exceeded"
260
352
  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)
353
+ json_data_with_header = header + json_data
354
+ @pipe.puts(json_data_with_header)
355
+
356
+ while true
357
+ reply_data = @pipe.gets
358
+ reply = JSON.parse(reply_data || '[]')
359
+ if reply.length > 5
360
+ reply[5].each {|idx| @exported.remove(idx) }
361
+ reply[5] = nil
276
362
  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)
363
+ if @pipe.closed?
364
+ raise RuntimeError.new("connection closed: #{reply}")
365
+ elsif reply[0] == CMD_REPLY
366
+ result = decode_obj(reply[2])
367
+ if reply[1] != message_id
368
+ @pending_replies[reply[1]] = result
369
+ send_reply(reply[1], nil, false, CMD_REJECT)
370
+ elsif result.is_a?(JavaScriptError)
371
+ raise result
372
+ else
373
+ return result
374
+ end
375
+ elsif reply[0] == CMD_EVAL
376
+ begin
377
+ result = Object::TOPLEVEL_BINDING.eval(reply[2])
378
+ send_reply(reply[1], result)
379
+ rescue => e
380
+ send_error(reply[1], e)
381
+ end
382
+ elsif reply[0] == CMD_CALL
383
+ begin
384
+ receiver = decode_obj(reply[2])
385
+ name = reply[3]
386
+ args = reply[4].map {|e| decode_obj(e)}
387
+ result = receiver.public_send(name, *args)
388
+ send_reply(reply[1], result)
389
+ rescue => e
390
+ send_error(reply[1], e)
391
+ end
392
+ elsif reply[0] == CMD_RETRY
393
+ if reply[1] != message_id
394
+ send_reply(reply[1], nil, false, CMD_REJECT)
395
+ else
396
+ result = @pending_replies.delete(message_id)
397
+ if result.nil?
398
+ raise RuntimeError.new("bad CMD_RETRY: #{reply}")
399
+ elsif result.is_a?(JavaScriptError)
400
+ raise result
401
+ else
402
+ return result
403
+ end
404
+ end
405
+ else
406
+ # CMD_REJECT and other unknown commands
407
+ raise RuntimeError.new("bad message: #{reply}")
287
408
  end
288
- send_command([CMD_REPLY, encoded])
409
+ end
410
+ end
411
+
412
+ def send_reply(message_id, value, erroneous = false, cmd_id=CMD_REPLY)
413
+ if erroneous
414
+ encoded = encode_eval_error(value)
289
415
  else
290
- raise RuntimeError.new("bad reply: #{reply}")
416
+ encoded = encode_obj(value)
417
+ end
418
+ json_data = JSON.generate(send_with_piggyback([cmd_id, message_id, encoded]))
419
+ header = (Header_format % json_data.length)
420
+ if header.length != Header_size
421
+ raise "message length limit exceeded"
291
422
  end
423
+ json_data_with_header = header + json_data
424
+ @pipe.puts(json_data_with_header)
425
+ end
426
+
427
+ def send_error(message_id, e)
428
+ send_reply(message_id, e, true)
292
429
  end
293
430
  end
294
431
 
295
432
  @pipe = nil
296
- @module_names = []
297
- @options = ''
433
+ @configurations = {}
298
434
  @pipeToJsClass = PipeToJs
299
435
 
300
- def self.config(module_names: [], options: '', browser: false)
301
- @module_names = module_names
302
- @options = options
436
+ #def self.config(module_names: [], options: '', browser: false, sync: false)
437
+ def self.config(**kw)
438
+ if kw.nil? || kw == {}
439
+ @configurations = {}
440
+ else
441
+ @configurations = @configurations.merge!(kw)
442
+ end
443
+ browser = @configurations[:browser]
303
444
  @pipeToJsClass = if browser then PipeToBrowser else PipeToJs end
304
445
  nil
305
446
  end
@@ -310,35 +451,65 @@ module Jscall
310
451
 
311
452
  Signal.trap(0) { self.close } # close before termination
312
453
 
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
454
  # reclaim unused remote references.
329
455
  #
330
456
  def self.scavenge_references
331
457
  __getpipe__.scavenge
332
458
  end
333
459
 
334
- def self.method_missing(name, *args)
335
- __getpipe__.funcall(nil, name, args)
336
- end
337
-
338
460
  def self.__getpipe__
339
461
  if @pipe.nil?
340
- @pipe = @pipeToJsClass.new(@module_names, @options)
462
+ @pipe = @pipeToJsClass.new(@configurations)
463
+ @pipe.setup(@configurations)
341
464
  end
342
465
  @pipe
343
466
  end
467
+
468
+ module Interface
469
+ def exec(src)
470
+ __getpipe__.exec(src)
471
+ end
472
+
473
+ def async_exec(src)
474
+ __getpipe__.async_exec(src)
475
+ end
476
+
477
+ # name is a string object.
478
+ # Evaluating this string in JavaScript results in a JavaScript function.
479
+ #
480
+ def funcall(name, *args)
481
+ __getpipe__.funcall(nil, name, args)
482
+ end
483
+
484
+ def async_funcall(name, *args)
485
+ __getpipe__.async_funcall(nil, name, args)
486
+ end
487
+
488
+ def dyn_import(name, var_name=nil)
489
+ funcall('Ruby.dyn_import', name, var_name)
490
+ end
491
+
492
+ def method_missing(name, *args)
493
+ funcall(name, *args)
494
+ end
495
+ end
496
+
497
+ extend Interface
498
+
499
+ module AsyncInterface
500
+ include Interface
501
+
502
+ alias exec async_exec
503
+ alias funcall async_funcall
504
+ end
505
+
506
+ def self.async
507
+ @async ||= Class.new do
508
+ def __getpipe__
509
+ Jscall.__getpipe__
510
+ end
511
+
512
+ include AsyncInterface
513
+ end.new
514
+ end
344
515
  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.2.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-09-23 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: webrick
@@ -35,6 +35,7 @@ files:
35
35
  - LICENSE
36
36
  - README.md
37
37
  - Rakefile
38
+ - examples/pdf-js.rb
38
39
  - jscall.gemspec
39
40
  - lib/jscall.rb
40
41
  - lib/jscall/apple-touch-icon.png
@@ -43,6 +44,7 @@ files:
43
44
  - lib/jscall/favicon.ico
44
45
  - lib/jscall/jscall.html
45
46
  - lib/jscall/main.mjs
47
+ - lib/jscall/synch.mjs
46
48
  - lib/jscall/version.rb
47
49
  homepage: https://github.com/csg-tokyo/jscall
48
50
  licenses:
@@ -50,7 +52,7 @@ licenses:
50
52
  metadata:
51
53
  homepage_uri: https://github.com/csg-tokyo/jscall
52
54
  source_code_uri: https://github.com/csg-tokyo/jscall
53
- post_install_message:
55
+ post_install_message:
54
56
  rdoc_options: []
55
57
  require_paths:
56
58
  - lib
@@ -65,8 +67,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
65
67
  - !ruby/object:Gem::Version
66
68
  version: '0'
67
69
  requirements: []
68
- rubygems_version: 3.1.6
69
- signing_key:
70
+ rubygems_version: 3.3.3
71
+ signing_key:
70
72
  specification_version: 4
71
73
  summary: a library for calling JavaScript functions
72
74
  test_files: []