├── .gitignore ├── .merlin ├── .travis.yml ├── Makefile ├── README.md ├── _tags ├── examples ├── browser.html ├── node.js └── tests.js ├── lib ├── helpers.js ├── irmin_js.ml └── irmin_js_api.mli └── opam /.gitignore: -------------------------------------------------------------------------------- 1 | _build 2 | -------------------------------------------------------------------------------- /.merlin: -------------------------------------------------------------------------------- 1 | S lib 2 | B _build/lib 3 | PKG irmin irmin.mem js_of_ocaml 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | install: wget https://raw.githubusercontent.com/ocaml/ocaml-travisci-skeleton/master/.travis-opam.sh 3 | script: bash -ex .travis-opam.sh 4 | sudo: false 5 | addons: 6 | apt: 7 | sources: 8 | - avsm 9 | packages: 10 | - ocaml 11 | - ocaml-base 12 | - ocaml-native-compilers 13 | - ocaml-compiler-libs 14 | - ocaml-interp 15 | - ocaml-base-nox 16 | - ocaml-nox 17 | - camlp4 18 | - camlp4-extra 19 | - time 20 | - libgmp-dev 21 | env: 22 | - FORK_USER=talex5 FORK_BRANCH=containers OPAMYES=true OCAML_VERSION=4.01 23 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # make JFLAGS="--pretty --noinline" 2 | JFLAGS = 3 | 4 | # Let ocamlbuild figure out the build dependencies 5 | .PHONY: irmin_js 6 | 7 | all: test 8 | 9 | test: _build/lib/irmin_js.js 10 | if [ -f /usr/bin/nodejs ]; then nodejs examples/node.js; else node examples/node.js; fi 11 | 12 | _build/lib/irmin_js.js: irmin_js 13 | js_of_ocaml ${JFLAGS} +weak.js +cstruct/cstruct.js lib/helpers.js _build/lib/irmin_js.byte -o "$@" 14 | 15 | irmin_js: 16 | ocamlbuild -use-ocamlfind -no-links lib/irmin_js.byte 17 | 18 | clean: 19 | rm -rf _build 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Irmin 2 | 3 | [Irmin][] provides a Git-like API for data-storage. 4 | It can be compiled to Javascript using [js_of_ocaml][] and run in the browser, using IndexedDB for local storage, which can be used offline and later sync'd with a remote server. 5 | See [CueKeeper][] for an example of an application using Irmin. 6 | 7 | `irmin-js` makes Irmin available as a regular Javascript library (without requiring your application to be written in OCaml). 8 | It can be used client-side in the browser, or server-side with Node.js. 9 | 10 | 11 | 12 | ### Conditions 13 | 14 | Copyright (c) 2015 Thomas Leonard 15 | 16 | Permission to use, copy, modify, and distribute this software for any 17 | purpose with or without fee is hereby granted, provided that the above 18 | copyright notice and this permission notice appear in all copies. 19 | 20 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 21 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 22 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 23 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 24 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 25 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 26 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 27 | 28 | [Irmin]: https://github.com/mirage/irmin/ 29 | [js_of_ocaml]: http://ocsigen.org/js_of_ocaml/ 30 | [CueKeeper]: http://roscidus.com/blog/blog/2015/04/28/cuekeeper-gitting-things-done-in-the-browser/ 31 | -------------------------------------------------------------------------------- /_tags: -------------------------------------------------------------------------------- 1 | true: warn(A), strict_sequence 2 | true: syntax(camlp4o), package(irmin.mem, js_of_ocaml, js_of_ocaml.syntax, irmin-indexeddb) 3 | : include 4 | -------------------------------------------------------------------------------- /examples/browser.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Irmin-JS test page 6 | 7 | 8 |

If the JavaScript is working, you should see some output here:

9 |

10 |     
11 |     
12 |     
29 |   
30 | 
31 | 


--------------------------------------------------------------------------------
/examples/node.js:
--------------------------------------------------------------------------------
1 | require('../_build/lib/irmin_js.js');
2 | var tests = require('./tests');
3 | 
4 | var memTest = irmin.memRepo().then(function (repo) { tests.testIrmin(repo, console.log) });
5 | console.log("\ntest result: " + memTest);
6 | 


