diff --git a/aio/src/app/documents/document.service.spec.ts b/aio/src/app/documents/document.service.spec.ts index 756651c1ce..4acddefe7c 100644 --- a/aio/src/app/documents/document.service.spec.ts +++ b/aio/src/app/documents/document.service.spec.ts @@ -67,6 +67,30 @@ describe('DocumentService', () => { expect(latestDocument).toEqual(doc1); }); + // HACK: PREPARE FOR CHANGING TO CASE-INSENSITIVE URLS + it('should attempt disambiguated document paths if the document is not found on the server', () => { + let currentDocument: DocumentContents|undefined; + const notFoundDoc = { id: FILE_NOT_FOUND_ID, contents: '

Page Not Found

' }; + const { docService, logger } = getServices('missing/Doc-1'); + docService.currentDocument.subscribe(doc => currentDocument = doc); + + // Initial request return 404. + httpMock.expectOne({url: 'generated/docs/missing/Doc-1.json'}).flush(null, {status: 404, statusText: 'NOT FOUND'}); + httpMock.expectOne({url: 'generated/docs/missing/d_oc-1.json'}).flush(null, {status: 404, statusText: 'NOT FOUND'}); + httpMock.expectOne({url: 'generated/docs/missing/d_oc.json'}).flush(null, {status: 404, statusText: 'NOT FOUND'}); + expect(logger.output.error).toEqual([ + [jasmine.any(Error)] + ]); + expect(logger.output.error[0][0].message).toEqual(`Document file not found at 'missing/Doc-1'`); + + // Subsequent request for not-found document. + logger.output.error = []; + httpMock.expectOne(CONTENT_URL_PREFIX + 'file-not-found.json').flush(notFoundDoc); + expect(logger.output.error).toEqual([]); // does not report repeated errors + expect(currentDocument).toEqual(notFoundDoc); + }); + // END HACK: PREPARE FOR CHANGING TO CASE-INSENSITIVE URLS + it('should emit the not-found document if the document is not found on the server', () => { let currentDocument: DocumentContents|undefined; const notFoundDoc = { id: FILE_NOT_FOUND_ID, contents: '

Page Not Found

' }; @@ -83,7 +107,7 @@ describe('DocumentService', () => { // Subsequent request for not-found document. logger.output.error = []; httpMock.expectOne(CONTENT_URL_PREFIX + 'file-not-found.json').flush(notFoundDoc); - expect(logger.output.error).toEqual([]); // does not report repeate errors + expect(logger.output.error).toEqual([]); // does not report repeated errors expect(currentDocument).toEqual(notFoundDoc); }); diff --git a/aio/src/app/documents/document.service.ts b/aio/src/app/documents/document.service.ts index 168e6e71c8..9d521f3232 100644 --- a/aio/src/app/documents/document.service.ts +++ b/aio/src/app/documents/document.service.ts @@ -1,7 +1,7 @@ import { Injectable } from '@angular/core'; import { HttpClient, HttpErrorResponse } from '@angular/common/http'; -import { AsyncSubject, Observable, of } from 'rxjs'; +import { AsyncSubject, Observable, of, throwError } from 'rxjs'; import { catchError, switchMap, tap } from 'rxjs/operators'; import { DocumentContents } from './document-contents'; @@ -66,6 +66,20 @@ export class DocumentService { throw Error('Invalid data'); } }), + // HACK: PREPARE FOR CHANGING TO CASE-INSENSITIVE URLS + catchError((error: HttpErrorResponse) => { + const encodedPath = encodeToLowercase(requestPath); + return error.status === 404 && encodedPath !== requestPath ? + this.http.get(encodedPath) : + throwError(error); + }), + catchError((error: HttpErrorResponse) => { + const disambiguatedPath = convertDisambiguatedPath(requestPath); + return error.status === 404 && disambiguatedPath !== requestPath ? + this.http.get(disambiguatedPath) : + throwError(error); + }), + // END HACK: PREPARE FOR CHANGING TO CASE-INSENSITIVE URLS catchError((error: HttpErrorResponse) => { return error.status === 404 ? this.getFileNotFoundDoc(id) : this.getErrorDoc(id, error); }), @@ -97,3 +111,30 @@ export class DocumentService { }); } } + +/** + * Encode the path to the content in a deterministic, reversible, case-insensitive form. + * + * This avoids collisions on case-insensitive file-systems. + * + * - Escape underscores (_) to double underscores (__). + * - Convert all uppercase letters to lowercase followed by an underscore. + */ +function encodeToLowercase(str: string): string { + return str.replace(/[A-Z_]/g, char => char.toLowerCase() + '_'); +} + +/** + * A temporary function to deal with a future change to URL disambiguation. + * + * Currently there are disambiguated URLs such as `INJECTOR-0` and `Injector-1`, which + * will attempt to load their document contents from `injector-0.json` and `injector-1.json` + * respectively. In a future version of the AIO app, the disambiguation will be changed to + * escape the upper-case characters instead. + * + * This function will be called if the current AIO is trying to request documents from a + * server that has been updated to use the new disambiguated URLs. + */ +function convertDisambiguatedPath(str: string): string { + return encodeToLowercase(str.replace(/-\d+\.json$/, '.json')); +} diff --git a/goldens/size-tracking/aio-payloads.json b/goldens/size-tracking/aio-payloads.json index cbb7ae312c..94291ad955 100755 --- a/goldens/size-tracking/aio-payloads.json +++ b/goldens/size-tracking/aio-payloads.json @@ -3,7 +3,7 @@ "master": { "uncompressed": { "runtime-es2015": 4619, - "main-es2015": 453172, + "main-es2015": 453855, "polyfills-es2015": 55210 } } @@ -12,7 +12,7 @@ "master": { "uncompressed": { "runtime-es2015": 4619, - "main-es2015": 453394, + "main-es2015": 453981, "polyfills-es2015": 55291 } }