Skip to content

Commit

Permalink
improve client signals management, update tests
Browse files Browse the repository at this point in the history
  • Loading branch information
PGimenez committed Dec 9, 2024
1 parent 12fd1af commit a747808
Show file tree
Hide file tree
Showing 4 changed files with 63 additions and 68 deletions.
2 changes: 1 addition & 1 deletion Project.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name = "Surge"
uuid = "349ef902-387d-4643-bd1b-125067241d07"
authors = ["Pere Giménez <[email protected]>"]
version = "0.1.1"
version = "0.1.2"

[deps]
HTTP = "cd3eb016-35fb-5094-929b-558a96fad6f3"
Expand Down
5 changes: 2 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,8 @@ effect(() -> println("Counter updated to ", counter()))
effect(() -> println("Countertwo updated to ", countertwo()))
effect(() -> println("Total is ", total()))

map(expose_signal, [counter, countertwo, total, word])

server=start_server(8080)
server = start_server([counter, countertwo, total, word], 8080)
```

Open the file `index.html` in the `example` folder. You'll see the signal values and controls to modify them. When the counter variables are modified, messages will be printed to the REPL.
Expand All @@ -34,7 +33,7 @@ Alternatively, you can send websocket messages as
using HTTP.WebSockets
using JSON

WebSockets.open("ws://localhost:8081") do ws
WebSockets.open("ws://localhost:8080'") do ws
# Send an update message
update_msg = JSON.json(Dict(
"type" => "update",
Expand Down
55 changes: 25 additions & 30 deletions src/Surge.jl
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,12 @@ using JSON
using HTTP

export start_websocket_server, attach_websocket, start_server, stop_server, expose_signal
export Signal, effect, @signal, computed, infalidate, pull!
export Signal, effect, @signal, computed, invalidate, pull!

const signal_map = Dict{Symbol, StateSignals.Signal}()

const connected_clients = Set{HTTP.WebSockets.WebSocket}()

function expose_signal(signal::Signal)
signal_map[signal.id] = signal
return signal
end

function attach_websocket(signal::Signal, ws::HTTP.WebSockets.WebSocket)

effect(() -> begin
Expand All @@ -32,23 +27,19 @@ function attach_websocket(signals::Vector{Signal}, ws::HTTP.WebSockets.WebSocket
end

"""
start_websocket_server(port)
start_websocket_server(signals, port)
Starts the WebSocket server using HTTP.jl on the specified port.
Starts the server that will expose the signals via websockets.
"""
function start_websocket_server(port)
function start_server(signals::Vector{<:Signal}, port::Int; async=false)
map((s) -> signal_map[s.id] = s, signals)
println("Starting WebSocket server on port $port...")
server = HTTP.WebSockets.listen("127.0.0.1", port) do ws
server_task = @async HTTP.WebSockets.listen("127.0.0.1", port) do ws
push!(connected_clients, ws)
client_signal_map = deepcopy(signal_map)
# this function attaches a websocket and triggers first sync
attach_websocket(collect(values(client_signal_map)), ws)
try
# Send initial state of all signals to the new client
for (_, signal) in client_signal_map
msg = JSON.json(Dict("type" => "update", "id" => string(signal.id), "value" => signal()))
WebSockets.send(ws, msg)
end

for msg in ws
handle_message(ws, msg, client_signal_map)
end
Expand All @@ -57,26 +48,30 @@ function start_websocket_server(port)
close(ws)
end
end
return server
!async && return wait(server_task)
return server_task
end

function handle_message(ws::HTTP.WebSockets.WebSocket, msg, client_signal_map)
data = JSON.parse(msg)
if data["type"] == "update"
id = Symbol(data["id"])
val = data["value"]
if haskey(signal_map, id)
client_signal_map[id](val)
end
msg = JSON.json(Dict("type" => "ACK"))
WebSockets.send(ws, msg)
data = JSON.parse(msg)
if data["type"] == "update"
msg = JSON.json(Dict("type" => "ACK"))
WebSockets.send(ws, msg)
id = Symbol(data["id"])
val = data["value"]
if haskey(signal_map, id)
client_signal_map[id](val) #TODO: this shouldn't trigger the ws sync effect
end
elseif data["type"] == "get"
id = Symbol(data["id"])
if haskey(signal_map, id)
signal = client_signal_map[id]
msg = JSON.json(Dict("type" => "update", "id" => string(signal.id), "value" => signal()))
WebSockets.send(ws, msg)
end
end
end

function start_server(port=8080)
ws_server = start_websocket_server(port)
return ws_server
end

"""
stop_server(servers)
Expand Down
69 changes: 35 additions & 34 deletions test/test-websignals.jl
Original file line number Diff line number Diff line change
Expand Up @@ -5,32 +5,24 @@ using JSON
using StateSignals

@testset "Surge tests" begin
@testset "Signal attachment" begin
# Create and attach a signal
counter = Signal(0, :counter)
attached = attach_websocket(counter)

# Test that signal was added to signal_map
@test haskey(Surge.signal_map, counter.id)
@test Surge.signal_map[counter.id] === counter
end

@testset "WebSocket server" begin
# Start server on test port
test_port = 8082
server = start_websocket_server(test_port)

# Create test signal
counter = Signal(42, :test_counter)
attach_websocket(counter)

server = start_server([counter], test_port; async=true)
sleep(1)

# Connect test client
client = HTTP.WebSockets.open("ws://127.0.0.1:$test_port") do ws
# Should receive initial state
msg = JSON.parse(String(HTTP.WebSockets.receive(ws)))
@test msg["type"] == "update"
@test msg["id"] == "test_counter"
@test msg["value"] == 42
answer = JSON.parse(String(HTTP.WebSockets.receive(ws)))
@test answer["type"] == "update"
@test answer["id"] == "test_counter"
@test answer["value"] == 42

# Test sending update from client
update_msg = JSON.json(Dict(
Expand All @@ -39,42 +31,51 @@ using StateSignals
"value" => 100
))
HTTP.WebSockets.send(ws, update_msg)


println("set to 100")
answer = JSON.parse(String(HTTP.WebSockets.receive(ws)))
@show answer
@test answer["type"] == "ACK"
answer = JSON.parse(String(HTTP.WebSockets.receive(ws)))
@show answer
@test answer["value"] == 100

println("get value")
update_msg = JSON.json(Dict(
"type" => "get",
"id" => "test_counter",
))
HTTP.WebSockets.send(ws, update_msg)
answer = JSON.parse(String(HTTP.WebSockets.receive(ws)))
@show answer
@test answer["value"] == 100
# Wait briefly for update to process
sleep(0.1)

# Verify signal was updated
@test counter() == 100

end

# Clean up
Base.schedule(server, InterruptException(); error=true)
stop_server(server)
end

@testset "Server start/stop" begin
# Start both servers
servers = start_server(8083)

# Verify servers are running
@test servers.http.state == :runnable
@test servers.websocket.state == :runnable

# Create and attach test signal
test_signal = Signal("test", :test)
attach_websocket(test_signal)

server = start_server([test_signal], 8083; async=true)
sleep(1)

@test server.state == :runnable

# Stop servers and verify cleanup
stop_server(servers)
stop_server(server)
sleep(0.5)

# Verify cleanup
@test isempty(Surge.client_sockets)
@test isempty(Surge.connected_clients)
@test isempty(Surge.signal_map)

# Verify servers stopped
@show servers.http.state
@show servers.websocket.state
@test servers.http.state == :failed
@test servers.websocket.state == :failed
@test server.state == :failed
end
end

2 comments on commit a747808

@PGimenez
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@JuliaRegistrator
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Registration pull request updated: JuliaRegistries/General/121064

Tip: Release Notes

Did you know you can add release notes too? Just add markdown formatted text underneath the comment after the text
"Release notes:" and it will be added to the registry PR, and if TagBot is installed it will also be added to the
release that TagBot creates. i.e.

@JuliaRegistrator register

Release notes:

## Breaking changes

- blah

To add them here just re-invoke and the PR will be updated.

Tagging

After the above pull request is merged, it is recommended that a tag is created on this repository for the registered package version.

This will be done automatically if the Julia TagBot GitHub Action is installed, or can be done manually through the github interface, or via:

git tag -a v0.1.2 -m "<description of version>" a74780836528a9d90df349617684b92e38c964c6
git push origin v0.1.2

Please sign in to comment.