@evidence-dev/evidence
Version:
dependencies for evidence projects
188 lines (164 loc) • 5.48 kB
JavaScript
import { browser, building, dev } from '$app/environment';
import {
tableFromIPC,
initDB,
setParquetURLs,
query as usqlQuery,
updateSearchPath,
arrowTableToJSON
} from '@evidence-dev/universal-sql/client-duckdb';
import { profile } from '@evidence-dev/component-utilities/profile';
import { toasts } from '@evidence-dev/component-utilities/stores';
import { setTrackProxy } from '@evidence-dev/sdk/usql';
import { addBasePath } from '@evidence-dev/sdk/utils/svelte';
import md5 from 'blueimp-md5';
export const ssr = !dev;
export const prerender = import.meta.env.VITE_EVIDENCE_SPA !== 'true';
export const trailingSlash = 'always';
const loadDB = async () => {
let renderedFiles = {};
if (!browser) {
const { readFile } = await import('fs/promises');
({ renderedFiles } = JSON.parse(
await readFile('./static/data/manifest.json', 'utf-8').catch(() => '{}')
));
} else {
const res = await fetch(addBasePath('/data/manifest.json'));
if (res.ok) ({ renderedFiles } = await res.json());
}
await profile(initDB);
if (Object.keys(renderedFiles ?? {}).length === 0) {
console.warn(`No sources found, execute "npm run sources" to generate`.trim());
if (dev) {
toasts.add(
{
id: 'MissingManifest',
status: 'warning',
title: 'No Sources Found',
message: 'Configure and run sources to include data in your project.'
},
10000
);
}
} else {
await profile(setParquetURLs, renderedFiles, { addBasePath });
await profile(updateSearchPath, Object.keys(renderedFiles));
}
};
const database_initialization = profile(loadDB);
/**
*
* @param {string} routeHash
* @param {string} paramsHash
* @param {typeof fetch} fetch
* @returns {Promise<Record<string, unknown[]>>}
*/
async function getPrerenderedQueries(routeHash, paramsHash, fetch) {
// get every query that's run in the component
const res = await fetch(addBasePath(`/api/${routeHash}/${paramsHash}/all-queries.json`));
if (!res.ok) return {};
const sql_cache_with_hashed_query_strings = await res.json();
const resolved_entries = await Promise.all(
Object.entries(sql_cache_with_hashed_query_strings).map(async ([query_name, query_hash]) => {
const res = await fetch(addBasePath(`/api/prerendered_queries/${query_hash}.arrow`));
if (!res.ok) return null;
const table = await tableFromIPC(res);
return [query_name, arrowTableToJSON(table)];
})
);
return Object.fromEntries(resolved_entries.filter(Boolean));
}
const system_routes = ['/settings', '/explore'];
/** @type {Map<string, { inputs: Record<string, string> }>} */
const dummy_pages = new Map();
/** @satisfies {import("./$types").LayoutLoad} */
export const load = async ({ fetch, route, params, url }) => {
const [{ customFormattingSettings }, pagesManifest, evidencemeta] = await Promise.all([
fetch(addBasePath('/api/customFormattingSettings.json/GET.json')).then((x) => x.json()),
fetch(addBasePath('/api/pagesManifest.json')).then((x) => x.json()),
fetch(addBasePath(`/api/${route.id}/evidencemeta.json`))
.then((x) => x.json())
.catch(() => ({ queries: [] }))
]);
const routeHash = md5(route.id);
const paramsHash = md5(
Object.entries(params)
.sort()
.map(([key, value]) => `${key}\x1F${value}`)
.join('\x1E')
);
const isUserPage =
route.id && system_routes.every((system_route) => !route.id.startsWith(system_route));
/** @type {App.PageData["data"]} */
let data = {};
const {
inputs = setTrackProxy({
label: '',
value: '(SELECT NULL WHERE 0 /* An Input has not been set */)'
}) /* Create a proxy by default */
} = dummy_pages.get(url.pathname) ?? {};
const is_dummy_page = dummy_pages.has(url.pathname);
if ((dev || building) && !browser && !is_dummy_page) {
dummy_pages.set(url.pathname, { inputs });
await fetch(url);
dummy_pages.delete(url.pathname);
}
if (!browser) await database_initialization;
// account for potential changes in manifest (source query hmr)
if (!browser && dev) await initDB();
// let SSR saturate the cache first
if (browser && isUserPage && prerender) {
data = await getPrerenderedQueries(routeHash, paramsHash, fetch);
}
/** @type {App.PageData["__db"]["query"]} */
function query(sql, { query_name, callback = (x) => x } = {}) {
if (browser) {
return (async () => {
await database_initialization;
const result = await usqlQuery(sql);
return callback(result);
})();
}
return callback(
usqlQuery(sql, {
route_hash: routeHash,
additional_hash: paramsHash,
query_name,
prerendering: building
})
);
}
let tree = pagesManifest;
for (const part of (route.id ?? '').split('/').slice(1)) {
tree = tree.children[part];
if (!tree) break;
if (tree.frontMatter?.title) {
tree.title = tree.frontMatter.title;
} else if (tree.frontMatter?.breadcrumb) {
let { breadcrumb } = tree.frontMatter;
for (const [param, value] of Object.entries(params)) {
breadcrumb = breadcrumb.replaceAll(`\${params.${param}}`, value);
}
tree.title = (await query(breadcrumb))[0]?.breadcrumb;
}
}
return /** @type {App.PageData} */ ({
__db: {
query,
async load() {
return database_initialization;
},
async updateParquetURLs(manifest) {
// todo: maybe diff with old?
const { renderedFiles } = JSON.parse(manifest);
await profile(setParquetURLs, renderedFiles, { addBasePath });
}
},
inputs,
data,
customFormattingSettings,
isUserPage,
evidencemeta,
pagesManifest
});
};