homura-runtime 0.3.6 → 0.3.8

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.
@@ -28,8 +28,8 @@
28
28
  # Note: Opal Strings are immutable (they map to JS Strings), so this file
29
29
  # uses reassignment (`@buffer = @buffer + str`) instead of `<<` mutation.
30
30
 
31
- require 'stringio'
32
- require 'await'
31
+ require "stringio"
32
+ require "await"
33
33
 
34
34
  # ---------------------------------------------------------------------------
35
35
  # 1. stdout / stderr → console.log / console.error
@@ -37,8 +37,8 @@ require 'await'
37
37
 
38
38
  class HomuraRuntimeIO
39
39
  def initialize(channel)
40
- @channel = channel # 'log' or 'error'
41
- @buffer = ''
40
+ @channel = channel # 'log' or 'error'
41
+ @buffer = ""
42
42
  end
43
43
 
44
44
  def write(*args)
@@ -54,7 +54,7 @@ class HomuraRuntimeIO
54
54
 
55
55
  def puts(*args)
56
56
  if args.empty?
57
- emit('')
57
+ emit("")
58
58
  return nil
59
59
  end
60
60
  args.each do |arg|
@@ -78,22 +78,31 @@ class HomuraRuntimeIO
78
78
  def flush
79
79
  return self if @buffer.empty?
80
80
  emit(@buffer)
81
- @buffer = ''
81
+ @buffer = ""
82
82
  self
83
83
  end
84
84
 
85
- def sync; true; end
86
- def sync=(_); end
87
- def tty?; false; end
88
- def isatty; false; end
89
- def closed?; false; end
85
+ def sync
86
+ true
87
+ end
88
+ def sync=(_)
89
+ end
90
+ def tty?
91
+ false
92
+ end
93
+ def isatty
94
+ false
95
+ end
96
+ def closed?
97
+ false
98
+ end
90
99
 
91
100
  private
92
101
 
93
102
  def flush_lines
94
103
  while (idx = @buffer.index("\n"))
95
104
  line = @buffer[0...idx]
96
- @buffer = @buffer[(idx + 1)..-1] || ''
105
+ @buffer = @buffer[(idx + 1)..-1] || ""
97
106
  emit(line)
98
107
  end
99
108
  end
@@ -105,10 +114,14 @@ class HomuraRuntimeIO
105
114
  end
106
115
  end
107
116
 
108
- $stdout = HomuraRuntimeIO.new('log')
109
- $stderr = HomuraRuntimeIO.new('error')
110
- Object.const_set(:STDOUT, $stdout) unless Object.const_defined?(:STDOUT) && STDOUT.is_a?(HomuraRuntimeIO)
111
- Object.const_set(:STDERR, $stderr) unless Object.const_defined?(:STDERR) && STDERR.is_a?(HomuraRuntimeIO)
117
+ $stdout = HomuraRuntimeIO.new("log")
118
+ $stderr = HomuraRuntimeIO.new("error")
119
+ unless Object.const_defined?(:STDOUT) && STDOUT.is_a?(HomuraRuntimeIO)
120
+ Object.const_set(:STDOUT, $stdout)
121
+ end
122
+ unless Object.const_defined?(:STDERR) && STDERR.is_a?(HomuraRuntimeIO)
123
+ Object.const_set(:STDERR, $stderr)
124
+ end
112
125
 
113
126
  # ---------------------------------------------------------------------------
114
127
  # 2. Rack::Handler::Homura
@@ -122,7 +135,7 @@ Object.const_set(:STDERR, $stderr) unless Object.const_defined?(:STDERR) && STDE
122
135
  module Rack
123
136
  module Handler
124
137
  module Homura
125
- EMPTY_STRING_IO = StringIO.new('').freeze
138
+ EMPTY_STRING_IO = StringIO.new("").freeze
126
139
 
127
140
  def self.run(app, **_options)
128
141
  @app = app
@@ -168,18 +181,21 @@ module Rack
168
181
  # `js_ctx` is the ExecutionContext, `body_text` is the pre-resolved
169
182
  # request body (the worker.mjs front awaits req.text() before
170
183
  # handing control to Ruby because Opal runs synchronously).
171
- def self.call(js_req, js_env, js_ctx, body_text = '')
184
+ def self.call(js_req, js_env, js_ctx, body_text = "")
172
185
  if @app.nil?
173
186
  if defined?(::Sinatra::Homura) &&
174
- ::Sinatra::Homura.respond_to?(:ensure_rack_app!)
187
+ ::Sinatra::Homura.respond_to?(:ensure_rack_app!)
175
188
  ::Sinatra::Homura.ensure_rack_app!
176
189
  end
177
- raise '`run app` was never called from user code, and no Sinatra app was discoverable (define `class App < Sinatra::Base` or use top-level classic Sinatra routes)' if @app.nil?
190
+ if @app.nil?
191
+ raise "`run app` was never called from user code, and no Sinatra app was discoverable (define `class App < Sinatra::Base` or use top-level classic Sinatra routes)"
192
+ end
178
193
  end
179
194
 
180
195
  env = build_rack_env(js_req, js_env, js_ctx, body_text)
181
196
  result = @app.call(env)
182
- result = result.__await__ if defined?(::Cloudflare) && ::Cloudflare.js_promise?(result)
197
+ result = result.__await__ if defined?(::Cloudflare) &&
198
+ ::Cloudflare.js_promise?(result)
183
199
 
