123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346(*{{{ Copyright (c) 2014 Andy Ray
* Copyright (c) 2014 Anil Madhavapeddy <anil@recoil.org>
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*
}}}*)openJs_of_ocamlmoduleC=CohttpmoduleCLB=Cohttp_lwt.Bodylet(>>=)=Lwt.(>>=)let(>|=)=Lwt.(>|=)moduletypeParams=sigvalchunked_response:boolvalchunk_size:intvalconvert_body_string:Js.js_stringJs.t->stringvalwith_credentials:boolendletxhr_response_supported=(* from http://stackoverflow.com/questions/8926505/how-to-feature-detect-if-xmlhttprequest-supports-responsetype-arraybuffer *)lazy(letxhr=XmlHttpRequest.create()inletrt=xhr##.responseTypeinJs.to_string(Js.typeofrt)="string")letbinary_stringstr=letlen=String.lengthstrinleta=new%jsTyped_array.uint8Arrayleninfori=0tolen-1doTyped_array.setai(Char.codestr.[i])done;aletstring_of_uint8arrayu8aoffsetlen=String.initlen(funi->Char.chr(Typed_array.unsafe_getu8a(offset+i)))moduleString_io=Cohttp.Private.String_iomoduleIO=Cohttp_lwt.Private.String_iomoduleHeader_io=Cohttp.Private.Header_io.Make(IO)moduleBody_builder(P:Params)=structletsrc=Logs.Src.create"cohttp.lwt.jsoo"~doc:"Cohttp Lwt JSOO module"moduleLog=(valLogs.src_logsrc:Logs.LOG)(* perform the body transfer in chunks from string. *)letchunked_body_strtext=letbody_len=text##.lengthinletpos=ref0inletchunkerizer()=if!pos=body_lenthenLwt.returnC.Transfer.Doneelseif!pos+P.chunk_size>=body_lenthen(letstr=text##(substring_toEnd!pos)inpos:=body_len;Lwt.return(C.Transfer.Final_chunk(P.convert_body_stringstr)))elseletstr=text##(substring!pos(!pos+P.chunk_size))inpos:=!pos+P.chunk_size;Lwt.return(C.Transfer.Chunk(P.convert_body_stringstr))inifbody_len=0thenCLB.emptyelseCLB.of_stream(CLB.create_streamchunkerizer())(* perform the body transfer in chunks from arrayBuffer. *)letchunked_body_binary(ab:Typed_array.arrayBufferJs.t)=letbody_len=ab##.byteLengthinletu8a=new%jsTyped_array.uint8Array_fromBufferabinletpos=ref0inletchunkerizer()=if!pos=body_lenthenLwt.returnC.Transfer.Doneelseif!pos+P.chunk_size>=body_lenthen(letstr=string_of_uint8arrayu8a!pos(body_len-!pos)inpos:=body_len;Lwt.return(C.Transfer.Final_chunkstr))elseletstr=string_of_uint8arrayu8a!posP.chunk_sizeinpos:=!pos+P.chunk_size;Lwt.return(C.Transfer.Chunkstr)inifbody_len=0thenCLB.emptyelseCLB.of_stream(CLB.create_streamchunkerizer())(* choose between chunked and direct transfer *)letget=function|`Stringjs_str->ifP.chunked_responsethenchunked_body_strjs_strelseCLB.of_string(P.convert_body_stringjs_str)|`ArrayBufferab->ifP.chunked_responsethenchunked_body_binaryabelseletu8a=new%jsTyped_array.uint8Array_fromBufferabinCLB.of_string(string_of_uint8arrayu8a0ab##.byteLength)letconstruct_bodyxml=(* construct body *)letb=letrespText()=Js.Opt.casexml##.responseText(fun()->`String(Js.string""))(funs->`Strings)inmatchLazy.forcexhr_response_supportedwith|truewhenJs.Opt.returnxml##.response==Js.null->Log.warn(funm->m"XHR Response is null; using empty string");`String(Js.string"")|true->Js.Opt.case(File.CoerceTo.arrayBufferxml##.response)(fun()->Log.warn(funm->m"XHR Response is not an arrayBuffer; using responseText");respText())(funab->`ArrayBufferab)|false->respText()ingetbendmoduleMake_api(X:sigmoduleRequest:Cohttp.S.RequestmoduleResponse:Cohttp.S.Responsevalcall:?headers:Http.Header.t->?body:Cohttp_lwt.Body.t->Http.Method.t->Uri.t->(Response.t*Cohttp_lwt.Body.t)Lwt.tend)=structmoduleRequest=X.RequestmoduleResponse=X.Responsetype'aio='aLwt.ttypebody=Cohttp_lwt.Body.ttypectx=unittype'awith_context=?ctx:ctx->'aletmap_contextvf?ctx=f(v?ctx)letcall?ctx:_?headers?body?chunked:_methuri=X.call?headers?bodymethuri(* The HEAD should not have a response body *)lethead?ctx?headersuri=letopenLwtincall?ctx?headers~chunked:false`HEADuri>|=fstletget?ctx?headersuri=call?ctx?headers~chunked:false`GETuriletdelete?ctx?body?chunked?headersuri=call?ctx?headers?body?chunked`DELETEuriletpost?ctx?body?chunked?headersuri=call?ctx?headers?body?chunked`POSTuriletput?ctx?body?chunked?headersuri=call?ctx?headers?body?chunked`PUTuriletpatch?ctx?body?chunked?headersuri=call?ctx?headers?body?chunked`PATCHuriletpost_form?ctx?headers~paramsuri=letheaders=C.Header.add_optheaders"content-type""application/x-www-form-urlencoded"inletbody=Cohttp_lwt.Body.of_string(Uri.encoded_of_queryparams)inpost?ctx~chunked:false~headers~bodyuriexceptionCohttp_lwt_xhr_set_cache_not_implementedletset_cache_=raiseCohttp_lwt_xhr_set_cache_not_implemented(* No implementation (can it be done?). What should the failure exception be? *)exceptionCohttp_lwt_xhr_callv_not_implementedletcallv?ctx:__uri_reqs=raiseCohttp_lwt_xhr_callv_not_implemented(* ??? *)endmoduleMake_client_async(P:Params)=Make_api(structmoduleResponse=Cohttp.ResponsemoduleRequest=Cohttp.RequestmoduleBb=Body_builder(P)letcall?headers?bodymethuri=letxml=XmlHttpRequest.create()inxml##.withCredentials:=Js.boolP.with_credentials;ifLazy.forcexhr_response_supportedthenxml##.responseType:=Js.string"arraybuffer";let(res:(Http.Response.tLwt.t*CLB.t)Lwt.t),wake=Lwt.task()inlet()=xml##(_open(Js.string(C.Code.string_of_methodmeth))(Js.string(Uri.to_stringuri))Js._true)(* asynchronous call *)in(* set request headers *)let()=matchheaderswith|None->()|Someheaders->C.Header.iter(funkv->(* some headers lead to errors in the javascript console, should
we filter then out here? *)xml##(setRequestHeader(Js.stringk)(Js.stringv)))headersinxml##.onreadystatechange:=Js.wrap_callback(fun_->matchxml##.readyStatewith|XmlHttpRequest.DONE->(tryletbody=Bb.construct_bodyxmlin(* Note; a type checker subversion seems to be possible here (4.01.0).
* Remove the type constraint on Lwt.task above and return any old
* guff here. It'll compile and crash in the browser! *)(* (re-)construct the response *)letresp_headers=Js.to_stringxml##getAllResponseHeadersinletchannel=String_io.open_inresp_headersinletresponse=Lwt.(Header_io.parsechannel>|=funresp_headers->Cohttp.Response.make~version:`HTTP_1_1~status:(C.Code.status_of_codexml##.status)~encoding:(CLB.transfer_encodingbody)~headers:resp_headers())inLwt.wakeupwake(response,body)with|e(* If we exhaust the stack, it is possible that
Lwt.wakeup just above marks the promise as
completed, but raises Stack_overflow while
running the promise callbacks. In this case
waking calling wakeup_exn on the already
completed promise would raise an Invalid_arg
exception, so although the promise is in a
really bad state we may as well let the actual
Stack_overflow exception go through. *)whenLwt.stateres=Lwt.Sleep->Lwt.wakeup_exnwakee)|_->());(* perform call *)(matchbodywith|None->Lwt.returnxml##(sendJs.null)|Somebody->CLB.to_stringbody>>=funbody->letbs=binary_stringbodyin(*Js.Opt.case (File.CoerceTo.blob (Obj.magic blob))
(fun () -> failwith "could not coerce to blob")
(fun blob -> Lwt.return (xml##(send_blob blob)))*)(*Lwt.return (xml##send (Js.Opt.return bs)) *)Lwt.return(xml##send(Js.Opt.return(Obj.magicbs))))>>=fun()->Lwt.on_cancelres(fun()->xml##abort);(* unwrap the response *)Lwt.(res>>=fun(r,b)->r>>=funr->Lwt.return(r,b))end)moduleMake_client_sync(P:Params)=Make_api(structmoduleResponse=Cohttp.ResponsemoduleRequest=Cohttp.RequestmoduleBb=Body_builder(P)letcall?headers?bodymethuri=letxml=XmlHttpRequest.create()inxml##.withCredentials:=Js.boolP.with_credentials;ifLazy.forcexhr_response_supportedthenxml##.responseType:=Js.string"arraybuffer";let()=xml##(_open(Js.string(C.Code.string_of_methodmeth))(Js.string(Uri.to_stringuri))Js._false)(* synchronous call *)in(* set request headers *)let()=matchheaderswith|None->()|Someheaders->C.Header.iter(funkv->(* some headers lead to errors in the javascript console, should
we filter then out here? *)xml##(setRequestHeader(Js.stringk)(Js.stringv)))headersin(* perform call *)(matchbodywith|None->Lwt.returnxml##(sendJs.null)|Somebody->CLB.to_stringbody>|=funbody->letbs=binary_stringbodyinxml##(send(Js.Opt.return(Obj.magicbs))))>>=fun_body->letbody=Bb.construct_bodyxmlin(* (re-)construct the response *)letresp_headers=Js.to_stringxml##getAllResponseHeadersinHeader_io.parse(String_io.open_inresp_headers)>>=funresp_headers->letresponse=Response.make~version:`HTTP_1_1~status:(Cohttp.Code.status_of_codexml##.status)~encoding:(CLB.transfer_encodingbody)~headers:resp_headers()inLwt.return(response,body)end)moduleClient=Make_client_async(structletchunked_response=trueletchunk_size=128*1024letconvert_body_string=Js.to_bytestringletwith_credentials=falseend)moduleClient_sync=Make_client_sync(structletchunked_response=falseletchunk_size=0letconvert_body_string=Js.to_bytestringletwith_credentials=falseend)