Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
d336d83
Cache-bust dynamic configuration files
ybnd Feb 13, 2025
fdc6e8a
Don't try to 'run' JSON on load
ybnd Feb 17, 2025
2f23599
Add theme CSS to hashed file map
ybnd Feb 17, 2025
02bbd7c
124369: Add PrefetchConfig
AAwouters Jan 16, 2025
4024840
124369: Prefetch requests and write to config
AAwouters Feb 19, 2025
66cdb69
124369: Separate request performing into method
AAwouters Jan 31, 2025
7abd9bb
124369: Retrieve prefetched response
AAwouters Jan 31, 2025
932bde7
124369: Fill request cache with prefetched requests in server & browser
AAwouters Feb 3, 2025
509ed7a
124369: Add StatisticsEndpoint model to models array of core.module
AAwouters Feb 4, 2025
93dbe13
124369: Fix tests by providing APP_CONFIG
AAwouters Feb 19, 2025
ba91fe5
124369: Retrieve 'browse' model constructors by their 'browseType'
AAwouters Feb 20, 2025
38ed248
Follow REST model for browe definitions more closely
ybnd Feb 24, 2025
f54baf7
Merge remote-tracking branch 'ybnd/cache-bust-dynamic-configuration-7…
ybnd Feb 24, 2025
168a967
Compress hashed config.json
ybnd Feb 24, 2025
79d4da1
Ensure bootstrapped requests are included in hashed config
ybnd Feb 24, 2025
13e039e
Fix browse response parsing test
AAwouters Feb 26, 2025
5ba59b6
124369: Update prefetch config comments
AAwouters Mar 12, 2025
d520511
124369: Fix prefetching info message
AAwouters Mar 12, 2025
b353bf9
124369: Catch errors during fetching of responses
AAwouters Mar 14, 2025
cb00b72
Merge branch 'w2p-124369_Bootstrap-HAL-endpoint-maps' into prefetch-r…
AAwouters Mar 14, 2025
db6c4f3
124369: Apply 8_x specific fixes after merge
AAwouters Mar 13, 2025
afffc16
124369: Stop prefetching responses when process is terminated
AAwouters Mar 13, 2025
95b58d2
124369: Wait until responses have been used before clearing prefetche…
AAwouters Mar 18, 2025
aa06660
Merge branch 'w2p-124369_Bootstrap-HAL-endpoint-maps' into prefetch-r…
AAwouters Mar 18, 2025
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
10 changes: 10 additions & 0 deletions config/config.example.yml
Original file line number Diff line number Diff line change
Expand Up @@ -550,3 +550,13 @@ liveRegion:
messageTimeOutDurationMs: 30000
# The visibility of the live region. Setting this to true is only useful for debugging purposes.
isVisible: false


# REST response prefetching configuration
prefetch:
# The URLs for which the response will be prefetched
urls:
- /api
- /api/discover
# How often the responses are refreshed in milliseconds
refreshInterval: 60000
18 changes: 18 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@
"ngx-pagination": "6.0.3",
"ngx-skeleton-loader": "^9.0.0",
"ngx-ui-switch": "^15.0.0",
"node-html-parser": "^7.0.1",
"nouislider": "^15.7.1",
"orejime": "^2.3.1",
"pem": "1.14.8",
Expand Down
51 changes: 32 additions & 19 deletions server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,13 @@ import { createProxyMiddleware } from 'http-proxy-middleware';
import { hasValue } from './src/app/shared/empty.util';
import { UIServerConfig } from './src/config/ui-server-config.interface';
import bootstrap from './src/main.server';
import { buildAppConfig } from './src/config/config.server';
import { buildAppConfig, setupEndpointPrefetching } from './src/config/config.server';
import {
APP_CONFIG,
AppConfig,
} from './src/config/app-config.interface';
import { extendEnvironmentWithAppConfig } from './src/config/config.util';
import { ServerHashedFileMapping } from './src/modules/dynamic-hash/hashed-file-mapping.server';
import { logStartupMessage } from './startup-message';
import { TOKENITEM } from './src/app/core/auth/models/auth-token-info.model';
import { CommonEngine } from '@angular/ssr';
Expand All @@ -70,7 +71,11 @@ const indexHtml = join(DIST_FOLDER, 'index.html');

const cookieParser = require('cookie-parser');

const appConfig: AppConfig = buildAppConfig(join(DIST_FOLDER, 'assets/config.json'));
const destConfigPath = join(DIST_FOLDER, 'assets/config.json');
const hashedFileMapping = new ServerHashedFileMapping(DIST_FOLDER, 'index.html');
const appConfig: AppConfig = buildAppConfig(destConfigPath, hashedFileMapping);
hashedFileMapping.addThemeStyles();
hashedFileMapping.save();

