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/queue.rb
CHANGED
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
# lib/sinatra/queue.rb) and invokes whichever matches the batch's
|
|
37
37
|
# `queue` name.
|
|
38
38
|
|
|
39
|
-
require
|
|
39
|
+
require "json"
|
|
40
40
|
|
|
41
41
|
module Cloudflare
|
|
42
42
|
class QueueError < StandardError
|
|
@@ -44,7 +44,9 @@ module Cloudflare
|
|
|
44
44
|
def initialize(message, operation: nil, queue: nil)
|
|
45
45
|
@operation = operation
|
|
46
46
|
@queue = queue
|
|
47
|
-
super(
|
|
47
|
+
super(
|
|
48
|
+
"[Cloudflare::Queue] queue=#{queue || "?"} op=#{operation || "send"}: #{message}"
|
|
49
|
+
)
|
|
48
50
|
end
|
|
49
51
|
end
|
|
50
52
|
|
|
@@ -57,7 +59,7 @@ module Cloudflare
|
|
|
57
59
|
|
|
58
60
|
def initialize(js, name = nil)
|
|
59
61
|
@js = js
|
|
60
|
-
@name = (name ||
|
|
62
|
+
@name = (name || "queue").to_s
|
|
61
63
|
end
|
|
62
64
|
|
|
63
65
|
def available?
|
|
@@ -79,12 +81,18 @@ module Cloudflare
|
|
|
79
81
|
js = @js
|
|
80
82
|
qname = @name
|
|
81
83
|
err_klass = Cloudflare::QueueError
|
|
82
|
-
|
|
84
|
+
unless available?
|
|
85
|
+
raise QueueError.new(
|
|
86
|
+
"queue binding not bound",
|
|
87
|
+
operation: "send",
|
|
88
|
+
queue: qname
|
|
89
|
+
)
|
|
90
|
+
end
|
|
83
91
|
|
|
84
92
|
js_body = ruby_to_js(body)
|
|
85
93
|
js_opts = `({})`
|
|
86
94
|
`#{js_opts}.delaySeconds = #{delay_seconds.to_i}` if delay_seconds
|
|
87
|
-
`#{js_opts}.contentType = #{content_type.to_s}`
|
|
95
|
+
`#{js_opts}.contentType = #{content_type.to_s}` if content_type
|
|
88
96
|
|
|
89
97
|
# Single-line IIFE — see `lib/homura/runtime/cache.rb#put`
|
|
90
98
|
# for the Opal multi-line x-string quirk. Passing arguments in
|
|
@@ -99,14 +107,20 @@ module Cloudflare
|
|
|
99
107
|
js = @js
|
|
100
108
|
qname = @name
|
|
101
109
|
err_klass = Cloudflare::QueueError
|
|
102
|
-
|
|
110
|
+
unless available?
|
|
111
|
+
raise QueueError.new(
|
|
112
|
+
"queue binding not bound",
|
|
113
|
+
operation: "send_batch",
|
|
114
|
+
queue: qname
|
|
115
|
+
)
|
|
116
|
+
end
|
|
103
117
|
|
|
104
118
|
js_msgs = `([])`
|
|
105
119
|
messages.each do |m|
|
|
106
120
|
if m.is_a?(Hash)
|
|
107
|
-
body = m[
|
|
108
|
-
ct
|
|
109
|
-
ds
|
|
121
|
+
body = m["body"] || m[:body]
|
|
122
|
+
ct = m["content_type"] || m[:content_type]
|
|
123
|
+
ds = m["delay_seconds"] || m[:delay_seconds]
|
|
110
124
|
js_body = ruby_to_js(body)
|
|
111
125
|
js_msg = `({ body: #{js_body} })`
|
|
112
126
|
`#{js_msg}.contentType = #{ct.to_s}` if ct
|
|
@@ -129,10 +143,11 @@ module Cloudflare
|
|
|
129
143
|
# Same Ruby→JS conversion that Cloudflare::AI.ruby_to_js uses, but
|
|
130
144
|
# local to avoid cross-wrapper coupling.
|
|
131
145
|
def ruby_to_js(val)
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
return val
|
|
146
|
+
if val.is_a?(String) || val.is_a?(Numeric) || val == true ||
|
|
147
|
+
val == false || val.nil?
|
|
148
|
+
return val
|
|
135
149
|
end
|
|
150
|
+
return val.to_s if val.is_a?(Symbol)
|
|
136
151
|
if val.is_a?(Hash)
|
|
137
152
|
obj = `({})`
|
|
138
153
|
val.each do |k, v|
|
|
@@ -144,7 +159,10 @@ module Cloudflare
|
|
|
144
159
|
end
|
|
145
160
|
if val.is_a?(Array)
|
|
146
161
|
arr = `([])`
|
|
147
|
-
val.each
|
|
162
|
+
val.each do |v|
|
|
163
|
+
jv = ruby_to_js(v)
|
|
164
|
+
`#{arr}.push(#{jv})`
|
|
165
|
+
end
|
|
148
166
|
return arr
|
|
149
167
|
end
|
|
150
168
|
val
|
|
@@ -167,7 +185,8 @@ module Cloudflare
|
|
|
167
185
|
|
|
168
186
|
def timestamp
|
|
169
187
|
js = @js
|
|
170
|
-
ms =
|
|
188
|
+
ms =
|
|
189
|
+
`(#{js} && #{js}.timestamp && typeof #{js}.timestamp.getTime === 'function' ? #{js}.timestamp.getTime() : null)`
|
|
171
190
|
return nil if `#{ms} == null`
|
|
172
191
|
Time.at(ms.to_f / 1000.0)
|
|
173
192
|
end
|
|
@@ -222,7 +241,9 @@ module Cloudflare
|
|
|
222
241
|
|
|
223
242
|
def js_to_ruby(v)
|
|
224
243
|
return nil if `#{v} == null`
|
|
225
|
-
|
|
244
|
+
if `typeof #{v} === 'string' || typeof #{v} === 'number' || typeof #{v} === 'boolean'`
|
|
245
|
+
return v
|
|
246
|
+
end
|
|
226
247
|
if `Array.isArray(#{v})`
|
|
227
248
|
out = []
|
|
228
249
|
len = `#{v}.length`
|
|
@@ -333,7 +354,14 @@ module Cloudflare
|
|
|
333
354
|
handler = handler_for(queue_name)
|
|
334
355
|
if handler.nil?
|
|
335
356
|
warn "[Cloudflare::QueueConsumer] no handler registered for queue #{queue_name.inspect}; messages will time out and retry"
|
|
336
|
-
return
|
|
357
|
+
return(
|
|
358
|
+
{
|
|
359
|
+
"queue" => queue_name,
|
|
360
|
+
"handled" => false,
|
|
361
|
+
"size" => batch.size,
|
|
362
|
+
"reason" => "no_handler"
|
|
363
|
+
}
|
|
364
|
+
)
|
|
337
365
|
end
|
|
338
366
|
|
|
339
367
|
ctx = QueueContext.new(batch, js_env, js_ctx)
|
|
@@ -341,7 +369,12 @@ module Cloudflare
|
|
|
341
369
|
if `(#{result} != null && typeof #{result}.then === 'function')`
|
|
342
370
|
result = result.__await__
|
|
343
371
|
end
|
|
344
|
-
{
|
|
372
|
+
{
|
|
373
|
+
"queue" => queue_name,
|
|
374
|
+
"handled" => true,
|
|
375
|
+
"size" => batch.size,
|
|
376
|
+
"result" => result.is_a?(Hash) ? result : nil
|
|
377
|
+
}
|
|
345
378
|
end
|
|
346
379
|
|
|
347
380
|
# Single-line backtick (see scheduled.rb for the Opal multi-line
|
|
@@ -367,15 +400,33 @@ module Cloudflare
|
|
|
367
400
|
@env = build_env(js_env)
|
|
368
401
|
end
|
|
369
402
|
|
|
370
|
-
def d1
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
def
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
def
|
|
377
|
-
|
|
378
|
-
|
|
403
|
+
def d1
|
|
404
|
+
env["cloudflare.DB"]
|
|
405
|
+
end
|
|
406
|
+
def db
|
|
407
|
+
d1
|
|
408
|
+
end
|
|
409
|
+
def cf_env
|
|
410
|
+
env["cloudflare.env"]
|
|
411
|
+
end
|
|
412
|
+
def cf_ctx
|
|
413
|
+
env["cloudflare.ctx"]
|
|
414
|
+
end
|
|
415
|
+
def kv
|
|
416
|
+
env["cloudflare.KV"]
|
|
417
|
+
end
|
|
418
|
+
def bucket
|
|
419
|
+
env["cloudflare.BUCKET"]
|
|
420
|
+
end
|
|
421
|
+
def ai
|
|
422
|
+
Cloudflare::Bindings.ai(env)
|
|
423
|
+
end
|
|
424
|
+
def send_email
|
|
425
|
+
env["cloudflare.SEND_EMAIL"]
|
|
426
|
+
end
|
|
427
|
+
def jobs_queue
|
|
428
|
+
env["cloudflare.QUEUE_JOBS"]
|
|
429
|
+
end
|
|
379
430
|
def durable_object(name, id_or_name = nil)
|
|
380
431
|
Cloudflare::Bindings.durable_object(env, name, id_or_name)
|
|
381
432
|
end
|
|
@@ -392,9 +443,11 @@ module Cloudflare
|
|
|
392
443
|
private
|
|
393
444
|
|
|
394
445
|
def build_env(js_env)
|
|
395
|
-
Cloudflare::Bindings.build_env(
|
|
396
|
-
|
|
397
|
-
|
|
446
|
+
Cloudflare::Bindings.build_env(
|
|
447
|
+
js_env,
|
|
448
|
+
@js_ctx,
|
|
449
|
+
{ "cloudflare.queue" => true }
|
|
450
|
+
)
|
|
398
451
|
end
|
|
399
452
|
end
|
|
400
453
|
end
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
# Workers runtime takes is exercised under Node, without needing the
|
|
31
31
|
# JS dispatcher.
|
|
32
32
|
|
|
33
|
-
require
|
|
33
|
+
require "time"
|
|
34
34
|
|
|
35
35
|
module Cloudflare
|
|
36
36
|
# Wrapper around the JS ScheduledEvent. The Workers runtime gives us:
|
|
@@ -44,7 +44,7 @@ module Cloudflare
|
|
|
44
44
|
class ScheduledEvent
|
|
45
45
|
attr_reader :cron, :scheduled_time, :type, :raw
|
|
46
46
|
|
|
47
|
-
def initialize(cron:, scheduled_time:, type:
|
|
47
|
+
def initialize(cron:, scheduled_time:, type: "scheduled", raw: nil)
|
|
48
48
|
@cron = cron.to_s.freeze
|
|
49
49
|
@scheduled_time = scheduled_time
|
|
50
50
|
@type = type.to_s.freeze
|
|
@@ -60,26 +60,28 @@ module Cloudflare
|
|
|
60
60
|
# JS undefined into Ruby's nil — calling `.nil?` on it raises
|
|
61
61
|
# TypeError.
|
|
62
62
|
def self.from_js(js_event)
|
|
63
|
-
return new(cron:
|
|
63
|
+
return new(cron: "", scheduled_time: Time.now) if `#{js_event} == null`
|
|
64
64
|
|
|
65
|
-
cron
|
|
66
|
-
type
|
|
65
|
+
cron = `(#{js_event}.cron == null ? '' : String(#{js_event}.cron))`
|
|
66
|
+
type =
|
|
67
|
+
`(#{js_event}.type == null ? 'scheduled' : String(#{js_event}.type))`
|
|
67
68
|
has_sched = `(#{js_event}.scheduledTime != null)`
|
|
68
|
-
sched_t
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
69
|
+
sched_t =
|
|
70
|
+
if has_sched
|
|
71
|
+
sched_ms = `Number(#{js_event}.scheduledTime)`
|
|
72
|
+
Time.at(sched_ms.to_f / 1000.0)
|
|
73
|
+
else
|
|
74
|
+
Time.now
|
|
75
|
+
end
|
|
74
76
|
|
|
75
77
|
new(cron: cron, scheduled_time: sched_t, type: type, raw: js_event)
|
|
76
78
|
end
|
|
77
79
|
|
|
78
80
|
def to_h
|
|
79
81
|
{
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
82
|
+
"cron" => cron,
|
|
83
|
+
"scheduled_time" => scheduled_time.to_i,
|
|
84
|
+
"type" => type
|
|
83
85
|
}
|
|
84
86
|
end
|
|
85
87
|
end
|
|
@@ -119,8 +121,10 @@ module Cloudflare
|
|
|
119
121
|
event = ScheduledEvent.from_js(js_event)
|
|
120
122
|
target = resolve_app
|
|
121
123
|
if target.nil?
|
|
122
|
-
warn
|
|
123
|
-
return
|
|
124
|
+
warn "[Cloudflare::Scheduled] no app registered; ignoring scheduled event"
|
|
125
|
+
return(
|
|
126
|
+
{ "fired" => 0, "total" => 0, "results" => [], "error" => "no_app" }
|
|
127
|
+
)
|
|
124
128
|
end
|
|
125
129
|
target.dispatch_scheduled(event, js_env, js_ctx).__await__
|
|
126
130
|
end
|
|
@@ -130,10 +134,15 @@ module Cloudflare
|
|
|
130
134
|
# hook. `cron` / `scheduled_time` are plain Ruby values. Returns
|
|
131
135
|
# the awaited result Hash (callers can still `__await__` the
|
|
132
136
|
# outer Promise — this method is async since it uses `__await__`).
|
|
133
|
-
def self.dispatch(
|
|
137
|
+
def self.dispatch(
|
|
138
|
+
cron,
|
|
139
|
+
scheduled_time = Time.now,
|
|
140
|
+
js_env = nil,
|
|
141
|
+
js_ctx = nil
|
|
142
|
+
)
|
|
134
143
|
event = ScheduledEvent.new(cron: cron, scheduled_time: scheduled_time)
|
|
135
144
|
target = resolve_app
|
|
136
|
-
raise
|
|
145
|
+
raise "no app registered for Cloudflare::Scheduled" if target.nil?
|
|
137
146
|
target.dispatch_scheduled(event, js_env, js_ctx).__await__
|
|
138
147
|
end
|
|
139
148
|
|
|
@@ -170,7 +179,8 @@ module Cloudflare
|
|
|
170
179
|
# mistake is visible.
|
|
171
180
|
if candidate.respond_to?(:dispatch_scheduled)
|
|
172
181
|
candidate
|
|
173
|
-
elsif candidate.respond_to?(:class) &&
|
|
182
|
+
elsif candidate.respond_to?(:class) &&
|
|
183
|
+
candidate.class.respond_to?(:dispatch_scheduled)
|
|
174
184
|
candidate.class
|
|
175
185
|
else
|
|
176
186
|
candidate
|
|
@@ -35,10 +35,10 @@ module Cloudflare
|
|
|
35
35
|
# `new Response(readable, { headers })`.
|
|
36
36
|
class SSEStream
|
|
37
37
|
DEFAULT_HEADERS = {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
38
|
+
"content-type" => "text/event-stream; charset=utf-8",
|
|
39
|
+
"cache-control" => "no-cache, no-transform",
|
|
40
|
+
"x-accel-buffering" => "no",
|
|
41
|
+
"connection" => "keep-alive"
|
|
42
42
|
}.freeze
|
|
43
43
|
|
|
44
44
|
def initialize(headers: nil, ctx: nil, &block)
|
|
@@ -65,7 +65,7 @@ module Cloudflare
|
|
|
65
65
|
return @js_stream if @js_stream
|
|
66
66
|
blk = @block
|
|
67
67
|
ctx = @ctx
|
|
68
|
-
raise ArgumentError,
|
|
68
|
+
raise ArgumentError, "SSEStream needs a block" if blk.nil?
|
|
69
69
|
|
|
70
70
|
ts = `new TransformStream()`
|
|
71
71
|
writer = `#{ts}.writable.getWriter()`
|
|
@@ -86,8 +86,10 @@ module Cloudflare
|
|
|
86
86
|
# through the JS pipe. Sinatra's content-length calculator therefore
|
|
87
87
|
# sees no body and leaves the header out, which is exactly right for
|
|
88
88
|
# a chunked streaming response.
|
|
89
|
-
def each
|
|
90
|
-
|
|
89
|
+
def each
|
|
90
|
+
end
|
|
91
|
+
def close
|
|
92
|
+
end
|
|
91
93
|
|
|
92
94
|
private
|
|
93
95
|
|
|
@@ -168,7 +170,8 @@ module Cloudflare
|
|
|
168
170
|
# enforced but the promise graph stays flat (Copilot review #3 —
|
|
169
171
|
# prior implementation pushed every write into an unbounded
|
|
170
172
|
# array which would leak memory on long-running streams).
|
|
171
|
-
@tail =
|
|
173
|
+
@tail =
|
|
174
|
+
`#{@tail}.then(function() { return #{w}.write(#{enc}.encode(#{s})); })`
|
|
172
175
|
self
|
|
173
176
|
end
|
|
174
177
|
alias_method :<<, :write
|
|
@@ -176,7 +179,7 @@ module Cloudflare
|
|
|
176
179
|
# Helper: emit a well-formed SSE event. `data` is split on LF and
|
|
177
180
|
# each line prefixed with `data:` per the SSE spec.
|
|
178
181
|
def event(data, event: nil, id: nil, retry_ms: nil)
|
|
179
|
-
buf =
|
|
182
|
+
buf = ""
|
|
180
183
|
buf += "event: #{event}\n" if event
|
|
181
184
|
buf += "id: #{id}\n" if id
|
|
182
185
|
buf += "retry: #{retry_ms.to_i}\n" if retry_ms
|
|
@@ -224,7 +227,8 @@ module Cloudflare
|
|
|
224
227
|
|
|
225
228
|
# Rack-body compatibility so the SSEOut itself is also iterable by
|
|
226
229
|
# `build_js_response` when used as a body (unusual but supported).
|
|
227
|
-
def each
|
|
230
|
+
def each
|
|
231
|
+
end
|
|
228
232
|
end
|
|
229
233
|
end
|
|
230
234
|
|
|
@@ -239,7 +243,7 @@ module Sinatra
|
|
|
239
243
|
# for framed events. The block runs in an async task attached to
|
|
240
244
|
# `ctx.waitUntil`, so the isolate stays alive until the block ends.
|
|
241
245
|
def sse(headers: nil, &block)
|
|
242
|
-
ctx = env[
|
|
246
|
+
ctx = env["cloudflare.ctx"]
|
|
243
247
|
::Cloudflare::SSEStream.new(headers: headers, ctx: ctx, &block)
|
|
244
248
|
end
|
|
245
249
|
|
|
@@ -267,17 +271,18 @@ module Sinatra
|
|
|
267
271
|
# `keep_open:` は本家互換だが Workers では意味がない(EM 前提)ため
|
|
268
272
|
# 受け取るだけで使わない。
|
|
269
273
|
def stream(keep_open: false, type: :plain, headers: nil, &block)
|
|
270
|
-
ctx = env[
|
|
274
|
+
ctx = env["cloudflare.ctx"]
|
|
271
275
|
extra_headers = headers || {}
|
|
272
|
-
merged =
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
276
|
+
merged =
|
|
277
|
+
case type
|
|
278
|
+
when :sse, :event_stream
|
|
279
|
+
extra_headers # SSE defaults は SSEStream 側で入る
|
|
280
|
+
else
|
|
281
|
+
# Plain streaming — start from an empty default set so
|
|
282
|
+
# the SSE headers don't get force-injected into e.g. a
|
|
283
|
+
# log-tailing or chunked-JSON endpoint.
|
|
284
|
+
{ "content-type" => "text/plain; charset=utf-8" }.merge(extra_headers)
|
|
285
|
+
end
|
|
281
286
|
::Cloudflare::SSEStream.new(headers: merged, ctx: ctx, &block)
|
|
282
287
|
end
|
|
283
288
|
|
|
@@ -305,13 +310,14 @@ module Sinatra
|
|
|
305
310
|
# route through Cloudflare::SSEStream. Upstream's `Stream` class
|
|
306
311
|
# is not used on Workers (no EventMachine / Thread pool).
|
|
307
312
|
def stream(keep_open = false, &block)
|
|
308
|
-
ctx = env[
|
|
313
|
+
ctx = env["cloudflare.ctx"]
|
|
309
314
|
::Cloudflare::SSEStream.new(
|
|
310
|
-
headers: {
|
|
315
|
+
headers: {
|
|
316
|
+
"content-type" => "text/plain; charset=utf-8"
|
|
317
|
+
},
|
|
311
318
|
ctx: ctx,
|
|
312
319
|
&block
|
|
313
320
|
)
|
|
314
321
|
end
|
|
315
322
|
end
|
|
316
323
|
end
|
|
317
|
-
|