Skip to content

Commit

Permalink
feat(@nguniversal/common): add support for blob and arraybuffer i…
Browse files Browse the repository at this point in the history
…n TransferHttpCacheModule

This commit add support to handle responses  of types `arraybuffer` and `blob` in `TransferHttpCacheModule`.

Closes #2734

(cherry picked from commit 868db00)
  • Loading branch information
alan-agius4 committed Aug 11, 2022
1 parent b511f53 commit 26f2aa5
Show file tree
Hide file tree
Showing 3 changed files with 72 additions and 9 deletions.
Expand Up @@ -20,12 +20,15 @@ import { StateKey, TransferState, makeStateKey } from '@angular/platform-browser
import { Observable, of } from 'rxjs';
import { filter, take, tap } from 'rxjs/operators';

type ResponseType = HttpRequest<unknown>['responseType'];

interface TransferHttpResponse {
body?: any | null;
headers?: Record<string, string[]>;
status?: number;
statusText?: string;
url?: string;
responseType?: ResponseType;
}

@Injectable()
Expand All @@ -36,14 +39,16 @@ export class TransferHttpCacheInterceptor implements HttpInterceptor {
method: string,
url: string,
params: HttpParams,
responseType?: ResponseType,
): StateKey<TransferHttpResponse> {
// make the params encoded same as a url so it's easy to identify
const encodedParams = params
.keys()
.sort()
.map((k) => `${k}=${params.getAll(k)}`)
.join('&');
const key = (method === 'GET' ? 'G.' : 'H.') + url + '?' + encodedParams;

const key = (method === 'GET' ? 'G.' : 'H.') + responseType + '.' + url + '?' + encodedParams;

return makeStateKey(key);
}
Expand All @@ -66,15 +71,38 @@ export class TransferHttpCacheInterceptor implements HttpInterceptor {
return next.handle(req);
}

const storeKey = this.makeCacheKey(req.method, req.url, req.params);
const storeKey = this.makeCacheKey(req.method, req.url, req.params, req.responseType);

