homura-runtime 0.3.8 → 0.3.10

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.
data/exe/homura-build CHANGED
@@ -8,6 +8,7 @@ require "fileutils"
8
8
  require "open3"
9
9
  require "optparse"
10
10
  require "pathname"
11
+
11
12
  require_relative "../lib/homura/runtime/build_support"
12
13
 
13
14
  module HomuraRuntimeBuild
@@ -55,6 +56,7 @@ OptionParser
55
56
  o.on("--root PATH", "Project root (default: cwd)") do |p|
56
57
  options[:root] = p
57
58
  end
59
+
58
60
  o.on(
59
61
  "--standalone",
60
62
  "Consumer app (skip inline-routes; use Gemfile-resolved load paths)"
@@ -62,6 +64,7 @@ OptionParser
62
64
  o.on("--with-db", "Include sequel-d1 on Opal load path (standalone)") do
63
65
  options[:with_db] = true
64
66
  end
67
+
65
68
  o.on("--input PATH", "Opal entry .rb") { |p| options[:opal_input] = p }
66
69
  o.on("--output PATH", "Opal bundle .mjs") { |p| options[:opal_output] = p }
67
70
  o.on(
@@ -71,9 +74,11 @@ OptionParser
71
74
  o.on("--setup-import PATH", "setup-node-crypto import in entrypoint") do |p|
72
75
  options[:setup_import] = p
73
76
  end
77
+
74
78
  o.on("--bundle-import PATH", "Opal bundle import in entrypoint") do |p|
75
79
  options[:bundle_import] = p
76
80
  end
81
+
77
82
  o.on(
78
83
  "--worker-module-import PATH",
79
84
  "worker_module.mjs import in entrypoint"
@@ -81,6 +86,7 @@ OptionParser
81
86
  o.on("--entrypoint-out PATH", "Where to write worker.entrypoint.mjs") do |p|
82
87
  options[:entrypoint_out] = p
83
88
  end
89
+
84
90
  o.on(
85
91
  "--templates-namespace NAME",
86
92
  "Standalone templates module name (default: project-derived)"
@@ -95,16 +101,13 @@ OptionParser
95
101
  options[:standalone] = true if options[:with_db]
96
102
 
97
103
  root = Pathname(options[:root]).expand_path
98
- options[
99
- :templates_namespace
100
- ] ||= HomuraRuntime::BuildSupport.standalone_namespace(
101
- root,
102
- "Templates"
103
- ) if options[:standalone]
104
- options[:assets_namespace] ||= HomuraRuntime::BuildSupport.standalone_namespace(
105
- root,
106
- "Assets"
107
- ) if options[:standalone]
104
+ if options[:standalone]
105
+ options[:templates_namespace] ||= HomuraRuntime::BuildSupport.standalone_namespace(root, "Templates")
106
+ end
107
+
108
+ if options[:standalone]
109
+ options[:assets_namespace] ||= HomuraRuntime::BuildSupport.standalone_namespace(root, "Assets")
110
+ end
108
111
 
109
112
  if options[:standalone]
110
113
  Dir.chdir(root) { require "bundler/setup" }
@@ -175,7 +178,7 @@ def run_opal_homura!(root, opal_input, opal_output)
175
178
  ]
176
179
  stderr_log = root.join("build/opal.stderr.log")
177
180
  FileUtils.mkdir_p(root.join("build"))
178
- env = { "OPAL_PREFORK_DISABLE" => "1" }
181
+ env = {"OPAL_PREFORK_DISABLE" => "1"}
179
182
  out_err, status = Open3.capture2e(env, *argv, chdir: root.to_s)
180
183
  File.write(stderr_log, out_err)
181
184
  abort("homura build: opal failed (see #{stderr_log})") unless status.success?
@@ -186,18 +189,19 @@ def homura_vendor_from_gemfile(project_root)
186
189
  end
187
190
 
188
191
  def run_opal_standalone!(root, opal_input, opal_output, with_db:)
189
- load_paths =
190
- HomuraRuntime::BuildSupport.standalone_load_paths(root, with_db: with_db)
192
+ load_paths = HomuraRuntime::BuildSupport.standalone_load_paths(root, with_db: with_db)
193
+ opal_gems_prelude = root.join("build", "homura_opal_gems.rb")
191
194
 
192
195
  argv = %w[bundle exec opal -c -E --esm --no-source-map]
193
196
  load_paths.each { |p| argv.push("-I", p) }
194
- argv +=
195
- %w[-r opal_patches -r homura/runtime -r app_templates -r app_assets -o] +
196
- [opal_output, opal_input]
197
+ argv += %w[-r opal_patches -r homura/runtime -r app_templates -r app_assets]
198
+ argv += %w[-r homura_opal_gems] if opal_gems_prelude.file?
199
+ argv += %w[-o] +
200
+ [opal_output, opal_input]
197
201
 
198
202
  stderr_log = root.join("build/opal.stderr.log")
199
203
  FileUtils.mkdir_p(root.join("build"))
200
- env = { "OPAL_PREFORK_DISABLE" => "1" }
204
+ env = {"OPAL_PREFORK_DISABLE" => "1"}
201
205
  out_err, status = Open3.capture2e(env, *argv, chdir: root.to_s)
202
206
  File.write(stderr_log, out_err)
203
207
  abort("homura build: opal failed (see #{stderr_log})") unless status.success?
@@ -206,11 +210,58 @@ end
206
210
  def write_entrypoint!(root, out_path, setup:, bundle:, worker_mod:)
207
211
  out_file = root.join(out_path)
208
212
  out_dir = out_file.dirname
213
+ bundle_spec = relative_import_spec(out_dir, root.join(bundle))
214
+ worker_spec = relative_import_spec(out_dir, root.join(worker_mod))
209
215
  body = <<~JS
210
216
  // GENERATED by homura build — do not edit by hand.
211
217
  import "#{relative_import_spec(out_dir, root.join(setup))}";
212
- import "#{relative_import_spec(out_dir, root.join(bundle))}";
213
- export { default, HomuraCounterDO } from "#{relative_import_spec(out_dir, root.join(worker_mod))}";
218
+ import runtimeDefault, { HomuraCounterDO as RuntimeHomuraCounterDO } from "#{worker_spec}";
219
+
220
+ let opalBundleReady;
221
+
222
+ function ensureOpalBundle() {
223
+ opalBundleReady ||= import("#{bundle_spec}");
224
+ return opalBundleReady;
225
+ }
226
+
227
+ export default {
228
+ async fetch(request, env, ctx) {
229
+ await ensureOpalBundle();
230
+ return runtimeDefault.fetch(request, env, ctx);
231
+ },
232
+
233
+ async scheduled(event, env, ctx) {
234
+ await ensureOpalBundle();
235
+ return runtimeDefault.scheduled(event, env, ctx);
236
+ },
237
+
238
+ async queue(batch, env, ctx) {
239
+ await ensureOpalBundle();
240
+ return runtimeDefault.queue(batch, env, ctx);
241
+ }
242
+ };
243
+
244
+ export class HomuraCounterDO extends RuntimeHomuraCounterDO {
245
+ async fetch(request) {
246
+ await ensureOpalBundle();
247
+ return super.fetch(request);
248
+ }
249
+
250
+ async webSocketMessage(ws, message) {
251
+ await ensureOpalBundle();
252
+ return super.webSocketMessage(ws, message);
253
+ }
254
+
255
+ async webSocketClose(ws, code, reason, wasClean) {
256
+ await ensureOpalBundle();
257
+ return super.webSocketClose(ws, code, reason, wasClean);
258
+ }
259
+
260
+ async webSocketError(ws, err) {
261
+ await ensureOpalBundle();
262
+ return super.webSocketError(ws, err);
263
+ }
264
+ }
214
265
  JS
215
266
  File.write(out_file, body)
216
267
  end
@@ -367,7 +418,7 @@ else
367
418
  chdir: root
368
419
  )
369
420
  else
370
- warn "homura build: no app/ directory or top-level app.rb — skipping auto-await"
421
+ warn("homura build: no app/ directory or top-level app.rb — skipping auto-await")
371
422
  end
372
423
 
373
424
  # Also run auto-await over every gem we ship to Opal:
@@ -399,6 +450,8 @@ else
399
450
  end
400
451
  end
401
452
 
453
+ HomuraRuntime::BuildSupport.write_opal_gems_prelude(root)
454
+
402
455
  opal_in = Pathname(resolve_opal_input(root, options[:opal_input]))
403
456
  opal_out = Pathname(options[:opal_output])
404
457
  opal_in = root.join(opal_in) unless opal_in.absolute?
@@ -435,4 +488,4 @@ write_entrypoint!(
435
488
  worker_mod: options[:worker_module_import]
436
489
  )
437
490
 
438
- $stderr.puts "homura build: ok"
491
+ $stderr.puts("homura build: ok")
@@ -56,15 +56,21 @@ module Cloudflare
56
56
 
57
57
  def run(model, inputs = nil, options: nil, **input_options)
58
58
  payload = inputs || input_options
59
- payload = payload.merge(input_options) if inputs.is_a?(Hash) &&
60
- !input_options.empty?
59
+ if inputs.is_a?(Hash) &&
60
+ !input_options.empty?
61
+ payload = payload.merge(input_options)
62
+ end
63
+
61
64
  Cloudflare::AI.run(model, payload, binding: @js, options: options)
62
65
  end
63
66
 
64
67
  def run_stream(model, inputs = nil, **input_options)
65
68
  payload = inputs || input_options
66
- payload = payload.merge(input_options) if inputs.is_a?(Hash) &&
67
- !input_options.empty?
69
+ if inputs.is_a?(Hash) &&
70
+ !input_options.empty?
71
+ payload = payload.merge(input_options)
72
+ end
73
+
68
74
  run(model, payload.merge(stream: true))
69
75
  end
70
76
 
@@ -78,12 +84,11 @@ module Cloudflare
78
84
  )
79
85
  chat_options = Cloudflare::AI.chat_input_options(model, input_options)
80
86
  payload = {
81
- messages:
82
- Cloudflare::AI.build_messages(
83
- prompt,
84
- messages: messages,
85
- system: system
86
- )
87
+ messages: Cloudflare::AI.build_messages(
88
+ prompt,
89
+ messages: messages,
90
+ system: system
91
+ )
87
92
  }.merge(chat_options)
88
93
  run(model, payload, options: options)
89
94
  end
@@ -96,15 +101,14 @@ module Cloudflare
96
101
  options: nil,
97
102
  **input_options
98
103
  )
