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