Browse Source

refactor: Eliminate code duplication in publishing handlers

- Extract shared result processing logic into processPublishResults utility
- Add ProcessedPublishResults interface for type safety
- Create helper functions for error handling and event logging
- Reduce publishing handler code from ~200 to ~50 lines each
- Maintain all existing functionality while improving maintainability

Both handlePublishArticle and handlePublishScatteredNotes now use:
- processPublishResults() for consistent result processing
- createErrorResult() for standardized error handling
- logEventSummary() for centralized debug logging

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
master
limina1 6 months ago
parent
commit
de8fb36f41
  1. 167
      src/lib/services/publisher.ts
  2. 201
      src/routes/new/compose/+page.svelte

167
src/lib/services/publisher.ts

@ -1,10 +1,6 @@
import { getMimeTags } from "../utils/mime.ts"; import { getMimeTags } from "../utils/mime.ts";
import { import { metadataToTags } from "../utils/asciidoc_metadata.ts";
metadataToTags, import { parseAsciiDocWithMetadata } from "../utils/asciidoc_parser.ts";
} from "../utils/asciidoc_metadata.ts";
import {
parseAsciiDocWithMetadata,
} from "../utils/asciidoc_parser.ts";
import NDK, { NDKEvent, NDKRelaySet } from "@nostr-dev-kit/ndk"; import NDK, { NDKEvent, NDKRelaySet } from "@nostr-dev-kit/ndk";
import { nip19 } from "nostr-tools"; import { nip19 } from "nostr-tools";
@ -14,6 +10,14 @@ export interface PublishResult {
error?: string; error?: string;
} }
export interface ProcessedPublishResults {
successCount: number;
total: number;
errors: string[];
successfulEvents: Array<{ eventId: string; title: string }>;
failedEvents: Array<{ title: string; error: string; sectionIndex: number }>;
}
export interface PublishOptions { export interface PublishOptions {
content: string; content: string;
kind?: number; kind?: number;
@ -98,9 +102,8 @@ export async function publishZettel(
throw new Error("Failed to publish to any relays"); throw new Error("Failed to publish to any relays");
} }
} catch (error) { } catch (error) {
const errorMessage = error instanceof Error const errorMessage =
? error.message error instanceof Error ? error.message : "Unknown error";
: "Unknown error";
onError?.(errorMessage); onError?.(errorMessage);
return { success: false, error: errorMessage }; return { success: false, error: errorMessage };
} }
@ -122,43 +125,56 @@ export async function publishSingleEvent(
): Promise<PublishResult> { ): Promise<PublishResult> {
const { content, kind, tags, onError } = options; const { content, kind, tags, onError } = options;
if (!ndk?.activeUser) { if (!ndk?.activeUser) {
const error = 'Please log in first'; const error = "Please log in first";
onError?.(error); onError?.(error);
return { success: false, error }; return { success: false, error };
} }
try { try {
const allRelayUrls = Array.from(ndk.pool?.relays.values() || []).map((r) => r.url); const allRelayUrls = Array.from(ndk.pool?.relays.values() || []).map(
(r) => r.url,
);
if (allRelayUrls.length === 0) { if (allRelayUrls.length === 0) {
throw new Error('No relays available in NDK pool'); throw new Error("No relays available in NDK pool");
} }
const relaySet = NDKRelaySet.fromRelayUrls(allRelayUrls, ndk); const relaySet = NDKRelaySet.fromRelayUrls(allRelayUrls, ndk);
// Fix a-tags that have placeholder "pubkey" with actual pubkey // Fix a-tags that have placeholder "pubkey" with actual pubkey
const fixedTags = tags.map(tag => { const fixedTags = tags.map((tag) => {
if (tag[0] === 'a' && tag[1] && tag[1].includes(':pubkey:') && ndk.activeUser) { if (
tag[0] === "a" &&
tag[1] &&
tag[1].includes(":pubkey:") &&
ndk.activeUser
) {
// Replace "pubkey" placeholder with actual pubkey // Replace "pubkey" placeholder with actual pubkey
const fixedATag = tag[1].replace(':pubkey:', `:${ndk.activeUser.pubkey}:`); const fixedATag = tag[1].replace(
return [tag[0], fixedATag, tag[2] || '', tag[3] || '']; ":pubkey:",
`:${ndk.activeUser.pubkey}:`,
);
return [tag[0], fixedATag, tag[2] || "", tag[3] || ""];
} }
return tag; return tag;
}); });
// Auto-add author identity if not publishing on behalf of others // Auto-add author identity if not publishing on behalf of others
const hasAuthorTag = fixedTags.some(tag => tag[0] === 'author'); const hasAuthorTag = fixedTags.some((tag) => tag[0] === "author");
const hasPTag = fixedTags.some(tag => tag[0] === 'p'); const hasPTag = fixedTags.some((tag) => tag[0] === "p");
const finalTags = [...fixedTags]; const finalTags = [...fixedTags];
if (!hasAuthorTag && ndk.activeUser) { if (!hasAuthorTag && ndk.activeUser) {
// Add display name as author // Add display name as author
const displayName = ndk.activeUser.profile?.displayName || ndk.activeUser.profile?.name || 'Anonymous'; const displayName =
finalTags.push(['author', displayName]); ndk.activeUser.profile?.displayName ||
ndk.activeUser.profile?.name ||
"Anonymous";
finalTags.push(["author", displayName]);
} }
if (!hasPTag && ndk.activeUser) { if (!hasPTag && ndk.activeUser) {
// Add pubkey as p-tag // Add pubkey as p-tag
finalTags.push(['p', ndk.activeUser.pubkey]); finalTags.push(["p", ndk.activeUser.pubkey]);
} }
// Create and sign NDK event // Create and sign NDK event
@ -176,22 +192,25 @@ export async function publishSingleEvent(
if (publishedToRelays.size > 0) { if (publishedToRelays.size > 0) {
// Debug: Log the event structure in a clean, concise format // Debug: Log the event structure in a clean, concise format
const dTagEntry = tags.find(t => t[0] === 'd'); const dTagEntry = tags.find((t) => t[0] === "d");
const dTag = dTagEntry ? dTagEntry[1] : ''; const dTag = dTagEntry ? dTagEntry[1] : "";
const titleTag = tags.find(t => t[0] === 'title'); const titleTag = tags.find((t) => t[0] === "title");
const title = titleTag ? titleTag[1] : 'Untitled'; const title = titleTag ? titleTag[1] : "Untitled";
console.log(`Event verified: ${ndkEvent.id}`); console.log(`Event verified: ${ndkEvent.id}`);
return { success: true, eventId: ndkEvent.id }; return { success: true, eventId: ndkEvent.id };
} else { } else {
const titleTag = tags.find(t => t[0] === 'title'); const titleTag = tags.find((t) => t[0] === "title");
const title = titleTag ? titleTag[1] : 'Untitled'; const title = titleTag ? titleTag[1] : "Untitled";
console.error(`Failed to publish event: ${title} (${kind}) - no relays responded`); console.error(
throw new Error('Failed to publish to any relays'); `Failed to publish event: ${title} (${kind}) - no relays responded`,
);
throw new Error("Failed to publish to any relays");
} }
} catch (error) { } catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error'; const errorMessage =
error instanceof Error ? error.message : "Unknown error";
console.error(`Error publishing event: ${errorMessage}`); console.error(`Error publishing event: ${errorMessage}`);
onError?.(errorMessage); onError?.(errorMessage);
return { success: false, error: errorMessage }; return { success: false, error: errorMessage };
@ -227,8 +246,8 @@ export async function publishMultipleZettels(
throw new Error("No valid sections found in content"); throw new Error("No valid sections found in content");
} }
const allRelayUrls = Array.from(ndk.pool?.relays.values() || []).map((r) => const allRelayUrls = Array.from(ndk.pool?.relays.values() || []).map(
r.url (r) => r.url,
); );
if (allRelayUrls.length === 0) { if (allRelayUrls.length === 0) {
throw new Error("No relays available in NDK pool"); throw new Error("No relays available in NDK pool");
@ -266,9 +285,8 @@ export async function publishMultipleZettels(
}); });
} }
} catch (err) { } catch (err) {
const errorMessage = err instanceof Error const errorMessage =
? err.message err instanceof Error ? err.message : "Unknown error";
: "Unknown error";
results.push({ success: false, error: errorMessage }); results.push({ success: false, error: errorMessage });
} }
} }
@ -293,14 +311,87 @@ export async function publishMultipleZettels(
}); });
return results; return results;
} catch (error) { } catch (error) {
const errorMessage = error instanceof Error const errorMessage =
? error.message error instanceof Error ? error.message : "Unknown error";
: "Unknown error";
onError?.(errorMessage); onError?.(errorMessage);
return [{ success: false, error: errorMessage }]; return [{ success: false, error: errorMessage }];
} }
} }
/**
* Processes publish results and extracts success/failure information
* @param results - Array of publish results
* @param events - Event objects containing content and metadata
* @param hasIndexEvent - Whether the events include an index event
* @returns Processed results with counts and event details
*/
export function processPublishResults(
results: PublishResult[],
events: { indexEvent?: any; contentEvents: any[] },
hasIndexEvent: boolean = false,
): ProcessedPublishResults {
const successCount = results.filter((r) => r.success).length;
const errors = results
.filter((r) => !r.success && r.error)
.map((r) => r.error!);
// Extract successful events with their titles
const successfulEvents = results
.filter((r) => r.success && r.eventId)
.map((r, index) => {
let title: string;
if (index === 0 && hasIndexEvent && events.indexEvent) {
title = "Article Index";
} else {
const contentIndex = hasIndexEvent ? index - 1 : index;
const contentEvent = events.contentEvents[contentIndex];
title =
contentEvent?.title ||
contentEvent?.tags?.find((t: any) => t[0] === "title")?.[1] ||
`Note ${contentIndex + 1}`;
}
return {
eventId: r.eventId!,
title,
};
});
// Extract failed events with their titles and errors
const failedEvents = results
.map((r, index) => ({ result: r, index }))
.filter(({ result }) => !result.success)
.map(({ result, index }) => {
let title: string;
if (index === 0 && hasIndexEvent && events.indexEvent) {
title = "Article Index";
} else {
const contentIndex = hasIndexEvent ? index - 1 : index;
const contentEvent = events.contentEvents[contentIndex];
title =
contentEvent?.title ||
contentEvent?.tags?.find((t: any) => t[0] === "title")?.[1] ||
`Note ${contentIndex + 1}`;
}
return {
title,
error: result.error || "Unknown error",
sectionIndex: index,
};
});
return {
successCount,
total: results.length,
errors,
successfulEvents,
failedEvents,
};
}
function generateDTag(title: string): string { function generateDTag(title: string): string {
return title return title
.toLowerCase() .toLowerCase()

201
src/routes/new/compose/+page.svelte

@ -2,7 +2,11 @@
import { Heading, Button, Alert } from "flowbite-svelte"; import { Heading, Button, Alert } from "flowbite-svelte";
import ZettelEditor from "$lib/components/ZettelEditor.svelte"; import ZettelEditor from "$lib/components/ZettelEditor.svelte";
import { nip19 } from "nostr-tools"; import { nip19 } from "nostr-tools";
import { publishSingleEvent } from "$lib/services/publisher"; import {
publishSingleEvent,
processPublishResults,
type ProcessedPublishResults,
} from "$lib/services/publisher";
import { getNdkContext } from "$lib/ndk"; import { getNdkContext } from "$lib/ndk";
const ndk = getNdkContext(); const ndk = getNdkContext();
@ -10,13 +14,7 @@
let content = $state(""); let content = $state("");
let showPreview = $state(false); let showPreview = $state(false);
let isPublishing = $state(false); let isPublishing = $state(false);
let publishResults = $state<{ let publishResults = $state<ProcessedPublishResults | null>(null);
successCount: number;
total: number;
errors: string[];
successfulEvents: Array<{ eventId: string; title: string }>;
failedEvents: Array<{ title: string; error: string; sectionIndex: number }>;
} | null>(null);
// Handle content changes from ZettelEditor // Handle content changes from ZettelEditor
function handleContentChange(newContent: string) { function handleContentChange(newContent: string) {
@ -28,6 +26,53 @@
showPreview = show; showPreview = show;
} }
// Helper function to create error result
function createErrorResult(error: unknown): ProcessedPublishResults {
return {
successCount: 0,
total: 0,
errors: [error instanceof Error ? error.message : "Unknown error"],
successfulEvents: [],
failedEvents: [],
};
}
// Helper function to log event summaries
function logEventSummary(
events: any,
successfulEvents: Array<{ eventId: string; title: string }>,
) {
console.log("\n=== Events Summary ===");
if (events.indexEvent) {
console.log("\nRoot Index:");
console.log(`Event Summary:`);
console.log(` ID: ${successfulEvents[0]?.eventId || "Failed"}`);
console.log(` Kind: 30040`);
console.log(` Tags:`);
events.indexEvent.tags.forEach((tag: string[]) => {
console.log(` - ${JSON.stringify(tag)}`);
});
console.log(" ---");
}
console.log("\nContent:");
events.contentEvents.forEach((event: any, index: number) => {
const eventId =
successfulEvents.find((e) => e.title === event.title)?.eventId ||
"Failed";
console.log(`\nEvent Summary:`);
console.log(` ID: ${eventId}`);
console.log(` Kind: 30041`);
console.log(` Tags:`);
event.tags.forEach((tag: any) => {
console.log(` - ${JSON.stringify(tag)}`);
});
console.log(` Content preview: ${event.content.substring(0, 100)}...`);
console.log(" ---");
});
}
// Handle unified publishing from ZettelEditor // Handle unified publishing from ZettelEditor
async function handlePublishArticle(events: any) { async function handlePublishArticle(events: any) {
isPublishing = true; isPublishing = true;
@ -82,84 +127,18 @@
results.push(result); results.push(result);
} }
// Process results // Process results using shared utility
const successCount = results.filter((r) => r.success).length; publishResults = processPublishResults(
const errors = results results,
.filter((r) => !r.success && r.error) events,
.map((r) => r.error!); !!events.indexEvent,
);
// Extract successful events with their titles
const successfulEvents = results
.filter((r) => r.success && r.eventId)
.map((r, index) => ({
eventId: r.eventId!,
title:
index === 0 && events.indexEvent
? "Article Index"
: events.contentEvents[index - (events.indexEvent ? 1 : 0)]
?.title || `Note ${index}`,
}));
// Extract failed events with their titles and errors
const failedEvents = results
.map((r, index) => ({ result: r, index }))
.filter(({ result }) => !result.success)
.map(({ result, index }) => ({
title:
index === 0 && events.indexEvent
? "Article Index"
: events.contentEvents[index - (events.indexEvent ? 1 : 0)]
?.title || `Note ${index}`,
error: result.error || "Unknown error",
sectionIndex: index,
}));
publishResults = {
successCount,
total: results.length,
errors,
successfulEvents,
failedEvents,
};
// Show summary // Show summary
console.log("\n=== Events Summary ==="); logEventSummary(events, publishResults.successfulEvents);
if (events.indexEvent) {
console.log("\nRoot Index:");
console.log(`Event Summary:`);
console.log(` ID: ${successfulEvents[0]?.eventId || "Failed"}`);
console.log(` Kind: 30040`);
console.log(` Tags:`);
events.indexEvent.tags.forEach((tag: string[]) => {
console.log(` - ${JSON.stringify(tag)}`);
});
console.log(" ---");
}
console.log("\nContent:");
events.contentEvents.forEach((event: any, index: number) => {
const eventId =
successfulEvents.find((e) => e.title === event.title)?.eventId ||
"Failed";
console.log(`\nEvent Summary:`);
console.log(` ID: ${eventId}`);
console.log(` Kind: 30041`);
console.log(` Tags:`);
event.tags.forEach((tag: any) => {
console.log(` - ${JSON.stringify(tag)}`);
});
console.log(` Content preview: ${event.content.substring(0, 100)}...`);
console.log(" ---");
});
} catch (error) { } catch (error) {
console.error("Publishing failed:", error); console.error("Publishing failed:", error);
publishResults = { publishResults = createErrorResult(error);
successCount: 0,
total: 0,
errors: [error instanceof Error ? error.message : "Unknown error"],
successfulEvents: [],
failedEvents: [],
};
} }
isPublishing = false; isPublishing = false;
@ -193,64 +172,14 @@
results.push(result); results.push(result);
} }
// Process results // Process results using shared utility
const successCount = results.filter((r) => r.success).length; publishResults = processPublishResults(results, events, false);
const errors = results
.filter((r) => !r.success && r.error)
.map((r) => r.error!);
// Extract successful events with their titles
const successfulEvents = results
.filter((r) => r.success && r.eventId)
.map((r, index) => ({
eventId: r.eventId!,
title: events.contentEvents[index]?.title || `Note ${index + 1}`,
}));
// Extract failed events with their titles and errors
const failedEvents = results
.map((r, index) => ({ result: r, index }))
.filter(({ result }) => !result.success)
.map(({ result, index }) => ({
title: events.contentEvents[index]?.title || `Note ${index + 1}`,
error: result.error || "Unknown error",
sectionIndex: index,
}));
publishResults = {
successCount,
total: results.length,
errors,
successfulEvents,
failedEvents,
};
// Show summary // Show summary
console.log("\n=== Events Summary ==="); logEventSummary(events, publishResults.successfulEvents);
console.log("\nContent:");
events.contentEvents.forEach((event: any, index: number) => {
const eventId =
successfulEvents.find((e) => e.title === event.title)?.eventId ||
"Failed";
console.log(`\nEvent Summary:`);
console.log(` ID: ${eventId}`);
console.log(` Kind: 30041`);
console.log(` Tags:`);
event.tags.forEach((tag: any) => {
console.log(` - ${JSON.stringify(tag)}`);
});
console.log(` Content preview: ${event.content.substring(0, 100)}...`);
console.log(" ---");
});
} catch (error) { } catch (error) {
console.error("Publishing failed:", error); console.error("Publishing failed:", error);
publishResults = { publishResults = createErrorResult(error);
successCount: 0,
total: 0,
errors: [error instanceof Error ? error.message : "Unknown error"],
successfulEvents: [],
failedEvents: [],
};
} }
isPublishing = false; isPublishing = false;

Loading…
Cancel
Save