Skip to content
Open
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
27 changes: 21 additions & 6 deletions specifyweb/frontend/js_src/lib/components/Core/ContextLoader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,25 +5,40 @@ import { useDelay } from '../../hooks/useDelay';
import { commonText } from '../../localization/common';
import { f } from '../../utils/functools';
import { SECOND } from '../Atoms/timeUnits';
import { crash } from '../Errors/Crash';
import { crash, softFail } from '../Errors/Crash';
import { useMenuItems } from '../Header/menuItemProcessing';
import { initialContext } from '../InitialContext';
import { initialContext, secondaryContext } from '../InitialContext';
import { Main } from './Main';
import { SplashScreen } from './SplashScreen';

// Show loading splash screen if didn't finish load within 2 seconds
const LOADING_TIMEOUT = 2 * SECOND;

const fetchContext = async (): Promise<true | void> =>
initialContext.then(f.true).catch(crash);
const fetchContext =
(
context: Promise<unknown>,
errorMode: 'console' | 'crash'
): (() => Promise<true | void>) =>
async () =>
context.then(f.true).catch(errorMode === 'crash' ? crash : softFail);

/**
* - Load initial context
* - Load initial and secondary context
* - Display loading screen while loading
* - Display the main component afterward
*/
export function ContextLoader(): JSX.Element | null {
const [isContextLoaded = false] = useAsyncState(fetchContext, false);
const [isContextLoaded = false] = useAsyncState(
React.useCallback(fetchContext(initialContext, 'crash'), []),
false
);
const [_secondaryContextLoaded = false] = useAsyncState(
React.useCallback(
isContextLoaded ? fetchContext(secondaryContext, 'console') : f.undefined,
[isContextLoaded]
),
false
);
const menuItems = useMenuItems();
const isLoaded = isContextLoaded && typeof menuItems === 'object';

Expand Down
29 changes: 29 additions & 0 deletions specifyweb/frontend/js_src/lib/components/InitialContext/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,35 @@ export const load = async <T>(path: string, mimeType: MimeType): Promise<T> =>
return data;
});

/**
* These endpoints should still be called as a part of loading the initial
* context, but are not strictly required for normal operation of the
* application.
* Because of this, these endpoints are called after the initialContext and do
* not block or prevent access to Specify
*/
export const secondaryContext = Promise.all([
/**
* REFACTOR: Move non-essential endpoints here from initialContext to speed
* up initial loading times.
* Icon Definitions, Legacy UI Localization, Uniqueness Rules, and possibly
* even Field Formatters and Remote Prefs can all theoretically be moved here.
*
* Some more work would need to be done to handle the case where a component
* attempts to access the resources as they're being fetched.
*/
// Send basic stats
import('./stats'),
]).then(async (modules) =>
Promise.all(modules.map(async ({ fetchContext }) => fetchContext))
);

