SAFE Stack の ToDo アプリはデータを保存できるようにする続き。

クライアントで周期タスクを動作できるようになった (link) ので、これを利用してデータを自動保存するようにした。 周期タスクで最後にデータを更新してから 3 秒 (SaveWaitMsec で指定) を経過以上しているかチェックして、該当したらサーバの API を呼び出してデータを保存する。 また、ユーザの手動で指示するための Save ボタンは不要になったので、セーブの状態表示だけするようにした。

サーバでおこなうデータ保存を自動化するためのトリガを、クライアントの周期タスクに頼るのがよいかというと、そこは歪だと自覚している。 ただ、機能の実現方法をあれこれ試しているなかで、クライアントがユーザ発のイベントに頼らずに背後で処理する仕掛けをとにかく何か 1 つでも確立しておきたかった。

もちろん、サーバの方でもクライアントからの API 呼び出しとは独立して処理することが必要になるケースもある。 サーバは素直な .NET 環境なのでとくに心配無いと思っている。 そのため今回は、Web ブラウザ、Fable、Elmish といった環境の制約が多そうなクライアントで実験してみた。

src/Client/Index.fs の変更

diff --git a/src/Client/Index.fs b/src/Client/Index.fs
--- a/src/Client/Index.fs
+++ b/src/Client/Index.fs
@@ -49,9 +49,11 @@ type SaveState =
 type Model = {
     PeriodicTaskItvlMsec: int;
     PeriodicTaskCount: int;
+    SaveState: SaveState;
+    SaveWaitMsec: int;
+    LastChanged: System.DateTime;
     Todos: Todo list;
     Input: string;
-    SaveState: SaveState;
     DragFrom: string option;
     DragOn:   string option;
 }
@@ -65,9 +67,11 @@ let init () : Model * Cmd<Msg> =
     let model = {
         PeriodicTaskItvlMsec = 1000;
         PeriodicTaskCount = 0;
+        SaveState = Saving;
+        SaveWaitMsec = 3000;
+        LastChanged = System.DateTime.Now;
         Todos = [];
         Input = "";
-        SaveState = Saving;
         DragFrom = None;
         DragOn   = None;
     }
@@ -77,6 +81,12 @@ let init () : Model * Cmd<Msg> =
 
     model, cmd
 
+let setChanged (model: Model) =
+    { model with
+        SaveState = ToBeSaved;
+        LastChanged = System.DateTime.Now;
+    }
+
 let todo_idx2label (idx: int) =
     "todo:" + (string idx)
 
@@ -108,13 +118,25 @@ let dropTarget_label2posOpt (label: string) =
     else
         None
 
+let elapsedMsec (tm: System.DateTime) =
+    (System.DateTime.Now - tm).TotalMilliseconds |> int
+
 let update (msg: Msg) (model: Model) : Model * Cmd<Msg> =
     logMessage msg
     match msg with
     | PeriodicTask ->
         periodicTaskNum <- periodicTaskNum - 1
         let model = { model with PeriodicTaskCount = model.PeriodicTaskCount + 1 }
-        model, Cmd.none
+        let (mode, cmd) =
+            if model.SaveState = ToBeSaved &&
+               elapsedMsec model.LastChanged > model.SaveWaitMsec
+            then
+                let model = { model with SaveState = ToBeSaved }
+                let cmd = Cmd.OfAsync.perform todosApi.saveTodos () SavedTodos
+                model, cmd
+            else
+                model, Cmd.none
+        model, cmd
     | GotTodos todos ->
         { model with
               Todos = todos;
@@ -141,11 +163,10 @@ let update (msg: Msg) (model: Model) : Model * Cmd<Msg> =
 
         { model with Input = "" }, cmd
     | AddedTodo todo ->
-        { model with
-              Todos = model.Todos @ [ todo ];
-              SaveState = ToBeSaved;
-        },
-        Cmd.none
+        let model =
+            { model with Todos = model.Todos @ [ todo ] }
+            |> setChanged
+        model, Cmd.none
     | MoveTodo (srcLabel, dstLabel) ->
         // ToDo: implement: more gentle error handling
         let cmd =
@@ -165,11 +186,10 @@ let update (msg: Msg) (model: Model) : Model * Cmd<Msg> =
         },
         cmd
     | MovedTodos todos ->
-        { model with
-              Todos = todos;
-              SaveState = ToBeSaved;
-        },
-        Cmd.none
+        let model =
+            { model with Todos = todos }
+            |> setChanged
+        model, Cmd.none
     | DragStart label ->
         { model with
               DragFrom = Some label;
@@ -300,14 +320,12 @@ let containerBox (model: Model) (dispatch: Msg -> unit) =
                 ]
             ]
         ]
-        Bulma.control.p [
-            Bulma.button.a [
-                color.isPrimary
-                prop.disabled (model.SaveState <> ToBeSaved)
-                prop.onClick (fun _ -> dispatch SaveTodos)
-                prop.text "Save"
-            ]
-        ]
+        let saveStateMsg =
+            match model.SaveState with
+            | Saved     -> "synced"
+            | Saving    -> "saving"
+            | ToBeSaved -> "changed"
+        Html.span saveStateMsg
     ]
 
 let view (model: Model) (dispatch: Msg -> unit) =