99
- response =
100
- chat(
101
- prompt,
102
- messages: messages,
103
- system: system,
104
- model: model,
105
- options: options,
106
- **input_options
107
- )
104
+ response = chat(
105
+ prompt,
106
+ messages: messages,
107
+ system: system,
108
+ model: model,
109
+ options: options,
110
+ **input_options
111
+ )
108
112
  response = response.__await__ if Cloudflare.js_promise?(response)
109
113
  Cloudflare::AI.extract_text(response)
110
114
  end
@@ -115,7 +119,7 @@ module Cloudflare
115
119
  options: nil,
116
120
  **input_options
117
121
  )
118
- payload = { audio: Cloudflare::AI.audio_input(audio) }.merge(
122
+ payload = {audio: Cloudflare::AI.audio_input(audio)}.merge(
119
123
  input_options
120
124
  )
121
125
  run(model, payload, options: options)
@@ -127,16 +131,14 @@ module Cloudflare
127
131
  options: nil,
128
132
  **input_options
129
133
  )
130
- response =
131
- transcribe(audio, model: model, options: options, **input_options)
134
+ response = transcribe(audio, model: model, options: options, **input_options)
132
135
  response = response.__await__ if Cloudflare.js_promise?(response)
133
136
  Cloudflare::AI.extract_text(response)
