Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -349,7 +349,7 @@ Standard browser APIs backed by BEAM primitives, not JS polyfills:
| `document`, `querySelector`, `createElement` | lexbor (native C DOM) |
| `URL`, `URLSearchParams` | `:uri_string` |
| `EventSource` (SSE) | `:httpc` streaming |
| `WebSocket` | `:gun` |
| `WebSocket` | `Mint.WebSocket` |
| `Worker` | BEAM process per worker |
| `BroadcastChannel` | `:pg` (distributed) |
| `navigator.locks` | GenServer + monitors |
Expand Down
2 changes: 1 addition & 1 deletion docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ original BEAM term.
The runtime loads different polyfill sets based on the `:apis` option:

- **`:browser`** (default) — Web APIs backed by OTP: fetch (`:httpc`),
URL (`:uri_string`), crypto.subtle (`:crypto`), WebSocket (`:gun`),
URL (`:uri_string`), crypto.subtle (`:crypto`), WebSocket (`Mint.WebSocket`),
Worker (BEAM processes), BroadcastChannel (`:pg`), localStorage (ETS),
navigator.locks (GenServer), DOM (lexbor), streams, events, etc.

Expand Down
2 changes: 1 addition & 1 deletion docs/javascript-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ Loaded by default. These are Web platform APIs backed by OTP.
|---|---|
| `BroadcastChannel` | `:pg` (distributed process groups) |
| `MessageChannel` / `MessagePort` | — |
| `WebSocket` | `:gun` |
| `WebSocket` | `Mint.WebSocket` |
| `EventSource` | `:httpc` streaming |
| `Worker` | Spawns a separate BEAM GenServer with its own JS runtime |

Expand Down
49 changes: 47 additions & 2 deletions lib/quickbeam/context.ex
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ defmodule QuickBEAM.Context do
handlers: %{},
pending: %{},
workers: %{},
websockets: %{},
next_worker_id: 1
]

Expand All @@ -55,6 +56,7 @@ defmodule QuickBEAM.Context do
handlers: map(),
pending: map(),
workers: map(),
websockets: map(),
next_worker_id: pos_integer()
}

Expand Down Expand Up @@ -411,6 +413,28 @@ defmodule QuickBEAM.Context do
{:context_worker, action} ->
handle_worker_call(action, args, call_id, state)

{:with_caller, fun} ->
caller = self()

Task.start(fn ->
try do
args = if is_list(args), do: args, else: [args]
result = fun.(args, caller)

QuickBEAM.Native.pool_resolve_call_term(resource, context_id, call_id, result)
rescue
e ->
QuickBEAM.Native.pool_reject_call_term(
resource,
context_id,
call_id,
Exception.message(e)
)
end
end)

{:noreply, state}

handler ->
Task.start(fn ->
try do
Expand Down Expand Up @@ -462,9 +486,26 @@ defmodule QuickBEAM.Context do
{:noreply, state}
end

def handle_info({:websocket_started, socket_id, pid}, state) do
ref = Process.monitor(pid)
websockets = Map.put(state.websockets, ref, {pid, socket_id})
{:noreply, %{state | websockets: websockets}}
end

def handle_info({:websocket_event, message}, state) do
QuickBEAM.Native.pool_send_message(state.pool_resource, state.context_id, message)
{:noreply, state}
end

def handle_info({:DOWN, ref, :process, _pid, _reason}, state) do
{_worker_id, workers} = Map.pop(state.workers, ref)
{:noreply, %{state | workers: workers}}
case Map.pop(state.workers, ref) do
{nil, workers} ->
{_socket, websockets} = Map.pop(state.websockets, ref)
{:noreply, %{state | workers: workers, websockets: websockets}}

{_worker_id, workers} ->
{:noreply, %{state | workers: workers}}
end
end

def handle_info({ref, result}, state) when is_reference(ref) do
Expand All @@ -481,6 +522,10 @@ defmodule QuickBEAM.Context do
Process.exit(pid, :shutdown)
end

for {_ref, {pid, _id}} <- state.websockets do
Process.exit(pid, :shutdown)
end

QuickBEAM.Native.pool_destroy_context(state.pool_resource, state.context_id)
:ok
end
Expand Down
41 changes: 33 additions & 8 deletions lib/quickbeam/runtime.ex
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,14 @@
require Logger

@enforce_keys [:resource]
defstruct [:resource, handlers: %{}, monitors: %{}, workers: %{}, pending: %{}]
defstruct [:resource, handlers: %{}, monitors: %{}, workers: %{}, websockets: %{}, pending: %{}]

@type t :: %__MODULE__{
resource: reference(),
handlers: map(),
monitors: map(),
workers: map(),
websockets: map(),
pending: map()
}

Expand Down Expand Up @@ -173,7 +174,10 @@
"__storage_key" => &QuickBEAM.Storage.key/1,
"__storage_length" => &QuickBEAM.Storage.length/1,
"__eventsource_open" => {:with_caller, &QuickBEAM.EventSource.open/2},
"__eventsource_close" => &QuickBEAM.EventSource.close/1
"__eventsource_close" => &QuickBEAM.EventSource.close/1,
"__ws_connect" => {:with_caller, &QuickBEAM.WebSocket.connect/2},
"__ws_send" => &QuickBEAM.WebSocket.send_frame/1,
"__ws_close" => &QuickBEAM.WebSocket.close/1
}

@beam_handlers %{
Expand Down Expand Up @@ -656,6 +660,17 @@
end
end

def handle_info({:websocket_started, socket_id, pid}, state) do
ref = Process.monitor(pid)
websockets = Map.put(state.websockets, ref, {pid, socket_id})
{:noreply, %{state | websockets: websockets}}
end

def handle_info({:websocket_event, message}, state) do
QuickBEAM.Native.send_message(state.resource, message)
{:noreply, state}
end

def handle_info({:eventsource_open, id}, state) do
QuickBEAM.Native.send_message(state.resource, ["__eventsource_open", id])
{:noreply, state}
Expand Down Expand Up @@ -701,13 +716,19 @@
{:noreply, %{state | workers: workers}}

nil ->
case Map.pop(state.monitors, ref) do
{nil, _} ->
{:noreply, state}
case Map.pop(state.websockets, ref) do
{nil, websockets} ->
case Map.pop(state.monitors, ref) do

Check warning on line 721 in lib/quickbeam/runtime.ex

View workflow job for this annotation

GitHub Actions / test (27.0, 1.18)

Function body is nested too deep (max depth is 2, was 3).
{nil, _} ->
{:noreply, %{state | websockets: websockets}}

{callback_id, monitors} ->
QuickBEAM.Native.send_message(state.resource, ["__qb_down", callback_id, reason])
{:noreply, %{state | monitors: monitors, websockets: websockets}}
end

{callback_id, monitors} ->
QuickBEAM.Native.send_message(state.resource, ["__qb_down", callback_id, reason])
{:noreply, %{state | monitors: monitors}}
{_socket, websockets} ->
{:noreply, %{state | websockets: websockets}}
end
end
end
Expand All @@ -729,6 +750,10 @@

@impl true
def terminate(_reason, %{resource: resource} = state) do
for {_ref, {pid, _id}} <- state.websockets do
Process.exit(pid, :shutdown)
end

drain_beam_calls(resource, state.handlers)
QuickBEAM.Native.stop_runtime(resource)
:ok
Expand Down
Loading
Loading