if (this.transferState.hasKey(storeKey)) {
// Request found in cache. Respond using it.
const response = this.transferState.get(storeKey, {});
let body: ArrayBuffer | Blob | string | undefined = response.body;

switch (response.responseType) {
case 'arraybuffer':
{
// If we're in Node...
if (typeof Buffer !== 'undefined') {
const buf = Buffer.from(response.body);
body = new ArrayBuffer(buf.length);
const view = new Uint8Array(body);
for (let i = 0; i < buf.length; ++i) {
view[i] = buf[i];
}
} else if (typeof TextEncoder !== 'undefined') {
// Modern browsers implement TextEncode.
body = new TextEncoder().encode(response.body).buffer;
}
}
break;
case 'blob':
body = new Blob([response.body]);
break;
}

return of(
new HttpResponse<any>({
body: response.body,
body,
headers: new HttpHeaders(response.headers),
status: response.status,
statusText: response.statusText,
Expand All @@ -95,6 +123,7 @@ export class TransferHttpCacheInterceptor implements HttpInterceptor {
status: event.status,
statusText: event.statusText,
url: event.url ?? '',
responseType: req.responseType,
});
}
}),
Expand Down
11 changes: 8 additions & 3 deletions modules/common/spec/transfer_http.spec.ts
Expand Up @@ -32,29 +32,33 @@ describe('TransferHttp', () => {
'GET',
'https://google.com/api',
new HttpParams().append('foo', 'bar'),
'text',
);
expect(key).toEqual('G.https://google.com/api?foo=bar');
expect(key).toEqual('G.text.https://google.com/api?foo=bar');
});
it('should sort the keys by unicode points', () => {
const interceptor = new TransferHttpCacheInterceptor(mockAppRef(), mockTransferState());
const key = interceptor['makeCacheKey'](
'GET',
'https://google.com/api',
new HttpParams().append('b', 'foo').append('a', 'bar'),
'text',
);
expect(key).toEqual('G.https://google.com/api?a=bar&b=foo');
expect(key).toEqual('G.text.https://google.com/api?a=bar&b=foo');
});
it('should make equal keys if order of params changes', () => {
const interceptor = new TransferHttpCacheInterceptor(mockAppRef(), mockTransferState());
const key1 = interceptor['makeCacheKey'](
'GET',
'https://google.com/api',
new HttpParams().append('a', 'bar').append('b', 'foo'),
'text',
);
const key2 = interceptor['makeCacheKey'](
'GET',
'https://google.com/api',
new HttpParams().append('b', 'foo').append('a', 'bar'),
'text',
);
expect(key1).toEqual(key2);
});
Expand All @@ -64,8 +68,9 @@ describe('TransferHttp', () => {
'GET',
'https://google.com/api',
new HttpParams().append('b', 'xyz').append('a', 'foo').append('a', 'bar'),
'text',
);
expect(key).toEqual('G.https://google.com/api?a=foo,bar&b=xyz');
expect(key).toEqual('G.text.https://google.com/api?a=foo,bar&b=xyz');
});
});
});
35 changes: 32 additions & 3 deletions modules/common/src/transfer_http.ts
Expand Up @@ -26,12 +26,15 @@ import {
import { Observable, of as observableOf } from 'rxjs';
import { filter, take, tap } from 'rxjs/operators';

type ResponseType = HttpRequest<unknown>['responseType'];

export interface TransferHttpResponse {
body?: any | null;
headers?: Record<string, string[]>;
status?: number;
statusText?: string;
url?: string;
responseType?: ResponseType;
}

function getHeadersMap(headers: HttpHeaders): Record<string, string[]> {
Expand Down Expand Up @@ -60,14 +63,16 @@ export class TransferHttpCacheInterceptor implements HttpInterceptor {
method: string,
url: string,
params: HttpParams,
responseType: ResponseType,
): StateKey<TransferHttpResponse> {
// make the params encoded same as a url so it's easy to identify
const encodedParams = params
.keys()
.sort()
.map((k) => `${k}=${params.getAll(k)}`)
.join('&');
const key = (method === 'GET' ? 'G.' : 'H.') + url + '?' + encodedParams;

const key = (method === 'GET' ? 'G.' : 'H.') + responseType + '.' + url + '?' + encodedParams;

return makeStateKey<TransferHttpResponse>(key);
}
Expand Down Expand Up @@ -97,15 +102,38 @@ export class TransferHttpCacheInterceptor implements HttpInterceptor {
return next.handle(req);
}

const storeKey = this.makeCacheKey(req.method, req.url, req.params);
const storeKey = this.makeCacheKey(req.method, req.url, req.params, req.responseType);

if (this.transferState.hasKey(storeKey)) {
// Request found in cache. Respond using it.
const response = this.transferState.get(storeKey, {});
let body: ArrayBuffer | Blob | string | undefined = response.body;

switch (response.responseType) {
case 'arraybuffer':
{
// If we're in Node...
if (typeof Buffer !== 'undefined') {
const buf = Buffer.from(response.body);
body = new ArrayBuffer(buf.length);
const view = new Uint8Array(body);
for (let i = 0; i < buf.length; ++i) {
view[i] = buf[i];
}
} else if (typeof TextEncoder !== 'undefined') {
// Modern browsers implement TextEncode.
body = new TextEncoder().encode(response.body).buffer;
}
}
break;
case 'blob':
body = new Blob([response.body]);
break;
}

return observableOf(
new HttpResponse<any>({
body: response.body,
body,
headers: new HttpHeaders(response.headers),
status: response.status,
statusText: response.statusText,
Expand All @@ -125,6 +153,7 @@ export class TransferHttpCacheInterceptor implements HttpInterceptor {
status: event.status,
statusText: event.statusText,
url: event.url || '',
responseType: req.responseType,
});
}
}),
Expand Down

0 comments on commit 26f2aa5

Please sign in to comment.