Class: Dommy::WebSocket

Inherits:
Object
  • Object
show all
Includes:
Bridge::Methods, EventTarget
Defined in:
lib/dommy/web_socket.rb

Overview

‘WebSocket` polyfill. Real implementations open a TCP-then-frame connection; dommy exposes an in-memory transport tests drive via the `__*` seams:

ws.__test_simulate_open__               — fires `open`
ws.__test_simulate_message__(data)      — fires `message`
ws.__test_simulate_close__(code, reason) — fires `close`
ws.__test_simulate_error__              — fires `error`
ws.__test_sent_messages__               — array of sent payloads

By default a ‘new WebSocket(url)` auto-opens via microtask so the common pattern (`ws.onopen = …; ws.send(…)`) works without extra setup.

Spec: websockets.spec.whatwg.org/

Defined Under Namespace

Classes: Error

Constant Summary collapse

CONNECTING =
0
OPEN =
1
CLOSING =
2
CLOSED =
3
INLINE_HANDLERS =
%w[open message close error].freeze

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from Bridge::Methods

included

Methods included from EventTarget

#__internal_deliver_event__, #add_event_listener, capture_flag, #deliver_at, #dispatch_event, js_truthy?, #remove_event_listener

Constructor Details

#initialize(window, url, protocols = nil) ⇒ WebSocket

Returns a new instance of WebSocket.



32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
# File 'lib/dommy/web_socket.rb', line 32

def initialize(window, url, protocols = nil)
  @window = window
  @url = url.to_s
  @ready_state = CONNECTING
  @buffered_amount = 0
  @extensions = ""
  @binary_type = "blob"
  # The subprotocol stays "" until the server selects one at the handshake;
  # remember what was requested and adopt the first on open.
  @requested_protocols = Array(protocols).flatten.map(&:to_s)
  @protocol = ""
  @sent_messages = []
  @inline_handlers = {}

  # Auto-open via microtask unless tests disable.
  auto_open = window.globals["__ws_auto_open__"]
  @window.scheduler.queue_microtask(proc { __test_simulate_open__ }) unless auto_open == false
end

Instance Attribute Details

#binary_typeObject

Returns the value of attribute binary_type.



30
31
32
# File 'lib/dommy/web_socket.rb', line 30

def binary_type
  @binary_type
end

#buffered_amountObject (readonly)

Returns the value of attribute buffered_amount.



29
30
31
# File 'lib/dommy/web_socket.rb', line 29

def buffered_amount
  @buffered_amount
end

#extensionsObject (readonly)

Returns the value of attribute extensions.



29
30
31
# File 'lib/dommy/web_socket.rb', line 29

def extensions
  @extensions
end

#protocolObject (readonly)

Returns the value of attribute protocol.



29
30
31
# File 'lib/dommy/web_socket.rb', line 29

def protocol
  @protocol
end

#ready_stateObject (readonly)

Returns the value of attribute ready_state.



29
30
31
# File 'lib/dommy/web_socket.rb', line 29

def ready_state
  @ready_state
end

#urlObject (readonly)

Returns the value of attribute url.



29
30
31
# File 'lib/dommy/web_socket.rb', line 29

def url
  @url
end

Instance Method Details

#__internal_event_parent__Object



184
185
186
# File 'lib/dommy/web_socket.rb', line 184

def __internal_event_parent__
  nil
end

#__js_call__(method, args) ⇒ Object



169
170
171
172
173
174
175
176
177
178
179
180
181
182
# File 'lib/dommy/web_socket.rb', line 169

def __js_call__(method, args)
  case method
  when "send"
    send(args[0])
  when "close"
    close(args[0] || 1000, args[1] || "")
  when "addEventListener"
    add_event_listener(args[0], args[1], args[2])
  when "removeEventListener"
    remove_event_listener(args[0], args[1], args[2])
  when "dispatchEvent"
    dispatch_event(args[0])
  end
end

#__js_get__(key) ⇒ Object

