123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801# 1 "src/lib/eliom_comet.client.ml"(* 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_ocamlmoduleEcb=Eliom_comet_baseletsection=Eliom_lib.Lwt_log.Section.make"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=(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()=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->Eliom_lib.Lwt_log.raise_error~section~exns|None->Eliom_lib.Lwt_log.debug~sections)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)inignore@@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(new%jsJs.date_now)##getTimeinletvisibility_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=(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((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%lwt()=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%lwt()=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->Eliom_lib.Lwt_log.ign_warning~section"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%lwt()=Configuration.sleep_before_next_request(fun()->hd_activity.focused)(fun()->hd_activity.active=`Idle)(fun()->hd_activity.active_waiter)inletrequest=make_requesthdinlet%lwts=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%lwt()=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(Eliom_lib.Lwt_log.ign_notice~section"connection failure";set_activityhd`Inactive;aux0)elselet%lwt()=Js_of_ocaml_lwt.Lwt_js.sleep(delayretries)inaux(retries+1)|Restart->Eliom_lib.Lwt_log.ign_info~section"restart";aux0|exn->Eliom_lib.Lwt_log.ign_notice~exn~section"connection failure";let%lwt()=handle_exn~exn()inLwt.failexn)inupdate_activityhd;aux0letcall_commands{hd_service=Ecb.Comet_service(srv,queue);_}command=ignore(try%lwtcall_service_after_load_endsrvqueue(false,Ecb.Stateful(Ecb.Commandscommand))withexn->Eliom_lib.Lwt_log.ign_notice_f~section~exn"request failed";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->Eliom_lib.Lwt_log.ign_info_f~section"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|_->Eliom_lib.Lwt_log.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_->Eliom_lib.Lwt_log.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=()