184
200
  status, headers, body = result
185
201
  build_js_response(status, headers, body)
@@ -201,43 +217,50 @@ module Rack
201
217
 
202
218
  # Build a Rack-compliant env Hash from a Cloudflare Workers Request.
203
219
  # See https://github.com/rack/rack/blob/main/SPEC.rdoc for the contract.
204
- def build_rack_env(js_req, js_env, js_ctx, body_text = '')
205
- method = `#{js_req}.method`
220
+ def build_rack_env(js_req, js_env, js_ctx, body_text = "")
221
+ method = `#{js_req}.method`
206
222
  url_obj = `new URL(#{js_req}.url)`
207
- path = `#{url_obj}.pathname`
223
+ path = `#{url_obj}.pathname`
208
224
  # Phase 16 docs: Sinatra + Opal builds responses with String#<< and raises on PATH_INFO `/docs/`.
209
- path = '/docs' if path == '/docs/'
210
- raw_qs = `#{url_obj}.search` # includes leading '?' or empty string
211
- qs = raw_qs && raw_qs.length > 0 ? raw_qs[1..-1] : ''
212
- scheme = `#{url_obj}.protocol`.sub(/:\z/, '')
213
- host = `#{url_obj}.hostname`
214
- port = `#{url_obj}.port`
215
- port = (scheme == 'https' ? '443' : '80') if port.nil? || port.empty?
225
+ path = "/docs" if path == "/docs/"
226
+ raw_qs = `#{url_obj}.search` # includes leading '?' or empty string
227
+ qs = raw_qs && raw_qs.length > 0 ? raw_qs[1..-1] : ""
228
+ scheme = `#{url_obj}.protocol`.sub(/:\z/, "")
229
+ host = `#{url_obj}.hostname`
230
+ port = `#{url_obj}.port`
231
+ port = (scheme == "https" ? "443" : "80") if port.nil? || port.empty?
216
232
 
217
233
  env = {
218
- 'REQUEST_METHOD' => method,
219
- 'SCRIPT_NAME' => '',
220
- 'PATH_INFO' => path,
221
- 'QUERY_STRING' => qs,
222
- 'SERVER_NAME' => host,
223
- 'SERVER_PORT' => port,
224
- 'SERVER_PROTOCOL' => 'HTTP/1.1',
225
- 'HTTPS' => scheme == 'https' ? 'on' : 'off',
226
- 'rack.url_scheme' => scheme,
227
- 'rack.input' => body_text.nil? || body_text.empty? ? EMPTY_STRING_IO : StringIO.new(body_text),
228
- 'rack.errors' => $stderr,
229
- 'rack.multithread' => false,
230
- 'rack.multiprocess' => false,
231
- 'rack.run_once' => false,
232
- 'rack.hijack?' => false,
234
+ "REQUEST_METHOD" => method,
235
+ "SCRIPT_NAME" => "",
236
+ "PATH_INFO" => path,
237
+ "QUERY_STRING" => qs,
238
+ "SERVER_NAME" => host,
239
+ "SERVER_PORT" => port,
240
+ "SERVER_PROTOCOL" => "HTTP/1.1",
241
+ "HTTPS" => scheme == "https" ? "on" : "off",
242
+ "rack.url_scheme" => scheme,
243
+ "rack.input" =>
244
+ (
245
+ if body_text.nil? || body_text.empty?
246
+ EMPTY_STRING_IO
247
+ else
248
+ StringIO.new(body_text)
249
+ end
250
+ ),
251
+ "rack.errors" => $stderr,
252
+ "rack.multithread" => false,
253
+ "rack.multiprocess" => false,
254
+ "rack.run_once" => false,
255
+ "rack.hijack?" => false
233
256
  }
234
257
 
235
258
  copy_headers_into_env(js_req, env)
236
- env['HTTP_HOST'] = if port == (scheme == 'https' ? '443' : '80')
237
- host
238
- else
239
- "#{host}:#{port}"
240
- end
259
+ env["HTTP_HOST"] = if port == (scheme == "https" ? "443" : "80")
260
+ host
261
+ else
262
+ "#{host}:#{port}"
263
+ end
241
264
 
242
265
  Cloudflare::Bindings.attach!(env, js_env, js_ctx)
243
266
  end
@@ -280,11 +303,14 @@ module Rack
280
303
  # stub.fetch) wraps it in Cloudflare::RawResponse. We pass
281
304
  # the JS object through unchanged — any reconstruction
282
305
  # would strip runtime-only properties the client depends on.
306
+ first_body = (body.first if body.respond_to?(:first))
307
+ first_body_ruby =
308
+ !`(#{first_body} == null || #{first_body}.$$class == null)`
283
309
  raw = nil
284
310
  if body.is_a?(::Cloudflare::RawResponse)
285
311
  raw = body
286
- elsif body.respond_to?(:first) && body.first.is_a?(::Cloudflare::RawResponse)
287
- raw = body.first
312
+ elsif first_body_ruby && first_body.is_a?(::Cloudflare::RawResponse)
313
+ raw = first_body
288
314
  end
289
315
  if raw
290
316
  js_resp = raw.js_response
@@ -293,39 +319,62 @@ module Rack
293
319
 