--------------------------------------------------------------------------------
/examples/tests.js:
--------------------------------------------------------------------------------
 1 | (function(exports){
 2 |   function run(repo, log) {					log("--- testSetKey:");
 3 |     return repo.branch("master").then(function (master) {
 4 |     return testSetKey(master, log).then(function () { 		log("\n--- testView:");
 5 |     return testView(master, log).then(function () { 		log("\n--- contents:");
 6 |     return listContents(master, ["dir"], log)
 7 |     })})})
 8 |   }
 9 | 
10 |   function listContents(branch, dir, log) {
11 |     return branch.list(dir).then(function (paths) {
12 |       for (var i in paths) {
13 | 	log("/" + paths[i].join("/"));
14 |       }
15 |     })
16 |   }
17 | 
18 |   function testView(branch, log) {
19 |     var note = ["dir", "note"];
20 |     var meta = irmin.commitMetadata("user", "testView");
21 |     return branch.withMergeView(meta, [], function (view) {
22 |       return view.update(["dir", "3"], "hello").then(function () {
23 |       return listContents(view, ["dir"], log).then(function () {
24 |       return view.read(note).then(function (one) {		if (one !== null) { throw("Already set!"); }
25 |       return view.update(note, "Initial note").then(function () {
26 |       return branch.read(note).then(function (v) {		log("Note in branch: " + v);
27 |       return view.read(note).then(function (v) {		log("Note in view: " + v);
28 |       })})})})})})
29 |     }).then(function (mergeConflict) {
30 |     if (mergeConflict !== null) { throw("Merge conflict: " + mergeConflict); }
31 |     log("Merge successful")
32 |     return branch.read(note).then(function (v) {		log("Note in branch: " + v);
33 |     })})
34 |   }
35 | 
36 |   function testSetKey(branch, log) {
37 |     return branch.read(['key']).then(function(val) {		log("before update: key=" + val);
38 |     var meta = irmin.commitMetadata("user", "Set key");
39 |     return branch.update(meta, ['key'], 'value').then(function() {
40 |     return branch.read(['key']).then(function(val) {		log("after update:  key=" + val);
41 |     return branch.head().then(function (head) {			log("head: " + head);
42 |     return listContents(head, [], log);
43 |     }) }) }) })
44 |   }
45 | 
46 |   exports.testIrmin = run
47 | })(typeof exports === 'undefined' ? this['irmin_tests']={} : exports);
48 | 


--------------------------------------------------------------------------------
/lib/helpers.js:
--------------------------------------------------------------------------------
 1 | // TODO: These should have proper bounds checks.
 2 | 
 3 | //Provides: bin_prot_blit_string_buf_stub
 4 | //Requires: caml_blit_string_to_bigstring
 5 | function bin_prot_blit_string_buf_stub(ofs1, buf1, ofs2, buf2, len) {
 6 | 	caml_blit_string_to_bigstring(buf1, ofs1, buf2, ofs2, len)
 7 | }
 8 | 
 9 | //Provides: bin_prot_blit_buf_string_stub
10 | //Requires: caml_blit_bigstring_to_string
11 | function bin_prot_blit_buf_string_stub(ofs1, buf1, ofs2, buf2, len) {
12 | 	caml_blit_bigstring_to_string(buf1, ofs1, buf2, ofs2, len)
13 | }
14 | 
15 | //Provides: bin_prot_blit_buf_stub
16 | //Requires: caml_ba_sub, caml_ba_blit
17 | function bin_prot_blit_buf_stub(ofs1, buf1, ofs2, buf2, len) {
18 | 	var src = caml_ba_sub(buf1, ofs1, len)
19 | 	var dst = caml_ba_sub(buf2, ofs2, len)
20 | 	caml_ba_blit(src, dst)
21 | }
22 | 


