Browse Source

bug-fixes

master
Silberengel 4 weeks ago
parent
commit
460bb38439
  1. 4
      package-lock.json
  2. 2
      package.json
  3. 18
      src/lib/components/write/CreateEventForm.svelte
  4. 88
      src/lib/services/cache/event-cache.ts
  5. 20
      src/lib/services/cache/profile-cache.ts
  6. 55
      src/lib/services/user-data.ts
  7. 11
      src/routes/repos/+page.svelte
  8. 1
      static/Untitled
  9. 2
      static/changelog.yaml
  10. 6
      static/healthz.json

4
package-lock.json generated

@ -1,12 +1,12 @@ @@ -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",

2
package.json

@ -1,6 +1,6 @@ @@ -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.",

18
src/lib/components/write/CreateEventForm.svelte

@ -211,6 +211,7 @@ @@ -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 @@ @@ -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 @@ @@ -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 @@ @@ -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 @@ @@ -796,7 +806,7 @@
Events will be published to:
</p>
<ul class="relay-list">
{#each [...config.documentationPublishRelays, ...config.graspRelays] as relay}
{#each allPublishRelays as relay}
<li>
{relay}
{#if config.documentationPublishRelays.includes(relay)}

88
src/lib/services/cache/event-cache.ts vendored

@ -58,9 +58,13 @@ export async function cacheEvent(event: NostrEvent): Promise<void> { @@ -58,9 +58,13 @@ export async function cacheEvent(event: NostrEvent): Promise<void> {
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<void> { @@ -101,12 +105,76 @@ export async function cacheEvents(events: NostrEvent[]): Promise<void> {
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<string>(); // 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<void> { @@ -120,6 +188,18 @@ export async function cacheEvents(events: NostrEvent[]): Promise<void> {
// 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

20
src/lib/services/cache/profile-cache.ts vendored

@ -14,17 +14,37 @@ export interface CachedProfile { @@ -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<void> {
if (event.kind !== KIND.METADATA) throw new Error('Not a profile event');
try {
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

55
src/lib/services/user-data.ts

@ -80,61 +80,42 @@ export async function fetchProfile( @@ -80,61 +80,42 @@ export async function fetchProfile(
pubkey: string,
relays?: string[]
): Promise<ProfileData | null> {
// 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);
// 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);
}
/**
* Fetch multiple profiles
*/

11
src/routes/repos/+page.svelte

@ -6,6 +6,7 @@ @@ -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 @@ @@ -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

1
static/Untitled

@ -0,0 +1 @@ @@ -0,0 +1 @@
n

2
static/changelog.yaml

@ -1,4 +1,6 @@ @@ -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)'

6
static/healthz.json

@ -1,8 +1,8 @@ @@ -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
}
Loading…
Cancel
Save