March Docs

IslandSocket

IslandSocket — WebSocket transport for server-side island state management.

Each island instance is registered with an update, render, and optional handle_child_event function. The socket routes messages between the browser and the registered handlers.

Client messages (JSON):

{"island":"Counter-1","type":"init","module":"Counter","payload":{...}}
  Register a new island instance with initial state.

{"island":"Counter-1","type":"msg","payload":"Increment"}
  Forward a message to an existing island instance.

{"island":"Counter-1","type":"child_event","event":"item_toggled","payload":{...}}
  A child island dispatched an event upward. Route to the parent island's
  handle_child_event function. The "island" field here is the PARENT's ID.

{"island":"Counter-1","type":"optimistic","payload":{...}}
  Client sends locally-computed state for CRDT reconciliation.

{"island":"Counter-1","type":"destroy"}
  Remove an island instance.

Server responses (JSON):

{"island":"Counter-1","type":"render","payload":"<html>..."}
  Server-rendered HTML after a state change.

{"island":"Counter-1","type":"state","payload":{...}}
  State update (sent alongside render for client-side state tracking).

{"island":"Counter-1","type":"merge","payload":{...}}
  Merged state (CRDT reconciliation response).

{"island":"child-id","type":"props","payload":{...}}
  Parent pushes new props to a child island after its own state changed.

Usage:

let registry = IslandSocket.new_registry()
               |> IslandSocket.register("Counter", counter_update, counter_render)

HttpServer.new(4000)
|> HttpServer.plug(IslandSocket.plug(IslandSocket.default_config(), registry))
|> HttpServer.listen()

See also: island_view.march, island_assets.march, islands.march.

Types

typeSocketConfigSocketConfig = SocketConfig(String, List(String))#
typeIslandHandlerIslandHandler = IslandHandler(#
typeRegistryRegistry = Registry(List(IslandHandler))#
typeIslandEntryIslandEntry = IslandEntry(String, String)#
typeIslandStateIslandState = IslandState(List((String, IslandEntry)))#

Functions

fnconfig_allowed_originsconfig_allowed_origins(cfg : SocketConfig) : List(String)#

Return the allowed cross-origin list from a config.

fnconfig_pathconfig_path(cfg : SocketConfig) : String#

Return the path from a config.

fndefault_configdefault_config() : SocketConfig#

Create a default config listening on /_bastion/ws with same-origin only.

fnempty_storeempty_store() : IslandState#

Create an empty island state store. Used to initialise a fresh connection.

fnfind_handlerfind_handler(reg : Registry, name : String) : Option(IslandHandler)#

Look up a handler by module name. Returns Some(handler) or None.

fnhandler_dataflowhandler_dataflow(h : IslandHandler) : String#

Return the dataflow mode string from an island handler.

fnhas_event_handlerhas_event_handler(reg : Registry, name : String) : Bool#

Return true if the registry has a child event handler registered for the given island.

fnhas_islandhas_island(store : IslandState, id : String) : Bool#

Return true if the store has an island registered with the given instance ID.

fnmake_configmake_config(path : String) : SocketConfig#

Create a config with a custom WebSocket path (same-origin only).

fnmake_config_with_originsmake_config_with_origins(path : String, origins : List(String)) : SocketConfig#

Create a config that allows WebSocket upgrades from an explicit list of cross-origin clients (in addition to same-origin).

Example:
  IslandSocket.make_config_with_origins("/_bastion/ws", ["https://admin.example.com"])
fnnew_registrynew_registry() : Registry#

Create an empty island registry.

fnplugplug(cfg : SocketConfig, registry : Registry) : Conn -> Conn#

Build a Conn -> Conn plug that upgrades matching requests to an island WebSocket with server-side rendering.

Rejects WebSocket handshakes with a missing Origin header, or an Origin
that does not match the request Host and is not on the config's
allowed-origin list.  This defends against cross-site WebSocket hijacking:
browsers do not enforce the same-origin policy on WebSocket connections,
so the server must validate the Origin itself.
fnprocess_msg_for_testprocess_msg_for_test(text : String, store : IslandState, registry : Registry) : (IslandState, List(String))#

Process a single JSON message against the given store and registry, returning the new store and a list of JSON response strings.

This is the pure, side-effect-free equivalent of the live WebSocket loop.
Use it in tests to exercise the full bidirectional message protocol without
a live WebSocket connection.

Example:
  let reg = IslandSocket.new_registry()
            |> IslandSocket.register("Counter", counter_update, counter_render)
  let store = IslandSocket.empty_store()
  let init = "{\"island\":\"c-1\",\"type\":\"init\",\"module\":\"Counter\",\"payload\":{\"count\":0}}"
  let (store2, responses) = IslandSocket.process_msg_for_test(init, store, reg)
  -- responses contains render + state JSON strings
fnregisterregister(reg : Registry, name : String, update_fn : (String, String) -> String, render_fn : String -> String) : Registry#

Register an island module with its update and render functions.

Defaults to Client dataflow mode.

update_fn takes (state_json, msg_json) and returns new state_json.
render_fn takes state_json and returns an HTML string.

Example:
  IslandSocket.register(registry, "Counter", counter_update, counter_render)
fnregister_fullregister_full(reg : Registry, name : String, update_fn : (String, String) -> String, render_fn : String -> String, merge_fn : (String, String) -> String, child_fn : (String, String, String) -> String) : Registry#

Register an island module with both a merge function and a handle_child_event function.

fnregister_with_child_handlerregister_with_child_handler(reg : Registry, name : String, update_fn : (String, String) -> String, render_fn : String -> String, child_fn : (String, String, String) -> String) : Registry#

Register an island module with a handle_child_event function.

child_fn receives (state_json, event_name, payload_json) and returns the new
parent state_json. This is called when a child island dispatches an event
upward via the runtime's parent-child tree-walk.
fnregister_with_dataflowregister_with_dataflow(reg : Registry, name : String, update_fn : (String, String) -> String, render_fn : String -> String, mode : String) : Registry#

Register an island module with an explicit dataflow mode string.

Use "server" for display-only islands that receive state exclusively
via the channel. Use "client" (the default) for interactive islands.
fnregister_with_event_handlerregister_with_event_handler(reg : Registry, name : String, update_fn : (String, String) -> String, render_fn : String -> String, child_fn : (String, String, String) -> String) : Registry#

Register an island module with a handle_child_event function (alias for register_with_child_handler).

Used when the parent island needs to receive events dispatched upward by
child islands. child_fn receives (state_json, event_name, payload_json).
fnregister_with_mergeregister_with_merge(reg : Registry, name : String, update_fn : (String, String) -> String, render_fn : String -> String, merge_fn : (String, String) -> String) : Registry#

Register an island module with a custom merge function for CRDT reconciliation.

merge_fn takes (local_state_json, remote_state_json) and returns merged state_json.