--------------------------------------------------------------------------------
/lib/irmin_js.ml:
--------------------------------------------------------------------------------
  1 | (* Copyright (C) 2015, Thomas Leonard.
  2 |    See the README file for details. *)
  3 | 
  4 | (** Implements Irmin_js_api. *)
  5 | 
  6 | open Lwt
  7 | open Irmin_js_api
  8 | 
  9 | let lwt_of_js_promise p =
 10 |   match Js.typeof p |> Js.to_string with
 11 |   | "object" ->
 12 |       let lwt : _ Lwt.t Js.Optdef.t = Js.Unsafe.get p (Js.string "lwtThread") in
 13 |       Js.Optdef.get lwt (fun () -> failwith "Not an irmin-js promise!")
 14 |   | "undefined" ->
 15 |       return (Obj.magic ())
 16 |   | ty -> failwith (Printf.sprintf "callback should return a promise object, not %S" ty)
 17 | 
 18 | (** Wrap an Lwt promise to provide a Javascript promise object. *)
 19 | let rec js_promise_of (type a) (t:a Lwt.t) : a promise Js.t =
 20 |   let and_then cb =
 21 |     js_promise_of begin
 22 |       t >>= fun v ->
 23 |       Js.Unsafe.fun_call cb [| Js.Unsafe.inject v |] |> lwt_of_js_promise
 24 |     end in
 25 |   let to_string () =
 26 |     let state =
 27 |       match Lwt.state t with
 28 |       | Sleep -> "unresolved"
 29 |       | Fail ex -> Printexc.to_string ex
 30 |       | Return _ -> "ok" in
 31 |     Js.string (Printf.sprintf "" state) in
 32 |   let promise = Js.Unsafe.obj [||] in
 33 |   promise##lwtThread <- t;
 34 |   let promise = (promise :> a promise Js.t) in
 35 |   promise##_then <- Js.wrap_callback and_then |> Obj.magic;
 36 |   promise##toString <- Js.wrap_callback to_string;
 37 |   promise
 38 | 
 39 | let id_task t = t
 40 | 
 41 | let commit_metadata owner msg =
 42 |   let owner = Js.to_string owner in
 43 |   let msg = Js.to_string msg in
 44 |   let date = Unix.gettimeofday () |> Int64.of_float in
 45 |   Irmin.Task.create ~date ~owner msg
 46 | 
 47 | (* Irmin currently requires commit metadata for all operations, but it's only
 48 |    used for writes. For reads, we use this dummy value. *)
 49 | let dummy_msg =
 50 |   Irmin.Task.create ~date:0L ~owner:"irmin-js" "unused"
 51 | 
 52 | let key_of_js arr =
 53 |   Js.to_array arr |> Array.to_list |> List.map Js.to_string
 54 | 
 55 | let key_to_js segs =
 56 |   segs |> List.map Js.string |> Array.of_list |> Js.array
 57 | 
 58 | module Repo (Store : Irmin.BASIC with type key = string list and type value = string) = struct
 59 |   module View = Irmin.View(Store)
 60 | 
 61 |   let read store key =
 62 |     js_promise_of begin
 63 |       let key = key_of_js key in
 64 |       Store.read (store dummy_msg) key >|= function
 65 |       | None -> Js.Opt.empty
 66 |       | Some value -> Js.Opt.return (Js.string value)
 67 |     end
 68 | 
 69 |   let list store path =
 70 |     js_promise_of begin
 71 |       Store.list (store dummy_msg) (key_of_js path) >|= fun keys ->
 72 |       keys |> List.map key_to_js |> Array.of_list |> Js.array
 73 |     end
 74 | 
 75 |   let commit repo hash =
 76 |     let str_hash = Irmin.Hash.SHA1.to_hum hash in
 77 |     Store.of_commit_id id_task hash repo >>= fun store ->
 78 |     let c : commit Js.t = Js.Unsafe.obj [||] in
 79 |     c##hash <- Js.string str_hash;
 80 |     c##toString <- Js.wrap_callback (fun () -> Printf.sprintf "" str_hash |> Js.string);
 81 |     c##read <- Js.wrap_callback (read store);
 82 |     c##list <- Js.wrap_callback (list store);
 83 |     return c
 84 | 
 85 |   let wrap_view v =
 86 |     let view : view Js.t = Js.Unsafe.obj [||] in
 87 |     let update key value =
 88 |       js_promise_of begin
 89 |         let key = key_of_js key in
 90 |         let value = Js.to_string value in
 91 |         View.update v key value
 92 |       end in
 93 |     let read key =
 94 |       js_promise_of begin
 95 |         let key = key_of_js key in
 96 |         View.read v key >|= function
 97 |         | None -> Js.Opt.empty
 98 |         | Some value -> Js.Opt.return (Js.string value)
 99 |       end in