294
320
  # Binary body fast-path: pass the JS ReadableStream directly
295
321
  # to Response without touching Opal's String encoding.
296
- if body.is_a?(::Cloudflare::BinaryBody) || (body.respond_to?(:first) && body.first.is_a?(::Cloudflare::BinaryBody))
297
- bin = body.is_a?(::Cloudflare::BinaryBody) ? body : body.first
322
+ if body.is_a?(::Cloudflare::BinaryBody) ||
323
+ (first_body_ruby && first_body.is_a?(::Cloudflare::BinaryBody))
324
+ bin = body.is_a?(::Cloudflare::BinaryBody) ? body : first_body
298
325
  js_stream = bin.stream
299
326
  ct = bin.content_type
300
327
  cc = bin.cache_control
301
328
  js_headers = `({})`
302
- headers.each { |k, v| ks = k.to_s; vs = v.to_s; `#{js_headers}[#{ks}] = #{vs}` }
329
+ headers.each do |k, v|
330
+ ks = k.to_s
331
+ vs = v.to_s
332
+ `#{js_headers}[#{ks}] = #{vs}`
333
+ end
303
334
  `#{js_headers}['content-type'] = #{ct}` if ct
304
335
  `#{js_headers}['cache-control'] = #{cc}` if cc
305
- return `new Response(#{js_stream}, { status: #{status.to_i}, headers: #{js_headers} })`
336
+ return(
337
+ `new Response(#{js_stream}, { status: #{status.to_i}, headers: #{js_headers} })`
338
+ )
306
339
  end
307
340
 
308
- if body.is_a?(::Cloudflare::EmbeddedBinaryBody) || (body.respond_to?(:first) && body.first.is_a?(::Cloudflare::EmbeddedBinaryBody))
309
- bin = body.is_a?(::Cloudflare::EmbeddedBinaryBody) ? body : body.first
341
+ if body.is_a?(::Cloudflare::EmbeddedBinaryBody) ||
342
+ (
343
+ first_body_ruby &&
344
+ first_body.is_a?(::Cloudflare::EmbeddedBinaryBody)
345
+ )
346
+ bin =
347
+ body.is_a?(::Cloudflare::EmbeddedBinaryBody) ? body : first_body
310
348
  js_stream = bin.stream
311
349
  ct = bin.content_type
312
350
  cc = bin.cache_control
313
351
  js_headers = `({})`
314
- headers.each { |k, v| ks = k.to_s; vs = v.to_s; `#{js_headers}[#{ks}] = #{vs}` }
352
+ headers.each do |k, v|
353
+ ks = k.to_s
354
+ vs = v.to_s
355
+ `#{js_headers}[#{ks}] = #{vs}`
356
+ end
315
357
  `#{js_headers}['content-type'] = #{ct}` if ct
316
358
  `#{js_headers}['cache-control'] = #{cc}` if cc
317
- return `new Response(#{js_stream}, { status: #{status.to_i}, headers: #{js_headers} })`
359
+ return(
360
+ `new Response(#{js_stream}, { status: #{status.to_i}, headers: #{js_headers} })`
361
+ )
318
362
  end
319
363
 
320
364
  # Phase 10 — Workers AI streaming: a Cloudflare::AI::Stream wraps
321
365
  # a JS ReadableStream<Uint8Array> emitting SSE-formatted bytes
322
366
  # ("data: {json}\n\n"). Pass it straight through so the client
323
367
  # receives the chunks as they arrive.
368
+ first_body = (body.first if body.respond_to?(:first))
369
+ first_body_ruby =
370
+ !`(#{first_body} == null || #{first_body}.$$class == null)`
371
+
324
372
  stream_obj = nil
325
373
  if body.respond_to?(:sse_stream?) && body.sse_stream?
326
374
  stream_obj = body
327
- elsif body.respond_to?(:first) && body.first.respond_to?(:sse_stream?) && body.first.sse_stream?
328
- stream_obj = body.first
375
+ elsif first_body_ruby && first_body.respond_to?(:sse_stream?) &&
376
+ first_body.sse_stream?
377
+ stream_obj = first_body
329
378
  end
330
379
  if stream_obj
331
380
  js_stream = stream_obj.js_stream
@@ -336,9 +385,17 @@ module Rack
336
385
  # dropped any `headers:` hash the caller passed to `sse`.
337
386
  # Merge the stream's own response_headers if it exposes them
338
387
  # (duck-type; non-SSE stream wrappers may not).
339
- headers.each { |k, v| ks = k.to_s; vs = v.to_s; `#{js_headers}[#{ks}] = #{vs}` }
388
+ headers.each do |k, v|
389
+ ks = k.to_s
390
+ vs = v.to_s
391
+ `#{js_headers}[#{ks}] = #{vs}`
392
+ end
340
393
  if stream_obj.respond_to?(:response_headers)
341
- stream_obj.response_headers.each { |k, v| ks = k.to_s; vs = v.to_s; `#{js_headers}[#{ks}] = #{vs}` }
394
+ stream_obj.response_headers.each do |k, v|
395
+ ks = k.to_s
396
+ vs = v.to_s
397
+ `#{js_headers}[#{ks}] = #{vs}`
398
+ end
342
399
  else
343
400
  # Legacy Cloudflare::AI::Stream (Phase 10.3) doesn't expose
