Kosh implements a service-oriented architecture with dependency injection, separating business logic from orchestration and data access.
Service Interfaces
All services are defined as interfaces in builder/services/interfaces.go, following Go’s implicit interface satisfaction.
PostService
Handles all post-related operations: parsing, rendering, and indexing.
type PostService interface {
Process(ctx context.Context, shouldForce, forceSocialRebuild, outputMissing bool) (*PostResult, error)
ProcessSingle(ctx context.Context, path string) error
RenderCachedPosts()
}
Key Responsibilities:
- Parse markdown files with Goldmark
- Render HTML with SSR (D2 diagrams, KaTeX math)
- Extract metadata and table of contents
- Build search index with BM25 scoring
- Generate social cards
- Manage post dependencies (templates, includes)
Implementation Details:
type postServiceImpl struct {
cfg *config.Config
cache CacheService
renderer RenderService
logger *slog.Logger
metrics *metrics.BuildMetrics
md goldmark.Markdown
nativeRenderer *native.Renderer
sourceFs afero.Fs
destFs afero.Fs
diagramAdapter *cache.DiagramCacheAdapter
}
The PostService uses worker pools to parallelize markdown parsing across CPU cores, significantly improving build times for large sites.
CacheService
Provides a thread-safe wrapper around the cache manager.
type CacheService interface {
// Read operations
GetPost(id string) (*cache.PostMeta, error)
ListAllPosts() ([]string, error)
GetPostByPath(path string) (*cache.PostMeta, error)
GetPostsByIDs(ids []string) (map[string]*cache.PostMeta, error)
GetSearchRecords(ids []string) (map[string]*cache.SearchRecord, error)
GetHTMLContent(post *cache.PostMeta) ([]byte, error)
// Write operations
StoreHTML(content []byte) (string, error)
StoreHTMLForPost(post *cache.PostMeta, content []byte) error
BatchCommit(posts []*cache.PostMeta, records map[string]*cache.SearchRecord, deps map[string]*cache.Dependencies) error
DeletePost(postID string) error
// Dirty tracking
MarkDirty(postID string)
IsDirty(postID string) bool
// Lifecycle
Stats() (*cache.CacheStats, error)
Close() error
}
Key Features:
- Dirty tracking with
sync.Map for lock-free access
- Batch operations to minimize BoltDB transactions
- Type-safe reads using generics
- In-memory LRU cache with 5-minute TTL for hot data
Implementation:
type cacheServiceImpl struct {
manager *cache.Manager
logger *slog.Logger
// Dirty tracking using sync.Map for thread safety
dirty sync.Map
}
The CacheService uses sync.Map instead of map[string]bool with mutex to provide lock-free dirty tracking in high-concurrency scenarios.
AssetService
Manages static assets: CSS, JavaScript, images, fonts, and other files.
type AssetService interface {
Build(ctx context.Context) error
}
Responsibilities:
- Copy static files from theme and site directories
- Bundle and minify CSS/JS with esbuild
- Compress images (PNG/JPG → WebP)
- Generate content hashes for cache-busting
- Populate
Assets map for template rendering
Build Process:
func (s *assetServiceImpl) Build(ctx context.Context) error {
var wg sync.WaitGroup
wg.Add(2)
// 1. Static Copy (excluding source CSS/JS)
go func() {
defer wg.Done()
// Copy theme static assets
// Copy site static assets
// Handle special files (wasm_exec.js)
}()
// 2. CSS/JS Bundling
go func() {
defer wg.Done()
// Bundle and minify with esbuild
// Generate hashed filenames
// Populate Assets map
}()
wg.Wait()
return nil
}
Asset processing runs in parallel, but must complete before post rendering to ensure the Assets map is populated.
RenderService
Wraps the HTML template renderer with asset management.
type RenderService interface {
RenderPage(path string, data models.PageData)
RenderIndex(path string, data models.PageData)
Render404(path string, data models.PageData)
RenderGraph(path string, data models.PageData)
RegisterFile(path string)
SetAssets(assets map[string]string)
GetAssets() map[string]string
GetRenderedFiles() map[string]bool
ClearRenderedFiles()
}
Key Features:
- Template caching for fast re-renders
- Asset map injection for cache-busted URLs
- File registration for VFS sync tracking
- Multiple render targets (pages, indexes, 404, graph)
Template Data:
type PageData struct {
Title string
Description string
Content template.HTML
Meta map[string]interface{}
BaseURL string
Permalink string
Image string
TOC []TOCEntry
Config *config.Config
Assets map[string]string // Injected by RenderService
// ... versioning, navigation, etc.
}
Service Composition
Services are composed in builder/run/builder.go through constructor injection:
func newBuilderWithConfig(cfg *config.Config) *Builder {
// Initialize infrastructure
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelInfo,
}))
buildMetrics := metrics.NewBuildMetrics()
sourceFs := afero.NewOsFs()
destFs := afero.NewMemMapFs()
// Open cache
cacheManager, err := cache.OpenWithTimeout(cfg.CacheDir, cfg.IsDev, cacheTimeout)
if err != nil {
logger.Warn("Failed to open cache database, using in-memory cache", "error", err)
}
// Create cache adapter
diagramAdapter := cache.NewDiagramCacheAdapter(cacheManager)
// Create native renderer
nativeRenderer := native.New()
// Create markdown parser
md := mdParser.New(cfg.BaseURL, nativeRenderer, diagramCache)
rnd := renderer.New(cfg.CompressImages, destFs, cfg.TemplateDir, logger)
// Wire services together
var cacheSvc services.CacheService
if cacheManager != nil {
cacheSvc = services.NewCacheService(cacheManager, logger)
}
renderSvc := services.NewRenderService(rnd, logger)
assetSvc := services.NewAssetService(sourceFs, destFs, cfg, renderSvc, logger)
postSvc := services.NewPostService(
cfg, cacheSvc, renderSvc, logger, buildMetrics,
md, nativeRenderer, sourceFs, destFs, diagramAdapter,
)
return &Builder{
cfg: cfg,
cacheService: cacheSvc,
postService: postSvc,
assetService: assetSvc,
renderService: renderSvc,
diagramAdapter: diagramAdapter,
logger: logger,
metrics: buildMetrics,
SourceFs: sourceFs,
DestFs: destFs,
md: md,
}
}
Service Lifecycle
Initialization
- Config Loading: Parse
kosh.yaml and CLI flags
- Cache Opening: Connect to BoltDB (or create new)
- Service Creation: Instantiate services with dependencies
- Validation: Check theme existence, create directories
Build Execution
- Builder.Build(ctx): Main entry point
- Asset Processing:
assetService.Build(ctx)
- Post Processing:
postService.Process(ctx, ...)
- Global Pages: Pagination, tags, graph, metadata
- PWA Generation: Manifest, service worker, icons
- VFS Sync: Flush memory file system to disk
Cleanup
- Save Caches: Flush diagram and metadata caches
- Increment Build Count: Track successful builds
- Close Resources: Close BoltDB connections
- Print Metrics: Display build statistics
func Run(args []string) {
b := NewBuilder(args)
defer b.Close() // Close BoltDB
defer b.SaveCaches() // Flush caches
if err := b.Build(context.Background()); err != nil {
b.logger.Error("Build failed", "error", err)
}
}
Testing Services
The service layer’s interface-based design enables easy mocking:
type mockCacheService struct {
posts map[string]*cache.PostMeta
}
func (m *mockCacheService) GetPost(id string) (*cache.PostMeta, error) {
post, ok := m.posts[id]
if !ok {
return nil, nil
}
return post, nil
}
// Use in tests
func TestPostService(t *testing.T) {
mockCache := &mockCacheService{
posts: map[string]*cache.PostMeta{
"test-id": {Title: "Test Post"},
},
}
svc := services.NewPostService(
cfg, mockCache, mockRender, logger, metrics,
md, nativeRenderer, sourceFs, destFs, diagramAdapter,
)
// Test service methods...
}
Error Handling in Services
Services follow consistent error handling patterns:
- Wrap errors with context:
fmt.Errorf("failed to parse post: %w", err)
- Log errors with structured fields:
logger.Error("msg", "key", value, "error", err)
- Return errors to caller: Let orchestration layer decide how to handle
- Continue on non-critical errors: Log warnings but keep building
func (s *postServiceImpl) Process(ctx context.Context, ...) (*PostResult, error) {
// Non-critical: log and continue
if err := s.cache.DeletePost(id); err != nil {
s.logger.Warn("Failed to delete stale post", "id", id, "error", err)
}
// Critical: return error
if err := s.cache.BatchCommit(posts, records, deps); err != nil {
return nil, fmt.Errorf("failed to commit cache batch: %w", err)
}
return result, nil
}
Worker Pools
Services use utils.WorkerPool[T] for bounded concurrency:
parsePool := utils.NewWorkerPool(ctx, numWorkers, func(task PostTask) {
// Parse markdown
// Render HTML
// Extract metadata
})
parsePool.Start()
for _, file := range files {
parsePool.Submit(PostTask{path: file})
}
parsePool.Stop() // Wait for completion
Object Pooling
Reduce GC pressure with sync.Pool:
buf := utils.SharedBufferPool.Get()
defer utils.SharedBufferPool.Put(buf)
if err := s.md.Renderer().Render(buf, source, docNode); err != nil {
return err
}
htmlContent := buf.String()
Batch Operations
Group database writes to minimize transactions:
var newPosts []*cache.PostMeta
var newRecords map[string]*cache.SearchRecord
// Collect all changes
for _, post := range posts {
newPosts = append(newPosts, post.Meta)
newRecords[post.ID] = post.SearchRecord
}
// Single transaction
if err := s.cache.BatchCommit(newPosts, newRecords, deps); err != nil {
return err
}
Next Steps