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
73 changes: 55 additions & 18 deletions lib/quickbeam/runtime.ex
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,14 @@ defmodule QuickBEAM.Runtime do
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 @@ defmodule QuickBEAM.Runtime do
"__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 @@ defmodule QuickBEAM.Runtime do
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 @@ -691,24 +706,10 @@ defmodule QuickBEAM.Runtime do
def handle_info({:DOWN, ref, :process, _pid, reason}, state) do
case find_worker_by_ref(state.workers, ref) do
{worker_id, _child_pid} ->
workers = Map.delete(state.workers, worker_id)

unless reason == :normal do
message = inspect(reason)
QuickBEAM.Native.send_message(state.resource, ["__worker_err", worker_id, message])
end

{:noreply, %{state | workers: workers}}
handle_worker_down(worker_id, reason, state)

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

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

Expand All @@ -727,8 +728,44 @@ defmodule QuickBEAM.Runtime do
end)
end

defp handle_worker_down(worker_id, reason, state) do
workers = Map.delete(state.workers, worker_id)

unless reason == :normal do
message = inspect(reason)
QuickBEAM.Native.send_message(state.resource, ["__worker_err", worker_id, message])
end

{:noreply, %{state | workers: workers}}
end

defp handle_non_worker_down(ref, reason, state) do
case Map.pop(state.websockets, ref) do
{{_pid, _socket_id}, websockets} ->
{:noreply, %{state | websockets: websockets}}

{nil, websockets} ->
handle_monitored_down(ref, reason, %{state | websockets: websockets})
end
end

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

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

@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