344
401
  # response_headers; keep the hardcoded SSE defaults for
@@ -347,9 +404,20 @@ module Rack
347
404
  `#{js_headers}['cache-control'] = 'no-cache, no-transform'`
348
405
  `#{js_headers}['x-accel-buffering'] = 'no'`
349
406
  end
350
- return `new Response(#{js_stream}, { status: #{status.to_i}, headers: #{js_headers} })`
407
+ return(
408
+ `new Response(#{js_stream}, { status: #{status.to_i}, headers: #{js_headers} })`
409
+ )
351
410
  end
352
411
 
412
+ raw_response = nil
413
+ if body.respond_to?(:raw_response?) && body.raw_response?
414
+ raw_response = body
415
+ elsif first_body_ruby && first_body.respond_to?(:raw_response?) &&
416
+ first_body.raw_response?
417
+ raw_response = first_body
418
+ end
419
+ return raw_response.js_response if raw_response
420
+
353
421
  chunks = []
354
422
  if body.respond_to?(:each)
355
423
  body.each { |chunk| chunks << chunk }
@@ -357,6 +425,52 @@ module Rack
357
425
  chunks << body
358
426
  end
359
427
 
428
+ raw_chunk =
429
+ chunks.find do |chunk|
430
+ chunk_ruby = !`(#{chunk} == null || #{chunk}.$$class == null)`
431
+ chunk_ruby && chunk.respond_to?(:raw_response?) &&
432
+ chunk.raw_response?
433
+ end
434
+ return raw_chunk.js_response if raw_chunk
435
+
436
+ binary_chunk =
437
+ chunks.find do |chunk|
438
+ chunk_ruby = !`(#{chunk} == null || #{chunk}.$$class == null)`
439
+ (
440
+ chunk_ruby && chunk.respond_to?(:stream) &&
441
+ chunk.respond_to?(:content_type)
442
+ ) ||
443
+ `#{chunk} != null && #{chunk}.stream != null && #{chunk}.content_type != null`
444
+ end
445
+ if binary_chunk
446
+ binary_chunk_ruby =
447
+ !`(#{binary_chunk} == null || #{binary_chunk}.$$class == null)`
448
+ stream =
449
+ if binary_chunk_ruby && binary_chunk.respond_to?(:stream)
450
+ binary_chunk.stream
451
+ else
452
+ `#{binary_chunk}.stream`
453
+ end
454
+ content_type =
455
+ if binary_chunk_ruby && binary_chunk.respond_to?(:content_type)
456
+ binary_chunk.content_type
457
+ else
458
+ `#{binary_chunk}.content_type`
459
+ end
460
+ cache_control =
461
+ if binary_chunk_ruby && binary_chunk.respond_to?(:cache_control)
462
+ binary_chunk.cache_control
463
+ else
464
+ `#{binary_chunk}.cache_control`
465
+ end
466
+ body_headers = {}
467
+ body_headers["content-type"] = content_type if content_type
468
+ body_headers["cache-control"] = cache_control if cache_control
469
+ return(
470
+ `new Response(#{stream}, { status: #{status.to_i}, headers: #{Cloudflare.headers_to_js(body_headers)} })`
471
+ )
472
+ end
473
+
360
474
  # Build JS-side headers. Set-Cookie is the one HTTP response
361
475
  # header that legitimately repeats — Rack 3 surfaces it as an
362
476
  # Array of cookie strings (e.g. session middleware + auth
@@ -382,7 +496,8 @@ module Rack
382
496
  # Convert any `{ __multi__: true, values: [...] }` markers into
383
497
  # a real `Headers` object that Workers' `new Response(headers:)`
384
498
  # accepts. Single-valued headers stay as plain string values.
