SAFE Stack アプリのデータ保存 (5. 自動保存)
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) =