Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refresh client hourly when inactive #4648

Merged
26 changes: 26 additions & 0 deletions client/src/api/API.res
Expand Up @@ -261,6 +261,32 @@ let saveTest = (m: model): cmd =>
x,
))

let fetchServerBuildHash = (m: model): cmd => {
let url = "https://editor.darklang.com/latest-backend-build-hash"

let request = Tea.Http.request({
method: "GET",
headers: list{clientVersionHeader(m)},
url: url,
body: Web.XMLHttpRequest.EmptyBody,
expect: Tea.Http.expectStringResponse(Decoders.wrapExpect(Json.Decode.string)),
timeout: None,
withCredentials: false,
})

// If origin is https://darklang.com, then we're in prod (or ngrok, running against
// prod) and editor.darklang.com's CORS rules will allow this request. If not,
// we're in local, and both CORS and auth (session, canvas_id) will not work
// against editor.darklang.com. By putting the conditional here instead of at the
// beginning of the function, we still exercise the message and request generating
// code locally.
if m.origin == "https://darklang.com" {
Tea.Http.send(serverBuildHash => AppTypes.Msg.RefreshClientIfOutdated(serverBuildHash), request)
} else {
Tea.Cmd.none
}
}

let integration = (m: model, name: string): cmd =>
apiCallPreloaded(
m,
Expand Down
9 changes: 7 additions & 2 deletions client/src/app/AppTypes.res
Expand Up @@ -55,7 +55,7 @@ module CanvasProps = {
module PageVisibility = {
@ppx.deriving(show({with_path: false}))
type rec t =
| Hidden
| Hidden(Js.Date.t)
StachuDotNet marked this conversation as resolved.
Show resolved Hide resolved
| Visible
}

Expand Down Expand Up @@ -734,6 +734,7 @@ module Msg = {
| UpdateHeapio(Types.heapioTrack)
| SettingsMsg(Settings.msg)
| SecretMsg(SecretTypes.msg)
| RefreshClientIfOutdated(result<string, Types.httpError>)

let toDebugString = (msg: t<'model, 'cmd>): string =>
switch msg {
Expand Down Expand Up @@ -839,6 +840,7 @@ module Msg = {
| SaveTestAPICallback(_) => "SaveTestAPICallback"
| GetUnlockedDBsAPICallback(_) => "GetUnlockedDBsAPICallback"
| Get404sAPICallback(_) => "Get404sAPICallback"
| RefreshClientIfOutdated(_) => "RefreshClientIfOutdated"
}
}

Expand Down Expand Up @@ -877,6 +879,8 @@ module Modification = {
'model => ('model, Tea.Cmd.t<Msg.t<'model, t<'model>>>),
)

| NoChange

// API Calls
| AddOps((list<PT.Op.t>, Focus.t))
| HandleAPIError(APIError.t)
Expand All @@ -887,6 +891,7 @@ module Modification = {
| TriggerHandlerAPICall(TLID.t)
| UpdateDBStatsAPICall(TLID.t)
| DeleteToplevelForeverAPICall(TLID.t)
| GetServerBuildHash
// End API Calls
| Select(TLID.t, tlidSelectTarget)
| SetHover(TLID.t, Types.idOrTraceID)
Expand All @@ -907,7 +912,6 @@ module Modification = {
| EnterWithOffset(TLID.t, ID.t, int) // Entering a blankOr with a desired caret offset
| OpenOmnibox(option<Pos.t>) // Open the omnibox
| UpdateWorkerSchedules(Tc.Map.String.t<AnalysisTypes.WorkerState.t>)
| NoChange
| MakeCmd(Tea.Cmd.t<Msg.t<'model, t<'model>>>)
| AutocompleteMod(AutoComplete.mod)
| Many(list<t<'model>>)
Expand Down Expand Up @@ -965,6 +969,7 @@ module Modification = {
| TriggerHandlerAPICall(_) => "TriggerHandlerAPICall"
| UpdateDBStatsAPICall(_) => "UpdateDBStatsAPICall"
| DeleteToplevelForeverAPICall(_) => "DeleteToplevelForeverAPICall"
| GetServerBuildHash => "GetServerBuildHash"
| Select(_, _) => "Select"
| SetHover(_, _) => "SetHover"
| ClearHover(_, _) => "ClearHover"
Expand Down
66 changes: 58 additions & 8 deletions client/src/app/Main.res
Expand Up @@ -418,6 +418,7 @@ let rec updateMod = (mod: modification, (m, cmd): (model, AppTypes.cmd)): (model
| UpdateDBStatsAPICall(tlid) => Analysis.updateDBStats(m, tlid)
| GetWorkerStatsAPICall(tlid) => Analysis.getWorkerStats(m, tlid)
| DeleteToplevelForeverAPICall(tlid) => (m, API.deleteToplevelForever(m, {tlid: tlid}))
| GetServerBuildHash => (m, API.fetchServerBuildHash(m))
| NoChange => (m, Cmd.none)
| TriggerIntegrationTest(name) =>
let expect = IntegrationTest.trigger(name)
Expand Down Expand Up @@ -1832,8 +1833,8 @@ let update_ = (msg: msg, m: model): modification => {
| TimerFire(action, _) =>
switch action {
| RefreshAnalysis =>
let getUnlockedDBs = // Small optimization
if Map.length(m.dbs) > 0 {
// Small optimization - only refresh unlocked DBs if there's at least 1 known
let getUnlockedDBs = if !Map.isEmpty(m.dbs) {
GetUnlockedDBsAPICall
} else {
NoChange
Expand All @@ -1854,6 +1855,8 @@ let update_ = (msg: msg, m: model): modification => {
| _ => getUnlockedDBs
}
| RefreshAvatars => ExpireAvatars

| CheckIfClientIsOutdated => GetServerBuildHash
| _ => NoChange
}
| IgnoreMsg(_) =>
Expand All @@ -1864,7 +1867,16 @@ let update_ = (msg: msg, m: model): modification => {
| PageVisibilityChange(vis) =>
ReplaceAllModificationsWithThisOne(
"PageVisibilityChange",
m => ({...m, visibility: vis}, Cmd.none),
m => {
let newVis = switch (vis, m.visibility) {
| (Hidden(existingTimestamp), Hidden(_newTimestamp)) =>
// ensure we don't "overwrite" the date at which the page was 'hidden'
AppTypes.PageVisibility.Hidden(existingTimestamp)
| _ => vis
}

({...m, visibility: newVis}, Cmd.none)
},
)
| CreateHandlerFrom404({space, path, modifier, _} as fof) =>
let center = Viewport.findNewPos(m)
Expand Down Expand Up @@ -2121,6 +2133,34 @@ let update_ = (msg: msg, m: model): modification => {
| UploadFnAPICallback(_, Ok(_)) =>
Model.updateErrorMod(Error.set("Successfully uploaded function"))
| SecretMsg(msg) => InsertSecret.update(msg)

| RefreshClientIfOutdated(Error(_err)) => NoChange
| RefreshClientIfOutdated(Ok(serverHash)) =>
let hasBeenInactiveForPastHour = switch m.visibility {
| Visible => false
| Hidden(since) =>
let oneHourAgo = Js.Date.now() -. 60.0 *. 60.0 *. 1000.0 |> Js.Date.fromFloat
since < oneHourAgo
}

let isPageSafelyRefreshable = switch m.currentPage {
| FocusedPackageManagerFn(_)
| Architecture => true
| FocusedFn(_)
| FocusedDB(_)
| FocusedType(_)
| SettingsModal(_)
| FocusedHandler(_) => false
}

let hashesMatch = m.buildHash == serverHash

if hasBeenInactiveForPastHour && isPageSafelyRefreshable && !hashesMatch {
Webapi.Dom.location->Webapi.Dom.Location.reload
}

NoChange

| RenderEvent =>
ReplaceAllModificationsWithThisOne(
"RenderEvent",
Expand Down Expand Up @@ -2203,8 +2243,17 @@ let subscriptions = (m: model): Tea.Sub.t<msg> => {
}

let timers = if m.editorSettings.runTimers {
let refreshOutdatedClient = Tea.Time.every(
~key="refresh_outdated_client",
Tea.Time.minute,
f => AppTypes.Msg.TimerFire(CheckIfClientIsOutdated, f),
)

// Note: putting a timer in the 'hidden' list doesn't prevent it from being run
// only when the page is invisible. Rather, it only prevents us from _starting_
// the timer until the page is invisible.
switch m.visibility {
| Hidden => list{}
| Hidden(_since) => list{refreshOutdatedClient}
| Visible => list{
Tea.Time.every(~key="refresh_analysis", Tea.Time.second, f => AppTypes.Msg.TimerFire(
RefreshAnalysis,
Expand All @@ -2214,6 +2263,7 @@ let subscriptions = (m: model): Tea.Sub.t<msg> => {
RefreshAvatars,
f,
)),
refreshOutdatedClient,
}
}
} else {
Expand All @@ -2225,11 +2275,11 @@ let subscriptions = (m: model): Tea.Sub.t<msg> => {
}

let visibility = list{
BrowserSubscriptions.Window.OnFocusChange.listen(~key="window_on_focus_change", v =>
if v {
PageVisibilityChange(Visible)
BrowserSubscriptions.Window.OnFocusChange.listen(~key="window_on_focus_change", hidden =>
if hidden {
PageVisibilityChange(Hidden(Js.Date.now() |> Js.Date.fromFloat))
} else {
PageVisibilityChange(Hidden)
PageVisibilityChange(Visible)
}
),
}
Expand Down
8 changes: 7 additions & 1 deletion client/src/components/Settings/SettingsContributingView.res
Expand Up @@ -82,7 +82,13 @@ let viewTunnelSectionHeader = {
Html.a(list{Attrs.href("https://localtunnel.me")}, list{Html.text("localtunnel")}),
Html.text(" or "),
Html.a(list{Attrs.href("https://ngrok.com")}, list{Html.text("ngrok")}),
Html.text(") to use your local client against the Darklang production API"),
Html.text(") to use your local client against the Darklang production API. "),
Html.text("See the "),
Html.a(
list{Attrs.href("https://docs.darklang.com/contributing/client-asset-tunnelling")},
list{Html.text("contributor docs")},
),
Html.text(" for instructions."),
}),
}
}
Expand Down
1 change: 1 addition & 0 deletions client/src/core/Types.res
Expand Up @@ -138,6 +138,7 @@ and timerAction =
| RefreshAnalysis
| RefreshAvatars
| CheckUrlHashPosition
| CheckIfClientIsOutdated

and traceID = string

Expand Down