@discoveryjs/cli
Version:
CLI tools to serve & build projects based on Discovery.js
445 lines (368 loc) • 16.1 kB
JavaScript
/* eslint-env browser */
/* global SINGLE_FILE, MODEL_RESET_CACHE */
import { preloader } from '@discoveryjs/discovery/src/preloader.js';
import { inflate, decodeBase64 } from './inflate/decoder.js';
export const colorSchemeOptions = ({
darkmode,
darkmodePersistent,
colorScheme = darkmode,
colorSchemePersistent = darkmodePersistent
}) =>
preloader.colorScheme
? { colorScheme, colorSchemePersistent }
: { darkmode: colorScheme, darkmodePersistent: colorSchemePersistent };
export const loadStyle = SINGLE_FILE
? url => document.querySelector(`style[type="discovery/style"][src=${JSON.stringify(url)}]`).firstChild.nodeValue
: url => ({ type: 'link', href: url });
export function load(module, styles, setup, dataLoaderOptions) {
const container = document.body;
const dataRequestId = String(Math.random()).slice(2, 18).padStart(16, '0');
const loadData = preloader({
...dataLoaderOptions,
loadDataOptions: {
...dataLoaderOptions.loadDataOptions,
encodings: dataLoaderOptions.encodings,
fetch: { headers: {
// using Cache-Control to prevent stalling requests in Chromium & Safari
// see https://stackoverflow.com/questions/27513994/chrome-stalls-when-making-multiple-requests-to-same-resource
'Cache-Control': 'no-cache, no-transform',
'x-data-request-id': dataRequestId
} }
},
container
});
// alter loader API
alterLoaderPush();
// status of awating data generation
if (dataLoaderOptions.dataSource === 'url' && dataLoaderOptions.data) {
awaitingDataStatus(loadData, dataRequestId);
}
// main part
return Promise.all([
module,
loadData
]).then(([init, dataset]) =>
init({ ...setup, styles }, loadData.progressbar, loadData.disposeEmbed?.(), dataset)
).then(() => {
loadData.el.remove();
}, (error) => {
const discoveryEl = document.querySelector('body > .discovery');
const actionButtonsEl = document.createElement('div');
if (MODEL_RESET_CACHE && setup.model?.cacheReset) {
const resetBtn = document.createElement('button', 'view-button');
resetBtn.className = 'view-button';
resetBtn.innerHTML = 'Reload with no cache';
resetBtn.onclick = () => fetch('drop-cache').then(() => location.reload());
actionButtonsEl.append(resetBtn);
}
if (actionButtonsEl.firstChild) {
actionButtonsEl.className = 'action-buttons';
loadData.progressbar.el.before(actionButtonsEl);
}
if (!error.supressLoadDataError) {
const el = document.createElement('pre');
const errorTypeBadgeEl = document.createElement('div');
const errorText = String(error);
let errorStack = String(error.stack || '');
if (errorStack.startsWith(errorText)) {
errorStack = errorStack.slice(errorText.length);
}
errorTypeBadgeEl.className = 'error-type-badge';
errorTypeBadgeEl.dataset.type = error.isFetchError
? 'server'
: 'client';
el.className = 'error';
el.append(
errorTypeBadgeEl,
errorText + (errorStack ? '\n' + errorStack.replace(/^[\r\n]+/, '') : '')
);
loadData.progressbar.el.replaceWith(el);
}
loadData.disposeEmbed?.();
loadData.progressbar.dispose();
discoveryEl && discoveryEl.remove();
});
}
function alterLoaderPush() {
const { push: origLoaderPush } = window.discoveryLoader || {};
if (typeof origLoaderPush === 'function') {
window.discoveryLoader.push = function(chunk, binary, compressed) {
if (compressed) {
chunk = binary
? inflate(chunk).slice()
: new TextDecoder().decode(inflate(chunk));
} else if (binary) {
chunk = decodeBase64(chunk);
}
origLoaderPush(chunk);
};
}
}
function createDataStatusBlock() {
const dataStatusEl = document.createElement('div');
dataStatusEl.className = 'data-status';
dataStatusEl.innerHTML =
'<div class="header">Getting data: <span class="elapsed-time"></span></div>' +
'<div class="output"></div>';
dataStatusEl.firstChild.addEventListener('click', function() {
dataStatusEl.classList.toggle('collapsed');
}, true);
dataStatusEl.elapsedTimeEl = dataStatusEl.querySelector(':scope > .header > .elapsed-time');
return dataStatusEl;
}
// shows status for a long awaiting data request
function awaitingDataStatus(loadData, dataRequestId) {
let supressLoadDataError = false;
let allowServerTimeUpdate = true;
let isServerError = false;
const dataStatusEl = createDataStatusBlock();
const activateDataStatusBlock = () => {
if (loadData.progressbar.value.stage === 'request' && !dataStatusEl.parentNode) {
loadData.progressbar.el.getRootNode().append(dataStatusEl);
}
};
const dataStatusListenInit = setTimeout(() => {
if (loadData.progressbar.value.stage !== 'request') {
return;
}
loadData.progressbar.subscribe(({ stage }, unsubscribe) => {
if (stage !== 'request') {
unsubscribe();
dataStatusEl.classList.add('finished');
dataStatusEventSource.close();
allowServerTimeUpdate = false;
}
});
const dataStatusEventSource = new EventSource('data-status?data-request-id=' + dataRequestId);
let planTreeMap = null;
let startTime;
let serverTime;
let optimisticServerTimeUpdateTimer;
let optimisticServerTimeUpdateFrom;
let lastStderrEl;
const updateServerTime = (newServerTime) => {
if (!allowServerTimeUpdate ||
!isFinite(newServerTime) ||
(serverTime !== undefined && newServerTime <= serverTime)) {
return;
}
clearTimeout(optimisticServerTimeUpdateTimer);
serverTime = Number(newServerTime);
if (startTime && serverTime - startTime >= 1000) {
dataStatusEl.elapsedTimeEl.textContent =
duration(serverTime - startTime, supressLoadDataError ? 1 : 0);
}
if (planTreeMap) {
for (const { started, elapsedTimeEl } of planTreeMap.values()) {
if (started) {
elapsedTimeEl.textContent = duration(serverTime - started, 1);
}
}
}
optimisticServerTimeUpdateFrom = Date.now();
optimisticServerTimeUpdateTimer = setTimeout(
() => updateServerTime(serverTime + (Date.now() - optimisticServerTimeUpdateFrom) - 5),
42
);
};
// dataStatusEventSource.addEventListener('open', () => {
// setTimeout(activateDataStatusBlock, 3000);
// }, { once: true });
dataStatusEventSource.addEventListener('message', message => {
try {
const data = JSON.parse(message.data);
switch (data.type) {
case 'start':
setTimeout(activateDataStatusBlock, 3000);
startTime = data.timestamp;
isServerError = true;
break;
case 'finish':
// do nothing
break;
case 'crash':
supressLoadDataError = true;
activateDataStatusBlock();
if (planTreeMap) {
for (const { started, el } of planTreeMap.values()) {
if (started) {
el.classList.add('crashed');
el.classList.remove('started');
el.classList.toggle('collapsed', el !== lastStderrEl);
}
}
}
break;
case 'stderr':
case 'stdout': {
const frameEl = document.createElement('div');
frameEl.className = data.type;
frameEl.append(String(data.chunk));
if (!planTreeMap) {
activateDataStatusBlock();
dataStatusEl.lastChild.append(frameEl);
scrollIntoViewIfNeeded(frameEl);
} else {
const step = planTreeMap.get(data.stepId);
if (step) {
step.contentEl.append(frameEl);
step.el.classList.add('has-output');
scrollIntoViewIfNeeded(frameEl);
if (data.type == 'stderr') {
lastStderrEl = step.el;
}
}
}
break;
}
case 'plan': {
const planTree = createPlanTree(data.plan.steps);
planTreeMap = planTree.map;
dataStatusEl.lastChild.innerHTML = '';
dataStatusEl.lastChild.appendChild(planTree.el);
if (planTreeMap) {
activateDataStatusBlock();
}
break;
}
case 'plan-step-event': {
const step = planTreeMap.get(data.stepId);
if (step) {
switch (data.stepEvent) {
case 'start':
step.started = data.timestamp;
step.el.classList.add('started');
scrollIntoViewIfNeeded(step.el);
break;
case 'finish':
step.elapsedTimeEl.textContent = duration(data.timestamp - step.started);
step.started = false;
step.el.classList.remove('started');
step.el.classList.add('finished');
break;
case 'summary':
step.summaryEl.innerHTML = numDelim(data.data);
break;
default:
console.warn('Unhandled data status SSE pipeline step event', data);
}
} else {
console.warn('Pipeline step not found', data);
}
break;
}
default:
console.warn('Unhandled data status SSE event', data);
}
updateServerTime(data.timestamp);
} catch (e) {
console.error('SSE message parse error', e);
}
});
dataStatusEventSource.addEventListener('server-time', ({ data }) => {
updateServerTime(data);
});
dataStatusEventSource.addEventListener('done', () => {
dataStatusEventSource.close();
allowServerTimeUpdate = false;
});
}, 150);
loadData.then(
() => clearTimeout(dataStatusListenInit),
(error) => {
allowServerTimeUpdate = false;
if (!isServerError) {
return;
}
if (supressLoadDataError) {
error.supressLoadDataError = true;
loadData.el.classList.add('generate-data-crash');
} else {
dataStatusEl.classList.add('compliment-error', 'collapsed');
}
for (const el of dataStatusEl.querySelectorAll('.plan-step.started')) {
el.classList.remove('started');
}
dataStatusEl.classList.add('crashed');
dataStatusEl.firstChild.firstChild.textContent = 'Retrieving data failed ' +
(dataStatusEl.elapsedTimeEl.textContent ? 'in ' : '');
}
);
}
function scrollIntoViewIfNeeded(el) {
try {
if (typeof el.scrollIntoViewIfNeeded === 'function') {
el.scrollIntoViewIfNeeded(false);
} else {
el.scrollIntoView({ block: 'nearest' });
}
} catch (e) {}
}
function createPlanTree(steps, level = 0, map = new Map()) {
const listEl = document.createElement('ul');
listEl.className = 'plan-step-list';
listEl.style.setProperty('--level', level);
for (const step of steps) {
const stepEl = listEl.appendChild(document.createElement('li'));
const stepHeaderEl = stepEl.appendChild(document.createElement('div'));
const stepHeaderToggleEl = stepHeaderEl.appendChild(document.createElement('span'));
const stepHeaderStatusEl = stepHeaderEl.appendChild(document.createElement('span'));
const stepHeaderContentEl = stepHeaderEl.appendChild(document.createElement('span'));
const stepHeaderSummaryEl = stepHeaderEl.appendChild(document.createElement('span'));
const stepHeaderElapsedTimeEl = stepHeaderEl.appendChild(document.createElement('span'));
const stepContentEl = stepEl.appendChild(document.createElement('div'));
map.set(step.id, {
step,
el: stepEl,
elapsedTimeEl: stepHeaderElapsedTimeEl,
summaryEl: stepHeaderSummaryEl,
contentEl: stepContentEl,
started: false
});
stepEl.className = 'plan-step collapsed';
stepHeaderEl.className = 'plan-step__header';
stepHeaderEl.addEventListener('click', () => stepEl.classList.toggle('collapsed'));
stepHeaderToggleEl.className = 'plan-step__header-toggle';
stepHeaderStatusEl.className = 'plan-step__header-status';
stepHeaderContentEl.className = 'plan-step__header-content';
stepHeaderContentEl.textContent = step.name || 'Untitled';
stepHeaderSummaryEl.className = 'plan-step__header-summary';
stepHeaderElapsedTimeEl.className = 'plan-step__elapsed-time';
stepContentEl.className = 'plan-step__content';
if (step.steps) {
stepEl.append(createPlanTree(step.steps, level + 1, map).el);
}
}
return { el: listEl, map: map.size ? map : null };
}
function escapeHtml(str) {
return str
.replace(/&/g, '&')
.replace(/"/g, '"')
.replace(/</g, '<')
.replace(/>/g, '>');
}
function numDelim(value, escape = true) {
const strValue = escape && typeof value !== 'number'
? escapeHtml(String(value))
: String(value);
if (strValue.length > 3) {
return strValue.replace(
/\.\d+(eE[-+]?\d+)?|\B(?=(\d{3})+(\D|$))/g,
m => m || '<span class="num-delim"></span>'
);
}
return strValue;
}
function duration(value, prec = 1) {
if (value < 1000) {
return value + 'ms';
}
if (value < 10000) {
return (value / 1000).toFixed(prec) + 's';
}
if (value < 60000) {
return Math.round(value / 1000) + 's';
}
return `${Math.floor(value / 60000)}:${String(Math.floor(value / 1000) % 60).padStart(2, '0')}`;
}