— JS bridge ————————————————-



128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
# File 'lib/dommy/web_socket.rb', line 128

def __js_get__(key)
  case key
  when "url"
    @url
  when "readyState"
    @ready_state
  when "bufferedAmount"
    @buffered_amount
  when "extensions"
    @extensions
  when "protocol"
    @protocol
  when "binaryType"
    @binary_type
  when "CONNECTING"
    CONNECTING
  when "OPEN"
    OPEN
  when "CLOSING"
    CLOSING
  when "CLOSED"
    CLOSED
  else
    @inline_handlers[inline_event_for(key)]
  end
end

#__js_set__(key, value) ⇒ Object



155
156
157
158
159
160
161
162
163
164
165
# File 'lib/dommy/web_socket.rb', line 155

def __js_set__(key, value)
  case key
  when "binaryType"
    self.binary_type = value
  else
    event = inline_event_for(key)
    set_inline_handler(event, value) if event
  end

  nil
end

#__test_sent_messages__Object

— Test seams ————————————————



91
92
93
# File 'lib/dommy/web_socket.rb', line 91

def __test_sent_messages__
  @sent_messages.dup
end

#__test_simulate_close__(code = 1000, reason = "", was_clean: true) ⇒ Object



110
111
112
113
114
115
116
117
118
119
120
# File 'lib/dommy/web_socket.rb', line 110

def __test_simulate_close__(code = 1000, reason = "", was_clean: true)
  @ready_state = CLOSED
  dispatch_event(
    CloseEvent.new(
      "close",
      "code" => code,
      "reason" => reason,
      "wasClean" => was_clean
    )
  )
end

#__test_simulate_error__Object



122
123
124
# File 'lib/dommy/web_socket.rb', line 122

def __test_simulate_error__
  dispatch_event(Event.new("error"))
end

#__test_simulate_message__(data) ⇒ Object



104
105
106
107
108
# File 'lib/dommy/web_socket.rb', line 104

def __test_simulate_message__(data)
  return if @ready_state != OPEN

  dispatch_event(MessageEvent.new("message", "data" => data))
end

#__test_simulate_open__Object



95
96
97
98
99
100
101
102
# File 'lib/dommy/web_socket.rb', line 95

def __test_simulate_open__
  return if @ready_state != CONNECTING

  @ready_state = OPEN
  # The handshake "selects" the first requested subprotocol.
  @protocol = @requested_protocols.first || ""
  dispatch_event(Event.new("open"))
end

#close(code = nil, reason = nil) ⇒ Object

close([code[, reason]]): code must be 1000 or in 3000–4999, and the UTF-8 reason must be ≤ 123 bytes, else throw — matching the WebSocket spec.



70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
# File 'lib/dommy/web_socket.rb', line 70

def close(code = nil, reason = nil)
  unless code.nil?
    c = code.to_i
    unless c == 1000 || c.between?(3000, 4999)
      raise DOMException::InvalidAccessError, "The close code must be 1000 or in 3000-4999, got #{c}."
    end
  end
  if reason && reason.to_s.bytesize > 123
    raise DOMException::SyntaxError, "The close reason must not exceed 123 UTF-8 bytes."
  end
  return if @ready_state == CLOSED || @ready_state == CLOSING

  @ready_state = CLOSING
  final_code = code.nil? ? 1005 : code.to_i
  final_reason = reason.to_s
  @window.scheduler.queue_microtask(proc { __test_simulate_close__(final_code, final_reason) })
  nil
end

#send(data) ⇒ Object



58
59
60
61
62
63
64
65
66
# File 'lib/dommy/web_socket.rb', line 58

def send(data)
  # send() before the connection opens is an InvalidStateError (a
  # DOMException), not a bare Ruby error.
  raise DOMException::InvalidStateError, "WebSocket is not open" if @ready_state == CONNECTING
  return if @ready_state != OPEN # CLOSING/CLOSED silently discard (buffered)

  @sent_messages << data
  nil
end