@@ -20,6 +20,7 @@ export const ACTOR_ID_HEADER_NAME = "x-deno-isolate-instance-id";
2020export const ACTOR_ID_QS_NAME = "deno_isolate_instance_id" ;
2121export const ACTOR_CONSTRUCTOR_NAME_HEADER = "x-error-constructor-name" ;
2222
23+ export type ConnectMode = "websocket" | "stream" ;
2324export type StubFactory < TInstance > = {
2425 id : StubFactoryFn < TInstance > ;
2526} ;
@@ -56,7 +57,7 @@ export interface ActorInvoker<
5657 method : string ,
5758 methodArgs : unknown [ ] ,
5859 metadata : unknown ,
59- connect : true ,
60+ connect : ConnectMode ,
6061 ) : Promise < TChannel > ;
6162}
6263export class ActorAwaiter <
@@ -67,6 +68,7 @@ export class ActorAwaiter<
6768 TResponse
6869 > ,
6970 DuplexChannel < any , any > {
71+ mode : ConnectMode = "websocket" ;
7072 ch : Promise < TChannel > | null = null ;
7173 ctrl : AbortController ;
7274 _disconnected : PromiseWithResolvers < void > = Promise . withResolvers ( ) ;
@@ -83,6 +85,9 @@ export class ActorAwaiter<
8385 this . close ( ) ;
8486 }
8587 async close ( ) {
88+ if ( this . ch === null ) {
89+ return ;
90+ }
8691 const ch = await this . channel ;
8792 await ch . close ( ) ;
8893 this . ch = null ;
@@ -122,7 +127,7 @@ export class ActorAwaiter<
122127 this . actorMethod ,
123128 this . methodArgs ,
124129 this . mMetadata ,
125- true ,
130+ this . mode ,
126131 ) ;
127132 const nextConnection = async ( ) => {
128133 const ch = await retry ( connect , {
@@ -207,8 +212,15 @@ export type StubOptions<TInstance extends Actor> = ProxyOptions<TInstance>;
207212
208213export type PromisifyKey < Actor , key extends keyof Actor > = Actor [ key ] extends
209214 ( ...args : infer Args ) => Awaited < infer Return >
210- ? Return extends ChannelUpgrader < infer TSend , infer TReceive >
211- ? { ( ...args : Args ) : DuplexChannel < TReceive , TSend > }
215+ ? Return extends ChannelUpgrader < infer TSend , infer TReceive > ? {
216+ (
217+ ...args : Args
218+ ) :
219+ & DuplexChannel < TReceive , TSend >
220+ & ( TReceive extends Uint8Array ? { mode ?: ConnectMode }
221+ // deno-lint-ignore ban-types
222+ : { } ) ;
223+ }
212224 : { ( ...args : Args ) : Promise < Awaited < Return > > }
213225 : Actor [ key ] ;
214226
@@ -445,9 +457,15 @@ export const createHttpInvoker = <
445457 }
446458 const actorsServer = server ?? _server ! ;
447459 return {
448- invoke : async ( name , method , methodArgs , metadata , connect ?: true ) => {
460+ invoke : async (
461+ name ,
462+ method ,
463+ methodArgs ,
464+ metadata ,
465+ connect ?: ConnectMode ,
466+ ) => {
449467 const endpoint = urlFor ( actorsServer . url , name , method ) ;
450- if ( connect ) {
468+ if ( connect === "websocket" ) {
451469 const url = new URL ( `${ endpoint } ?args=${
452470 encodeURIComponent (
453471 btoa (
@@ -470,6 +488,97 @@ export const createHttpInvoker = <
470488 ) ;
471489 return makeWebSocket ( ws , options ?. maxWsChunkSize ) as Promise < TChannel > ;
472490 }
491+ if ( connect === "stream" ) {
492+ const url = new URL ( `${ endpoint } ?args=${
493+ encodeURIComponent (
494+ btoa (
495+ JSON . stringify ( {
496+ args : methodArgs ?? [ ] ,
497+ metadata : metadata ?? { } ,
498+ } ) ,
499+ ) ,
500+ )
501+ } &${ ACTOR_ID_QS_NAME } =${ actorId } `) ;
502+
503+ const sendChan = makeChan < unknown > ( ) ;
504+ const recvChan = makeChan < unknown > ( ) ;
505+
506+ // Create streams for request body with an encoder
507+ const { readable : requestReadable , writable : requestWritable } =
508+ new TransformStream ( {
509+ transform ( chunk , controller ) {
510+ // Ensure we're sending Uint8Array
511+ if ( chunk instanceof Uint8Array ) {
512+ controller . enqueue ( chunk ) ;
513+ } else {
514+ controller . enqueue (
515+ new TextEncoder ( ) . encode ( JSON . stringify ( chunk ) ) ,
516+ ) ;
517+ }
518+ } ,
519+ } ) ;
520+
521+ const requestWriter = requestWritable . getWriter ( ) ;
522+
523+ const fetcher = options ?. fetcher ?. fetch ?? fetch ;
524+ fetcher ( url , {
525+ credentials : actorsServer ?. credentials ,
526+ method : "POST" ,
527+ headers : {
528+ "Content-Type" : "application/octet-stream" ,
529+ "Transfer-Encoding" : "chunked" ,
530+ [ options ?. actorIdHeaderName ?? ACTOR_ID_HEADER_NAME ] : actorId ,
531+ ...actorsServer . deploymentId
532+ ? { [ "x-deno-deployment-id" ] : actorsServer . deploymentId }
533+ : { } ,
534+ } ,
535+ body : requestReadable ,
536+ } ) . then ( async ( response ) => {
537+ if ( ! response . ok ) {
538+ throw new Error ( `HTTP error! status: ${ response . status } ` ) ;
539+ }
540+
541+ const reader = response . body ?. getReader ( ) ;
542+ if ( ! reader ) {
543+ throw new Error ( "Response body is null" ) ;
544+ }
545+
546+ let err : unknown | undefined = undefined ;
547+ try {
548+ while ( true ) {
549+ const { done, value } = await reader . read ( ) ;
550+ if ( done ) break ;
551+ await recvChan . send ( value ) ;
552+ }
553+ } catch ( e ) {
554+ err = e ;
555+ } finally {
556+ reader . releaseLock ( ) ;
557+ recvChan . close ( err ) ;
558+ }
559+ } ) . catch ( ( error ) => {
560+ recvChan . close ( error ) ;
561+ } ) ;
562+
563+ // Handle sending data through the request body
564+ ( async ( ) => {
565+ let err : unknown | undefined = undefined ;
566+ try {
567+ for await ( const chunk of sendChan . recv ( ) ) {
568+ await requestWriter . write ( chunk ) ;
569+ }
570+ } catch ( e ) {
571+ err = e ;
572+ } finally {
573+ await requestWriter . close ( ) ;
574+ sendChan . close ( err ) ;
575+ }
576+ } ) ( ) ;
577+
578+ // Create and return the duplex channel
579+ const channel = makeDuplexChannelWith ( sendChan , recvChan ) ;
580+ return channel as TChannel ;
581+ }
473582 const abortCtrl = new AbortController ( ) ;
474583 const fetcher = options ?. fetcher ?. fetch ?? fetch ;
475584 const resp = await fetcher (
0 commit comments