From 874103a6e0bbc81c35cad42afd4cf6bac7ae5c9e Mon Sep 17 00:00:00 2001 From: buttercat1791 Date: Tue, 4 Mar 2025 23:08:11 -0600 Subject: [PATCH 01/28] 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/28] 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/28] 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/28] 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/28] 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/28] 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/28] 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/28] 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/28] 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/28] 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/28] 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/28] 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/28] 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/28] 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/28] 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/28] 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/28] 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/28] 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 3a313b1f3988c9ea65839577fccb10134c0826c1 Mon Sep 17 00:00:00 2001 From: buttercat1791 Date: Thu, 20 Mar 2025 09:33:07 -0500 Subject: [PATCH 19/28] 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 20/28] 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 21/28] 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 22/28] 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 23/28] 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 24/28] 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 25/28] 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 26/28] 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 613c3a4185fc5c18e0b475e0140e3d473d2236d0 Mon Sep 17 00:00:00 2001 From: buttercat1791 Date: Mon, 7 Apr 2025 09:39:31 -0500 Subject: [PATCH 27/28] 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 0a5ae19905c6cdf74593a39e89ea89e94c205baa Mon Sep 17 00:00:00 2001 From: buttercat1791 Date: Tue, 8 Apr 2025 08:32:19 -0500 Subject: [PATCH 28/28] 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); }