123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819# 1 "src/lib/eliom_comet.client.ml"openLwt.Syntax(* Ocsigen
* http://www.ocsigen.org
* Copyright (C) 2010-2011
* Raphaël Proust
* Pierre Chambart
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, with linking exception;
* either version 2.1 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
*)(* This file is for client-side comet-programming. *)openJs_of_ocamlopenEliom_libmoduleEcb=Eliom_comet_baseletsection=Logs.Src.create"eliom:comet"moduleConfiguration=structtypeconfiguration_data={active_until_timeout:bool;time_between_request_unfocused:(float*float*float)listoption;(* (a, b) for a * t + b
(0, 0) means always active
None means: no request
The list is here if there are several configurations
(we take the min of all values, for a given t)
*)time_after_unfocus:float;time_between_request:float}letdefault_configuration={active_until_timeout=false;time_between_request_unfocused=Some[0.5,60.,600.];time_after_unfocus=180.;time_between_request=0.}typet=intletconfiguration_table=Hashtbl.create1letglobal_configuration=refdefault_configurationletconfig_minc1c2={active_until_timeout=c1.active_until_timeout||c2.active_until_timeout;time_between_request_unfocused=(matchc1.time_between_request_unfocused,c2.time_between_request_unfocusedwith|Somel1,Somel2->Some(l1@l2)|Somev,None|None,Somev->Somev|None,None->None);time_after_unfocus=maxc1.time_after_unfocusc2.time_after_unfocus;time_between_request=minc1.time_between_requestc2.time_between_request}exceptionCofconfiguration_dataletfirst_confc=tryignore(Hashtbl.fold(fun_v->raise(Cv))c());assertfalsewithCv->vletget_configuration()=ifHashtbl.lengthconfiguration_table=0thendefault_configurationelseHashtbl.fold(fun_->config_min)configuration_table(first_confconfiguration_table)letupdate_configuration_waiter,update_configuration_waker=lett,u=Lwt.wait()inreft,refuletupdate_configuration()=global_configuration:=get_configuration();lett,u=Lwt.wait()inupdate_configuration_waiter:=t;letwakener=!update_configuration_wakerinupdate_configuration_waker:=u;Lwt.wakeupwakener()letget()=!global_configurationletdrop_configurationc=Hashtbl.removeconfiguration_tablec;update_configuration()letnew_configuration:unit->t=letr=ref0infun()->incrr;Hashtbl.addconfiguration_table!rdefault_configuration;update_configuration();!rletset_funcf=tryHashtbl.replaceconfiguration_tablec(f(Hashtbl.findconfiguration_tablec));update_configuration()withNot_found->()letset_always_activeconfv=set_funconf(func->{cwithtime_between_request_unfocused=(ifvthenSome[0.,0.,0.]elseNone)})letset_timeoutconfv=set_funconf(func->{cwithtime_after_unfocus=v})letset_active_until_timeoutconfv=set_funconf(func->{cwithactive_until_timeout=v})letset_time_between_requestsconfv=set_funconf(func->{cwithtime_between_request=v})letset_time_between_requests_when_idleconfv=set_funconf(func->{cwithtime_between_request_unfocused=Some[v]})letsleep_before_next_requestfocusedis_idleactive_waiter=lettime=Sys.time()inletsleep_duration()=ifis_idle()thenmatch(get()).time_between_request_unfocused,focused()with|Some((a,b,c)::l),Somestart->letnow=Js.to_float(new%jsJs.date_now)##getTimein(* time from idle start *)lett=max0.(((now-.start)*.0.001)-.(get()).time_after_unfocus)inletv=min((a*.t)+.b)cinletv=List.fold_left(funv(a,b,c)->minv(min((a*.t)+.b)c))vlinv|_->0.(* Configuration changed.
We do not sleep and we'll see later. (?) *)else(get()).time_between_requestinletrecauxt=let*()=Lwt.pick[Js_of_ocaml_lwt.Lwt_js.sleept;!update_configuration_waiter;active_waiter()]inletremaining_time=sleep_duration()-.(Sys.time()-.time)inifremaining_time>0.thenauxremaining_timeelseLwt.return_unitinletsleep_duration=sleep_duration()inifsleep_duration<=0.thenLwt.return_unitelseauxsleep_durationendexceptionRestartexceptionChannel_closedexceptionChannel_fullexceptionComet_errorofstringlethandle_exn,set_handle_exn_function=letclosed=reffalseinletr=ref(fun?exn()->lets="Unknown exception during comet. Customize this with Eliom_comet.set_handle_exn_function. "inmatchexnwith|Someexn->raise_error~section~exn"%s"s|None->Logs.debug~src:section(funfmt->fmt"%s"s);Lwt.return_unit)in((fun?exn()->ifnot!closedthen(closed:=true;!r?exn())elseLwt.return_unit),funf->r:=f)typechan_id=stringtypestateless_message=(chan_id*(string*int)Ecb.channel_data)listmoduleService_handler:sigtype'attype'akindtypestatelesstypestatefulvalstateless:statelesskindvalstateful:statefulkindvalmake:Ecb.comet_service->'akind->'atvalwait_data:'at->(chan_id*intoption*stringEcb.channel_data)listLwt.t(** Returns the messages received in the last request. If the
channel is stateless, it also returns the message number in the [int option] *)valactivate:'at->unitvalis_active:'at->[`Active|`Inactive|`Idle]valrestart:'at->unitvaladd_channel_stateful:statefult->chan_id->unitvaladd_channel_stateless:statelesst->chan_id->Ecb.stateless_kind->unitvalclose:'at->chan_id->unitend=structtypeactivity={mutableactive:[`Inactive|`Active|`Idle](** [!hd.active] is true when the [hd] channel handler is
receiving data.
Idle means that the window is not active but we want to
keep updated from time to time. *);mutablefocused:floatoption(** [focused] is None when the page is visible and Some [t]
when the page became hidden at time [t] (in ms) *);mutableactive_waiter:unitLwt.t(** [active_waiter] terminates when the page get visible *);mutableactive_wakener:unitLwt.u;mutablerestart_waiter:Ecb.answerLwt.t;mutablerestart_wakener:Ecb.answerLwt.u;mutableactive_channels:Eliom_lib.String.Set.t}typestateful_state=intref(* id of the next request *)typestateless_state_={count:int;position:Ecb.position}typestateless_state=stateless_state_Eliom_lib.String.Table.tref(* index for each channel of the last message *)typechannel_state=|Stateful_stateofstateful_state|Stateless_stateofstateless_statetypekind'=Stateful|Statelesstype'akind=kind'typestatelesstypestatefulletstateless:statelesskind=Statelessletstateful:statefulkind=Statefultype'at={hd_service:Ecb.comet_service;hd_state:channel_state;hd_activity:activity}letadd_listenertargeteventf=letlistener=Dom_html.handler(fun_->f();Js._true)in(* Note: no [Dom_html.(...)] local open here: since js_of_ocaml 6.4 it
would resolve [listener] to [Dom_html.listener] instead of the local
binding. *)ignore(Dom_html.addEventListenertargeteventlistenerJs._false)letadd_visibility_change_listenerf=Dom_html.(add_listenerdocument(Event.make"visibilitychange")f)letset_activityhdv=ifEliom_lib.String.Set.is_emptyhd.hd_activity.active_channelsthenhd.hd_activity.active<-`Inactiveelseifv<>hd.hd_activity.activethenmatchvwith|`Inactive->hd.hd_activity.active<-`Inactive|_->hd.hd_activity.active<-v;lett,u=Lwt.wait()inhd.hd_activity.active_waiter<-t;letwakener=hd.hd_activity.active_wakenerinhd.hd_activity.active_wakener<-u;Lwt.wakeupwakener()letis_activehd=hd.hd_activity.activeletdocument_hidden()=Js.Optdef.caseJs.Unsafe.global##.document##.hidden(fun()->false)Js.to_bool(** register callbacks to 'visibility' events of the root
window. That way, we can tell when the client is active or not and do
calls to the server only if it is active *)lethandle_visibilityhandler=letresume_activity()=ifhandler.hd_activity.focused<>Nonethen(handler.hd_activity.focused<-None;set_activityhandler`Active)inletsuspend_activity()=ifhandler.hd_activity.focused=Nonethenhandler.hd_activity.focused<-Some(Js.to_float(new%jsJs.date_now)##getTime)inletvisibility_change_callback()=ifdocument_hidden()thensuspend_activity()elseresume_activity()inadd_visibility_change_listenervisibility_change_callbackletexpected_activityhd=matchhd.hd_activity.focusedwith|None->`Active|Somet->lettbru=(Configuration.get()).Configuration.time_between_request_unfocusediniftbru=Some[0.,0.,0.](* Always active *)then`Activeelseletnow=Js.to_float(new%jsJs.date_now)##getTimeinifnow-.t<(Configuration.get()).Configuration.time_after_unfocus*.1000.then`Activeelseiftbru=None(* Always inactive when idle *)then`Inactiveelse`Idleletactivatehd=(* Make sure visibility status is up to date *)ifdocument_hidden()then(ifhd.hd_activity.focused=Nonethenhd.hd_activity.focused<-Some(Js.to_float(new%jsJs.date_now)##getTime-.((Configuration.get()).Configuration.time_after_unfocus*.1000.)))elsehd.hd_activity.focused<-None;set_activityhd(expected_activityhd)letrestarthd=letact=hd.hd_activityinlett,u=Lwt.wait()inact.restart_waiter<-t;letwakener=act.restart_wakenerinact.restart_wakener<-u;Lwt.wakeup_exnwakenerRestart;activatehdletmax_retries=10letinitial_delay=0.5(* seconds *)letjitter=0.25letmultiplier=sqrt2.letmax_delay=4.(* seconds *)letdelayi=minmax_delay(initial_delay*.(multiplier**floati))*.(1.+.(jitter*.(Random.float2.-.1.)))letcall_service_after_load_endservicequeuep=matchpwith|_,Ecb.Stateful(Ecb.Commandscommands)->queue:=List.rev_append(Array.to_listcommands)!queue;let*()=Eliom_client.wait_load_end()inletq=!queueinifq<>[]then(queue:=[];Eliom_client.call_service~service()(false,Ecb.Stateful(Ecb.Commands(Array.of_list(List.revq)))))elseLwt.return""|_->let*()=Eliom_client.wait_load_end()inEliom_client.call_service~service()pletmake_requesthd=matchhd.hd_statewith|Stateful_statecount->Ecb.Stateful(Ecb.Request_data!count)|Stateless_statemap->letl=Eliom_lib.String.Table.fold(funchannel{position;_}l->(channel,position)::l)!map[]inEcb.Stateless(Array.of_listl)letstop_waitinghdchan_id=hd.hd_activity.active_channels<-Eliom_lib.String.Set.removechan_idhd.hd_activity.active_channels;ifEliom_lib.String.Set.is_emptyhd.hd_activity.active_channelsthenset_activityhd`Inactiveletupdate_stateful_statehdmessage=matchhd.hd_statewith|Stateful_stater->incrr;List.iter(function|_chan_id,Ecb.Data_->()|_chan_id,Ecb.Closed->Logs.warn~src:section(funfmt->fmt"update_stateful_state: received Closed: should not happen, this is an eliom bug, please report it")|chan_id,Ecb.Full->stop_waitinghdchan_id)message|Stateless_state_->raise(Comet_error"update_stateful_state on stateless one")letset_positionposvalue=matchposwith|Ecb.Newest_->Ecb.Newestvalue|Ecb.After_->Ecb.Aftervalue|Ecb.LastNone->Ecb.Newestvalue|Ecb.Last(Some_)->Ecb.Aftervalueletposition_valuepos=matchposwithEcb.Newesti|Ecb.Afteri->i|Ecb.Last_->0letclose_all_channelshd=lets=hd.hd_activity.active_channelsinEliom_lib.String.Set.iter(funchan_id->stop_waitinghdchan_id)s;Eliom_lib.String.Set.fold(funchan_idl->(chan_id,None,Ecb.Closed)::l)s[]letupdate_stateless_statehd(message:stateless_message)=matchhd.hd_statewith|Stateless_stater->lettable=List.fold_left(funtable->function|chan_id,Ecb.Data(_,index)->(tryletstate=Eliom_lib.String.Table.findchan_idtableinifposition_valuestate.position<index+1thenEliom_lib.String.Table.addchan_id{statewithposition=set_positionstate.position(index+1)}tableelsetablewithNot_found->table)|chan_id,Ecb.Closed|chan_id,Ecb.Full->stop_waitinghdchan_id;Eliom_lib.String.Table.removechan_idtable)!rmessageinr:=table|Stateful_state_->raise(Comet_error"update_stateless_state on stateful one")letcall_service({hd_activity;hd_service=Ecb.Comet_service(srv,queue);_}ashd)=let*()=Configuration.sleep_before_next_request(fun()->hd_activity.focused)(fun()->hd_activity.active=`Idle)(fun()->hd_activity.active_waiter)inletrequest=make_requesthdinlet*s=call_service_after_load_endsrvqueue(hd_activity.active=`Idle,request)inLwt.return(Deriving_Json.from_stringEcb.answer_jsons)letdrop_message_index=letaux=function|chan,Ecb.Data(m,i)->chan,Somei,Ecb.Datam|chan,(Ecb.Closedasm)|chan,(Ecb.Fullasm)->chan,None,minList.mapauxletadd_no_index=letaux=function|chan,(Ecb.Data_asm)|chan,(Ecb.Closedasm)|chan,(Ecb.Fullasm)->chan,None,minList.mapauxletupdate_activity?(timeout=false)hd=ifhd.hd_activity.active<>`Inactive&&(timeout||not(Configuration.get()).Configuration.active_until_timeout)thenset_activityhd(expected_activityhd)letwait_datahd:(string*intoption*stringEcb.channel_data)listLwt.t=letrecauxretries=ifhd.hd_activity.active=`Inactivethenlet*()=hd.hd_activity.active_waiterinaux0elseLwt.try_bind(fun()->Lwt.pick[call_servicehd;hd.hd_activity.restart_waiter])(funs->matchswith|Ecb.Timeout->update_activity~timeout:truehd;aux0|Ecb.State_closed->Lwt.return(close_all_channelshd)|Ecb.Comet_errore->Lwt.fail(Comet_errore)|Ecb.Stateless_messagesl->letl=Array.to_listlinupdate_stateless_statehdl;Lwt.return(drop_message_indexl)|Ecb.Stateful_messagesl->letl=Array.to_listlinupdate_stateful_statehdl;Lwt.return(add_no_indexl))(fune->matchewith|Eliom_request.Failed_request(0|502|504)->ifretries>max_retriesthen(Logs.app~src:section(funfmt->fmt"connection failure");set_activityhd`Inactive;aux0)elselet*()=Js_of_ocaml_lwt.Lwt_js.sleep(delayretries)inaux(retries+1)|Restart->Logs.info~src:section(funfmt->fmt"restart");aux0|exn->Logs.app~src:section(funfmt->fmt("connection failure"^^"@\n%s")(Printexc.to_stringexn));let*()=handle_exn~exn()inLwt.failexn)inupdate_activityhd;aux0letcall_commands{hd_service=Ecb.Comet_service(srv,queue);_}command=ignore(Lwt.catch(fun()->call_service_after_load_endsrvqueue(false,Ecb.Stateful(Ecb.Commandscommand)))(funexn->Logs.app~src:section(funfmt->fmt("request failed"^^"@\n%s")(Printexc.to_stringexn));Lwt.return""))letclosehdchan_id=matchhd.hd_statewith|Stateful_state_->stop_waitinghdchan_id;call_commandshd[|Ecb.Closechan_id|]|Stateless_statemap->(tryletstate=Eliom_lib.String.Table.findchan_id!mapinifstate.count=1thenmap:=Eliom_lib.String.Table.removechan_id!mapelsemap:=Eliom_lib.String.Table.addchan_id{statewithcount=state.count-1}!mapwithNot_found->Logs.info~src:section(funfmt->fmt"trying to close a non existent channel: %s"chan_id))letadd_channel_statefulhdchan_id=hd.hd_activity.active_channels<-Eliom_lib.String.Set.addchan_idhd.hd_activity.active_channels;call_commandshd[|Eliom_comet_base.Registerchan_id|]letmin_pos=function|Ecb.Newesti,Ecb.Newestj->Ecb.Newest(minij)|Ecb.Afteri,Ecb.Afterj->Ecb.After(minij)|Ecb.Lasti,Ecb.Lastj->Ecb.Last(maxij)|p,Ecb.Last_->p|Ecb.Last_,p->p|_->raise_error~section"not corresponding position"letadd_channel_statelesshdchan_idkind=letpos=matchkindwith|Ecb.Newest_kindi->Ecb.Newesti|Ecb.After_kindi->Ecb.Afteri|Ecb.Last_kindi->Ecb.Lastiinhd.hd_activity.active_channels<-Eliom_lib.String.Set.addchan_idhd.hd_activity.active_channels;matchhd.hd_statewith|Stateful_state_->assertfalse|Stateless_statemap->(letstate=tryletold_state=Eliom_lib.String.Table.findchan_id!mapinletpos=min_pos(old_state.position,pos)in{count=old_state.count+1;position=pos}withNot_found->{count=1;position=pos}inmap:=Eliom_lib.String.Table.addchan_idstate!map);restarthdletinit_activity()=letactive_waiter,active_wakener=Lwt.wait()inletrestart_waiter,restart_wakener=Lwt.wait()in{active=`Inactive;focused=None;active_waiter;active_wakener;restart_waiter;restart_wakener;active_channels=Eliom_lib.String.Set.empty}letmakehd_servicehd_kind=lethd_state=matchhd_kindwith|Stateless->Stateless_state(refEliom_lib.String.Table.empty)|Stateful->Stateful_state(ref0)inlethd={hd_service;hd_state;hd_activity=init_activity()}inhandle_visibilityhd;hdendtype'ahandler={hd_service_handler:'aService_handler.t;hd_stream:(string*intoption*stringEcb.channel_data)Lwt_stream.t}lethandler_streamhd=Lwt_stream.map_list(funx->x)(Lwt_stream.from(fun()->Lwt.try_bind(fun()->Service_handler.wait_datahd)(funs->Lwt.return_somes)(fun_->Lwt.return_none)))letstateful_handler_table:(Ecb.comet_service,Service_handler.statefulhandler)Hashtbl.t=Hashtbl.create1letstateless_handler_table:(Ecb.comet_service,Service_handler.statelesshandler)Hashtbl.t=Hashtbl.create1letinit(service:Ecb.comet_service)kindtable=lethd_service_handler=Service_handler.makeservicekindinlethd_stream=handler_streamhd_service_handlerinlethd={hd_service_handler;hd_stream}inHashtbl.addtableservicehd;hdletget_stateful_hd(service:Ecb.comet_service):Service_handler.statefulhandler=tryHashtbl.findstateful_handler_tableservicewithNot_found->initserviceService_handler.statefulstateful_handler_tableletget_stateless_hd(service:Ecb.comet_service):Service_handler.statelesshandler=tryHashtbl.findstateless_handler_tableservicewithNot_found->initserviceService_handler.statelessstateless_handler_tableletactivate()=letf_{hd_service_handler;_}=Service_handler.activatehd_service_handlerinHashtbl.iterfstateless_handler_table;Hashtbl.iterfstateful_handler_tableletrestart()=letf_{hd_service_handler;_}=Service_handler.restarthd_service_handlerinHashtbl.iterfstateless_handler_table;Hashtbl.iterfstateful_handler_tableletclose=function|Ecb.Stateful_channel(chan_service,chan_id)->let{hd_service_handler;_}=get_stateful_hdchan_serviceinService_handler.closehd_service_handler(Ecb.string_of_chan_idchan_id)|Ecb.Stateless_channel(chan_service,chan_id,_kind)->let{hd_service_handler;_}=get_stateless_hdchan_serviceinService_handler.closehd_service_handler(Ecb.string_of_chan_idchan_id)letunmarshals:'a=Eliom_unwrap.unwrap(Eliom_lib.Url.decodes)0typeposition_relation=|Equal(* stateless after channels *)|Greater(* stateless newest channels *)typeposition=|No_position(* stateful channels*)|Positionofposition_relation*intoptionref(* stateless channels *)letposition_of_kind=function|Ecb.After_kindi->Position(Equal,ref(Somei))|Ecb.Newest_kindi->Position(Greater,ref(Somei))|Ecb.Last_kindNone->Position(Greater,refNone)|Ecb.Last_kind(Some_)->Position(Equal,refNone)letcheck_and_update_positionpositionmsg_posdata=matchposition,msg_pos,datawith|No_position,None,_->true|No_position,Some_,_|Position_,None,Ecb.Data_->raise_error~section"check_position: channel kind and message do not match"|Position_,None,(Ecb.Full|Ecb.Closed)->true|Position(relation,r),Somej,_->(match!rwith|None->r:=Some(j+1);true|Somei->ifmatchrelationwithEqual->j=i|Greater->j>=ithen(r:=Some(j+1);true)elsefalse)(* stateless channels are registered with a position: when a channel
is registered more than one time, it is possible to receive old
messages: the position is used to filter them out. *)letregister'hdposition(_:Ecb.comet_service)(chan_id:'aEcb.chan_id)=letchan_id=Ecb.string_of_chan_idchan_idinletstream=Lwt_stream.filter_map_s(function|id,pos,datawhenid=chan_id&&check_and_update_positionpositionposdata->(matchdatawith|Ecb.Full->Lwt.failChannel_full|Ecb.Closed->Lwt.failChannel_closed|Ecb.Datax->Lwt.return_some(unmarshalx:'a))|_->Lwt.return_none)(Lwt_stream.clonehd.hd_stream)inletprotect_and_closet=lett'=Lwt.protectedtinLwt.on_cancelt'(fun()->Service_handler.closehd.hd_service_handlerchan_id);t'in(* protect the stream from cancels *)Lwt_stream.from(fun()->protect_and_close(Lwt_stream.getstream))letregister_stateful?(wake=true)servicechan_id=lethd=get_stateful_hdserviceinletstream=register'hdNo_positionservicechan_idinletchan_id=Ecb.string_of_chan_idchan_idinService_handler.add_channel_statefulhd.hd_service_handlerchan_id;ifwakethenService_handler.activatehd.hd_service_handler;streamletregister_stateless?(wake=true)servicechan_idkind=lethd=get_stateless_hdserviceinletstream=register'hd(position_of_kindkind)servicechan_idinletchan_id=Ecb.string_of_chan_idchan_idinService_handler.add_channel_statelesshd.hd_service_handlerchan_idkind;ifwakethenService_handler.activatehd.hd_service_handler;streamletregister?(wake=true)(wrapped_chan:'aEcb.wrapped_channel)=matchwrapped_chanwith|Ecb.Stateful_channel(s,c)->register_stateful~wakesc|Ecb.Stateless_channel(s,c,kind)->register_stateless~wakesckindletinternal_unwrap(wrapped_chan,_unwrapper)=registerwrapped_chanlet()=Eliom_unwrap.register_unwrapperEliom_common.comet_channel_unwrap_idinternal_unwrapletis_active()=(*VVV Check. Isn't it the contrary? (fold from `Inactive?) *)letmaxab=matchawith|`Inactive->`Inactive|`Idle->letb=b()inifb=`Activethen`Idleelseb|`Active->b()inletf_hdactive=maxactive(fun()->Service_handler.is_activehd.hd_service_handler)inmax(Hashtbl.foldfstateless_handler_table`Active)(fun()->Hashtbl.foldfstateful_handler_table`Active)moduleChannel=structtype'at='aLwt_stream.tendletforce_link=()