Module: Cloudflare

Defined in:
lib/homura/runtime.rb,
lib/homura/runtime/ai.rb,
lib/homura/runtime/http.rb,
lib/homura/runtime/cache.rb,
lib/homura/runtime/email.rb,
lib/homura/runtime/queue.rb,
lib/homura/runtime/stream.rb,
lib/homura/runtime/multipart.rb,
lib/homura/runtime/scheduled.rb,
lib/homura/runtime/durable_object.rb

Overview

backtick_javascript: true

Phase 11A — multipart/form-data receive pipeline.

Why a bespoke parser instead of Rack::Multipart::Parser?

Rack’s parser does work on Opal in principle (strscan is in stdlib), but it relies on Tempfile — which is a stub on Workers since there is no writable filesystem. It also assumes the request body is a true binary ByteString, whereas on Workers we have to cross the JS/Ruby boundary and Opal Strings are JS Strings (UTF-16 code units). The correct way to pass bytes through that boundary is to encode each byte as a single ‘char code 0-255` latin1 character, then `unescape / String.fromCharCode.apply` back into a Uint8Array when we need raw bytes again (e.g. R2.put).

This module exposes:

Cloudflare::Multipart.parse(body_binstr, content_type)
  → Hash[String => Cloudflare::UploadedFile | String]

Cloudflare::UploadedFile
  #filename      — original filename from the Content-Disposition header
  #content_type  — part Content-Type (defaults to application/octet-stream)
  #name          — form field name
  #size          — byte length
  #bytes_binstr  — the latin1-encoded byte string (1 char = 1 byte)
  #to_uint8_array — JS Uint8Array suitable for fetch body / R2.put
  #read          — returns bytes_binstr, mirroring Tempfile#read
  #rewind        — no-op (content is fully in-memory, there is no
                    writable filesystem on Workers)

Also installs ‘Rack::Request#post?`-path hook: when the Sinatra route calls `params`, `Rack::Request#POST` delegates to `Cloudflare::Multipart.rack_params(env)` which parses once, caches on the env, and hydrates `params` with UploadedFile / String values.

Defined Under Namespace

Modules: AI, BindingHelpers, Bindings, DurableObject, HTTP, Multipart, QueueConsumer, Scheduled Classes: AIError, BinaryBody, BindingError, Cache, CacheError, D1Database, D1Error, D1Statement, DurableObjectError, DurableObjectId, DurableObjectNamespace, DurableObjectRequest, DurableObjectRequestContext, DurableObjectState, DurableObjectStorage, DurableObjectStub, Email, EmbeddedBinaryBody, HTTPError, HTTPResponse, KVError, KVNamespace, Queue, QueueBatch, QueueContext, QueueError, QueueMessage, R2Bucket, R2Error, RawResponse, SSEOut, SSEStream, ScheduledEvent, UploadedFile

Class Method Summary collapse

Class Method Details

.headers_to_js(headers, fallback = nil) ⇒ Object



675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
# File 'lib/homura/runtime.rb', line 675

def self.headers_to_js(headers, fallback = nil)
  return fallback || `({})` if headers.nil?
  return headers if `#{headers} != null && #{headers}.constructor === Object`

  js_headers = fallback || `({})`
  if headers.respond_to?(:each)
    headers.each do |key, value|
      ks = key.to_s
      vs = value.to_s
      `#{js_headers}[#{ks}] = #{vs}`
    end
  end

  js_headers
end

.js_object_to_hash(js_obj) ⇒ Object

Deep copy of a JS object’s own enumerable string keys into a Hash. Recursively converts nested plain objects so Opal code can call Ruby methods (e.g. #is_a?) on every value.



651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
# File 'lib/homura/runtime.rb', line 651

def self.js_object_to_hash(js_obj)
  h = {}
  return h if `#{js_obj} == null`
  keys = `Object.keys(#{js_obj})`
  len = `#{keys}.length`
  i = 0
  while i < len
    k = `#{keys}[#{i}]`
    v = `#{js_obj}[#{k}]`
    # Normalize bare JS null/undefined to Ruby nil before storing them.
    if `#{v} == null`
      v = nil
      # Recurse for nested plain objects (but not Arrays, Dates, etc.)
    elsif `typeof #{v} === 'object' && !Array.isArray(#{v}) && !(#{v} instanceof Date)`
      v = js_object_to_hash(v)
    end

    h[k] = v
    i += 1
  end

  h
end

.js_promise?(obj) ⇒ Boolean

Check whether the argument is a native JS Promise / thenable. Ruby’s ‘Object#then` (alias of `yield_self`) is a universal method since Ruby 2.6, so `obj.respond_to?(:then)` is always true and is useless as a Promise detector. We must check for a JS function at `.then` instead.

Returns:

  • (Boolean)


629
630
631
# File 'lib/homura/runtime.rb', line 629

def self.js_promise?(obj)
  `(#{obj} != null && typeof #{obj}.then === 'function' && typeof #{obj}.catch === 'function')`
end

.js_rows_to_ruby(js_rows) ⇒ Object

JS Array -> Ruby Array of Ruby Hashes (for D1 result.results).



634
635
636
637
638
639
640
641
642
643
644
645
646
# File 'lib/homura/runtime.rb', line 634

def self.js_rows_to_ruby(js_rows)
  out = []
  return out if `#{js_rows} == null`
  len = `#{js_rows}.length`
  i = 0
  while i < len
    js_row = `#{js_rows}[#{i}]`
    out << js_object_to_hash(js_row)
    i += 1
  end

  out
end

.js_to_ruby(js_val) ⇒ Object

Generic JS->Ruby for the common Workers AI response shape:

{ response: "...", usage: { prompt_tokens: ... } }

Recursively converts nested objects + arrays.



499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
# File 'lib/homura/runtime/ai.rb', line 499

def self.js_to_ruby(js_val)
  return nil if `#{js_val} == null`
  if `typeof #{js_val} === 'string' || typeof #{js_val} === 'number' || typeof #{js_val} === 'boolean'`
    return js_val
  end

  if `Array.isArray(#{js_val})`
    out = []
    len = `#{js_val}.length`
    i = 0
    while i < len
      out << js_to_ruby(`#{js_val}[#{i}]`)
      i += 1
    end

    return out
  end

  if `typeof #{js_val} === 'object'`
    h = {}
    keys = `Object.keys(#{js_val})`
    len = `#{keys}.length`
    i = 0
    while i < len
      k = `#{keys}[#{i}]`
      h[k] = js_to_ruby(`#{js_val}[#{k}]`)
      i += 1
    end

    return h
  end

  js_val
end