134
137
  end
135
138
 
136
139
  def speak(text, model: DEFAULT_SPEAK_MODEL, options: nil, **input_options)
137
- payload = { text: text.to_s }.merge(input_options)
138
- response =
139
- Cloudflare::AI.speak(model, payload, binding: @js, options: options)
140
+ payload = {text: text.to_s}.merge(input_options)
141
+ response = Cloudflare::AI.speak(model, payload, binding: @js, options: options)
140
142
  response = response.__await__ if Cloudflare.js_promise?(response)
141
143
  response
142
144
  end
@@ -147,14 +149,13 @@ module Cloudflare
147
149
  options: nil,
148
150
  **input_options
149
151
  )
150
- payload = { text: text.to_s }.merge(input_options)
151
- response =
152
- Cloudflare::AI.speak_data_url(
153
- model,
154
- payload,
155
- binding: @js,
156
- options: options
157
- )
152
+ payload = {text: text.to_s}.merge(input_options)
153
+ response = Cloudflare::AI.speak_data_url(
154
+ model,
155
+ payload,
156
+ binding: @js,
157
+ options: options
158
+ )
158
159
  response = response.__await__ if Cloudflare.js_promise?(response)
159
160
  response.to_s
160
161
  end
@@ -169,14 +170,17 @@ module Cloudflare
169
170
  # @param binding [JS object] env.AI binding (required)
