Skip to content

Commit b279a9b

Browse files
committed
make app a mutable struct, support reconnection and autocompletion
1 parent b81f9f1 commit b279a9b

1 file changed

Lines changed: 80 additions & 27 deletions

File tree

src/GenieTest.jl

Lines changed: 80 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
module GenieTest
22

33
using Test
4-
export App, wait_for, notify_test
4+
export App, wait_for, notify_test, @App
55

66
using Reexport
77
@reexport using Stipple
@@ -10,13 +10,20 @@ using Reexport
1010
using Electron
1111
using UUIDs
1212

13-
struct App
14-
__model__::Union{ReactiveModel, Nothing}
15-
__window__::Union{Window, Nothing}
16-
__priority__::Base.RefValue{Symbol}
13+
abstract type DummyWindow end
14+
Base.run(w::Union{DummyWindow, Window}, code::JSONText) = run(w, json(code))
15+
16+
Base.@kwdef mutable struct App
17+
__model__::Union{ReactiveModel, Nothing} = nothing
18+
__window__::Union{Window, Nothing} = nothing
19+
__priority__::Symbol = :model
20+
__url__::String = ""
21+
__electron_options__::Dict{String, Any} = Dict{String, Any}()
22+
__timeout__::Float64 = 30.0
23+
__port__::Union{Int, Nothing} = nothing
1724
end
1825

19-
App(model::Union{ReactiveModel, Nothing}, window::Union{Window, Nothing}, priority::Symbol = :model) = App(model, window, Ref(:model))
26+
const AppDict = Dict{Any, App}
2027

