Module: Cloudflare::DurableObject
- Defined in:
- lib/cloudflare_workers/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.
205 206 207 |
# File 'lib/cloudflare_workers/durable_object.rb', line 205 def handlers @handlers end |
Class Method Details
.build_js_response(status, headers, body) ⇒ Object
317 318 319 320 321 322 323 324 325 326 327 |
# File 'lib/cloudflare_workers/durable_object.rb', line 317 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`.
225 226 227 228 229 230 231 232 233 234 235 236 237 |
# File 'lib/cloudflare_workers/durable_object.rb', line 225 def self.define(class_name, &block) raise ArgumentError, 'define requires a block' unless block raise ArgumentError, 'class_name must be a String' unless class_name.is_a?(String) @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.
254 255 256 257 258 259 260 261 262 |
# File 'lib/cloudflare_workers/durable_object.rb', line 254 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.
275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 |
# File 'lib/cloudflare_workers/durable_object.rb', line 275 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) ctx = DurableObjectRequestContext.new(state, js_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
340 341 342 343 344 345 346 |
# File 'lib/cloudflare_workers/durable_object.rb', line 340 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
348 349 350 351 352 353 354 |
# File 'lib/cloudflare_workers/durable_object.rb', line 348 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.
332 333 334 335 336 337 338 |
# File 'lib/cloudflare_workers/durable_object.rb', line 332 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.
269 270 271 |
# File 'lib/cloudflare_workers/durable_object.rb', line 269 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/cloudflare_workers/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.
364 365 366 367 368 369 370 371 |
# File 'lib/cloudflare_workers/durable_object.rb', line 364 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)
303 304 305 306 307 308 309 310 311 312 313 314 315 |
# File 'lib/cloudflare_workers/durable_object.rb', line 303 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
264 265 266 |
# File 'lib/cloudflare_workers/durable_object.rb', line 264 def self.web_socket_handlers_for(class_name) (@ws_handlers || {})[class_name.to_s] end |