UNPKG

@discoveryjs/discovery

Version:

Frontend framework for rapid data (JSON) analysis, shareable serverless reports and dashboards

205 lines (204 loc) 6 kB
import { Observer } from "../observer.js"; import { createElement } from "./dom.js"; export const loadStages = { inited: { value: 0, duration: 0, title: "Init" }, request: { value: 0, duration: 0.1, title: "Awaiting data" }, receiving: { value: 0.1, duration: 0.8, title: "Receiving data" }, decoding: { value: 0.9, duration: 0.015, title: "Decoding data" }, received: { value: 0.915, duration: 0.01, title: "Await app ready" }, prepare: { value: 0.925, duration: 0.055, title: "Processing data (prepare)" }, initui: { value: 0.98, duration: 0.02, title: "Rendering UI" }, done: { value: 1, duration: 0, title: "Done!" }, error: { value: 1, duration: 0, title: "Error" } }; const int = (value) => value | 0; const ensureFunction = (value) => typeof value === "function" ? value : () => void 0; const waitMs = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); const letRepaintIfNeeded = async () => { if (!document.hidden) { return Promise.race([ new Promise(requestAnimationFrame).then(() => waitMs(0)), waitMs(12) ]); } }; export function decodeStageProgress(stage, progress, step) { const { value, title: stageTitle, duration } = loadStages[stage]; let progressValue = 0; let progressText = null; if (progress) { const { done, elapsed, units, completed, total } = progress; if (total) { progressValue = done ? 1 : completed / total; progressText = units === "bytes" ? Math.round(progressValue * 100) + "%" : `${completed}/${total}`; } else { progressValue = done ? 1 : 0.1 + Math.min(0.9, elapsed / 2e4); progressText = units === "bytes" ? (completed / (1024 * 1024)).toFixed(1) + "MB" : String(completed); } } return { stageTitle, progressValue: value + progressValue * duration, progressText, stepText: step || "", title: progressText ? `${stageTitle} (${progressText})${step ? ":" : "..."}` : stage !== "done" ? `${stageTitle}${step ? ":" : "..."}` : stageTitle }; } export class Progressbar extends Observer { startTime; lastStageStartTime; awaitRepaintPenaltyTime; finished; awaitRepaint; timings; onTiming; onFinish; appearanceDelay; domReady; el; #titleEl; #stepEl; constructor({ onTiming, onFinish, domReady }) { super({ stage: "inited", progress: null, error: null }); this.startTime = null; this.lastStageStartTime = null; this.awaitRepaintPenaltyTime = 0; this.finished = false; this.awaitRepaint = null; this.timings = []; this.onTiming = ensureFunction(onTiming); this.onFinish = ensureFunction(onFinish); this.domReady = domReady || Promise.resolve(); this.el = createElement("div", "view-progress skip-fast-track", [ createElement("div", "content main-secondary", [ this.#titleEl = createElement("span", "main"), this.#stepEl = createElement("span", "secondary") ]), createElement("div", "progress") ]); } recordTiming(stage, start, end = performance.now()) { const entry = { stage, title: loadStages[stage].title, duration: int(end - start) }; this.lastStageStartTime = end; this.timings.push(entry); this.onTiming(entry); } async #awaitRenderIfNeeded(enforce = false, now = performance.now()) { const timeSinceAwaitRepaint = now - (this.awaitRepaint || 0); const timeSinceLastStageStart = now - (this.lastStageStartTime || 0); if (enforce || timeSinceAwaitRepaint > 65 && timeSinceLastStageStart > 200) { const startAwaitRepaint = performance.now(); await letRepaintIfNeeded(); this.awaitRepaintPenaltyTime += performance.now() - startAwaitRepaint; this.awaitRepaint = performance.now(); } } async setState(state, step) { const currentStage = this.value.stage; const { stage = currentStage, progress = null, error = null } = state; if (this.finished) { return; } if (error) { this.set( "stage" in state ? { stage, progress, error } : { ...this.value, error } ); this.finish(error); return; } this.set({ stage, progress, error }); const stageChanged = stage !== currentStage; const now = performance.now(); if (currentStage === "inited") { this.startTime = now; } if (stageChanged) { if (this.lastStageStartTime !== null) { this.recordTiming(currentStage, this.lastStageStartTime, now); } this.lastStageStartTime = now; this.awaitRepaint = now; } const { title, stepText, progressValue } = decodeStageProgress(stage, progress, step); this.el.style.setProperty("--progress", String(progressValue)); this.#titleEl.textContent = title; this.#stepEl.textContent = stepText; this.el.offsetWidth; await this.#awaitRenderIfNeeded(stageChanged, now); } async setStateStep(step) { const { title, stepText } = decodeStageProgress(this.value.stage, this.value.progress, step); this.#titleEl.textContent = title; this.#stepEl.textContent = stepText; this.el.offsetWidth; await this.#awaitRenderIfNeeded(true); } finish(error) { if (!this.finished) { this.finished = true; if (this.lastStageStartTime !== null) { this.recordTiming( this.value.stage, this.lastStageStartTime ); } this.recordTiming(error ? "error" : "done", this.startTime || this.lastStageStartTime || performance.now()); this.set({ stage: "done", progress: null, error: error || null }); this.onFinish(Object.assign([...this.timings], { awaitRepaintPenaltyTime: Math.round(this.awaitRepaintPenaltyTime) })); this.el.classList.add("done"); this.el.classList.toggle("error", Boolean(error)); } } dispose() { this.finish(); this.el.remove(); } }