jscall 1.1.0 → 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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a5fb1072aa12a5e884d9c92537f6ae7a5c632d4940715008865ec9760940f1c2
4
- data.tar.gz: 597c3199f5ee21f98d80a42b05bf9822b729a22311d7c9f43e2cf45d54594983
3
+ metadata.gz: dad6ca4017b93ba7f1675b3d1e3727a143d5e413f86d9f000966f7eae9306d06
4
+ data.tar.gz: e50ef13d47441f195194e4a36886b506266f87339f9d7bbf7f5b9d79dc500814
5
5
  SHA512:
6
- metadata.gz: 54c16c0015f2296309523ecf5d966bbf2071303136afef04e0bbdf139aa204c4b3db332a73ea65c0194fce7cea795503db17577805b4110a56edda796fca0a0b
7
- data.tar.gz: ada3a68d6c19093c22087982e520ed18a0f3f3709584fc2c30978aed7909c1b6b8568eb48e24fde6c57cc759dc235b1a824524b1c0d43053d4d80512767339a8
6
+ metadata.gz: 16a7885d6e0e5ef993a9f8ea43a6dc2bcfe78624f20cd10517642c00f59525b1edab1a91ab99367f962d3f63ba5a5c6705bf7cad915673613fb9ee805358baeb
7
+ data.tar.gz: 1f7d95224652882839fd9bce1d4fd539021980018ecc1cca73aed56e598a2c034b8800cdfb88cc6ffd4b09f6488867e33335e8180f7e5e81aa29663ebdc6a5d4
data/README.md CHANGED
@@ -18,7 +18,7 @@ Jscall.exec '1 + 1'
18
18
  ```
19
19
 
20
20
  This returns `2`. The argument passed to `Jscall.exec` can be
21
- multipe lines. It is executed as source code written in JavaScript.
21
+ multiple lines. It is executed as source code written in JavaScript.
22
22
 
23
23
  `Jscall.exec` returns a resulting value. Numbers, character strings (and symbols), boolean values, and `nil` (and `null`)
24
24
  are copied when passing between Ruby and JavaScript. An array is shallow-copied.
@@ -64,7 +64,7 @@ when this ruby object is passed to JavaScript as an argument,
64
64
  a normal object `{ a: 2, b: 3 }` is created as its copy in JavaScript
65
65
  and passed to a JavaScript method.
66
66
 
67
- To call a JavaScript function from Ruby, call a mehtod on `Jscall`.
67
+ To call a JavaScript function from Ruby, call a method on `Jscall`.
68
68
  For example,
69
69
 
70
70
  ```
@@ -90,7 +90,7 @@ Jscall.console.log('Hello')
90
90
  ```
91
91
 
92
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')`
93
+ reference to the value of `console` in JavaScript. Then, `.log('Hello')`
94
94
  calls the `log` method on `console` in JavaScript.
95
95
 
96
96
  When a Ruby object is passed to a JavaScript function/method,
@@ -110,7 +110,7 @@ created in Ruby.
110
110
  Note that you must `await` every call to Ruby object since it is
111
111
  asynchronous call.
112
112
 
113
- In JavaScript, `Ruby.exec` is availale to run a program in Ruby.
113
+ In JavaScript, `Ruby.exec` is available to run a program in Ruby.
114
114
  For example,
115
115
 
116
116
  ```
@@ -261,7 +261,7 @@ fs_module = await load('fs')
261
261
  ## Promise
262
262
 
263
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
264
+ it waits until the promise is fulfilled. Then Jscall passes
265
265
  the value of that promise from JavaScript to Ruby instead of that
266
266
  promise itself (or a remote reference to that promise). When that promise
267
267
  is rejected, an error object is passed to Ruby
@@ -270,7 +270,7 @@ This design reflects the fact that an `async` function in JavaScript
270
270
  also returns a `Promise` object but this object must not be returned
271
271
  to Ruby as is when that `async` function is called from Ruby.
272
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.
273
+ or its value must be passed to Ruby after the promise is fulfilled.
274
274
 
275
275
  When enforcing Jscall to pass a `Promise` object from JavaScript to Ruby,
276
276
  `.async` must be inserted between a receiver and a method name.
@@ -288,6 +288,27 @@ prom = obj.async.a # promise
288
288
  prom.then(->(r) { puts r }) # 7
289
289
  ```
290
290
 
