···11An OCaml toplevel designed to run in a web worker
22+33+To run the example, the worker needs to be served by an http server rather
44+than loaded from the filesystem. Therefore the example may be run in the
55+following way:
66+77+```
88+$ dune build
99+$ cd _build/default/example
1010+$ python3 -m http.server 8000
1111+```
1212+1313+and then opening the URL `http://localhost:8000/`
···11+(* Worker_rpc *)
22+33+open Js_top_worker_rpc
44+55+(** Functions to facilitate RPC calls to web workers. *)
66+77+exception Timeout
88+(** When RPC calls take too long, the Lwt promise is set to failed state with
99+ this exception. *)
1010+1111+type rpc = Rpc.call -> Rpc.response Lwt.t
1212+1313+val start : string -> int -> (unit -> unit) -> rpc
1414+(** [start url timeout timeout_fn] initialises a web worker from [url] and
1515+ starts communications with it. [timeout] is the number of seconds to wait
1616+ for a response from any RPC before raising an error, and [timeout_fn] is
1717+ called when a timeout occurs. *)
1818+1919+module W : sig
2020+ val init :
2121+ rpc ->
2222+ Toplevel_api_gen.init_libs ->
2323+ (unit, Toplevel_api_gen.err) result Lwt.t
2424+2525+ val setup :
2626+ rpc ->
2727+ unit ->
2828+ (Toplevel_api_gen.exec_result, Toplevel_api_gen.err) result Lwt.t
2929+3030+ val exec :
3131+ rpc ->
3232+ string ->
3333+ (Toplevel_api_gen.exec_result, Toplevel_api_gen.err) result Lwt.t
3434+3535+ val complete :
3636+ rpc ->
3737+ string ->
3838+ (Toplevel_api_gen.completion_result, Toplevel_api_gen.err) result Lwt.t
3939+end
+14-23
idl/toplevel_api.ml
···2020type completion_result = {
2121 n : int;
2222 (** The position in the input string from where the completions may be
2323- inserted *)
2323+ inserted *)
2424 completions : string list; (** The list of possible completions *)
2525}
2626[@@deriving rpcty]
2727(** The result returned by a 'complete' call. *)
28282929-type string_list = string list [@@deriving rpcty]
3030-(** Used by setup *)
2929+type cma = {
3030+ url : string; (** URL where the cma is available *)
3131+ fn : string; (** Name of the 'wrapping' function *)
3232+}
3333+[@@deriving rpcty]
31343232-type string_string_list = (string * string) list [@@deriving rpcty]
3333-(** Used by setup *)
3535+type init_libs = { cmi_urls : string list; cmas : cma list } [@@deriving rpcty]
34363537(** For now we are only using a simple error type *)
3638type err = InternalError of string [@@deriving rpcty]
···6365 let exec_result_p = Param.mk exec_result
6466 let completion_p = Param.mk completion_result
65676666- let cmas =
6767- Param.mk ~name:"cmas"
6868+ let init_libs =
6969+ Param.mk ~name:"init_libs"
6870 ~description:
6971 [
7070- "A list of pairs. The first element of the pair is a urls to a";
7171- "cma file pre-compiled to javascript. The second item is the";
7272- "name of the function to be invoked to load the cma file";
7373- "(ie, the cma was compiled with --wrap-func).";
7474- "These will be loaded synchronously during the init call.";
7272+ "Libraries to load during the initialisation of the toplevel. ";
7373+ "If the stdlib cmis have not been compiled into the worker this ";
7474+ "MUST include the urls from which they may be fetched";
7575 ]
7676- string_string_list
7777-7878- let cmis =
7979- Param.mk ~name:"cmis"
8080- ~description:
8181- [
8282- "A list of urls of cmi files. These files will be loaded on demand";
8383- "during evaluation of toplevel phrases.";
8484- ]
8585- string_list
7676+ init_libs
86778778 let init =
8879 declare "init"
8980 [ "Initialise the toplevel." ]
9090- (cmas @-> cmis @-> returning unit_p err)
8181+ (init_libs @-> returning unit_p err)
91829283 let setup =
9384 declare "setup"
+121-36
idl/toplevel_api_gen.ml
···251251 {
252252 n: int
253253 [@ocaml.doc
254254- " The position in the input string from where the completions may be\n inserted "];
254254+ " The position in the input string from where the completions may be\n inserted "];
255255 completions: string list [@ocaml.doc " The list of possible completions "]}
256256[@@deriving rpcty][@@ocaml.doc " The result returned by a 'complete' call. "]
257257include
···317317 and _ = typ_of_completion_result
318318 and _ = completion_result
319319 end[@@ocaml.doc "@inline"][@@merlin.hide ]
320320-type string_list = string list[@@deriving rpcty][@@ocaml.doc
321321- " Used by setup "]
320320+type cma =
321321+ {
322322+ url: string [@ocaml.doc " URL where the cma is available "];
323323+ fn: string [@ocaml.doc " Name of the 'wrapping' function "]}[@@deriving
324324+ rpcty]
322325include
323326 struct
324324- let _ = fun (_ : string_list) -> ()
325325- let rec typ_of_string_list =
326326- Rpc.Types.List (let open Rpc.Types in Basic String)
327327- and string_list =
327327+ let _ = fun (_ : cma) -> ()
328328+ let rec (cma_url : (_, cma) Rpc.Types.field) =
329329+ {
330330+ Rpc.Types.fname = "url";
331331+ Rpc.Types.field = (let open Rpc.Types in Basic String);
332332+ Rpc.Types.fdefault = None;
333333+ Rpc.Types.fdescription = ["URL where the cma is available"];
334334+ Rpc.Types.fversion = None;
335335+ Rpc.Types.fget = (fun _r -> _r.url);
336336+ Rpc.Types.fset = (fun v -> fun _s -> { _s with url = v })
337337+ }
338338+ and (cma_fn : (_, cma) Rpc.Types.field) =
339339+ {
340340+ Rpc.Types.fname = "fn";
341341+ Rpc.Types.field = (let open Rpc.Types in Basic String);
342342+ Rpc.Types.fdefault = None;
343343+ Rpc.Types.fdescription = ["Name of the 'wrapping' function"];
344344+ Rpc.Types.fversion = None;
345345+ Rpc.Types.fget = (fun _r -> _r.fn);
346346+ Rpc.Types.fset = (fun v -> fun _s -> { _s with fn = v })
347347+ }
348348+ and typ_of_cma =
349349+ Rpc.Types.Struct
350350+ ({
351351+ Rpc.Types.fields =
352352+ [Rpc.Types.BoxedField cma_url; Rpc.Types.BoxedField cma_fn];
353353+ Rpc.Types.sname = "cma";
354354+ Rpc.Types.version = None;
355355+ Rpc.Types.constructor =
356356+ (fun getter ->
357357+ let open Rresult.R in
358358+ (getter.Rpc.Types.field_get "fn"
359359+ (let open Rpc.Types in Basic String))
360360+ >>=
361361+ (fun cma_fn ->
362362+ (getter.Rpc.Types.field_get "url"
363363+ (let open Rpc.Types in Basic String))
364364+ >>=
365365+ (fun cma_url ->
366366+ return { url = cma_url; fn = cma_fn })))
367367+ } : cma Rpc.Types.structure)
368368+ and cma =
328369 {
329329- Rpc.Types.name = "string_list";
330330- Rpc.Types.description = ["Used by setup"];
331331- Rpc.Types.ty = typ_of_string_list
370370+ Rpc.Types.name = "cma";
371371+ Rpc.Types.description = [];
372372+ Rpc.Types.ty = typ_of_cma
332373 }
333333- let _ = typ_of_string_list
334334- and _ = string_list
374374+ let _ = cma_url
375375+ and _ = cma_fn
376376+ and _ = typ_of_cma
377377+ and _ = cma
335378 end[@@ocaml.doc "@inline"][@@merlin.hide ]
336336-type string_string_list = (string * string) list[@@deriving rpcty][@@ocaml.doc
337337- " Used by setup "]
379379+type init_libs = {
380380+ cmi_urls: string list ;
381381+ cmas: cma list }[@@deriving rpcty]
338382include
339383 struct
340340- let _ = fun (_ : string_string_list) -> ()
341341- let rec typ_of_string_string_list =
342342- Rpc.Types.Dict (Rpc.Types.String, (let open Rpc.Types in Basic String))
343343- and string_string_list =
384384+ let _ = fun (_ : init_libs) -> ()
385385+ let rec (init_libs_cmi_urls : (_, init_libs) Rpc.Types.field) =
386386+ {
387387+ Rpc.Types.fname = "cmi_urls";
388388+ Rpc.Types.field =
389389+ (Rpc.Types.List (let open Rpc.Types in Basic String));
390390+ Rpc.Types.fdefault = None;
391391+ Rpc.Types.fdescription = [];
392392+ Rpc.Types.fversion = None;
393393+ Rpc.Types.fget = (fun _r -> _r.cmi_urls);
394394+ Rpc.Types.fset = (fun v -> fun _s -> { _s with cmi_urls = v })
395395+ }
396396+ and (init_libs_cmas : (_, init_libs) Rpc.Types.field) =
344397 {
345345- Rpc.Types.name = "string_string_list";
346346- Rpc.Types.description = ["Used by setup"];
347347- Rpc.Types.ty = typ_of_string_string_list
398398+ Rpc.Types.fname = "cmas";
399399+ Rpc.Types.field = (Rpc.Types.List typ_of_cma);
400400+ Rpc.Types.fdefault = None;
401401+ Rpc.Types.fdescription = [];
402402+ Rpc.Types.fversion = None;
403403+ Rpc.Types.fget = (fun _r -> _r.cmas);
404404+ Rpc.Types.fset = (fun v -> fun _s -> { _s with cmas = v })
348405 }
349349- let _ = typ_of_string_string_list
350350- and _ = string_string_list
406406+ and typ_of_init_libs =
407407+ Rpc.Types.Struct
408408+ ({
409409+ Rpc.Types.fields =
410410+ [Rpc.Types.BoxedField init_libs_cmi_urls;
411411+ Rpc.Types.BoxedField init_libs_cmas];
412412+ Rpc.Types.sname = "init_libs";
413413+ Rpc.Types.version = None;
414414+ Rpc.Types.constructor =
415415+ (fun getter ->
416416+ let open Rresult.R in
417417+ (getter.Rpc.Types.field_get "cmas"
418418+ (Rpc.Types.List typ_of_cma))
419419+ >>=
420420+ (fun init_libs_cmas ->
421421+ (getter.Rpc.Types.field_get "cmi_urls"
422422+ (Rpc.Types.List
423423+ (let open Rpc.Types in Basic String)))
424424+ >>=
425425+ (fun init_libs_cmi_urls ->
426426+ return
427427+ {
428428+ cmi_urls = init_libs_cmi_urls;
429429+ cmas = init_libs_cmas
430430+ })))
431431+ } : init_libs Rpc.Types.structure)
432432+ and init_libs =
433433+ {
434434+ Rpc.Types.name = "init_libs";
435435+ Rpc.Types.description = [];
436436+ Rpc.Types.ty = typ_of_init_libs
437437+ }
438438+ let _ = init_libs_cmi_urls
439439+ and _ = init_libs_cmas
440440+ and _ = typ_of_init_libs
441441+ and _ = init_libs
351442 end[@@ocaml.doc "@inline"][@@merlin.hide ]
352443type err =
353444 | InternalError of string [@@ocaml.doc
···422513 let phrase_p = Param.mk Types.string
423514 let exec_result_p = Param.mk exec_result
424515 let completion_p = Param.mk completion_result
425425- let cmas =
426426- Param.mk ~name:"cmas"
427427- ~description:["A list of pairs. The first element of the pair is a urls to a";
428428- "cma file pre-compiled to javascript. The second item is the";
429429- "name of the function to be invoked to load the cma file";
430430- "(ie, the cma was compiled with --wrap-func).";
431431- "These will be loaded synchronously during the init call."]
432432- string_string_list
433433- let cmis =
434434- Param.mk ~name:"cmis"
435435- ~description:["A list of urls of cmi files. These files will be loaded on demand";
436436- "during evaluation of toplevel phrases."] string_list
516516+ let init_libs =
517517+ Param.mk ~name:"init_libs"
518518+ ~description:["Libraries to load during the initialisation of the toplevel. ";
519519+ "If the stdlib cmis have not been compiled into the worker this ";
520520+ "MUST include the urls from which they may be fetched"]
521521+ init_libs
437522 let init =
438523 declare "init" ["Initialise the toplevel."]
439439- (cmas @-> (cmis @-> (returning unit_p err)))
524524+ (init_libs @-> (returning unit_p err))
440525 let setup =
441526 declare "setup"
442527 ["Start the toplevel. Return value is the initial blurb ";
+41-7
idl/worker_rpc.ml
idl/js_top_worker_client.ml
···1616 waiting : ((Rpc.response, exn) Result.t Lwt_mvar.t * int) Queue.t;
1717}
18181919+type rpc = Rpc.call -> Rpc.response Lwt.t
2020+1921exception Timeout
20222123let demux context msg =
···2729 let msg : string = Message.Ev.data (Brr.Ev.as_type msg) in
2830 Lwt_mvar.put mv (Ok (Marshal.from_string msg 0)))
29313030-let start worker timeout timeout_fn =
3131- let context = { worker; timeout; timeout_fn; waiting = Queue.create () } in
3232- let () =
3333- Brr.Ev.listen Message.Ev.message (demux context) (Worker.as_target worker)
3434- in
3535- context
3636-3732let rpc : context -> Rpc.call -> Rpc.response Lwt.t =
3833 fun context call ->
3934 let open Lwt in
···4237 let outstanding_execution =
4338 Brr.G.set_timeout ~ms:context.timeout (fun () ->
4439 Lwt.async (fun () -> Lwt_mvar.put mv (Error Timeout));
4040+ Worker.terminate context.worker;
4541 context.timeout_fn ())
4642 in
4743 Queue.push (mv, outstanding_execution) context.waiting;
···5248 let response = jv in
5349 Lwt.return response
5450 | Error exn -> Lwt.fail exn
5151+5252+let start url timeout timeout_fn : rpc =
5353+ let worker = Worker.create (Jstr.v url) in
5454+ let context = { worker; timeout; timeout_fn; waiting = Queue.create () } in
5555+ let () =
5656+ Brr.Ev.listen Message.Ev.message (demux context) (Worker.as_target worker)
5757+ in
5858+ rpc context
5959+6060+module Rpc_lwt = Idl.Make (Lwt)
6161+module Wraw = Toplevel_api_gen.Make (Rpc_lwt.GenClient ())
6262+6363+module W : sig
6464+ val init :
6565+ rpc ->
6666+ Toplevel_api_gen.init_libs ->
6767+ (unit, Toplevel_api_gen.err) result Lwt.t
6868+6969+ val setup :
7070+ rpc ->
7171+ unit ->
7272+ (Toplevel_api_gen.exec_result, Toplevel_api_gen.err) result Lwt.t
7373+7474+ val exec :
7575+ rpc ->
7676+ string ->
7777+ (Toplevel_api_gen.exec_result, Toplevel_api_gen.err) result Lwt.t
7878+7979+ val complete :
8080+ rpc ->
8181+ string ->
8282+ (Toplevel_api_gen.completion_result, Toplevel_api_gen.err) result Lwt.t
8383+end = struct
8484+ let init rpc a = Wraw.init rpc a |> Rpc_lwt.T.get
8585+ let setup rpc a = Wraw.setup rpc a |> Rpc_lwt.T.get
8686+ let exec rpc a = Wraw.exec rpc a |> Rpc_lwt.T.get
8787+ let complete rpc a = Wraw.complete rpc a |> Rpc_lwt.T.get
8888+end
-26
idl/worker_rpc.mli
···11-(* Worker_rpc *)
22-33-(** Functions to facilitate RPC calls to web workers.
44-55- The assumption made in this module is that RPCs are answered in the order
66- they are made. *)
77-88-type context
99-(** Represents the channel used to communicate with the worker *)
1010-1111-exception Timeout
1212-(** When RPC calls take too long, the Lwt promise is set to failed state with
1313- this exception. *)
1414-1515-val start : Brr_webworkers.Worker.t -> int -> (unit -> unit) -> context
1616-(** [start worker timeout timeout_fn] initialises communications with a web
1717- worker. [timeout] is the number of seconds to wait for a response from any
1818- RPC before raising an error, and [timeout_fn] is called when a timeout
1919- occurs. *)
2020-2121-open Js_top_worker_rpc
2222-2323-val rpc : context -> Rpc.call -> Rpc.response Lwt.t
2424-(** [rpc context call] returns a promise containing the result from the worker.
2525- If we wait longer than the timeout specified in [context] for a response,
2626- the Lwt promise will fail with exception {!Timeout}. *)
+13-5
lib/worker.ml
···11open Js_of_ocaml_toplevel
22open Js_top_worker_rpc
3344-let log fmt = Format.kasprintf (fun s -> Js_of_ocaml.(Firebug.console##log (Js.string s))) fmt
44+let log fmt =
55+ Format.kasprintf
66+ (fun s -> Js_of_ocaml.(Firebug.console##log (Js.string s)))
77+ fmt
5869(* OCamlorg toplevel in a web worker
710···176179177180let functions : (unit -> unit) list option ref = ref None
178181179179-let init cmas cmis =
182182+let init (init_libs : Toplevel_api_gen.init_libs) =
180183 let open Js_of_ocaml in
181184 try
182185 Clflags.no_check_prims := true;
183183- let cmi_files = List.map (fun cmi -> (Filename.basename cmi |> Filename.chop_extension, cmi)) cmis in
186186+ let cmi_files =
187187+ List.map
188188+ (fun cmi -> (Filename.basename cmi |> Filename.chop_extension, cmi))
189189+ init_libs.cmi_urls
190190+ in
184191 let old_loader = !Persistent_env.Persistent_signature.load in
185192 (Persistent_env.Persistent_signature.load :=
186193 fun ~unit_name ->
···198205 cmi = read_cmi unit_name (Bytes.of_string x);
199206 }
200207 | _ -> old_loader ~unit_name);
201201- Js_of_ocaml.Worker.import_scripts (List.map fst cmas);
208208+ Js_of_ocaml.Worker.import_scripts
209209+ (List.map (fun cma -> cma.Toplevel_api_gen.url) init_libs.cmas);
202210 functions :=
203211 Some
204212 (List.map
···207215 let func = Js.Unsafe.js_expr func_name in
208216 fun () ->
209217 Js.Unsafe.fun_call func [| Js.Unsafe.inject Dom_html.window |])
210210- (List.map snd cmas));
218218+ (List.map (fun cma -> cma.Toplevel_api_gen.fn) init_libs.cmas));
211219 IdlM.ErrM.return ()
212220 with e ->
213221 IdlM.ErrM.return_err (Toplevel_api_gen.InternalError (Printexc.to_string e))