Module: Cloudflare::DurableObject
- Defined in:
- lib/homura/runtime/durable_object.rb
Overview
Ruby-side DO class registry + DSL.
The JS side (src/worker.mjs) defines a single ‘HomuraCounterDO` export. When the DO runtime instantiates it and calls its fetch, the JS class hands the call off to `globalThis.HOMURA_DO_DISPATCH(class_name, state, env, request, body_text)`. We dispatch to whichever Ruby handler block was registered for `class_name` via `Cloudflare::DurableObject.define`.
Class Attribute Summary collapse
-
.handlers ⇒ Object
readonly
Returns the value of attribute handlers.
Class Method Summary collapse
- .build_js_response(status, headers, body) ⇒ Object
-
.define(class_name, &block) ⇒ Object
Register a Ruby handler for a DO class.
-
.define_web_socket_handlers(class_name, on_message: nil, on_close: nil, on_error: nil) ⇒ Object
Register a WebSocket-event handler for a DO class.
-
.dispatch_js(class_name, js_state, js_env, js_request, body_text) ⇒ Object
Dispatcher called from the JS DO class.
- .dispatch_ws_close(class_name, js_ws, code, reason, was_clean, js_state, js_env) ⇒ Object
- .dispatch_ws_error(class_name, js_ws, js_error, js_state, js_env) ⇒ Object
-
.dispatch_ws_message(class_name, js_ws, js_message, js_state, js_env) ⇒ Object
WebSocket dispatchers — called from the JS DO class’s ‘webSocketMessage` / `webSocketClose` / `webSocketError` methods.
-
.handler_for(class_name) ⇒ Object
Lookup handler by class name.
-
.install_dispatcher ⇒ Object
Install the JS dispatcher hook.
-
.normalise_response(result) ⇒ Object
Accept common return shapes from the user block: - Array triple [status, headers, body] - Hash headers:, body: - String (200 OK, text/plain).
- .web_socket_handlers_for(class_name) ⇒ Object
Class Attribute Details
.handlers ⇒ Object (readonly)
Returns the value of attribute handlers.
236 237 238 |
# File 'lib/homura/runtime/durable_object.rb', line 236 def handlers @handlers end |
Class Method Details
.build_js_response(status, headers, body) ⇒ Object
361 362 363 364 365 366 367 368 369 370 371 |
# File 'lib/homura/runtime/durable_object.rb', line 361 def self.build_js_response(status, headers, body) js_headers = `({})` headers.each do |k, v| ks = k.to_s vs = v.to_s `#{js_headers}[#{ks}] = #{vs}` end status_int = status.to_i body_str = body.to_s `new Response(#{body_str}, { status: #{status_int}, headers: #{js_headers} })` end |
.define(class_name, &block) ⇒ Object
Register a Ruby handler for a DO class.
Cloudflare::DurableObject.define('HomuraCounterDO') do |state, request|
prev = (state.storage.get('count').__await__ || 0).to_i
if request.path == '/inc'
state.storage.put('count', prev + 1).__await__
[200, { 'content-type' => 'application/json' }, { 'count' => prev + 1 }.to_json]
elsif request.path == '/reset'
state.storage.delete('count').__await__
[200, { 'content-type' => 'application/json' }, '{"reset":true}']
else
[200, { 'content-type' => 'application/json' }, { 'count' => prev }.to_json]
end
end
The block must return a Rack-style triple ‘[status, headers, body]`. `body` may be a String or an object that responds to `to_s`.
256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 |
# File 'lib/homura/runtime/durable_object.rb', line 256 def self.define(class_name, &block) raise ArgumentError, "define requires a block" unless block unless class_name.is_a?(String) raise ArgumentError, "class_name must be a String" end @handlers ||= {} # Wrap via define_method so Opal's `# await: true` picks it up as # async (same trick Sinatra::Scheduled uses for its jobs). method_name = "__do_handler_#{class_name.gsub(/[^A-Za-z0-9_]/, "_")}".to_sym DurableObjectRequestContext.send(:define_method, method_name, &block) unbound = DurableObjectRequestContext.instance_method(method_name) DurableObjectRequestContext.send(:remove_method, method_name) @handlers[class_name] = unbound nil end |
.define_web_socket_handlers(class_name, on_message: nil, on_close: nil, on_error: nil) ⇒ Object
Register a WebSocket-event handler for a DO class. Accepts any combination of ‘on_message: proc { |ws, msg, state| … }`, `on_close: proc { |ws, code, reason, clean, state| … }`, `on_error: proc { |ws, err, state| … }`.
Cloudflare::DurableObject.define_web_socket_handlers('HomuraCounterDO',
on_message: ->(ws, msg, _state) { `#{ws}.send(#{msg})` },
on_close: ->(ws, code, reason, clean, _state) { `#{ws}.close(#{code}, #{reason})` }
)
The callbacks are invoked from ‘webSocketMessage` / `webSocketClose` / `webSocketError` dispatches on the JS DO class (wired by the exported HomuraCounterDO in src/worker.mjs). Return value is ignored — the runtime doesn’t expect a body.
288 289 290 291 292 293 294 295 296 297 298 299 300 301 |
# File 'lib/homura/runtime/durable_object.rb', line 288 def self.define_web_socket_handlers( class_name, on_message: nil, on_close: nil, on_error: nil ) @ws_handlers ||= {} @ws_handlers[class_name] = { on_message: , on_close: on_close, on_error: on_error }.compact nil end |
.dispatch_js(class_name, js_state, js_env, js_request, body_text) ⇒ Object
Dispatcher called from the JS DO class. Returns a JS Promise that resolves to a JS Response.
314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 |
# File 'lib/homura/runtime/durable_object.rb', line 314 def self.dispatch_js(class_name, js_state, js_env, js_request, body_text) handler = handler_for(class_name) if handler.nil? body = { "error" => "no Ruby handler for DurableObject class #{class_name}" }.to_json return( build_js_response(500, { "content-type" => "application/json" }, body) ) end state = DurableObjectState.new(js_state) request = DurableObjectRequest.new(js_request, body_text) env = Cloudflare::Bindings.build_env(js_env) ctx = DurableObjectRequestContext.new(state, env, request) result = handler.bind(ctx).call(state, request) # If the block itself was async (used __await__ internally), its # return value is a Promise — await it so the caller sees the # resolved triple. if `(#{result} != null && typeof #{result}.then === 'function')` result = result.__await__ end status, headers, body = normalise_response(result) build_js_response(status, headers, body) end |
.dispatch_ws_close(class_name, js_ws, code, reason, was_clean, js_state, js_env) ⇒ Object
390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 |
# File 'lib/homura/runtime/durable_object.rb', line 390 def self.dispatch_ws_close( class_name, js_ws, code, reason, was_clean, js_state, js_env ) h = web_socket_handlers_for(class_name) return nil if h.nil? || h[:on_close].nil? state = DurableObjectState.new(js_state) h[:on_close].call(js_ws, code, reason, was_clean, state) nil end |
.dispatch_ws_error(class_name, js_ws, js_error, js_state, js_env) ⇒ Object
406 407 408 409 410 411 412 |
# File 'lib/homura/runtime/durable_object.rb', line 406 def self.dispatch_ws_error(class_name, js_ws, js_error, js_state, js_env) h = web_socket_handlers_for(class_name) return nil if h.nil? || h[:on_error].nil? state = DurableObjectState.new(js_state) h[:on_error].call(js_ws, js_error, state) nil end |
.dispatch_ws_message(class_name, js_ws, js_message, js_state, js_env) ⇒ Object
WebSocket dispatchers — called from the JS DO class’s ‘webSocketMessage` / `webSocketClose` / `webSocketError` methods. Each returns a JS Promise that resolves to undefined.
376 377 378 379 380 381 382 383 384 385 386 387 388 |
# File 'lib/homura/runtime/durable_object.rb', line 376 def self.( class_name, js_ws, , js_state, js_env ) h = web_socket_handlers_for(class_name) return nil if h.nil? || h[:on_message].nil? state = DurableObjectState.new(js_state) h[:on_message].call(js_ws, , state) nil end |
.handler_for(class_name) ⇒ Object
Lookup handler by class name.
308 309 310 |
# File 'lib/homura/runtime/durable_object.rb', line 308 def self.handler_for(class_name) (@handlers || {})[class_name.to_s] end |
.install_dispatcher ⇒ Object
Install the JS dispatcher hook. Idempotent.
Kept as a single-line backtick x-string — Opal’s compiler refuses multi-line backticks as expressions (same constraint documented in ‘lib/homura/runtime/scheduled.rb#install_dispatcher`). Installs FOUR hooks: fetch dispatcher + 3 websocket event dispatchers. Each wraps Ruby exceptions in a console.error so a bad handler doesn’t crash the DO.
422 423 424 425 426 427 428 429 |
# File 'lib/homura/runtime/durable_object.rb', line 422 def self.install_dispatcher mod = self `globalThis.__HOMURA_DO_DISPATCH__ = async function(class_name, state, env, request, body_text) { try { return await #{mod}.$dispatch_js(class_name, state, env, request, body_text == null ? '' : body_text); } catch (err) { try { globalThis.console.error('[Cloudflare::DurableObject] dispatch failed:', err && err.stack || err); } catch (e) {} return new Response(JSON.stringify({ error: String(err && err.message || err) }), { status: 500, headers: { 'content-type': 'application/json' } }); } };` `globalThis.__HOMURA_DO_WS_MESSAGE__ = async function(class_name, ws, message, state, env) { try { await #{mod}.$dispatch_ws_message(class_name, ws, message, state, env); } catch (err) { try { globalThis.console.error('[Cloudflare::DurableObject] ws.message dispatch failed:', err && err.stack || err); } catch (e) {} } };` `globalThis.__HOMURA_DO_WS_CLOSE__ = async function(class_name, ws, code, reason, wasClean, state, env) { try { await #{mod}.$dispatch_ws_close(class_name, ws, code, reason, wasClean, state, env); } catch (err) { try { globalThis.console.error('[Cloudflare::DurableObject] ws.close dispatch failed:', err && err.stack || err); } catch (e) {} } };` `globalThis.__HOMURA_DO_WS_ERROR__ = async function(class_name, ws, err, state, env) { try { await #{mod}.$dispatch_ws_error(class_name, ws, err, state, env); } catch (e2) { try { globalThis.console.error('[Cloudflare::DurableObject] ws.error dispatch failed:', e2 && e2.stack || e2); } catch (_) {} } };` `(function(){var g=globalThis;g.__OPAL_WORKERS__=g.__OPAL_WORKERS__||{};var d=g.__OPAL_WORKERS__.durableObject=g.__OPAL_WORKERS__.durableObject||{};d.dispatch=g.__HOMURA_DO_DISPATCH__;d.wsMessage=g.__HOMURA_DO_WS_MESSAGE__;d.wsClose=g.__HOMURA_DO_WS_CLOSE__;d.wsError=g.__HOMURA_DO_WS_ERROR__;})();` end |
.normalise_response(result) ⇒ Object
Accept common return shapes from the user block:
- Array triple [status, headers, body]
- Hash {status:, headers:, body:}
- String (200 OK, text/plain)
347 348 349 350 351 352 353 354 355 356 357 358 359 |
# File 'lib/homura/runtime/durable_object.rb', line 347 def self.normalise_response(result) if result.is_a?(Array) && result.length == 3 [result[0].to_i, result[1] || {}, result[2].to_s] elsif result.is_a?(Hash) [ (result["status"] || result[:status] || 200).to_i, result["headers"] || result[:headers] || {}, (result["body"] || result[:body] || "").to_s ] else [200, { "content-type" => "text/plain; charset=utf-8" }, result.to_s] end end |
.web_socket_handlers_for(class_name) ⇒ Object
303 304 305 |
# File 'lib/homura/runtime/durable_object.rb', line 303 def self.web_socket_handlers_for(class_name) (@ws_handlers || {})[class_name.to_s] end |