Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 28 additions & 3 deletions packages/common/http/src/transfer_cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,10 +188,10 @@ export function transferCacheInterceptorFn(

switch (responseType) {
case 'arraybuffer':
body = new TextEncoder().encode(undecodedBody).buffer;
body = fromBase64(undecodedBody);
break;
case 'blob':
body = new Blob([undecodedBody]);
body = new Blob([fromBase64(undecodedBody)]);
break;
}

Expand Down Expand Up @@ -226,7 +226,10 @@ export function transferCacheInterceptorFn(
// Only cache successful HTTP responses.
if (event instanceof HttpResponse) {
transferState.set<TransferHttpResponse>(storeKey, {
[BODY]: event.body,
[BODY]:
req.responseType === 'arraybuffer' || req.responseType === 'blob'
? toBase64(event.body)
: event.body,
[HEADERS]: getFilteredHeaders(event.headers, headersToInclude),
[STATUS]: event.status,
[STATUS_TEXT]: event.statusText,
Expand Down Expand Up @@ -313,6 +316,28 @@ function generateHash(value: string): string {
return hash.toString();
}

function toBase64(buffer: unknown): string {
//TODO: replace with when is Baseline widely available
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array/toBase64
const bytes = new Uint8Array(buffer as ArrayBufferLike);

const CHUNK_SIZE = 0x8000; // 32,768 bytes (~32 KB) per chunk, to avoid stack overflow

let binaryString = '';

for (let i = 0; i < bytes.length; i += CHUNK_SIZE) {
const chunk = bytes.subarray(i, i + CHUNK_SIZE);
binaryString += String.fromCharCode.apply(null, chunk as unknown as number[]);
}
return btoa(binaryString);
}

function fromBase64(base64: string): ArrayBuffer {
const binary = atob(base64);
const bytes = Uint8Array.from(binary, (c) => c.charCodeAt(0));
return bytes.buffer;
}

/**
* Returns the DI providers needed to enable HTTP transfer cache.
*
Expand Down
31 changes: 31 additions & 0 deletions packages/common/http/test/transfer_cache_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,37 @@ describe('TransferCache', () => {
expect(transferState.get(key, null)).toEqual(jasmine.objectContaining({[BODY]: 'foo'}));
});

it('should cache arraybuffer responses correctly', () => {
const testData = new Uint8Array([1, 2, 3, 4, 5]).buffer;
let response!: ArrayBuffer;
TestBed.inject(HttpClient)
.get('/test-arraybuffer', {responseType: 'arraybuffer'})
.subscribe((r) => (response = r));
TestBed.inject(HttpTestingController).expectOne('/test-arraybuffer').flush(testData);

expect(new Uint8Array(response)).toEqual(new Uint8Array([1, 2, 3, 4, 5]));

let cachedResponse!: ArrayBuffer;
TestBed.inject(HttpClient)
.get('/test-arraybuffer', {responseType: 'arraybuffer'})
.subscribe((r) => (cachedResponse = r));
TestBed.inject(HttpTestingController).expectNone('/test-arraybuffer');

expect(new Uint8Array(cachedResponse)).toEqual(new Uint8Array([1, 2, 3, 4, 5]));
});

it('should cache blob responses correctly', () => {
const testData = new Uint8Array([10, 20, 30, 40, 50]).buffer;
let response!: Blob;
TestBed.inject(HttpClient)
.get('/test-blob', {responseType: 'blob'})
.subscribe((r) => (response = r));
TestBed.inject(HttpTestingController).expectOne('/test-blob').flush(testData);

expect(response instanceof Blob).toBeTrue();
expect(response.size).toBe(5);
});

it('should stop storing HTTP calls in `TransferState` after application becomes stable', fakeAsync(() => {
makeRequestAndExpectOne('/test-1', 'foo');
makeRequestAndExpectOne('/test-2', 'buzz');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -445,6 +445,7 @@
"from",
"fromArrayLike",
"fromAsyncIterable",
"fromBase64",
"fromInteropObservable",
"fromIterable",
"fromPromise",
Expand Down