%%% -*- erlang -*- %%% %%% This file is part of couchbeam released under the MIT license. %%% See the NOTICE for more information. -module(couchbeam_changes). -include("couchbeam.hrl"). -export([follow/1, follow/2, cancel_stream/1, stream_next/1, follow_once/1, follow_once/2]). -spec follow(Db::db()) -> {ok, StreamRef::atom()} | {error, term()}. follow(Db) -> follow(Db, []). %% @doc Stream changes to a pid %%

Db : a db record

%%

Client : pid or callback where to send changes events where events are %% The pid receive these events: %%

%%
{change, StartRef, {done, Lastseq::integer()}
%%
Connection terminated or you got all changes
%%
{change, StartRef, Row :: ejson_object()}
%%
Line of change
%%
{error, LastSeq::integer(), Msg::term()}
%%
Got an error, connection is closed when an error %% happend.
%%
%% LastSeq is the last sequence of changes.

%% While the callbac could be like: %%
%%      fun({done, LastSeq}) ->
%%          ok;
%%      fun({done, LastSeq}) ->
%%          ok;
%%      fun({done, LastSeq}) ->
%%          ok.
%%

>Options :: changes_stream_options() [continuous
%%    | longpoll
%%    | normal
%%    | include_docs
%%    | {since, integer() | now}
%%    | {timeout, integer()}
%%    | heartbeat | {heartbeat, integer()}
%%    | {filter, string()} | {filter, string(), list({string(), string() | integer()})}
%%    | {view, string()},
%%    | {docids, list))},
%%    | {stream_to, pid()},
%%    | {async, once | normal}]
%% %%

%% %%

Return {ok, StartRef, ChangesPid} or {error, Error}. Ref can be %% used to disctint all changes from this pid. ChangesPid is the pid of %% the changes loop process. Can be used to monitor it or kill it %% when needed.

-spec follow(Db::db(), Options::changes_options()) -> {ok, StreamRef::atom()} | {error, term()}. follow(Db, Options) -> {To, Options1} = case proplists:get_value(stream_to, Options) of undefined -> {self(), Options}; Pid -> {Pid, proplists:delete(stream_to, Options)} end, Ref = make_ref(), case supervisor:start_child(couchbeam_changes_sup, [To, Ref, Db, Options1]) of {ok, _Pid} -> {ok, Ref}; Error -> Error end. -spec follow_once(Db::db()) -> {ok, LastSeq::integer(), Changes::list()} | {error,term()}. follow_once(Db) -> follow_once(Db, []). %% @doc fetch all changes at once using a normal or longpoll %% connections. %% %%

Db : a db record

%%

Options :: changes_options() [
%%    | longpoll
%%    | normal
%%    | include_docs
%%    | {since, integer() | now}
%%    | {timeout, integer()}
%%    | heartbeat | {heartbeat, integer()}
%%    | {filter, string()}
%%    | {filter, string(), list({string(), string() | integer()})}
%%    | {docids, list()))},
%%    | {stream_to, pid()}
%%    ]
%% %%

%% %%

Result: {ok, LastSeq::integer(), Rows::list()} or %% {error, LastSeq, Error}. LastSeq is the last sequence of changes.

-spec follow_once(Db::db(), Options::changes_options()) -> {ok, LastSeq::integer(), Changes::list()} | {error,term()}. follow_once(Db, Options) -> case parse_options_once(Options, []) of {error, _}=Error -> Error; Options1 -> FinalOptions = couchbeam_util:force_param(reconnect_after, false, Options1), case proplists:get_value(feed, FinalOptions) of longpoll -> case follow(Db, FinalOptions) of {ok, Ref} -> collect_changes(Ref); Error -> Error end; _ -> changes_request(Db, FinalOptions) end end. cancel_stream(Ref) -> with_changes_stream(Ref, fun(Pid) -> case supervisor:terminate_child(couch_view_sup, Pid) of ok -> case supervisor:delete_child(couch_view_sup, Pid) of ok ->ok; {error, not_found} -> ok; Error -> Error end; Error -> Error end end). stream_next(Ref) -> with_changes_stream(Ref, fun(Pid) -> Pid ! {Ref, stream_next} end). %% @private collect_changes(Ref) -> collect_changes(Ref, []). collect_changes(Ref, Acc) -> receive {Ref, {done, LastSeq}} -> Changes = lists:reverse(Acc), {ok, LastSeq, Changes}; {Ref, {change, Change}} -> collect_changes(Ref, [Change|Acc]); {Ref, Error} -> Error; Error -> Error end. with_changes_stream(Ref, Fun) -> case ets:lookup(couchbeam_changes_streams, Ref) of [] -> {error, stream_undefined}; [{Ref, Pid}] -> Fun(Pid) end. changes_request(#db{server=Server, options=ConnOptions}=Db, Options) -> %% if we are filtering the changes using docids, send a POST request %% instead of a GET to make sure it will be accepted whatever the %% number of doc ids given. {DocIds, Options1} = case proplists:get_value(doc_ids, Options) of undefined -> {[], Options}; [] -> {[], Options}; Ids -> {Ids, proplists:delete(doc_ids, Options)} end, %% make url Url = hackney_url:make_url(couchbeam_httpc:server_url(Server), [couchbeam_httpc:db_url(Db), <<"_changes">>], Options1), %% do the request Resp = case DocIds of [] -> couchbeam_httpc:db_request(get, Url, [], <<>>, ConnOptions, [200, 202]); _ -> Body = couchbeam_ejson:encode({[{<<"doc_ids">>, DocIds}]}), Headers = [{<<"Content-Type">>, <<"application/json">>}], couchbeam_httpc:db_request(post, Url, Headers, Body, ConnOptions, [200, 202]) end, case Resp of {ok, _, _, Ref} -> {Props} = couchbeam_httpc:json_body(Ref), LastSeq = couchbeam_util:get_value(<<"last_seq">>, Props), Changes = couchbeam_util:get_value(<<"results">>, Props), {ok, LastSeq, Changes}; Error -> Error end. parse_options_once([], Acc) -> lists:reverse(Acc); parse_options_once([normal | Rest], Acc) -> parse_options_once(Rest, couchbeam_util:force_param(feed, normal, Acc)); parse_options_once([continuous | _Rest], _Acc) -> {error, {badarg, continuous}}; parse_options_once([longpoll | Rest], Acc) -> parse_options_once(Rest, couchbeam_util:force_param(feed, longpoll, Acc)); parse_options_once([heartbeat | Rest], Acc) -> parse_options_once(Rest, couchbeam_util:force_param(heartbeat, true, Acc)); parse_options_once([descending | Rest], Acc) -> parse_options_once(Rest, couchbeam_util:force_param(descending, true, Acc)); parse_options_once([conflicts | Rest], Acc) -> parse_options_once(Rest, couchbeam_util:force_param(conflicts, true, Acc)); parse_options_once([include_docs | Rest], Acc) -> parse_options_once(Rest, couchbeam_util:force_param(include_docs, true, Acc)); parse_options_once([{K, V} | Rest], Acc) -> parse_options_once(Rest, [{K, V} | Acc]).