homura-runtime 0.3.5 → 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.
@@ -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 'json'
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("[Cloudflare::Queue] queue=#{queue || '?'} op=#{operation || 'send'}: #{message}")
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 || 'queue').to_s
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
- raise QueueError.new('queue binding not bound', operation: 'send', queue: qname) unless available?
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}` if content_type
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
- raise QueueError.new('queue binding not bound', operation: 'send_batch', queue: qname) unless available?
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['body'] || m[:body]
108
- ct = m['content_type'] || m[:content_type]
109
- ds = m['delay_seconds'] || m[:delay_seconds]
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
- return val if val.is_a?(String) || val.is_a?(Numeric) || val == true || val == false || val.nil?
133
- if val.is_a?(Symbol)
134
- return val.to_s
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 { |v| jv = ruby_to_js(v); `#{arr}.push(#{jv})` }
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 = `(#{js} && #{js}.timestamp && typeof #{js}.timestamp.getTime === 'function' ? #{js}.timestamp.getTime() : null)`
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
- return v if `typeof #{v} === 'string' || typeof #{v} === 'number' || typeof #{v} === 'boolean'`
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 { 'queue' => queue_name, 'handled' => false, 'size' => batch.size, 'reason' => 'no_handler' }
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
- { 'queue' => queue_name, 'handled' => true, 'size' => batch.size, 'result' => result.is_a?(Hash) ? result : nil }
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; env['cloudflare.DB']; end
371
- def db; d1; end
372
- def cf_env; env['cloudflare.env']; end
373
- def cf_ctx; env['cloudflare.ctx']; end
374
- def kv; env['cloudflare.KV']; end
375
- def bucket; env['cloudflare.BUCKET']; end
376
- def ai; Cloudflare::Bindings.ai(env); end
377
- def send_email; env['cloudflare.SEND_EMAIL']; end
378
- def jobs_queue; env['cloudflare.QUEUE_JOBS']; end
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(js_env, @js_ctx, {
396
- 'cloudflare.queue' => true,
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 'time'
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: 'scheduled', raw: nil)
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: '', scheduled_time: Time.now) if `#{js_event} == null`
63
+ return new(cron: "", scheduled_time: Time.now) if `#{js_event} == null`
64
64
 
65
- cron = `(#{js_event}.cron == null ? '' : String(#{js_event}.cron))`
66
- type = `(#{js_event}.type == null ? 'scheduled' : String(#{js_event}.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 = if has_sched
69
- sched_ms = `Number(#{js_event}.scheduledTime)`
70
- Time.at(sched_ms.to_f / 1000.0)
71
- else
72
- Time.now
73
- end
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
- 'cron' => cron,
81
- 'scheduled_time' => scheduled_time.to_i,
82
- 'type' => type
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 '[Cloudflare::Scheduled] no app registered; ignoring scheduled event'
123
- return { 'fired' => 0, 'total' => 0, 'results' => [], 'error' => 'no_app' }
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(cron, scheduled_time = Time.now, js_env = nil, js_ctx = nil)
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 'no app registered for Cloudflare::Scheduled' if target.nil?
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) && candidate.class.respond_to?(:dispatch_scheduled)
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
- 'content-type' => 'text/event-stream; charset=utf-8',
39
- 'cache-control' => 'no-cache, no-transform',
40
- 'x-accel-buffering' => 'no',
41
- 'connection' => 'keep-alive'
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, 'SSEStream needs a block' if blk.nil?
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; end
90
- def close; end
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 = `#{@tail}.then(function() { return #{w}.write(#{enc}.encode(#{s})); })`
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; end
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['cloudflare.ctx']
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['cloudflare.ctx']
274
+ ctx = env["cloudflare.ctx"]
271
275
  extra_headers = headers || {}
272
- merged = case type
273
- when :sse, :event_stream
274
- extra_headers # SSE defaults は SSEStream 側で入る
275
- else
276
- # Plain streaming — start from an empty default set so
277
- # the SSE headers don't get force-injected into e.g. a
278
- # log-tailing or chunked-JSON endpoint.
279
- { 'content-type' => 'text/plain; charset=utf-8' }.merge(extra_headers)
280
- end
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['cloudflare.ctx']
313
+ ctx = env["cloudflare.ctx"]
309
314
  ::Cloudflare::SSEStream.new(
310
- headers: { 'content-type' => 'text/plain; charset=utf-8' },
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
-
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module HomuraRuntime
4
- VERSION = '0.3.5'
4
+ VERSION = "0.3.7"
5
5
  end