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>
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.)?