170
171
  # @param options [Hash] gateway / extra options forwarded as the 3rd arg
171
172
  def self.run(model, inputs, binding: nil, options: nil, raw_response: false)
172
- binding = binding.js if defined?(Binding) &&
173
- `(#{binding} != null && #{binding}.$$class === #{Binding})`
173
+ if defined?(Binding) &&
174
+ `(#{binding} != null && #{binding}.$$class === #{Binding})`
175
+ binding = binding.js
176
+ end
174
177
  # Use a JS-side null check because `binding` may be a raw JS object
175
178
  # (env.AI), which has no Ruby `#nil?` method on the prototype.
176
179
  bound = !`(#{binding} == null)`
177
180
  unless bound
178
181
  raise AIError.new("AI binding not bound (env.AI is null)", model: model)
179
182
  end
183
+
180
184
  js_inputs = ruby_to_js(inputs)
181
185
  js_options = options ? ruby_to_js(options) : `({})`
182
186
  ai_binding = binding
@@ -186,15 +190,10 @@ module Cloudflare
186
190
  # newer Workers AI shape) or `options: { stream: true }` (the
187
191
  # 3rd-arg "options" contract). Accept both so callers can use
188
192
  # whichever idiom matches the model docs they're following.
189
- streaming =
190
- (
191
- inputs.is_a?(Hash) &&
192
- (inputs[:stream] == true || inputs["stream"] == true)
193
- ) ||
194
- (
195
- options.is_a?(Hash) &&
196
- (options[:stream] == true || options["stream"] == true)
197
- )
193
+ streaming = (inputs.is_a?(Hash) &&
194
+ (inputs[:stream] == true || inputs["stream"] == true)) ||
195
+ (options.is_a?(Hash) &&
196
+ (options[:stream] == true || options["stream"] == true))
198
197
  cf = Cloudflare
199
198
 
200
199
  # NOTE: multi-line backtick → Promise works HERE because the
@@ -204,8 +203,7 @@ module Cloudflare
204
203
  # or the Promise will be silently dropped (same pitfall
205
204
  # documented in lib/homura/runtime/{cache,queue}.rb —
206
205
  # Phase 11B audit).
