Merge: Knowledge caching layer (closes #79)

This commit is contained in:
Jason Woltje
2026-01-30 00:16:36 -06:00
13 changed files with 1522 additions and 388 deletions

View File

@@ -17,6 +17,7 @@ import type {
} from "./entities/knowledge-entry-version.entity";
import { renderMarkdown } from "./utils/markdown";
import { LinkSyncService } from "./services/link-sync.service";
import { KnowledgeCacheService } from "./services/cache.service";
/**
* Service for managing knowledge entries
@@ -25,7 +26,8 @@ import { LinkSyncService } from "./services/link-sync.service";
export class KnowledgeService {
constructor(
private readonly prisma: PrismaService,
private readonly linkSync: LinkSyncService
private readonly linkSync: LinkSyncService,
private readonly cache: KnowledgeCacheService
) {}
@@ -120,6 +122,13 @@ export class KnowledgeService {
workspaceId: string,
slug: string
): Promise<KnowledgeEntryWithTags> {
// Check cache first
const cached = await this.cache.getEntry(workspaceId, slug);
if (cached) {
return cached;
}
// Cache miss - fetch from database
const entry = await this.prisma.knowledgeEntry.findUnique({
where: {
workspaceId_slug: {
@@ -142,7 +151,7 @@ export class KnowledgeService {
);
}
return {
const result: KnowledgeEntryWithTags = {
id: entry.id,
workspaceId: entry.workspaceId,
slug: entry.slug,
@@ -163,6 +172,11 @@ export class KnowledgeService {
color: et.tag.color,
})),
};
// Populate cache
await this.cache.setEntry(workspaceId, slug, result);
return result;
}
/**
@@ -236,6 +250,10 @@ export class KnowledgeService {
// Sync wiki links after entry creation
await this.linkSync.syncLinks(workspaceId, result.id, createDto.content);
// Invalidate search and graph caches (new entry affects search results)
await this.cache.invalidateSearches(workspaceId);
await this.cache.invalidateGraphs(workspaceId);
return {
id: result.id,
workspaceId: result.workspaceId,
@@ -390,6 +408,20 @@ export class KnowledgeService {
await this.linkSync.syncLinks(workspaceId, result.id, result.content);
}
// Invalidate caches
// Invalidate old slug cache if slug changed
if (newSlug !== slug) {
await this.cache.invalidateEntry(workspaceId, slug);
}
// Invalidate new slug cache
await this.cache.invalidateEntry(workspaceId, result.slug);
// Invalidate search caches (content/title/tags may have changed)
await this.cache.invalidateSearches(workspaceId);
// Invalidate graph caches if links changed
if (updateDto.content !== undefined) {
await this.cache.invalidateGraphsForEntry(workspaceId, result.id);
}
return {
id: result.id,
workspaceId: result.workspaceId,
@@ -444,6 +476,11 @@ export class KnowledgeService {
updatedBy: userId,
},
});
// Invalidate caches
await this.cache.invalidateEntry(workspaceId, slug);
await this.cache.invalidateSearches(workspaceId);
await this.cache.invalidateGraphsForEntry(workspaceId, entry.id);
}
/**
@@ -737,6 +774,11 @@ export class KnowledgeService {
// Sync wiki links after restore
await this.linkSync.syncLinks(workspaceId, result.id, result.content);
// Invalidate caches (content changed, links may have changed)
await this.cache.invalidateEntry(workspaceId, slug);
await this.cache.invalidateSearches(workspaceId);
await this.cache.invalidateGraphsForEntry(workspaceId, result.id);
return {
id: result.id,
workspaceId: result.workspaceId,