diff --git a/src/index/datacore.ts b/src/index/datacore.ts index efa72c1d..1f316371 100644 --- a/src/index/datacore.ts +++ b/src/index/datacore.ts @@ -100,6 +100,7 @@ export class Datacore extends Component { this.datastore.touch(); this.trigger("update", this.revision); + this.trigger("initialized"); // Clean up any documents which no longer exist in the vault. // TODO: I think this may race with other concurrent operations, so @@ -194,6 +195,7 @@ export class Datacore extends Component { /** Called whenever the index updates to a new revision. This is the broadest possible datacore event. */ public on(evt: "update", callback: (revision: number) => any, context?: any): EventRef; + public on(evt: "initialized", callback: () => any, context?: any): EventRef; on(evt: string, callback: (...data: any) => any, context?: any): EventRef { return this.events.on(evt, callback, context); @@ -211,6 +213,8 @@ export class Datacore extends Component { /** Trigger an update event. */ private trigger(evt: "update", revision: number): void; + /** Trigger an initialization event. */ + private trigger(evt: "initialized"): void; /** Trigger an event. */ private trigger(evt: string, ...args: any[]): void { @@ -233,6 +237,8 @@ export class DatacoreInitializer extends Component { /** Deferred promise which resolves when importing is done. */ done: Deferred; + /** The total number of target files to import. */ + targetTotal: number; /** The time that init started in milliseconds. */ start: number; /** Total number of files to import. */ @@ -251,6 +257,7 @@ export class DatacoreInitializer extends Component { this.active = false; this.queue = this.core.vault.getFiles(); + this.targetTotal = this.queue.length; this.files = this.queue.length; this.start = Date.now(); this.current = []; diff --git a/src/styles/errors.css b/src/ui/errors.css similarity index 75% rename from src/styles/errors.css rename to src/ui/errors.css index 388aeaed..2fd249b4 100644 --- a/src/styles/errors.css +++ b/src/ui/errors.css @@ -24,6 +24,29 @@ text-align: center; } +/** Loading views while the index is initializing. */ + +.datacore-loading-boundary { + width: 100%; + min-height: 150px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + border: 4px dashed var(--background-secondary); +} + +.datacore-loading-title { + text-align: center; +} + +.datacore-loading-content { + color: var(--text-muted); + text-align: center; +} + +/** Some niceties for rendering language blocks. */ + .block-language-datacore li.selected, .block-language-datacorejs li.selected { background: var(--text-accent); diff --git a/src/ui/javascript.tsx b/src/ui/javascript.tsx index 6665b097..2202273e 100644 --- a/src/ui/javascript.tsx +++ b/src/ui/javascript.tsx @@ -1,9 +1,11 @@ -import { Lit, ErrorMessage, SimpleErrorBoundary, CURRENT_FILE_CONTEXT, DatacoreContextProvider } from "ui/markdown"; -import { MarkdownRenderChild } from "obsidian"; +import { ErrorMessage, SimpleErrorBoundary, CURRENT_FILE_CONTEXT, DatacoreContextProvider } from "ui/markdown"; +import { App, MarkdownRenderChild } from "obsidian"; import { DatacoreLocalApi } from "api/local-api"; -import { JSX, createElement, h, isValidElement, render, Fragment } from "preact"; +import { h, render, Fragment, VNode } from "preact"; import { unmountComponentAtNode } from "preact/compat"; import { ScriptLanguage, asyncEvalInContext, transpile } from "utils/javascript"; +import { LoadingBoundary, ScriptContainer } from "./loading-boundary"; +import { Datacore } from "index/datacore"; /** * Renders a script by executing it and handing it the appropriate React context to execute @@ -28,17 +30,14 @@ export class DatacoreJSRenderer extends MarkdownRenderChild { // Attempt to parse and evaluate the script to produce either a renderable JSX object or a function. try { const primitiveScript = transpile(this.script, this.language); + const renderer = async () => { + return await asyncEvalInContext(primitiveScript, { + dc: this.api, + h: h, + Fragment: Fragment, + }); + }; - const renderable = await asyncEvalInContext(primitiveScript, { - dc: this.api, - h: h, - Fragment: Fragment, - }); - - // Early return in case state changes during the async call above. - if (!this.loaded) return; - - const renderableElement = makeRenderableElement(renderable, this.path); render( - {renderableElement} + + + , this.container ); } catch (ex) { - console.error(ex); render( , this.container @@ -67,23 +67,37 @@ export class DatacoreJSRenderer extends MarkdownRenderChild { if (this.loaded) unmountComponentAtNode(this.container); this.loaded = false; } - - /** Attempts to convert the script in the given language to plain javascript; will throw an Error on failure. */ } -/** Make a renderable element from the returned object; if this transformation is not possible, throw an exception. */ -export function makeRenderableElement(object: any, sourcePath: string): JSX.Element { - if (typeof object === "function") { - return createElement(object, {}); - } else if (Array.isArray(object)) { - return createElement( - "div", - {}, - (object as any[]).map((x) => makeRenderableElement(x, sourcePath)) +/** A trivial wrapper which allows a react component to live for the duration of a `MarkdownRenderChild`. */ +export class ReactRenderer extends MarkdownRenderChild { + public constructor( + public app: App, + public datacore: Datacore, + public container: HTMLElement, + public sourcePath: string, + public element: VNode + ) { + super(container); + } + + public onload(): void { + render( + + + {this.element} + + , + this.container ); - } else if (isValidElement(object)) { - return object; - } else { - return ; + } + + public onunload(): void { + unmountComponentAtNode(this.container); } } diff --git a/src/ui/loading-boundary.tsx b/src/ui/loading-boundary.tsx new file mode 100644 index 00000000..5a25d67d --- /dev/null +++ b/src/ui/loading-boundary.tsx @@ -0,0 +1,94 @@ +import { Datacore } from "index/datacore"; +import { PropsWithChildren, useEffect, useState } from "preact/compat"; +import { useIndexUpdates } from "./hooks"; +import { Literal } from "expression/literal"; +import { VNode, createElement, isValidElement } from "preact"; +import { ErrorMessage, Lit } from "./markdown"; + +import "./errors.css"; + +function LoadingProgress({ datacore }: { datacore: Datacore }) { + useIndexUpdates(datacore, { debounce: 250 }); + + return ( +

+ {datacore.initializer?.initialized ?? 0} / {datacore.initializer?.targetTotal ?? 0} +

+ ); +} + +/** Loading boundary which shows a loading screen while Datacore is initializing. */ +export function LoadingBoundary({ children, datacore }: PropsWithChildren<{ datacore: Datacore }>) { + const [initialized, setInitialized] = useState(datacore.initialized); + + // Syncs the boundary with datacore's initialization state. + // TODO: Add an event to datacore which indicates when a reindex happens (i.e., initialized + // returns back to 'false'). + useEffect(() => { + if (initialized) return; + + const ref = datacore.on("initialized", () => setInitialized(true)); + return () => datacore.offref(ref); + }, [initialized, datacore]); + + if (initialized) { + return <>{children}; + } else { + return ( +
+

Datacore is getting ready...

+
+ +
+
+ ); + } +} + +/** + * Executes a vanilla javasript function lazily one time. Mainly useful to only run a script + * once the parent loading boundary is actually ready. + */ +export function ScriptContainer({ + executor, + sourcePath, +}: { + executor: () => Promise; + sourcePath: string; +}) { + const [element, setElement] = useState(undefined); + const [error, setError] = useState(undefined); + + useEffect(() => { + setElement(undefined); + setError(undefined); + + executor() + .then((result) => setElement(makeRenderableElement(result, sourcePath))) + .catch((error) => setError(error)); + }, [executor]); + + // Propogate error upwards. + if (error) { + throw error; + } + + return <>{element ?? }; +} + +/** Make a renderable element from the returned object; if this transformation is not possible, throw an exception. */ +export function makeRenderableElement(object: any, sourcePath: string): JSX.Element { + if (typeof object === "function") { + return createElement(object, {}); + } else if (Array.isArray(object)) { + return createElement( + "div", + {}, + (object as any[]).map((x) => makeRenderableElement(x, sourcePath)) + ); + } else if (isValidElement(object)) { + return object; + } else { + return ; + } +} diff --git a/src/ui/markdown.tsx b/src/ui/markdown.tsx index 39c4a32b..4c2c3ba5 100644 --- a/src/ui/markdown.tsx +++ b/src/ui/markdown.tsx @@ -1,5 +1,5 @@ /** Provides core preact / rendering utilities for all view types. */ -import { App, MarkdownRenderChild, MarkdownRenderer } from "obsidian"; +import { App, MarkdownRenderer } from "obsidian"; import { Component } from "obsidian"; import { Link, Literal, Literals } from "expression/literal"; import { Datacore } from "index/datacore"; @@ -7,12 +7,12 @@ import { Settings } from "settings"; import { currentLocale, renderMinimalDate, renderMinimalDuration } from "utils/normalizers"; import { extractImageDimensions, isImageEmbed } from "utils/media"; -import { createContext, Fragment, VNode, render } from "preact"; +import { createContext, Fragment, render } from "preact"; import { useContext, useMemo, useCallback, useRef, useEffect, useErrorBoundary } from "preact/hooks"; -import { CSSProperties, PropsWithChildren, memo, unmountComponentAtNode } from "preact/compat"; -import { Embed } from "../api/ui/embed"; +import { CSSProperties, PropsWithChildren, memo } from "preact/compat"; +import { Embed } from "api/ui/embed"; -import "styles/errors.css"; +import "./errors.css"; export const COMPONENT_CONTEXT = createContext(undefined!); export const APP_CONTEXT = createContext(undefined!); @@ -301,34 +301,3 @@ export function SimpleErrorBoundary({ return {children}; } } - -/** A trivial wrapper which allows a react component to live for the duration of a `MarkdownRenderChild`. */ -export class ReactRenderer extends MarkdownRenderChild { - public constructor( - public app: App, - public datacore: Datacore, - public container: HTMLElement, - public sourcePath: string, - public element: VNode - ) { - super(container); - } - - public onload(): void { - render( - - {this.element} - , - this.container - ); - } - - public onunload(): void { - unmountComponentAtNode(this.container); - } -}