@@ -48,6 +48,69 @@ const getBodyPreview = (body: unknown): { preview: string; size?: number } => {
4848 return { preview : String ( body ) } ;
4949} ;
5050
51+ const readStreamBody = async ( stream : ReadableStream ) : Promise < string > => {
52+ try {
53+ return await new Response ( stream ) . text ( ) ;
54+ } catch {
55+ return "" ;
56+ }
57+ } ;
58+
59+ const readStreamBlob = async ( stream : ReadableStream ) : Promise < Blob | null > => {
60+ try {
61+ return await new Response ( stream ) . blob ( ) ;
62+ } catch {
63+ return null ;
64+ }
65+ } ;
66+
67+ const ensureScramjetResponse = ( response : any ) => {
68+ if ( ! response ) return response ;
69+ const BareResponseCtor = ( globalThis as any ) . $scramjet ?. BareResponse ;
70+ if ( response instanceof Response && BareResponseCtor ?. fromNativeResponse ) {
71+ return BareResponseCtor . fromNativeResponse ( response ) ;
72+ }
73+ if ( ! Array . isArray ( response . rawHeaders ) && response . headers ) {
74+ try {
75+ response . rawHeaders = [ ...response . headers . entries ( ) ] ;
76+ } catch {
77+ response . rawHeaders = [ ] ;
78+ }
79+ }
80+ return response ;
81+ } ;
82+
83+ const getHeaderValue = (
84+ headers : Array < [ string , string ] > | undefined ,
85+ name : string
86+ ) => {
87+ if ( ! headers ) return undefined ;
88+ const match = headers . find (
89+ ( [ key ] ) => key . toLowerCase ( ) === name . toLowerCase ( )
90+ ) ;
91+ return match ? match [ 1 ] : undefined ;
92+ } ;
93+
94+ const isMediaContentType = ( contentType ?: string | null ) =>
95+ ! ! contentType &&
96+ ( contentType . toLowerCase ( ) . startsWith ( "image/" ) ||
97+ contentType . toLowerCase ( ) . startsWith ( "video/" ) ) ;
98+
99+ const getMediaUrlFromBody = (
100+ body : unknown ,
101+ contentType ?: string | null
102+ ) : { url ?: string ; size ?: number } => {
103+ if ( ! isMediaContentType ( contentType ) || body == null ) return { } ;
104+ if ( body instanceof Blob ) {
105+ return { url : URL . createObjectURL ( body ) , size : body . size } ;
106+ }
107+ if ( body instanceof ArrayBuffer ) {
108+ const blob = new Blob ( [ body ] , { type : contentType ?? "" } ) ;
109+ return { url : URL . createObjectURL ( blob ) , size : blob . size } ;
110+ }
111+ return { } ;
112+ } ;
113+
51114export const App : Component <
52115 { } ,
53116 { } ,
@@ -58,6 +121,7 @@ export const App: Component<
58121 requestSeq : number ;
59122 requestIdByRequest : WeakMap < object , string > ;
60123 requestStartByRequest : WeakMap < object , number > ;
124+ preResponseBodyStreamByRequest : WeakMap < object , ReadableStream > ;
61125 requests : RequestEntry [ ] ;
62126 selectedId : string | null ;
63127 }
@@ -73,6 +137,7 @@ export const App: Component<
73137 this . requestSeq = 0 ;
74138 this . requestIdByRequest = new WeakMap ( ) ;
75139 this . requestStartByRequest = new WeakMap ( ) ;
140+ this . preResponseBodyStreamByRequest = new WeakMap ( ) ;
76141
77142 this . frame = controller . createFrame ( this . frameel ) ;
78143
@@ -112,11 +177,60 @@ export const App: Component<
112177
113178 plugin . tap (
114179 this . frame . hooks . fetch . preresponse ,
115- ( context : any , props : any ) => {
180+ async ( context : any , props : any ) => {
116181 const id = this . requestIdByRequest . get ( context . request as object ) ;
117182 if ( ! id ) return ;
118- const preHeaders = normalizeHeaders ( props . response ?. rawHeaders ) ;
119- const preBodyInfo = getBodyPreview ( props . response ?. body ) ;
183+ props . response = ensureScramjetResponse ( props . response ) ;
184+ const preHeaders = normalizeHeaders (
185+ ( props . response as any ) ?. rawHeaders
186+ ) ;
187+ const preContentType = getHeaderValue ( preHeaders , "content-type" ) ;
188+
189+ let preBodyInfo : { preview : string ; size ?: number } = {
190+ preview : "" ,
191+ } ;
192+ let preMediaUrl : string | undefined ;
193+ if (
194+ props . response ?. body &&
195+ typeof ReadableStream !== "undefined" &&
196+ props . response . body instanceof ReadableStream
197+ ) {
198+ const [ streamForResponse , streamForPreview ] =
199+ props . response . body . tee ( ) ;
200+ const [ streamForStore , streamForRead ] = streamForPreview . tee ( ) ;
201+ if ( props . response instanceof Response ) {
202+ const rebuilt = new Response ( streamForResponse , props . response ) ;
203+ props . response = ensureScramjetResponse ( rebuilt ) ;
204+ } else {
205+ props . response . body = streamForResponse ;
206+ }
207+ this . preResponseBodyStreamByRequest . set (
208+ context . request as object ,
209+ streamForStore
210+ ) ;
211+ if ( isMediaContentType ( preContentType ) ) {
212+ const blob = await readStreamBlob ( streamForRead ) ;
213+ if ( blob ) {
214+ preMediaUrl = URL . createObjectURL ( blob ) ;
215+ preBodyInfo = {
216+ preview : "" ,
217+ size : blob . size ,
218+ } ;
219+ }
220+ } else {
221+ const previewText = await readStreamBody ( streamForRead ) ;
222+ preBodyInfo = getBodyPreview ( previewText ) ;
223+ }
224+ } else {
225+ const media = getMediaUrlFromBody (
226+ props . response ?. body ,
227+ preContentType
228+ ) ;
229+ preMediaUrl = media . url ;
230+ preBodyInfo = media . url
231+ ? { preview : "" , size : media . size }
232+ : getBodyPreview ( props . response ?. body ) ;
233+ }
120234
121235 this . requests = this . requests . map ( ( entry ) =>
122236 entry . id === id
@@ -125,6 +239,7 @@ export const App: Component<
125239 responseHeadersPre : preHeaders ,
126240 responseBodyPreviewPre : preBodyInfo . preview ,
127241 responseBodySizePre : preBodyInfo . size ,
242+ responseBodyMediaUrlPre : preMediaUrl ,
128243 }
129244 : entry
130245 ) ;
@@ -133,9 +248,10 @@ export const App: Component<
133248
134249 plugin . tap (
135250 this . frame . hooks . fetch . response ,
136- ( context : any , props : any ) => {
251+ async ( context : any , props : any ) => {
137252 const id = this . requestIdByRequest . get ( context . request as object ) ;
138253 if ( ! id ) return ;
254+ props . response = ensureScramjetResponse ( props . response ) ;
139255 const start = this . requestStartByRequest . get (
140256 context . request as object
141257 ) ;
@@ -145,9 +261,48 @@ export const App: Component<
145261 : undefined ;
146262 const contentType = props . response . headers ?. get ?.( "content-type" ) ;
147263 const respHeaders = normalizeHeaders (
148- props . response . headers ?. toRawHeaders ?.( )
264+ props . response . headers ?. toRawHeaders ?.( ) ??
265+ props . response . headers ??
266+ ( props . response as any ) ?. rawHeaders
149267 ) ;
150- const respBodyInfo = getBodyPreview ( props . response . body ) ;
268+
269+ let respBodyInfo : { preview : string ; size ?: number } = {
270+ preview : "" ,
271+ } ;
272+ let respMediaUrl : string | undefined ;
273+ if (
274+ props . response ?. body &&
275+ typeof ReadableStream !== "undefined" &&
276+ props . response . body instanceof ReadableStream
277+ ) {
278+ const [ streamForResponse , streamForPreview ] =
279+ props . response . body . tee ( ) ;
280+ if ( props . response instanceof Response ) {
281+ const rebuilt = new Response ( streamForResponse , props . response ) ;
282+ props . response = ensureScramjetResponse ( rebuilt ) ;
283+ } else {
284+ props . response . body = streamForResponse ;
285+ }
286+ if ( isMediaContentType ( contentType ) ) {
287+ const blob = await readStreamBlob ( streamForPreview ) ;
288+ if ( blob ) {
289+ respMediaUrl = URL . createObjectURL ( blob ) ;
290+ respBodyInfo = { preview : "" , size : blob . size } ;
291+ }
292+ } else {
293+ const previewText = await readStreamBody ( streamForPreview ) ;
294+ respBodyInfo = getBodyPreview ( previewText ) ;
295+ }
296+ } else {
297+ const media = getMediaUrlFromBody (
298+ props . response ?. body ,
299+ contentType
300+ ) ;
301+ respMediaUrl = media . url ;
302+ respBodyInfo = media . url
303+ ? { preview : "" , size : media . size }
304+ : getBodyPreview ( props . response . body ) ;
305+ }
151306
152307 this . requests = this . requests . map ( ( entry ) =>
153308 entry . id === id
@@ -160,6 +315,7 @@ export const App: Component<
160315 responseHeaders : respHeaders ,
161316 responseBodyPreview : respBodyInfo . preview ,
162317 responseBodySize : respBodyInfo . size ,
318+ responseBodyMediaUrl : respMediaUrl ,
163319 }
164320 : entry
165321 ) ;
0 commit comments