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

24
src/lib/components/PublicationSection.svelte

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

20
src/lib/data_structures/lazy.ts

@ -1,16 +1,32 @@ @@ -1,16 +1,32 @@
export enum LazyStatus {
Pending,
Resolved,
Error,
}
export class Lazy<T> {
#value?: T;
#resolver: () => Promise<T>;
status: LazyStatus;
constructor(resolver: () => Promise<T>) {
this.#resolver = resolver;
this.status = LazyStatus.Pending;
}
async value(): Promise<T> {
async value(): Promise<T | null> {
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;
}
}

55
src/lib/data_structures/publication_tree.ts

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

Loading…
Cancel
Save