From 874103a6e0bbc81c35cad42afd4cf6bac7ae5c9e Mon Sep 17 00:00:00 2001 From: buttercat1791 Date: Tue, 4 Mar 2025 23:08:11 -0600 Subject: [PATCH 01/40] Start work on publication tree data structure --- src/lib/data_structures/publication_tree.ts | 57 +++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 src/lib/data_structures/publication_tree.ts diff --git a/src/lib/data_structures/publication_tree.ts b/src/lib/data_structures/publication_tree.ts new file mode 100644 index 0000000..07c7f84 --- /dev/null +++ b/src/lib/data_structures/publication_tree.ts @@ -0,0 +1,57 @@ +import type { NDKEvent } from "@nostr-dev-kit/ndk"; + +interface PublicationTreeNode { + address: string; + parent?: PublicationTreeNode; + children?: PublicationTreeNode[]; +} + +// TODO: Add public method(s) for event retrieval. +// TODO: Add methods for DFS and BFS traversal-retrieval. +export class PublicationTree { + private root: PublicationTreeNode; + private nodes: Map; + private events: Map; + + constructor(rootEvent: NDKEvent) { + const rootAddress = this.getAddress(rootEvent); + this.root = { address: rootAddress, children: [] }; + + this.nodes = new Map(); + this.nodes.set(rootAddress, this.root); + + this.events = new Map(); + this.events.set(rootAddress, rootEvent); + } + + addEvent(event: NDKEvent, parentEvent: NDKEvent) { + const address = this.getAddress(event); + const parentAddress = this.getAddress(parentEvent); + const parentNode = this.nodes.get(parentAddress); + + if (!parentNode) { + throw new Error( + `PublicationTree: Parent node with address ${parentAddress} not found.` + ); + } + + const node = { + address, + parent: parentNode, + children: [], + }; + parentNode.children!.push(node); + this.nodes.set(address, node); + this.events.set(address, event); + } + + private getAddress(event: NDKEvent): string { + if (event.kind! < 30000 || event.kind! >= 40000) { + throw new Error( + "PublicationTree: Invalid event kind. Event kind must be in the range 30000-39999" + ); + } + + return `${event.kind}:${event.pubkey}:${event.dTag}`; + } +} \ No newline at end of file From f11dca162cb86d84ff9c568459bbdb278402b17b Mon Sep 17 00:00:00 2001 From: buttercat1791 Date: Wed, 5 Mar 2025 08:43:54 -0600 Subject: [PATCH 02/40] Add depth-first traversal to the publication tree --- src/lib/data_structures/publication_tree.ts | 105 ++++++++++++++++++-- 1 file changed, 97 insertions(+), 8 deletions(-) diff --git a/src/lib/data_structures/publication_tree.ts b/src/lib/data_structures/publication_tree.ts index 07c7f84..a9076ea 100644 --- a/src/lib/data_structures/publication_tree.ts +++ b/src/lib/data_structures/publication_tree.ts @@ -1,4 +1,5 @@ -import type { NDKEvent } from "@nostr-dev-kit/ndk"; +import type NDK from "@nostr-dev-kit/ndk"; +import type { NDKEvent, NDKFilter } from "@nostr-dev-kit/ndk"; interface PublicationTreeNode { address: string; @@ -6,15 +7,15 @@ interface PublicationTreeNode { children?: PublicationTreeNode[]; } -// TODO: Add public method(s) for event retrieval. -// TODO: Add methods for DFS and BFS traversal-retrieval. +// TODO: Add an iterator over the leaves of the tree. export class PublicationTree { private root: PublicationTreeNode; private nodes: Map; private events: Map; + private ndk: NDK; - constructor(rootEvent: NDKEvent) { - const rootAddress = this.getAddress(rootEvent); + constructor(rootEvent: NDKEvent, ndk: NDK) { + const rootAddress = this.getAddressFromEvent(rootEvent); this.root = { address: rootAddress, children: [] }; this.nodes = new Map(); @@ -22,11 +23,21 @@ export class PublicationTree { this.events = new Map(); this.events.set(rootAddress, rootEvent); + + this.ndk = ndk; } + /** + * Adds an event to the publication tree. + * @param event The event to be added. + * @param parentEvent The parent event of the event to be added. + * @throws An error if the parent event is not in the tree. + * @description The parent event must already be in the tree. Use + * {@link PublicationTree.getEvent} to retrieve an event already in the tree. + */ addEvent(event: NDKEvent, parentEvent: NDKEvent) { - const address = this.getAddress(event); - const parentAddress = this.getAddress(parentEvent); + const address = this.getAddressFromEvent(event); + const parentAddress = this.getAddressFromEvent(parentEvent); const parentNode = this.nodes.get(parentAddress); if (!parentNode) { @@ -45,7 +56,83 @@ export class PublicationTree { this.events.set(address, event); } - private getAddress(event: NDKEvent): string { + /** + * Retrieves an event from the publication tree. + * @param address The address of the event to retrieve. + * @returns The event, or null if the event is not found. + */ + async getEvent(address: string): Promise { + let event = this.events.get(address) ?? null; + if (!event) { + event = await this.depthFirstRetrieve(address); + } + + return event; + } + + // #region Private Methods + + /** + * Traverses the publication tree in a depth-first manner to retrieve an event, filling in + * missing nodes during the traversal. + * @param address The address of the event to retrieve. + * @returns The event, or null if the event is not found. + */ + private async depthFirstRetrieve(address: string): Promise { + if (this.nodes.has(address)) { + return this.events.get(address)!; + } + + const stack: string[] = [this.root.address]; + let currentEvent: NDKEvent | null | undefined; + while (stack.length > 0) { + const currentAddress = stack.pop(); + + // Stop immediately if the target of the search is found. + if (currentAddress === address) { + return this.events.get(address)!; + } + + // Augment the tree with the children of the current event. + const currentChildAddresses = this.events + .get(currentAddress!)!.tags + .filter(tag => tag[0] === 'a') + .map(tag => tag[1]); + + const kinds = new Set(); + const pubkeys = new Set(); + const dTags = new Set(); + for (const childAddress of currentChildAddresses) { + if (this.nodes.has(childAddress)) { + continue; + } + + const [kind, pubkey, dTag] = childAddress.split(':'); + kinds.add(parseInt(kind)); + pubkeys.add(pubkey); + dTags.add(dTag); + } + + const childEvents = await this.ndk.fetchEvents({ + kinds: Array.from(kinds), + authors: Array.from(pubkeys), + '#d': Array.from(dTags), + }); + + for (const childEvent of childEvents) { + this.addEvent(childEvent, currentEvent!); + } + + // Push the popped address's children onto the stack for the next iteration. + while (currentChildAddresses.length > 0) { + stack.push(currentChildAddresses.pop()!); + } + } + + return null; + } + + private getAddressFromEvent(event: NDKEvent): string { if (event.kind! < 30000 || event.kind! >= 40000) { throw new Error( "PublicationTree: Invalid event kind. Event kind must be in the range 30000-39999" @@ -54,4 +141,6 @@ export class PublicationTree { return `${event.kind}:${event.pubkey}:${event.dTag}`; } + + // #endregion } \ No newline at end of file From 8c9319aa64bef82ce6d508b8ad1ca06b7c169643 Mon Sep 17 00:00:00 2001 From: buttercat1791 Date: Thu, 6 Mar 2025 09:26:01 -0600 Subject: [PATCH 03/40] Stub out methods to make the tree both iterable and its own iterator Consumers should be able to invoke `.next()` on an instance of the tree to retrieve events one at a time. --- src/lib/data_structures/publication_tree.ts | 33 +++++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/src/lib/data_structures/publication_tree.ts b/src/lib/data_structures/publication_tree.ts index a9076ea..c6e7802 100644 --- a/src/lib/data_structures/publication_tree.ts +++ b/src/lib/data_structures/publication_tree.ts @@ -7,11 +7,30 @@ interface PublicationTreeNode { children?: PublicationTreeNode[]; } -// TODO: Add an iterator over the leaves of the tree. -export class PublicationTree { +export class PublicationTree implements Iterable { + /** + * The root node of the tree. + */ private root: PublicationTreeNode; + + /** + * A map of addresses in the tree to their corresponding nodes. + */ private nodes: Map; + + /** + * A map of addresses in the tree to their corresponding events. + */ private events: Map; + + /** + * The address of the last-visited node. Used for iteration and progressive retrieval. + */ + private bookmark?: string; + + /** + * The NDK instance used to fetch events. + */ private ndk: NDK; constructor(rootEvent: NDKEvent, ndk: NDK) { @@ -70,6 +89,16 @@ export class PublicationTree { return event; } + [Symbol.iterator](): Iterator { + return this; + } + + next(): IteratorResult { + // TODO: Implement iteration from the bookmark over subsequent leaves. + + return { done: true, value: null }; + } + // #region Private Methods /** From baca84f487c515a51ced3d99f2d18dae4e08ada0 Mon Sep 17 00:00:00 2001 From: buttercat1791 Date: Fri, 7 Mar 2025 09:32:22 -0600 Subject: [PATCH 04/40] Iteration WIP --- src/lib/data_structures/publication_tree.ts | 93 +++++++++++++++++++-- 1 file changed, 85 insertions(+), 8 deletions(-) diff --git a/src/lib/data_structures/publication_tree.ts b/src/lib/data_structures/publication_tree.ts index c6e7802..9862608 100644 --- a/src/lib/data_structures/publication_tree.ts +++ b/src/lib/data_structures/publication_tree.ts @@ -7,7 +7,7 @@ interface PublicationTreeNode { children?: PublicationTreeNode[]; } -export class PublicationTree implements Iterable { +export class PublicationTree implements AsyncIterable { /** * The root node of the tree. */ @@ -23,6 +23,11 @@ export class PublicationTree implements Iterable { */ private events: Map; + /** + * An ordered list of the addresses of the leaves of the tree. + */ + private leaves: string[] = []; + /** * The address of the last-visited node. Used for iteration and progressive retrieval. */ @@ -89,12 +94,33 @@ export class PublicationTree implements Iterable { return event; } - [Symbol.iterator](): Iterator { + /** + * Sets a start point for iteration over the leaves of the tree. + * @param address The address of the event to bookmark. + */ + setBookmark(address: string) { + this.bookmark = address; + } + + [Symbol.asyncIterator](): AsyncIterator { return this; } - next(): IteratorResult { - // TODO: Implement iteration from the bookmark over subsequent leaves. + async next(): Promise> { + // If no bookmark is set, start at the first leaf. Retrieve that first leaf if necessary. + if (!this.bookmark) { + this.bookmark = this.leaves.at(0); + if (this.bookmark) { + const bookmarkEvent = await this.getEvent(this.bookmark); + return { done: false, value: bookmarkEvent! }; + } + + const firstLeafEvent = await this.depthFirstRetrieve(); + this.bookmark = this.getAddressFromEvent(firstLeafEvent!); + return { done: false, value: firstLeafEvent! }; + } + + // TODO: Invoke a funciton to retrieve the next sibling of the bookmark. return { done: true, value: null }; } @@ -104,11 +130,12 @@ export class PublicationTree implements Iterable { /** * Traverses the publication tree in a depth-first manner to retrieve an event, filling in * missing nodes during the traversal. - * @param address The address of the event to retrieve. + * @param address The address of the event to retrieve. If no address is provided, the function + * will return the first leaf in the tree. * @returns The event, or null if the event is not found. */ - private async depthFirstRetrieve(address: string): Promise { - if (this.nodes.has(address)) { + private async depthFirstRetrieve(address?: string): Promise { + if (address && this.nodes.has(address)) { return this.events.get(address)!; } @@ -118,7 +145,7 @@ export class PublicationTree implements Iterable { const currentAddress = stack.pop(); // Stop immediately if the target of the search is found. - if (currentAddress === address) { + if (address != null && currentAddress === address) { return this.events.get(address)!; } @@ -152,6 +179,18 @@ export class PublicationTree implements Iterable { this.addEvent(childEvent, currentEvent!); } + // If the current event has no children, it is a leaf. + if (childEvents.size === 0) { + this.leaves.push(currentAddress!); + + // Return the first leaf if no address was provided. + if (address == null) { + return currentEvent!; + } + + continue; + } + // Push the popped address's children onto the stack for the next iteration. while (currentChildAddresses.length > 0) { stack.push(currentChildAddresses.pop()!); @@ -161,6 +200,44 @@ export class PublicationTree implements Iterable { return null; } + private async getNextSibling(address: string): Promise { + if (!this.leaves.includes(address)) { + throw new Error( + `PublicationTree: Address ${address} is not a leaf. Cannot retrieve next sibling.` + ); + } + + let currentNode = this.nodes.get(address); + if (!currentNode) { + return null; + } + + let parent = currentNode.parent; + if (!parent) { + throw new Error( + `PublicationTree: Address ${address} has no parent. Cannot retrieve next sibling.` + ); + } + + // TODO: Handle the case where the current node is the last leaf. + + let nextSibling: PublicationTreeNode | null = null; + do { + const siblings: PublicationTreeNode[] = parent!.children!; + const currentIndex = siblings.findIndex(sibling => sibling.address === currentNode!.address); + nextSibling = siblings.at(currentIndex + 1) ?? null; + + // If the next sibling has children, it is not a leaf. + if ((nextSibling?.children?.length ?? 0) > 0) { + currentNode = nextSibling!.children!.at(0)!; + parent = currentNode.parent; + nextSibling = null; + } + } while (nextSibling == null); + + return this.getEvent(nextSibling!.address); + } + private getAddressFromEvent(event: NDKEvent): string { if (event.kind! < 30000 || event.kind! >= 40000) { throw new Error( From 04c2a809377994a5e662c5c695cee6437aa8090c Mon Sep 17 00:00:00 2001 From: buttercat1791 Date: Sat, 8 Mar 2025 12:39:02 -0600 Subject: [PATCH 05/40] Define a nested Cursor class for tree iteration --- src/lib/data_structures/publication_tree.ts | 32 +++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/src/lib/data_structures/publication_tree.ts b/src/lib/data_structures/publication_tree.ts index 9862608..080a8db 100644 --- a/src/lib/data_structures/publication_tree.ts +++ b/src/lib/data_structures/publication_tree.ts @@ -249,4 +249,36 @@ export class PublicationTree implements AsyncIterable { } // #endregion + + // #region Iteration Cursor + + // TODO: Flesh out this class. + Cursor = class { + private tree: PublicationTree; + private currentNode: PublicationTreeNode | null | undefined; + + constructor(tree: PublicationTree, currentNode: PublicationTreeNode | null = null) { + this.tree = tree; + + if (!currentNode) { + this.currentNode = this.tree.bookmark + ? this.tree.nodes.get(this.tree.bookmark) + : null; + } + } + + firstChild(): PublicationTreeNode | null { + + } + + nextSibling(): PublicationTreeNode | null { + + } + + parent(): PublicationTreeNode | null { + + } + }; + + // #endregion } \ No newline at end of file From 866a54be496f20a76ca457dbd042b321e13d97ac Mon Sep 17 00:00:00 2001 From: buttercat1791 Date: Sun, 9 Mar 2025 12:16:41 -0500 Subject: [PATCH 06/40] Implement tree walking via internal Cursor class --- src/lib/data_structures/publication_tree.ts | 49 +++++++++++++++++++-- 1 file changed, 45 insertions(+), 4 deletions(-) diff --git a/src/lib/data_structures/publication_tree.ts b/src/lib/data_structures/publication_tree.ts index 080a8db..1713dfd 100644 --- a/src/lib/data_structures/publication_tree.ts +++ b/src/lib/data_structures/publication_tree.ts @@ -252,7 +252,6 @@ export class PublicationTree implements AsyncIterable { // #region Iteration Cursor - // TODO: Flesh out this class. Cursor = class { private tree: PublicationTree; private currentNode: PublicationTreeNode | null | undefined; @@ -267,16 +266,58 @@ export class PublicationTree implements AsyncIterable { } } - firstChild(): PublicationTreeNode | null { + moveToFirstChild(): boolean { + if (!this.currentNode) { + throw new Error("Cursor: Current node is null or undefined."); + } + + const hasChildren = (this.currentNode.children?.length ?? 0) > 0; + const isLeaf = this.tree.leaves.includes(this.currentNode.address); + + if (!hasChildren && isLeaf) { + return false; + } + if (!hasChildren && !isLeaf) { + // TODO: Fetch any missing children, then return the first child. + } + + this.currentNode = this.currentNode.children?.at(0); + return true; } - nextSibling(): PublicationTreeNode | null { + moveToNextSibling(): boolean { + if (!this.currentNode) { + throw new Error("Cursor: Current node is null or undefined."); + } + + const parent = this.currentNode.parent; + const siblings = parent?.children; + const currentIndex = siblings?.findIndex(sibling => + sibling.address === this.currentNode!.address + ); + + const nextSibling = siblings?.at(currentIndex! + 1); + if (!nextSibling) { + return false; + } + this.currentNode = nextSibling; + return true; } - parent(): PublicationTreeNode | null { + moveToParent(): boolean { + if (!this.currentNode) { + throw new Error("Cursor: Current node is null or undefined."); + } + + const parent = this.currentNode.parent; + if (!parent) { + return false; + } + this.currentNode = parent; + return true; } }; From d723a56fe5094d41ce6f8c402ea5f87fc4f02ff7 Mon Sep 17 00:00:00 2001 From: buttercat1791 Date: Sun, 9 Mar 2025 23:17:32 -0500 Subject: [PATCH 07/40] Make node children lazy-load --- src/lib/data_structures/lazy.ts | 13 +++++++++ src/lib/data_structures/publication_tree.ts | 30 ++++++++++++--------- 2 files changed, 31 insertions(+), 12 deletions(-) create mode 100644 src/lib/data_structures/lazy.ts diff --git a/src/lib/data_structures/lazy.ts b/src/lib/data_structures/lazy.ts new file mode 100644 index 0000000..d7203b8 --- /dev/null +++ b/src/lib/data_structures/lazy.ts @@ -0,0 +1,13 @@ +export class Lazy { + #value?: T; + + constructor(private readonly resolver: () => Promise) {} + + async value(): Promise { + if (!this.#value) { + this.#value = await this.resolver(); + } + + return this.#value; + } +} \ No newline at end of file diff --git a/src/lib/data_structures/publication_tree.ts b/src/lib/data_structures/publication_tree.ts index 1713dfd..c2289f0 100644 --- a/src/lib/data_structures/publication_tree.ts +++ b/src/lib/data_structures/publication_tree.ts @@ -1,10 +1,11 @@ import type NDK from "@nostr-dev-kit/ndk"; import type { NDKEvent, NDKFilter } from "@nostr-dev-kit/ndk"; +import type { Lazy } from "./lazy"; interface PublicationTreeNode { address: string; parent?: PublicationTreeNode; - children?: PublicationTreeNode[]; + children?: Array>; } export class PublicationTree implements AsyncIterable { @@ -40,7 +41,10 @@ export class PublicationTree implements AsyncIterable { constructor(rootEvent: NDKEvent, ndk: NDK) { const rootAddress = this.getAddressFromEvent(rootEvent); - this.root = { address: rootAddress, children: [] }; + this.root = { + address: rootAddress, + children: [], + }; this.nodes = new Map(); this.nodes.set(rootAddress, this.root); @@ -70,11 +74,13 @@ export class PublicationTree implements AsyncIterable { ); } + // TODO: Determine node type. const node = { address, parent: parentNode, children: [], }; + // TODO: Define a resolver for the lazy node. parentNode.children!.push(node); this.nodes.set(address, node); this.events.set(address, event); @@ -223,13 +229,13 @@ export class PublicationTree implements AsyncIterable { let nextSibling: PublicationTreeNode | null = null; do { - const siblings: PublicationTreeNode[] = parent!.children!; - const currentIndex = siblings.findIndex(sibling => sibling.address === currentNode!.address); - nextSibling = siblings.at(currentIndex + 1) ?? null; + const siblings: Lazy[] = parent!.children!; + const currentIndex = siblings.findIndex(async sibling => (await sibling.value()).address === currentNode!.address); + nextSibling = (await siblings.at(currentIndex + 1)?.value()) ?? null; // If the next sibling has children, it is not a leaf. if ((nextSibling?.children?.length ?? 0) > 0) { - currentNode = nextSibling!.children!.at(0)!; + currentNode = (await nextSibling!.children!.at(0)!.value())!; parent = currentNode.parent; nextSibling = null; } @@ -266,7 +272,7 @@ export class PublicationTree implements AsyncIterable { } } - moveToFirstChild(): boolean { + async moveToFirstChild(): Promise { if (!this.currentNode) { throw new Error("Cursor: Current node is null or undefined."); } @@ -282,22 +288,22 @@ export class PublicationTree implements AsyncIterable { // TODO: Fetch any missing children, then return the first child. } - this.currentNode = this.currentNode.children?.at(0); + this.currentNode = (await this.currentNode.children?.at(0)?.value())!; return true; } - moveToNextSibling(): boolean { + async moveToNextSibling(): Promise { if (!this.currentNode) { throw new Error("Cursor: Current node is null or undefined."); } const parent = this.currentNode.parent; const siblings = parent?.children; - const currentIndex = siblings?.findIndex(sibling => - sibling.address === this.currentNode!.address + const currentIndex = siblings?.findIndex(async sibling => + (await sibling.value()).address === this.currentNode!.address ); - const nextSibling = siblings?.at(currentIndex! + 1); + const nextSibling = (await siblings?.at(currentIndex! + 1)?.value()) ?? null; if (!nextSibling) { return false; } From b4906215c5a789ea72745389a9223a84a678f185 Mon Sep 17 00:00:00 2001 From: buttercat1791 Date: Mon, 10 Mar 2025 09:33:45 -0500 Subject: [PATCH 08/40] Work with lazy-loading nodes --- src/lib/data_structures/publication_tree.ts | 116 +++++++++++++++----- 1 file changed, 86 insertions(+), 30 deletions(-) diff --git a/src/lib/data_structures/publication_tree.ts b/src/lib/data_structures/publication_tree.ts index c2289f0..0ec929c 100644 --- a/src/lib/data_structures/publication_tree.ts +++ b/src/lib/data_structures/publication_tree.ts @@ -1,8 +1,15 @@ import type NDK from "@nostr-dev-kit/ndk"; import type { NDKEvent, NDKFilter } from "@nostr-dev-kit/ndk"; -import type { Lazy } from "./lazy"; +import { Lazy } from "./lazy"; + +enum PublicationTreeNodeType { + Root, + Branch, + Leaf, +} interface PublicationTreeNode { + type: PublicationTreeNodeType; address: string; parent?: PublicationTreeNode; children?: Array>; @@ -19,6 +26,12 @@ export class PublicationTree implements AsyncIterable { */ private nodes: Map; + /** + * A map of addresses in the tree to their corresponding lazy-loaded nodes. When a lazy node is + * retrieved, it is added to the {@link PublicationTree.nodes} map. + */ + private lazyNodes: Map> = new Map(); + /** * A map of addresses in the tree to their corresponding events. */ @@ -40,8 +53,9 @@ export class PublicationTree implements AsyncIterable { private ndk: NDK; constructor(rootEvent: NDKEvent, ndk: NDK) { - const rootAddress = this.getAddressFromEvent(rootEvent); + const rootAddress = rootEvent.tagAddress(); this.root = { + type: PublicationTreeNodeType.Root, address: rootAddress, children: [], }; @@ -64,8 +78,8 @@ export class PublicationTree implements AsyncIterable { * {@link PublicationTree.getEvent} to retrieve an event already in the tree. */ addEvent(event: NDKEvent, parentEvent: NDKEvent) { - const address = this.getAddressFromEvent(event); - const parentAddress = this.getAddressFromEvent(parentEvent); + const address = event.tagAddress(); + const parentAddress = parentEvent.tagAddress(); const parentNode = this.nodes.get(parentAddress); if (!parentNode) { @@ -74,18 +88,66 @@ export class PublicationTree implements AsyncIterable { ); } - // TODO: Determine node type. const node = { + type: this.getNodeType(event), address, parent: parentNode, children: [], }; - // TODO: Define a resolver for the lazy node. - parentNode.children!.push(node); + parentNode.children!.push(new Lazy(() => Promise.resolve(node))); this.nodes.set(address, node); this.events.set(address, event); } + /** + * Lazily adds an event to the publication tree by address if the full event is not already + * loaded into memory. + * @param address The address of the event to add. + * @param parentEvent The parent event of the event to add. + * @description The parent event must already be in the tree. Use + * {@link PublicationTree.getEvent} to retrieve an event already in the tree. + */ + addEventByAddress(address: string, parentEvent: NDKEvent) { + const parentAddress = parentEvent.tagAddress(); + const parentNode = this.nodes.get(parentAddress); + + if (!parentNode) { + throw new Error( + `PublicationTree: Parent node with address ${parentAddress} not found.` + ); + } + + const lazyNode = new Lazy(async () => { + const [kind, pubkey, dTag] = address.split(':'); + const event = await this.ndk.fetchEvent({ + kinds: [parseInt(kind)], + authors: [pubkey], + '#d': [dTag], + }); + + if (!event) { + throw new Error( + `PublicationTree: Event with address ${address} not found.` + ); + } + + const node: PublicationTreeNode = { + type: this.getNodeType(event), + address, + parent: parentNode, + children: [], + }; + + this.nodes.set(address, node); + this.events.set(address, event); + + return node; + }); + + parentNode.children!.push(lazyNode); + this.lazyNodes.set(address, lazyNode); + } + /** * Retrieves an event from the publication tree. * @param address The address of the event to retrieve. @@ -122,7 +184,7 @@ export class PublicationTree implements AsyncIterable { } const firstLeafEvent = await this.depthFirstRetrieve(); - this.bookmark = this.getAddressFromEvent(firstLeafEvent!); + this.bookmark = firstLeafEvent!.tagAddress(); return { done: false, value: firstLeafEvent! }; } @@ -161,32 +223,16 @@ export class PublicationTree implements AsyncIterable { .filter(tag => tag[0] === 'a') .map(tag => tag[1]); - const kinds = new Set(); - const pubkeys = new Set(); - const dTags = new Set(); for (const childAddress of currentChildAddresses) { if (this.nodes.has(childAddress)) { continue; } - - const [kind, pubkey, dTag] = childAddress.split(':'); - kinds.add(parseInt(kind)); - pubkeys.add(pubkey); - dTags.add(dTag); - } - - const childEvents = await this.ndk.fetchEvents({ - kinds: Array.from(kinds), - authors: Array.from(pubkeys), - '#d': Array.from(dTags), - }); - for (const childEvent of childEvents) { - this.addEvent(childEvent, currentEvent!); + this.addEventByAddress(childAddress, currentEvent!); } // If the current event has no children, it is a leaf. - if (childEvents.size === 0) { + if (currentChildAddresses.length === 0) { this.leaves.push(currentAddress!); // Return the first leaf if no address was provided. @@ -244,14 +290,24 @@ export class PublicationTree implements AsyncIterable { return this.getEvent(nextSibling!.address); } - private getAddressFromEvent(event: NDKEvent): string { - if (event.kind! < 30000 || event.kind! >= 40000) { + private getNodeType(event: NDKEvent): PublicationTreeNodeType { + const address = event.tagAddress(); + const node = this.nodes.get(address); + if (!node) { throw new Error( - "PublicationTree: Invalid event kind. Event kind must be in the range 30000-39999" + `PublicationTree: Event with address ${address} not found in the tree.` ); } - return `${event.kind}:${event.pubkey}:${event.dTag}`; + if (!node.parent) { + return PublicationTreeNodeType.Root; + } + + if (event.tags.some(tag => tag[0] === 'a')) { + return PublicationTreeNodeType.Branch; + } + + return PublicationTreeNodeType.Leaf; } // #endregion From 7da145fb5ec2466c9b497459aca409de1517cbcd Mon Sep 17 00:00:00 2001 From: buttercat1791 Date: Tue, 11 Mar 2025 09:14:51 -0500 Subject: [PATCH 09/40] Implement iterator `next()` method using cursor --- src/lib/data_structures/publication_tree.ts | 190 +++++++++----------- 1 file changed, 81 insertions(+), 109 deletions(-) diff --git a/src/lib/data_structures/publication_tree.ts b/src/lib/data_structures/publication_tree.ts index 0ec929c..9fd26c5 100644 --- a/src/lib/data_structures/publication_tree.ts +++ b/src/lib/data_structures/publication_tree.ts @@ -26,12 +26,6 @@ export class PublicationTree implements AsyncIterable { */ private nodes: Map; - /** - * A map of addresses in the tree to their corresponding lazy-loaded nodes. When a lazy node is - * retrieved, it is added to the {@link PublicationTree.nodes} map. - */ - private lazyNodes: Map> = new Map(); - /** * A map of addresses in the tree to their corresponding events. */ @@ -117,35 +111,9 @@ export class PublicationTree implements AsyncIterable { ); } - const lazyNode = new Lazy(async () => { - const [kind, pubkey, dTag] = address.split(':'); - const event = await this.ndk.fetchEvent({ - kinds: [parseInt(kind)], - authors: [pubkey], - '#d': [dTag], - }); - - if (!event) { - throw new Error( - `PublicationTree: Event with address ${address} not found.` - ); - } - - const node: PublicationTreeNode = { - type: this.getNodeType(event), - address, - parent: parentNode, - children: [], - }; - - this.nodes.set(address, node); - this.events.set(address, event); - - return node; - }); - - parentNode.children!.push(lazyNode); - this.lazyNodes.set(address, lazyNode); + parentNode.children!.push( + new Lazy(() => this.resolveNode(address, parentNode)) + ); } /** @@ -168,29 +136,35 @@ export class PublicationTree implements AsyncIterable { */ setBookmark(address: string) { this.bookmark = address; + this.#cursor.tryMoveTo(address); } [Symbol.asyncIterator](): AsyncIterator { + this.#cursor.tryMoveTo(this.bookmark); return this; } async next(): Promise> { - // If no bookmark is set, start at the first leaf. Retrieve that first leaf if necessary. - if (!this.bookmark) { - this.bookmark = this.leaves.at(0); - if (this.bookmark) { - const bookmarkEvent = await this.getEvent(this.bookmark); - return { done: false, value: bookmarkEvent! }; + while (this.#cursor.target?.type !== PublicationTreeNodeType.Leaf) { + if (await this.#cursor.tryMoveToFirstChild()) { + continue; } - const firstLeafEvent = await this.depthFirstRetrieve(); - this.bookmark = firstLeafEvent!.tagAddress(); - return { done: false, value: firstLeafEvent! }; - } + if (await this.#cursor.tryMoveToNextSibling()) { + continue; + } - // TODO: Invoke a funciton to retrieve the next sibling of the bookmark. + if (await this.#cursor.tryMoveToParent()) { + continue; + } - return { done: true, value: null }; + if (this.#cursor.target?.type === PublicationTreeNodeType.Root) { + return { done: true, value: null }; + } + } + + const event = await this.getEvent(this.#cursor.target!.address); + return { done: false, value: event! }; } // #region Private Methods @@ -252,42 +226,37 @@ export class PublicationTree implements AsyncIterable { return null; } - private async getNextSibling(address: string): Promise { - if (!this.leaves.includes(address)) { - throw new Error( - `PublicationTree: Address ${address} is not a leaf. Cannot retrieve next sibling.` - ); - } - - let currentNode = this.nodes.get(address); - if (!currentNode) { - return null; - } + private async resolveNode( + address: string, + parentNode: PublicationTreeNode + ): Promise { + const [kind, pubkey, dTag] = address.split(':'); + const event = await this.ndk.fetchEvent({ + kinds: [parseInt(kind)], + authors: [pubkey], + '#d': [dTag], + }); - let parent = currentNode.parent; - if (!parent) { + if (!event) { throw new Error( - `PublicationTree: Address ${address} has no parent. Cannot retrieve next sibling.` + `PublicationTree: Event with address ${address} not found.` ); } - // TODO: Handle the case where the current node is the last leaf. - - let nextSibling: PublicationTreeNode | null = null; - do { - const siblings: Lazy[] = parent!.children!; - const currentIndex = siblings.findIndex(async sibling => (await sibling.value()).address === currentNode!.address); - nextSibling = (await siblings.at(currentIndex + 1)?.value()) ?? null; + const childAddresses = event.tags.filter(tag => tag[0] === 'a').map(tag => tag[1]); + const node: PublicationTreeNode = { + type: this.getNodeType(event), + address, + parent: parentNode, + children: childAddresses.map( + address => new Lazy(() => this.resolveNode(address, node)) + ), + }; - // If the next sibling has children, it is not a leaf. - if ((nextSibling?.children?.length ?? 0) > 0) { - currentNode = (await nextSibling!.children!.at(0)!.value())!; - parent = currentNode.parent; - nextSibling = null; - } - } while (nextSibling == null); + this.nodes.set(address, node); + this.events.set(address, event); - return this.getEvent(nextSibling!.address); + return node; } private getNodeType(event: NDKEvent): PublicationTreeNodeType { @@ -314,49 +283,52 @@ export class PublicationTree implements AsyncIterable { // #region Iteration Cursor - Cursor = class { - private tree: PublicationTree; - private currentNode: PublicationTreeNode | null | undefined; + #cursor = new class { + target: PublicationTreeNode | null | undefined; - constructor(tree: PublicationTree, currentNode: PublicationTreeNode | null = null) { - this.tree = tree; - - if (!currentNode) { - this.currentNode = this.tree.bookmark - ? this.tree.nodes.get(this.tree.bookmark) - : null; - } + #tree: PublicationTree; + + constructor(tree: PublicationTree) { + this.#tree = tree; } - async moveToFirstChild(): Promise { - if (!this.currentNode) { - throw new Error("Cursor: Current node is null or undefined."); + async tryMoveTo(address?: string) { + if (!address) { + const startEvent = await this.#tree.depthFirstRetrieve(); + this.target = this.#tree.nodes.get(startEvent!.tagAddress()); + } else { + this.target = this.#tree.nodes.get(address); } - const hasChildren = (this.currentNode.children?.length ?? 0) > 0; - const isLeaf = this.tree.leaves.includes(this.currentNode.address); - - if (!hasChildren && isLeaf) { + if (!this.target) { return false; } - if (!hasChildren && !isLeaf) { - // TODO: Fetch any missing children, then return the first child. + return true; + } + + async tryMoveToFirstChild(): Promise { + if (!this.target) { + throw new Error("Cursor: Target node is null or undefined."); } - this.currentNode = (await this.currentNode.children?.at(0)?.value())!; + if (this.target.type === PublicationTreeNodeType.Leaf) { + return false; + } + + this.target = (await this.target.children?.at(0)?.value())!; return true; } - async moveToNextSibling(): Promise { - if (!this.currentNode) { - throw new Error("Cursor: Current node is null or undefined."); + async tryMoveToNextSibling(): Promise { + if (!this.target) { + throw new Error("Cursor: Target node is null or undefined."); } - const parent = this.currentNode.parent; + const parent = this.target.parent; const siblings = parent?.children; const currentIndex = siblings?.findIndex(async sibling => - (await sibling.value()).address === this.currentNode!.address + (await sibling.value()).address === this.target!.address ); const nextSibling = (await siblings?.at(currentIndex! + 1)?.value()) ?? null; @@ -364,24 +336,24 @@ export class PublicationTree implements AsyncIterable { return false; } - this.currentNode = nextSibling; + this.target = nextSibling; return true; } - moveToParent(): boolean { - if (!this.currentNode) { - throw new Error("Cursor: Current node is null or undefined."); + tryMoveToParent(): boolean { + if (!this.target) { + throw new Error("Cursor: Target node is null or undefined."); } - const parent = this.currentNode.parent; + const parent = this.target.parent; if (!parent) { return false; } - this.currentNode = parent; + this.target = parent; return true; } - }; + }(this); // #endregion } \ No newline at end of file From 43dac80578341c67c642d35808ecdd8cb45b17df Mon Sep 17 00:00:00 2001 From: buttercat1791 Date: Tue, 11 Mar 2025 09:23:25 -0500 Subject: [PATCH 10/40] Use ES2025 private member syntax --- src/lib/data_structures/lazy.ts | 7 +- src/lib/data_structures/publication_tree.ts | 82 ++++++++++----------- 2 files changed, 46 insertions(+), 43 deletions(-) diff --git a/src/lib/data_structures/lazy.ts b/src/lib/data_structures/lazy.ts index d7203b8..6be32fb 100644 --- a/src/lib/data_structures/lazy.ts +++ b/src/lib/data_structures/lazy.ts @@ -1,11 +1,14 @@ export class Lazy { #value?: T; + #resolver: () => Promise; - constructor(private readonly resolver: () => Promise) {} + constructor(resolver: () => Promise) { + this.#resolver = resolver; + } async value(): Promise { if (!this.#value) { - this.#value = await this.resolver(); + this.#value = await this.#resolver(); } return this.#value; diff --git a/src/lib/data_structures/publication_tree.ts b/src/lib/data_structures/publication_tree.ts index 9fd26c5..a6a1f56 100644 --- a/src/lib/data_structures/publication_tree.ts +++ b/src/lib/data_structures/publication_tree.ts @@ -19,48 +19,48 @@ export class PublicationTree implements AsyncIterable { /** * The root node of the tree. */ - private root: PublicationTreeNode; + #root: PublicationTreeNode; /** * A map of addresses in the tree to their corresponding nodes. */ - private nodes: Map; + #nodes: Map; /** * A map of addresses in the tree to their corresponding events. */ - private events: Map; + #events: Map; /** * An ordered list of the addresses of the leaves of the tree. */ - private leaves: string[] = []; + #leaves: string[] = []; /** * The address of the last-visited node. Used for iteration and progressive retrieval. */ - private bookmark?: string; + #bookmark?: string; /** * The NDK instance used to fetch events. */ - private ndk: NDK; + #ndk: NDK; constructor(rootEvent: NDKEvent, ndk: NDK) { const rootAddress = rootEvent.tagAddress(); - this.root = { + this.#root = { type: PublicationTreeNodeType.Root, address: rootAddress, children: [], }; - this.nodes = new Map(); - this.nodes.set(rootAddress, this.root); + this.#nodes = new Map(); + this.#nodes.set(rootAddress, this.#root); - this.events = new Map(); - this.events.set(rootAddress, rootEvent); + this.#events = new Map(); + this.#events.set(rootAddress, rootEvent); - this.ndk = ndk; + this.#ndk = ndk; } /** @@ -74,7 +74,7 @@ export class PublicationTree implements AsyncIterable { addEvent(event: NDKEvent, parentEvent: NDKEvent) { const address = event.tagAddress(); const parentAddress = parentEvent.tagAddress(); - const parentNode = this.nodes.get(parentAddress); + const parentNode = this.#nodes.get(parentAddress); if (!parentNode) { throw new Error( @@ -83,14 +83,14 @@ export class PublicationTree implements AsyncIterable { } const node = { - type: this.getNodeType(event), + type: this.#getNodeType(event), address, parent: parentNode, children: [], }; parentNode.children!.push(new Lazy(() => Promise.resolve(node))); - this.nodes.set(address, node); - this.events.set(address, event); + this.#nodes.set(address, node); + this.#events.set(address, event); } /** @@ -103,7 +103,7 @@ export class PublicationTree implements AsyncIterable { */ addEventByAddress(address: string, parentEvent: NDKEvent) { const parentAddress = parentEvent.tagAddress(); - const parentNode = this.nodes.get(parentAddress); + const parentNode = this.#nodes.get(parentAddress); if (!parentNode) { throw new Error( @@ -112,7 +112,7 @@ export class PublicationTree implements AsyncIterable { } parentNode.children!.push( - new Lazy(() => this.resolveNode(address, parentNode)) + new Lazy(() => this.#resolveNode(address, parentNode)) ); } @@ -122,9 +122,9 @@ export class PublicationTree implements AsyncIterable { * @returns The event, or null if the event is not found. */ async getEvent(address: string): Promise { - let event = this.events.get(address) ?? null; + let event = this.#events.get(address) ?? null; if (!event) { - event = await this.depthFirstRetrieve(address); + event = await this.#depthFirstRetrieve(address); } return event; @@ -135,12 +135,12 @@ export class PublicationTree implements AsyncIterable { * @param address The address of the event to bookmark. */ setBookmark(address: string) { - this.bookmark = address; + this.#bookmark = address; this.#cursor.tryMoveTo(address); } [Symbol.asyncIterator](): AsyncIterator { - this.#cursor.tryMoveTo(this.bookmark); + this.#cursor.tryMoveTo(this.#bookmark); return this; } @@ -176,29 +176,29 @@ export class PublicationTree implements AsyncIterable { * will return the first leaf in the tree. * @returns The event, or null if the event is not found. */ - private async depthFirstRetrieve(address?: string): Promise { - if (address && this.nodes.has(address)) { - return this.events.get(address)!; + async #depthFirstRetrieve(address?: string): Promise { + if (address && this.#nodes.has(address)) { + return this.#events.get(address)!; } - const stack: string[] = [this.root.address]; + const stack: string[] = [this.#root.address]; let currentEvent: NDKEvent | null | undefined; while (stack.length > 0) { const currentAddress = stack.pop(); // Stop immediately if the target of the search is found. if (address != null && currentAddress === address) { - return this.events.get(address)!; + return this.#events.get(address)!; } // Augment the tree with the children of the current event. - const currentChildAddresses = this.events + const currentChildAddresses = this.#events .get(currentAddress!)!.tags .filter(tag => tag[0] === 'a') .map(tag => tag[1]); for (const childAddress of currentChildAddresses) { - if (this.nodes.has(childAddress)) { + if (this.#nodes.has(childAddress)) { continue; } @@ -207,7 +207,7 @@ export class PublicationTree implements AsyncIterable { // If the current event has no children, it is a leaf. if (currentChildAddresses.length === 0) { - this.leaves.push(currentAddress!); + this.#leaves.push(currentAddress!); // Return the first leaf if no address was provided. if (address == null) { @@ -226,12 +226,12 @@ export class PublicationTree implements AsyncIterable { return null; } - private async resolveNode( + async #resolveNode( address: string, parentNode: PublicationTreeNode ): Promise { const [kind, pubkey, dTag] = address.split(':'); - const event = await this.ndk.fetchEvent({ + const event = await this.#ndk.fetchEvent({ kinds: [parseInt(kind)], authors: [pubkey], '#d': [dTag], @@ -245,23 +245,23 @@ export class PublicationTree implements AsyncIterable { const childAddresses = event.tags.filter(tag => tag[0] === 'a').map(tag => tag[1]); const node: PublicationTreeNode = { - type: this.getNodeType(event), + type: this.#getNodeType(event), address, parent: parentNode, children: childAddresses.map( - address => new Lazy(() => this.resolveNode(address, node)) + address => new Lazy(() => this.#resolveNode(address, node)) ), }; - this.nodes.set(address, node); - this.events.set(address, event); + this.#nodes.set(address, node); + this.#events.set(address, event); return node; } - private getNodeType(event: NDKEvent): PublicationTreeNodeType { + #getNodeType(event: NDKEvent): PublicationTreeNodeType { const address = event.tagAddress(); - const node = this.nodes.get(address); + const node = this.#nodes.get(address); if (!node) { throw new Error( `PublicationTree: Event with address ${address} not found in the tree.` @@ -294,10 +294,10 @@ export class PublicationTree implements AsyncIterable { async tryMoveTo(address?: string) { if (!address) { - const startEvent = await this.#tree.depthFirstRetrieve(); - this.target = this.#tree.nodes.get(startEvent!.tagAddress()); + const startEvent = await this.#tree.#depthFirstRetrieve(); + this.target = this.#tree.#nodes.get(startEvent!.tagAddress()); } else { - this.target = this.#tree.nodes.get(address); + this.target = this.#tree.#nodes.get(address); } if (!this.target) { From acea0fc0784a68eeb6070a8332a1c4c8b5b85ce2 Mon Sep 17 00:00:00 2001 From: buttercat1791 Date: Tue, 11 Mar 2025 09:25:09 -0500 Subject: [PATCH 11/40] Code organization --- src/lib/data_structures/publication_tree.ts | 156 ++++++++++---------- 1 file changed, 80 insertions(+), 76 deletions(-) diff --git a/src/lib/data_structures/publication_tree.ts b/src/lib/data_structures/publication_tree.ts index a6a1f56..0b16045 100644 --- a/src/lib/data_structures/publication_tree.ts +++ b/src/lib/data_structures/publication_tree.ts @@ -139,6 +139,84 @@ export class PublicationTree implements AsyncIterable { this.#cursor.tryMoveTo(address); } + // #region Iteration Cursor + + #cursor = new class { + target: PublicationTreeNode | null | undefined; + + #tree: PublicationTree; + + constructor(tree: PublicationTree) { + this.#tree = tree; + } + + async tryMoveTo(address?: string) { + if (!address) { + const startEvent = await this.#tree.#depthFirstRetrieve(); + this.target = this.#tree.#nodes.get(startEvent!.tagAddress()); + } else { + this.target = this.#tree.#nodes.get(address); + } + + if (!this.target) { + return false; + } + + return true; + } + + async tryMoveToFirstChild(): Promise { + if (!this.target) { + throw new Error("Cursor: Target node is null or undefined."); + } + + if (this.target.type === PublicationTreeNodeType.Leaf) { + return false; + } + + this.target = (await this.target.children?.at(0)?.value())!; + return true; + } + + async tryMoveToNextSibling(): Promise { + if (!this.target) { + throw new Error("Cursor: Target node is null or undefined."); + } + + const parent = this.target.parent; + const siblings = parent?.children; + const currentIndex = siblings?.findIndex(async sibling => + (await sibling.value()).address === this.target!.address + ); + + const nextSibling = (await siblings?.at(currentIndex! + 1)?.value()) ?? null; + if (!nextSibling) { + return false; + } + + this.target = nextSibling; + return true; + } + + tryMoveToParent(): boolean { + if (!this.target) { + throw new Error("Cursor: Target node is null or undefined."); + } + + const parent = this.target.parent; + if (!parent) { + return false; + } + + this.target = parent; + return true; + } + }(this); + + // #endregion + + // #region Async Iterator Implementation + [Symbol.asyncIterator](): AsyncIterator { this.#cursor.tryMoveTo(this.#bookmark); return this; @@ -167,6 +245,8 @@ export class PublicationTree implements AsyncIterable { return { done: false, value: event! }; } + // #endregion + // #region Private Methods /** @@ -280,80 +360,4 @@ export class PublicationTree implements AsyncIterable { } // #endregion - - // #region Iteration Cursor - - #cursor = new class { - target: PublicationTreeNode | null | undefined; - - #tree: PublicationTree; - - constructor(tree: PublicationTree) { - this.#tree = tree; - } - - async tryMoveTo(address?: string) { - if (!address) { - const startEvent = await this.#tree.#depthFirstRetrieve(); - this.target = this.#tree.#nodes.get(startEvent!.tagAddress()); - } else { - this.target = this.#tree.#nodes.get(address); - } - - if (!this.target) { - return false; - } - - return true; - } - - async tryMoveToFirstChild(): Promise { - if (!this.target) { - throw new Error("Cursor: Target node is null or undefined."); - } - - if (this.target.type === PublicationTreeNodeType.Leaf) { - return false; - } - - this.target = (await this.target.children?.at(0)?.value())!; - return true; - } - - async tryMoveToNextSibling(): Promise { - if (!this.target) { - throw new Error("Cursor: Target node is null or undefined."); - } - - const parent = this.target.parent; - const siblings = parent?.children; - const currentIndex = siblings?.findIndex(async sibling => - (await sibling.value()).address === this.target!.address - ); - - const nextSibling = (await siblings?.at(currentIndex! + 1)?.value()) ?? null; - if (!nextSibling) { - return false; - } - - this.target = nextSibling; - return true; - } - - tryMoveToParent(): boolean { - if (!this.target) { - throw new Error("Cursor: Target node is null or undefined."); - } - - const parent = this.target.parent; - if (!parent) { - return false; - } - - this.target = parent; - return true; - } - }(this); - - // #endregion } \ No newline at end of file From 177a5711554a5117d7f1dee2952972d1d1af4262 Mon Sep 17 00:00:00 2001 From: buttercat1791 Date: Tue, 11 Mar 2025 22:08:41 -0500 Subject: [PATCH 12/40] Move Modal component to components directory --- src/lib/{ => components}/Modal.svelte | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/lib/{ => components}/Modal.svelte (100%) diff --git a/src/lib/Modal.svelte b/src/lib/components/Modal.svelte similarity index 100% rename from src/lib/Modal.svelte rename to src/lib/components/Modal.svelte From 7e0165fdc5c26fd50eefad7f8fbf9223dfd18f4b Mon Sep 17 00:00:00 2001 From: buttercat1791 Date: Tue, 11 Mar 2025 22:08:50 -0500 Subject: [PATCH 13/40] Update Deno lockfile --- deno.lock | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/deno.lock b/deno.lock index e3b827f..f113237 100644 --- a/deno.lock +++ b/deno.lock @@ -2862,6 +2862,22 @@ } }, "workspace": { + "dependencies": [ + "npm:@nostr-dev-kit/ndk-cache-dexie@2.5", + "npm:@nostr-dev-kit/ndk@2.11", + "npm:@popperjs/core@2.11", + "npm:@tailwindcss/forms@0.5", + "npm:@tailwindcss/typography@0.5", + "npm:asciidoctor@3.0", + "npm:d3@7.9", + "npm:flowbite-svelte-icons@2.0", + "npm:flowbite-svelte@0.44", + "npm:flowbite@2.2", + "npm:he@1.2", + "npm:nostr-tools@2.10", + "npm:svelte@5.0", + "npm:tailwind-merge@2.5" + ], "packageJson": { "dependencies": [ "npm:@nostr-dev-kit/ndk-cache-dexie@2.5", From 7ef3cb8ab895339e0b35fdffa15b47f7d5a942b1 Mon Sep 17 00:00:00 2001 From: buttercat1791 Date: Fri, 14 Mar 2025 08:02:42 -0500 Subject: [PATCH 14/40] Fix an iterator error that would cause an infinite loop --- src/lib/data_structures/publication_tree.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/lib/data_structures/publication_tree.ts b/src/lib/data_structures/publication_tree.ts index 0b16045..4306b1e 100644 --- a/src/lib/data_structures/publication_tree.ts +++ b/src/lib/data_structures/publication_tree.ts @@ -223,7 +223,7 @@ export class PublicationTree implements AsyncIterable { } async next(): Promise> { - while (this.#cursor.target?.type !== PublicationTreeNodeType.Leaf) { + do { if (await this.#cursor.tryMoveToFirstChild()) { continue; } @@ -232,14 +232,14 @@ export class PublicationTree implements AsyncIterable { continue; } - if (await this.#cursor.tryMoveToParent()) { + if (this.#cursor.tryMoveToParent()) { continue; } if (this.#cursor.target?.type === PublicationTreeNodeType.Root) { return { done: true, value: null }; } - } + } while (this.#cursor.target?.type !== PublicationTreeNodeType.Leaf); const event = await this.getEvent(this.#cursor.target!.address); return { done: false, value: event! }; From 837b371551aa7678b9ecfa6911e6d03439b4efea Mon Sep 17 00:00:00 2001 From: buttercat1791 Date: Fri, 14 Mar 2025 08:03:12 -0500 Subject: [PATCH 15/40] Add a method to return the hierarchy in which an event lives --- src/lib/data_structures/publication_tree.ts | 22 +++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/lib/data_structures/publication_tree.ts b/src/lib/data_structures/publication_tree.ts index 4306b1e..bac54a0 100644 --- a/src/lib/data_structures/publication_tree.ts +++ b/src/lib/data_structures/publication_tree.ts @@ -130,6 +130,28 @@ export class PublicationTree implements AsyncIterable { return event; } + /** + * Retrieves the events in the hierarchy of the event with the given address. + * @param address The address of the event for which to retrieve the hierarchy. + * @returns Returns an array of events in the addressed event's hierarchy, beginning with the + * root and ending with the addressed event. + */ + async getHierarchy(address: string): Promise { + let node = this.#nodes.get(address); + if (!node) { + return null; + } + + const hierarchy: NDKEvent[] = [this.#events.get(address)!]; + + while (node.parent) { + hierarchy.push(this.#events.get(node.parent.address)!); + node = node.parent; + } + + return hierarchy.reverse(); + } + /** * Sets a start point for iteration over the leaves of the tree. * @param address The address of the event to bookmark. From e8ed3ac08f37dd047b62d1a400dadac3a73dea0d Mon Sep 17 00:00:00 2001 From: buttercat1791 Date: Fri, 14 Mar 2025 08:09:02 -0500 Subject: [PATCH 16/40] Throw error rather than return `null` --- src/lib/data_structures/publication_tree.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib/data_structures/publication_tree.ts b/src/lib/data_structures/publication_tree.ts index bac54a0..1cdbe16 100644 --- a/src/lib/data_structures/publication_tree.ts +++ b/src/lib/data_structures/publication_tree.ts @@ -136,10 +136,10 @@ export class PublicationTree implements AsyncIterable { * @returns Returns an array of events in the addressed event's hierarchy, beginning with the * root and ending with the addressed event. */ - async getHierarchy(address: string): Promise { + async getHierarchy(address: string): Promise { let node = this.#nodes.get(address); if (!node) { - return null; + throw new Error(`PublicationTree: Node with address ${address} not found.`); } const hierarchy: NDKEvent[] = [this.#events.get(address)!]; From 9011eb643f8373d409f8e3af7a40ec89d9f41a29 Mon Sep 17 00:00:00 2001 From: buttercat1791 Date: Fri, 14 Mar 2025 08:11:05 -0500 Subject: [PATCH 17/40] Make `getHierarchy` synchronous --- src/lib/data_structures/publication_tree.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/data_structures/publication_tree.ts b/src/lib/data_structures/publication_tree.ts index 1cdbe16..ab011a2 100644 --- a/src/lib/data_structures/publication_tree.ts +++ b/src/lib/data_structures/publication_tree.ts @@ -136,7 +136,7 @@ export class PublicationTree implements AsyncIterable { * @returns Returns an array of events in the addressed event's hierarchy, beginning with the * root and ending with the addressed event. */ - async getHierarchy(address: string): Promise { + getHierarchy(address: string): NDKEvent[] { let node = this.#nodes.get(address); if (!node) { throw new Error(`PublicationTree: Node with address ${address} not found.`); From c403a245b81f8c9d82bc8ccae08f82d87172dc14 Mon Sep 17 00:00:00 2001 From: buttercat1791 Date: Fri, 14 Mar 2025 08:26:47 -0500 Subject: [PATCH 18/40] Move some publication rendering snippets to a separate file --- src/lib/components/Preview.svelte | 45 ++------------------- src/lib/snippets/PublicationSnippets.svelte | 45 +++++++++++++++++++++ 2 files changed, 48 insertions(+), 42 deletions(-) create mode 100644 src/lib/snippets/PublicationSnippets.svelte diff --git a/src/lib/components/Preview.svelte b/src/lib/components/Preview.svelte index f7dfe03..c980b48 100644 --- a/src/lib/components/Preview.svelte +++ b/src/lib/components/Preview.svelte @@ -1,8 +1,9 @@ -{#snippet sectionHeading(title: string, depth: number)} - {#if depth === 0} -

