Browse Source

Gracefully handle missing events when loading a publication

master
buttercat1791 12 months ago
parent
commit
e35bfd75a7
  1. 25
      src/lib/components/Publication.svelte
  2. 24
      src/lib/components/PublicationSection.svelte
  3. 20
      src/lib/data_structures/lazy.ts
  4. 55
      src/lib/data_structures/publication_tree.ts

25
src/lib/components/Publication.svelte

@ -1,5 +1,6 @@
<script lang="ts"> <script lang="ts">
import { import {
Alert,
Button, Button,
Sidebar, Sidebar,
SidebarGroup, SidebarGroup,
@ -10,7 +11,7 @@
Tooltip, Tooltip,
} from "flowbite-svelte"; } from "flowbite-svelte";
import { getContext, onMount } from "svelte"; import { getContext, onMount } from "svelte";
import { BookOutline } from "flowbite-svelte-icons"; import { BookOutline, ExclamationCircleOutline } from "flowbite-svelte-icons";
import { page } from "$app/state"; import { page } from "$app/state";
import type { NDKEvent } from "@nostr-dev-kit/ndk"; import type { NDKEvent } from "@nostr-dev-kit/ndk";
import PublicationSection from "./PublicationSection.svelte"; import PublicationSection from "./PublicationSection.svelte";
@ -153,6 +154,9 @@
}); });
</script> </script>
<!-- TODO: Keep track of already-loaded leaves. -->
<!-- TODO: Handle entering mid-document and scrolling up. -->
<!-- TODO: Make loading more gradual. -->
{#if showTocButton && !showToc} {#if showTocButton && !showToc}
<!-- <Button <!-- <Button
@ -185,12 +189,19 @@
{/if} --> {/if} -->
<div class="flex flex-col space-y-4 max-w-2xl"> <div class="flex flex-col space-y-4 max-w-2xl">
{#each leaves as leaf, i} {#each leaves as leaf, i}
<PublicationSection {#if leaf == null}
rootAddress={rootAddress} <Alert class='flex space-x-2'>
leaves={leaves} <ExclamationCircleOutline class='w-5 h-5' />
address={leaf.tagAddress()} Error loading content. One or more events could not be loaded.
ref={(el) => setLastElementRef(el, i)} </Alert>
/> {:else}
<PublicationSection
rootAddress={rootAddress}
leaves={leaves}
address={leaf.tagAddress()}
ref={(el) => setLastElementRef(el, i)}
/>
{/if}
{/each} {/each}
</div> </div>

24
src/lib/components/PublicationSection.svelte

@ -23,24 +23,38 @@
let leafEvent: Promise<NDKEvent | null> = $derived.by(async () => let leafEvent: Promise<NDKEvent | null> = $derived.by(async () =>
await publicationTree.getEvent(address)); await publicationTree.getEvent(address));
let rootEvent: Promise<NDKEvent | null> = $derived.by(async () => let rootEvent: Promise<NDKEvent | null> = $derived.by(async () =>
await publicationTree.getEvent(rootAddress)); await publicationTree.getEvent(rootAddress));
let publicationType: Promise<string | undefined> = $derived.by(async () => let publicationType: Promise<string | undefined> = $derived.by(async () =>
(await rootEvent)?.getMatchingTags('type')[0]?.[1]); (await rootEvent)?.getMatchingTags('type')[0]?.[1]);
let leafHierarchy: Promise<NDKEvent[]> = $derived.by(async () => let leafHierarchy: Promise<NDKEvent[]> = $derived.by(async () =>
await publicationTree.getHierarchy(address)); await publicationTree.getHierarchy(address));
let leafTitle: Promise<string | undefined> = $derived.by(async () => let leafTitle: Promise<string | undefined> = $derived.by(async () =>
(await leafEvent)?.getMatchingTags('title')[0]?.[1]); (await leafEvent)?.getMatchingTags('title')[0]?.[1]);
let leafContent: Promise<string | Document> = $derived.by(async () => let leafContent: Promise<string | Document> = $derived.by(async () =>
asciidoctor.convert((await leafEvent)?.content ?? '')); asciidoctor.convert((await leafEvent)?.content ?? ''));
let previousLeafEvent: NDKEvent | null = $derived.by(() => { let previousLeafEvent: NDKEvent | null = $derived.by(() => {
const index = leaves.findIndex(leaf => leaf.tagAddress() === address); let index: number;
if (index === 0) { let event: NDKEvent | null = null;
return null; let decrement = 1;
}
return leaves[index - 1]; do {
index = leaves.findIndex(leaf => leaf?.tagAddress() === address);
if (index === 0) {
return null;
}
event = leaves[index - decrement++];
} while (event == null && index - decrement >= 0);
return event;
}); });
let previousLeafHierarchy: Promise<NDKEvent[] | null> = $derived.by(async () => { let previousLeafHierarchy: Promise<NDKEvent[] | null> = $derived.by(async () => {
if (!previousLeafEvent) { if (!previousLeafEvent) {
return null; return null;

20
src/lib/data_structures/lazy.ts

@ -1,16 +1,32 @@
export enum LazyStatus {
Pending,
Resolved,
Error,
}
export class Lazy<T> { export class Lazy<T> {
#value?: T; #value?: T;
#resolver: () => Promise<T>; #resolver: () => Promise<T>;
status: LazyStatus;
constructor(resolver: () => Promise<T>) { constructor(resolver: () => Promise<T>) {
this.#resolver = resolver; this.#resolver = resolver;
this.status = LazyStatus.Pending;
} }
async value(): Promise<T> { async value(): Promise<T | null> {
if (!this.#value) { if (!this.#value) {
this.#value = await this.#resolver(); try {
this.#value = await this.#resolver();
} catch (error) {
this.status = LazyStatus.Error;
console.error(error);
return null;
}
} }
this.status = LazyStatus.Resolved;
return this.#value; return this.#value;
} }
} }

55
src/lib/data_structures/publication_tree.ts

@ -8,14 +8,20 @@ enum PublicationTreeNodeType {
Leaf, Leaf,
} }
enum PublicationTreeNodeStatus {
Resolved,
Error,
}
interface PublicationTreeNode { interface PublicationTreeNode {
type: PublicationTreeNodeType; type: PublicationTreeNodeType;
status: PublicationTreeNodeStatus;
address: string; address: string;
parent?: PublicationTreeNode; parent?: PublicationTreeNode;
children?: Array<Lazy<PublicationTreeNode>>; children?: Array<Lazy<PublicationTreeNode>>;
} }
export class PublicationTree implements AsyncIterable<NDKEvent> { export class PublicationTree implements AsyncIterable<NDKEvent | null> {
/** /**
* The root node of the tree. * The root node of the tree.
*/ */
@ -50,6 +56,7 @@ export class PublicationTree implements AsyncIterable<NDKEvent> {
const rootAddress = rootEvent.tagAddress(); const rootAddress = rootEvent.tagAddress();
this.#root = { this.#root = {
type: this.#getNodeType(rootEvent), type: this.#getNodeType(rootEvent),
status: PublicationTreeNodeStatus.Resolved,
address: rootAddress, address: rootAddress,
children: [], children: [],
}; };
@ -84,6 +91,7 @@ export class PublicationTree implements AsyncIterable<NDKEvent> {
const node: PublicationTreeNode = { const node: PublicationTreeNode = {
type: await this.#getNodeType(event), type: await this.#getNodeType(event),
status: PublicationTreeNodeStatus.Resolved,
address, address,
parent: parentNode, parent: parentNode,
children: [], children: [],
@ -134,7 +142,7 @@ export class PublicationTree implements AsyncIterable<NDKEvent> {
* @param address The address of the parent node. * @param address The address of the parent node.
* @returns An array of addresses of any loaded child nodes. * @returns An array of addresses of any loaded child nodes.
*/ */
async getChildAddresses(address: string): Promise<string[]> { async getChildAddresses(address: string): Promise<Array<string | null>> {
const node = await this.#nodes.get(address)?.value(); const node = await this.#nodes.get(address)?.value();
if (!node) { if (!node) {
throw new Error(`PublicationTree: Node with address ${address} not found.`); throw new Error(`PublicationTree: Node with address ${address} not found.`);
@ -142,7 +150,7 @@ export class PublicationTree implements AsyncIterable<NDKEvent> {
return Promise.all( return Promise.all(
node.children?.map(async child => node.children?.map(async child =>
(await child.value()).address (await child.value())?.address ?? null
) ?? [] ) ?? []
); );
} }
@ -205,20 +213,26 @@ export class PublicationTree implements AsyncIterable<NDKEvent> {
async tryMoveToFirstChild(): Promise<boolean> { async tryMoveToFirstChild(): Promise<boolean> {
if (!this.target) { if (!this.target) {
throw new Error("Cursor: Target node is null or undefined."); console.debug("Cursor: Target node is null or undefined.");
return false;
} }
if (this.target.type === PublicationTreeNodeType.Leaf) { if (this.target.type === PublicationTreeNodeType.Leaf) {
return false; return false;
} }
this.target = (await this.target.children?.at(0)?.value())!; if (this.target.children == null || this.target.children.length === 0) {
return false;
}
this.target = await this.target.children?.at(0)?.value();
return true; return true;
} }
async tryMoveToNextSibling(): Promise<boolean> { async tryMoveToNextSibling(): Promise<boolean> {
if (!this.target) { if (!this.target) {
throw new Error("Cursor: Target node is null or undefined."); console.debug("Cursor: Target node is null or undefined.");
return false;
} }
const parent = this.target.parent; const parent = this.target.parent;
@ -228,25 +242,27 @@ export class PublicationTree implements AsyncIterable<NDKEvent> {
} }
const currentIndex = await siblings.findIndexAsync( const currentIndex = await siblings.findIndexAsync(
async (sibling: Lazy<PublicationTreeNode>) => (await sibling.value()).address === this.target!.address async (sibling: Lazy<PublicationTreeNode>) => (await sibling.value())?.address === this.target!.address
); );
if (currentIndex === -1) { if (currentIndex === -1) {
return false; return false;
} }
const nextSibling = (await siblings.at(currentIndex + 1)?.value()) ?? null; if (currentIndex + 1 >= siblings.length) {
if (!nextSibling) {
return false; return false;
} }
const nextSibling = (await siblings.at(currentIndex + 1)?.value());
this.target = nextSibling; this.target = nextSibling;
return true; return true;
} }
tryMoveToParent(): boolean { tryMoveToParent(): boolean {
if (!this.target) { if (!this.target) {
throw new Error("Cursor: Target node is null or undefined."); console.debug("Cursor: Target node is null or undefined.");
return false;
} }
const parent = this.target.parent; const parent = this.target.parent;
@ -263,11 +279,11 @@ export class PublicationTree implements AsyncIterable<NDKEvent> {
// #region Async Iterator Implementation // #region Async Iterator Implementation
[Symbol.asyncIterator](): AsyncIterator<NDKEvent> { [Symbol.asyncIterator](): AsyncIterator<NDKEvent | null> {
return this; return this;
} }
async next(): Promise<IteratorResult<NDKEvent>> { async next(): Promise<IteratorResult<NDKEvent | null>> {
if (!this.#cursor.target) { if (!this.#cursor.target) {
await this.#cursor.tryMoveTo(this.#bookmark); await this.#cursor.tryMoveTo(this.#bookmark);
} }
@ -297,7 +313,7 @@ export class PublicationTree implements AsyncIterable<NDKEvent> {
} while (this.#cursor.target?.type !== PublicationTreeNodeType.Leaf); } while (this.#cursor.target?.type !== PublicationTreeNodeType.Leaf);
const event = await this.getEvent(this.#cursor.target!.address); const event = await this.getEvent(this.#cursor.target!.address);
return { done: false, value: event! }; return { done: false, value: event };
} }
// #endregion // #endregion
@ -396,9 +412,17 @@ export class PublicationTree implements AsyncIterable<NDKEvent> {
}); });
if (!event) { if (!event) {
throw new Error( console.debug(
`PublicationTree: Event with address ${address} not found.` `PublicationTree: Event with address ${address} not found.`
); );
return {
type: PublicationTreeNodeType.Leaf,
status: PublicationTreeNodeStatus.Error,
address,
parent: parentNode,
children: [],
};
} }
this.#events.set(address, event); this.#events.set(address, event);
@ -406,7 +430,8 @@ export class PublicationTree implements AsyncIterable<NDKEvent> {
const childAddresses = event.tags.filter(tag => tag[0] === 'a').map(tag => tag[1]); const childAddresses = event.tags.filter(tag => tag[0] === 'a').map(tag => tag[1]);
const node: PublicationTreeNode = { const node: PublicationTreeNode = {
type: await this.#getNodeType(event), type: this.#getNodeType(event),
status: PublicationTreeNodeStatus.Resolved,
address, address,
parent: parentNode, parent: parentNode,
children: [], children: [],

Loading…
Cancel
Save