Skip to content

Commit

Permalink
Add crappy loading loading boundary + script loader to all views
Browse files Browse the repository at this point in the history
  • Loading branch information
blacksmithgu committed Jul 3, 2024
1 parent 98ea006 commit 1521445
Show file tree
Hide file tree
Showing 5 changed files with 173 additions and 66 deletions.
7 changes: 7 additions & 0 deletions src/index/datacore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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);
Expand All @@ -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 {
Expand All @@ -233,6 +237,8 @@ export class DatacoreInitializer extends Component {
/** Deferred promise which resolves when importing is done. */
done: Deferred<InitializationStats>;

/** 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. */
Expand All @@ -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 = [];
Expand Down
23 changes: 23 additions & 0 deletions src/styles/errors.css → src/ui/errors.css
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
74 changes: 44 additions & 30 deletions src/ui/javascript.tsx
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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(
<DatacoreContextProvider
app={this.api.app}
Expand All @@ -48,14 +47,15 @@ export class DatacoreJSRenderer extends MarkdownRenderChild {
>
<CURRENT_FILE_CONTEXT.Provider value={this.path}>
<SimpleErrorBoundary message="The datacore script failed to execute.">
{renderableElement}
<LoadingBoundary datacore={this.api.core}>
<ScriptContainer executor={renderer} sourcePath={this.path} />
</LoadingBoundary>
</SimpleErrorBoundary>
</CURRENT_FILE_CONTEXT.Provider>
</DatacoreContextProvider>,
this.container
);
} catch (ex) {
console.error(ex);
render(
<ErrorMessage message="Datacore failed to render the code block." error={"" + ex} />,
this.container
Expand All @@ -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(
<DatacoreContextProvider
app={this.app}
component={this}
datacore={this.datacore}
settings={this.datacore.settings}
>
<CURRENT_FILE_CONTEXT.Provider value={this.sourcePath}>
<LoadingBoundary datacore={this.datacore}>{this.element}</LoadingBoundary>
</CURRENT_FILE_CONTEXT.Provider>
</DatacoreContextProvider>,
this.container
);
} else if (isValidElement(object)) {
return object;
} else {
return <Lit value={object} sourcePath={sourcePath} />;
}

public onunload(): void {
unmountComponentAtNode(this.container);
}
}
94 changes: 94 additions & 0 deletions src/ui/loading-boundary.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<p>
{datacore.initializer?.initialized ?? 0} / {datacore.initializer?.targetTotal ?? 0}
</p>
);
}

/** 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 (
<div className="datacore-loading-boundary">
<h4 className="datacore-loading-title">Datacore is getting ready...</h4>
<div className="datacore-loading-content">
<LoadingProgress datacore={datacore} />
</div>
</div>
);
}
}

/**
* 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<Literal | VNode | Function>;
sourcePath: string;
}) {
const [element, setElement] = useState<JSX.Element | undefined>(undefined);
const [error, setError] = useState<Error | undefined>(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 ?? <ErrorMessage message="< View is rendering >" />}</>;
}

/** 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 <Lit value={object} sourcePath={sourcePath} />;
}
}
41 changes: 5 additions & 36 deletions src/ui/markdown.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
/** 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";
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<Component>(undefined!);
export const APP_CONTEXT = createContext<App>(undefined!);
Expand Down Expand Up @@ -301,34 +301,3 @@ export function SimpleErrorBoundary({
return <Fragment>{children}</Fragment>;
}
}

/** 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(
<DatacoreContextProvider
app={this.app}
component={this}
datacore={this.datacore}
settings={this.datacore.settings}
>
<CURRENT_FILE_CONTEXT.Provider value={this.sourcePath}>{this.element}</CURRENT_FILE_CONTEXT.Provider>
</DatacoreContextProvider>,
this.container
);
}

public onunload(): void {
unmountComponentAtNode(this.container);
}
}

0 comments on commit 1521445

Please sign in to comment.