- {title} -

- {:else if depth === 1} -

- {title} -

- {:else if depth === 2} -

- {title} -

- {:else if depth === 3} -

- {title} -

- {:else if depth === 4} -
- {title} -
- {:else} -
- {title} -
- {/if} -{/snippet} - -{#snippet contentParagraph(content: string, publicationType: string)} - {#if publicationType === 'novel'} -

- {@html content} -

- {:else} -

- {@html content} -

- {/if} -{/snippet} -
{:else} - {@render contentParagraph(currentContent, publicationType)} + {@render contentParagraph(currentContent, publicationType, isSectionStart)} {/if} {/key} {:else} diff --git a/src/lib/snippets/PublicationSnippets.svelte b/src/lib/snippets/PublicationSnippets.svelte new file mode 100644 index 0000000..26645fa --- /dev/null +++ b/src/lib/snippets/PublicationSnippets.svelte @@ -0,0 +1,45 @@ + + +{#snippet sectionHeading(title: string, depth: number)} + {#if depth === 0} +

+ {title} +

+ {:else if depth === 1} +

+ {title} +

+ {:else if depth === 2} +

+ {title} +

+ {:else if depth === 3} +

+ {title} +

+ {:else if depth === 4} +
+ {title} +
+ {:else} +
+ {title} +
+ {/if} +{/snippet} + +{#snippet contentParagraph(content: string, publicationType: string, isSectionStart: boolean)} + {#if publicationType === 'novel'} +

+ {@html content} +

+ {:else} +

+ {@html content} +

+ {/if} +{/snippet} From 3a5f2a75abc47cc78ab7d19860387e8696cfc074 Mon Sep 17 00:00:00 2001 From: Silberengel Date: Wed, 19 Mar 2025 14:00:32 -0700 Subject: [PATCH 19/40] added discrete headers to .css --- src/app.css | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/src/app.css b/src/app.css index 358d034..187a433 100644 --- a/src/app.css +++ b/src/app.css @@ -74,7 +74,7 @@ @apply hover:bg-primary-100 dark:hover:bg-primary-800; } - /* Heading */ + /* Section headers */ h1.h-leather, h2.h-leather, h3.h-leather, @@ -108,6 +108,30 @@ @apply text-base font-semibold; } + /* Discrete headers */ + h3.discrete, + h4.discrete, + h5.discrete, + h6.discrete { + @apply text-gray-800 dark:text-gray-300; + } + + h3.discrete { + @apply text-2xl font-bold; + } + + h4.discrete { + @apply text-xl font-bold; + } + + h5.discrete { + @apply text-lg font-semibold; + } + + h6.discrete { + @apply text-base font-semibold; + } + /* Modal */ div.modal-leather > div { @apply bg-primary-0 dark:bg-primary-950 border-b-[1px] border-primary-100 dark:border-primary-600; From 6eed77b967b55264ece6ff65e81bcf4a0c05b061 Mon Sep 17 00:00:00 2001 From: Silberengel Date: Wed, 19 Mar 2025 14:03:21 -0700 Subject: [PATCH 20/40] Addresses issue#201 --- src/app.css | 24 ------------------------ src/styles/publications.css | 24 ++++++++++++++++++++++++ 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/src/app.css b/src/app.css index 187a433..011ebd9 100644 --- a/src/app.css +++ b/src/app.css @@ -108,30 +108,6 @@ @apply text-base font-semibold; } - /* Discrete headers */ - h3.discrete, - h4.discrete, - h5.discrete, - h6.discrete { - @apply text-gray-800 dark:text-gray-300; - } - - h3.discrete { - @apply text-2xl font-bold; - } - - h4.discrete { - @apply text-xl font-bold; - } - - h5.discrete { - @apply text-lg font-semibold; - } - - h6.discrete { - @apply text-base font-semibold; - } - /* Modal */ div.modal-leather > div { @apply bg-primary-0 dark:bg-primary-950 border-b-[1px] border-primary-100 dark:border-primary-600; diff --git a/src/styles/publications.css b/src/styles/publications.css index b2b2847..fe30740 100644 --- a/src/styles/publications.css +++ b/src/styles/publications.css @@ -229,4 +229,28 @@ .audioblock .content audio { @apply w-full; } + + /* Discrete headers */ + h3.discrete, + h4.discrete, + h5.discrete, + h6.discrete { + @apply text-gray-800 dark:text-gray-300; + } + + h3.discrete { + @apply text-2xl font-bold; + } + + h4.discrete { + @apply text-xl font-bold; + } + + h5.discrete { + @apply text-lg font-semibold; + } + + h6.discrete { + @apply text-base font-semibold; + } } \ No newline at end of file From 3a313b1f3988c9ea65839577fccb10134c0826c1 Mon Sep 17 00:00:00 2001 From: buttercat1791 Date: Thu, 20 Mar 2025 09:33:07 -0500 Subject: [PATCH 21/40] Fix some bugs in the publication tree traversal --- src/lib/data_structures/publication_tree.ts | 120 +++++++++++--------- 1 file changed, 69 insertions(+), 51 deletions(-) diff --git a/src/lib/data_structures/publication_tree.ts b/src/lib/data_structures/publication_tree.ts index ab011a2..f67aa03 100644 --- a/src/lib/data_structures/publication_tree.ts +++ b/src/lib/data_structures/publication_tree.ts @@ -24,7 +24,7 @@ export class PublicationTree implements AsyncIterable { /** * A map of addresses in the tree to their corresponding nodes. */ - #nodes: Map; + #nodes: Map>; /** * A map of addresses in the tree to their corresponding events. @@ -54,8 +54,8 @@ export class PublicationTree implements AsyncIterable { children: [], }; - this.#nodes = new Map(); - this.#nodes.set(rootAddress, this.#root); + this.#nodes = new Map>(); + this.#nodes.set(rootAddress, new Lazy(() => Promise.resolve(this.#root))); this.#events = new Map(); this.#events.set(rootAddress, rootEvent); @@ -71,10 +71,10 @@ export class PublicationTree implements AsyncIterable { * @description The parent event must already be in the tree. Use * {@link PublicationTree.getEvent} to retrieve an event already in the tree. */ - addEvent(event: NDKEvent, parentEvent: NDKEvent) { + async addEvent(event: NDKEvent, parentEvent: NDKEvent) { const address = event.tagAddress(); const parentAddress = parentEvent.tagAddress(); - const parentNode = this.#nodes.get(parentAddress); + const parentNode = await this.#nodes.get(parentAddress)?.value(); if (!parentNode) { throw new Error( @@ -82,14 +82,14 @@ export class PublicationTree implements AsyncIterable { ); } - const node = { - type: this.#getNodeType(event), + const node: PublicationTreeNode = { + type: await this.#getNodeType(event), address, parent: parentNode, children: [], }; parentNode.children!.push(new Lazy(() => Promise.resolve(node))); - this.#nodes.set(address, node); + this.#nodes.set(address, new Lazy(() => Promise.resolve(node))); this.#events.set(address, event); } @@ -101,9 +101,9 @@ export class PublicationTree implements AsyncIterable { * @description The parent event must already be in the tree. Use * {@link PublicationTree.getEvent} to retrieve an event already in the tree. */ - addEventByAddress(address: string, parentEvent: NDKEvent) { + async addEventByAddress(address: string, parentEvent: NDKEvent) { const parentAddress = parentEvent.tagAddress(); - const parentNode = this.#nodes.get(parentAddress); + const parentNode = await this.#nodes.get(parentAddress)?.value(); if (!parentNode) { throw new Error( @@ -111,9 +111,7 @@ export class PublicationTree implements AsyncIterable { ); } - parentNode.children!.push( - new Lazy(() => this.#resolveNode(address, parentNode)) - ); + await this.#addNode(address, parentNode); } /** @@ -136,8 +134,8 @@ export class PublicationTree implements AsyncIterable { * @returns Returns an array of events in the addressed event's hierarchy, beginning with the * root and ending with the addressed event. */ - getHierarchy(address: string): NDKEvent[] { - let node = this.#nodes.get(address); + async getHierarchy(address: string): Promise { + let node = await this.#nodes.get(address)?.value(); if (!node) { throw new Error(`PublicationTree: Node with address ${address} not found.`); } @@ -175,9 +173,9 @@ export class PublicationTree implements AsyncIterable { async tryMoveTo(address?: string) { if (!address) { const startEvent = await this.#tree.#depthFirstRetrieve(); - this.target = this.#tree.#nodes.get(startEvent!.tagAddress()); + this.target = await this.#tree.#nodes.get(startEvent!.tagAddress())?.value(); } else { - this.target = this.#tree.#nodes.get(address); + this.target = await this.#tree.#nodes.get(address)?.value(); } if (!this.target) { @@ -240,11 +238,14 @@ export class PublicationTree implements AsyncIterable { // #region Async Iterator Implementation [Symbol.asyncIterator](): AsyncIterator { - this.#cursor.tryMoveTo(this.#bookmark); return this; } async next(): Promise> { + if (!this.#cursor.target) { + await this.#cursor.tryMoveTo(this.#bookmark); + } + do { if (await this.#cursor.tryMoveToFirstChild()) { continue; @@ -284,33 +285,31 @@ export class PublicationTree implements AsyncIterable { } const stack: string[] = [this.#root.address]; - let currentEvent: NDKEvent | null | undefined; + let currentNode: PublicationTreeNode | null | undefined = this.#root; + let currentEvent: NDKEvent | null | undefined = this.#events.get(this.#root.address)!; while (stack.length > 0) { const currentAddress = stack.pop(); + currentNode = await this.#nodes.get(currentAddress!)?.value(); + if (!currentNode) { + throw new Error(`PublicationTree: Node with address ${currentAddress} not found.`); + } + + currentEvent = this.#events.get(currentAddress!); + if (!currentEvent) { + throw new Error(`PublicationTree: Event with address ${currentAddress} not found.`); + } // Stop immediately if the target of the search is found. if (address != null && currentAddress === address) { - return this.#events.get(address)!; + return currentEvent; } - // Augment the tree with the children of the current event. - const currentChildAddresses = this.#events - .get(currentAddress!)!.tags + const currentChildAddresses = currentEvent.tags .filter(tag => tag[0] === 'a') .map(tag => tag[1]); - for (const childAddress of currentChildAddresses) { - if (this.#nodes.has(childAddress)) { - continue; - } - - this.addEventByAddress(childAddress, currentEvent!); - } - // If the current event has no children, it is a leaf. if (currentChildAddresses.length === 0) { - this.#leaves.push(currentAddress!); - // Return the first leaf if no address was provided. if (address == null) { return currentEvent!; @@ -319,15 +318,40 @@ export class PublicationTree implements AsyncIterable { continue; } + // Augment the tree with the children of the current event. + for (const childAddress of currentChildAddresses) { + if (this.#nodes.has(childAddress)) { + continue; + } + + await this.#addNode(childAddress, currentNode!); + } + // Push the popped address's children onto the stack for the next iteration. while (currentChildAddresses.length > 0) { - stack.push(currentChildAddresses.pop()!); + const nextAddress = currentChildAddresses.pop()!; + stack.push(nextAddress); } } return null; } + async #addNode(address: string, parentNode: PublicationTreeNode): Promise { + const lazyNode = new Lazy(() => this.#resolveNode(address, parentNode)); + parentNode.children!.push(lazyNode); + this.#nodes.set(address, lazyNode); + } + + /** + * Resolves a node address into an event, and creates new nodes for its children. + * + * This method is intended for use as a {@link Lazy} resolver. + * + * @param address The address of the node to resolve. + * @param parentNode The parent node of the node to resolve. + * @returns The resolved node. + */ async #resolveNode( address: string, parentNode: PublicationTreeNode @@ -345,36 +369,30 @@ export class PublicationTree implements AsyncIterable { ); } + this.#events.set(address, event); + const childAddresses = event.tags.filter(tag => tag[0] === 'a').map(tag => tag[1]); + const node: PublicationTreeNode = { - type: this.#getNodeType(event), + type: await this.#getNodeType(event), address, parent: parentNode, - children: childAddresses.map( - address => new Lazy(() => this.#resolveNode(address, node)) - ), + children: [], }; - this.#nodes.set(address, node); - this.#events.set(address, event); + for (const address of childAddresses) { + this.addEventByAddress(address, event); + } return node; } - #getNodeType(event: NDKEvent): PublicationTreeNodeType { - const address = event.tagAddress(); - const node = this.#nodes.get(address); - if (!node) { - throw new Error( - `PublicationTree: Event with address ${address} not found in the tree.` - ); - } - - if (!node.parent) { + async #getNodeType(event: NDKEvent): Promise { + if (event.tagAddress() === this.#root.address) { return PublicationTreeNodeType.Root; } - if (event.tags.some(tag => tag[0] === 'a')) { + if (event.kind === 30040 && event.tags.some(tag => tag[0] === 'a')) { return PublicationTreeNodeType.Branch; } From 947470b3a1a095fbfc6631e43420ccde32bde09a Mon Sep 17 00:00:00 2001 From: buttercat1791 Date: Fri, 21 Mar 2025 08:09:23 -0500 Subject: [PATCH 22/40] Small edits to publication tree --- src/lib/data_structures/publication_tree.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/lib/data_structures/publication_tree.ts b/src/lib/data_structures/publication_tree.ts index f67aa03..9c5cb64 100644 --- a/src/lib/data_structures/publication_tree.ts +++ b/src/lib/data_structures/publication_tree.ts @@ -1,6 +1,6 @@ import type NDK from "@nostr-dev-kit/ndk"; -import type { NDKEvent, NDKFilter } from "@nostr-dev-kit/ndk"; -import { Lazy } from "./lazy"; +import type { NDKEvent } from "@nostr-dev-kit/ndk"; +import { Lazy } from "./lazy.ts"; enum PublicationTreeNodeType { Root, @@ -337,7 +337,7 @@ export class PublicationTree implements AsyncIterable { return null; } - async #addNode(address: string, parentNode: PublicationTreeNode): Promise { + #addNode(address: string, parentNode: PublicationTreeNode) { const lazyNode = new Lazy(() => this.#resolveNode(address, parentNode)); parentNode.children!.push(lazyNode); this.#nodes.set(address, lazyNode); From 1272d312d9e827b999e28f05c8c549bf3d4dc8ee Mon Sep 17 00:00:00 2001 From: buttercat1791 Date: Fri, 28 Mar 2025 23:30:11 -0500 Subject: [PATCH 23/40] Support async find for lazy publication tree nodes --- src/lib/data_structures/publication_tree.ts | 14 +++++++-- src/lib/utils.ts | 35 +++++++++++++++++++++ 2 files changed, 46 insertions(+), 3 deletions(-) diff --git a/src/lib/data_structures/publication_tree.ts b/src/lib/data_structures/publication_tree.ts index 9c5cb64..e273711 100644 --- a/src/lib/data_structures/publication_tree.ts +++ b/src/lib/data_structures/publication_tree.ts @@ -205,11 +205,19 @@ export class PublicationTree implements AsyncIterable { const parent = this.target.parent; const siblings = parent?.children; - const currentIndex = siblings?.findIndex(async sibling => - (await sibling.value()).address === this.target!.address + if (!siblings) { + return false; + } + + const currentIndex = await siblings.findIndexAsync( + async (sibling: Lazy) => (await sibling.value()).address === this.target!.address ); - const nextSibling = (await siblings?.at(currentIndex! + 1)?.value()) ?? null; + if (currentIndex === -1) { + return false; + } + + const nextSibling = (await siblings.at(currentIndex + 1)?.value()) ?? null; if (!nextSibling) { return false; } diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 3a54d64..021c979 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -109,3 +109,38 @@ export function filterValidIndexEvents(events: Set): Set { console.debug(`Filtered index events: ${events.size} events remaining.`); return events; } + +/** + * Async version of Array.findIndex() that runs sequentially. + * Returns the index of the first element that satisfies the provided testing function. + * @param array The array to search + * @param predicate The async testing function + * @returns A promise that resolves to the index of the first matching element, or -1 if none found + */ +export async function findIndexAsync( + array: T[], + predicate: (element: T, index: number, array: T[]) => Promise +): Promise { + for (let i = 0; i < array.length; i++) { + if (await predicate(array[i], i, array)) { + return i; + } + } + return -1; +} + +// Extend Array prototype with findIndexAsync +declare global { + interface Array { + findIndexAsync( + predicate: (element: T, index: number, array: T[]) => Promise + ): Promise; + } +} + +Array.prototype.findIndexAsync = function( + this: T[], + predicate: (element: T, index: number, array: T[]) => Promise +): Promise { + return findIndexAsync(this, predicate); +}; From ef0c033d9bde1bcab21db537213883b92f0cb889 Mon Sep 17 00:00:00 2001 From: buttercat1791 Date: Fri, 28 Mar 2025 23:31:41 -0500 Subject: [PATCH 24/40] Fix import for findindexasync --- src/lib/data_structures/publication_tree.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/lib/data_structures/publication_tree.ts b/src/lib/data_structures/publication_tree.ts index e273711..11fbf31 100644 --- a/src/lib/data_structures/publication_tree.ts +++ b/src/lib/data_structures/publication_tree.ts @@ -1,6 +1,7 @@ import type NDK from "@nostr-dev-kit/ndk"; import type { NDKEvent } from "@nostr-dev-kit/ndk"; import { Lazy } from "./lazy.ts"; +import { findIndexAsync as _findIndexAsync } from '../utils.ts'; enum PublicationTreeNodeType { Root, From f1bdb20e21d41ba81b4f10b75b8f42c78e636cfb Mon Sep 17 00:00:00 2001 From: buttercat1791 Date: Fri, 28 Mar 2025 23:32:18 -0500 Subject: [PATCH 25/40] Add `getChildAddresses` function to publication tree --- src/lib/data_structures/publication_tree.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/lib/data_structures/publication_tree.ts b/src/lib/data_structures/publication_tree.ts index 11fbf31..e7e0e41 100644 --- a/src/lib/data_structures/publication_tree.ts +++ b/src/lib/data_structures/publication_tree.ts @@ -129,6 +129,23 @@ export class PublicationTree implements AsyncIterable { return event; } + /** + * Retrieves the addresses of the loaded children, if any, of the node with the given address. + * @param address The address of the parent node. + * @returns An array of addresses of any loaded child nodes. + */ + async getChildAddresses(address: string): Promise { + const node = await this.#nodes.get(address)?.value(); + if (!node) { + throw new Error(`PublicationTree: Node with address ${address} not found.`); + } + + return Promise.all( + node.children?.map(async child => + (await child.value()).address + ) ?? [] + ); + } /** * Retrieves the events in the hierarchy of the event with the given address. * @param address The address of the event for which to retrieve the hierarchy. From 4292cf3b134ec4590ce27f6eb680418b0bb41bc9 Mon Sep 17 00:00:00 2001 From: buttercat1791 Date: Thu, 3 Apr 2025 09:04:59 -0500 Subject: [PATCH 26/40] Open #11 --- src/routes/publication/+page.ts | 12 +++++++----- tsconfig.json | 3 ++- vite.config.ts | 6 ++++++ 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/src/routes/publication/+page.ts b/src/routes/publication/+page.ts index 3d58b39..7fddcdc 100644 --- a/src/routes/publication/+page.ts +++ b/src/routes/publication/+page.ts @@ -1,11 +1,11 @@ import { error } from '@sveltejs/kit'; -import { NDKRelay, NDKRelaySet, type NDKEvent } from '@nostr-dev-kit/ndk'; +import type { NDKEvent } from '@nostr-dev-kit/ndk'; import type { PageLoad } from './$types'; -import { get } from 'svelte/store'; -import { getActiveRelays, inboxRelays, ndkInstance } from '$lib/ndk'; -import { standardRelays } from '$lib/consts'; +import { getActiveRelays } from '$lib/ndk.ts'; +import { setContext } from 'svelte'; +import { PublicationTree } from '$lib/data_structures/publication_tree.ts'; -export const load: PageLoad = async ({ url, parent }) => { +export const load: PageLoad = async ({ url, parent }: { url: URL; parent: () => Promise }) => { const id = url.searchParams.get('id'); const dTag = url.searchParams.get('d'); @@ -41,6 +41,8 @@ export const load: PageLoad = async ({ url, parent }) => { const publicationType = indexEvent?.getMatchingTags('type')[0]?.[1]; const fetchPromise = parser.fetch(indexEvent); + setContext('publicationTree', new PublicationTree(indexEvent, ndk)); + return { waitable: fetchPromise, publicationType, diff --git a/tsconfig.json b/tsconfig.json index 794b95b..ec41776 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -8,7 +8,8 @@ "resolveJsonModule": true, "skipLibCheck": true, "sourceMap": true, - "strict": true + "strict": true, + "allowImportingTsExtensions": true } // Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias // diff --git a/vite.config.ts b/vite.config.ts index b9ba52c..d723dc1 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -3,6 +3,12 @@ import { defineConfig } from "vite"; export default defineConfig({ plugins: [sveltekit()], + resolve: { + alias: { + $lib: './src/lib', + $components: './src/components' + } + }, test: { include: ['./tests/unit/**/*.unit-test.js'] } From a04040861ea344bb7968167cf91b46e442e2ff02 Mon Sep 17 00:00:00 2001 From: Silberengel Date: Sat, 5 Apr 2025 12:01:09 +0200 Subject: [PATCH 27/40] Changed class to match the others. --- src/routes/publication/+error.svelte | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/routes/publication/+error.svelte b/src/routes/publication/+error.svelte index a31bec9..92e716f 100644 --- a/src/routes/publication/+error.svelte +++ b/src/routes/publication/+error.svelte @@ -20,10 +20,10 @@ {page.error?.message}

- -
From ed214b9a126487e6bf6a64dea6cfe88147617704 Mon Sep 17 00:00:00 2001 From: Silberengel Date: Sat, 5 Apr 2025 12:01:09 +0200 Subject: [PATCH 28/40] Revert "Changed class to match the others." This reverts commit a04040861ea344bb7968167cf91b46e442e2ff02 Accidentally checked into the wrong branch, sorry. --- src/routes/publication/+error.svelte | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/routes/publication/+error.svelte b/src/routes/publication/+error.svelte index 92e716f..a31bec9 100644 --- a/src/routes/publication/+error.svelte +++ b/src/routes/publication/+error.svelte @@ -20,10 +20,10 @@ {page.error?.message}

- -
From a5c80b6567663874b06848c89484abf008315574 Mon Sep 17 00:00:00 2001 From: Silberengel Date: Sat, 5 Apr 2025 15:55:00 +0200 Subject: [PATCH 29/40] catch all 404 errors, that haven't been handled --- src/routes/[...catchall]/+page.svelte | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 src/routes/[...catchall]/+page.svelte diff --git a/src/routes/[...catchall]/+page.svelte b/src/routes/[...catchall]/+page.svelte new file mode 100644 index 0000000..b1910e2 --- /dev/null +++ b/src/routes/[...catchall]/+page.svelte @@ -0,0 +1,14 @@ + + +
+

404 - Page Not Found

+

The page you are looking for does not exist or has been moved.

+
+ + +
+
From a263a6fec6ca0643d61de9ffea43a6a7c3537898 Mon Sep 17 00:00:00 2001 From: Silberengel Date: Sat, 5 Apr 2025 16:01:38 +0200 Subject: [PATCH 30/40] fix formatting on 404 page --- src/routes/[...catchall]/+page.svelte | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/routes/[...catchall]/+page.svelte b/src/routes/[...catchall]/+page.svelte index b1910e2..dd838c9 100644 --- a/src/routes/[...catchall]/+page.svelte +++ b/src/routes/[...catchall]/+page.svelte @@ -4,11 +4,11 @@ import { Button, P } from 'flowbite-svelte'; -
-

404 - Page Not Found

-

The page you are looking for does not exist or has been moved.

+
+

404 - Page Not Found

+

The page you are looking for does not exist or has been moved.

- - + +
From d62c424a102082b66790dbf2020739fc502f8178 Mon Sep 17 00:00:00 2001 From: Silberengel Date: Sat, 5 Apr 2025 16:14:29 +0200 Subject: [PATCH 31/40] Handle discrete headings --- src/lib/components/Preview.svelte | 74 +++++++++++++++++++++---------- src/lib/parser.ts | 15 +++++++ 2 files changed, 66 insertions(+), 23 deletions(-) diff --git a/src/lib/components/Preview.svelte b/src/lib/components/Preview.svelte index f7dfe03..0a1ffdb 100644 --- a/src/lib/components/Preview.svelte +++ b/src/lib/components/Preview.svelte @@ -150,30 +150,58 @@ {#snippet sectionHeading(title: string, depth: number)} - {#if depth === 0} -

- {title} -

- {:else if depth === 1} -

- {title} -

- {:else if depth === 2} -

- {title} -

- {:else if depth === 3} -

- {title} -

- {:else if depth === 4} -
- {title} -
+ {#if $pharosInstance.isFloatingTitle(rootId)} + {#if depth === 0} +

+ {title} +

+ {:else if depth === 1} +

+ {title} +

+ {:else if depth === 2} +

+ {title} +

+ {:else if depth === 3} +

+ {title} +

+ {:else if depth === 4} +
+ {title} +
+ {:else} +
+ {title} +
+ {/if} {:else} -
- {title} -
+ {#if depth === 0} +

+ {title} +

+ {:else if depth === 1} +

+ {title} +

+ {:else if depth === 2} +

+ {title} +

+ {:else if depth === 3} +

+ {title} +

+ {:else if depth === 4} +
+ {title} +
+ {:else} +
+ {title} +
+ {/if} {/if} {/snippet} diff --git a/src/lib/parser.ts b/src/lib/parser.ts index b9f9545..273c8a3 100644 --- a/src/lib/parser.ts +++ b/src/lib/parser.ts @@ -270,6 +270,21 @@ export default class Pharos { return block.convert(); } + /** + * Checks if the node with the given ID is a floating title (discrete header). + * @param id The ID of the node to check. + * @returns True if the node is a floating title, false otherwise. + */ + isFloatingTitle(id: string): boolean { + const normalizedId = this.normalizeId(id); + if (!normalizedId || !this.nodes.has(normalizedId)) { + return false; + } + + const context = this.eventToContextMap.get(normalizedId); + return context === 'floating_title'; + } + /** * Updates the `content` field of a Nostr event in-place. * @param dTag The d tag of the event to update. From aa091014875904c7bcfc615a4ae468574a2c99a7 Mon Sep 17 00:00:00 2001 From: Silberengel Date: Sun, 6 Apr 2025 08:43:58 +0200 Subject: [PATCH 32/40] made header handling more concise and dynamic section/disrete --- src/lib/components/Preview.svelte | 59 ++++--------------------------- 1 file changed, 6 insertions(+), 53 deletions(-) diff --git a/src/lib/components/Preview.svelte b/src/lib/components/Preview.svelte index 0a1ffdb..6130feb 100644 --- a/src/lib/components/Preview.svelte +++ b/src/lib/components/Preview.svelte @@ -150,59 +150,12 @@ {#snippet sectionHeading(title: string, depth: number)} - {#if $pharosInstance.isFloatingTitle(rootId)} - {#if depth === 0} -

- {title} -

- {:else if depth === 1} -

- {title} -

- {:else if depth === 2} -

- {title} -

- {:else if depth === 3} -

- {title} -

- {:else if depth === 4} -
- {title} -
- {:else} -
- {title} -
- {/if} - {:else} - {#if depth === 0} -

- {title} -

- {:else if depth === 1} -

- {title} -

- {:else if depth === 2} -

- {title} -

- {:else if depth === 3} -

- {title} -

- {:else if depth === 4} -
- {title} -
- {:else} -
- {title} -
- {/if} - {/if} + {@const headingLevel = Math.min(depth + 1, 6)} + {@const className = $pharosInstance.isFloatingTitle(rootId) ? 'discrete' : 'h-leather'} + + + {title} + {/snippet} {#snippet contentParagraph(content: string, publicationType: string)} From 613c3a4185fc5c18e0b475e0140e3d473d2236d0 Mon Sep 17 00:00:00 2001 From: buttercat1791 Date: Mon, 7 Apr 2025 09:39:31 -0500 Subject: [PATCH 33/40] Scope Pharos extensions to Asciidoctor extension registry We were previously registering extensions globally, which impacted every usage of Asciidoctor.js, even those where the extensions were not needed. --- src/lib/parser.ts | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/src/lib/parser.ts b/src/lib/parser.ts index b9f9545..0454a10 100644 --- a/src/lib/parser.ts +++ b/src/lib/parser.ts @@ -12,7 +12,7 @@ import type { } from 'asciidoctor'; import he from 'he'; import { writable, type Writable } from 'svelte/store'; -import { zettelKinds } from './consts'; +import { zettelKinds } from './consts.ts'; interface IndexMetadata { authors?: string[]; @@ -66,6 +66,8 @@ export default class Pharos { private asciidoctor: Asciidoctor; + private pharosExtensions: Extensions.Registry; + private ndk: NDK; private contextCounters: Map = new Map(); @@ -130,25 +132,26 @@ export default class Pharos { constructor(ndk: NDK) { this.asciidoctor = asciidoctor(); + this.pharosExtensions = this.asciidoctor.Extensions.create(); this.ndk = ndk; const pharos = this; - this.asciidoctor.Extensions.register(function () { - const registry = this; - registry.treeProcessor(function () { - const dsl = this; - dsl.process(function (document) { - const treeProcessor = this; - pharos.treeProcessor(treeProcessor, document); - }); - }) + this.pharosExtensions.treeProcessor(function () { + const dsl = this; + dsl.process(function (document) { + const treeProcessor = this; + pharos.treeProcessor(treeProcessor, document); + }); }); } parse(content: string, options?: ProcessorOptions | undefined): void { try { - this.html = this.asciidoctor.convert(content, options) as string | Document | undefined; + this.html = this.asciidoctor.convert(content, { + 'extension_registry': this.pharosExtensions, + ...options, + }) as string | Document | undefined; } catch (error) { console.error(error); throw new Error('Failed to parse AsciiDoc document.'); From a3fccb33150541d13ec10f824f42a3f677ac82fc Mon Sep 17 00:00:00 2001 From: Silberengel Date: Tue, 8 Apr 2025 08:22:36 +0200 Subject: [PATCH 34/40] Reformatted About page, fixed broken hyperlinks, added project documentation page, and made all internal links relative. --- src/routes/about/+page.svelte | 143 ++++++++++++++++++++++------------ 1 file changed, 94 insertions(+), 49 deletions(-) diff --git a/src/routes/about/+page.svelte b/src/routes/about/+page.svelte index 9e4ee80..6f4f96a 100644 --- a/src/routes/about/+page.svelte +++ b/src/routes/about/+page.svelte @@ -1,56 +1,101 @@
-
- About -

Alexandria is a reader and writer for curated publications (in Asciidoc), and will eventually also support long-form articles (Markdown) and wiki pages (Asciidoc). It is produced by the GitCitadel project team.

- -

Please submit support issues on the project repo page and follow us on GitHub and Geyserfund.

- -

We are easiest to contact over our Nostr address npub1s3h…75wz.

- - Overview - -

Alexandria opens up to the landing page, where the user can: login (top-right), select whether to only view the publications hosted on the thecitadel document relay or add in their own relays, and scroll/search the publications.

- -

Landing page

-

Relay selection

- -

There is also the ability to view the publications as a diagram, if you click on "Visualize", and to publish an e-book or other document (coming soon).

- -

If you click on a card, which represents a 30040 index event, the associated reading view opens to the publication. The app then pulls all of the content events (30041s), in the order in which they are indexed, and displays them as a single document.

+
+ About -

Each 30041 section is also a level in the table of contents, which can be accessed from the floating icon top-left in the reading view. This allows for navigation within the publication. (This functionality has been temporarily disabled.)

- -

ToC icon

-

Table of contents example

- - Typical use cases - - For e-books -

The most common use for Alexandria is for e-books: both those users have written themselves and those uploaded to Nostr from other sources. The first minor version of the app, Gutenberg, is focused on displaying and producing these publications.

- -

An example of a book is Jane Eyre

- -

Jane Eyre, by Charlotte Brontë

- - For scientific papers -

Alexandria will also display research papers with Asciimath and LaTeX embedding, and the normal advanced formatting options available for Asciidoc. In addition, we will be implementing special citation events, which will serve as an alternative or addition to the normal footnotes.

+

+ Alexandria is a reader and writer for curated publications (in Asciidoc), and will eventually also support long-form articles (Markdown) and wiki pages (Asciidoc). It is produced by the GitCitadel project team. +

-

Correctly displaying such papers, integrating citations, and allowing them to be reviewed (with kind 1111 comments), and annotated (with highlights) by users, is the focus of the second minor version, Euler.

+

+ Please submit support issues on the project repo page and follow us on GitHub and Geyserfund. +

-

Euler will also pioneer the HTTP-based (rather than websocket-based) e-paper compatible version of the web app.

- -

An example of a research paper is Less Partnering, Less Children, or Both?

- -

Research paper

- - For documentation -

Our own team uses Alexandria to document the app, to display our blog entries, as well as to store copies of our most interesting technical specifications.

- -

Documentation

- -
-
\ No newline at end of file +

+ We are easiest to contact over our Nostr address npub1s3h…75wz. +

+ + Overview + +

+ Alexandria opens up to the landing page, where the user can: login (top-right), select whether to only view the publications hosted on the thecitadel document relay or add in their own relays, and scroll/search the publications. +

+ +
+ Landing page + Relay selection +
+ +

+ There is also the ability to view the publications as a diagram, if you click on "Visualize", and to publish an e-book or other document (coming soon). +

+ +

+ If you click on a card, which represents a 30040 index event, the associated reading view opens to the publication. The app then pulls all of the content events (30041s), in the order in which they are indexed, and displays them as a single document. +

+ +

+ Each 30041 section is also a level in the table of contents, which can be accessed from the floating icon top-left in the reading view. This allows for navigation within the publication. (This functionality has been temporarily disabled.) +

+ +
+ ToC icon + Table of contents example +
+ + Typical use cases + + For e-books +
+ +

+ The most common use for Alexandria is for e-books: both those users have written themselves and those uploaded to Nostr from other sources. The first minor version of the app, Gutenberg, is focused on displaying and producing these publications. +

+ +

+ An example of a book is Jane Eyre +

+ +
+ Jane Eyre, by Charlotte Brontë +
+ + For scientific papers +
+ +

+ Alexandria will also display research papers with Asciimath and LaTeX embedding, and the normal advanced formatting options available for Asciidoc. In addition, we will be implementing special citation events, which will serve as an alternative or addition to the normal footnotes. +

+ +

+ Correctly displaying such papers, integrating citations, and allowing them to be reviewed (with kind 1111 comments), and annotated (with highlights) by users, is the focus of the second minor version, Euler. +

+ +

+ Euler will also pioneer the HTTP-based (rather than websocket-based) e-paper compatible version of the web app. +

+ +

+ An example of a research paper is Less Partnering, Less Children, or Both? +

+ +
+ Research paper +
+ + For documentation +
+ +

+ Our own team uses Alexandria to document the app, to display our blog entries, as well as to store copies of our most interesting technical specifications. +

+ +
+ Documentation +
+ + +
From bc8bd6f847122bcb55c76c33c71045b68ee6bfe7 Mon Sep 17 00:00:00 2001 From: Silberengel Date: Tue, 8 Apr 2025 08:56:37 +0200 Subject: [PATCH 35/40] Corrected more links. Tried to make the horizontal rules (Hr) more apparent, in light view, but it didn't work. --- src/routes/about/+page.svelte | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/routes/about/+page.svelte b/src/routes/about/+page.svelte index 6f4f96a..627bc30 100644 --- a/src/routes/about/+page.svelte +++ b/src/routes/about/+page.svelte @@ -7,11 +7,11 @@ About

- Alexandria is a reader and writer for curated publications (in Asciidoc), and will eventually also support long-form articles (Markdown) and wiki pages (Asciidoc). It is produced by the GitCitadel project team. + Alexandria is a reader and writer for curated publications (in Asciidoc), and will eventually also support long-form articles (Markdown) and wiki pages (Asciidoc). It is produced by the GitCitadel project team.

- Please submit support issues on the project repo page and follow us on GitHub and Geyserfund. + Please submit support issues on the Alexandria repo page and follow us on GitHub and Geyserfund.

@@ -49,7 +49,7 @@ Typical use cases For e-books -


+

The most common use for Alexandria is for e-books: both those users have written themselves and those uploaded to Nostr from other sources. The first minor version of the app, Gutenberg, is focused on displaying and producing these publications. @@ -64,7 +64,7 @@ For scientific papers -


+

Alexandria will also display research papers with Asciimath and LaTeX embedding, and the normal advanced formatting options available for Asciidoc. In addition, we will be implementing special citation events, which will serve as an alternative or addition to the normal footnotes. @@ -87,7 +87,7 @@ For documentation -


+

Our own team uses Alexandria to document the app, to display our blog entries, as well as to store copies of our most interesting technical specifications. From 0a5ae19905c6cdf74593a39e89ea89e94c205baa Mon Sep 17 00:00:00 2001 From: buttercat1791 Date: Tue, 8 Apr 2025 08:32:19 -0500 Subject: [PATCH 36/40] Remove a redundant lazy object instantiation --- src/lib/data_structures/publication_tree.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/lib/data_structures/publication_tree.ts b/src/lib/data_structures/publication_tree.ts index e7e0e41..7f6c968 100644 --- a/src/lib/data_structures/publication_tree.ts +++ b/src/lib/data_structures/publication_tree.ts @@ -89,8 +89,9 @@ export class PublicationTree implements AsyncIterable { parent: parentNode, children: [], }; - parentNode.children!.push(new Lazy(() => Promise.resolve(node))); - this.#nodes.set(address, new Lazy(() => Promise.resolve(node))); + const lazyNode = new Lazy(() => Promise.resolve(node)); + parentNode.children!.push(lazyNode); + this.#nodes.set(address, lazyNode); this.#events.set(address, event); } From 61d25cb1ca32d331c048a404119f9a46e4879749 Mon Sep 17 00:00:00 2001 From: Silberengel Date: Tue, 8 Apr 2025 20:56:08 +0200 Subject: [PATCH 37/40] Removed the rules. --- src/routes/about/+page.svelte | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/routes/about/+page.svelte b/src/routes/about/+page.svelte index 627bc30..60d3738 100644 --- a/src/routes/about/+page.svelte +++ b/src/routes/about/+page.svelte @@ -1,5 +1,5 @@

@@ -49,7 +49,6 @@ Typical use cases For e-books -

The most common use for Alexandria is for e-books: both those users have written themselves and those uploaded to Nostr from other sources. The first minor version of the app, Gutenberg, is focused on displaying and producing these publications. @@ -64,8 +63,7 @@

For scientific papers -
- +

Alexandria will also display research papers with Asciimath and LaTeX embedding, and the normal advanced formatting options available for Asciidoc. In addition, we will be implementing special citation events, which will serve as an alternative or addition to the normal footnotes.

@@ -87,8 +85,7 @@ For documentation -
- +

Our own team uses Alexandria to document the app, to display our blog entries, as well as to store copies of our most interesting technical specifications.

From 2ca90147021c1fe5d7e0c06de9061a976f2188e9 Mon Sep 17 00:00:00 2001 From: Silberengel Date: Tue, 8 Apr 2025 22:23:16 +0200 Subject: [PATCH 38/40] Allow for publications to be found with naddr or event, and not only d-tags. Prettified the json formatting in the View JSON window. Change the remaining blank links to buttons. Refactored. Closes#194 --- src/lib/components/PublicationHeader.svelte | 9 +- src/lib/components/util/CardActions.svelte | 41 +++---- .../components/util/CopyToClipboard.svelte | 4 +- src/lib/components/util/Profile.svelte | 12 +- src/lib/utils.ts | 14 +++ src/routes/publication/+page.ts | 103 +++++++++++++----- 6 files changed, 116 insertions(+), 67 deletions(-) diff --git a/src/lib/components/PublicationHeader.svelte b/src/lib/components/PublicationHeader.svelte index f9ded78..c7f9e15 100644 --- a/src/lib/components/PublicationHeader.svelte +++ b/src/lib/components/PublicationHeader.svelte @@ -1,6 +1,6 @@ - + diff --git a/src/lib/components/util/Profile.svelte b/src/lib/components/util/Profile.svelte index fd23c9f..6918677 100644 --- a/src/lib/components/util/Profile.svelte +++ b/src/lib/components/util/Profile.svelte @@ -70,25 +70,21 @@ function shortenNpub(long: string|undefined) { {#if isNav}
  • - Sign out - +
  • {:else} {/if} diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 021c979..35d6e03 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -10,6 +10,20 @@ export function neventEncode(event: NDKEvent, relays: string[]) { }); } +export function naddrEncode(event: NDKEvent, relays: string[]) { + const dTag = event.getMatchingTags('d')[0]?.[1]; + if (!dTag) { + throw new Error('Event does not have a d tag'); + } + + return nip19.naddrEncode({ + identifier: dTag, + pubkey: event.pubkey, + kind: event.kind || 0, + relays, + }); +} + export function formatDate(unixtimestamp: number) { const months = [ "Jan", diff --git a/src/routes/publication/+page.ts b/src/routes/publication/+page.ts index 7fddcdc..052cc75 100644 --- a/src/routes/publication/+page.ts +++ b/src/routes/publication/+page.ts @@ -1,43 +1,90 @@ import { error } from '@sveltejs/kit'; import type { NDKEvent } from '@nostr-dev-kit/ndk'; import type { PageLoad } from './$types'; +import { nip19 } from 'nostr-tools'; import { getActiveRelays } from '$lib/ndk.ts'; import { setContext } from 'svelte'; import { PublicationTree } from '$lib/data_structures/publication_tree.ts'; +/** + * Decodes an naddr identifier and returns a filter object + */ +function decodeNaddr(id: string) { + try { + if (!id.startsWith('naddr1')) return {}; + + const decoded = nip19.decode(id); + if (decoded.type !== 'naddr') return {}; + + const data = decoded.data; + return { + kinds: [data.kind], + authors: [data.pubkey], + '#d': [data.identifier] + }; + } catch (e) { + console.error('Failed to decode naddr:', e); + return {}; + } +} + +/** + * Fetches an event by ID or filter + */ +async function fetchEventById(ndk: any, id: string): Promise { + const filter = decodeNaddr(id); + const hasFilter = Object.keys(filter).length > 0; + + try { + const event = await (hasFilter ? + ndk.fetchEvent(filter) : + ndk.fetchEvent(id)); + + if (!event) { + throw new Error(`Event not found for ID: ${id}`); + } + + return event; + } catch (err) { + throw error(404, `Failed to fetch publication root event for ID: ${id}\n${err}`); + } +} + +/** + * Fetches an event by d tag + */ +async function fetchEventByDTag(ndk: any, dTag: string): Promise { + try { + const event = await ndk.fetchEvent( + { '#d': [dTag] }, + { closeOnEose: false }, + getActiveRelays(ndk) + ); + + if (!event) { + throw new Error(`Event not found for d tag: ${dTag}`); + } + + return event; + } catch (err) { + throw error(404, `Failed to fetch publication root event for d tag: ${dTag}\n${err}`); + } +} + export const load: PageLoad = async ({ url, parent }: { url: URL; parent: () => Promise }) => { const id = url.searchParams.get('id'); const dTag = url.searchParams.get('d'); - const { ndk, parser } = await parent(); - - let eventPromise: Promise; - let indexEvent: NDKEvent | null; - - if (id) { - eventPromise = ndk.fetchEvent(id) - .then((ev: NDKEvent | null) => { - return ev; - }) - .catch((err: any) => { - error(404, `Failed to fetch publication root event for ID: ${id}\n${err}`); - }); - } else if (dTag) { - eventPromise = new Promise(resolve => { - ndk - .fetchEvent({ '#d': [dTag] }, { closeOnEose: false }, getActiveRelays(ndk)) - .then((event: NDKEvent | null) => { - resolve(event); - }) - .catch((err: any) => { - error(404, `Failed to fetch publication root event for d tag: ${dTag}\n${err}`); - }); - }); - } else { - error(400, 'No publication root event ID or d tag provided.'); + + if (!id && !dTag) { + throw error(400, 'No publication root event ID or d tag provided.'); } - - indexEvent = await eventPromise as NDKEvent; + + // Fetch the event based on available parameters + const indexEvent = id + ? await fetchEventById(ndk, id) + : await fetchEventByDTag(ndk, dTag!); + const publicationType = indexEvent?.getMatchingTags('type')[0]?.[1]; const fetchPromise = parser.fetch(indexEvent); From 732d518eac182eb3e1ef1fb24836402c2696a4c5 Mon Sep 17 00:00:00 2001 From: buttercat1791 Date: Tue, 8 Apr 2025 22:49:15 -0500 Subject: [PATCH 39/40] Fix errors blocking publication load --- src/lib/components/Preview.svelte | 4 ++-- src/routes/publication/+page.ts | 2 -- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/lib/components/Preview.svelte b/src/lib/components/Preview.svelte index 30b7f10..0c85484 100644 --- a/src/lib/components/Preview.svelte +++ b/src/lib/components/Preview.svelte @@ -1,6 +1,6 @@