/**
* These endpoints are essential for nearly all operations in Specify and have
* to be fetched before the application can even be accessed.
* That is, the application will necessarily be blocked until
* all of these requests are resolved.
*/
export const initialContext = Promise.all([
// Fetch general context information (NOT CACHED)
import('../DataModel/schema'),
Expand Down
81 changes: 81 additions & 0 deletions specifyweb/frontend/js_src/lib/components/InitialContext/stats.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { ping } from '../../utils/ajax/ping';
import { softFail } from '../Errors/Crash';
import { formatUrl } from '../Router/queryString';
import { load } from './index';
import { fetchContext as fetchSystemInfo } from './systemInfo';

type StatsCounts = {
readonly Collectionobject: number;
readonly Collection: number;
readonly Specifyuser: number;
};

function buildStatsLambdaUrl(base: string | null | undefined): string | null {
if (!base) return null;
let u = base.trim();

if (!/^https?:\/\//i.test(u)) u = `https://${u}`;

const hasRoute = /\/(prod|default)\/[^\s/]+/.test(u);
if (!hasRoute) {
const stage = 'prod';
const route = 'AggrgatedSp7Stats';
u = `${u.replace(/\/$/, '')}/${stage}/${route}`;
}
return u;
}

export const fetchContext = fetchSystemInfo.then(async (systemInfo) => {
if (systemInfo === undefined) {
return;
}
if (systemInfo.stats_url === null && systemInfo.stats_2_url === null) {
return;
}
let counts: StatsCounts | null = null;
try {
counts = await load<StatsCounts>(
'/context/stats_counts.json',
'application/json'
);
} catch {
// If counts fetch fails, proceed without them.
counts = null;
}

const parameters = {
version: systemInfo.version,
dbVersion: systemInfo.database_version,
institution: systemInfo.institution,
institutionGUID: systemInfo.institution_guid,
discipline: systemInfo.discipline,
collection: systemInfo.collection,
collectionGUID: systemInfo.collection_guid,
isaNumber: systemInfo.isa_number,
disciplineType: systemInfo.discipline_type,
collectionObjectCount: counts?.Collectionobject ?? 0,
collectionCount: counts?.Collection ?? 0,
userCount: counts?.Specifyuser ?? 0,
};
if (systemInfo.stats_url)
await ping(
formatUrl(
systemInfo.stats_url,
parameters,
/*
* I don't know if the receiving server handles GET parameters in a
* case-sensitive way. Thus, don't convert keys to lower case, but leave
* them as they were sent in previous versions of Specify 7
*/
false
),
{ errorMode: 'silent' }
).catch(softFail);

const lambdaUrl = buildStatsLambdaUrl(systemInfo.stats_2_url);
if (lambdaUrl) {
await ping(formatUrl(lambdaUrl, parameters, false), {
errorMode: 'silent',
}).catch(softFail);
}
});
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,6 @@

import type { LocalizedString } from 'typesafe-i18n';

import { ping } from '../../utils/ajax/ping';
import { softFail } from '../Errors/Crash';
import { formatUrl } from '../Router/queryString';
import { load } from './index';

type SystemInfo = {
Expand All @@ -26,91 +23,14 @@ type SystemInfo = {
readonly discipline_type: string;
};

type StatsCounts = {
readonly Collectionobject: number;
readonly Collection: number;
readonly Specifyuser: number;
};

let systemInfo: SystemInfo;

function buildStatsLambdaUrl(base: string | null | undefined): string | null {
if (!base) return null;
let u = base.trim();

if (!/^https?:\/\//i.test(u)) u = `https://${u}`;

const hasRoute = /\/(prod|default)\/[^\s/]+/.test(u);
if (!hasRoute) {
const stage = 'prod';
const route = 'AggrgatedSp7Stats';
u = `${u.replace(/\/$/, '')}/${stage}/${route}`;
}
return u;
}

export const fetchContext = load<SystemInfo>(
'/context/system_info.json',
'application/json'
).then(async (data) => {
).then((data) => {
systemInfo = data;

if (systemInfo.stats_url !== null) {
let counts: StatsCounts | null = null;
try {
counts = await load<StatsCounts>(
'/context/stats_counts.json',
'application/json'
);
} catch {
// If counts fetch fails, proceed without them.
counts = null;
}

const parameters = {
version: systemInfo.version,
dbVersion: systemInfo.database_version,
institution: systemInfo.institution,
institutionGUID: systemInfo.institution_guid,
discipline: systemInfo.discipline,
collection: systemInfo.collection,
collectionGUID: systemInfo.collection_guid,
isaNumber: systemInfo.isa_number,
disciplineType: systemInfo.discipline_type,
collectionObjectCount: counts?.Collectionobject ?? 0,
collectionCount: counts?.Collection ?? 0,
userCount: counts?.Specifyuser ?? 0,
};

await ping(
formatUrl(
systemInfo.stats_url,
parameters,
/*
* I don't know if the receiving server handles GET parameters in a
* case-sensitive way. Thus, don't convert keys to lower case, but leave
* them as they were sent in previous versions of Specify 7
*/
false
),
{ errorMode: 'silent' }
).catch(softFail);

/*
* Await ping(
* formatUrl(systemInfo.stats_2_url, parameters, false),
* { errorMode: 'silent' }
* ).catch(softFail);
*/

const lambdaUrl = buildStatsLambdaUrl(systemInfo.stats_2_url);
if (lambdaUrl) {
await ping(formatUrl(lambdaUrl, parameters, false), {
errorMode: 'silent',
}).catch(softFail);
}
}

return systemInfo;
});

Expand Down