2128
"""
2229
unproxy(msg::String)
@@ -28,10 +35,10 @@ function unproxy(msg::String)
2835
end
2936

3037
function Base.getproperty(app::App, fieldname::Symbol)
31-
fieldname [:__model__, :__window__, :__priority__] && return getfield(app, fieldname)
38+
fieldname fieldnames(App) && return getfield(app, fieldname)
3239

33-
if app.__priority__[] == :window && app.__window__ !== nothing
34-
run(app.__window__, unproxy("GENIEMODEL['$fieldname']"))
40+
if app.__priority__ == :window && app.__window__ !== nothing
41+
run(app.__window__, unproxy("window?.GENIEMODEL?.['$fieldname']"))
3542
elseif app.__model__ !== nothing
3643
model = getfield(app, :__model__)
3744
if !hasproperty(model, fieldname)
@@ -47,18 +54,16 @@ function Base.getproperty(app::App, fieldname::Symbol)
4754
field isa Reactive ? field[] : field
4855
end
4956
elseif app.__window__ !== nothing
50-
try
51-
run(app.__window__, unproxy("GENIEMODEL['$fieldname']"))
52-
catch _
53-
nothing
54-
end
57+
run(app.__window__, unproxy("window?.GENIEMODEL?.['$fieldname']"))
5558
else
5659
@warn("App has neither model nor window")
5760
end
5861
end
5962

6063
function Base.setproperty!(app::App, fieldname::Symbol, value)
61-
if app.__priority__[] == :window && app.__window__ !== nothing
64+
fieldname fieldnames(App) && return setfield!(app, fieldname, value)
65+
66+
if app.__priority__ == :window && app.__window__ !== nothing
6267
js_value = json(render(value))
6368
run(app.__window__, unproxy("GENIEMODEL['$fieldname'] = $js_value"))
6469
elseif app.__model__ !== nothing
@@ -96,10 +101,10 @@ Base.notify(app::App, msg::AbstractString, type::Union{Nothing, String, Symbol}
96101

97102
"""
98103
App(url::String = "/";
99-
timeout::Int = 30,
104+
timeout::Float64 = 30,
100105
port = nothing,
101106
id::String = string(uuid4()),
102-
frontend::Symbol = :browser,
107+
frontend::Symbol = startswith(url, r"https://"i) || !backend ? :electron : :browser,
103108
backend::Bool = !startswith(url, r"https://"i),
104109
isready::Function = app -> app.isready
105110
)
@@ -109,7 +114,7 @@ Create a Stipple App with optional frontend and backend.
109114
- `url::String = "/"`: URL to open in the frontend. If it does not start with
110115
"http://" or "https://", it is assumed to be "http://localhost:port/".
111116
# Keyword Arguments
112-
- `timeout::Int = 10`: Timeout in seconds to wait for the backend to be ready.
117+
- `timeout::Float64 = 30`: Timeout in seconds to wait for the backend to be ready.
113118
- `port = nothing`: Port where the Genie server is running. If `nothing`,
114119
it uses `Genie.config.server_port`.
115120
- `id::String = string(uuid4())`: Debug ID used to identify the Stipple model
@@ -119,21 +124,22 @@ Create a Stipple App with optional frontend and backend.
119124
opens an Electron window. If `:none`, it does not open any frontend.
120125
- `backend::Bool = true`: Whether to start the backend Stipple model. If `false`,
121126
only the frontend is started. This can be useful for testing remote apps.
122-
- `backend_ready::Function = model -> model.isready[]`: Function to check if the
127+
- `backend_ready::Function = app -> app.isready === true`: Function to check if the
123128
backend is ready. It takes the Stipple model as argument and should return
124129
`true` if the backend is ready. By default, it checks the `isready` field
125130
of the model.
126131
# Returns
127132
An `App` instance containing the backend model and the frontend window.
128133
"""
129134
function App(url::String;
130-
timeout::Int = 10,
135+
timeout::Real = 30,
131136
port = nothing,
132137
id::String = string(uuid4()),
133-
frontend::Symbol = :browser,
134138
backend::Bool = !startswith(url, r"https://"i),
139+
frontend::Symbol = startswith(url, r"https://"i) || !backend ? :electron : :browser,
135140
isready::Function = app -> app.isready === true,
136-
electron_options::Dict{String, <:Any} = Dict{String, Any}()
141+
electron_options::Dict{String, <:Any} = Dict{String, Any}(),
142+
priority::Symbol = :model
137143
)
138144
port === nothing && (port = Genie.config.server_port)
139145
println()
@@ -161,15 +167,17 @@ function App(url::String;
161167
frontend == :none && (model.isready[] = true)
162168
end
163169

164-
app = App(model, win)
170+
app = App(model, win, priority, "$url", electron_options, float(timeout), port)
165171
if model === nothing && win === nothing
166172
@warn("App has neither frontend nor backend")
167173
return app
168174
end
169175
print("Waiting for App to be ready ")
170-
while !isready(app) && time() < t0 + timeout
171-
sleep(1)
172-
print('.')
176+
dt = time() - t0
177+
while !isready(app) && dt < timeout
178+
delay = dt < 1 ? 0.1 : 1
179+
sleep(delay)
180+
delay > 1 && print('.')
173181
end
174182
println()
175183

@@ -185,11 +193,29 @@ end
185193

186194
function App(::Type{T}; kwargs...) where T <: ReactiveModel
187195
model = Stipple.ReactiveTools.init_model(T; kwargs...)
188-
return App(model, nothing)
196+
model.isready[] = true
197+
return App(__model__ = model; kwargs...)
189198
end
190199

191200
App(context::Module) = App(@eval context Stipple.@type)
192201

202+
macro App()
203+
:(App(@__MODULE__))
204+
end
205+
206+
function Base.propertynames(app::App)
207+
if app.__model__ !== nothing
208+
tuple(propertynames(app.__model__)..., fieldnames(App)...)
209+
elseif app.__window__ !== nothing
210+
fnames = run(app.__window__, """
211+
(x => Object.keys(x).filter(k => typeof x[k] !== 'function' && k !== 'WebChannel'))(window.GENIEMODEL || {})
212+
""")
213+
tuple(Symbol.(fnames)..., fieldnames(App)...)
214+
else
215+
fieldnames(App)
216+
end
217+
end
218+
193219
function Base.run(app::App, msg::Union{String, JSONText})
194220
msg isa JSONText && (msg = json(msg))
195221
if app.__window__ !== nothing
@@ -244,4 +270,31 @@ function notify_test(app::App, test::Test.Result, test_str::AbstractString = "Te
244270
notify_test(app.__model__, test, test_str)
245271
end
246272

273+
function connect!(app::App; timeout = nothing, port = nothing, isready::Function = app -> app.isready === true)
274+
if app.__window__ !== nothing && !app.__window__.exists
275+
@info "App window appears to be closed. Recreating the window..."
276+
try
277+
a = App(
278+
app.__url__,
279+
frontend = :electron,
280+
electron_options = app.__electron_options__,
281+
priority = app.__priority__,
282+
backend = app.__model__ !== nothing,
283+
timeout = timeout === nothing ? app.__timeout__ : timeout,
284+
port = port === nothing ? app.__port__ : port,
285+
)
286+
app.__model__ = a.__model__
287+
app.__window__ = a.__window__
288+
app.__port__ = a.__port__
289+
app.__timeout__ = a.__timeout__
290+
true
291+
catch e
292+
@warn "Failed to recreate app window: $e"
293+
false
294+
end
295+
else
296+
true
297+
end
298+
end
299+
247300
end # GenieTest

0 commit comments

Comments
 (0)