385
- js_headers = `(function(h) {
499
+ js_headers =
500
+ `(function(h) {
386
501
  var hasMulti = false;
387
502
  for (var key in h) {
388
503
  if (h[key] && typeof h[key] === 'object' && h[key].__multi__ === true) {
@@ -411,7 +526,8 @@ module Rack
411
526
  has_promise = false
412
527
  chunks.each do |c|
413
528
  `#{js_chunks}.push(#{c})`
414
- has_promise = true if `#{c} != null && typeof #{c}.then === 'function'`
529
+ has_promise =
530
+ true if `#{c} != null && typeof #{c}.then === 'function'`
415
531
  end
416
532
 
417
533
  if has_promise
@@ -429,7 +545,7 @@ module Rack
429
545
  # (`.webSocket`) that a reconstructed Response would lose.
430
546
  `Promise.all(#{js_chunks}).then(function(resolved) { var bodyToText = function(v) { if (v == null) { return ''; } if (Array.isArray(v)) { var joined = ''; for (var j = 0; j < v.length; j++) { joined += bodyToText(v[j]); } return joined; } if (typeof v === 'string') { return v; } if (v != null && v.$$is_string) { return v.toString(); } try { return JSON.stringify(v); } catch (e) { return String(v); } }; for (var i = 0; i < resolved.length; i++) { var r = resolved[i]; if (r != null && typeof r === 'object' && typeof r['$raw_response?'] === 'function' && typeof r['$js_response'] === 'function') { try { if (r['$raw_response?']()) { return r['$js_response'](); } } catch (_) {} } } for (var i = 0; i < resolved.length; i++) { var r = resolved[i]; if (r != null && r.stream != null && r.content_type != null) { var bh = {}; bh['content-type'] = r.content_type; if (r.cache_control) bh['cache-control'] = r.cache_control; return new Response(r.stream, { status: #{status_int}, headers: bh }); } } if (resolved.length === 1 && resolved[0] != null && Array.isArray(resolved[0]) && resolved[0].length >= 1 && typeof resolved[0][0] === 'number') { var ov = resolved[0]; var ovs = ov[0]|0; var ovh = #{Cloudflare}.$headers_to_js(nil, #{js_headers}); var ovb = ''; if (ov.length >= 3 && ov[1] != null) { ovh = #{Cloudflare}.$headers_to_js(ov[1], #{js_headers}); ovb = bodyToText(ov[2]); } else if (ov.length >= 2) { ovb = bodyToText(ov[ov.length - 1]); } return new Response(ovb, { status: ovs, headers: ovh }); } var parts = []; for (var i = 0; i < resolved.length; i++) { var r = resolved[i]; if (r == null) { parts.push(''); continue; } if (typeof r === 'string') { parts.push(r); continue; } if (r != null && r.$$is_string) { parts.push(r.toString()); continue; } try { parts.push(JSON.stringify(r)); } catch (e) { parts.push(String(r)); } } return new Response(parts.join(''), { status: #{status_int}, headers: #{js_headers} }); })`
431
547
  else
432
- body_str = ''
548
+ body_str = ""
433
549
  chunks.each { |c| body_str = body_str + c.to_s }
434
550
  `new Response(#{body_str}, { status: #{status_int}, headers: #{js_headers} })`
435
551
  end
@@ -487,9 +603,12 @@ module Cloudflare
487
603
  end
488
604
  end
489
605
 
490
- class D1Error < BindingError; end
491
- class KVError < BindingError; end
492
- class R2Error < BindingError; end
606
+ class D1Error < BindingError
607
+ end
608
+ class KVError < BindingError
609
+ end
610
+ class R2Error < BindingError
611
+ end
493
612
 
494
613
  # Check whether the argument is a native JS Promise / thenable.
495
614
  # Ruby's `Object#then` (alias of `yield_self`) is a universal method
@@ -521,7 +640,7 @@ module Cloudflare
521
640
  h = {}
522
641
  return h if `#{js_obj} == null`
523
642
  keys = `Object.keys(#{js_obj})`
524
- len = `#{keys}.length`
643
+ len = `#{keys}.length`
525
644
  i = 0
526
645
  while i < len
527
646
  k = `#{keys}[#{i}]`
@@ -529,7 +648,7 @@ module Cloudflare
529
648
  # Normalize bare JS null/undefined to Ruby nil before storing them.
530
649
  if `#{v} == null`
531
650
  v = nil
532
- # Recurse for nested plain objects (but not Arrays, Dates, etc.)
651
+ # Recurse for nested plain objects (but not Arrays, Dates, etc.)
533
652
  elsif `typeof #{v} === 'object' && !Array.isArray(#{v}) && !(#{v} instanceof Date)`
534
653
  v = js_object_to_hash(v)
535
654
  end
@@ -570,9 +689,11 @@ module Cloudflare
570
689
 
571
690
  # Rack body contract — yield nothing. The bytes never flow
572
691
  # through Ruby; the JS Response goes straight to the runtime.
573
- def each; end
692
+ def each
693
+ end
574
694
 
575
- def close; end
695
+ def close
696
+ end
576
697
 
577
698
  def raw_response?
578
699
  true
@@ -586,7 +707,11 @@ module Cloudflare
586
707
  class BinaryBody
587
708
  attr_reader :stream, :content_type, :cache_control
588
709
 
589
- def initialize(stream, content_type = 'application/octet-stream', cache_control = nil)
710
+ def initialize(
711
+ stream,
712
+ content_type = "application/octet-stream",
713
+ cache_control = nil
714
+ )
590
715
  @stream = stream
591
716
  @content_type = content_type
592
717
  @cache_control = cache_control
@@ -594,9 +719,11 @@ module Cloudflare
594
719
 
595
720
  # Rack body contract — yield nothing so Sinatra's content-length
596
721
  # calculation skips this body. The real bytes go through JS.
597
- def each; end
722
+ def each
723
+ end
598
724
 
599
- def close; end
725
+ def close
726
+ end
600
727
  end
601
728
 
602
729
  # EmbeddedBinaryBody carries a base64-encoded asset payload produced at
@@ -605,22 +732,34 @@ module Cloudflare
605
732
  class EmbeddedBinaryBody
606
733
  attr_reader :body_base64, :content_type, :cache_control
607
734
 
608
- def initialize(body_base64, content_type = 'application/octet-stream', cache_control = nil)
609
- @body_base64 = body_base64 || ''
735
+ def initialize(
736
+ body_base64,
737
+ content_type = "application/octet-stream",
738
+ cache_control = nil
739
+ )
740
+ @body_base64 = body_base64 || ""
610
741
  @content_type = content_type
611
742
  @cache_control = cache_control
612
743
  end
613
744
 
614
- def each; end
745
+ def each
746
+ end
615
747
 
616
- def close; end
748
+ def close
749
+ end
617
750
 
618
751
  def raw_response(status, headers = {})
619
752
  js_headers = `({})`
620
- headers.each { |k, v| ks = k.to_s; vs = v.to_s; `#{js_headers}[#{ks}] = #{vs}` }
753
+ headers.each do |k, v|
754
+ ks = k.to_s
755
+ vs = v.to_s
756
+ `#{js_headers}[#{ks}] = #{vs}`
757
+ end
621
758
  `#{js_headers}['content-type'] = #{@content_type}` if @content_type
622
759
  `#{js_headers}['cache-control'] = #{@cache_control}` if @cache_control
623
- RawResponse.new(`new Response(#{stream}, { status: #{status.to_i}, headers: #{js_headers} })`)
760
+ RawResponse.new(
761
+ `new Response(#{stream}, { status: #{status.to_i}, headers: #{js_headers} })`
762
+ )
624
763
  end
625
764
 
626
765
  def stream
@@ -721,12 +860,17 @@ module Cloudflare
721
860
  result = result.__await__
722
861
  end
723
862
  return result unless result.is_a?(Hash)
724
- nested = result['meta']
863
+ nested = result["meta"]
725
864
  return result unless nested.is_a?(Hash)
726
865
 
727
- %w[last_row_id changes duration size_after rows_read rows_written].each do |k|
728
- result[k] = nested[k] unless result.key?(k)
729
- end
866
+ %w[
867
+ last_row_id
868
+ changes
869
+ duration
870
+ size_after
871
+ rows_read
872
+ rows_written
873
+ ].each { |k| result[k] = nested[k] unless result.key?(k) }
730
874
  result
731
875
  end
732
876
 
@@ -855,7 +999,7 @@ module Cloudflare
855
999
  end
856
1000
 
857
1001
  # Put a value. `body` may be a String. Returns a JS Promise.
858
- def put(key, body, content_type = 'application/octet-stream')
1002
+ def put(key, body, content_type = "application/octet-stream")
859
1003
  js_bucket = @js
860
1004
  `#{js_bucket}.put(#{key}, #{body}, { httpMetadata: { contentType: #{content_type} } })`
861
1005
  end
@@ -891,7 +1035,10 @@ module Cloudflare
891
1035
  `#{opts}.cursor = #{cursor}` if cursor
892
1036
  if include && !include.empty?
893
1037
  js_include = `[]`
894
- include.each { |v| vs = v.to_s; `#{js_include}.push(#{vs})` }
1038
+ include.each do |v|
1039
+ vs = v.to_s
1040
+ `#{js_include}.push(#{vs})`
1041
+ end
895
1042
  `#{opts}.include = #{js_include}`
896
1043
  end
897
1044
  `#{js_bucket}.list(#{opts}).then(function(res) { var rows = []; var arr = res && res.objects ? res.objects : []; for (var i = 0; i < arr.length; i++) { var o = arr[i]; var ct = (o.httpMetadata && o.httpMetadata.contentType) || 'application/octet-stream'; var h = new Map(); h.set('key', o.key); h.set('size', o.size|0); h.set('uploaded', o.uploaded ? o.uploaded.toISOString() : null); h.set('content_type', ct); rows.push(h); } return rows; })`
@@ -907,29 +1054,46 @@ module Cloudflare
907
1054
  end
908
1055
 
909
1056
  def attach!(env, js_env, js_ctx = nil)
910
- env['cloudflare.env'] = js_env
911
- env['cloudflare.ctx'] = js_ctx unless `(#{js_ctx} == null || #{js_ctx} === undefined || #{js_ctx} === Opal.nil)`
912
- return env if `(#{js_env} == null || #{js_env} === undefined || #{js_env} === Opal.nil)`
1057
+ env["cloudflare.env"] = js_env
1058
+ env[
1059
+ "cloudflare.ctx"
1060
+ ] = js_ctx unless `(#{js_ctx} == null || #{js_ctx} === undefined || #{js_ctx} === Opal.nil)`
1061
+ if `(#{js_env} == null || #{js_env} === undefined || #{js_env} === Opal.nil)`
1062
+ return env
1063
+ end
913
1064
 
914
1065
  js_db = `#{js_env} && #{js_env}.DB`
915
1066
  js_kv = `#{js_env} && #{js_env}.KV`
916
1067
  js_r2 = `#{js_env} && #{js_env}.BUCKET`
917
1068
  js_ai = `#{js_env} && #{js_env}.AI`
918
- env['cloudflare.DB'] = D1Database.new(js_db) if `#{js_db} != null`
919
- env['cloudflare.KV'] = KVNamespace.new(js_kv) if `#{js_kv} != null`
920
- env['cloudflare.BUCKET'] = R2Bucket.new(js_r2) if `#{js_r2} != null`
921
- env['cloudflare.AI'] = js_ai if `#{js_ai} != null`
1069
+ env["cloudflare.DB"] = D1Database.new(js_db) if `#{js_db} != null`
1070
+ env["cloudflare.KV"] = KVNamespace.new(js_kv) if `#{js_kv} != null`
1071
+ env["cloudflare.BUCKET"] = R2Bucket.new(js_r2) if `#{js_r2} != null`
1072
+ env["cloudflare.AI"] = js_ai if `#{js_ai} != null`
922
1073
 
1074
+ attach_all_durable_objects!(env, js_env)
923
1075
  attach_durable_object!(env, :counter, `#{js_env} && #{js_env}.COUNTER`)
924
- attach_queue!(env, :jobs, `#{js_env} && #{js_env}.JOBS_QUEUE`, 'JOBS_QUEUE')
925
- attach_queue!(env, :jobs_dlq, `#{js_env} && #{js_env}.JOBS_DLQ`, 'JOBS_DLQ')
1076
+ attach_queue!(
1077
+ env,
1078
+ :jobs,
1079
+ `#{js_env} && #{js_env}.JOBS_QUEUE`,
1080
+ "JOBS_QUEUE"
1081
+ )
1082
+ attach_queue!(
1083
+ env,
1084
+ :jobs_dlq,
1085
+ `#{js_env} && #{js_env}.JOBS_DLQ`,
1086
+ "JOBS_DLQ"
1087
+ )
926
1088
  attach_send_email!(env, js_env)
927
1089
 
928
1090
  env
929
1091
  end
930
1092
 
931
1093
  def attach_durable_object!(env, name, js_binding)
932
- return env if `(#{js_binding} == null || #{js_binding} === undefined || #{js_binding} === Opal.nil)`
1094
+ if `(#{js_binding} == null || #{js_binding} === undefined || #{js_binding} === Opal.nil)`
1095
+ return env
1096
+ end
933
1097
  return env unless defined?(::Cloudflare::DurableObjectNamespace)
934
1098
 
935
1099
  suffix = normalize_binding_name(name)
@@ -937,8 +1101,30 @@ module Cloudflare
937
1101
  env
938
1102
  end
939
1103
 
1104
+ def attach_all_durable_objects!(env, js_env)
1105
+ return env unless defined?(::Cloudflare::DurableObjectNamespace)
1106
+ if `(#{js_env} == null || #{js_env} === undefined || #{js_env} === Opal.nil)`
1107
+ return env
1108
+ end
1109
+
1110
+ keys = `Object.keys(#{js_env})`
1111
+ i = 0
1112
+ len = `#{keys}.length`
1113
+ while i < len
1114
+ key = `#{keys}[#{i}]`
1115
+ js_binding = `#{js_env}[#{key}]`
1116
+ is_do =
1117
+ `#{js_binding} != null && typeof #{js_binding}.idFromName === 'function' && typeof #{js_binding}.get === 'function'`
1118
+ attach_durable_object!(env, key, js_binding) if is_do
1119
+ i += 1
1120
+ end
1121
+ env
1122
+ end
1123
+
940
1124
  def attach_queue!(env, name, js_binding, binding_name)
941
- return env if `(#{js_binding} == null || #{js_binding} === undefined || #{js_binding} === Opal.nil)`
1125
+ if `(#{js_binding} == null || #{js_binding} === undefined || #{js_binding} === Opal.nil)`
1126
+ return env
1127
+ end
942
1128
  return env unless defined?(::Cloudflare::Queue)
943
1129
 
944
1130
  suffix = normalize_binding_name(name)
@@ -951,14 +1137,17 @@ module Cloudflare
951
1137
 
952
1138
  js_send_email = `#{js_env} && #{js_env}.SEND_EMAIL`
953
1139
  if `#{js_send_email} == null || #{js_send_email} === undefined`
954
- js_send_email = `(typeof globalThis !== 'undefined' && globalThis.__OPAL_WORKERS__ && globalThis.__OPAL_WORKERS__.sendEmailBinding) || null`
1140
+ js_send_email =
1141
+ `(typeof globalThis !== 'undefined' && globalThis.__OPAL_WORKERS__ && globalThis.__OPAL_WORKERS__.sendEmailBinding) || null`
955
1142
  end
956
- env['cloudflare.SEND_EMAIL'] = Email.new(js_send_email) if `#{js_send_email} != null`
1143
+ env["cloudflare.SEND_EMAIL"] = Email.new(
1144
+ js_send_email
1145
+ ) if `#{js_send_email} != null`
957
1146
  env
958
1147
  end
959
1148
 
960
1149
  def normalize_binding_name(name)
961
- name.to_s.upcase.gsub(/[^A-Z0-9]+/, '_').sub(/\A_+/, '').sub(/_+\z/, '')
1150
+ name.to_s.upcase.gsub(/[^A-Z0-9]+/, "_").sub(/\A_+/, "").sub(/_+\z/, "")
962
1151
  end
963
1152
 
964
1153
  def durable_object(env, name, id_or_name = nil)
@@ -971,28 +1160,59 @@ module Cloudflare
971
1160
  end
972
1161
 
973
1162
  def ai(env)
974
- raw = env['cloudflare.AI']
975
- return nil if `(#{raw} == null || #{raw} === undefined || #{raw} === Opal.nil)`
976
- return raw if defined?(::Cloudflare::AI::Binding) && `(#{raw} != null && #{raw}.$$class === #{::Cloudflare::AI::Binding})`
977
- return ::Cloudflare::AI::Binding.new(raw) if defined?(::Cloudflare::AI::Binding)
1163
+ raw = env["cloudflare.AI"]
1164
+ if `(#{raw} == null || #{raw} === undefined || #{raw} === Opal.nil)`
1165
+ return nil
1166
+ end
1167
+ if defined?(::Cloudflare::AI::Binding) &&
1168
+ `(#{raw} != null && #{raw}.$$class === #{::Cloudflare::AI::Binding})`
1169
+ return raw
1170
+ end
1171
+ if defined?(::Cloudflare::AI::Binding)
1172
+ return ::Cloudflare::AI::Binding.new(raw)
1173
+ end
978
1174
 
979
1175
  raw
980
1176
  end
981
1177
  end
982
1178
 
983
1179
  module BindingHelpers
984
- def cf_env; env['cloudflare.env']; end
985
- def cf_ctx; env['cloudflare.ctx']; end
986
- def d1; env['cloudflare.DB']; end
987
- def db; d1; end
988
- def kv; env['cloudflare.KV']; end
989
- def bucket; env['cloudflare.BUCKET']; end
990
- def ai; Cloudflare::Bindings.ai(env); end
991
- def send_email; env['cloudflare.SEND_EMAIL']; end
992
- def jobs_queue; env['cloudflare.QUEUE_JOBS']; end
993
- def jobs_dlq; env['cloudflare.QUEUE_JOBS_DLQ']; end
994
- def do_counter; env['cloudflare.DO_COUNTER']; end
995
- def cache; @__homura_cache ||= Cloudflare::Cache.default; end
1180
+ def cf_env
1181
+ env["cloudflare.env"]
1182
+ end
1183
+ def cf_ctx
1184
+ env["cloudflare.ctx"]
1185
+ end
1186
+ def d1
1187
+ env["cloudflare.DB"]
1188
+ end
1189
+ def db
1190
+ d1
1191
+ end
1192
+ def kv
1193
+ env["cloudflare.KV"]
1194
+ end
1195
+ def bucket
1196
+ env["cloudflare.BUCKET"]
1197
+ end
1198
+ def ai
1199
+ Cloudflare::Bindings.ai(env)
1200
+ end
1201
+ def send_email
1202
+ env["cloudflare.SEND_EMAIL"]
1203
+ end
1204
+ def jobs_queue
1205
+ env["cloudflare.QUEUE_JOBS"]
1206
+ end
1207
+ def jobs_dlq
1208
+ env["cloudflare.QUEUE_JOBS_DLQ"]
1209
+ end
1210
+ def do_counter
1211
+ env["cloudflare.DO_COUNTER"]
1212
+ end
1213
+ def cache
1214
+ @__homura_cache ||= Cloudflare::Cache.default
1215
+ end
996
1216
 
997
1217
  def durable_object(name, id_or_name = nil)
998
1218
  Cloudflare::Bindings.durable_object(env, name, id_or_name)
@@ -1003,7 +1223,7 @@ end
1003
1223
  # Phase 6 — HTTP client foundation. Loaded as part of the Cloudflare
1004
1224
  # Workers adapter so user code can simply `require 'sinatra/base'`
1005
1225
  # and use Net::HTTP / Cloudflare::HTTP.fetch without an extra require.
1006
- require 'homura/runtime/http'
1226
+ require "homura/runtime/http"
1007
1227
 
1008
1228
  # Phase 9 — Scheduled (Cron Triggers) dispatcher. Installs the JS
1009
1229
  # `globalThis.__HOMURA_SCHEDULED_DISPATCH__` hook that
@@ -1011,11 +1231,11 @@ require 'homura/runtime/http'
1011
1231
  # Must be loaded after the Cloudflare::* binding wrappers above
1012
1232
  # because it constructs D1Database/KVNamespace/R2Bucket instances
1013
1233
  # inside the dispatcher's per-job env.
1014
- require 'homura/runtime/scheduled'
1234
+ require "homura/runtime/scheduled"
1015
1235
 
1016
1236
  # Phase 10 — Workers AI binding wrapper. Loaded here so any Sinatra
1017
1237
  # route can call the `ai` helper without an extra require.
1018
- require 'homura/runtime/ai'
1238
+ require "homura/runtime/ai"
1019
1239
 
1020
1240
  # Phase 11A — HTTP foundations.
1021
1241
  #
@@ -1024,17 +1244,17 @@ require 'homura/runtime/ai'
1024
1244
  # `stream` adds `Cloudflare::SSEStream` + `Sinatra::Streaming#sse`
1025
1245
  # so a route can `sse do |out| ... end` and flush chunks
1026
1246
  # through a Workers ReadableStream.
1027
- require 'homura/runtime/multipart'
1028
- require 'homura/runtime/stream'
1247
+ require "homura/runtime/multipart"
1248
+ require "homura/runtime/stream"
1029
1249
 
1030
1250
  # Phase 11B — Cloudflare native bindings (Durable Objects / Cache /
1031
1251
  # Queues). Each file registers its own globalThis dispatcher hook
1032
1252
  # where applicable (DO / Queue consumer). Loaded here so user code
1033
1253
  # just needs `require 'sinatra/base'` — no extra `require` per
1034
1254
  # binding.
1035
- require 'homura/runtime/cache'
1036
- require 'homura/runtime/queue'
1037
- require 'homura/runtime/email'
1038
- require 'homura/runtime/durable_object'
1255
+ require "homura/runtime/cache"
1256
+ require "homura/runtime/queue"
1257
+ require "homura/runtime/email"
1258
+ require "homura/runtime/durable_object"
1039
1259
 
1040
- require 'homura/runtime/async_registry'
1260
+ require "homura/runtime/async_registry"