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}
- invalidateAll()}>
+ invalidateAll()}>
Try Again
- goto('/')}>
+ goto('/')}>
Return to Home
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);
}