refactor: consolidate JSON parsing into cached getAppMetadata helper

Performance optimization:
- Create getAppMetadata() helper that parses content JSON once and caches
  the result using Symbol.for('nip89-metadata') as cache key
- All metadata helpers (getAppName, getAppDescription, getAppWebsite) now
  use the cached metadata instead of parsing JSON multiple times
- Prevents redundant JSON.parse() calls when multiple helpers are used

Code cleanup - removed unused functions:
- getAppImage() - no longer used after removing image display
- getHandlersByPlatform() - filtering done in component state
- substituteTemplate() - not needed in current implementation
- hasPlaceholder() - utility never used
- formatAddressPointer() - not needed anymore

Updated tests:
- Replace getAppImage tests with getAppWebsite tests
- Remove tests for deleted utility functions
- All remaining tests pass

This consolidation improves performance by ensuring JSON.parse() is called
at most once per event, regardless of how many metadata fields are accessed.
This commit is contained in:
Claude
2026-01-05 10:33:33 +00:00
parent f0ce89a7bb
commit 94028bdec4
2 changed files with 55 additions and 198 deletions

View File

@@ -2,7 +2,7 @@ import { describe, it, expect } from "vitest";
import {
getAppName,
getAppDescription,
getAppImage,
getAppWebsite,
getSupportedKinds,
getPlatformUrls,
getAvailablePlatforms,
@@ -10,11 +10,7 @@ import {
getRecommendedKind,
parseAddressPointer,
getHandlerReferences,
getHandlersByPlatform,
getRecommendedPlatforms,
substituteTemplate,
hasPlaceholder,
formatAddressPointer,
} from "./nip89-helpers";
import { NostrEvent } from "@/types/nostr";
@@ -119,29 +115,24 @@ describe("Kind 31990 (Application Handler) Helpers", () => {
});
});
describe("getAppImage", () => {
it("should extract image from content JSON", () => {
describe("getAppWebsite", () => {
it("should extract website from content JSON", () => {
const event = createHandlerEvent({
content: JSON.stringify({ image: "https://example.com/logo.png" }),
content: JSON.stringify({ website: "https://example.com" }),
});
expect(getAppImage(event)).toBe("https://example.com/logo.png");
expect(getAppWebsite(event)).toBe("https://example.com");
});
it("should extract picture field as fallback", () => {
it("should return undefined if no website field", () => {
const event = createHandlerEvent({
content: JSON.stringify({ picture: "https://example.com/pic.png" }),
content: JSON.stringify({ name: "App" }),
});
expect(getAppImage(event)).toBe("https://example.com/pic.png");
expect(getAppWebsite(event)).toBeUndefined();
});
it("should prefer image over picture", () => {
const event = createHandlerEvent({
content: JSON.stringify({
image: "https://example.com/logo.png",
picture: "https://example.com/pic.png",
}),
});
expect(getAppImage(event)).toBe("https://example.com/logo.png");
it("should return undefined if content is empty", () => {
const event = createHandlerEvent({ content: "" });
expect(getAppWebsite(event)).toBeUndefined();
});
});
@@ -351,39 +342,6 @@ describe("Kind 31989 (Handler Recommendation) Helpers", () => {
});
});
describe("getHandlersByPlatform", () => {
it("should filter handlers by platform", () => {
const event = createRecommendationEvent({
tags: [
["d", "9802"],
["a", "31990:pubkey1:handler1", "", "web"],
["a", "31990:pubkey2:handler2", "", "ios"],
["a", "31990:pubkey3:handler3", "", "web"],
],
});
const webHandlers = getHandlersByPlatform(event, "web");
expect(webHandlers).toHaveLength(2);
expect(webHandlers[0].platform).toBe("web");
expect(webHandlers[1].platform).toBe("web");
const iosHandlers = getHandlersByPlatform(event, "ios");
expect(iosHandlers).toHaveLength(1);
expect(iosHandlers[0].platform).toBe("ios");
});
it("should return all handlers if no platform specified", () => {
const event = createRecommendationEvent({
tags: [
["d", "9802"],
["a", "31990:pubkey1:handler1", "", "web"],
["a", "31990:pubkey2:handler2", "", "ios"],
],
});
const allHandlers = getHandlersByPlatform(event);
expect(allHandlers).toHaveLength(2);
});
});
describe("getRecommendedPlatforms", () => {
it("should return unique platforms from handler references", () => {
const event = createRecommendationEvent({
@@ -410,46 +368,3 @@ describe("Kind 31989 (Handler Recommendation) Helpers", () => {
});
});
});
describe("URL Template Utilities", () => {
describe("substituteTemplate", () => {
it("should replace <bech32> placeholder with entity", () => {
const template = "https://app.com/view/<bech32>";
const result = substituteTemplate(template, "nevent1abc123");
expect(result).toBe("https://app.com/view/nevent1abc123");
});
it("should replace multiple occurrences", () => {
const template = "https://app.com/<bech32>/view/<bech32>";
const result = substituteTemplate(template, "note1xyz");
expect(result).toBe("https://app.com/note1xyz/view/note1xyz");
});
it("should return unchanged if no placeholder", () => {
const template = "https://app.com/view";
const result = substituteTemplate(template, "nevent1abc");
expect(result).toBe("https://app.com/view");
});
});
describe("hasPlaceholder", () => {
it("should return true if template contains <bech32>", () => {
expect(hasPlaceholder("https://app.com/<bech32>")).toBe(true);
});
it("should return false if template does not contain <bech32>", () => {
expect(hasPlaceholder("https://app.com/view")).toBe(false);
});
});
describe("formatAddressPointer", () => {
it("should format address pointer as string", () => {
const pointer = {
kind: 31990,
pubkey: "abcd1234",
identifier: "my-handler",
};
expect(formatAddressPointer(pointer)).toBe("31990:abcd1234:my-handler");
});
});
});

View File

@@ -22,27 +22,41 @@ function getTagValues(event: NostrEvent, tagName: string): string[] {
// Kind 31990 (Application Handler) Helpers
// ============================================================================
/**
* Get parsed metadata from kind 31990 event content JSON
* Caches the parsed result to avoid redundant JSON.parse calls
*/
function getAppMetadata(event: NostrEvent): Record<string, any> | null {
if (event.kind !== 31990 || !event.content) return null;
// Use a symbol as cache key to avoid property name conflicts
const cacheKey = Symbol.for("nip89-metadata");
const cached = (event as any)[cacheKey];
if (cached !== undefined) return cached;
try {
const metadata = JSON.parse(event.content);
if (metadata && typeof metadata === "object") {
(event as any)[cacheKey] = metadata;
return metadata;
}
} catch {
// Invalid JSON
}
(event as any)[cacheKey] = null;
return null;
}
/**
* Extract app name from kind 31990 event content JSON or fallback to d tag
*/
export function getAppName(event: NostrEvent): string {
if (event.kind !== 31990) return "";
// Try to parse content as JSON
if (event.content) {
try {
const metadata = JSON.parse(event.content);
if (
metadata &&
typeof metadata === "object" &&
metadata.name &&
typeof metadata.name === "string"
) {
return metadata.name;
}
} catch {
// Not valid JSON, continue to fallback
}
const metadata = getAppMetadata(event);
if (metadata?.name && typeof metadata.name === "string") {
return metadata.name;
}
// Fallback to d tag identifier
@@ -55,40 +69,31 @@ export function getAppName(event: NostrEvent): string {
* Checks both 'description' and 'about' fields
*/
export function getAppDescription(event: NostrEvent): string | undefined {
if (event.kind !== 31990 || !event.content) return undefined;
if (event.kind !== 31990) return undefined;
try {
const metadata = JSON.parse(event.content);
if (metadata && typeof metadata === "object") {
// Check description first, then about (common in kind 0 profile format)
const desc = metadata.description || metadata.about;
if (desc && typeof desc === "string") {
return desc;
}
const metadata = getAppMetadata(event);
if (metadata) {
// Check description first, then about (common in kind 0 profile format)
const desc = metadata.description || metadata.about;
if (desc && typeof desc === "string") {
return desc;
}
} catch {
// Invalid JSON
}
return undefined;
}
/**
* Extract app image URL from kind 31990 event content JSON
* Extract website URL from kind 31990 event content JSON
*/
export function getAppImage(event: NostrEvent): string | undefined {
if (event.kind !== 31990 || !event.content) return undefined;
export function getAppWebsite(event: NostrEvent): string | undefined {
if (event.kind !== 31990) return undefined;
try {
const metadata = JSON.parse(event.content);
if (metadata && typeof metadata === "object") {
const image = metadata.image || metadata.picture;
if (image && typeof image === "string") {
return image;
}
}
} catch {
// Invalid JSON
const metadata = getAppMetadata(event);
if (metadata?.website && typeof metadata.website === "string") {
return metadata.website;
}
return undefined;
}
@@ -150,26 +155,6 @@ export function getAvailablePlatforms(event: NostrEvent): string[] {
return Object.keys(getPlatformUrls(event));
}
/**
* Extract website URL from kind 31990 event content JSON
*/
export function getAppWebsite(event: NostrEvent): string | undefined {
if (event.kind !== 31990 || !event.content) return undefined;
try {
const metadata = JSON.parse(event.content);
if (metadata && typeof metadata === "object") {
const website = metadata.website;
if (website && typeof website === "string") {
return website;
}
}
} catch {
// Invalid JSON
}
return undefined;
}
/**
* Get the d tag identifier from kind 31990 event
*/
@@ -255,20 +240,6 @@ export function getHandlerReferences(event: NostrEvent): HandlerReference[] {
return references;
}
/**
* Get handler references filtered by platform
*/
export function getHandlersByPlatform(
event: NostrEvent,
platform?: string,
): HandlerReference[] {
const allRefs = getHandlerReferences(event);
if (!platform) return allRefs;
return allRefs.filter((ref) => ref.platform === platform);
}
/**
* Get unique platforms from handler references in kind 31989
*/
@@ -284,32 +255,3 @@ export function getRecommendedPlatforms(event: NostrEvent): string[] {
return Array.from(platforms).sort();
}
// ============================================================================
// URL Template Utilities
// ============================================================================
/**
* Substitute <bech32> placeholder in URL template with actual bech32 entity
*/
export function substituteTemplate(
template: string,
bech32Entity: string,
): string {
return template.replace(/<bech32>/g, bech32Entity);
}
/**
* Check if a string contains the <bech32> placeholder
*/
export function hasPlaceholder(template: string): boolean {
return template.includes("<bech32>");
}
/**
* Format an address pointer as a string for display
* Format: "kind:pubkey:identifier"
*/
export function formatAddressPointer(pointer: AddressPointer): string {
return `${pointer.kind}:${pointer.pubkey}:${pointer.identifier}`;
}