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,
sig/quickjs.rbs,
ext/quickjsrb/quickjsrb.c

Defined Under Namespace

Modules: PolyfillLoader, SubtleCrypto Classes: AggregateError, Blob, CryptoKey, EvalError, File, Function, InterruptedError, NoAwaitError, RangeError, ReferenceError, Runnable, RuntimeError, SyntaxError, TypeError, URIError, VM, Value

Constant Summary collapse

VERSION =

Returns:

  • (String)
"0.21.0"
MODULE_STD =

Returns:

  • (Symbol)
MODULE_OS =

Returns:

  • (Symbol)
FEATURE_TIMEOUT =

Returns:

  • (Symbol)
POLYFILL_FILE =

Returns:

  • (Symbol)
POLYFILL_HTML_BASE64 =

Returns:

  • (Symbol)
POLYFILL_ENCODING =

Returns:

  • (Symbol)
POLYFILL_URL =

Returns:

  • (Symbol)
POLYFILL_CRYPTO =

Returns:

  • (Symbol)

Class Method Summary collapse

Class Method Details

._apply_registered_polyfills(vm, features) ⇒ Object



38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
# File 'lib/quickjs/polyfills.rb', line 38

def self._apply_registered_polyfills(vm, features)
  features.each do |feature|
    next unless (entry = @_polyfills[feature])
    # The per-entry mutex makes first-use compilation happen exactly once
    # even when threads race to construct VMs with the same polyfill —
    # `||=` alone isn't atomic (`_precompile_polyfill` yields the GVL
    # inside `Quickjs.compile`), and a losing thread could otherwise
    # double-compile or read entry[:source] after the winner cleared it.
    # A compile failure leaves bytecode nil and source intact, so a later
    # attempt can retry. The unlocked first read keeps the post-compile
    # hot path (warmer pools constructing VMs concurrently) off the lock;
    # a stale nil just falls into the synchronize.
    bytecode = entry[:bytecode] || entry[:mutex].synchronize {
      entry[:bytecode] ||= begin
        compiled = _precompile_polyfill(entry, feature)
        # The Proc / String source isn't needed again once the bytecode
        # is cached — drop it so its captured scope can be GC'd.
        entry[:source] = nil
        entry[:init]   = nil
        compiled
      end
    }
    vm.send(:_load_polyfill_bytecode, bytecode)
  end
end

._build_import(imported) ⇒ Object



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

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



29
30
31
# File 'lib/quickjs/polyfills.rb', line 29

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 (e.g. the companion quickjs-polyfill-intl gem's FormatJS bundles run a couple 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).



71
72
73
74
75
76
# File 'lib/quickjs/polyfills.rb', line 71

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



33
34
35
36
# File 'lib/quickjs/polyfills.rb', line 33

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

._with_timeout(msec, proc, args) ⇒ Object



43
44
45
46
47
48
49
# File 'lib/quickjs.rb', line 43

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



52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
# File 'lib/quickjs.rb', line 52

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) ⇒ Quickjs::Runnable

Parameters:

  • source (String)
  • filename: (String)
  • (Object)

Returns:



33
34
35
36
37
38
39
40
# File 'lib/quickjs.rb', line 33

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

Parameters:

  • code (String)
  • overwrite_opts (Hash[Symbol, untyped]) (defaults to: {})

Returns:

  • (Object)


22
23
24
25
26
27
28
29
30
# File 'lib/quickjs.rb', line 22

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) ⇒ nil

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. The Proc must not itself construct a VM with the same feature: compilation is locked per entry, so that recursion raises ThreadError instead of deadlocking silently.

Re-registering a name replaces the entry wholesale, lock included: a VM construction already compiling under the old entry finishes against it (and loads the old bytecode) while the first construction under the new entry compiles the new source independently. Each registration compiles at most once; the two compiles can overlap.

Parameters:

  • name (Symbol)
  • source: (String, ^() -> String)
  • init: (String, nil) (defaults to: nil)

Returns:

  • (nil)

Raises:

  • (::TypeError)


20
21
22
23
24
25
26
27
# File 'lib/quickjs/polyfills.rb', line 20

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, mutex: Mutex.new}
  nil
end