Module: Quickjs

Defined in:
lib/quickjs.rb,
lib/quickjs/version.rb,
lib/quickjs/function.rb,
lib/quickjs/runnable.rb,
lib/quickjs/polyfills.rb,
lib/quickjs/crypto_key.rb,
lib/quickjs/subtle_crypto.rb,
ext/quickjsrb/quickjsrb.c

Defined Under Namespace

Modules: PolyfillLoader, SubtleCrypto Classes: Blob, CryptoKey, File, Function, Runnable, VM

Constant Summary collapse

VERSION =
"0.19.0.pre1"

Class Method Summary collapse

Class Method Details

._apply_registered_polyfills(vm, features) ⇒ Object



30
31
32
33
34
35
36
37
38
39
40
41
42
43
# File 'lib/quickjs/polyfills.rb', line 30

def self._apply_registered_polyfills(vm, features)
  features.each do |feature|
    next unless (entry = @_polyfills[feature])
    # `||=` isn't atomic — `_precompile_polyfill` releases the GVL inside
    # `Quickjs.compile`, so two threads racing to construct VMs with the
    # same polyfill can both see nil and both compile. The bytecode write
    # is harmless because both threads produce identical bytes; the only
    # observable cost is wasted compile work, and (for `source: Proc`) a
    # second Proc invocation — so register Procs that are safe to call
    # more than once.
    entry[:bytecode] ||= _precompile_polyfill(entry, feature)
    vm.send(:_load_polyfill_bytecode, entry[:bytecode])
  end
end

._build_import(imported) ⇒ Object



73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
# File 'lib/quickjs.rb', line 73

def _build_import(imported)
  code_define_global = ->(name) { "globalThis['#{name}'] = #{name};" }
  case imported
  in String if matched = imported.match(/\* as (.+)/)
    [imported, code_define_global.call(matched[1])]
  in String
    [imported, code_define_global.call(imported)]
  in [*all] if all.all? {|e| e.is_a? String }
    [
      imported.join(', ').yield_self{|s| '{ %s }' % s },
      imported.map(&code_define_global).join("\n")
    ]
  in { ** }
    imports, aliases = imported.to_a.map do |imp|
      ["#{imp[0]} as #{imp[1]}", imp[1].to_s]
    end.transpose

    [
      imports.join(', ').yield_self{|s| '{ %s }' % s },
      aliases.map(&code_define_global).join("\n")
    ]
  else
    raise 'Unsupported importing style'
  end
end

._polyfill_for(name) ⇒ Object



21
22
23
# File 'lib/quickjs/polyfills.rb', line 21

def self._polyfill_for(name)
  @_polyfills[name]
end

._precompile_polyfill(entry, feature) ⇒ Object

Compiled once per process per polyfill on a disposable VM whose generous timeout covers parsing multi-MB bundles (FormatJS Intl is ~2 MB). The user’s per-VM ‘timeout_msec` is for their own JS — it would otherwise interrupt our infrastructure on tight defaults. `features: []` skips applying any registered polyfills to the temp VM (no recursion / no wasted polyfill loads).



51
52
53
54
55
56
# File 'lib/quickjs/polyfills.rb', line 51

def self._precompile_polyfill(entry, feature)
  source = entry[:source]
  source = source.call if source.is_a?(Proc)
  combined = entry[:init] ? "#{entry[:init]}\n#{source}" : source
  Quickjs.compile(combined, filename: feature.to_s, timeout_msec: 60_000, features: []).to_s
end

._unregister_polyfill(name) ⇒ Object



25
26
27
28
# File 'lib/quickjs/polyfills.rb', line 25

def self._unregister_polyfill(name)
  @_polyfills.delete(name)
  nil
end

._with_timeout(msec, proc, args) ⇒ Object



46
47
48
49
50
51
52
# File 'lib/quickjs.rb', line 46

def _with_timeout(msec, proc, args)
  Timeout.timeout(msec / 1_000.0) { proc.call(*args) }
rescue Timeout::Error
  raise Quickjs::InterruptedError.new('Ruby runtime got timeout', nil)
rescue
  raise
end

._with_vm(on) ⇒ Object



55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
# File 'lib/quickjs.rb', line 55

def _with_vm(on)
  case on
  when Quickjs::VM
    yield on
  when nil
    vm = Quickjs::VM.new
    yield vm
  when Hash
    vm = Quickjs::VM.new(**on)
    yield vm
  else
    raise ArgumentError, 'on: must be a Quickjs::VM, a Hash of VM options, or nil'
  end
ensure
  vm&.dispose!
end

.compile(source, **opts) ⇒ Object



36
37
38
39
40
41
42
43
# File 'lib/quickjs.rb', line 36

def compile(source, **opts)
  compile_opts = {}
  compile_opts[:filename] = opts.delete(:filename) if opts.key?(:filename)
  vm = Quickjs::VM.new(**opts)
  vm.compile(source, **compile_opts)
ensure
  vm&.dispose!
end

.eval_code(code, overwrite_opts = {}) ⇒ Object



25
26
27
28
29
30
31
32
33
# File 'lib/quickjs.rb', line 25

def eval_code(code, overwrite_opts = {})
  eval_opts = {}
  eval_opts[:filename] = overwrite_opts.delete(:filename) if overwrite_opts.key?(:filename)
  eval_opts[:async] = overwrite_opts.delete(:async) if overwrite_opts.key?(:async)
  vm = Quickjs::VM.new(**overwrite_opts)
  vm.eval_code(code, **eval_opts)
ensure
  vm&.dispose!
end

.register_polyfill(name, source:, init: nil) ⇒ Object

‘source:` accepts either a `String` (eager) or a `Proc` returning one (lazy). The lazy form lets a companion gem call `register_polyfill` at require time without paying the file-read cost unless a VM actually opts into the feature.

Raises:

  • (::TypeError)


12
13
14
15
16
17
18
19
# File 'lib/quickjs/polyfills.rb', line 12

def self.register_polyfill(name, source:, init: nil)
  raise ::TypeError, "name must be a Symbol, got #{name.class}" unless name.is_a?(Symbol)
  raise ::TypeError, "source: must be a String or Proc, got #{source.class}" unless source.is_a?(String) || source.is_a?(Proc)
  raise ::TypeError, "init: must be a String or nil, got #{init.class}" unless init.nil? || init.is_a?(String)

  @_polyfills[name] = {source: source, init: init&.freeze, bytecode: nil}
  nil
end