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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: deee61a6cf85e633844b5e18df6da9658294bd10db266c1d2525760369610d98
4
- data.tar.gz: a40ad24b9012f798ee0eb90e1b11bc819e9cd8d8aa418c73482208d3ffc58303
3
+ metadata.gz: b18f68e4c858d9ebaaf71a3626486387595fa6a15d6394096aac320e878d8d29
4
+ data.tar.gz: 0c5c9142f8be62d0e167d3885503c27139318087d21cadd598bde76607178abd
5
5
  SHA512:
6
- metadata.gz: 5ddc0b574901b97a3c50db3389c41acbcff7ea5e2990483c4edb46cd08ddf4d2c2ee7e950acec6b4d6ea0ad09f3e470c423428cbbadc5c271bbf2cf3c1532095
7
- data.tar.gz: 38959a44892a5ddac26d1d31e4087ee333efda1d840aed96abab9f3971449f96aa7012c22068b3da6a260c57ffb4d7e564c2d7baf753bb20c4574b185d9ac5b3
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
- | Rack env | `env['cloudflare.SEND_EMAIL']` は `Cloudflare::Email`(JS `env.SEND_EMAIL.send(...)` を Ruby 側で `await`)。 |
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 との違い** | それらは典型的に `env['cloudflare.env']` 経由で JS バインディングを参照する。`SEND_EMAIL` は **Worker の `env` に付く生バインディング**を `worker_module` が先に global に載せ、`Cloudflare::Email` がそれを包む(Rack `cloudflare.*` はラッパー用の入口)。 |
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
 
@@ -4,18 +4,15 @@
4
4
  #
5
5
  # Phase 10 — Workers AI binding wrapper.
6
6
  #
7
- # `Cloudflare::AI.run(model, inputs, binding: env['cloudflare.AI'])`
8
- # wraps `env.AI.run(model, inputs, options)` and returns a Ruby Hash so
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
- # ai = env['cloudflare.AI']
12
- # out = Cloudflare::AI.run(
10
+ # out = ai.run(
13
11
  # '@cf/google/gemma-4-26b-a4b-it',
14
- # { messages: [
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
- ctx = DurableObjectRequestContext.new(state, js_env, request)
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
 
@@ -367,9 +367,18 @@ module Cloudflare
367
367
  @env = build_env(js_env)
368
368
  end
369
369
 
370
- def db; env['cloudflare.DB']; end
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
- env = {
395
+ Cloudflare::Bindings.build_env(js_env, @js_ctx, {
387
396
  'cloudflare.queue' => true,
388
- 'cloudflare.env' => js_env,
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module HomuraRuntime
4
- VERSION = '0.3.4'
4
+ VERSION = '0.3.5'
5
5
  end
@@ -239,55 +239,7 @@ module Rack
239
239
  "#{host}:#{port}"
240
240
  end
241
241
 
242
- # Cloudflare-specific extras under their own namespace, per the
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 Cloudflare::AI.run(...) without an extra require.
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.
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: homura-runtime
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.4
4
+ version: 0.3.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kazuhiro Homma