207
- js_promise =
208
- `
206
+ js_promise = `
209
207
  (async function() {
210
208
  var out;
211
209
  try {
@@ -231,8 +229,11 @@ module Cloudflare
231
229
  end
232
230
 
233
231
  def self.speak(model, inputs, binding: nil, options: nil)
234
- binding = binding.js if defined?(Binding) &&
235
- `(#{binding} != null && #{binding}.$$class === #{Binding})`
232
+ if defined?(Binding) &&
233
+ `(#{binding} != null && #{binding}.$$class === #{Binding})`
234
+ binding = binding.js
235
+ end
236
+
236
237
  bound = !`(#{binding} == null)`
237
238
  unless bound
238
239
  raise AIError.new("AI binding not bound (env.AI is null)", model: model)
@@ -243,8 +244,7 @@ module Cloudflare
243
244
  ai_binding = binding
244
245
  err_klass = Cloudflare::AIError
245
246
 
246
- js_response =
247
- `
247
+ js_response = `
248
248
  (async function() {
249
249
  try {
250
250
  return await #{ai_binding}.run(#{model}, #{js_inputs}, #{js_options});
@@ -252,17 +252,20 @@ module Cloudflare
252
252
  #{Kernel}.$raise(#{err_klass}.$new(e && e.message ? e.message : String(e), Opal.hash({ model: #{model}, operation: 'speak' })));
253
253
  }
254
254
  })()
255
- `.__await__
255
+ `
256
+ .__await__
256
257
 
257
- content_type =
258
- `#{js_response}.headers.get("content-type") || "application/octet-stream"`
258
+ content_type = `#{js_response}.headers.get("content-type") || "application/octet-stream"`
259
259
  cache_control = `#{js_response}.headers.get("cache-control")`
260
260
  BinaryBody.new(`#{js_response}.body`, content_type, cache_control)
261
261
  end
262
262
 
263
263
  def self.speak_data_url(model, inputs, binding: nil, options: nil)
264
- binding = binding.js if defined?(Binding) &&
265
- `(#{binding} != null && #{binding}.$$class === #{Binding})`
264
+ if defined?(Binding) &&
265
+ `(#{binding} != null && #{binding}.$$class === #{Binding})`
266
+ binding = binding.js
267
+ end
268
+
266
269
  bound = !`(#{binding} == null)`
267
270
  unless bound
268
271
  raise AIError.new("AI binding not bound (env.AI is null)", model: model)
@@ -273,8 +276,7 @@ module Cloudflare
273
276
  ai_binding = binding
274
277
  err_klass = Cloudflare::AIError
275
278
 
276
- js_response =
277
- `
279
+ js_response = `
278
280
  (async function() {
279
281
  try {
280
282
  return await #{ai_binding}.run(#{model}, #{js_inputs}, #{js_options});
@@ -282,10 +284,10 @@ module Cloudflare
282
284
  #{Kernel}.$raise(#{err_klass}.$new(e && e.message ? e.message : String(e), Opal.hash({ model: #{model}, operation: 'speak_data_url' })));
283
285
  }
284
286
  })()
285
- `.__await__
287
+ `
288
+ .__await__
286
289
 
287
- content_type =
288
- `#{js_response}.headers.get("content-type") || "application/octet-stream"`
290
+ content_type = `#{js_response}.headers.get("content-type") || "application/octet-stream"`
289
291
  `
290
292
  (async function(resp, ct) {
291
293
  var buf = await resp.arrayBuffer();
@@ -294,19 +296,22 @@ module Cloudflare
294
296
  for (var i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]);
295
297
  return 'data:' + ct + ';base64,' + globalThis.btoa(bin);
296
298
  })(#{js_response}, #{content_type})
297
- `.__await__
299
+ `
300
+ .__await__
298
301
  end
299
302
 
300
303
  def self.build_messages(prompt = nil, messages: nil, system: nil)
301
304
  out = []
302
- out << { role: "system", content: system.to_s } if system
305
+ out << {role: "system", content: system.to_s} if system
303
306
  if messages
304
307
  unless messages.is_a?(Array)
305
308
  raise ArgumentError, "messages must be an Array of chat messages"
306
309
  end
310
+
307
311
  out.concat(messages)
308
312
  end
309
- out << { role: "user", content: prompt.to_s } unless prompt.nil?
313
+
314
+ out << {role: "user", content: prompt.to_s} unless prompt.nil?
310
315
  raise ArgumentError, "chat requires a prompt or messages" if out.empty?
311
316
  out
312
317
  end
@@ -316,10 +321,12 @@ module Cloudflare
316
321
  uint8 = audio.to_uint8_array
317
322
  return `Array.from(#{uint8})`
318
323
  end
324
+
319
325
  return audio if audio.is_a?(Array)
320
326
  if `typeof #{audio} !== 'undefined' && #{audio} instanceof Uint8Array`
321
327
  return `Array.from(#{audio})`
322
328
  end
329
+
323
330
  audio
324
331
  end
325
332
 
@@ -327,12 +334,12 @@ module Cloudflare
327
334
  return input_options unless model.to_s == DEFAULT_CHAT_MODEL
328
335
 
329
336
  options = input_options.dup
330
- key =
331
- if options.key?(:chat_template_kwargs)
332
- :chat_template_kwargs
333
- elsif options.key?("chat_template_kwargs")
334
- "chat_template_kwargs"
335
- end
337
+ key = if options.key?(:chat_template_kwargs)
338
+ :chat_template_kwargs
339
+ elsif options.key?("chat_template_kwargs")
340
+ "chat_template_kwargs"
341
+ end
342
+
336
343
  template_kwargs = key ? options[key] : nil
337
344
  if template_kwargs && !template_kwargs.is_a?(Hash)
338
345
  raise ArgumentError, "chat_template_kwargs must be a Hash"
@@ -354,18 +361,21 @@ module Cloudflare
354
361
  text = message_hash_text(msg)
355
362
  return text unless text.empty?
356
363
  end
364
+
357
365
  if out["messages"].is_a?(Array) && !out["messages"].empty?
358
- msg =
359
- out["messages"].find do |entry|
360
- entry.is_a?(Hash) && entry["role"].to_s == "assistant"
361
- end || out["messages"][0]
366
+ msg = out["messages"].find do |entry|
367
+ entry.is_a?(Hash) && entry["role"].to_s == "assistant"
368
+ end ||
369
+ out["messages"][0]
362
370
  text = message_hash_text(msg)
363
371
  return text unless text.empty?
364
372
  end
373
+
365
374
  %w[text transcription response result output].each do |key|
366
375
  value = message_content_text(out[key])
367
376
  return value unless value.empty?
368
377
  end
378
+
369
379
  nested = out["result"]
370
380
  return extract_text(nested) if nested.is_a?(Hash)
371
381
  ""
@@ -396,6 +406,7 @@ module Cloudflare
396
406
  text = message_content_text(value[key])
397
407
  return text unless text.empty?
398
408
  end
409
+
399
410
  ""
400
411
  end
401
412
 
@@ -405,6 +416,7 @@ module Cloudflare
405
416
  if `#{val} != null && typeof #{val} === 'object' && #{val}.$$class == null`
406
417
  return val
407
418
  end
419
+
408
420
  if val.is_a?(Hash)
409
421
  obj = `({})`
410
422
  val.each do |k, v|
@@ -412,6 +424,7 @@ module Cloudflare
412
424
  jv = ruby_to_js(v)
413
425
  `#{obj}[#{ks}] = #{jv}`
414
426
  end
427
+
415
428
  obj
416
429
  elsif val.is_a?(Array)
417
430
  arr = `([])`
@@ -419,6 +432,7 @@ module Cloudflare
419
432
  jv = ruby_to_js(v)
420
433
  `#{arr}.push(#{jv})`
421
434
  end
435
+
422
436
  arr
423
437
  elsif val.is_a?(Symbol)
424
438
  val.to_s
@@ -457,23 +471,23 @@ module Cloudflare
457
471
  # this file still loads if stream.rb hasn't been required yet
458
472
  # (Phase 11A load-order flip: ai.rb currently loads first).
459
473
  def response_headers
460
- defaults =
461
- (
462
- if defined?(::Cloudflare::SSEStream)
463
- ::Cloudflare::SSEStream::DEFAULT_HEADERS
464
- else
465
- {
466
- "content-type" => "text/event-stream; charset=utf-8",
467
- "cache-control" => "no-cache, no-transform",
468
- "x-accel-buffering" => "no"
469
- }
470
- end
471
- )
474
+ defaults = (
475
+ if defined?(::Cloudflare::SSEStream)
476
+ ::Cloudflare::SSEStream::DEFAULT_HEADERS
477
+ else
478
+ {
479
+ "content-type" => "text/event-stream; charset=utf-8",
480
+ "cache-control" => "no-cache, no-transform",
481
+ "x-accel-buffering" => "no"
482
+ }
483
+ end
484
+ )
472
485
  defaults.merge(@extra_headers)
473
486
  end
474
487
 
475
488
  def each
476
489
  end
490
+
477
491
  def close
478
492
  end
479
493
  end
@@ -487,6 +501,7 @@ module Cloudflare
487
501
  if `typeof #{js_val} === 'string' || typeof #{js_val} === 'number' || typeof #{js_val} === 'boolean'`
488
502
  return js_val
489
503
  end
504
+
490
505
  if `Array.isArray(#{js_val})`
491
506
  out = []
492
507
  len = `#{js_val}.length`
@@ -495,8 +510,10 @@ module Cloudflare
495
510
  out << js_to_ruby(`#{js_val}[#{i}]`)
496
511
  i += 1
497
512
  end
513
+
498
514
  return out
499
515
  end
516
+
500
517
  if `typeof #{js_val} === 'object'`
501
518
  h = {}
502
519
  keys = `Object.keys(#{js_val})`
@@ -507,8 +524,10 @@ module Cloudflare
507
524
  h[k] = js_to_ruby(`#{js_val}[#{k}]`)
508
525
  i += 1
509
526
  end
527
+
510
528
  return h
511
529
  end
530
+
512
531
  js_val
513
532
  end
514
533
  end