homura-runtime 0.3.4 → 0.3.5
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 +17 -0
- data/docs/ARCHITECTURE.md +2 -2
- data/lib/homura/runtime/ai.rb +31 -8
- data/lib/homura/runtime/async_registry.rb +18 -0
- data/lib/homura/runtime/durable_object.rb +43 -1
- data/lib/homura/runtime/queue.rb +12 -17
- data/lib/homura/runtime/version.rb +1 -1
- data/lib/homura/runtime.rb +103 -50
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b18f68e4c858d9ebaaf71a3626486387595fa6a15d6394096aac320e878d8d29
|
|
4
|
+
data.tar.gz: 0c5c9142f8be62d0e167d3885503c27139318087d21cadd598bde76607178abd
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 903d5230d3a15060921f790b07d1bfb847bafaf2b9033575faa5c7c8d7be4cf57c6118554112ab64a280476736c05b2bfa2a976b60fbc4d761eacb7b13ed1b1f
|
|
7
|
+
data.tar.gz: 20d5a599a1dcf2c0046718128eaa35456e6ce30449b078f37438a6df345ee40fa99d0ede7dceab7096dee4dbc9a2f6588905f3c5174071897be0625fba905cba
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.5 (2026-05-03)
|
|
4
|
+
|
|
5
|
+
- Add Ruby-shaped Cloudflare binding helpers for HTTP, scheduled, queue,
|
|
6
|
+
and Durable Object contexts: `db` / `d1`, `kv`, `bucket`, `ai`,
|
|
7
|
+
`send_email`, `jobs_queue`, `jobs_dlq`, `do_counter`, `cache`, and
|
|
8
|
+
`durable_object(:counter, 'global')`.
|
|
9
|
+
- Add `Cloudflare::AI::Binding#run`, so app code can call
|
|
10
|
+
`ai.run(model, messages: [...])` instead of passing a raw binding into
|
|
11
|
+
`Cloudflare::AI.run`.
|
|
12
|
+
- Add `DurableObjectStub#request` plus `#get`, `#post`, `#put`, and
|
|
13
|
+
`#delete` convenience methods. Hash/Array bodies are JSON encoded with
|
|
14
|
+
`content-type: application/json`.
|
|
15
|
+
- Share Cloudflare binding env construction across HTTP, scheduled,
|
|
16
|
+
queue, and Durable Object dispatch paths.
|
|
17
|
+
- Register the new helpers with auto-await so sync-shaped app code keeps
|
|
18
|
+
working without explicit `.__await__` calls.
|
|
19
|
+
|
|
3
20
|
## 0.3.4 (2026-05-02)
|
|
4
21
|
|
|
5
22
|
- Package the Rack vendor files in `homura-runtime` itself. The 0.3.3
|
data/docs/ARCHITECTURE.md
CHANGED
|
@@ -44,7 +44,7 @@ flowchart LR
|
|
|
44
44
|
| 項目 | 内容 |
|
|
45
45
|
|------|------|
|
|
46
46
|
| Wrangler | `[[send_email]]` に `name = "SEND_EMAIL"`(Cloudflare Email Service · Agents Week 2026)。 |
|
|
47
|
-
|
|
|
47
|
+
| Ruby helper | `send_email` は `Cloudflare::Email`(JS `env.SEND_EMAIL.send(...)` を Ruby 側で `await`)。 |
|
|
48
48
|
| 備考 | consumer アプリ側で verified sender を wrangler `[vars]` などに載せ、アプリから `from` に渡す。 |
|
|
49
49
|
|
|
50
50
|
### Phase 17-E — `/cdn-cgi/*`(Rack に渡さない)とバインディング注入
|
|
@@ -53,7 +53,7 @@ flowchart LR
|
|
|
53
53
|
|------|------|
|
|
54
54
|
| **`worker_module.mjs` の `fetch` 先頭** | `env.SEND_EMAIL` があれば **`globalThis.__OPAL_WORKERS__.sendEmailBinding`** にコピーしてから Rack を呼ぶ。Miniflare 等で `js_env.SEND_EMAIL` が欠ける場合でも Ruby が同じ JS オブジェクトを拾える。 |
|
|
55
55
|
| **`/cdn-cgi/*`** | **Sinatra に渡さない**。Miniflare の entry.worker は `/cdn-cgi/mf/scheduled` だけ先処理する。`/cdn-cgi/handler/email` などはユーザ Worker の `fetch` に届くため、ここで処理しないと Rack が 404 になる。将来 Phase 18(Email 受信)で `export async email(...)` と接続する前提。 |
|
|
56
|
-
| **D1 / KV / Queue との違い** |
|
|
56
|
+
| **D1 / KV / Queue との違い** | D1 / KV / Queue は `db` / `kv` / `jobs_queue` ヘルパーとして使える。`SEND_EMAIL` は **Worker の `env` に付く生バインディング**を `worker_module` が先に global に載せ、`Cloudflare::Email` がそれを包み、アプリ側では `send_email` として触る。 |
|
|
57
57
|
|
|
58
58
|
## wrangler.json について
|
|
59
59
|
|
data/lib/homura/runtime/ai.rb
CHANGED
|
@@ -4,18 +4,15 @@
|
|
|
4
4
|
#
|
|
5
5
|
# Phase 10 — Workers AI binding wrapper.
|
|
6
6
|
#
|
|
7
|
-
# `
|
|
8
|
-
#
|
|
9
|
-
# Sinatra routes can call:
|
|
7
|
+
# `ai.run(model, inputs)` wraps `env.AI.run(model, inputs, options)` and
|
|
8
|
+
# returns a Ruby Hash so Sinatra routes can call:
|
|
10
9
|
#
|
|
11
|
-
#
|
|
12
|
-
# out = Cloudflare::AI.run(
|
|
10
|
+
# out = ai.run(
|
|
13
11
|
# '@cf/google/gemma-4-26b-a4b-it',
|
|
14
|
-
#
|
|
12
|
+
# messages: [
|
|
15
13
|
# { role: 'system', content: 'You are a helpful assistant.' },
|
|
16
14
|
# { role: 'user', content: 'こんにちは' }
|
|
17
|
-
# ]
|
|
18
|
-
# binding: ai
|
|
15
|
+
# ]
|
|
19
16
|
# ).__await__
|
|
20
17
|
# out['response'] # => "..."
|
|
21
18
|
#
|
|
@@ -40,6 +37,31 @@ module Cloudflare
|
|
|
40
37
|
# Default REST options forwarded to env.AI.run as the third argument.
|
|
41
38
|
DEFAULT_OPTIONS = {}.freeze
|
|
42
39
|
|
|
40
|
+
class Binding
|
|
41
|
+
attr_reader :js
|
|
42
|
+
|
|
43
|
+
def initialize(js)
|
|
44
|
+
@js = js
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def available?
|
|
48
|
+
js = @js
|
|
49
|
+
!!`(#{js} !== null && #{js} !== undefined && #{js} !== Opal.nil)`
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def run(model, inputs = nil, options: nil, **input_options)
|
|
53
|
+
payload = inputs || input_options
|
|
54
|
+
payload = payload.merge(input_options) if inputs.is_a?(Hash) && !input_options.empty?
|
|
55
|
+
Cloudflare::AI.run(model, payload, binding: @js, options: options)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def run_stream(model, inputs = nil, **input_options)
|
|
59
|
+
payload = inputs || input_options
|
|
60
|
+
payload = payload.merge(input_options) if inputs.is_a?(Hash) && !input_options.empty?
|
|
61
|
+
run(model, payload.merge(stream: true))
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
43
65
|
# Run a Workers AI model. Returns a JS Promise that resolves to a
|
|
44
66
|
# Ruby Hash for non-streaming calls, or to a Cloudflare::AI::Stream
|
|
45
67
|
# wrapping the JS ReadableStream for streaming calls.
|
|
@@ -49,6 +71,7 @@ module Cloudflare
|
|
|
49
71
|
# @param binding [JS object] env.AI binding (required)
|
|
50
72
|
# @param options [Hash] gateway / extra options forwarded as the 3rd arg
|
|
51
73
|
def self.run(model, inputs, binding: nil, options: nil)
|
|
74
|
+
binding = binding.js if defined?(Binding) && `(#{binding} != null && #{binding}.$$class === #{Binding})`
|
|
52
75
|
# Use a JS-side null check because `binding` may be a raw JS object
|
|
53
76
|
# (env.AI), which has no Ruby `#nil?` method on the prototype.
|
|
54
77
|
bound = !`(#{binding} == null)`
|
|
@@ -171,6 +171,8 @@ HomuraRuntime::AsyncRegistry.register_async_source do
|
|
|
171
171
|
|
|
172
172
|
async_method 'Cloudflare::AI', :run
|
|
173
173
|
taint_return 'Cloudflare::AI', :run_stream, 'Cloudflare::AI::Stream'
|
|
174
|
+
async_method 'Cloudflare::AI::Binding', :run
|
|
175
|
+
taint_return 'Cloudflare::AI::Binding', :run_stream, 'Cloudflare::AI::Stream'
|
|
174
176
|
|
|
175
177
|
async_method 'Cloudflare::Cache', :match
|
|
176
178
|
async_method 'Cloudflare::Cache', :put
|
|
@@ -187,6 +189,11 @@ HomuraRuntime::AsyncRegistry.register_async_source do
|
|
|
187
189
|
taint_return 'Cloudflare::DurableObjectNamespace', :get_by_name, 'Cloudflare::DurableObjectStub'
|
|
188
190
|
taint_return 'Cloudflare::DurableObjectState', :storage, 'Cloudflare::DurableObjectStorage'
|
|
189
191
|
async_method 'Cloudflare::DurableObjectStub', :fetch
|
|
192
|
+
async_method 'Cloudflare::DurableObjectStub', :request
|
|
193
|
+
async_method 'Cloudflare::DurableObjectStub', :get
|
|
194
|
+
async_method 'Cloudflare::DurableObjectStub', :post
|
|
195
|
+
async_method 'Cloudflare::DurableObjectStub', :put
|
|
196
|
+
async_method 'Cloudflare::DurableObjectStub', :delete
|
|
190
197
|
|
|
191
198
|
async_method 'Cloudflare::DurableObjectStorage', :get
|
|
192
199
|
async_method 'Cloudflare::DurableObjectStorage', :put
|
|
@@ -202,4 +209,15 @@ HomuraRuntime::AsyncRegistry.register_async_source do
|
|
|
202
209
|
async_method 'Faraday::Connection', :delete
|
|
203
210
|
async_method 'Faraday::Connection', :patch
|
|
204
211
|
async_method 'Faraday::Connection', :head
|
|
212
|
+
|
|
213
|
+
helper_factory :d1, 'Cloudflare::D1Database'
|
|
214
|
+
helper_factory :db, 'Cloudflare::D1Database'
|
|
215
|
+
helper_factory :kv, 'Cloudflare::KVNamespace'
|
|
216
|
+
helper_factory :bucket, 'Cloudflare::R2Bucket'
|
|
217
|
+
helper_factory :ai, 'Cloudflare::AI::Binding'
|
|
218
|
+
helper_factory :send_email, 'Cloudflare::Email'
|
|
219
|
+
helper_factory :jobs_queue, 'Cloudflare::Queue'
|
|
220
|
+
helper_factory :jobs_dlq, 'Cloudflare::Queue'
|
|
221
|
+
helper_factory :do_counter, 'Cloudflare::DurableObjectNamespace'
|
|
222
|
+
helper_factory :durable_object, 'Cloudflare::DurableObjectStub'
|
|
205
223
|
end
|
|
@@ -185,6 +185,34 @@ module Cloudflare
|
|
|
185
185
|
url: url_str
|
|
186
186
|
)
|
|
187
187
|
end
|
|
188
|
+
|
|
189
|
+
def request(path, method: 'GET', headers: nil, body: nil)
|
|
190
|
+
hdrs = headers ? headers.dup : {}
|
|
191
|
+
request_body = body
|
|
192
|
+
if body.is_a?(Hash) || body.is_a?(Array)
|
|
193
|
+
request_body = body.to_json
|
|
194
|
+
unless hdrs.key?('content-type') || hdrs.key?('Content-Type')
|
|
195
|
+
hdrs['content-type'] = 'application/json'
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
fetch(path, method: method, headers: hdrs, body: request_body)
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def get(path, headers: nil)
|
|
202
|
+
request(path, method: 'GET', headers: headers)
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def post(path, body = nil, headers: nil)
|
|
206
|
+
request(path, method: 'POST', headers: headers, body: body)
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def put(path, body = nil, headers: nil)
|
|
210
|
+
request(path, method: 'PUT', headers: headers, body: body)
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def delete(path, headers: nil)
|
|
214
|
+
request(path, method: 'DELETE', headers: headers)
|
|
215
|
+
end
|
|
188
216
|
end
|
|
189
217
|
|
|
190
218
|
# -----------------------------------------------------------------
|
|
@@ -281,7 +309,8 @@ module Cloudflare
|
|
|
281
309
|
|
|
282
310
|
state = DurableObjectState.new(js_state)
|
|
283
311
|
request = DurableObjectRequest.new(js_request, body_text)
|
|
284
|
-
|
|
312
|
+
env = Cloudflare::Bindings.build_env(js_env)
|
|
313
|
+
ctx = DurableObjectRequestContext.new(state, env, request)
|
|
285
314
|
|
|
286
315
|
result = handler.bind(ctx).call(state, request)
|
|
287
316
|
|
|
@@ -581,6 +610,19 @@ module Cloudflare
|
|
|
581
610
|
end
|
|
582
611
|
|
|
583
612
|
def storage; @state.storage; end
|
|
613
|
+
def cf_env; env['cloudflare.env']; end
|
|
614
|
+
def cf_ctx; env['cloudflare.ctx']; end
|
|
615
|
+
|
|
616
|
+
def d1; env['cloudflare.DB']; end
|
|
617
|
+
def db; d1; end
|
|
618
|
+
def kv; env['cloudflare.KV']; end
|
|
619
|
+
def bucket; env['cloudflare.BUCKET']; end
|
|
620
|
+
def ai; Cloudflare::Bindings.ai(env); end
|
|
621
|
+
def send_email; env['cloudflare.SEND_EMAIL']; end
|
|
622
|
+
def jobs_queue; env['cloudflare.QUEUE_JOBS']; end
|
|
623
|
+
def durable_object(name, id_or_name = nil)
|
|
624
|
+
Cloudflare::Bindings.durable_object(env, name, id_or_name)
|
|
625
|
+
end
|
|
584
626
|
end
|
|
585
627
|
end
|
|
586
628
|
|
data/lib/homura/runtime/queue.rb
CHANGED
|
@@ -367,9 +367,18 @@ module Cloudflare
|
|
|
367
367
|
@env = build_env(js_env)
|
|
368
368
|
end
|
|
369
369
|
|
|
370
|
-
def
|
|
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
|
|
371
374
|
def kv; env['cloudflare.KV']; end
|
|
372
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
|
|
379
|
+
def durable_object(name, id_or_name = nil)
|
|
380
|
+
Cloudflare::Bindings.durable_object(env, name, id_or_name)
|
|
381
|
+
end
|
|
373
382
|
|
|
374
383
|
# Hand a long-running promise to ctx.waitUntil. Mirrors the same
|
|
375
384
|
# helper in Sinatra::Scheduled's ScheduledContext.
|
|
@@ -383,23 +392,9 @@ module Cloudflare
|
|
|
383
392
|
private
|
|
384
393
|
|
|
385
394
|
def build_env(js_env)
|
|
386
|
-
|
|
395
|
+
Cloudflare::Bindings.build_env(js_env, @js_ctx, {
|
|
387
396
|
'cloudflare.queue' => true,
|
|
388
|
-
|
|
389
|
-
'cloudflare.ctx' => @js_ctx
|
|
390
|
-
}
|
|
391
|
-
# js_env is a raw JS object when called from the Workers runtime,
|
|
392
|
-
# so `.nil?` would explode with NoMethodError. Use a JS-level
|
|
393
|
-
# null/undefined/Opal.nil check instead — same pattern
|
|
394
|
-
# `Cloudflare::Cache#available?` uses.
|
|
395
|
-
return env if `(#{js_env} == null || #{js_env} === undefined || #{js_env} === Opal.nil)`
|
|
396
|
-
js_db = `#{js_env} && #{js_env}.DB`
|
|
397
|
-
js_kv = `#{js_env} && #{js_env}.KV`
|
|
398
|
-
js_r2 = `#{js_env} && #{js_env}.BUCKET`
|
|
399
|
-
env['cloudflare.DB'] = Cloudflare::D1Database.new(js_db) if `#{js_db} != null`
|
|
400
|
-
env['cloudflare.KV'] = Cloudflare::KVNamespace.new(js_kv) if `#{js_kv} != null`
|
|
401
|
-
env['cloudflare.BUCKET'] = Cloudflare::R2Bucket.new(js_r2) if `#{js_r2} != null`
|
|
402
|
-
env
|
|
397
|
+
})
|
|
403
398
|
end
|
|
404
399
|
end
|
|
405
400
|
end
|
data/lib/homura/runtime.rb
CHANGED
|
@@ -239,55 +239,7 @@ module Rack
|
|
|
239
239
|
"#{host}:#{port}"
|
|
240
240
|
end
|
|
241
241
|
|
|
242
|
-
|
|
243
|
-
# Rack convention that env keys other than the standard ones
|
|
244
|
-
# SHOULD use a `<library>.<key>` form.
|
|
245
|
-
env['cloudflare.env'] = js_env
|
|
246
|
-
env['cloudflare.ctx'] = js_ctx
|
|
247
|
-
|
|
248
|
-
# Expose D1 / KV / R2 bindings as plain Ruby wrapper objects.
|
|
249
|
-
# The user Sinatra routes reach them via
|
|
250
|
-
# `env['cloudflare.DB']` / `.KV` / `.BUCKET` and call ordinary
|
|
251
|
-
# Ruby methods on them. Under the hood those methods are async,
|
|
252
|
-
# but homura's auto-await build step inserts `.__await__` for the
|
|
253
|
-
# common binding/helper patterns so app source usually does not.
|
|
254
|
-
js_db = `#{js_env} && #{js_env}.DB`
|
|
255
|
-
js_kv = `#{js_env} && #{js_env}.KV`
|
|
256
|
-
js_r2 = `#{js_env} && #{js_env}.BUCKET`
|
|
257
|
-
js_ai = `#{js_env} && #{js_env}.AI`
|
|
258
|
-
env['cloudflare.DB'] = Cloudflare::D1Database.new(js_db) if `#{js_db} != null`
|
|
259
|
-
env['cloudflare.KV'] = Cloudflare::KVNamespace.new(js_kv) if `#{js_kv} != null`
|
|
260
|
-
env['cloudflare.BUCKET'] = Cloudflare::R2Bucket.new(js_r2) if `#{js_r2} != null`
|
|
261
|
-
# Phase 10: env.AI is a Workers AI binding object. Routes call
|
|
262
|
-
# Cloudflare::AI.run(model, inputs, binding: env['cloudflare.AI'])
|
|
263
|
-
# to invoke a model. We expose the raw JS object (not a wrapper)
|
|
264
|
-
# because the wrapper is stateless — every call passes both the
|
|
265
|
-
# model id and the binding explicitly.
|
|
266
|
-
env['cloudflare.AI'] = js_ai if `#{js_ai} != null`
|
|
267
|
-
|
|
268
|
-
# Phase 11B: Durable Objects / Queues.
|
|
269
|
-
# env.COUNTER is a DurableObjectNamespace binding; wrap it into
|
|
270
|
-
# Cloudflare::DurableObjectNamespace so routes can call
|
|
271
|
-
# `do_counter.get_by_name("global").fetch('/inc').__await__`
|
|
272
|
-
# without a backtick. env.JOBS_QUEUE is a Queue producer binding.
|
|
273
|
-
js_do_counter = `#{js_env} && #{js_env}.COUNTER`
|
|
274
|
-
if `#{js_do_counter} != null`
|
|
275
|
-
env['cloudflare.DO_COUNTER'] = Cloudflare::DurableObjectNamespace.new(js_do_counter)
|
|
276
|
-
end
|
|
277
|
-
js_queue = `#{js_env} && #{js_env}.JOBS_QUEUE`
|
|
278
|
-
env['cloudflare.QUEUE_JOBS'] = Cloudflare::Queue.new(js_queue, 'JOBS_QUEUE') if `#{js_queue} != null`
|
|
279
|
-
js_dlq = `#{js_env} && #{js_env}.JOBS_DLQ`
|
|
280
|
-
env['cloudflare.QUEUE_JOBS_DLQ'] = Cloudflare::Queue.new(js_dlq, 'JOBS_DLQ') if `#{js_dlq} != null`
|
|
281
|
-
|
|
282
|
-
# Phase 17 — SEND_EMAIL。worker_module.fetch は先に globalThis.__OPAL_WORKERS__.sendEmailBinding を設定する。
|
|
283
|
-
# 本番では js_env.SEND_EMAIL が直に入る。Miniflare で欠ける場合のみ global を試す(2 段に分けて Opal の埋め込みを単純化)。
|
|
284
|
-
js_send_email = `#{js_env}.SEND_EMAIL`
|
|
285
|
-
if `#{js_send_email} == null || #{js_send_email} === undefined`
|
|
286
|
-
js_send_email = `(typeof globalThis !== 'undefined' && globalThis.__OPAL_WORKERS__ && globalThis.__OPAL_WORKERS__.sendEmailBinding) || null`
|
|
287
|
-
end
|
|
288
|
-
env['cloudflare.SEND_EMAIL'] = Cloudflare::Email.new(js_send_email) if `#{js_send_email} != null`
|
|
289
|
-
|
|
290
|
-
env
|
|
242
|
+
Cloudflare::Bindings.attach!(env, js_env, js_ctx)
|
|
291
243
|
end
|
|
292
244
|
|
|
293
245
|
# Copy CF Workers Request headers into Rack HTTP_* keys, with the
|
|
@@ -945,6 +897,107 @@ module Cloudflare
|
|
|
945
897
|
`#{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; })`
|
|
946
898
|
end
|
|
947
899
|
end
|
|
900
|
+
|
|
901
|
+
module Bindings
|
|
902
|
+
module_function
|
|
903
|
+
|
|
904
|
+
def build_env(js_env, js_ctx = nil, extras = nil)
|
|
905
|
+
env = extras ? extras.dup : {}
|
|
906
|
+
attach!(env, js_env, js_ctx)
|
|
907
|
+
end
|
|
908
|
+
|
|
909
|
+
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)`
|
|
913
|
+
|
|
914
|
+
js_db = `#{js_env} && #{js_env}.DB`
|
|
915
|
+
js_kv = `#{js_env} && #{js_env}.KV`
|
|
916
|
+
js_r2 = `#{js_env} && #{js_env}.BUCKET`
|
|
917
|
+
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`
|
|
922
|
+
|
|
923
|
+
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')
|
|
926
|
+
attach_send_email!(env, js_env)
|
|
927
|
+
|
|
928
|
+
env
|
|
929
|
+
end
|
|
930
|
+
|
|
931
|
+
def attach_durable_object!(env, name, js_binding)
|
|
932
|
+
return env if `(#{js_binding} == null || #{js_binding} === undefined || #{js_binding} === Opal.nil)`
|
|
933
|
+
return env unless defined?(::Cloudflare::DurableObjectNamespace)
|
|
934
|
+
|
|
935
|
+
suffix = normalize_binding_name(name)
|
|
936
|
+
env["cloudflare.DO_#{suffix}"] = DurableObjectNamespace.new(js_binding)
|
|
937
|
+
env
|
|
938
|
+
end
|
|
939
|
+
|
|
940
|
+
def attach_queue!(env, name, js_binding, binding_name)
|
|
941
|
+
return env if `(#{js_binding} == null || #{js_binding} === undefined || #{js_binding} === Opal.nil)`
|
|
942
|
+
return env unless defined?(::Cloudflare::Queue)
|
|
943
|
+
|
|
944
|
+
suffix = normalize_binding_name(name)
|
|
945
|
+
env["cloudflare.QUEUE_#{suffix}"] = Queue.new(js_binding, binding_name)
|
|
946
|
+
env
|
|
947
|
+
end
|
|
948
|
+
|
|
949
|
+
def attach_send_email!(env, js_env)
|
|
950
|
+
return env unless defined?(::Cloudflare::Email)
|
|
951
|
+
|
|
952
|
+
js_send_email = `#{js_env} && #{js_env}.SEND_EMAIL`
|
|
953
|
+
if `#{js_send_email} == null || #{js_send_email} === undefined`
|
|
954
|
+
js_send_email = `(typeof globalThis !== 'undefined' && globalThis.__OPAL_WORKERS__ && globalThis.__OPAL_WORKERS__.sendEmailBinding) || null`
|
|
955
|
+
end
|
|
956
|
+
env['cloudflare.SEND_EMAIL'] = Email.new(js_send_email) if `#{js_send_email} != null`
|
|
957
|
+
env
|
|
958
|
+
end
|
|
959
|
+
|
|
960
|
+
def normalize_binding_name(name)
|
|
961
|
+
name.to_s.upcase.gsub(/[^A-Z0-9]+/, '_').sub(/\A_+/, '').sub(/_+\z/, '')
|
|
962
|
+
end
|
|
963
|
+
|
|
964
|
+
def durable_object(env, name, id_or_name = nil)
|
|
965
|
+
suffix = normalize_binding_name(name)
|
|
966
|
+
ns = env["cloudflare.DO_#{suffix}"] || env["cloudflare.#{suffix}"]
|
|
967
|
+
return nil unless ns
|
|
968
|
+
return ns if id_or_name.nil?
|
|
969
|
+
|
|
970
|
+
ns.get_by_name(id_or_name.to_s)
|
|
971
|
+
end
|
|
972
|
+
|
|
973
|
+
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)
|
|
978
|
+
|
|
979
|
+
raw
|
|
980
|
+
end
|
|
981
|
+
end
|
|
982
|
+
|
|
983
|
+
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
|
|
996
|
+
|
|
997
|
+
def durable_object(name, id_or_name = nil)
|
|
998
|
+
Cloudflare::Bindings.durable_object(env, name, id_or_name)
|
|
999
|
+
end
|
|
1000
|
+
end
|
|
948
1001
|
end
|
|
949
1002
|
|
|
950
1003
|
# Phase 6 — HTTP client foundation. Loaded as part of the Cloudflare
|
|
@@ -961,7 +1014,7 @@ require 'homura/runtime/http'
|
|
|
961
1014
|
require 'homura/runtime/scheduled'
|
|
962
1015
|
|
|
963
1016
|
# Phase 10 — Workers AI binding wrapper. Loaded here so any Sinatra
|
|
964
|
-
# route can call
|
|
1017
|
+
# route can call the `ai` helper without an extra require.
|
|
965
1018
|
require 'homura/runtime/ai'
|
|
966
1019
|
|
|
967
1020
|
# Phase 11A — HTTP foundations.
|