291
+ ## Synchronous calls
292
+
293
+ You might want to avoid writing `await` when you call a method on a Ruby
294
+ object or you execute Ruby code by `Ruby.exec` from JavaScript.
295
+ For example, that call is included in library code and you might not
296
+ be able to modify the library code so that `await` will be inserted.
297
+
298
+ Jscall supports synchronous calls from JavaScript to Ruby only when
299
+ the underlying JavaScript engine is node.js on Linux.
300
+ In the mode of synchronous calls, you do not have to `await` a method call
301
+ on a Ruby object or a call to `Ruby.exec`.
302
+ It blocks until the return value comes back from Ruby.
303
+ While it blocks, all calls from Ruby to JavaScript are
304
+ synchronously processed.
305
+
306
+ To change to the mode of synchronous calls,
307
+ call `Jscall.config`:
308
+
309
+ ```
310
+ Jscall.config(sync: true)
311
+ ```
291
312
 
292
313
  ## Configuration
293
314
 
@@ -348,13 +369,27 @@ Passing `true` for `browser:` switches the execution engine to a web browser.
348
369
  The default engine is node.js.
349
370
  To switch the engine back to node.js, pass `false` for `browser:`.
350
371
  Call `Jscall.close` to detach the current execution engine.
351
- A new enigine with a new configuration will be created.
372
+ A new engine with a new configuration will be created.
352
373
 
353
374
  `port:` specifies the port number of an http server. It is optional.
354
375
  The example above specifies that Ruby receives http requests
355
376
  sent to http://localhost:10082 from JavaScript on a web browser.
356
377
 
357
378
 
379
+ ### Misc.
380
+
381
+ To change to the mode of synchronous calls,
382
+
383
+ ```
384
+ Jscall.config(sync: true)
385
+ ```
386
+
387
+ To set all the configurations to the default ones,
388
+
389
+ ```
390
+ Jscall.config()
391
+ ```
392
+
358
393
  ### Other configurations
359
394
 
360
395
  To change the name of the node command,
@@ -415,7 +450,7 @@ Bug reports and pull requests are welcome on GitHub at https://github.com/csg-to
415
450
 
416
451
  The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
417
452
 
418
- ## Acknowledgement
453
+ ## Acknowledgment
419
454
 
420
455
  The icon image for jscall was created by partly using the Ruby logo, which was obtained
421
456
  from https://www.ruby-lang.org/en/about/logo/ under CC BY-SA 2.5.
@@ -0,0 +1,56 @@
1
+ # Display a pdf file by using pdf.js
2
+
3
+ require 'jscall'
4
+
5
+ # Run a JavaScript program on a browser
6
+ Jscall.config browser: true
7
+
8
+ # Write HTML code on a blank web page.
9
+ Jscall.dom.append_to_body(<<CODE)
10
+ <h1>PDF.js 'Hello, world!' example</h1>
11
+ <canvas id="the-canvas"></canvas>
12
+ CODE
13
+
14
+ # import pdf.js
15
+ pdfjs = Jscall.dyn_import('https://mozilla.github.io/pdf.js/build/pdf.js')
16
+
17
+ # A pdf file
18
+ url = "https://raw.githubusercontent.com/mozilla/pdf.js/ba2edeae/examples/learning/helloworld.pdf"
19
+
20
+ pdf = Jscall.exec 'window["pdfjs-dist/build/pdf"]'
21
+
22
+ pdf.GlobalWorkerOptions.workerSrc = "https://mozilla.github.io/pdf.js/build/pdf.worker.js"
23
+
24
+ loadingTask = pdf.getDocument(url)
25
+ loadingTask.async.promise.then(-> (pdf) {
26
+ puts "PDF loaded"
27
+
28
+ # Fetch the first page
29
+ pageNumber = 1;
30
+ pdf.async.getPage(pageNumber).then(-> (page) {
31
+ puts "Page loaded"
32
+
33
+ scale = 1.5;
34
+ viewport = page.getViewport({ scale: scale })
35
+
36
+ canvas = Jscall.document.getElementById("the-canvas")
37
+ context = canvas.getContext("2d")
38
+ canvas.height = viewport.height
39
+ canvas.width = viewport.width
40
+
41
+ # Render the pdf page
42
+ renderContext = {
43
+ canvasContext: context,
44
+ viewport: viewport,
45
+ }
46
+ renderTask = page.render(renderContext)
47
+ renderTask.async.promise.then(-> (r) {
48
+ # Print a message when the rendering succeeds.
49
+ puts "Page rendered #{r}"
50
+ })
51
+ })
52
+ },
53
+ -> (reason) {
54
+ # an error occurs.
55
+ puts reason
56
+ })
data/lib/jscall/main.mjs CHANGED
@@ -1,12 +1,12 @@
1
1
  // Copyright (C) 2022- Shigeru Chiba. All rights reserved.
