feat(backend): add domain event bus and wire note CRUD events

- New lib/event-bus.ts: lightweight typed pub/sub with error isolation
  via Promise.allSettled. Supports note.created, note.updated,
  note.deleted, task.created, workspace.created events.
- notes/repository.ts: emit events on create, update, and delete.
- .env.example: add TELEMETRY_ENABLED, FEATURE_FLAGS_ENABLED,
  FIELD_ENCRYPT_ENABLED, PALACE_ENABLED, PALACE_EXTRACTION_ENABLED.
This commit is contained in:
saravanakumardb1 2026-04-13 10:00:12 -07:00
parent 58a778bc1e
commit 190d23280f
3 changed files with 115 additions and 2 deletions

View File

@ -14,6 +14,10 @@ PLATFORM_SERVICE_URL=http://localhost:4003
EXTRACTION_SERVICE_URL=http://localhost:4005
MCP_SERVER_URL=http://localhost:4007
# Observability
TELEMETRY_ENABLED=false
FEATURE_FLAGS_ENABLED=false
# LLM (@bytelyst/llm)
LLM_PROVIDER=mock
OPENAI_API_KEY=
@ -25,7 +29,12 @@ LLM_VISION_MODEL=gpt-4o
LLM_EMBEDDING_MODEL=text-embedding-3-small
# Field-level encryption
FIELD_ENCRYPT_ENABLED=true
FIELD_ENCRYPT_KEY_PROVIDER=memory
FIELD_ENCRYPT_KEY=
FIELD_ENCRYPT_MEK_NAME=notelett-mek
AZURE_KEYVAULT_URL=
# Palace (MemPalace)
PALACE_ENABLED=true
PALACE_EXTRACTION_ENABLED=true

View File

@ -0,0 +1,84 @@
/**
* Domain event bus singleton for NoteLett backend.
*
* Lightweight typed pub/sub for domain events. Handlers run via
* Promise.allSettled a failing handler never blocks others.
*/
export interface NoteCreatedEvent {
noteId: string;
workspaceId: string;
userId: string;
title: string;
}
export interface NoteUpdatedEvent {
noteId: string;
workspaceId: string;
userId: string;
title: string;
}
export interface NoteDeletedEvent {
noteId: string;
workspaceId: string;
userId: string;
}
export interface TaskCreatedEvent {
taskId: string;
noteId: string;
workspaceId: string;
userId: string;
title: string;
}
export interface WorkspaceCreatedEvent {
workspaceId: string;
userId: string;
name: string;
}
export type NoteLettEventMap = {
'note.created': NoteCreatedEvent;
'note.updated': NoteUpdatedEvent;
'note.deleted': NoteDeletedEvent;
'task.created': TaskCreatedEvent;
'workspace.created': WorkspaceCreatedEvent;
};
type Handler<T> = (payload: T) => void | Promise<void>;
class DomainEventBus {
private handlers = new Map<string, Set<Handler<unknown>>>();
on<K extends keyof NoteLettEventMap>(event: K, handler: Handler<NoteLettEventMap[K]>): () => void {
if (!this.handlers.has(event)) this.handlers.set(event, new Set());
this.handlers.get(event)!.add(handler as Handler<unknown>);
return () => { this.handlers.get(event)?.delete(handler as Handler<unknown>); };
}
async emit<K extends keyof NoteLettEventMap>(event: K, payload: NoteLettEventMap[K]): Promise<void> {
const fns = this.handlers.get(event);
if (!fns || fns.size === 0) return;
await Promise.allSettled([...fns].map(fn => fn(payload)));
}
removeAll(): void {
this.handlers.clear();
}
}
let _bus: DomainEventBus | null = null;
export function getEventBus(): DomainEventBus {
if (!_bus) {
_bus = new DomainEventBus();
}
return _bus;
}
/** @internal — for testing only. */
export function _resetEventBus(): void {
_bus = null;
}

View File

@ -1,5 +1,6 @@
import { getCollection } from '../../lib/datastore.js';
import { getEncryptor } from '../../lib/field-encrypt.js';
import { getEventBus } from '../../lib/event-bus.js';
import { isEncryptedField, type EncryptedField } from '@bytelyst/field-encrypt';
import type { NoteDoc, ListNotesQuery } from './types.js';
import type { FilterMap } from '@bytelyst/datastore';
@ -78,7 +79,14 @@ export async function getNote(id: string, workspaceId: string): Promise<NoteDoc
export async function createNote(doc: NoteDoc): Promise<NoteDoc> {
const encrypted = await encryptFields(doc);
const created = await collection().create(encrypted);
return decryptFields(created);
const result = await decryptFields(created);
getEventBus().emit('note.created', {
noteId: result.id,
workspaceId: result.workspaceId,
userId: result.userId,
title: result.title,
}).catch(() => {});
return result;
}
export async function deleteNote(id: string, workspaceId: string): Promise<boolean> {
@ -93,6 +101,11 @@ export async function deleteNote(id: string, workspaceId: string): Promise<boole
} as NoteDoc);
await collection().upsert(merged);
getEventBus().emit('note.deleted', {
noteId: id,
workspaceId: existing.workspaceId,
userId: existing.userId,
}).catch(() => {});
return true;
}
@ -120,5 +133,12 @@ export async function updateNote(
const encrypted = await encryptFields(merged);
const result = await collection().upsert(encrypted);
return decryptFields(result);
const decryptedResult = await decryptFields(result);
getEventBus().emit('note.updated', {
noteId: decryptedResult.id,
workspaceId: decryptedResult.workspaceId,
userId: decryptedResult.userId,
title: decryptedResult.title,
}).catch(() => {});
return decryptedResult;
}