mirror of
https://github.com/purrgrammer/grimoire.git
synced 2026-04-11 07:56:50 +02:00
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:
@@ -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");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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}`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user