100 |     let list path =
101 |       js_promise_of begin
102 |         View.list v (key_of_js path) >|= fun keys ->
103 |         keys |> List.map key_to_js |> Array.of_list |> Js.array
104 |       end in
105 |     view##toString <- Js.wrap_callback (fun () -> Js.string "");
106 |     view##update <- Js.wrap_callback update;
107 |     view##read <- Js.wrap_callback read;
108 |     view##list <- Js.wrap_callback list;
109 |     view
110 | 
111 |   let with_merge_view store metadata key cb =
112 |     let key = key_of_js key in
113 |     js_promise_of begin
114 |       Irmin.with_hrw_view (module View) (store metadata) ~path:key `Merge (fun v ->
115 |         let view = wrap_view v in
116 |         Js.Unsafe.fun_call cb [| Js.Unsafe.inject view |] |> lwt_of_js_promise
117 |       ) >|= function
118 |       | `Ok () -> Js.Opt.empty
119 |       | `Conflict msg -> Js.Opt.return (Js.string msg)
120 |     end
121 | 
122 |   let branch repo name =
123 |     js_promise_of begin
124 |       let name = Js.to_string name in
125 |       Store.of_branch_id id_task name repo >>= fun store ->
126 |       let b : branch Js.t = Js.Unsafe.obj [||] in
127 |       let head () =
128 |         js_promise_of begin
129 |           Store.head (store dummy_msg) >>= function
130 |           | None -> return Js.Opt.empty
131 |           | Some hash ->
132 |               commit repo hash >|= Js.Opt.return
133 |         end in
134 |       let update task key value =
135 |         js_promise_of begin
136 |           let key = key_of_js key in
137 |           let value = Js.to_string value in
138 |           Store.update (store task) key value
139 |         end in
140 |       b##head <- Js.wrap_callback head;
141 |       b##toString <- Js.wrap_callback (fun () -> Printf.sprintf "" name |> Js.string);
142 |       b##update <- Js.wrap_callback update;
143 |       b##read <- Js.wrap_callback (read store);
144 |       b##list <- Js.wrap_callback (list store);
145 |       b##withMergeView <- Js.wrap_callback (with_merge_view store);
146 |       return b
147 |     end
148 | 
149 |   let repo s =
150 |     let repo : repo Js.t = Js.Unsafe.obj [||] in
151 |     repo##branch <- Js.wrap_callback (branch s);
152 |     repo##toString <- Js.wrap_callback (fun () -> Js.string "");
153 |     return repo
154 | end
155 | 
156 | let mem_repo () = js_promise_of begin
157 |     let module Store = Irmin_mem.Make(Irmin.Contents.String)(Irmin.Ref.String)(Irmin.Hash.SHA1) in
158 |     let module R = Repo(Store) in
159 |     let config = Irmin_mem.config () in
160 |     Store.Repo.create config >>= R.repo
161 |   end
162 | 
163 | let idb_repo name = js_promise_of begin
164 |     let module Store = Irmin_IDB.Make(Irmin.Contents.String)(Irmin.Ref.String)(Irmin.Hash.SHA1) in
165 |     let module R = Repo(Store) in
166 |     let config = Irmin_IDB.config (Js.to_string name) in
167 |     Store.Repo.create config >>= R.repo
168 |   end
169 | 
170 | let resolve x = js_promise_of (return x)
171 | 
172 | let () =
173 |   let irmin : irmin Js.t = Js.Unsafe.obj [||] in
174 |   irmin##resolve <- Js.wrap_callback resolve;
175 |   irmin##memRepo <- Js.wrap_callback mem_repo;
176 |   irmin##idbRepo <- Js.wrap_callback idb_repo;
177 |   irmin##commitMetadata <- Js.wrap_callback commit_metadata;
178 |   Js.Unsafe.global##irmin <- irmin;
179 | 


--------------------------------------------------------------------------------
/lib/irmin_js_api.mli:
--------------------------------------------------------------------------------
  1 | (* Copyright (C) 2015, Thomas Leonard.
  2 |    See the README file for details. *)
  3 | 
  4 | (** Defines the API exposed by this library to its Javascript clients.
  5 |     Note that the types are from the perspective of the library: writeonly
  6 |     means that the library *provides* the member; from the client's point
  7 |     of view the property is read-only. *)
  8 | 
  9 | open Js
 10 | 
 11 | type 'a export = (unit, 'a) meth_callback writeonly_prop
 12 | 
 13 | type commit_metadata = Irmin.task
 14 | type path = js_string t js_array t
 15 | type value = js_string t
 16 | type hash = js_string t
 17 | type branch_name = js_string t
 18 | type user = js_string t
 19 | type mergeConflict = js_string t Opt.t
 20 | 
 21 | class type printable = object
 22 |   method toString : (unit -> js_string t) export
 23 | end
 24 | 
 25 | class type ['a] promise = object
 26 |   inherit printable
 27 | 
 28 |   method _then : (('a -> 'b promise t) -> 'b promise t) export
 29 |   (** When this promise resolves, pass the value to the given callback.
 30 |    * The callback should return another promise. If you have an immediate value instead,
 31 |    * use [irmin.resolve] to turn it into a promise.
 32 |    * As a convenience, returning [undefined] automatically uses [irmin.resolve(undefined)]. *)
 33 | end
 34 | 
 35 | class type commit = object
 36 |   inherit printable
 37 | 
 38 |   method hash : hash writeonly_prop
 39 |   (** Get the hash of the commit. *)
 40 | 
 41 |   method read : (path -> value Opt.t promise t) export
 42 |   (** Read a file from the head commit. *)
 43 | 
 44 |   method list : (path -> path js_array t promise t) export
 45 |   (** List the direct children of [path]. *)
 46 | end
 47 | 
 48 | class type view = object
 49 |   inherit printable
 50 | 
 51 |   method read : (path -> value Opt.t promise t) export
 52 |   (** Read a file from the view. *)
 53 | 
 54 |   method update : (path -> value -> unit promise t) export
 55 |   (** Update a file in the view. *)
 56 | 
 57 |   method list : (path -> path js_array t promise t) export
 58 |   (** List the direct children of [path]. *)
 59 | end
 60 | 
 61 | class type branch = object
 62 |   inherit printable
 63 | 
 64 |   method head : (unit -> commit t Opt.t promise t) export
 65 |   (** Get the commit currently at the tip of the branch.
 66 |    * [null] if the branch does not exist. *)
 67 | 
 68 |   method read : (path -> value Opt.t promise t) export
 69 |   (** Read a file from the head commit. *)
 70 | 
 71 |   method update : (commit_metadata -> path -> value -> unit promise t) export
 72 |   (** Write a file to a new commit and add it to the branch. *)
 73 | 
 74 |   method list : (path -> path js_array t promise t) export
 75 |   (** List the direct children of [path]. *)
 76 | 
 77 |   method withMergeView : (commit_metadata -> path -> (view t -> unit t) -> mergeConflict promise t) export
 78 |   (** [withMergeView(msg, path, fn)] creates a view of the tip of the branch and calls the
 79 |    * supplied function. When that resolves, the view is merged back into the branch. *)
 80 | end
 81 | 
 82 | class type repo = object
 83 |   inherit printable
 84 | 
 85 |   method branch : (branch_name -> branch t promise t) export
 86 |   (** Access the named branch, which need not yet exist. *)
 87 | end
 88 | 
 89 | (** Available as [window.irmin] from Javascript. *)
 90 | class type irmin = object
 91 |   method memRepo : (unit -> repo t promise t) export
 92 |   (** Create a new in-memory Irmin repository. *)
 93 | 
 94 |   method idbRepo : (js_string t -> repo t promise t) export
 95 |   (** Open (or create) an IndexedDB database with the given name and
 96 |    * return an Irmin repository for it. *)
 97 | 
 98 |   method commitMetadata : (user -> js_string t -> commit_metadata) export
 99 |   (** [commit_metadata user msg] creates commit metadata with the current time and
100 |       the given user and log message. *)
101 | 
102 |   method resolve : ('a -> 'a promise t) export
103 |   (** Convert an immediate value into a promise (e.g. [promise.then]'s callback needs
104 |    * a promise, but you might just want to return a value you have already. *)
105 | end
106 | 


--------------------------------------------------------------------------------
/opam:
--------------------------------------------------------------------------------
 1 | opam-version: "1.2"
 2 | name: "irmin-js"
 3 | version: "dev"
 4 | maintainer: "Thomas Leonard "
 5 | authors: "Thomas Leonard "
 6 | homepage: "https://github.com/talex5/irmin-js"
 7 | bug-reports: "https://github.com/talex5/irmin-js/issues"
 8 | license: "BSD-2-Clause"
 9 | dev-repo: "https://github.com/talex5/irmin-js.git"
10 | build: [
11 |   [make]
12 | ]
13 | depends: [
14 |   "irmin" {>= "0.10.0"}
15 |   "irmin-indexeddb"
16 |   "js_of_ocaml"
17 |   "cstruct" {>= "1.7.0"}
18 |   "ocamlfind" {build}
19 | ]
20 | 


--------------------------------------------------------------------------------