diff --git a/package-lock.json b/package-lock.json
index fa1d3ca..88c9eab 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "aitherboard",
- "version": "0.3.3",
+ "version": "0.3.4",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "aitherboard",
- "version": "0.3.3",
+ "version": "0.3.4",
"license": "MIT",
"dependencies": {
"@codemirror/autocomplete": "^6.20.0",
diff --git a/package.json b/package.json
index 1898425..e1b8fc7 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "aitherboard",
- "version": "0.3.3",
+ "version": "0.3.4",
"type": "module",
"author": "silberengel@gitcitadel.com",
"description": "A decentralized messageboard built on the Nostr protocol.",
diff --git a/src/lib/components/write/CreateEventForm.svelte b/src/lib/components/write/CreateEventForm.svelte
index 1c3ac4a..ab2cb25 100644
--- a/src/lib/components/write/CreateEventForm.svelte
+++ b/src/lib/components/write/CreateEventForm.svelte
@@ -211,6 +211,7 @@
const helpText = $derived(kindMetadata.helpText);
const isKind30040 = $derived(selectedKind === 30040);
const isKind10895 = $derived(selectedKind === 10895);
+ const allPublishRelays = $derived([...new Set([...config.documentationPublishRelays, ...config.graspRelays])]);
// Clear content for metadata-only kinds (but preserve content when cloning/editing)
$effect(() => {
@@ -289,7 +290,10 @@
const autoTagsResult = await autoExtractTags({
content: contentWithUrls,
existingTags: allTags,
- kind: effectiveKind
+ kind: effectiveKind,
+ // Skip p-tag extraction for kind 0 (profile/metadata) events
+ includeMentions: effectiveKind !== KIND.METADATA,
+ includeNostrLinks: effectiveKind !== KIND.METADATA
});
allTags.push(...autoTagsResult.tags);
@@ -357,7 +361,10 @@
const autoTagsResult = await autoExtractTags({
content: contentWithUrls,
existingTags: allTags,
- kind: effectiveKind
+ kind: effectiveKind,
+ // Skip p-tag extraction for kind 0 (profile/metadata) events
+ includeMentions: effectiveKind !== KIND.METADATA,
+ includeNostrLinks: effectiveKind !== KIND.METADATA
});
allTags.push(...autoTagsResult.tags);
@@ -597,7 +604,10 @@
const autoTagsResult = await autoExtractTags({
content: contentWithUrls,
existingTags: previewTags,
- kind: effectiveKind
+ kind: effectiveKind,
+ // Skip p-tag extraction for kind 0 (profile/metadata) events
+ includeMentions: effectiveKind !== KIND.METADATA,
+ includeNostrLinks: effectiveKind !== KIND.METADATA
});
previewTags.push(...autoTagsResult.tags);
@@ -796,7 +806,7 @@
Events will be published to:
- {#each [...config.documentationPublishRelays, ...config.graspRelays] as relay}
+ {#each allPublishRelays as relay}
-
{relay}
{#if config.documentationPublishRelays.includes(relay)}
diff --git a/src/lib/services/cache/event-cache.ts b/src/lib/services/cache/event-cache.ts
index 5404518..8d65879 100644
--- a/src/lib/services/cache/event-cache.ts
+++ b/src/lib/services/cache/event-cache.ts
@@ -58,9 +58,13 @@ export async function cacheEvent(event: NostrEvent): Promise {
existingEvent = eventsByPubkey.find((e: CachedEvent) => e.kind === event.kind) as CachedEvent | undefined;
}
- // If we found an existing event and it's different from the new one, save it to version history
- if (existingEvent && existingEvent.id !== event.id) {
+ // If we found an existing event and it's different from the new one,
+ // and the new event is newer, save the old one to version history
+ if (existingEvent && existingEvent.id !== event.id && event.created_at > existingEvent.created_at) {
await saveEventVersion(existingEvent);
+ } else if (existingEvent && existingEvent.id !== event.id && event.created_at <= existingEvent.created_at) {
+ // New event is not newer - don't cache it, keep the existing one
+ return;
}
} catch (error) {
// Version history save failed (non-critical) - continue with caching
@@ -101,12 +105,76 @@ export async function cacheEvents(events: NostrEvent[]): Promise {
const deletedIds = await getDeletedEventIds(eventIds);
// Filter out deleted events
- const eventsToCache = events.filter(e => !deletedIds.has(e.id));
+ let eventsToCache = events.filter(e => !deletedIds.has(e.id));
if (eventsToCache.length === 0) return;
- // Create a new transaction for writing (after the read transaction is complete)
+ // For replaceable events, check for existing versions and save them to history
const db = await getDB();
+ const replaceableEvents = eventsToCache.filter(e =>
+ isReplaceableKind(e.kind) || isParameterizedReplaceableKind(e.kind)
+ );
+
+ const eventsToSkip = new Set(); // Track events that shouldn't be cached
+
+ if (replaceableEvents.length > 0) {
+ try {
+ // Check each replaceable event for existing versions
+ for (const event of replaceableEvents) {
+ const dTag = isParameterizedReplaceableKind(event.kind)
+ ? event.tags.find(t => t[0] === 'd')?.[1] || null
+ : null;
+
+ // Find existing event with same pubkey, kind, and d tag (if parameterized)
+ let existingEvent: CachedEvent | undefined;
+
+ if (isParameterizedReplaceableKind(event.kind) && dTag) {
+ // For parameterized replaceable, need to search by pubkey, kind, and d tag
+ const tx = db.transaction('events', 'readonly');
+ const pubkeyIndex = tx.store.index('pubkey');
+ const eventsByPubkey = await pubkeyIndex.getAll(event.pubkey);
+ await tx.done;
+
+ // Filter to same kind and d tag
+ existingEvent = eventsByPubkey.find((e: CachedEvent) => {
+ if (e.kind !== event.kind) return false;
+ const existingDTag = e.tags.find(t => t[0] === 'd')?.[1] || null;
+ return existingDTag === dTag;
+ }) as CachedEvent | undefined;
+ } else {
+ // For regular replaceable, search by pubkey and kind
+ const tx = db.transaction('events', 'readonly');
+ const pubkeyIndex = tx.store.index('pubkey');
+ const eventsByPubkey = await pubkeyIndex.getAll(event.pubkey);
+ await tx.done;
+
+ // Filter to same kind
+ existingEvent = eventsByPubkey.find((e: CachedEvent) => e.kind === event.kind) as CachedEvent | undefined;
+ }
+
+ // If we found an existing event and it's different from the new one,
+ // and the new event is newer, save the old one to version history
+ if (existingEvent && existingEvent.id !== event.id && event.created_at > existingEvent.created_at) {
+ await saveEventVersion(existingEvent).catch((error) => {
+ // Non-critical - version history failures shouldn't break caching
+ console.debug('Error saving event version to history in batch:', error);
+ });
+ } else if (existingEvent && existingEvent.id !== event.id && event.created_at <= existingEvent.created_at) {
+ // New event is not newer - mark it to skip caching
+ eventsToSkip.add(event.id);
+ }
+ }
+ } catch (error) {
+ // Version history save failed (non-critical) - continue with caching
+ console.error('Error saving event versions to history in batch:', error);
+ }
+ }
+
+ // Filter out events that are not newer than existing cached versions
+ eventsToCache = eventsToCache.filter(e => !eventsToSkip.has(e.id));
+ if (eventsToCache.length === 0) return;
+
+ // Create a new transaction for writing (after the read transaction is complete)
const tx = db.transaction('events', 'readwrite');
// Prepare all cached events first
@@ -120,6 +188,18 @@ export async function cacheEvents(events: NostrEvent[]): Promise {
// Wait for transaction to complete
await tx.done;
+
+ // Also cache profile events (kind 0) in the profile cache
+ const profileEvents = eventsToCache.filter(e => e.kind === KIND.METADATA);
+ if (profileEvents.length > 0) {
+ const { cacheProfile } = await import('./profile-cache.js');
+ await Promise.all(profileEvents.map(event =>
+ cacheProfile(event).catch((error) => {
+ // Non-critical - profile cache failures shouldn't break event caching
+ console.debug('Error caching profile in profile cache:', error);
+ })
+ ));
+ }
} catch (error) {
// Cache write failed (non-critical)
// Don't throw - caching failures shouldn't break the app
diff --git a/src/lib/services/cache/profile-cache.ts b/src/lib/services/cache/profile-cache.ts
index 890d1b0..1bd35e6 100644
--- a/src/lib/services/cache/profile-cache.ts
+++ b/src/lib/services/cache/profile-cache.ts
@@ -14,17 +14,37 @@ export interface CachedProfile {
/**
* Store a profile in cache
+ * Only caches if the event is newer than the cached version (or if no cached version exists)
+ * Saves old versions to eventVersions before replacing
*/
export async function cacheProfile(event: NostrEvent): Promise {
if (event.kind !== KIND.METADATA) throw new Error('Not a profile event');
try {
- const db = await getDB();
- const cached: CachedProfile = {
- pubkey: event.pubkey,
- event,
- cached_at: Date.now()
- };
- await db.put('profiles', cached);
+ const db = await getDB();
+
+ // Check if we already have a cached version
+ const existing = await db.get('profiles', event.pubkey);
+
+ // Only cache if:
+ // 1. No existing cache, OR
+ // 2. The new event is newer (higher created_at)
+ if (!existing || event.created_at > existing.event.created_at) {
+ // If we have an existing version that's being replaced, save it to version history
+ if (existing && existing.event.id !== event.id) {
+ const { saveEventVersion } = await import('./version-history.js');
+ await saveEventVersion(existing.event).catch((error) => {
+ // Non-critical - version history failures shouldn't break profile caching
+ console.debug('Error saving profile version to history:', error);
+ });
+ }
+
+ const cached: CachedProfile = {
+ pubkey: event.pubkey,
+ event,
+ cached_at: Date.now()
+ };
+ await db.put('profiles', cached);
+ }
} catch (error) {
// Cache write failed (non-critical)
// Don't throw - caching failures shouldn't break the app
diff --git a/src/lib/services/user-data.ts b/src/lib/services/user-data.ts
index 372b30c..4fe8c02 100644
--- a/src/lib/services/user-data.ts
+++ b/src/lib/services/user-data.ts
@@ -80,59 +80,40 @@ export async function fetchProfile(
pubkey: string,
relays?: string[]
): Promise {
- // Try cache first
- const cached = await getProfile(pubkey);
- if (cached) {
- // Check if profile was recently cached (within last 5 minutes) - skip background refresh if so
- const cacheAge = Date.now() - cached.cached_at;
- const RECENT_CACHE_THRESHOLD = 300000; // 5 minutes
-
- // Only background refresh if cache is old
- if (cacheAge > RECENT_CACHE_THRESHOLD) {
- const relayList = relays || [
- ...config.defaultRelays,
- ...config.profileRelays
- ];
-
- // Background refresh - don't await, just fire and forget
- // Use low priority - profiles are background data, comments should load first
- nostrClient.fetchEvents(
- [{ kinds: [KIND.METADATA], authors: [pubkey], limit: 1 }],
- relayList,
- { useCache: false, cacheResults: true, priority: 'low' } // Don't use cache, but cache results
- ).then((events) => {
- if (events.length > 0) {
- cacheProfile(events[0]).catch(() => {
- // Silently fail - caching errors shouldn't break the app
- });
- }
- }).catch(() => {
- // Silently fail - background refresh errors shouldn't break the app
- });
- }
-
- return parseProfile(cached.event);
- }
-
- // No cache - fetch from relays
const relayList = relays || [
...config.defaultRelays,
...config.profileRelays
];
+ // Try cache first for immediate response
+ const cached = await getProfile(pubkey);
+
+ // Always fetch from relays to check for newer version
// Use low priority - profiles are background data, comments should load first
const events = await nostrClient.fetchEvents(
[{ kinds: [KIND.METADATA], authors: [pubkey], limit: 1 }],
relayList,
- { useCache: true, cacheResults: true, priority: 'low' }
+ { useCache: false, cacheResults: true, priority: 'low' } // Don't use cache, but cache results
);
- if (events.length === 0) return null;
+ if (events.length === 0) {
+ // No event from relays - return cached if available
+ if (cached) {
+ return parseProfile(cached.event);
+ }
+ return null;
+ }
const event = events[0];
- await cacheProfile(event);
-
- return parseProfile(event);
+
+ // Only use the fetched event if it's newer than cached (or if no cache exists)
+ if (!cached || event.created_at > cached.event.created_at) {
+ await cacheProfile(event);
+ return parseProfile(event);
+ }
+
+ // Cached version is newer or same - use cached
+ return parseProfile(cached.event);
}
/**
diff --git a/src/routes/repos/+page.svelte b/src/routes/repos/+page.svelte
index 3641432..368ab5e 100644
--- a/src/routes/repos/+page.svelte
+++ b/src/routes/repos/+page.svelte
@@ -6,6 +6,7 @@
import ProfileBadge from '../../lib/components/layout/ProfileBadge.svelte';
import { nostrClient } from '../../lib/services/nostr/nostr-client.js';
import { relayManager } from '../../lib/services/nostr/relay-manager.js';
+ import { config } from '../../lib/services/nostr/config.js';
import { onMount } from 'svelte';
import type { NostrEvent } from '../../lib/types/nostr.js';
import { nip19 } from 'nostr-tools';
@@ -159,13 +160,19 @@
}
try {
- const relays = relayManager.getProfileReadRelays();
+ // Combine profile read relays (includes user inbox and local relays), GRASP relays, and documentation relays
+ const profileRelays = relayManager.getProfileReadRelays();
+ const allRelays = [...new Set([
+ ...profileRelays,
+ ...config.graspRelays,
+ ...config.documentationPublishRelays
+ ])];
// Fetch repo announcement events with cache-first strategy
// This will check cache before making network requests
const events = await nostrClient.fetchEvents(
[{ kinds: [KIND.REPO_ANNOUNCEMENT], limit: 100 }],
- relays,
+ allRelays,
{
useCache: 'cache-first', // Check cache first, then fetch from relays if needed
cacheResults: true, // Cache any new results
diff --git a/static/Untitled b/static/Untitled
new file mode 100644
index 0000000..ef073cc
--- /dev/null
+++ b/static/Untitled
@@ -0,0 +1 @@
+n
\ No newline at end of file
diff --git a/static/changelog.yaml b/static/changelog.yaml
index c1a5e31..c959fec 100644
--- a/static/changelog.yaml
+++ b/static/changelog.yaml
@@ -1,4 +1,6 @@
versions:
+ '0.3.4':
+ - 'Added support for publishing to GRASP relays'
'0.3.3':
- 'Added GRASP repository management'
- 'Support manual creation and editing of User Grasp List (kind 10317) and repo announcements (kind 30617)'
diff --git a/static/healthz.json b/static/healthz.json
index 8ebb0e7..57404de 100644
--- a/static/healthz.json
+++ b/static/healthz.json
@@ -1,8 +1,8 @@
{
"status": "ok",
"service": "aitherboard",
- "version": "0.3.3",
- "buildTime": "2026-02-15T12:37:12.663Z",
+ "version": "0.3.4",
+ "buildTime": "2026-02-16T19:01:13.002Z",
"gitCommit": "unknown",
- "timestamp": 1771159032663
+ "timestamp": 1771268473002
}
\ No newline at end of file