├── .gitignore ├── .travis.yml ├── README.md ├── dataloader-lwt.descr ├── dataloader-lwt.opam ├── dataloader-lwt ├── src │ ├── dataloader_lwt.ml │ ├── dataloader_lwt.mli │ └── jbuild └── test │ ├── jbuild │ └── test.ml ├── dataloader.descr ├── dataloader.opam └── dataloader ├── src ├── dataloader.ml ├── dataloader.mli └── jbuild └── test ├── jbuild └── test.ml /.gitignore: -------------------------------------------------------------------------------- 1 | _build 2 | _tests 3 | *.native 4 | *.byte 5 | *.install 6 | .merlin -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: c 2 | sudo: false 3 | services: 4 | - docker 5 | install: wget https://raw.githubusercontent.com/ocaml/ocaml-travisci-skeleton/master/.travis-docker.sh 6 | script: bash -ex ./.travis-docker.sh 7 | env: 8 | global: 9 | - PINS="dataloader:. dataloader-lwt:." 10 | matrix: 11 | - PACKAGE="dataloader" DISTRO="ubuntu-16.04" OCAML_VERSION="4.03.0" 12 | - PACKAGE="dataloader" DISTRO="ubuntu-16.04" OCAML_VERSION="4.04.2" 13 | - PACKAGE="dataloader" DISTRO="ubuntu-16.04" OCAML_VERSION="4.05.0" 14 | - PACKAGE="dataloader" DISTRO="ubuntu-16.04" OCAML_VERSION="4.06.0" 15 | - PACKAGE="dataloader-lwt" DISTRO="ubuntu-16.04" OCAML_VERSION="4.03.0" 16 | - PACKAGE="dataloader-lwt" DISTRO="ubuntu-16.04" OCAML_VERSION="4.04.2" 17 | - PACKAGE="dataloader-lwt" DISTRO="ubuntu-16.04" OCAML_VERSION="4.05.0" 18 | - PACKAGE="dataloader-lwt" DISTRO="ubuntu-16.04" OCAML_VERSION="4.06.0" 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Dataloader is a utility to be used for your application's data fetching layer to provide batching and caching, in particular with [`ocaml-graphql-server`](https://github.com/andreas/ocaml-graphql-server). It is a port of [facebook/dataloader](https://github.com/facebook/dataloader) for Node. The library is still under active development. 2 | 3 | This repo contains two packages: 4 | 5 | - `dataloader`, which is IO-agnostic written in CPS-style. 6 | - `dataloader-lwt`, which is a shim on top of `dataloader` for [`Lwt`](https://github.com/ocsigen/lwt). 7 | 8 | ## Example 9 | 10 | The following examples assumes a function `batchLoadUsersFromDatabase` of the type `user_id list -> (user list, exn) result Lwt.t`: 11 | 12 | ```ocaml 13 | let user_loader = Dataloader_lwt.create ~load:(fun user_ids -> 14 | batchLoadUsersFromDatabase user_ids 15 | ) 16 | 17 | (* triggers only 1 query rather than 5 *) 18 | List.map (fun user_id -> 19 | Dataloader_lwt.load user_loader user_id 20 | ) [1; 2; 3; 4; 5] 21 | ``` 22 | 23 | The function `~load` provided to `Dataloader.create` must uphold the following constraints: 24 | 25 | - The list of values must be the same length as the list of keys. 26 | - Each index in the list of values must correspond to the same index in the list of keys. 27 | -------------------------------------------------------------------------------- /dataloader-lwt.descr: -------------------------------------------------------------------------------- 1 | Dataloader is a utility to be used for your application's data fetching layer to provide batching and caching, in particular with `graphql`. 2 | 3 | This library provides a shim over `dataloader` for use with Lwt. 4 | -------------------------------------------------------------------------------- /dataloader-lwt.opam: -------------------------------------------------------------------------------- 1 | opam-version: "1.2" 2 | maintainer: "Andreas Garnaes " 3 | authors: "Andreas Garnaes " 4 | homepage: "https://github.com/andreas/ocaml-dataloader" 5 | doc: "https://andreas.github.io/ocaml-dataloader/" 6 | bug-reports: "https://github.com/andreas/ocaml-dataloader/issues" 7 | dev-repo: "https://github.com/andreas/ocaml-dataloader.git" 8 | build: [["jbuilder" "build" "-p" name "-j" jobs]] 9 | build-test: [["jbuilder" "runtest" "-p" name "-j" jobs]] 10 | depends: [ 11 | "jbuilder" {build} 12 | "dataloader" 13 | "lwt" 14 | "alcotest" {test} 15 | ] 16 | available: [ 17 | ocaml-version >= "4.03.0" 18 | ] 19 | -------------------------------------------------------------------------------- /dataloader-lwt/src/dataloader_lwt.ml: -------------------------------------------------------------------------------- 1 | type ('a, 'b, 'err) t = { 2 | dataloader : ('a, 'b, 'err) Dataloader.t; 3 | mutable pause : unit Lwt.t option; 4 | } 5 | 6 | let create ~load = 7 | let dataloader = Dataloader.create ~load:(fun keys fail ok -> 8 | Lwt.on_success (load keys) (function 9 | | Ok values -> ok values 10 | | Error err -> fail err 11 | )) in 12 | { dataloader; pause = None } 13 | 14 | let pause t = 15 | let open Lwt.Infix in 16 | match t.pause with 17 | | None -> 18 | let pause = 19 | Lwt.pause () >|= fun () -> 20 | Dataloader.trigger t.dataloader 21 | in 22 | t.pause <- Some pause; 23 | pause 24 | | Some pause -> pause 25 | 26 | let load t key = 27 | let open Lwt.Infix in 28 | let p, resolver = Lwt.wait () in 29 | Dataloader.load t.dataloader key (fun err -> 30 | Lwt.wakeup resolver (Error err) 31 | ) (fun value -> 32 | Lwt.wakeup resolver (Ok value) 33 | ); 34 | pause t >>= fun () -> 35 | p 36 | -------------------------------------------------------------------------------- /dataloader-lwt/src/dataloader_lwt.mli: -------------------------------------------------------------------------------- 1 | type ('a, 'b, 'err) t 2 | 3 | val create : 4 | load:('a list -> ('b list, 'err) result Lwt.t) -> 5 | ('a, 'b, 'err) t 6 | 7 | val load : 8 | ('a, 'b, 'err) t -> 9 | 'a -> 10 | ('b, 'err) result Lwt.t 11 | -------------------------------------------------------------------------------- /dataloader-lwt/src/jbuild: -------------------------------------------------------------------------------- 1 | (jbuild_version 1) 2 | 3 | (library 4 | ((name dataloader_lwt) 5 | (public_name dataloader-lwt) 6 | (wrapped false) 7 | (libraries (dataloader lwt)))) 8 | 9 | -------------------------------------------------------------------------------- /dataloader-lwt/test/jbuild: -------------------------------------------------------------------------------- 1 | (jbuild_version 1) 2 | 3 | (executables 4 | ((libraries (dataloader-lwt lwt.unix alcotest)) 5 | (names (test)))) 6 | 7 | (alias 8 | ((name runtest) 9 | (package dataloader-lwt) 10 | (deps (test.exe)) 11 | (action (run ${<} -v)))) 12 | -------------------------------------------------------------------------------- /dataloader-lwt/test/test.ml: -------------------------------------------------------------------------------- 1 | open Lwt.Infix 2 | 3 | let suite = [ 4 | ("load one", `Quick, fun () -> 5 | let loader = Dataloader_lwt.create ~load:(fun keys -> 6 | Lwt_result.return keys 7 | ) in 8 | (Dataloader_lwt.load loader 1 >|= function 9 | | Error _ -> Alcotest.fail "Loader failed, expected 1" 10 | | Ok value -> Alcotest.(check int) "Load value" 1 value) 11 | |> Lwt_main.run 12 | ); 13 | ("load n", `Quick, fun () -> 14 | let loader_calls = ref 0 in 15 | let loader = Dataloader_lwt.create ~load:(fun keys -> 16 | loader_calls := !loader_calls + 1; 17 | Lwt_result.return keys 18 | ) in 19 | (List.map (fun i -> 20 | Dataloader_lwt.load loader i >|= function 21 | | Error _ -> Alcotest.failf "Loader failed, expected %d" i 22 | | Ok value -> Alcotest.(check int) "Load value" i value 23 | ) [1;2;3;4;5;6;7;8;9;10] 24 | |> Lwt.join >|= fun () -> 25 | Alcotest.(check int) "Load calls" 1 !loader_calls) 26 | |> Lwt_main.run 27 | ); 28 | ("failed load", `Quick, fun () -> 29 | let loader = Dataloader_lwt.create ~load:(fun keys -> 30 | Lwt_result.fail (Failure "boom") 31 | ) in 32 | List.map (fun i -> 33 | Dataloader_lwt.load loader i >|= function 34 | | Error _ -> () 35 | | Ok _ -> Alcotest.failf "Expected failure, got %d" i 36 | ) [1;2;3;4;5;6;7;8;9;10] 37 | |> Lwt.join 38 | |> Lwt_main.run 39 | ) 40 | ] 41 | 42 | let () = Alcotest.run "dataloader-lwt" [ 43 | "dataloader-lwt", suite 44 | ] 45 | -------------------------------------------------------------------------------- /dataloader.descr: -------------------------------------------------------------------------------- 1 | Dataloader is a utility to be used for your application's data fetching layer to provide batching and caching, in particular with `graphql`. It is a port of DataLoader for Node: https://github.com/facebook/dataloader 2 | 3 | To use with Lwt, see `dataloader-lwt`. 4 | -------------------------------------------------------------------------------- /dataloader.opam: -------------------------------------------------------------------------------- 1 | opam-version: "1.2" 2 | maintainer: "Andreas Garnaes " 3 | authors: "Andreas Garnaes " 4 | homepage: "https://github.com/andreas/ocaml-dataloader" 5 | doc: "https://andreas.github.io/ocaml-dataloader/" 6 | bug-reports: "https://github.com/andreas/ocaml-dataloader/issues" 7 | dev-repo: "https://github.com/andreas/ocaml-dataloader.git" 8 | build: [["jbuilder" "build" "-p" name "-j" jobs]] 9 | build-test: [["jbuilder" "runtest" "-p" name "-j" jobs]] 10 | depends: [ 11 | "jbuilder" {build} 12 | "rresult" 13 | "alcotest" {test} 14 | ] 15 | available: [ 16 | ocaml-version >= "4.03.0" 17 | ] 18 | -------------------------------------------------------------------------------- /dataloader/src/dataloader.ml: -------------------------------------------------------------------------------- 1 | type 'a cont = 'a -> unit 2 | type ('a, 'err) cps = 'err cont -> 'a cont -> unit 3 | type ('key, 'value, 'err) loader = 'key list -> ('value list, 'err) cps 4 | 5 | type ('key, 'value, 'err) entry = { 6 | key : 'key; 7 | fail : 'err cont; 8 | ok : 'value cont; 9 | } 10 | 11 | type ('key, 'value, 'err) t = { 12 | loader : ('key, 'value, 'err) loader; 13 | mutable entries : ('key, 'value, 'err) entry list 14 | } 15 | 16 | let create ~load = 17 | { loader = load; entries = []} 18 | 19 | let load t key fail ok = 20 | t.entries <- {key; fail; ok}::t.entries 21 | 22 | let trigger t = 23 | if t.entries = [] then 24 | () 25 | else 26 | let entries = t.entries in 27 | t.entries <- []; 28 | let keys = List.map (fun entry -> entry.key) entries in 29 | t.loader keys (fun err -> 30 | List.iter (fun entry -> entry.fail err) entries 31 | ) (fun values -> 32 | List.combine values entries 33 | |> List.iter (fun (value, entry) -> 34 | entry.ok value 35 | ) 36 | ) 37 | -------------------------------------------------------------------------------- /dataloader/src/dataloader.mli: -------------------------------------------------------------------------------- 1 | type ('a, 'b, 'err) t 2 | 3 | type 'a cont = 'a -> unit 4 | type ('a, 'err) cps = 'err cont -> 'a cont -> unit 5 | type ('a, 'b, 'err) loader = 'a list -> ('b list, 'err) cps 6 | 7 | val create : 8 | load:('a, 'b, 'err) loader -> 9 | ('a, 'b, 'err) t 10 | 11 | val load : 12 | ('a, 'b, 'err) t -> 13 | 'a -> 14 | ('b, 'err) cps 15 | 16 | val trigger : 17 | ('a, 'b, 'err) t -> 18 | unit 19 | -------------------------------------------------------------------------------- /dataloader/src/jbuild: -------------------------------------------------------------------------------- 1 | (jbuild_version 1) 2 | 3 | (library 4 | ((name dataloader) 5 | (public_name dataloader) 6 | (wrapped false) 7 | (libraries (result)))) 8 | -------------------------------------------------------------------------------- /dataloader/test/jbuild: -------------------------------------------------------------------------------- 1 | (jbuild_version 1) 2 | 3 | (executables 4 | ((libraries (dataloader alcotest)) 5 | (names (test)))) 6 | 7 | (alias 8 | ((name runtest) 9 | (package dataloader) 10 | (deps (test.exe)) 11 | (action (run ${<} -v)))) 12 | -------------------------------------------------------------------------------- /dataloader/test/test.ml: -------------------------------------------------------------------------------- 1 | let suite = [ 2 | ("load one", `Quick, fun () -> 3 | let loader = Dataloader.create ~load:(fun keys fail ok -> 4 | ok keys 5 | ) in 6 | Dataloader.load loader 1 7 | (fun _ -> Alcotest.fail "Loader failed, expected 1") 8 | (fun value -> Alcotest.(check int) "Load value" 1 value) 9 | ; 10 | Dataloader.trigger loader 11 | ); 12 | ("load n", `Quick, fun () -> 13 | let loader_calls = ref 0 in 14 | let loader = Dataloader.create ~load:(fun keys fail ok -> 15 | loader_calls := !loader_calls + 1; 16 | ok keys 17 | ) in 18 | List.iter (fun i -> 19 | Dataloader.load loader i 20 | (fun _ -> Alcotest.failf "Loader failed, expected %d" i) 21 | (fun value -> Alcotest.(check int) "Load value" i value) 22 | ) [1;2;3;4;5;6;7;8;9;10]; 23 | Dataloader.trigger loader; 24 | Alcotest.(check int) "Load calls" 1 !loader_calls 25 | ); 26 | ("failed load", `Quick, fun () -> 27 | let loader = Dataloader.create ~load:(fun keys fail ok -> 28 | fail (Failure "boom") 29 | ) in 30 | List.iter (fun i -> 31 | Dataloader.load loader i 32 | (fun _ -> ()) 33 | (fun value -> Alcotest.failf "Expected failure, got %d" i) 34 | ) [1;2;3;4;5;6;7;8;9;10]; 35 | Dataloader.trigger loader; 36 | ) 37 | ] 38 | 39 | let () = Alcotest.run "dataloader" [ 40 | "dataloader", suite 41 | ] 42 | --------------------------------------------------------------------------------