managarten/docs/optimizable/foundation-layer-improvements.md
Till JS 22a73943e1 chore: complete ManaCore → Mana rename (docs, go modules, plists, images)
Final cleanup of references missed in previous rename commits:

- Dockerfiles: PUBLIC_MANA_CORE_AUTH_URL → PUBLIC_MANA_AUTH_URL
- Go modules: github.com/manacore/* → github.com/mana/* (7 go.mod files)
- launchd plists: com.manacore.* → com.mana.* (14 files renamed + content)
- Image assets: *_Manacore_AI_Credits* → *_Mana_AI_Credits* (11 files)
- .env.example files: ManaCore brand strings → Mana
- .prettierignore: stale apps/manacore/* paths → apps/mana/*
- Markdown docs (CLAUDE.md, /docs/*): mana-core-auth → mana-auth, etc.

Excluded from rename: .claude/, devlog/, manascore/ (historical content),
client testimonials, blueprints, npm package refs (@mana-core/*).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 12:26:10 +02:00

10 KiB

Foundation Layer - Verbesserungsvorschläge

Stand: Dezember 2024 Betrifft: Contacts, Todo, Calendar (Foundation Services)

Aktuelle Architektur (Gut!)

Die drei Foundation Services sind korrekt als separate Services mit eigenen Datenbanken aufgesetzt:

┌─────────────┐   ┌─────────────┐   ┌─────────────┐
│  Contacts   │   │    Todo     │   │  Calendar   │
│   :3010     │   │   :3011     │   │   :3012     │
│             │   │             │   │             │
│ contacts DB │   │   todo DB   │   │ calendar DB │
└─────────────┘   └─────────────┘   └─────────────┘

Warum das richtig ist:

  • Unabhängige Deployments
  • Failure Isolation
  • Unabhängige Skalierung
  • Keine Schema-Konflikte zwischen Teams

Verbesserungsvorschläge

1. Foundation Clients Package

Aufwand: Mittel | Priorität: Hoch

Einheitlicher API-Client für alle Consumer Apps (Chat, Picture, Clock, etc.).

Neues Package: packages/foundation-clients/

// packages/foundation-clients/src/index.ts
export class FoundationClients {
  contacts: ContactsClient;
  todo: TodoClient;
  calendar: CalendarClient;

  constructor(config: FoundationConfig) {
    this.contacts = new ContactsClient(config);
    this.todo = new TodoClient(config);
    this.calendar = new CalendarClient(config);
  }
}

// packages/foundation-clients/src/contacts.client.ts
export class ContactsClient {
  private baseUrl: string;
  private cache: Map<string, CachedContact> = new Map();

  async get(id: string): Promise<Contact | null> {
    // Mit Caching
    const cached = this.cache.get(id);
    if (cached && !this.isStale(cached)) {
      return cached.data;
    }

    const response = await fetch(`${this.baseUrl}/contacts/${id}`, {
      headers: { Authorization: `Bearer ${this.token}` }
    });

    if (!response.ok) return null;
    const contact = await response.json();
    this.cache.set(id, { data: contact, fetchedAt: Date.now() });
    return contact;
  }

  async search(query: string): Promise<ContactSummary[]> {
    // Für Autocomplete in anderen Apps
  }

  async getBulk(ids: string[]): Promise<Contact[]> {
    // Effizient für Listen
  }
}

Nutzung in Consumer Apps:

// apps/chat/apps/backend/src/chat.service.ts
import { FoundationClients } from '@mana/foundation-clients';

@Injectable()
export class ChatService {
  private foundation: FoundationClients;

  constructor(configService: ConfigService) {
    this.foundation = new FoundationClients({
      contactsUrl: configService.get('CONTACTS_API_URL'),
      todoUrl: configService.get('TODO_API_URL'),
      calendarUrl: configService.get('CALENDAR_API_URL'),
    });
  }

  async getMessageWithContact(messageId: string) {
    const message = await this.getMessage(messageId);
    const sender = await this.foundation.contacts.get(message.senderId);
    return { ...message, sender };
  }
}

2. Event Bus (Redis Pub/Sub)

Aufwand: Mittel | Priorität: Mittel

Ermöglicht reaktive Updates zwischen Services ohne Polling.

Events definieren:

// packages/foundation-events/src/index.ts
export const FoundationEvents = {
  // Contacts
  CONTACT_CREATED: 'contact.created',
  CONTACT_UPDATED: 'contact.updated',
  CONTACT_DELETED: 'contact.deleted',

  // Todo
  TASK_CREATED: 'task.created',
  TASK_COMPLETED: 'task.completed',
  TASK_DELETED: 'task.deleted',

  // Calendar
  EVENT_CREATED: 'event.created',
  EVENT_UPDATED: 'event.updated',
  EVENT_DELETED: 'event.deleted',
} as const;

export interface TaskCompletedEvent {
  taskId: string;
  userId: string;
  completedAt: string;
  linkedCalendarEventId?: string;
}

Publisher (Todo Service):

// apps/todo/apps/backend/src/task/task.service.ts
import { RedisService } from '@mana/shared-redis';
import { FoundationEvents } from '@mana/foundation-events';

@Injectable()
export class TaskService {
  constructor(private redis: RedisService) {}

  async completeTask(taskId: string, userId: string) {
    const task = await this.markCompleted(taskId);

    // Event publizieren
    await this.redis.publish(FoundationEvents.TASK_COMPLETED, {
      taskId: task.id,
      userId,
      completedAt: new Date().toISOString(),
      linkedCalendarEventId: task.metadata?.linkedCalendarEventId,
    });

    return task;
  }
}

Subscriber (Calendar Service):

// apps/calendar/apps/backend/src/calendar.module.ts
import { FoundationEvents } from '@mana/foundation-events';

@Injectable()
export class CalendarEventSubscriber implements OnModuleInit {
  constructor(
    private redis: RedisService,
    private eventService: EventService
  ) {}

  onModuleInit() {
    this.redis.subscribe(FoundationEvents.TASK_COMPLETED, async (data) => {
      if (data.linkedCalendarEventId) {
        await this.eventService.markLinkedTaskCompleted(
          data.linkedCalendarEventId
        );
      }
    });
  }
}

Use Cases:

Event Reaktion
task.completed Calendar markiert verknüpftes Event
contact.updated Chat aktualisiert Sender-Anzeige
event.deleted Todo entfernt linkedCalendarEventId
contact.deleted Alle Apps entfernen Kontakt-Referenzen

3. Bulk-Endpoints

Aufwand: Klein | Priorität: Hoch

Reduziert N+1 API-Calls bei Listen-Ansichten.

Contacts Service:

// apps/contacts/apps/backend/src/contact/contact.controller.ts
@Controller('contacts')
export class ContactController {
  @Post('bulk')
  async getBulk(@Body() body: { ids: string[] }): Promise<Contact[]> {
    return this.contactService.findByIds(body.ids);
  }

  @Get('search')
  async search(
    @Query('q') query: string,
    @Query('limit') limit = 10
  ): Promise<ContactSummary[]> {
    return this.contactService.search(query, limit);
  }
}

Todo Service:

// apps/todo/apps/backend/src/task/task.controller.ts
@Controller('tasks')
export class TaskController {
  @Post('bulk')
  async getBulk(@Body() body: { ids: string[] }): Promise<Task[]> {
    return this.taskService.findByIds(body.ids);
  }

  @Get('by-contact/:contactId')
  async getByContact(@Param('contactId') contactId: string): Promise<Task[]> {
    // Tasks die mit einem Kontakt verknüpft sind
    return this.taskService.findByLinkedContact(contactId);
  }
}

Calendar Service:

// apps/calendar/apps/backend/src/event/event.controller.ts
@Controller('events')
export class EventController {
  @Post('bulk')
  async getBulk(@Body() body: { ids: string[] }): Promise<Event[]> {
    return this.eventService.findByIds(body.ids);
  }

  @Get('by-attendee')
  async getByAttendee(@Query('email') email: string): Promise<Event[]> {
    return this.eventService.findByAttendeeEmail(email);
  }
}

4. Caching-Layer

Aufwand: Klein | Priorität: Mittel

Kontakte ändern sich selten - perfekt für Caching.

In Foundation Clients (Client-Side Cache):

// packages/foundation-clients/src/cache.ts
export class SimpleCache<T> {
  private cache = new Map<string, { data: T; expiresAt: number }>();
  private ttlMs: number;

  constructor(ttlSeconds = 300) {
    this.ttlMs = ttlSeconds * 1000;
  }

  get(key: string): T | null {
    const entry = this.cache.get(key);
    if (!entry) return null;
    if (Date.now() > entry.expiresAt) {
      this.cache.delete(key);
      return null;
    }
    return entry.data;
  }

  set(key: string, data: T): void {
    this.cache.set(key, {
      data,
      expiresAt: Date.now() + this.ttlMs,
    });
  }

  invalidate(key: string): void {
    this.cache.delete(key);
  }
}

Redis Cache (Server-Side):

// apps/contacts/apps/backend/src/contact/contact.service.ts
@Injectable()
export class ContactService {
  private readonly CACHE_TTL = 300; // 5 Minuten

  async findById(id: string): Promise<Contact | null> {
    // 1. Redis Cache prüfen
    const cached = await this.redis.get(`contact:${id}`);
    if (cached) return JSON.parse(cached);

    // 2. DB Query
    const contact = await this.db
      .select()
      .from(contacts)
      .where(eq(contacts.id, id))
      .limit(1);

    if (contact[0]) {
      // 3. In Cache speichern
      await this.redis.setex(
        `contact:${id}`,
        this.CACHE_TTL,
        JSON.stringify(contact[0])
      );
    }

    return contact[0] || null;
  }

  async update(id: string, data: UpdateContactDto): Promise<Contact> {
    const updated = await this.db
      .update(contacts)
      .set(data)
      .where(eq(contacts.id, id))
      .returning();

    // Cache invalidieren
    await this.redis.del(`contact:${id}`);

    return updated[0];
  }
}

Implementierungs-Reihenfolge

Phase Task Abhängigkeiten
1 Bulk-Endpoints hinzufügen Keine
2 Foundation Clients Package erstellen Bulk-Endpoints
3 Client-Side Caching in Foundation Clients Foundation Clients
4 Redis Cache in Services Redis Setup
5 Event Bus Setup Redis Setup
6 Event Publisher/Subscriber Event Bus

Neue Package-Struktur

packages/
├── foundation-clients/           # NEU: API Clients
│   ├── src/
│   │   ├── contacts.client.ts
│   │   ├── todo.client.ts
│   │   ├── calendar.client.ts
│   │   ├── cache.ts
│   │   └── index.ts
│   └── package.json
│
├── foundation-events/            # NEU: Event Definitions
│   ├── src/
│   │   ├── contact.events.ts
│   │   ├── task.events.ts
│   │   ├── calendar.events.ts
│   │   └── index.ts
│   └── package.json
│
├── shared-types/                 # Existiert bereits
│   └── src/
│       ├── contact.ts            # ContactReference, ContactSummary
│       └── ...
│
└── shared-redis/                 # NEU oder erweitern
    └── src/
        ├── redis.service.ts
        ├── pub-sub.ts
        └── index.ts

Offene Fragen

  • Welche Consumer Apps werden als erste integriert?
  • Redis bereits im Stack oder neu einführen?
  • Cache TTL pro Entity-Typ oder einheitlich?
  • Event Bus: Redis Pub/Sub vs. dediziertes System (Bull, etc.)?