homura-runtime 0.3.6 → 0.3.7
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/CHANGELOG.md +11 -0
- data/exe/auto-await +42 -27
- data/exe/compile-assets +46 -37
- data/exe/compile-erb +86 -61
- data/exe/homura-build +223 -119
- data/lib/homura/runtime/ai.rb +316 -22
- data/lib/homura/runtime/async_registry.rb +135 -98
- data/lib/homura/runtime/auto_await/analyzer.rb +34 -19
- data/lib/homura/runtime/auto_await/transformer.rb +1 -1
- data/lib/homura/runtime/build_support.rb +74 -38
- data/lib/homura/runtime/cache.rb +29 -22
- data/lib/homura/runtime/durable_object.rb +110 -56
- data/lib/homura/runtime/email.rb +28 -14
- data/lib/homura/runtime/http.rb +5 -4
- data/lib/homura/runtime/multipart.rb +47 -47
- data/lib/homura/runtime/queue.rb +82 -29
- data/lib/homura/runtime/scheduled.rb +29 -19
- data/lib/homura/runtime/stream.rb +30 -24
- data/lib/homura/runtime/version.rb +1 -1
- data/lib/homura/runtime.rb +330 -131
- data/lib/homura_vendor_tempfile.rb +5 -4
- data/lib/homura_vendor_tilt.rb +4 -3
- data/lib/homura_vendor_zlib.rb +20 -13
- data/lib/opal_patches.rb +196 -109
- metadata +1 -1
data/lib/homura/runtime.rb
CHANGED
|
@@ -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
|
|
32
|
-
require
|
|
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
|
|
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
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
def
|
|
89
|
-
|
|
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(
|
|
109
|
-
$stderr = HomuraRuntimeIO.new(
|
|
110
|
-
|
|
111
|
-
Object.const_set(:
|
|
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(
|
|
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
|
-
|
|
187
|
+
::Sinatra::Homura.respond_to?(:ensure_rack_app!)
|
|
175
188
|
::Sinatra::Homura.ensure_rack_app!
|
|
176
189
|
end
|
|
177
|
-
|
|
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) &&
|
|
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
|
|
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
|
|
223
|
+
path = `#{url_obj}.pathname`
|
|
208
224
|
# Phase 16 docs: Sinatra + Opal builds responses with String#<< and raises on PATH_INFO `/docs/`.
|
|
209
|
-
path
|
|
210
|
-
raw_qs
|
|
211
|
-
qs
|
|
212
|
-
scheme
|
|
213
|
-
host
|
|
214
|
-
port
|
|
215
|
-
port
|
|
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
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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[
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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
|
|
287
|
-
raw =
|
|
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) ||
|
|
297
|
-
|
|
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
|
|
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
|
|
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) ||
|
|
309
|
-
|
|
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
|
|
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
|
|
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
|
|
328
|
-
|
|
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
|
|
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
|
|
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,8 +404,19 @@ 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
|
|
407
|
+
return(
|
|
408
|
+
`new Response(#{js_stream}, { status: #{status.to_i}, headers: #{js_headers} })`
|
|
409
|
+
)
|
|
410
|
+
end
|
|
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
|
|
351
418
|
end
|
|
419
|
+
return raw_response.js_response if raw_response
|
|
352
420
|
|
|
353
421
|
chunks = []
|
|
354
422
|
if body.respond_to?(:each)
|
|
@@ -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 =
|
|
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 =
|
|
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
|
|
491
|
-
|
|
492
|
-
class
|
|
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
|
|
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
|
-
|
|
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
|
|
692
|
+
def each
|
|
693
|
+
end
|
|
574
694
|
|
|
575
|
-
def close
|
|
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(
|
|
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
|
|
722
|
+
def each
|
|
723
|
+
end
|
|
598
724
|
|
|
599
|
-
def close
|
|
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(
|
|
609
|
-
|
|
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
|
|
745
|
+
def each
|
|
746
|
+
end
|
|
615
747
|
|
|
616
|
-
def close
|
|
748
|
+
def close
|
|
749
|
+
end
|
|
617
750
|
|
|
618
751
|
def raw_response(status, headers = {})
|
|
619
752
|
js_headers = `({})`
|
|
620
|
-
headers.each
|
|
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(
|
|
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[
|
|
863
|
+
nested = result["meta"]
|
|
725
864
|
return result unless nested.is_a?(Hash)
|
|
726
865
|
|
|
727
|
-
%w[
|
|
728
|
-
|
|
729
|
-
|
|
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 =
|
|
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
|
|
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,45 @@ module Cloudflare
|
|
|
907
1054
|
end
|
|
908
1055
|
|
|
909
1056
|
def attach!(env, js_env, js_ctx = nil)
|
|
910
|
-
env[
|
|
911
|
-
env[
|
|
912
|
-
|
|
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[
|
|
919
|
-
env[
|
|
920
|
-
env[
|
|
921
|
-
env[
|
|
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
|
|
|
923
1074
|
attach_durable_object!(env, :counter, `#{js_env} && #{js_env}.COUNTER`)
|
|
924
|
-
attach_queue!(
|
|
925
|
-
|
|
1075
|
+
attach_queue!(
|
|
1076
|
+
env,
|
|
1077
|
+
:jobs,
|
|
1078
|
+
`#{js_env} && #{js_env}.JOBS_QUEUE`,
|
|
1079
|
+
"JOBS_QUEUE"
|
|
1080
|
+
)
|
|
1081
|
+
attach_queue!(
|
|
1082
|
+
env,
|
|
1083
|
+
:jobs_dlq,
|
|
1084
|
+
`#{js_env} && #{js_env}.JOBS_DLQ`,
|
|
1085
|
+
"JOBS_DLQ"
|
|
1086
|
+
)
|
|
926
1087
|
attach_send_email!(env, js_env)
|
|
927
1088
|
|
|
928
1089
|
env
|
|
929
1090
|
end
|
|
930
1091
|
|
|
931
1092
|
def attach_durable_object!(env, name, js_binding)
|
|
932
|
-
|
|
1093
|
+
if `(#{js_binding} == null || #{js_binding} === undefined || #{js_binding} === Opal.nil)`
|
|
1094
|
+
return env
|
|
1095
|
+
end
|
|
933
1096
|
return env unless defined?(::Cloudflare::DurableObjectNamespace)
|
|
934
1097
|
|
|
935
1098
|
suffix = normalize_binding_name(name)
|
|
@@ -938,7 +1101,9 @@ module Cloudflare
|
|
|
938
1101
|
end
|
|
939
1102
|
|
|
940
1103
|
def attach_queue!(env, name, js_binding, binding_name)
|
|
941
|
-
|
|
1104
|
+
if `(#{js_binding} == null || #{js_binding} === undefined || #{js_binding} === Opal.nil)`
|
|
1105
|
+
return env
|
|
1106
|
+
end
|
|
942
1107
|
return env unless defined?(::Cloudflare::Queue)
|
|
943
1108
|
|
|
944
1109
|
suffix = normalize_binding_name(name)
|
|
@@ -951,14 +1116,17 @@ module Cloudflare
|
|
|
951
1116
|
|
|
952
1117
|
js_send_email = `#{js_env} && #{js_env}.SEND_EMAIL`
|
|
953
1118
|
if `#{js_send_email} == null || #{js_send_email} === undefined`
|
|
954
|
-
js_send_email =
|
|
1119
|
+
js_send_email =
|
|
1120
|
+
`(typeof globalThis !== 'undefined' && globalThis.__OPAL_WORKERS__ && globalThis.__OPAL_WORKERS__.sendEmailBinding) || null`
|
|
955
1121
|
end
|
|
956
|
-
env[
|
|
1122
|
+
env["cloudflare.SEND_EMAIL"] = Email.new(
|
|
1123
|
+
js_send_email
|
|
1124
|
+
) if `#{js_send_email} != null`
|
|
957
1125
|
env
|
|
958
1126
|
end
|
|
959
1127
|
|
|
960
1128
|
def normalize_binding_name(name)
|
|
961
|
-
name.to_s.upcase.gsub(/[^A-Z0-9]+/,
|
|
1129
|
+
name.to_s.upcase.gsub(/[^A-Z0-9]+/, "_").sub(/\A_+/, "").sub(/_+\z/, "")
|
|
962
1130
|
end
|
|
963
1131
|
|
|
964
1132
|
def durable_object(env, name, id_or_name = nil)
|
|
@@ -971,28 +1139,59 @@ module Cloudflare
|
|
|
971
1139
|
end
|
|
972
1140
|
|
|
973
1141
|
def ai(env)
|
|
974
|
-
raw = env[
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
1142
|
+
raw = env["cloudflare.AI"]
|
|
1143
|
+
if `(#{raw} == null || #{raw} === undefined || #{raw} === Opal.nil)`
|
|
1144
|
+
return nil
|
|
1145
|
+
end
|
|
1146
|
+
if defined?(::Cloudflare::AI::Binding) &&
|
|
1147
|
+
`(#{raw} != null && #{raw}.$$class === #{::Cloudflare::AI::Binding})`
|
|
1148
|
+
return raw
|
|
1149
|
+
end
|
|
1150
|
+
if defined?(::Cloudflare::AI::Binding)
|
|
1151
|
+
return ::Cloudflare::AI::Binding.new(raw)
|
|
1152
|
+
end
|
|
978
1153
|
|
|
979
1154
|
raw
|
|
980
1155
|
end
|
|
981
1156
|
end
|
|
982
1157
|
|
|
983
1158
|
module BindingHelpers
|
|
984
|
-
def cf_env
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
def
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
def
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
def
|
|
994
|
-
|
|
995
|
-
|
|
1159
|
+
def cf_env
|
|
1160
|
+
env["cloudflare.env"]
|
|
1161
|
+
end
|
|
1162
|
+
def cf_ctx
|
|
1163
|
+
env["cloudflare.ctx"]
|
|
1164
|
+
end
|
|
1165
|
+
def d1
|
|
1166
|
+
env["cloudflare.DB"]
|
|
1167
|
+
end
|
|
1168
|
+
def db
|
|
1169
|
+
d1
|
|
1170
|
+
end
|
|
1171
|
+
def kv
|
|
1172
|
+
env["cloudflare.KV"]
|
|
1173
|
+
end
|
|
1174
|
+
def bucket
|
|
1175
|
+
env["cloudflare.BUCKET"]
|
|
1176
|
+
end
|
|
1177
|
+
def ai
|
|
1178
|
+
Cloudflare::Bindings.ai(env)
|
|
1179
|
+
end
|
|
1180
|
+
def send_email
|
|
1181
|
+
env["cloudflare.SEND_EMAIL"]
|
|
1182
|
+
end
|
|
1183
|
+
def jobs_queue
|
|
1184
|
+
env["cloudflare.QUEUE_JOBS"]
|
|
1185
|
+
end
|
|
1186
|
+
def jobs_dlq
|
|
1187
|
+
env["cloudflare.QUEUE_JOBS_DLQ"]
|
|
1188
|
+
end
|
|
1189
|
+
def do_counter
|
|
1190
|
+
env["cloudflare.DO_COUNTER"]
|
|
1191
|
+
end
|
|
1192
|
+
def cache
|
|
1193
|
+
@__homura_cache ||= Cloudflare::Cache.default
|
|
1194
|
+
end
|
|
996
1195
|
|
|
997
1196
|
def durable_object(name, id_or_name = nil)
|
|
998
1197
|
Cloudflare::Bindings.durable_object(env, name, id_or_name)
|
|
@@ -1003,7 +1202,7 @@ end
|
|
|
1003
1202
|
# Phase 6 — HTTP client foundation. Loaded as part of the Cloudflare
|
|
1004
1203
|
# Workers adapter so user code can simply `require 'sinatra/base'`
|
|
1005
1204
|
# and use Net::HTTP / Cloudflare::HTTP.fetch without an extra require.
|
|
1006
|
-
require
|
|
1205
|
+
require "homura/runtime/http"
|
|
1007
1206
|
|
|
1008
1207
|
# Phase 9 — Scheduled (Cron Triggers) dispatcher. Installs the JS
|
|
1009
1208
|
# `globalThis.__HOMURA_SCHEDULED_DISPATCH__` hook that
|
|
@@ -1011,11 +1210,11 @@ require 'homura/runtime/http'
|
|
|
1011
1210
|
# Must be loaded after the Cloudflare::* binding wrappers above
|
|
1012
1211
|
# because it constructs D1Database/KVNamespace/R2Bucket instances
|
|
1013
1212
|
# inside the dispatcher's per-job env.
|
|
1014
|
-
require
|
|
1213
|
+
require "homura/runtime/scheduled"
|
|
1015
1214
|
|
|
1016
1215
|
# Phase 10 — Workers AI binding wrapper. Loaded here so any Sinatra
|
|
1017
1216
|
# route can call the `ai` helper without an extra require.
|
|
1018
|
-
require
|
|
1217
|
+
require "homura/runtime/ai"
|
|
1019
1218
|
|
|
1020
1219
|
# Phase 11A — HTTP foundations.
|
|
1021
1220
|
#
|
|
@@ -1024,17 +1223,17 @@ require 'homura/runtime/ai'
|
|
|
1024
1223
|
# `stream` adds `Cloudflare::SSEStream` + `Sinatra::Streaming#sse`
|
|
1025
1224
|
# so a route can `sse do |out| ... end` and flush chunks
|
|
1026
1225
|
# through a Workers ReadableStream.
|
|
1027
|
-
require
|
|
1028
|
-
require
|
|
1226
|
+
require "homura/runtime/multipart"
|
|
1227
|
+
require "homura/runtime/stream"
|
|
1029
1228
|
|
|
1030
1229
|
# Phase 11B — Cloudflare native bindings (Durable Objects / Cache /
|
|
1031
1230
|
# Queues). Each file registers its own globalThis dispatcher hook
|
|
1032
1231
|
# where applicable (DO / Queue consumer). Loaded here so user code
|
|
1033
1232
|
# just needs `require 'sinatra/base'` — no extra `require` per
|
|
1034
1233
|
# binding.
|
|
1035
|
-
require
|
|
1036
|
-
require
|
|
1037
|
-
require
|
|
1038
|
-
require
|
|
1234
|
+
require "homura/runtime/cache"
|
|
1235
|
+
require "homura/runtime/queue"
|
|
1236
|
+
require "homura/runtime/email"
|
|
1237
|
+
require "homura/runtime/durable_object"
|
|
1039
1238
|
|
|
1040
|
-
require
|
|
1239
|
+
require "homura/runtime/async_registry"
|