diff --git a/src/plugins/server/states.ml b/src/plugins/server/states.ml index f3af381df65192e3ea920fbbc0e9a87311c81c9a..dcc2e97ec4d56f1bf4d1393e318ba083f39dcfef 100644 --- a/src/plugins/server/states.ml +++ b/src/plugins/server/states.ml @@ -20,8 +20,10 @@ (* *) (**************************************************************************) +open Data + (* -------------------------------------------------------------------------- *) -(* --- Values & States --- *) +(* --- Values --- *) (* -------------------------------------------------------------------------- *) let register_value (type a) ~page ~name ~descr ?(details=[]) @@ -35,11 +37,15 @@ let register_value (type a) ~page ~name ~descr ?(details=[]) ~descr:(plain "Signal for value " @ href h) () in Request.register ~page ~kind:`GET ~name:(name ^ ".get") ~descr:(plain "Getter for value " @ href h) - ~input:(module Data.Junit) ~output get ; + ~input:(module Junit) ~output get ; signal +(* -------------------------------------------------------------------------- *) +(* --- States --- *) +(* -------------------------------------------------------------------------- *) + let register_state (type a) ~page ~name ~descr ?(details=[]) - ~(data : a Data.data) ~get ~set = + ~(data : a data) ~get ~set = let open Markdown in let title = Printf.sprintf "`STATE` %s" name in let index = [ Printf.sprintf "%s (`STATE`)" name ] in @@ -49,10 +55,222 @@ let register_state (type a) ~page ~name ~descr ?(details=[]) ~descr:(plain "Signal for state " @ href h) () in Request.register ~page ~kind:`GET ~name:(name ^ ".get") ~descr:(plain "Getter for state " @ href h) - ~input:(module Data.Junit) ~output:(module (val data)) get ; + ~input:(module Junit) ~output:(module (val data)) get ; Request.register ~page ~kind:`SET ~name:(name ^ ".set") ~descr:(plain "Setter for state " @ href h) - ~input:(module (val data)) ~output:(module Data.Junit) set ; + ~input:(module (val data)) ~output:(module Junit) set ; signal (* -------------------------------------------------------------------------- *) +(* --- Model Signature --- *) +(* -------------------------------------------------------------------------- *) + +type 'a column = Syntax.field * ('a -> json) + +type 'a signature = 'a column list ref + +let signature () = ref [] + +let column (type a) (s : a signature) ~name ~descr (output : a Request.output) = + let module D = (val output) in + if name = "id" then + raise (Invalid_argument "Server.States.column: invalid name") ; + if List.exists (fun (fd,_) -> fd.Syntax.name = name) !s then + raise (Invalid_argument "Server.States.column: duplicate name") ; + let fd = Syntax.{ name ; syntax = D.syntax ; descr } in + s := (fd , D.to_json) :: !s + +module Kmap = Map.Make(String) + +(* -------------------------------------------------------------------------- *) +(* --- Model Content --- *) +(* -------------------------------------------------------------------------- *) + +type 'a update = Remove | Add of 'a +type 'a content = { + mutable cleared : bool ; + mutable updates : 'a update Kmap.t ; +} + +type 'a model = { + signal : Request.signal ; + key : 'a -> string ; + iter : ('a -> unit) -> unit ; + getter : (string * ('a -> json)) list ; + mutable current : 'a content option ; (* fast access *) + projects : (string , 'a content) Hashtbl.t ; (* indexed by project *) +} + +let synchronize model = + begin + Project.register_after_set_current_hook + ~user_only:false (fun _ -> model.current <- None) ; + let cleanup p = + Hashtbl.remove model.projects (Project.get_unique_name p) in + Project.register_before_remove_hook cleanup ; + Project.register_todo_before_clear cleanup ; + end + +let current model = + match model.current with + | Some w -> w + | None -> + let prj = Project.(current () |> get_unique_name) in + let content = + try Hashtbl.find model.projects prj + with Not_found -> + let w = { + cleared = true ; + updates = Kmap.empty ; + } in + Hashtbl.add model.projects prj w ; w + in model.current <- Some content ; content + +let reload model = + let m = current model in + m.cleared <- true ; m.updates <- Kmap.empty + +let update model k = + let m = current model in + if not m.cleared then m.updates <- Kmap.add (model.key k) (Add k) m.updates + +let remove model k = + let m = current model in + if not m.cleared then m.updates <- Kmap.add (model.key k) Remove m.updates + +(* -------------------------------------------------------------------------- *) +(* --- Fetch Model Updates --- *) +(* -------------------------------------------------------------------------- *) + +type buffer = { + reload : bool ; + mutable capacity : int ; + mutable pending : int ; + mutable removed : string list ; + mutable updated : json list ; +} + +let add_entry buffer cols key v = + let fjs = List.fold_left (fun fjs (fd,to_json) -> + try (fd , to_json v) :: fjs + with Not_found -> fjs + ) [] cols in + buffer.updated <- `Assoc( ("key", `String key):: fjs) :: buffer.updated ; + buffer.capacity <- pred buffer.capacity + +let remove_entry buffer key = + buffer.removed <- key :: buffer.removed ; + buffer.capacity <- pred buffer.capacity + +let update_entry buffer cols key = function + | Remove -> remove_entry buffer key + | Add v -> add_entry buffer cols key v + +let fetch model n = + let m = current model in + let reload = m.cleared in + let buffer = { + reload ; + capacity = n ; + pending = 0 ; + removed = [] ; + updated = [] ; + } in + begin + if reload then + begin + m.cleared <- false ; + model.iter + begin fun v -> + let key = model.key v in + if buffer.capacity > 0 then + add_entry buffer model.getter key v + else + ( m.updates <- Kmap.add key (Add v) m.updates ; + buffer.pending <- succ buffer.pending ) ; + end ; + end + else + m.updates <- Kmap.filter + begin fun key upd -> + if buffer.capacity > 0 then + ( update_entry buffer model.getter key upd ; false ) + else + ( buffer.pending <- succ buffer.pending ; true ) + end m.updates ; + end ; + buffer + +(* -------------------------------------------------------------------------- *) +(* --- Signature Registry --- *) +(* -------------------------------------------------------------------------- *) + +let register_model ~page ~name ~descr ?(details=[]) ~key ~iter s = + let open Markdown in + let title = Printf.sprintf "`MODEL` %s" name in + let index = [ Printf.sprintf "%s (`MODEL`)" name ] in + let columns = !s in + let description = [ + Block [Text descr] ; + Syntax.fields ~title:(Printf.sprintf "Model %s" name) + begin + Syntax.{ + name="key" ; + syntax=Syntax.ident ; + descr=plain "entry identifier" ; + } :: List.rev (List.map fst columns) + end ; + Block details + ] in + let mref = Doc.publish ~page:page ~name:name ~title ~index description [] in + let signal = Request.signal ~page ~name:(name ^ ".sig") + ~descr:(plain "Signal for model " @ href mref) () in + let getter = List.map (fun (fd,to_js) -> fd.Syntax.name , to_js) columns in + let model = { + key ; iter ; getter ; signal ; + current = None ; projects = Hashtbl.create 0 + } in + let signature = + Request.signature ~kind:`GET ~page ~name:(name ^ ".fetch") + ~descr:(plain "Fetch updates for model " @ href mref) + ~input:(module Jint) + ~details:[ + Text(plain + "Collect all model updates since the last fetch.\n\ + The number of fetched entries is limited to the\n\ + provided integer. When `reload:true` is returned,\n\ + _all_ previously received entries must be removed.")] + () in + let module Jentries = + (struct + include Jany + let syntax = Syntax.data "entry" mref + end) in + let set_reload = Request.result signature + ~name:"reload" ~descr:(plain "model fully reloaded") + (module Jbool) in + let set_removed = Request.result signature + ~name:"removed" ~descr:(plain "removed entries") + (module Jident.Jlist) in + let set_updated = Request.result signature + ~name:"updated" ~descr:(plain "updated entries") + (module Jlist(Jentries)) in + let set_pending = Request.result signature + ~name:"pending" ~descr:(plain "remaining entries to be fetched") + (module Jint) in + Request.register_sig signature + begin fun rq n -> + let buffer = fetch model n in + set_reload rq buffer.reload ; + set_removed rq buffer.removed ; + set_updated rq buffer.updated ; + set_pending rq buffer.pending ; + end ; + Request.register ~kind:`GET ~page ~name:(name ^ ".reload") + ~descr:(plain "Force full reload for model " @ href mref) + ~input:(module Junit) ~output:(module Junit) + (fun () -> reload model) ; + synchronize model ; + model + +(* -------------------------------------------------------------------------- *) diff --git a/src/plugins/server/states.mli b/src/plugins/server/states.mli index 8bd97b3a34b424f98258308237381ca8d3971736..3dc8cb6b62f661a95abebf9601a63726265b6517 100644 --- a/src/plugins/server/states.mli +++ b/src/plugins/server/states.mli @@ -39,4 +39,28 @@ val register_state : set:('a -> unit) -> Request.signal +type 'a signature + +val signature : + unit -> 'a signature + +val column : + 'a signature -> name:string -> descr:Markdown.text -> + 'a Request.output -> unit + +type 'a model + +val reload : 'a model -> unit +val update : 'a model -> 'a -> unit +val remove : 'a model -> 'a -> unit + +val register_model : + page:Doc.page -> + name:string -> + descr:Markdown.text -> + ?details:Markdown.block -> + key:('a -> string) -> + iter:(('a -> unit) -> unit) -> + 'a signature -> 'a model + (* -------------------------------------------------------------------------- *) diff --git a/src/plugins/server/syntax.ml b/src/plugins/server/syntax.ml index 727d520370b2a9a5d4c7ec5a8eac0cd534c46842..bfc7218a57a7580c4c1544c03dd58b703c51d2e9 100644 --- a/src/plugins/server/syntax.ml +++ b/src/plugins/server/syntax.ml @@ -81,6 +81,7 @@ let ident = atom @@ Markdown.emph "ident" let string = atom @@ Markdown.emph "string" let number = atom @@ Markdown.emph "number" let boolean = atom @@ Markdown.emph "boolean" +let data name dref = atom @@ Markdown.href ~text:(Markdown.emph name) dref let escaped name = Markdown.code (Printf.sprintf "'%s'" @@ String.escaped name) diff --git a/src/plugins/server/syntax.mli b/src/plugins/server/syntax.mli index d2537edf19d77b2b70d9339c86223056c722eaaa..1080751de02a8a87362784a8db6f9d62499c3c8f 100644 --- a/src/plugins/server/syntax.mli +++ b/src/plugins/server/syntax.mli @@ -49,6 +49,7 @@ val tuple : t list -> t val union : t list -> t val option : t -> t val record : (string * t) list -> t +val data : string -> Markdown.href -> t type field = { name : string ; syntax : t ; descr : Markdown.text }