@@ -466,7 +466,105 @@ enum FunctionsConstants {
466466 }
467467 }
468468 }
469+
470+ @available ( iOS 13 , macCatalyst 13 , macOS 10 . 15 , tvOS 13 , watchOS 7 , * )
471+ func stream( at url: URL ,
472+ withObject data: Any ? ,
473+ options: HTTPSCallableOptions ? ,
474+ timeout: TimeInterval ) async throws
475+ -> AsyncThrowingStream < HTTPSCallableResult , Error > {
476+ let context = try await contextProvider. context ( options: options)
477+ let fetcher = try makeFetcherForStreamableContent (
478+ url: url,
479+ data: data,
480+ options: options,
481+ timeout: timeout,
482+ context: context
483+ )
484+
485+ do {
486+ let rawData = try await fetcher. beginFetch ( )
487+ return try callableResultFromResponseAsync ( data: rawData, error: nil )
488+ } catch {
489+ // This method always throws when `error` is not `nil`, but ideally,
490+ // it should be refactored so it looks less confusing.
491+ return try callableResultFromResponseAsync ( data: nil , error: error)
492+ }
493+ }
494+
495+ @available ( iOS 13 . 0 , * )
496+ func callableResultFromResponseAsync( data: Data ? ,
497+ error: Error ? ) throws -> AsyncThrowingStream <
498+ HTTPSCallableResult , Error
499+
500+ > {
501+ let processedData =
502+ try processResponseDataForStreamableContent (
503+ from: data,
504+ error: error
505+ )
506+
507+ return processedData
508+ }
469509
510+ private func makeFetcherForStreamableContent( url: URL ,
511+ data: Any ? ,
512+ options: HTTPSCallableOptions ? ,
513+ timeout: TimeInterval ,
514+ context: FunctionsContext ) throws -> GTMSessionFetcher {
515+ let request = URLRequest (
516+ url: url,
517+ cachePolicy: . useProtocolCachePolicy,
518+ timeoutInterval: timeout
519+ )
520+ let fetcher = fetcherService. fetcher ( with: request)
521+
522+ let data = data ?? NSNull ( )
523+ let encoded = try serializer. encode ( data)
524+ let body = [ " data " : encoded]
525+ let payload = try JSONSerialization . data ( withJSONObject: body, options: [ . fragmentsAllowed] )
526+ fetcher. bodyData = payload
527+
528+ // Set the headers for starting a streaming session.
529+ fetcher. setRequestValue ( " application/json " , forHTTPHeaderField: " Content-Type " )
530+ fetcher. setRequestValue ( " text/event-stream " , forHTTPHeaderField: " Accept " )
531+ fetcher. request? . httpMethod = " POST "
532+ if let authToken = context. authToken {
533+ let value = " Bearer \( authToken) "
534+ fetcher. setRequestValue ( value, forHTTPHeaderField: " Authorization " )
535+ }
536+
537+ if let fcmToken = context. fcmToken {
538+ fetcher. setRequestValue ( fcmToken, forHTTPHeaderField: Constants . fcmTokenHeader)
539+ }
540+
541+ if options? . requireLimitedUseAppCheckTokens == true {
542+ if let appCheckToken = context. limitedUseAppCheckToken {
543+ fetcher. setRequestValue (
544+ appCheckToken,
545+ forHTTPHeaderField: Constants . appCheckTokenHeader
546+ )
547+ }
548+ } else if let appCheckToken = context. appCheckToken {
549+ fetcher. setRequestValue (
550+ appCheckToken,
551+ forHTTPHeaderField: Constants . appCheckTokenHeader
552+ )
553+ }
554+ // Remove after genStream is updated on the emulator or deployed
555+ #if DEBUG
556+ fetcher. allowLocalhostRequest = true
557+ fetcher. allowedInsecureSchemes = [ " http " ]
558+ #endif
559+ // Override normal security rules if this is a local test.
560+ if emulatorOrigin != nil {
561+ fetcher. allowLocalhostRequest = true
562+ fetcher. allowedInsecureSchemes = [ " http " ]
563+ }
564+
565+ return fetcher
566+ }
567+
470568 private func makeFetcher( url: URL ,
471569 data: Any ? ,
472570 options: HTTPSCallableOptions ? ,
@@ -561,21 +659,73 @@ enum FunctionsConstants {
561659 // Case 4: `error` is `nil`; `data` is not `nil`; `data` doesn’t specify an error -> OK
562660 return data
563661 }
662+
663+ @available ( iOS 13 , macCatalyst 13 , macOS 10 . 15 , tvOS 13 , watchOS 7 , * )
664+ private func processResponseDataForStreamableContent( from data: Data ? ,
665+ error: Error ? ) throws -> AsyncThrowingStream <
666+ HTTPSCallableResult ,
667+ Error
668+ > {
669+
670+ return AsyncThrowingStream { continuation in
671+ Task {
672+ var resultArray = [ String] ( )
673+ do {
674+ if let error = error {
675+ throw error
676+ }
677+
678+ guard let data = data else {
679+ throw NSError ( domain: FunctionsErrorDomain . description, code: - 1 , userInfo: nil )
680+ }
681+
682+ if let dataChunk = String ( data: data, encoding: . utf8) {
683+ // We remove the "data :" field so it can be safely parsed to Json.
684+ let dataChunkToJson = dataChunk. split ( separator: " \n " ) . map {
685+ String ( $0. dropFirst ( 6 ) )
686+ }
687+ resultArray. append ( contentsOf: dataChunkToJson)
688+ } else {
689+ throw NSError ( domain: FunctionsErrorDomain . description, code: - 1 , userInfo: nil )
690+ }
691+
692+ for dataChunk in resultArray {
693+ let json = try callableResultFromResponse (
694+ data: dataChunk. data ( using: . utf8, allowLossyConversion: true ) ,
695+ error: error
696+ )
697+ continuation. yield ( HTTPSCallableResult ( data: json. data) )
698+ }
699+
700+ continuation. onTermination = { @Sendable _ in
701+ // Callback for cancelling the stream
702+ continuation. finish ( )
703+ }
704+ // Close the stream once it's done
705+ continuation. finish ( )
706+ } catch {
707+ continuation. finish ( throwing: error)
708+ }
709+ }
710+ }
711+ }
564712
565713 private func responseDataJSON( from data: Data ) throws -> Any {
566- let responseJSONObject = try JSONSerialization . jsonObject ( with: data)
714+ let responseJSONObject = try JSONSerialization . jsonObject ( with: data)
567715
568- guard let responseJSON = responseJSONObject as? NSDictionary else {
569- let userInfo = [ NSLocalizedDescriptionKey: " Response was not a dictionary. " ]
570- throw FunctionsError ( . internal, userInfo: userInfo)
571- }
716+ guard let responseJSON = responseJSONObject as? NSDictionary else {
717+ let userInfo = [ NSLocalizedDescriptionKey: " Response was not a dictionary. " ]
718+ throw FunctionsError ( . internal, userInfo: userInfo)
719+ }
572720
573- // `result` is checked for backwards compatibility:
574- guard let dataJSON = responseJSON [ " data " ] ?? responseJSON [ " result " ] else {
575- let userInfo = [ NSLocalizedDescriptionKey: " Response is missing data field. " ]
576- throw FunctionsError ( . internal, userInfo: userInfo)
721+ // `result` is checked for backwards compatibility,
722+ // `message` is checked for StramableContent:
723+ guard let dataJSON = responseJSON [ " data " ] ?? responseJSON [ " result " ] ?? responseJSON [ " message " ]
724+ else {
725+ let userInfo = [ NSLocalizedDescriptionKey: " Response is missing data field. " ]
726+ throw FunctionsError ( . internal, userInfo: userInfo)
727+ }
728+
729+ return dataJSON
577730 }
578-
579- return dataJSON
580- }
581731}
0 commit comments