(* Per-trainee mutable state, held in a hashtable keyed by trainee id. *) type trainee_state = { mutable active : Repository.routine_id option; mutable current : Evidence.Workout.t option; mutable stored : Repository.record list; (* most recent first *) mutable feedback : Evidence.Feedback.t list; (* most recent first *) mutable next_id : int; mutable next_app_feedback : int; } type t = { mutable trainees : Trainee.t list; states : (string, trainee_state) Hashtbl.t; mutable app_feedback : (Trainee.id * Repository.app_feedback) list; mutable app_feedback_votes : (Repository.app_feedback_id * Trainee.id) list; mutable next_trainee : int; } let create () = { trainees = []; states = Hashtbl.create 16; app_feedback = []; app_feedback_votes = []; next_trainee = 1; } let state t id = let key = Trainee.id_to_string id in match Hashtbl.find_opt t.states key with | Some s -> s | None -> let s = { active = None; current = None; stored = []; feedback = []; next_id = 1; next_app_feedback = 1; } in Hashtbl.replace t.states key s; s let create_trainee t ~(username : Trainee.username) ~credential = match List.find_opt (fun (tr : Trainee.t) -> String.equal (Trainee.username_to_string tr.username) (Trainee.username_to_string username)) t.trainees with | Some _ -> Lwt.return (Error `Username_taken) | None -> let id = Trainee.id (Printf.sprintf "t%d" t.next_trainee) in t.next_trainee <- t.next_trainee + 1; let trainee = { Trainee.id; username; credential } in t.trainees <- trainee :: t.trainees; Lwt.return (Ok trainee) let find_trainee_by_username t (username : Trainee.username) = Lwt.return (List.find_opt (fun (tr : Trainee.t) -> String.equal (Trainee.username_to_string tr.username) (Trainee.username_to_string username)) t.trainees) let find_trainee t id = Lwt.return (List.find_opt (fun (tr : Trainee.t) -> String.equal (Trainee.id_to_string tr.id) (Trainee.id_to_string id)) t.trainees) let replace_trainee t (updated : Trainee.t) = t.trainees <- List.map (fun (tr : Trainee.t) -> if String.equal (Trainee.id_to_string tr.id) (Trainee.id_to_string updated.id) then updated else tr) t.trainees let update_username t id (username : Trainee.username) = match List.find_opt (fun (tr : Trainee.t) -> String.equal (Trainee.id_to_string tr.id) (Trainee.id_to_string id)) t.trainees with | None -> Lwt.return (Error `Username_taken) | Some current -> let taken = List.exists (fun (tr : Trainee.t) -> (not (String.equal (Trainee.id_to_string tr.id) (Trainee.id_to_string id))) && String.equal (Trainee.username_to_string tr.username) (Trainee.username_to_string username)) t.trainees in if taken then Lwt.return (Error `Username_taken) else begin let updated = { current with Trainee.username } in replace_trainee t updated; Lwt.return (Ok updated) end let update_credential t id credential = match List.find_opt (fun (tr : Trainee.t) -> String.equal (Trainee.id_to_string tr.id) (Trainee.id_to_string id)) t.trainees with | None -> Lwt.return None | Some current -> let updated = { current with Trainee.credential } in replace_trainee t updated; Lwt.return (Some updated) let list_routines _ = Catalog.routines let find_routine _ id = Catalog.find id let active_routine t id = Lwt.return (state t id).active let set_active_routine t id routine = (state t id).active <- Some routine; Lwt.return_unit let in_progress t id = Lwt.return (state t id).current let set_in_progress t id workout = (state t id).current <- workout; Lwt.return_unit (* Store the finished workout and clear the in-progress slot together. In memory this is a single synchronous update, so it cannot tear. *) let finish_workout t id workout = let s = state t id in let wid = Repository.workout_id (Printf.sprintf "w%d" s.next_id) in let record = { Repository.id = wid; workout } in s.next_id <- s.next_id + 1; s.stored <- record :: s.stored; s.current <- None; Lwt.return record let equal_id (a : Repository.workout_id) (b : Repository.workout_id) = String.equal (a :> string) (b :> string) let find t id wid = let s = state t id in Lwt.return (List.find_opt (fun r -> equal_id r.Repository.id wid) s.stored) let replace t id record = let s = state t id in if not (List.exists (fun r -> equal_id r.Repository.id record.Repository.id) s.stored) then Lwt.return false else begin s.stored <- List.map (fun existing -> if equal_id existing.Repository.id record.Repository.id then record else existing) s.stored; Lwt.return true end let log t id = let s = state t id in Lwt.return (List.fold_left (fun log record -> Evidence.Log.add log record.Repository.workout) Evidence.Log.empty (List.rev s.stored)) let history t id = Lwt.return (state t id).stored let save_feedback t id report = let s = state t id in s.feedback <- report :: s.feedback; Lwt.return_unit let feedback t id = Lwt.return (state t id).feedback let save_app_feedback t id ~submitted_at ~message = let author = match List.find_opt (fun (trainee : Trainee.t) -> String.equal (Trainee.id_to_string trainee.id) (Trainee.id_to_string id)) t.trainees with | Some trainee -> Trainee.username_to_string trainee.username | None -> "" in let contributions = 1 + List.fold_left (fun count (owner, _) -> if String.equal (Trainee.id_to_string owner) (Trainee.id_to_string id) then count + 1 else count) 0 t.app_feedback in let s = state t id in let feedback_id = Repository.app_feedback_id (Printf.sprintf "%s:%d" (Trainee.id_to_string id) s.next_app_feedback) in s.next_app_feedback <- s.next_app_feedback + 1; let report = Repository. { feedback_id; author; contributions; submitted_at; message; upvotes = 0; viewer_upvoted = false; viewer_owns = true; } in t.app_feedback <- (id, report) :: t.app_feedback; Lwt.return report let app_feedback_id_parts report = let raw = Repository.app_feedback_id_to_string report.Repository.feedback_id in match String.rindex_opt raw ':' with | None -> (raw, 0) | Some separator -> let owner = String.sub raw 0 separator in let sequence = String.sub raw (separator + 1) (String.length raw - separator - 1) |> int_of_string_opt |> Option.value ~default:0 in (owner, sequence) let app_feedback t ~viewer = let reports = List.map (fun (owner, report) -> let owner_id = Trainee.id_to_string owner in let contributions = List.fold_left (fun count (candidate, _) -> if String.equal owner_id (Trainee.id_to_string candidate) then count + 1 else count) 0 t.app_feedback in let author = match List.find_opt (fun (trainee : Trainee.t) -> String.equal owner_id (Trainee.id_to_string trainee.id)) t.trainees with | Some trainee -> Trainee.username_to_string trainee.username | None -> report.Repository.author in let viewer_upvoted = List.exists (fun (feedback_id, voter) -> String.equal (Repository.app_feedback_id_to_string feedback_id) (Repository.app_feedback_id_to_string report.Repository.feedback_id) && String.equal (Trainee.id_to_string voter) (Trainee.id_to_string viewer)) t.app_feedback_votes in let viewer_owns = String.equal owner_id (Trainee.id_to_string viewer) in { report with author; contributions; viewer_upvoted; viewer_owns }) t.app_feedback in let compare left right = let by_votes = Int.compare right.Repository.upvotes left.Repository.upvotes in if by_votes <> 0 then by_votes else let by_time = Int.compare (Recovery.timestamp_to_unix_seconds right.submitted_at) (Recovery.timestamp_to_unix_seconds left.submitted_at) in if by_time <> 0 then by_time else let left_owner, left_sequence = app_feedback_id_parts left in let right_owner, right_sequence = app_feedback_id_parts right in let by_owner = String.compare left_owner right_owner in if by_owner <> 0 then by_owner else Int.compare right_sequence left_sequence in Lwt.return (List.sort compare reports) let upvote_app_feedback t ~voter feedback_id = match List.find_opt (fun (_, report) -> String.equal (Repository.app_feedback_id_to_string report.Repository.feedback_id) (Repository.app_feedback_id_to_string feedback_id)) t.app_feedback with | None -> Lwt.return false | Some (owner, _) when String.equal (Trainee.id_to_string owner) (Trainee.id_to_string voter) -> Lwt.return false | Some (_, report) -> if List.exists (fun (voted_feedback, existing_voter) -> String.equal (Repository.app_feedback_id_to_string voted_feedback) (Repository.app_feedback_id_to_string feedback_id) && String.equal (Trainee.id_to_string existing_voter) (Trainee.id_to_string voter)) t.app_feedback_votes then Lwt.return false else begin t.app_feedback_votes <- (feedback_id, voter) :: t.app_feedback_votes; t.app_feedback <- List.map (fun (owner, current) -> if String.equal (Repository.app_feedback_id_to_string current.Repository.feedback_id) (Repository.app_feedback_id_to_string report.Repository.feedback_id) then (owner, { current with upvotes = current.upvotes + 1 }) else (owner, current)) t.app_feedback; Lwt.return true end let update_app_feedback t ~author id ~message = let author_id = Trainee.id_to_string author in let updated = ref false in t.app_feedback <- List.map (fun (owner, report) -> if String.equal (Trainee.id_to_string owner) author_id && String.equal (Repository.app_feedback_id_to_string report.Repository.feedback_id) (Repository.app_feedback_id_to_string id) then begin updated := true; (owner, { report with message }) end else (owner, report)) t.app_feedback; Lwt.return !updated let delete_app_feedback t ~author id = let author_id = Trainee.id_to_string author in let owned (owner, report) = String.equal (Trainee.id_to_string owner) author_id && String.equal (Repository.app_feedback_id_to_string report.Repository.feedback_id) (Repository.app_feedback_id_to_string id) in let removed = List.exists owned t.app_feedback in if removed then begin t.app_feedback <- List.filter (fun item -> not (owned item)) t.app_feedback; t.app_feedback_votes <- List.filter (fun (feedback_id, _) -> not (String.equal (Repository.app_feedback_id_to_string feedback_id) (Repository.app_feedback_id_to_string id))) t.app_feedback_votes end; Lwt.return removed