2
2
 
3
- const cmd_eval = 1
4
- const cmd_call = 2
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
3
+ export const cmd_eval = 1
4
+ export const cmd_call = 2
5
+ export const cmd_reply = 3
6
+ export const cmd_async_call = 4
7
+ export const cmd_async_eval = 5
8
+ export const cmd_retry = 6
9
+ export const cmd_reject = 7
10
10
 
11
11
  const param_array = 0
12
12
  const param_object = 1
@@ -185,9 +185,17 @@ const decode_obj = obj => {
185
185
  throw `decode_obj: unsupported value, ${obj}`
186
186
  }
187
187
 
188
+ export const decode_obj_or_error = obj => {
189
+ const result = decode_obj(obj)
190
+ if (result instanceof RubyError)
191
+ return result.get()
192
+ else
193
+ return result
194
+ }
195
+
188
196
  const js_eval = eval
189
197
 
190
- const funcall_from_ruby = cmd => {
198
+ export const funcall_from_ruby = cmd => {
191
199
  const receiver = decode_obj(cmd[2])
192
200
  const name = cmd[3]
193
201
  const args = cmd[4].map(e => decode_obj(e))
@@ -221,7 +229,7 @@ const funcall_from_ruby = cmd => {
221
229
  throw `unknown JS function/method was called: ${name} on <${receiver}>`
222
230
  }
223
231
 
224
- let stdout_puts = console.log
232
+ export let stdout_puts = console.log
225
233
  let num_generated_ids = 0
226
234
 
227
235
  const fresh_id = () => {
@@ -229,7 +237,7 @@ const fresh_id = () => {
229
237
  return num_generated_ids
230
238
  }
231
239
 
232
- const reply = (message_id, value, sync_mode) => {
240
+ export const reply = (message_id, value, sync_mode) => {
233
241
  if (sync_mode && value instanceof Promise)
234
242
  value.then(result => { reply(message_id, result, true) })
235
243
  .catch(err => reply_error(message_id, err))
@@ -244,7 +252,7 @@ const reply = (message_id, value, sync_mode) => {
244
252
  }
245
253
  }
246
254
 
247
- const reply_error = (message_id, e) => {
255
+ export const reply_error = (message_id, e) => {
248
256
  const cmd = reply_with_piggyback([cmd_reply, message_id, encode_error(e)])
249
257
  stdout_puts(JSON.stringify(cmd))
250
258
  }
@@ -279,24 +287,36 @@ let reply_counter = 0
279
287
 
280
288
  export const exec = src => {
281
289
  return new Promise((resolve, reject) => {
282
- const message_id = fresh_id()
283
- const cmd = reply_with_piggyback([cmd_eval, message_id, src])
290
+ const cmd = make_cmd_eval(src)
291
+ const message_id = cmd[1]
284
292
  callback_stack.push([message_id, resolve, reject])
285
293
  stdout_puts(JSON.stringify(cmd))
286
294
  })
287
295
  }
288
296
 
289
- const funcall_to_ruby = (receiver_id, name, args) => {
297
+ export const make_cmd_eval = src => {
298
+ const message_id = fresh_id()
299
+ return reply_with_piggyback([cmd_eval, message_id, src])
300
+ }
301
+
302
+ let funcall_to_ruby = (receiver_id, name, args) => {
290
303
  return new Promise((resolve, reject) => {
291
- const message_id = fresh_id()
292
- const receiver = [param_local_object, receiver_id]
293
- const encoded_args = args.map(e => encode_obj(e))
294
- const cmd = reply_with_piggyback([cmd_call, message_id, receiver, name, encoded_args])
304
+ const cmd = make_cmd_call(receiver_id, name, args)
305
+ const message_id = cmd[1]
295
306
  callback_stack.push([message_id, resolve, reject])
296
307
  stdout_puts(JSON.stringify(cmd))
297
308
  })
298
309
  }
299
310
 
311
+ export const set_funcall_to_ruby = f => { funcall_to_ruby = f }
312
+
313
+ export const make_cmd_call = (receiver_id, name, args) => {
314
+ const message_id = fresh_id()
315
+ const receiver = [param_local_object, receiver_id]
316
+ const encoded_args = args.map(e => encode_obj(e))
317
+ return reply_with_piggyback([cmd_call, message_id, receiver, name, encoded_args])
318
+ }
319
+
300
320
  const returned_from_callback = cmd => {
301
321
  const message_id = cmd[1]
302
322
  const result = decode_obj(cmd[2])
@@ -412,6 +432,10 @@ export class MessageReader {
412
432
  }
413
433
  }
414
434
 
435
+ let make_message_reader = (stdin) => new MessageReader(stdin)
436
+
437
+ export const set_make_message_reader = (f) => { make_message_reader = f }
438
+
415
439
  export const start = async (stdin, use_stdout) => {
416
440
  if (use_stdout)
417
441
  console.log = console.error // on node.js
@@ -419,7 +443,7 @@ export const start = async (stdin, use_stdout) => {
419
443
  stdout_puts = (m) => stdin.puts(m) // on browser
420
444
 
421
445
  stdin.setEncoding('utf8')
422
- for await (const json_data of new MessageReader(stdin)) {
446
+ for await (const json_data of make_message_reader(stdin)) {
423
447
  let cmd
424
448
  try {
425
449
  cmd = JSON.parse(json_data)
@@ -0,0 +1,185 @@
1
+ // Copyright (C) 2022- Shigeru Chiba. All rights reserved.
2
+ // This works only with node.js on Linux
3
+
4
+ import * as main from './main.mjs'
5
+ import { readSync, openSync } from 'fs'
6
+
7
+ class SynchronousStdin {
8
+ constructor() {
9
+ this.buf_size = 4096
10
+ this.buffer = Buffer.alloc(this.buf_size);
11
+ this.stdin = openSync('/dev/stdin', 'rs')
12
+ }
13
+
14
+ *[Symbol.iterator]() {
15
+ let str
16
+ while ((str = this.readOne()) !== null)
17
+ yield str
18
+ }
19
+
20
+ readOne() {
21
+ while (true) {
22
+ try {
23
+ const nbytes = readSync(this.stdin, this.buffer, 0, this.buf_size)
24
+ if (nbytes > 0)
25
+ return this.buffer.toString('utf-8', 0, nbytes)
26
+ else
27
+ return null // maybe EOF on macOS
28
+ }
29
+ catch (e) {
30
+ if (e.code === 'EOF')
31
+ return null
32
+ else if (e.code !== 'EAGAIN')
33
+ throw e
34
+ }
35
+ }
36
+ }
37
+ }
38
+
39
+ class SyncMessageReader extends main.MessageReader {
40
+ constructor(stream) {
41
+ super(stream)
42
+ this.stdin = new SynchronousStdin()
43
+ this.generator = null
44
+ }
45
+
46
+ gets_function() {
47
+ const iterator = this[Symbol.iterator]()
48
+ return () => {
49
+ const v = iterator.next()
50
+ if (v.done)
51
+ return null
52
+ else
53
+ return v.value
54
+ }
55
+ }
56
+
57
+ async *[Symbol.asyncIterator]() {
58
+ if (this.generator !== null) {
59
+ while (true) {
60
+ const v = this.generator.next()
61
+ if (v.done)
62
+ break
63
+ else
64
+ yield v.value
65
+ }
66
+ }
67
+ for await (const data of this.stream) {
68
+ this.acc += data
69
+ this.generator = this.generatorBody()
70
+ while (true) {
71
+ const v = this.generator.next()
72
+ if (v.done)
73
+ break
74
+ else
75
+ yield v.value
76
+ }
77
+ }
78
+ this.checkEOS()
79
+ }
80
+
81
+ *[Symbol.iterator]() {
82
+ if (this.generator !== null) {
83
+ while (true) {
84
+ const v = this.generator.next()
85
+ if (v.done)
86
+ break
87
+ else
88
+ yield v.value
89
+ }
90
+ }
91
+ for (const data of this.stdin) {
92
+ this.acc += data
93
+ this.generator = this.generatorBody()
94
+ while (true) {
95
+ const v = this.generator.next()
96
+ if (v.done)
97
+ break
98
+ else
99
+ yield v.value
100
+ }
101
+ }
102
+ this.checkEOS()
103
+ }
104
+
105
+ *generatorBody() {
106
+ let pos = 0
107
+ while (true) {
108
+ const result = this.iteratorBody(pos)
109
+ if (result[0] === false)
110
+ break
111
+ else if (result[0] !== true)
112
+ yield result[0] // result[0] is a string
113
+
114
+ pos = result[1]
115
+ if (this.checkEmptiness(pos))
116
+ break
117
+ }
118
+ }
119
+ }
120
+
121
+ export const start = main.start
122
+
123
+ let stdin_gets = null
124
+
125
+ main.set_make_message_reader((stdin) => {
126
+ const reader = new SyncMessageReader(stdin)
127
+ stdin_gets = reader.gets_function()
128
+ return reader
129
+ })
130
+
131
+ const js_eval = eval
132
+ const exported = main.get_exported_imported()[0]
133
+
134
+ const event_loop = () => {
135
+ let json_data
136
+ while ((json_data = stdin_gets()) !== null) {
137
+ let cmd
138
+ try {
139
+ cmd = JSON.parse(json_data)
140
+
141
+ // scavenge remote references
142
+ if (cmd.length > 5)
143
+ cmd[5].forEach(i => exported.remove(i))
144
+
145
+ if (cmd[0] == main.cmd_eval || cmd[0] == main.cmd_async_eval) {
146
+ const result = js_eval(cmd[2])
147
+ main.reply(cmd[1], result, false)
148
+ }
149
+ else if (cmd[0] == main.cmd_call || cmd[0] == main.cmd_async_call) {
150
+ const result = main.funcall_from_ruby(cmd)
151
+ main.reply(cmd[1], result, false)
152
+ }
153
+ else if (cmd[0] == main.cmd_reply)
154
+ return main.decode_obj_or_error(cmd[2])
155
+ else { // cmd_retry, cmd_reject, and other unknown commands
156
+ console.error(`*** node.js; bad message received: ${json_data}`)
157
+ break
158
+ }
159
+ } catch (error) {
160
+ const msg = typeof error === 'string' ? error : error.toString() +
161
+ '\n ---\n' + error.stack
162
+ main.reply_error(cmd[1], msg)
163
+ }
164
+ }
165
+ return undefined
166
+ }
167
+
168
+ export const exec = src => {
169
+ const cmd = main.make_cmd_eval(src)
170
+ main.stdout_puts(JSON.stringify(cmd))
171
+ return event_loop()
172
+ }
173
+
174
+ main.set_funcall_to_ruby((receiver_id, name, args) => {
175
+ const cmd = main.make_cmd_call(receiver_id, name, args)
176
+ main.stdout_puts(JSON.stringify(cmd))
177
+ return event_loop()
178
+ })
179
+
180
+ export const scavenge_references = main.scavenge_references
181
+
182
+ // for testing and debugging
183
+ export const get_exported_imported = main.get_exported_imported
184
+
185
+ export const dyn_import = main.dyn_import
@@ -3,5 +3,5 @@
3
3
  # Copyright (C) 2022- Shigeru Chiba. All rights reserved.
4
4
 
5
5
  module Jscall
6
- VERSION = "1.1.0"
6
+ VERSION = "1.2.0"
7
7
  end
data/lib/jscall.rb CHANGED
@@ -231,7 +231,8 @@ module Jscall
231
231
  script2 += "import * as m#{i + 2} from \"#{module_names[i][1]}#{module_names[i][2]}\"; globalThis.#{module_names[i][0]} = m#{i + 2}; "
232
232
  end
233
233
  script2 += "import { createRequire } from \"node:module\"; globalThis.require = createRequire(\"file://#{Dir.pwd}/\");"
234
- 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)'"
235
236
  @pipe = IO.popen("#{@@node_cmd} #{options} --input-type 'module' -e #{script}", "r+t")
236
237
  @pipe.autoclose = true
237
238
  end
@@ -432,7 +433,7 @@ module Jscall
432
433
  @configurations = {}
433
434
  @pipeToJsClass = PipeToJs
434
435
 
435
- #def self.config(module_names: [], options: '', browser: false)
436
+ #def self.config(module_names: [], options: '', browser: false, sync: false)
436
437
  def self.config(**kw)
437
438
  if kw.nil? || kw == {}
438
439
  @configurations = {}
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.1.0
4
+ version: 1.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Shigeru Chiba
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2022-08-11 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: