Initial import
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..81d600b
--- /dev/null
+++ b/README.md
@@ -0,0 +1,4 @@
+Design Doc Cache
+================
+
+Pretty much covers it.
diff --git a/src/ddoc_cache.app.src b/src/ddoc_cache.app.src
new file mode 100644
index 0000000..184d0a9
--- /dev/null
+++ b/src/ddoc_cache.app.src
@@ -0,0 +1,22 @@
+% Copyright 2012 Cloudant. All rights reserved.
+
+{application, ddoc_cache, [
+    {description, "Design Document Cache"},
+    {vsn, git},
+    {registered, [
+        ddoc_cache_server
+    ]},
+    {applications, [
+        kernel,
+        stdlib,
+        crypto,
+        mem3,
+        fabric,
+        twig
+    ]},
+    {mod, {ddoc_cache_app, []}},
+    {env, [
+        {cache_size, 104857600}, % 100M
+        {cache_expiry, 3600} % 1h
+    ]}
+]}.
diff --git a/src/ddoc_cache.erl b/src/ddoc_cache.erl
new file mode 100644
index 0000000..42a4999
--- /dev/null
+++ b/src/ddoc_cache.erl
@@ -0,0 +1,17 @@
+% Copyright 2012 Cloudant. All rights reserved.
+
+-module(ddoc_cache).
+
+
+-export([
+    start/0,
+    stop/0
+]).
+
+
+start() ->
+    application:start(ddoc_cache).
+
+
+stop() ->
+    application:stop(ddoc_cache).
diff --git a/src/ddoc_cache_app.erl b/src/ddoc_cache_app.erl
new file mode 100644
index 0000000..922aab6
--- /dev/null
+++ b/src/ddoc_cache_app.erl
@@ -0,0 +1,15 @@
+% Copyright 2012 Cloudant. All rights reserved.
+
+-module(ddoc_cache_app).
+-behaviour(application).
+
+
+-export([start/2, stop/1]).
+
+
+start(_StartType, _StartArgs) ->
+    ddoc_cache_sup:start_link().
+
+
+stop(_State) ->
+    ok.
diff --git a/src/ddoc_cache_server.erl b/src/ddoc_cache_server.erl
new file mode 100644
index 0000000..ba176fd
--- /dev/null
+++ b/src/ddoc_cache_server.erl
@@ -0,0 +1,231 @@
+% Copyright 2012 Cloudant. All rights reserved.
+
+-module(ddoc_cache_server).
+-behaviour(gen_server).
+
+
+-export([
+    start_link/0,
+    open/2,
+    evict/2
+]).
+
+-export([
+    open_ddoc/1
+]).
+
+-export([
+    init/1,
+    terminate/2,
+    handle_call/3,
+    handle_cast/2,
+    handle_info/2,
+    code_change/3
+]).
+
+
+-define(CACHE, ddoc_cache_docs).
+-define(ATIMES, ddoc_cache_atimes).
+-define(OPENING, ddoc_cache_opening).
+
+
+-record(ddoc, {
+    key,
+    dbname,
+    atime,
+    doc,
+    lease
+}).
+
+-record(opener, {
+    key,
+    pid,
+    clients
+}).
+
+-record(st, {
+    uuid,
+    max_size,
+    expiry
+}).
+
+
+start_link() ->
+    gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
+
+
+open(DbName, <<"_design/", _/binary>>=DDocId) ->
+    case ets:lookup(?CACHE, {DbName, DDocId}) of
+        [#ddoc{doc=Doc}] ->
+            gen_server:cast(?MODULE, {cache_hit, {DbName, DDocId}}),
+            {ok, Doc};
+        _ ->
+            gen_server:call(?MODULE, {open, {DbName, DDocId}}, infinity)
+    end;
+open(DbName, DDocId) ->
+    open(DbName, <<"_design/", DDocId/binary>>).
+
+
+evict(DbName, DDocId) ->
+    gen_server:abcast(?MODULE, {evict, {DbName, DDocId}}).
+
+
+init(_) ->
+    process_flag(trap_exit, true),
+    ets:new(?CACHE, [protected, named_table, set, {keypos, #ddoc.key}]),
+    ets:new(?ATIMES, [protected, named_table, sorted_set]),
+    ets:new(?OPENING, [protected, named_table, set, {keypos, #opener.key}]),
+    {ok, #st{
+        uuid = ddoc_cache_util:new_uuid(),
+        max_size = get_cache_size(),
+        expiry = get_cache_expiry(),
+        in_progress = []
+    }}.
+
+
+terminate(_Reason, _State) ->
+    ok.
+
+
+handle_call({cache_hit, Key}, _From, St) ->
+    cache_hit(Key),
+    {ok, St};
+
+handle_call({open, Key}, From, #st{in_progress=IP}=St) ->
+    case ets:lookup(?OPENING, Key) of
+        [#opener{clients=Clients}=O] ->
+            ets:insert(?OPENING, O#opening{clients=[From | Clients]}),
+            {noreply, St};
+        [] ->
+            Pid = spawn_link(?MODULE, open_ddoc, [Key]),
+            ets:insert(?OPENING, #opener{key=Key, pid=Pid, clients=[From]})
+            {noreply, St}
+    end;
+
+handle_call(Msg, _From, St) ->
+    {stop, {invalid_call, Msg}, {invalid_call, Msg}, St}.
+
+
+handle_cast({evict, DbName, DDocId}, St) ->
+    cache_remove({DbName, DDocId}),
+    ets:insert(?LOG, [{erlang:now(), {DbName, DDocId}}]),
+    {noreply, St};
+    
+handle_cast(Msg, St) ->
+    {stop, {invalid_cast, Msg}, St}.
+
+
+handle_info({'EXIT', _Pid, {ddoc_ok, {DbName, _}=Key, Doc}}, St) ->
+    cache_insert(#ddoc{key=Key, dbname=DbName, doc=Doc}, St),
+    respond(Key, {ok, Doc}),
+    {noreply, St};
+
+handle_info({'EXIT', _Pid, {ddoc_error, Key, Error}}, St) ->
+    respond(Key, Error),
+    {noreply, St};
+
+handle_info({'EXIT', Pid, Reason}, St) ->
+    Pattern = #opener{pid=Pid, _='_'},
+    case ets:match_object(?OPENING, Pattern) of
+        [#opener{key=Key, clients=Clients}] ->
+            respond(Key, Reason),
+            {noreply, St};
+        [] ->
+            {stop, {unknown_pid_died, {Pid, Reason}}, St}
+    end;
+
+handle_info(Msg, St) ->
+    {stop, {invalid_info, Msg}, St}.
+
+
+code_change(_OldVsn, State, _Extra) ->
+    {ok, State}.
+
+
+open_ddoc({DbName, DDocId}=Key) ->
+    Resp = fabric:open_doc(DbName, DDocId) of
+        {ok, Doc} ->
+            exit({ddoc_ok, Key, Doc});
+        Else ->
+            exit({ddoc_error, Key, Error})
+    end.
+
+
+respond(Key, Resp) ->
+    [#opener{clients=Clients}] = ets:lookup(?OPENING, Key),
+    [gen_server:reply(C, Resp) || C <- Clients],
+    ets:delete(?OPENING, Key).
+
+
+cache_hit(Key) ->
+    % Using a different pattern than the usual ets:lookup/2
+    % method so that we can avoid needlessly copying the large
+    % #doc{} record in and out of ets.
+    case ets:match(?CACHE, #ddoc{key=Key, atime='$1', _='_'}) of
+        [[ATime]] ->
+            NewATime = erlang:now(),
+            ets:delete(?ATIMES, ATime),
+            ets:insert(?ATIMES, {NewATime, Key}),
+            ets:update_element(?CACHE, Key, {#ddoc.atime, NewATime});
+        [] ->
+            ok
+    end.
+
+
+cache_insert(#ddoc{key=Key}=DDoc, St) ->
+    % Same logic as cache_hit to avoid ets:lookup/2
+    case ets:match(?CACHE, #ddoc{key=Key, atime='$1', _='_'}) of
+        [[ATime]] ->
+            ets:delete(?ATIMES, ATime);
+        [] ->
+            ok
+    end,
+    NewATime = erlang:now(),
+    ets:insert(?CACHE, DDoc#ddoc{atime=ATime}),
+    ets:insert(?ATIMES, {ATime, DDoc#ddoc.key}),
+    cache_free_space(St).
+
+
+cache_free_space(St) ->
+    case ets:info(?CACHE, memory) > St#st.cache_size of
+        true ->
+            case ets:first(?ATIMES) of
+                {ATime, Key} ->
+                    ets:delete(?ATIMES, ATime),
+                    ets:delete(?CACHE, Key),
+                    cache_free_space(St)
+                '$end_of_table' ->
+                    ok
+            end;
+        false ->
+            ok
+    end.
+            
+
+cache_remove(Key) ->
+    % Same logic as cache_hit/1 to avoid ets:lookup/2
+    case ets:match(?CACHE, #ddoc{key=Key, atime=ATime, _='_'}) of
+        [[ATme]] ->
+            ets:delete(?CACHE, Key),
+            ets:delete(?ATIMES, ATime);
+        [] ->
+            ok
+    end.
+
+
+get_cache_size() ->
+    case application:get_env(ddoc_cache, cache_size) of
+        {ok, Value} when is_integer(Value), Value > 0 ->
+            Value;
+        _ ->
+            104857600 % Default 100M
+    end.
+
+
+get_cache_expiry() ->
+    case application:get_env(ddoc_cache, cache_expiry) of
+        {ok, Value} when is_integer(Value), Value > 0 ->
+            Value;
+        _ ->
+            3600 % Default 1h
+    end.
diff --git a/src/ddoc_cache_sup.erl b/src/ddoc_cache_sup.erl
new file mode 100644
index 0000000..2d8cd8c
--- /dev/null
+++ b/src/ddoc_cache_sup.erl
@@ -0,0 +1,29 @@
+% Copyright 2012 Cloudant. All rights reserved.
+
+-module(ddoc_cache_sup).
+-behaviour(supervisor).
+
+
+-export([
+    start_link/0,
+    init/1
+]).
+
+
+start_link() ->
+    supervisor:start_link({local, ?MODULE}, ?MODULE, []).
+
+
+init([]) ->
+    Children = [
+        {
+            ddoc_cache_server,
+            {ddoc_cache_server, start_link, []},
+            permanent,
+            5000,
+            worker,
+            [ddoc_cache_server]
+        }
+    ],
+    {ok, {{one_for_one, 5, 10}, Children}}.
+
diff --git a/src/ddoc_cache_util.erl b/src/ddoc_cache_util.erl
new file mode 100644
index 0000000..a725674
--- /dev/null
+++ b/src/ddoc_cache_util.erl
@@ -0,0 +1,22 @@
+-module(ddoc_cache_util).
+
+
+-export([
+    new_uuid/0
+]).
+
+
+new_uuid() ->
+    to_hex(crypto:rand_bytes(16), []).
+
+
+to_hex(<<>>, Acc) ->
+    list_to_binary(lists:reverse(Acc));
+to_hex(<<C1:4, C2:4, Rest/binary>>, Acc) ->
+    to_hex(Rest, [hexdig(C1), hexdig(C2) | Acc]).
+
+
+hexdig(C) when C >= 0, C =< 9 ->
+    C + $0;
+hexdig(C) when C >= 10, C =< 15 ->
+    C + $A - 10.