// cache of SSR pages for known bots, only enabled in production mode
let botCache: LRU<string, any>;
Expand Down Expand Up @@ -247,7 +252,7 @@ function serverSideRender(req, res, next, sendToUser: boolean = true) {
commonEngine
.render({
bootstrap,
documentFilePath: indexHtml,
documentFilePath: hashedFileMapping.resolve(indexHtml),
inlineCriticalCss: environment.ssr.inlineCriticalCss,
url: `${protocol}://${headers.host}${originalUrl}`,
publicPath: DIST_FOLDER,
Expand Down Expand Up @@ -309,7 +314,7 @@ function serverSideRender(req, res, next, sendToUser: boolean = true) {
* @param res current response
*/
function clientSideRender(req, res) {
res.sendFile(indexHtml);
res.sendFile(hashedFileMapping.resolve(indexHtml));
}


Expand Down Expand Up @@ -537,7 +542,7 @@ function serverStarted() {
* Create an HTTPS server with the configured port and host
* @param keys SSL credentials
*/
function createHttpsServer(keys) {
function createHttpsServer(prefetchRefreshTimeout: NodeJS.Timeout, keys) {
const listener = createServer({
key: keys.serviceKey,
cert: keys.certificate,
Expand All @@ -550,7 +555,7 @@ function createHttpsServer(keys) {
process.on('SIGINT', () => {
void (async ()=> {
console.debug('Closing HTTPS server on signal');
await terminator.terminate().catch(e => { console.error(e); });
clearTimeout(prefetchRefreshTimeout);await terminator.terminate().catch(e => { console.error(e); });
console.debug('HTTPS server closed');
})();
});
Expand All @@ -559,7 +564,7 @@ function createHttpsServer(keys) {
/**
* Create an HTTP server with the configured port and host.
*/
function run() {
function run(prefetchRefreshTimeout: NodeJS.Timeout) {
const port = environment.ui.port || 4000;
const host = environment.ui.host || '/';

Expand All @@ -574,13 +579,16 @@ function run() {
process.on('SIGINT', () => {
void (async () => {
console.debug('Closing HTTP server on signal');
await terminator.terminate().catch(e => { console.error(e); });
console.debug('HTTP server closed.');return undefined;
clearTimeout(prefetchRefreshTimeout);
await terminator.terminate().catch(e => {
console.error(e);
});
console.debug('HTTP server closed.');
})();
});
}

function start() {
function start(prefetchRefreshTimeout: NodeJS.Timeout) {
logStartupMessage(environment);

/*
Expand All @@ -606,10 +614,11 @@ function start() {
}

if (serviceKey && certificate) {
createHttpsServer({
serviceKey: serviceKey,
certificate: certificate,
});
createHttpsServer(prefetchRefreshTimeout,
{
serviceKey: serviceKey,
certificate: certificate,
});
} else {
console.warn('Disabling certificate validation and proceeding with a self-signed certificate. If this is a production server, it is recommended that you configure a valid certificate instead.');

Expand All @@ -619,11 +628,11 @@ function start() {
days: 1,
selfSigned: true,
}, (error, keys) => {
createHttpsServer(keys);
createHttpsServer(prefetchRefreshTimeout, keys);
});
}
} else {
run();
run(prefetchRefreshTimeout);
}
}

Expand All @@ -648,8 +657,12 @@ function healthCheck(req, res) {
declare const __non_webpack_require__: NodeRequire;
const mainModule = __non_webpack_require__.main;
const moduleFilename = (mainModule && mainModule.filename) || '';
if (moduleFilename === __filename || moduleFilename.includes('iisnode')) {
start();
}
setupEndpointPrefetching(appConfig, destConfigPath, environment, hashedFileMapping).then(prefetchRefreshTimeout => {
if (moduleFilename === __filename || moduleFilename.includes('iisnode')) {
start(prefetchRefreshTimeout);
}
}).catch((error) => {
console.error('Errored while prefetching Endpoint Maps', error);
});

export * from './src/main.server';
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@ import { of as observableOf } from 'rxjs';
import { DspaceRestService } from 'src/app/core/dspace-rest/dspace-rest.service';
import { RawRestResponse } from 'src/app/core/dspace-rest/raw-rest-response.model';
import { TranslateLoaderMock } from 'src/app/shared/mocks/translate-loader.mock';
import { environment } from 'src/environments/environment.test';

import { APP_CONFIG } from '../../../../config/app-config.interface';
import { FilteredCollectionsComponent } from './filtered-collections.component';

describe('FiltersComponent', () => {
Expand Down Expand Up @@ -57,6 +59,7 @@ describe('FiltersComponent', () => {
DspaceRestService,
provideHttpClient(withInterceptorsFromDi()),
provideHttpClientTesting(),
{ provide: APP_CONFIG, useValue: environment },
],
});
}));
Expand Down
6 changes: 6 additions & 0 deletions src/app/app.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ import {
} from '../config/app-config.interface';
import { StoreDevModules } from '../config/store/devtools';
import { environment } from '../environments/environment';
import { HashedFileMapping } from '../modules/dynamic-hash/hashed-file-mapping';
import { BrowserHashedFileMapping } from '../modules/dynamic-hash/hashed-file-mapping.browser';
import { EagerThemesModule } from '../themes/eager-themes.module';
import { appEffects } from './app.effects';
import {
Expand Down Expand Up @@ -154,6 +156,10 @@ export const commonAppConfig: ApplicationConfig = {
useClass: DspaceRestInterceptor,
multi: true,
},
{
provide: HashedFileMapping,
useClass: BrowserHashedFileMapping,
},
// register the dynamic matcher used by form. MUST be provided by the app module
...DYNAMIC_MATCHER_PROVIDERS,
provideCore(),
Expand Down
3 changes: 3 additions & 0 deletions src/app/core/auth/auth.interceptor.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ import { TestBed } from '@angular/core/testing';
import { Router } from '@angular/router';
import { Store } from '@ngrx/store';
import { of as observableOf } from 'rxjs';
import { environment } from 'src/environments/environment.test';

import { APP_CONFIG } from '../../../config/app-config.interface';
import { AuthServiceStub } from '../../shared/testing/auth-service.stub';
import { RouterStub } from '../../shared/testing/router.stub';
import { TruncatablesState } from '../../shared/truncatable/truncatable.reducer';
Expand Down Expand Up @@ -45,6 +47,7 @@ describe(`AuthInterceptor`, () => {
{ provide: Store, useValue: store },
provideHttpClient(withInterceptorsFromDi()),
provideHttpClientTesting(),
{ provide: APP_CONFIG, useValue: environment },
],
});

Expand Down
56 changes: 3 additions & 53 deletions src/app/core/browse/browse-definition-data.service.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,7 @@
// eslint-disable-next-line max-classes-per-file
import { Injectable } from '@angular/core';
import {
Observable,
of as observableOf,
} from 'rxjs';
import { take } from 'rxjs/operators';
import { Observable } from 'rxjs';

import {
hasValue,
isNotEmpty,
isNotEmptyOperator,
} from '../../shared/empty.util';
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
import { RequestParam } from '../cache/models/request-param.model';
Expand All @@ -27,55 +18,18 @@ import {
import { FindListOptions } from '../data/find-list-options.model';
import { PaginatedList } from '../data/paginated-list.model';
import { RemoteData } from '../data/remote-data';
import { BrowseDefinitionRestRequest } from '../data/request.models';
import { RequestService } from '../data/request.service';
import { BrowseDefinition } from '../shared/browse-definition.model';
import { HALEndpointService } from '../shared/hal-endpoint.service';

/**
* Create a GET request for the given href, and send it.
* Use a GET request specific for BrowseDefinitions.
*/
export const createAndSendBrowseDefinitionGetRequest = (requestService: RequestService,
responseMsToLive: number,
href$: string | Observable<string>,
useCachedVersionIfAvailable: boolean = true): void => {
if (isNotEmpty(href$)) {
if (typeof href$ === 'string') {
href$ = observableOf(href$);
}

href$.pipe(
isNotEmptyOperator(),
take(1),
).subscribe((href: string) => {
const requestId = requestService.generateRequestId();
const request = new BrowseDefinitionRestRequest(requestId, href);
if (hasValue(responseMsToLive)) {
request.responseMsToLive = responseMsToLive;
}
requestService.send(request, useCachedVersionIfAvailable);
});
}
};

/**
* Custom extension of {@link FindAllDataImpl} to be able to send BrowseDefinitionRestRequests
*/
class BrowseDefinitionFindAllDataImpl extends FindAllDataImpl<BrowseDefinition> {
createAndSendGetRequest(href$: string | Observable<string>, useCachedVersionIfAvailable: boolean = true) {
createAndSendBrowseDefinitionGetRequest(this.requestService, this.responseMsToLive, href$, useCachedVersionIfAvailable);
}
}

/**
* Data service responsible for retrieving browse definitions from the REST server
*/
@Injectable({
providedIn: 'root',
})
export class BrowseDefinitionDataService extends IdentifiableDataService<BrowseDefinition> implements FindAllData<BrowseDefinition>, SearchData<BrowseDefinition> {
private findAllData: BrowseDefinitionFindAllDataImpl;
private findAllData: FindAllData<BrowseDefinition>;
private searchData: SearchDataImpl<BrowseDefinition>;

constructor(
Expand All @@ -86,7 +40,7 @@ export class BrowseDefinitionDataService extends IdentifiableDataService<BrowseD
) {
super('browses', requestService, rdbService, objectCache, halService);
this.searchData = new SearchDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, this.responseMsToLive);
this.findAllData = new BrowseDefinitionFindAllDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, this.responseMsToLive);
this.findAllData = new FindAllDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, this.responseMsToLive);
}

/**
Expand Down Expand Up @@ -171,9 +125,5 @@ export class BrowseDefinitionDataService extends IdentifiableDataService<BrowseD
...linksToFollow,
);
}

createAndSendGetRequest(href$: string | Observable<string>, useCachedVersionIfAvailable: boolean = true) {
createAndSendBrowseDefinitionGetRequest(this.requestService, this.responseMsToLive, href$, useCachedVersionIfAvailable);
}
}

Loading
Loading