feat(docs): completely redo documentation

This commit is contained in:
perf3ct
2025-08-21 15:55:44 +00:00
parent 4ce9102f93
commit 065740eabc
155 changed files with 54757 additions and 105 deletions

View File

@@ -0,0 +1,899 @@
# API Architecture
Trilium provides multiple API layers for different use cases: Internal API for frontend-backend communication, ETAPI for external integrations, and WebSocket for real-time synchronization. This document details each API layer's design, usage, and best practices.
## API Layers Overview
```mermaid
graph TB
subgraph "Client Applications"
WebApp[Web Application]
Desktop[Desktop App]
Mobile[Mobile App]
External[External Apps]
Scripts[User Scripts]
end
subgraph "API Layers"
Internal[Internal API<br/>REST + WebSocket]
ETAPI[ETAPI<br/>External API]
WS[WebSocket<br/>Real-time Sync]
end
subgraph "Backend Services"
Routes[Route Handlers]
Services[Business Logic]
Becca[Becca Cache]
DB[(Database)]
end
WebApp --> Internal
Desktop --> Internal
Mobile --> Internal
External --> ETAPI
Scripts --> ETAPI
Internal --> Routes
ETAPI --> Routes
WS --> Services
Routes --> Services
Services --> Becca
Becca --> DB
style Internal fill:#e3f2fd
style ETAPI fill:#fff3e0
style WS fill:#f3e5f5
```
## Internal API
**Location**: `/apps/server/src/routes/api/`
The Internal API handles communication between Trilium's frontend and backend, providing full access to application functionality.
### Architecture
```typescript
// Route structure
/api/
notes.ts // Note operations
branches.ts // Branch management
attributes.ts // Attribute operations
tree.ts // Tree structure
search.ts // Search functionality
sync.ts // Synchronization
options.ts // Configuration
special.ts // Special operations
```
### Request/Response Pattern
```typescript
// Typical API endpoint structure
router.get('/notes/:noteId', (req, res) => {
const note = becca.getNote(req.params.noteId);
if (!note) {
return res.status(404).json({
error: 'Note not found'
});
}
res.json(note.getPojo());
});
router.put('/notes/:noteId', (req, res) => {
const note = becca.getNoteOrThrow(req.params.noteId);
note.title = req.body.title;
note.content = req.body.content;
note.save();
res.json({ success: true });
});
```
### Key Endpoints
#### Note Operations
```typescript
// Get note with content
GET /api/notes/:noteId
Response: {
noteId: string,
title: string,
type: string,
content: string,
dateCreated: string,
dateModified: string
}
// Update note
PUT /api/notes/:noteId
Body: {
title?: string,
content?: string,
type?: string,
mime?: string
}
// Create note
POST /api/notes/:parentNoteId/children
Body: {
title: string,
type: string,
content?: string,
position?: number
}
// Delete note
DELETE /api/notes/:noteId
```
#### Tree Operations
```typescript
// Get tree structure
GET /api/tree
Query: {
subTreeNoteId?: string,
includeAttributes?: boolean
}
Response: {
notes: FNoteRow[],
branches: FBranchRow[],
attributes: FAttributeRow[]
}
// Move branch
PUT /api/branches/:branchId/move
Body: {
parentNoteId: string,
position: number
}
```
#### Search Operations
```typescript
// Execute search
GET /api/search
Query: {
query: string,
fastSearch?: boolean,
includeArchivedNotes?: boolean,
ancestorNoteId?: string
}
Response: {
results: Array<{
noteId: string,
title: string,
path: string,
score: number
}>
}
```
### Authentication & Security
```typescript
// CSRF protection
app.use(csrfMiddleware);
// Session authentication
router.use((req, res, next) => {
if (!req.session.loggedIn) {
return res.status(401).json({
error: 'Not authenticated'
});
}
next();
});
// Protected note access
router.get('/notes/:noteId', (req, res) => {
const note = becca.getNote(req.params.noteId);
if (note.isProtected && !protectedSessionService.isProtectedSessionAvailable()) {
return res.status(403).json({
error: 'Protected session required'
});
}
res.json(note.getPojo());
});
```
## ETAPI (External API)
**Location**: `/apps/server/src/etapi/`
ETAPI provides a stable, versioned API for external applications and scripts to interact with Trilium.
### Architecture
```typescript
// ETAPI structure
/etapi/
etapi.openapi.yaml // OpenAPI specification
auth.ts // Authentication
notes.ts // Note endpoints
branches.ts // Branch endpoints
attributes.ts // Attribute endpoints
attachments.ts // Attachment endpoints
special_notes.ts // Special note operations
```
### Authentication
ETAPI uses token-based authentication:
```typescript
// Creating ETAPI token
POST /etapi/auth/login
Body: {
username: string,
password: string
}
Response: {
authToken: string
}
// Using token in requests
GET /etapi/notes/:noteId
Headers: {
Authorization: "authToken"
}
```
### Key Endpoints
#### Note CRUD Operations
```typescript
// Create note
POST /etapi/notes
Body: {
noteId?: string,
parentNoteId: string,
title: string,
type: string,
content?: string,
position?: number
}
// Get note
GET /etapi/notes/:noteId
Response: {
noteId: string,
title: string,
type: string,
mime: string,
isProtected: boolean,
attributes: Array<{
attributeId: string,
type: string,
name: string,
value: string
}>,
parentNoteIds: string[],
childNoteIds: string[],
dateCreated: string,
dateModified: string
}
// Update note content
PUT /etapi/notes/:noteId/content
Body: string | Buffer
Headers: {
"Content-Type": mime-type
}
// Delete note
DELETE /etapi/notes/:noteId
```
#### Attribute Management
```typescript
// Create attribute
POST /etapi/attributes
Body: {
noteId: string,
type: 'label' | 'relation',
name: string,
value: string,
isInheritable?: boolean
}
// Update attribute
PATCH /etapi/attributes/:attributeId
Body: {
value?: string,
isInheritable?: boolean
}
```
#### Search
```typescript
// Search notes
GET /etapi/notes/search
Query: {
search: string,
limit?: number,
orderBy?: string,
orderDirection?: 'asc' | 'desc'
}
Response: {
results: Array<{
noteId: string,
title: string,
// Other note properties
}>
}
```
### Client Libraries
```javascript
// JavaScript client example
class EtapiClient {
constructor(serverUrl, authToken) {
this.serverUrl = serverUrl;
this.authToken = authToken;
}
async getNote(noteId) {
const response = await fetch(
`${this.serverUrl}/etapi/notes/${noteId}`,
{
headers: {
'Authorization': this.authToken
}
}
);
return response.json();
}
async createNote(parentNoteId, title, content) {
const response = await fetch(
`${this.serverUrl}/etapi/notes`,
{
method: 'POST',
headers: {
'Authorization': this.authToken,
'Content-Type': 'application/json'
},
body: JSON.stringify({
parentNoteId,
title,
type: 'text',
content
})
}
);
return response.json();
}
}
```
### Python Client Example
```python
import requests
class TriliumETAPI:
def __init__(self, server_url, auth_token):
self.server_url = server_url
self.auth_token = auth_token
self.headers = {'Authorization': auth_token}
def get_note(self, note_id):
response = requests.get(
f"{self.server_url}/etapi/notes/{note_id}",
headers=self.headers
)
return response.json()
def create_note(self, parent_note_id, title, content=""):
response = requests.post(
f"{self.server_url}/etapi/notes",
headers=self.headers,
json={
'parentNoteId': parent_note_id,
'title': title,
'type': 'text',
'content': content
}
)
return response.json()
def search_notes(self, query):
response = requests.get(
f"{self.server_url}/etapi/notes/search",
headers=self.headers,
params={'search': query}
)
return response.json()
```
## WebSocket Real-time Synchronization
**Location**: `/apps/server/src/services/ws.ts`
WebSocket connections provide real-time updates and synchronization between clients.
### Architecture
```typescript
// WebSocket message types
interface WSMessage {
type: string;
data: any;
}
// Common message types
type MessageType =
| 'entity-changes' // Entity updates
| 'sync' // Sync events
| 'note-content-change' // Content updates
| 'refresh-tree' // Tree structure changes
| 'options-changed' // Configuration updates
```
### Connection Management
```typescript
// Client connection
const ws = new WebSocket('wss://server/ws');
ws.on('open', () => {
// Authenticate
ws.send(JSON.stringify({
type: 'auth',
token: sessionToken
}));
});
ws.on('message', (data) => {
const message = JSON.parse(data);
handleWSMessage(message);
});
// Server-side handling
import WebSocket from 'ws';
const wss = new WebSocket.Server({ server });
wss.on('connection', (ws, req) => {
const session = parseSession(req);
if (!session.authenticated) {
ws.close(1008, 'Not authenticated');
return;
}
clients.add(ws);
ws.on('message', (message) => {
handleClientMessage(ws, message);
});
ws.on('close', () => {
clients.delete(ws);
});
});
```
### Message Broadcasting
```typescript
// Broadcast entity changes
function broadcastEntityChanges(changes: EntityChange[]) {
const message = {
type: 'entity-changes',
data: changes
};
for (const client of clients) {
if (client.readyState === WebSocket.OPEN) {
client.send(JSON.stringify(message));
}
}
}
// Targeted messages
function sendToClient(clientId: string, message: WSMessage) {
const client = clients.get(clientId);
if (client?.readyState === WebSocket.OPEN) {
client.send(JSON.stringify(message));
}
}
```
### Real-time Sync Protocol
```typescript
// Entity change notification
{
type: 'entity-changes',
data: [
{
entityName: 'notes',
entityId: 'noteId123',
action: 'update',
entity: { /* note data */ }
}
]
}
// Sync pull request
{
type: 'sync-pull',
data: {
lastSyncId: 12345
}
}
// Sync push
{
type: 'sync-push',
data: {
entities: [ /* changed entities */ ]
}
}
```
### Client-side Handling
```typescript
// Froca WebSocket integration
class WSClient {
constructor() {
this.ws = null;
this.reconnectTimeout = null;
this.connect();
}
connect() {
this.ws = new WebSocket(this.getWSUrl());
this.ws.onmessage = (event) => {
const message = JSON.parse(event.data);
this.handleMessage(message);
};
this.ws.onclose = () => {
// Reconnect with exponential backoff
this.scheduleReconnect();
};
}
handleMessage(message: WSMessage) {
switch (message.type) {
case 'entity-changes':
this.handleEntityChanges(message.data);
break;
case 'refresh-tree':
froca.loadInitialTree();
break;
case 'note-content-change':
this.handleContentChange(message.data);
break;
}
}
handleEntityChanges(changes: EntityChange[]) {
for (const change of changes) {
if (change.entityName === 'notes') {
froca.reloadNotes([change.entityId]);
}
}
}
}
```
## API Security
### Authentication Methods
```typescript
// 1. Session-based (Internal API)
app.use(session({
secret: config.sessionSecret,
resave: false,
saveUninitialized: false
}));
// 2. Token-based (ETAPI)
router.use('/etapi', (req, res, next) => {
const token = req.headers.authorization;
const etapiToken = becca.getEtapiToken(token);
if (!etapiToken || etapiToken.isExpired()) {
return res.status(401).json({
error: 'Invalid or expired token'
});
}
req.etapiToken = etapiToken;
next();
});
// 3. WebSocket authentication
ws.on('connection', (socket) => {
socket.on('auth', (token) => {
if (!validateToken(token)) {
socket.close(1008, 'Invalid token');
}
});
});
```
### Rate Limiting
```typescript
import rateLimit from 'express-rate-limit';
// Global rate limit
const globalLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 1000 // limit each IP to 1000 requests per windowMs
});
// Strict limit for authentication
const authLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 5,
message: 'Too many authentication attempts'
});
app.use('/api', globalLimiter);
app.use('/api/auth', authLimiter);
```
### Input Validation
```typescript
import { body, validationResult } from 'express-validator';
router.post('/api/notes',
body('title').isString().isLength({ min: 1, max: 1000 }),
body('type').isIn(['text', 'code', 'file', 'image']),
body('content').optional().isString(),
(req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({
errors: errors.array()
});
}
// Process valid input
}
);
```
## Performance Optimization
### Caching Strategies
```typescript
// Response caching
const cache = new Map();
router.get('/api/notes/:noteId', (req, res) => {
const cacheKey = `note:${req.params.noteId}`;
const cached = cache.get(cacheKey);
if (cached && cached.expires > Date.now()) {
return res.json(cached.data);
}
const note = becca.getNote(req.params.noteId);
const data = note.getPojo();
cache.set(cacheKey, {
data,
expires: Date.now() + 60000 // 1 minute
});
res.json(data);
});
```
### Batch Operations
```typescript
// Batch API endpoint
router.post('/api/batch', async (req, res) => {
const operations = req.body.operations;
const results = [];
await sql.transactional(async () => {
for (const op of operations) {
const result = await executeOperation(op);
results.push(result);
}
});
res.json({ results });
});
// Client batch usage
const batch = [
{ method: 'PUT', path: '/notes/1', body: { title: 'Note 1' }},
{ method: 'PUT', path: '/notes/2', body: { title: 'Note 2' }},
{ method: 'POST', path: '/notes/3/attributes', body: { type: 'label', name: 'todo' }}
];
await api.post('/batch', { operations: batch });
```
### Streaming Responses
```typescript
// Stream large data
router.get('/api/export', (req, res) => {
res.writeHead(200, {
'Content-Type': 'application/x-ndjson',
'Transfer-Encoding': 'chunked'
});
const noteStream = createNoteExportStream();
noteStream.on('data', (note) => {
res.write(JSON.stringify(note) + '\n');
});
noteStream.on('end', () => {
res.end();
});
});
```
## Error Handling
### Standard Error Responses
```typescript
// Error response format
interface ErrorResponse {
error: string;
code?: string;
details?: any;
}
// Error handling middleware
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
console.error('API Error:', err);
if (err instanceof NotFoundError) {
return res.status(404).json({
error: err.message,
code: 'NOT_FOUND'
});
}
if (err instanceof ValidationError) {
return res.status(400).json({
error: err.message,
code: 'VALIDATION_ERROR',
details: err.details
});
}
// Generic error
res.status(500).json({
error: 'Internal server error',
code: 'INTERNAL_ERROR'
});
});
```
## API Documentation
### OpenAPI/Swagger
```yaml
# etapi.openapi.yaml
openapi: 3.0.0
info:
title: Trilium ETAPI
version: 1.0.0
description: External API for Trilium Notes
paths:
/etapi/notes/{noteId}:
get:
summary: Get note by ID
parameters:
- name: noteId
in: path
required: true
schema:
type: string
responses:
200:
description: Note found
content:
application/json:
schema:
$ref: '#/components/schemas/Note'
404:
description: Note not found
components:
schemas:
Note:
type: object
properties:
noteId:
type: string
title:
type: string
type:
type: string
enum: [text, code, file, image]
```
### API Testing
```typescript
// API test example
describe('Notes API', () => {
it('should create a note', async () => {
const response = await request(app)
.post('/api/notes/root/children')
.send({
title: 'Test Note',
type: 'text',
content: 'Test content'
})
.expect(200);
expect(response.body).toHaveProperty('noteId');
expect(response.body.title).toBe('Test Note');
});
it('should handle errors', async () => {
const response = await request(app)
.get('/api/notes/invalid')
.expect(404);
expect(response.body).toHaveProperty('error');
});
});
```
## Best Practices
### API Design
1. **RESTful conventions**: Use appropriate HTTP methods and status codes
2. **Consistent naming**: Use camelCase for JSON properties
3. **Versioning**: Version the API to maintain compatibility
4. **Documentation**: Keep OpenAPI spec up to date
### Security
1. **Authentication**: Always verify user identity
2. **Authorization**: Check permissions for each operation
3. **Validation**: Validate all input data
4. **Rate limiting**: Prevent abuse with appropriate limits
### Performance
1. **Pagination**: Limit response sizes with pagination
2. **Caching**: Cache frequently accessed data
3. **Batch operations**: Support bulk operations
4. **Async processing**: Use queues for long-running tasks
## Related Documentation
- [Three-Layer Cache System](Three-Layer-Cache-System.md) - Cache architecture
- [Entity System](Entity-System.md) - Data model
- [ETAPI Reference](/apps/server/src/etapi/etapi.openapi.yaml) - OpenAPI specification

View File

@@ -0,0 +1,612 @@
# Entity System Architecture
The Entity System forms the core data model of Trilium Notes, providing a flexible and powerful structure for organizing information. This document details the entities, their relationships, and usage patterns.
## Core Entities Overview
```mermaid
erDiagram
Note ||--o{ Branch : "parent-child"
Note ||--o{ Attribute : "has"
Note ||--o{ Revision : "history"
Note ||--o{ Attachment : "contains"
Attachment ||--|| Blob : "stores in"
Revision ||--|| Blob : "stores in"
Note }o--o{ Note : "relates via Attribute"
Note {
string noteId PK
string title
string type
string content
boolean isProtected
string dateCreated
string dateModified
}
Branch {
string branchId PK
string noteId FK
string parentNoteId FK
integer notePosition
string prefix
boolean isExpanded
}
Attribute {
string attributeId PK
string noteId FK
string type "label or relation"
string name
string value
integer position
boolean isInheritable
}
Revision {
string revisionId PK
string noteId FK
string title
string type
boolean isProtected
string dateCreated
}
Attachment {
string attachmentId PK
string ownerId FK
string role
string mime
string title
string blobId FK
}
Blob {
string blobId PK
binary content
string dateModified
}
Option {
string name PK
string value
boolean isSynced
}
```
## Entity Definitions
### BNote - Notes with Content and Metadata
**Location**: `/apps/server/src/becca/entities/bnote.ts`
Notes are the fundamental unit of information in Trilium. Each note can contain different types of content and maintain relationships with other notes.
#### Properties
```typescript
class BNote {
noteId: string; // Unique identifier
title: string; // Display title
type: string; // Content type (text, code, file, etc.)
mime: string; // MIME type for content
isProtected: boolean; // Encryption flag
dateCreated: string; // Creation timestamp
dateModified: string; // Last modification
utcDateCreated: string; // UTC creation
utcDateModified: string; // UTC modification
// Relationships
parentBranches: BBranch[]; // Parent connections
children: BBranch[]; // Child connections
attributes: BAttribute[]; // Metadata
// Content
content?: string | Buffer; // Note content (lazy loaded)
// Computed
isDecrypted: boolean; // Decryption status
}
```
#### Note Types
- **text**: Rich text content with HTML formatting
- **code**: Source code with syntax highlighting
- **file**: Binary file attachment
- **image**: Image with preview capabilities
- **search**: Saved search query
- **book**: Container for hierarchical documentation
- **relationMap**: Visual relationship diagram
- **canvas**: Drawing canvas (Excalidraw)
- **mermaid**: Mermaid diagram
- **mindMap**: Mind mapping visualization
- **webView**: Embedded web content
- **noteMap**: Tree visualization
#### Usage Examples
```typescript
// Create a new note
const note = new BNote({
noteId: generateNoteId(),
title: "My Note",
type: "text",
mime: "text/html",
content: "<p>Note content</p>"
});
note.save();
// Get note with content
const note = becca.getNote(noteId);
await note.loadContent();
// Update note
note.title = "Updated Title";
note.save();
// Protect note
note.isProtected = true;
note.encrypt();
note.save();
```
### BBranch - Hierarchical Relationships
**Location**: `/apps/server/src/becca/entities/bbranch.ts`
Branches define the parent-child relationships between notes, allowing a note to have multiple parents (cloning).
#### Properties
```typescript
class BBranch {
branchId: string; // Unique identifier
noteId: string; // Child note ID
parentNoteId: string; // Parent note ID
notePosition: number; // Order among siblings
prefix: string; // Optional prefix label
isExpanded: boolean; // Tree UI state
// Computed
childNote: BNote; // Reference to child
parentNote: BNote; // Reference to parent
}
```
#### Key Features
- **Multiple Parents**: Notes can appear in multiple locations
- **Ordering**: Explicit positioning among siblings
- **Prefixes**: Optional labels for context (e.g., "Chapter 1:")
- **UI State**: Expansion state persisted per branch
#### Usage Examples
```typescript
// Create parent-child relationship
const branch = new BBranch({
noteId: childNote.noteId,
parentNoteId: parentNote.noteId,
notePosition: 10
});
branch.save();
// Clone note to another parent
const cloneBranch = childNote.cloneTo(otherParent.noteId);
// Reorder children
parentNote.sortChildren((a, b) =>
a.title.localeCompare(b.title)
);
// Add prefix
branch.prefix = "Important: ";
branch.save();
```
### BAttribute - Key-Value Metadata
**Location**: `/apps/server/src/becca/entities/battribute.ts`
Attributes provide flexible metadata and relationships between notes.
#### Types
1. **Labels**: Key-value pairs for metadata
2. **Relations**: References to other notes
#### Properties
```typescript
class BAttribute {
attributeId: string; // Unique identifier
noteId: string; // Owning note
type: 'label' | 'relation';
name: string; // Attribute name
value: string; // Value or target noteId
position: number; // Display order
isInheritable: boolean; // Inherited by children
// Computed
note: BNote; // Owner note
targetNote?: BNote; // For relations
}
```
#### Common Patterns
```typescript
// Add label
note.addLabel("status", "active");
note.addLabel("priority", "high");
// Add relation
note.addRelation("template", templateNoteId);
note.addRelation("renderNote", renderNoteId);
// Query by attributes
const todos = becca.findAttributes("label", "todoItem");
const templates = becca.findAttributes("label", "template");
// Inheritable attributes
note.addLabel("workspace", "project", true); // Children inherit
```
#### System Attributes
Special attributes with system behavior:
- `#hidePromotedAttributes`: Hide promoted attributes in UI
- `#readOnly`: Prevent note editing
- `#autoReadOnlyDisabled`: Disable auto read-only
- `#hideChildrenOverview`: Hide children count
- `~template`: Note template relation
- `~renderNote`: Custom rendering relation
### BRevision - Version History
**Location**: `/apps/server/src/becca/entities/brevision.ts`
Revisions provide version history and recovery capabilities.
#### Properties
```typescript
class BRevision {
revisionId: string; // Unique identifier
noteId: string; // Parent note
type: string; // Content type
mime: string; // MIME type
title: string; // Historical title
isProtected: boolean; // Encryption flag
dateCreated: string; // Creation time
utcDateCreated: string; // UTC time
dateModified: string; // Content modification
blobId: string; // Content storage
// Methods
getContent(): string | Buffer;
restore(): void;
}
```
#### Revision Strategy
- Created automatically on significant changes
- Configurable retention period
- Day/week/month/year retention rules
- Protected note revisions are encrypted
#### Usage Examples
```typescript
// Get note revisions
const revisions = note.getRevisions();
// Restore revision
const revision = becca.getRevision(revisionId);
revision.restore();
// Manual revision creation
note.saveRevision();
// Compare revisions
const diff = revision1.getContent() !== revision2.getContent();
```
### BOption - Application Configuration
**Location**: `/apps/server/src/becca/entities/boption.ts`
Options store application and user preferences.
#### Properties
```typescript
class BOption {
name: string; // Option key
value: string; // Option value
isSynced: boolean; // Sync across instances
utcDateModified: string; // Last change
}
```
#### Common Options
```typescript
// Theme settings
setOption("theme", "dark");
// Protected session timeout
setOption("protectedSessionTimeout", "600");
// Sync settings
setOption("syncServerHost", "https://sync.server");
// Note settings
setOption("defaultNoteType", "text");
```
### BAttachment - File Attachments
**Location**: `/apps/server/src/becca/entities/battachment.ts`
Attachments link binary content to notes.
#### Properties
```typescript
class BAttachment {
attachmentId: string; // Unique identifier
ownerId: string; // Parent note ID
role: string; // Attachment role
mime: string; // MIME type
title: string; // Display title
blobId: string; // Content reference
utcDateScheduledForDeletion: string;
// Methods
getContent(): Buffer;
getBlob(): BBlob;
}
```
#### Usage Patterns
```typescript
// Add attachment to note
const attachment = note.addAttachment({
role: "file",
mime: "application/pdf",
title: "document.pdf",
content: buffer
});
// Get attachments
const attachments = note.getAttachments();
// Download attachment
const content = attachment.getContent();
```
## Entity Relationships
### Parent-Child Hierarchy
```typescript
// Single parent
childNote.setParent(parentNote.noteId);
// Multiple parents (cloning)
childNote.cloneTo(parent1.noteId);
childNote.cloneTo(parent2.noteId);
// Get parents
const parents = childNote.getParentNotes();
// Get children
const children = parentNote.getChildNotes();
// Get subtree
const subtree = parentNote.getSubtreeNotes();
```
### Attribute Relationships
```typescript
// Direct relations
note.addRelation("author", authorNote.noteId);
// Bidirectional relations
note1.addRelation("related", note2.noteId);
note2.addRelation("related", note1.noteId);
// Get related notes
const related = note.getRelations("related");
// Get notes relating to this one
const targetRelations = note.getTargetRelations();
```
## Entity Lifecycle
### Creation
```typescript
// Note creation
const note = new BNote({
noteId: generateNoteId(),
title: "New Note",
type: "text"
});
note.save();
// With parent
const child = parentNote.addChild({
title: "Child Note",
type: "text",
content: "Content"
});
```
### Updates
```typescript
// Atomic updates
note.title = "New Title";
note.save();
// Batch updates
sql.transactional(() => {
note1.title = "Title 1";
note1.save();
note2.content = "Content 2";
note2.save();
});
```
### Deletion
```typescript
// Soft delete (move to trash)
note.deleteNote();
// Mark for deletion
note.isDeleted = true;
note.save();
// Permanent deletion (after grace period)
note.eraseNote();
```
## Performance Considerations
### Lazy Loading
```typescript
// Note content loaded on demand
const note = becca.getNote(noteId); // Metadata only
await note.loadContent(); // Load content when needed
// Revisions loaded on demand
const revisions = note.getRevisions(); // Database query
```
### Batch Operations
```typescript
// Efficient bulk loading
const notes = becca.getNotes(noteIds);
// Batch attribute queries
const attributes = sql.getRows(`
SELECT * FROM attributes
WHERE noteId IN (???)
AND name = ?
`, [noteIds, 'label']);
```
### Indexing
```typescript
// Attribute index for fast lookups
const labels = becca.findAttributes("label", "important");
// Branch index for relationship queries
const branch = becca.getBranchFromChildAndParent(childId, parentId);
```
## Best Practices
### Entity Creation
```typescript
// Always use transactions for multiple operations
sql.transactional(() => {
const note = new BNote({...});
note.save();
note.addLabel("status", "draft");
note.addRelation("template", templateId);
});
```
### Entity Updates
```typescript
// Check existence before update
const note = becca.getNote(noteId);
if (note) {
note.title = "Updated";
note.save();
}
// Use proper error handling
try {
const note = becca.getNoteOrThrow(noteId);
note.save();
} catch (e) {
log.error(`Note ${noteId} not found`);
}
```
### Querying
```typescript
// Use indexed queries
const attrs = becca.findAttributes("label", "task");
// Avoid N+1 queries
const noteIds = [...];
const notes = becca.getNotes(noteIds); // Single batch
// Use SQL for complex queries
const results = sql.getRows(`
SELECT n.noteId, n.title, COUNT(b.branchId) as childCount
FROM notes n
LEFT JOIN branches b ON b.parentNoteId = n.noteId
GROUP BY n.noteId
`);
```
## Troubleshooting
### Common Issues
1. **Circular References**
```typescript
// Detect cycles before creating branches
if (!parentNote.hasAncestor(childNote.noteId)) {
childNote.setParent(parentNote.noteId);
}
```
2. **Orphaned Entities**
```typescript
// Find orphaned notes
const orphans = sql.getRows(`
SELECT noteId FROM notes
WHERE noteId != 'root'
AND noteId NOT IN (SELECT noteId FROM branches)
`);
```
3. **Attribute Conflicts**
```typescript
// Handle duplicate attributes
const existing = note.getAttribute("label", "status");
if (existing) {
existing.value = "new value";
existing.save();
} else {
note.addLabel("status", "new value");
}
```
## Related Documentation
- [Three-Layer Cache System](Three-Layer-Cache-System.md) - Cache architecture
- [Database Schema](../Development%20and%20architecture/Database/notes.md) - Database structure
- [Script API](../../Script%20API/) - Entity API for scripts

View File

@@ -0,0 +1,610 @@
# Monorepo Structure
Trilium is organized as a TypeScript monorepo using NX, facilitating code sharing, consistent tooling, and efficient build processes. This document provides a comprehensive overview of the project structure, build system, and development workflow.
## Project Organization
```
TriliumNext/Trilium/
├── apps/ # Runnable applications
│ ├── client/ # Frontend web application
│ ├── server/ # Node.js backend server
│ ├── desktop/ # Electron desktop application
│ ├── web-clipper/ # Browser extension
│ ├── db-compare/ # Database comparison tool
│ ├── dump-db/ # Database dump utility
│ └── edit-docs/ # Documentation editor
├── packages/ # Shared libraries
│ ├── commons/ # Shared interfaces and utilities
│ ├── ckeditor5/ # Rich text editor
│ ├── codemirror/ # Code editor
│ ├── highlightjs/ # Syntax highlighting
│ ├── ckeditor5-admonition/ # CKEditor plugin
│ ├── ckeditor5-footnotes/ # CKEditor plugin
│ ├── ckeditor5-math/ # CKEditor plugin
│ └── ckeditor5-mermaid/ # CKEditor plugin
├── docs/ # Documentation
├── nx.json # NX workspace configuration
├── package.json # Root package configuration
├── pnpm-workspace.yaml # PNPM workspace configuration
└── tsconfig.base.json # Base TypeScript configuration
```
## Applications
### Client (`/apps/client`)
The frontend application shared by both server and desktop versions.
```
apps/client/
├── src/
│ ├── components/ # Core UI components
│ ├── entities/ # Frontend entities (FNote, FBranch, etc.)
│ ├── services/ # Business logic and API calls
│ ├── widgets/ # UI widgets system
│ │ ├── type_widgets/ # Note type specific widgets
│ │ ├── dialogs/ # Dialog components
│ │ └── panels/ # Panel widgets
│ ├── public/
│ │ ├── fonts/ # Font assets
│ │ ├── images/ # Image assets
│ │ └── libraries/ # Third-party libraries
│ └── desktop.ts # Desktop entry point
├── package.json
├── project.json # NX project configuration
└── vite.config.ts # Vite build configuration
```
#### Key Files
- `desktop.ts` - Main application initialization
- `services/froca.ts` - Frontend cache implementation
- `widgets/basic_widget.ts` - Base widget class
- `services/server.ts` - API communication layer
### Server (`/apps/server`)
The Node.js backend providing API, database, and business logic.
```
apps/server/
├── src/
│ ├── becca/ # Backend cache system
│ │ ├── entities/ # Core entities (BNote, BBranch, etc.)
│ │ └── becca.ts # Cache interface
│ ├── routes/ # Express routes
│ │ ├── api/ # Internal API endpoints
│ │ └── pages/ # HTML page routes
│ ├── etapi/ # External API
│ ├── services/ # Business services
│ ├── share/ # Note sharing functionality
│ │ └── shaca/ # Share cache
│ ├── migrations/ # Database migrations
│ ├── assets/
│ │ ├── db/ # Database schema
│ │ └── doc_notes/ # Documentation notes
│ └── main.ts # Server entry point
├── package.json
├── project.json
└── webpack.config.js # Webpack configuration
```
#### Key Services
- `services/sql.ts` - Database access layer
- `services/sync.ts` - Synchronization logic
- `services/ws.ts` - WebSocket server
- `services/protected_session.ts` - Encryption handling
### Desktop (`/apps/desktop`)
Electron wrapper for the desktop application.
```
apps/desktop/
├── src/
│ ├── main.ts # Electron main process
│ ├── preload.ts # Preload script
│ ├── services/ # Desktop-specific services
│ └── utils/ # Desktop utilities
├── resources/ # Desktop resources (icons, etc.)
├── package.json
└── electron-builder.yml # Electron Builder configuration
```
### Web Clipper (`/apps/web-clipper`)
Browser extension for saving web content to Trilium.
```
apps/web-clipper/
├── src/
│ ├── background.js # Background script
│ ├── content.js # Content script
│ ├── popup/ # Extension popup
│ └── options/ # Extension options
├── manifest.json # Extension manifest
└── package.json
```
## Packages
### Commons (`/packages/commons`)
Shared TypeScript interfaces and utilities used across applications.
```typescript
// packages/commons/src/types.ts
export interface NoteRow {
noteId: string;
title: string;
type: string;
mime: string;
isProtected: boolean;
dateCreated: string;
dateModified: string;
}
export interface BranchRow {
branchId: string;
noteId: string;
parentNoteId: string;
notePosition: number;
prefix: string;
isExpanded: boolean;
}
```
### CKEditor5 (`/packages/ckeditor5`)
Custom CKEditor5 build with Trilium-specific plugins.
```
packages/ckeditor5/
├── src/
│ ├── ckeditor.ts # Editor configuration
│ ├── plugins.ts # Plugin registration
│ └── trilium/ # Custom plugins
├── theme/ # Editor themes
└── package.json
```
#### Custom Plugins
- **Admonition**: Note boxes with icons
- **Footnotes**: Reference footnotes
- **Math**: LaTeX equation rendering
- **Mermaid**: Diagram integration
### CodeMirror (`/packages/codemirror`)
Code editor customizations for the code note type.
```typescript
// packages/codemirror/src/index.ts
export function createCodeMirror(element: HTMLElement, options: CodeMirrorOptions) {
return CodeMirror(element, {
...defaultOptions,
...options,
// Trilium-specific customizations
});
}
```
## Build System
### NX Configuration
**`nx.json`**
```json
{
"tasksRunnerOptions": {
"default": {
"runner": "nx/tasks-runners/default",
"options": {
"cacheableOperations": ["build", "test", "lint"],
"parallel": 3
}
}
},
"targetDefaults": {
"build": {
"dependsOn": ["^build"],
"cache": true
},
"test": {
"cache": true
}
}
}
```
### Project Configuration
Each application and package has a `project.json` defining its targets:
```json
{
"name": "server",
"targets": {
"build": {
"executor": "@nx/webpack:webpack",
"options": {
"outputPath": "dist/apps/server",
"main": "apps/server/src/main.ts",
"tsConfig": "apps/server/tsconfig.app.json"
}
},
"serve": {
"executor": "@nx/node:node",
"options": {
"buildTarget": "server:build"
}
},
"test": {
"executor": "@nx/jest:jest",
"options": {
"jestConfig": "apps/server/jest.config.ts"
}
}
}
}
```
### Build Commands
```bash
# Build specific project
pnpm nx build server
pnpm nx build client
# Build all projects
pnpm nx run-many --target=build --all
# Build with dependencies
pnpm nx build server --with-deps
# Production build
pnpm nx build server --configuration=production
```
## Development Workflow
### Initial Setup
```bash
# Install dependencies
pnpm install
# Enable corepack for pnpm
corepack enable
# Build all packages
pnpm nx run-many --target=build --all
```
### Development Commands
```bash
# Start development server
pnpm run server:start
# or
pnpm nx run server:serve
# Start desktop app
pnpm nx run desktop:serve
# Run client dev server
pnpm nx run client:serve
# Watch mode for packages
pnpm nx run commons:build --watch
```
### Testing
```bash
# Run all tests
pnpm test:all
# Run tests for specific project
pnpm nx test server
pnpm nx test client
# Run tests in watch mode
pnpm nx test server --watch
# Generate coverage
pnpm nx test server --coverage
```
### Linting and Type Checking
```bash
# Lint specific project
pnpm nx lint server
# Type check
pnpm nx run server:typecheck
# Lint all projects
pnpm nx run-many --target=lint --all
# Fix lint issues
pnpm nx lint server --fix
```
## Dependency Management
### Package Dependencies
Dependencies are managed at both root and project levels:
```json
// Root package.json - shared dev dependencies
{
"devDependencies": {
"@nx/workspace": "^17.0.0",
"typescript": "^5.0.0",
"eslint": "^8.0.0"
}
}
// Project package.json - project-specific dependencies
{
"dependencies": {
"express": "^4.18.0",
"better-sqlite3": "^9.0.0"
}
}
```
### Adding Dependencies
```bash
# Add to root
pnpm add -D typescript
# Add to specific project
pnpm add express --filter server
# Add to multiple projects
pnpm add lodash --filter server --filter client
```
### Workspace References
Internal packages are referenced using workspace protocol:
```json
{
"dependencies": {
"@triliumnext/commons": "workspace:*"
}
}
```
## TypeScript Configuration
### Base Configuration
**`tsconfig.base.json`**
```json
{
"compilerOptions": {
"rootDir": ".",
"sourceMap": true,
"declaration": false,
"moduleResolution": "node",
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"importHelpers": true,
"target": "ES2022",
"module": "ESNext",
"lib": ["ES2022", "dom"],
"skipLibCheck": true,
"skipDefaultLibCheck": true,
"baseUrl": ".",
"paths": {
"@triliumnext/commons": ["packages/commons/src/index.ts"]
}
}
}
```
### Project-Specific Configuration
```json
// apps/server/tsconfig.json
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"module": "commonjs",
"types": ["node"]
},
"include": ["src/**/*"],
"exclude": ["**/*.spec.ts"]
}
```
## Build Optimization
### NX Cloud
```bash
# Enable NX Cloud for distributed caching
pnpm nx connect-to-nx-cloud
```
### Affected Commands
```bash
# Build only affected projects
pnpm nx affected:build --base=main
# Test only affected projects
pnpm nx affected:test --base=main
# Lint only affected projects
pnpm nx affected:lint --base=main
```
### Build Caching
NX caches build outputs to speed up subsequent builds:
```bash
# Clear cache
pnpm nx reset
# Run with cache disabled
pnpm nx build server --skip-nx-cache
# See cache statistics
pnpm nx report
```
## Production Builds
### Building for Production
```bash
# Build server for production
pnpm nx build server --configuration=production
# Build client with optimization
pnpm nx build client --configuration=production
# Build desktop app
pnpm nx build desktop --configuration=production
pnpm electron:build # Creates distributables
```
### Docker Build
```dockerfile
# Multi-stage build
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json pnpm-lock.yaml ./
RUN corepack enable && pnpm install --frozen-lockfile
COPY . .
RUN pnpm nx build server --configuration=production
FROM node:20-alpine
WORKDIR /app
COPY --from=builder /app/dist/apps/server ./
COPY --from=builder /app/node_modules ./node_modules
CMD ["node", "main.js"]
```
## Continuous Integration
### GitHub Actions Example
```yaml
name: CI
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: pnpm/action-setup@v2
with:
version: 8
- uses: actions/setup-node@v3
with:
node-version: 20
cache: 'pnpm'
- run: pnpm install --frozen-lockfile
- run: pnpm nx affected:lint --base=origin/main
- run: pnpm nx affected:test --base=origin/main
- run: pnpm nx affected:build --base=origin/main
```
## Troubleshooting
### Common Issues
1. **Build Cache Issues**
```bash
# Clear NX cache
pnpm nx reset
# Clear node_modules and reinstall
rm -rf node_modules
pnpm install
```
2. **Dependency Version Conflicts**
```bash
# Check for duplicate packages
pnpm list --depth=0
# Update all dependencies
pnpm update --recursive
```
3. **TypeScript Path Resolution**
```bash
# Verify TypeScript paths
pnpm nx run server:typecheck --traceResolution
```
### Debug Commands
```bash
# Show project graph
pnpm nx graph
# Show project dependencies
pnpm nx print-affected --type=app --select=projects
# Verbose output
pnpm nx build server --verbose
# Profile build performance
pnpm nx build server --profile
```
## Best Practices
### Project Structure
1. **Keep packages focused**: Each package should have a single, clear purpose
2. **Minimize circular dependencies**: Use dependency graph to identify issues
3. **Share common code**: Extract shared logic to packages/commons
### Development
1. **Use NX generators**: Generate consistent code structure
2. **Leverage caching**: Don't skip-nx-cache unless debugging
3. **Run affected commands**: Save time by only building/testing changed code
### Testing
1. **Colocate tests**: Keep test files next to source files
2. **Use workspace scripts**: Define common scripts in root package.json
3. **Parallel execution**: Use `--parallel` flag for faster execution
## Related Documentation
- [Environment Setup](../Environment%20Setup.md) - Development environment setup
- [Project Structure](../Project%20Structure.md) - Detailed project structure
- [Build Information](../Development%20and%20architecture/Build%20information.md) - Build details

View File

@@ -0,0 +1,89 @@
# Trilium Architecture Documentation
This comprehensive guide documents the architecture of Trilium Notes, providing developers with detailed information about the system's core components, data flow, and design patterns.
## Table of Contents
1. [Three-Layer Cache System](Three-Layer-Cache-System.md)
2. [Entity System](Entity-System.md)
3. [Widget-Based UI Architecture](Widget-Based-UI-Architecture.md)
4. [API Architecture](API-Architecture.md)
5. [Monorepo Structure](Monorepo-Structure.md)
## Overview
Trilium Notes is built as a TypeScript monorepo using NX, featuring a sophisticated architecture that balances performance, flexibility, and maintainability. The system is designed around several key architectural patterns:
- **Three-layer caching system** for optimal performance across backend, frontend, and shared content
- **Entity-based data model** supporting hierarchical note structures with multiple parent relationships
- **Widget-based UI architecture** enabling modular and extensible interface components
- **Multiple API layers** for internal operations, external integrations, and real-time synchronization
- **Monorepo structure** facilitating code sharing and consistent development patterns
## Quick Start for Developers
If you're new to Trilium development, start with these sections:
1. [Monorepo Structure](Monorepo-Structure.md) - Understand the project organization
2. [Entity System](Entity-System.md) - Learn about the core data model
3. [Three-Layer Cache System](Three-Layer-Cache-System.md) - Understand data flow and caching
For UI development, refer to:
- [Widget-Based UI Architecture](Widget-Based-UI-Architecture.md)
For API integration, see:
- [API Architecture](API-Architecture.md)
## Architecture Principles
### Performance First
- Lazy loading of note content
- Efficient caching at multiple layers
- Optimized database queries with prepared statements
### Flexibility
- Support for multiple note types
- Extensible through scripting
- Plugin architecture for UI widgets
### Data Integrity
- Transactional database operations
- Revision history for all changes
- Synchronization conflict resolution
### Security
- Per-note encryption
- Protected sessions
- API authentication tokens
## Development Workflow
1. **Setup Development Environment**
```bash
pnpm install
pnpm run server:start
```
2. **Make Changes**
- Backend changes in `apps/server/src/`
- Frontend changes in `apps/client/src/`
- Shared code in `packages/`
3. **Test Your Changes**
```bash
pnpm test:all
pnpm nx run <project>:lint
```
4. **Build for Production**
```bash
pnpm nx build server
pnpm nx build client
```
## Further Reading
- [Development Environment Setup](../Environment%20Setup.md)
- [Adding a New Note Type](../Development%20and%20architecture/Adding%20a%20new%20note%20type/First%20steps.md)
- [Database Schema](../Development%20and%20architecture/Database/notes.md)
- [Script API Documentation](../../Script%20API/)

View File

@@ -0,0 +1,369 @@
# Three-Layer Cache System Architecture
Trilium implements a sophisticated three-layer caching system to optimize performance and reduce database load. This architecture ensures fast access to frequently used data while maintaining consistency across different application contexts.
## Overview
The three cache layers are:
1. **Becca** (Backend Cache) - Server-side entity cache
2. **Froca** (Frontend Cache) - Client-side mirror of backend data
3. **Shaca** (Share Cache) - Optimized cache for shared/published notes
```mermaid
graph TB
subgraph "Database Layer"
DB[(SQLite Database)]
end
subgraph "Backend Layer"
Becca[Becca Cache<br/>Backend Cache]
API[API Layer]
end
subgraph "Frontend Layer"
Froca[Froca Cache<br/>Frontend Cache]
UI[UI Components]
end
subgraph "Share Layer"
Shaca[Shaca Cache<br/>Share Cache]
Share[Public Share Interface]
end
DB <--> Becca
Becca <--> API
API <--> Froca
Froca <--> UI
DB <--> Shaca
Shaca <--> Share
style Becca fill:#e1f5fe
style Froca fill:#fff3e0
style Shaca fill:#f3e5f5
```
## Becca (Backend Cache)
**Location**: `/apps/server/src/becca/`
Becca is the authoritative cache layer that maintains all notes, branches, attributes, and options in server memory.
### Key Components
#### Becca Interface (`becca-interface.ts`)
```typescript
export default class Becca {
loaded: boolean;
notes: Record<string, BNote>;
branches: Record<string, BBranch>;
childParentToBranch: Record<string, BBranch>;
attributes: Record<string, BAttribute>;
attributeIndex: Record<string, BAttribute[]>;
options: Record<string, BOption>;
etapiTokens: Record<string, BEtapiToken>;
allNoteSetCache: NoteSet | null;
}
```
### Features
- **In-memory storage**: All active entities are kept in memory for fast access
- **Lazy loading**: Related entities (revisions, attachments) loaded on demand
- **Index structures**: Optimized lookups via `childParentToBranch` and `attributeIndex`
- **Cache invalidation**: Automatic cache updates on entity changes
- **Protected note decryption**: On-demand decryption of encrypted content
### Usage Example
```typescript
import becca from "./becca/becca.js";
// Get a note
const note = becca.getNote("noteId");
// Find attributes by type and name
const labels = becca.findAttributes("label", "todoItem");
// Get branch relationships
const branch = becca.getBranchFromChildAndParent(childId, parentId);
```
### Data Flow
1. **Initialization**: Load all notes, branches, and attributes from database
2. **Access**: Direct memory access for cached entities
3. **Updates**: Write-through cache with immediate database persistence
4. **Invalidation**: Automatic cache refresh on entity changes
## Froca (Frontend Cache)
**Location**: `/apps/client/src/services/froca.ts`
Froca is the frontend mirror of Becca, maintaining a subset of backend data for client-side operations.
### Key Components
#### Froca Implementation (`froca.ts`)
```typescript
class FrocaImpl implements Froca {
notes: Record<string, FNote>;
branches: Record<string, FBranch>;
attributes: Record<string, FAttribute>;
attachments: Record<string, FAttachment>;
blobPromises: Record<string, Promise<FBlob | null> | null>;
}
```
### Features
- **Lazy loading**: Notes loaded on-demand with their immediate context
- **Subtree loading**: Efficient loading of note hierarchies
- **Real-time updates**: WebSocket synchronization with backend changes
- **Search note support**: Virtual branches for search results
- **Promise-based blob loading**: Asynchronous content loading
### Loading Strategy
```typescript
// Initial load - loads root and immediate children
await froca.loadInitialTree();
// Load subtree on demand
const note = await froca.loadSubTree(noteId);
// Reload specific notes
await froca.reloadNotes([noteId1, noteId2]);
```
### Synchronization
Froca maintains consistency with Becca through:
1. **Initial sync**: Load essential tree structure on startup
2. **On-demand loading**: Fetch notes as needed
3. **WebSocket updates**: Real-time push of changes from backend
4. **Batch reloading**: Efficient refresh of multiple notes
## Shaca (Share Cache)
**Location**: `/apps/server/src/share/shaca/`
Shaca is a specialized cache for publicly shared notes, optimized for read-only access.
### Key Components
#### Shaca Interface (`shaca-interface.ts`)
```typescript
export default class Shaca {
notes: Record<string, SNote>;
branches: Record<string, SBranch>;
childParentToBranch: Record<string, SBranch>;
attributes: Record<string, SAttribute>;
attachments: Record<string, SAttachment>;
aliasToNote: Record<string, SNote>;
shareRootNote: SNote | null;
shareIndexEnabled: boolean;
}
```
### Features
- **Read-only optimization**: Streamlined for public access
- **Alias support**: URL-friendly note access via aliases
- **Share index**: Optional indexing of all shared subtrees
- **Minimal memory footprint**: Only shared content cached
- **Security isolation**: Separate from main application cache
### Usage Patterns
```typescript
// Get shared note by ID
const note = shaca.getNote(noteId);
// Access via alias
const aliasedNote = shaca.aliasToNote[alias];
// Check if note is shared
const isShared = shaca.hasNote(noteId);
```
## Cache Interaction and Data Flow
### 1. Create/Update Flow
```mermaid
sequenceDiagram
participant Client
participant Froca
participant API
participant Becca
participant DB
Client->>API: Update Note
API->>Becca: Update Cache
Becca->>DB: Persist Change
Becca->>API: Confirm
API->>Froca: Push Update (WebSocket)
Froca->>Client: Update UI
```
### 2. Read Flow
```mermaid
sequenceDiagram
participant Client
participant Froca
participant API
participant Becca
Client->>Froca: Request Note
alt Note in Cache
Froca->>Client: Return Cached Note
else Note not in Cache
Froca->>API: Fetch Note
API->>Becca: Get Note
Becca->>API: Return Note
API->>Froca: Send Note Data
Froca->>Froca: Cache Note
Froca->>Client: Return Note
end
```
### 3. Share Access Flow
```mermaid
sequenceDiagram
participant Browser
participant ShareUI
participant Shaca
participant DB
Browser->>ShareUI: Access Shared URL
ShareUI->>Shaca: Get Shared Note
alt Note in Cache
Shaca->>ShareUI: Return Cached
else Not in Cache
Shaca->>DB: Load Shared Tree
DB->>Shaca: Return Data
Shaca->>Shaca: Build Cache
Shaca->>ShareUI: Return Note
end
ShareUI->>Browser: Render Content
```
## Performance Considerations
### Memory Management
- **Becca**: Keeps entire note tree in memory (~100-500MB for typical use)
- **Froca**: Loads notes on-demand, automatic cleanup of unused notes
- **Shaca**: Minimal footprint, only shared content
### Cache Warming
- **Becca**: Full load on server startup
- **Froca**: Progressive loading based on user navigation
- **Shaca**: Lazy loading with configurable index
### Optimization Strategies
1. **Attribute Indexing**: Pre-built indexes for fast attribute queries
2. **Batch Operations**: Group updates to minimize round trips
3. **Partial Loading**: Load only required fields for lists
4. **WebSocket Compression**: Compressed real-time updates
## Best Practices
### When to Use Each Cache
**Use Becca when**:
- Implementing server-side business logic
- Performing bulk operations
- Handling synchronization
- Managing protected notes
**Use Froca when**:
- Building UI components
- Handling user interactions
- Displaying note content
- Managing client state
**Use Shaca when**:
- Serving public content
- Building share pages
- Implementing read-only access
- Creating public APIs
### Cache Invalidation
```typescript
// Becca - automatic on entity save
note.save(); // Cache updated automatically
// Froca - manual reload when needed
await froca.reloadNotes([noteId]);
// Shaca - rebuild on share changes
shaca.reset();
shaca.load();
```
### Error Handling
```typescript
// Becca - throw on missing required entities
const note = becca.getNoteOrThrow(noteId); // throws NotFoundError
// Froca - graceful degradation
const note = await froca.getNote(noteId);
if (!note) {
// Handle missing note
}
// Shaca - check existence first
if (shaca.hasNote(noteId)) {
const note = shaca.getNote(noteId);
}
```
## Troubleshooting
### Common Issues
1. **Cache Inconsistency**
- Symptom: UI shows outdated data
- Solution: Force reload with `froca.reloadNotes()`
2. **Memory Growth**
- Symptom: Server memory usage increases
- Solution: Check for memory leaks in custom scripts
3. **Slow Initial Load**
- Symptom: Long startup time
- Solution: Optimize database queries, add indexes
### Debug Commands
```javascript
// Check cache sizes
console.log('Becca notes:', Object.keys(becca.notes).length);
console.log('Froca notes:', Object.keys(froca.notes).length);
console.log('Shaca notes:', Object.keys(shaca.notes).length);
// Force cache refresh
await froca.loadInitialTree();
// Clear and reload Shaca
shaca.reset();
await shaca.load();
```
## Related Documentation
- [Entity System](Entity-System.md) - Detailed entity documentation
- [Database Schema](../Development%20and%20architecture/Database/notes.md) - Database structure
- [WebSocket Synchronization](API-Architecture.md#websocket-real-time-synchronization) - Real-time updates

View File

@@ -0,0 +1,635 @@
# Widget-Based UI Architecture
Trilium's frontend is built on a modular widget system that provides flexibility, reusability, and maintainability. This architecture enables dynamic UI composition and extensibility through custom widgets.
## Widget System Overview
```mermaid
graph TB
subgraph "Widget Hierarchy"
Component[Component<br/>Base Class]
BasicWidget[BasicWidget<br/>UI Foundation]
NoteContextAware[NoteContextAwareWidget<br/>Note-Aware]
RightPanel[RightPanelWidget<br/>Side Panel]
TypeWidgets[Type Widgets<br/>Note Type Specific]
CustomWidgets[Custom Widgets<br/>User Scripts]
end
Component --> BasicWidget
BasicWidget --> NoteContextAware
NoteContextAware --> RightPanel
NoteContextAware --> TypeWidgets
NoteContextAware --> CustomWidgets
style Component fill:#e8f5e9
style BasicWidget fill:#c8e6c9
style NoteContextAware fill:#a5d6a7
```
## Core Widget Classes
### Component (Base Class)
**Location**: `/apps/client/src/components/component.js`
The foundational class for all UI components in Trilium.
```typescript
class Component {
componentId: string; // Unique identifier
children: Component[]; // Child components
parent: Component | null; // Parent reference
async refresh(): Promise<void>;
child(...components: Component[]): this;
handleEvent(name: string, data: any): void;
trigger(name: string, data?: any): void;
}
```
### BasicWidget
**Location**: `/apps/client/src/widgets/basic_widget.ts`
Base class for all UI widgets, providing DOM manipulation and styling capabilities.
```typescript
export class BasicWidget extends Component {
protected $widget: JQuery;
private attrs: Record<string, string>;
private classes: string[];
// Chaining methods for declarative UI
id(id: string): this;
class(className: string): this;
css(name: string, value: string): this;
contentSized(): this;
collapsible(): this;
filling(): this;
// Conditional rendering
optChild(condition: boolean, ...components: Component[]): this;
optCss(condition: boolean, name: string, value: string): this;
// Rendering
doRender(): JQuery;
}
```
#### Usage Example
```typescript
class MyWidget extends BasicWidget {
doRender() {
this.$widget = $('<div>')
.addClass('my-widget')
.append($('<h3>').text('Widget Title'));
return this.$widget;
}
async refreshWithNote(note: FNote) {
this.$widget.find('h3').text(note.title);
}
}
// Composing widgets
const container = new FlexContainer('column')
.id('main-container')
.css('padding', '10px')
.filling()
.child(
new MyWidget(),
new ButtonWidget()
.title('Click Me')
.onClick(() => console.log('Clicked'))
);
```
### NoteContextAwareWidget
**Location**: `/apps/client/src/widgets/note_context_aware_widget.ts`
Base class for widgets that respond to note context changes.
```typescript
class NoteContextAwareWidget extends BasicWidget {
noteContext: NoteContext | null;
note: FNote | null;
noteId: string | null;
notePath: string | null;
// Lifecycle methods
async refresh(): Promise<void>;
async refreshWithNote(note: FNote): Promise<void>;
async noteSwitched(): Promise<void>;
async activeContextChanged(): Promise<void>;
// Event handlers
async noteTypeMimeChanged(): Promise<void>;
async frocaReloaded(): Promise<void>;
// Utility methods
isNote(noteId: string): boolean;
get isEnabled(): boolean;
}
```
#### Context Management
```typescript
class MyNoteWidget extends NoteContextAwareWidget {
async refreshWithNote(note: FNote) {
// Called when note context changes
this.$widget.find('.note-title').text(note.title);
this.$widget.find('.note-type').text(note.type);
// Access note attributes
const labels = note.getLabels();
const relations = note.getRelations();
}
async noteSwitched() {
// Called when user switches to different note
console.log(`Switched to note: ${this.noteId}`);
}
async noteTypeMimeChanged() {
// React to note type changes
if (this.note?.type === 'code') {
this.setupCodeHighlighting();
}
}
}
```
### RightPanelWidget
**Location**: `/apps/client/src/widgets/right_panel_widget.ts`
Base class for widgets displayed in the right sidebar panel.
```typescript
abstract class RightPanelWidget extends NoteContextAwareWidget {
async doRenderBody(): Promise<JQuery>;
getTitle(): string;
getIcon(): string;
getPosition(): number;
async isEnabled(): Promise<boolean> {
// Override to control visibility
return true;
}
}
```
#### Creating Right Panel Widgets
```typescript
class InfoWidget extends RightPanelWidget {
getTitle() { return "Note Info"; }
getIcon() { return "info"; }
getPosition() { return 100; }
async doRenderBody() {
return $('<div class="info-widget">')
.append($('<div class="created">'))
.append($('<div class="modified">'))
.append($('<div class="word-count">'));
}
async refreshWithNote(note: FNote) {
this.$body.find('.created').text(`Created: ${note.dateCreated}`);
this.$body.find('.modified').text(`Modified: ${note.dateModified}`);
const wordCount = this.calculateWordCount(await note.getContent());
this.$body.find('.word-count').text(`Words: ${wordCount}`);
}
}
```
## Type-Specific Widgets
**Location**: `/apps/client/src/widgets/type_widgets/`
Each note type has a specialized widget for rendering and editing.
### TypeWidget Interface
```typescript
abstract class TypeWidget extends NoteContextAwareWidget {
abstract static getType(): string;
// Content management
async getContent(): Promise<string>;
async saveContent(content: string): Promise<void>;
// Focus management
async focus(): Promise<void>;
async blur(): Promise<void>;
// Cleanup
async cleanup(): Promise<void>;
}
```
### Common Type Widgets
#### TextTypeWidget
```typescript
class TextTypeWidget extends TypeWidget {
static getType() { return 'text'; }
private textEditor: TextEditor;
async doRender() {
const $editor = $('<div class="ck-editor">');
this.textEditor = await TextEditor.create($editor[0], {
noteId: this.noteId,
content: await this.note.getContent()
});
return $editor;
}
async getContent() {
return this.textEditor.getData();
}
}
```
#### CodeTypeWidget
```typescript
class CodeTypeWidget extends TypeWidget {
static getType() { return 'code'; }
private codeMirror: CodeMirror;
async doRender() {
const $container = $('<div class="code-editor">');
this.codeMirror = CodeMirror($container[0], {
value: await this.note.getContent(),
mode: this.note.mime,
theme: 'default',
lineNumbers: true
});
return $container;
}
}
```
## Widget Composition
### Container Widgets
```typescript
// Flexible container layouts
class FlexContainer extends BasicWidget {
constructor(private direction: 'row' | 'column') {
super();
}
doRender() {
this.$widget = $('<div class="flex-container">')
.css('display', 'flex')
.css('flex-direction', this.direction);
for (const child of this.children) {
this.$widget.append(child.render());
}
return this.$widget;
}
}
// Tab container
class TabContainer extends BasicWidget {
private tabs: Array<{title: string, widget: BasicWidget}> = [];
addTab(title: string, widget: BasicWidget) {
this.tabs.push({title, widget});
this.child(widget);
return this;
}
doRender() {
// Render tab headers and content panels
}
}
```
### Composite Widgets
```typescript
class NoteEditorWidget extends NoteContextAwareWidget {
private typeWidget: TypeWidget;
private titleWidget: NoteTitleWidget;
private toolbarWidget: NoteToolbarWidget;
constructor() {
super();
this.child(
this.toolbarWidget = new NoteToolbarWidget(),
this.titleWidget = new NoteTitleWidget(),
// Type widget added dynamically
);
}
async refreshWithNote(note: FNote) {
// Remove old type widget
if (this.typeWidget) {
this.typeWidget.remove();
}
// Add appropriate type widget
const WidgetClass = typeWidgetService.getWidgetClass(note.type);
this.typeWidget = new WidgetClass();
this.child(this.typeWidget);
await this.typeWidget.refresh();
}
}
```
## Widget Communication
### Event System
```typescript
// Publishing events
class PublisherWidget extends BasicWidget {
async handleClick() {
// Local event
this.trigger('itemSelected', { itemId: '123' });
// Global event
appContext.triggerEvent('noteChanged', { noteId: this.noteId });
}
}
// Subscribing to events
class SubscriberWidget extends BasicWidget {
constructor() {
super();
// Local event subscription
this.on('itemSelected', (event) => {
console.log('Item selected:', event.itemId);
});
// Global event subscription
appContext.addEventListener('noteChanged', (event) => {
this.handleNoteChange(event.noteId);
});
}
}
```
### Command System
```typescript
// Registering commands
class CommandWidget extends BasicWidget {
constructor() {
super();
this.bindCommand('saveNote', () => this.saveNote());
this.bindCommand('deleteNote', () => this.deleteNote());
}
getCommands() {
return [
{
command: 'myWidget:doAction',
handler: () => this.doAction(),
hotkey: 'ctrl+shift+a'
}
];
}
}
```
## Custom Widget Development
### Creating Custom Widgets
```typescript
// 1. Define widget class
class TaskListWidget extends NoteContextAwareWidget {
doRender() {
this.$widget = $('<div class="task-list-widget">');
this.$list = $('<ul>').appendTo(this.$widget);
return this.$widget;
}
async refreshWithNote(note: FNote) {
const tasks = await this.loadTasks(note);
this.$list.empty();
for (const task of tasks) {
$('<li>')
.text(task.title)
.toggleClass('completed', task.completed)
.appendTo(this.$list);
}
}
private async loadTasks(note: FNote) {
// Load task data from note attributes
const taskLabels = note.getLabels('task');
return taskLabels.map(label => JSON.parse(label.value));
}
}
// 2. Register widget
api.addWidget(TaskListWidget);
```
### Widget Lifecycle
```typescript
class LifecycleWidget extends NoteContextAwareWidget {
// 1. Construction
constructor() {
super();
console.log('Widget constructed');
}
// 2. Initial render
doRender() {
console.log('Initial render');
return $('<div>');
}
// 3. Context initialization
async refresh() {
console.log('Context refresh');
await super.refresh();
}
// 4. Note updates
async refreshWithNote(note: FNote) {
console.log('Note refresh:', note.noteId);
}
// 5. Cleanup
async cleanup() {
console.log('Widget cleanup');
// Release resources
}
}
```
## Performance Optimization
### Lazy Loading
```typescript
class LazyWidget extends BasicWidget {
private contentLoaded = false;
async becomeVisible() {
if (!this.contentLoaded) {
await this.loadContent();
this.contentLoaded = true;
}
}
private async loadContent() {
// Heavy content loading
const data = await server.get('expensive-data');
this.renderContent(data);
}
}
```
### Debouncing Updates
```typescript
class DebouncedWidget extends NoteContextAwareWidget {
private refreshDebounced = utils.debounce(
() => this.doRefresh(),
500
);
async refreshWithNote(note: FNote) {
// Debounce rapid updates
this.refreshDebounced();
}
private async doRefresh() {
// Actual refresh logic
}
}
```
### Virtual Scrolling
```typescript
class VirtualListWidget extends BasicWidget {
private visibleItems: any[] = [];
renderVisibleItems(scrollTop: number) {
const itemHeight = 30;
const containerHeight = this.$widget.height();
const startIndex = Math.floor(scrollTop / itemHeight);
const endIndex = Math.ceil((scrollTop + containerHeight) / itemHeight);
this.visibleItems = this.allItems.slice(startIndex, endIndex);
this.renderItems();
}
}
```
## Best Practices
### Widget Design
1. **Single Responsibility**: Each widget should have one clear purpose
2. **Composition over Inheritance**: Use composition for complex UIs
3. **Lazy Initialization**: Load resources only when needed
4. **Event Cleanup**: Remove event listeners in cleanup()
### State Management
```typescript
class StatefulWidget extends NoteContextAwareWidget {
private state = {
isExpanded: false,
selectedItems: new Set<string>()
};
setState(updates: Partial<typeof this.state>) {
Object.assign(this.state, updates);
this.renderState();
}
private renderState() {
this.$widget.toggleClass('expanded', this.state.isExpanded);
// Update DOM based on state
}
}
```
### Error Handling
```typescript
class ResilientWidget extends BasicWidget {
async refreshWithNote(note: FNote) {
try {
await this.loadData(note);
} catch (error) {
this.showError('Failed to load data');
console.error('Widget error:', error);
}
}
private showError(message: string) {
this.$widget.html(`
<div class="alert alert-danger">
${message}
</div>
`);
}
}
```
## Testing Widgets
```typescript
// Widget test example
describe('TaskListWidget', () => {
let widget: TaskListWidget;
let note: FNote;
beforeEach(() => {
widget = new TaskListWidget();
note = createMockNote({
noteId: 'test123',
attributes: [
{ type: 'label', name: 'task', value: '{"title":"Task 1"}' }
]
});
});
it('should render tasks', async () => {
await widget.refreshWithNote(note);
const tasks = widget.$widget.find('li');
expect(tasks.length).toBe(1);
expect(tasks.text()).toBe('Task 1');
});
});
```
## Related Documentation
- [Frontend Basics](../../Scripting/Frontend%20Basics.html) - Frontend scripting guide
- [Custom Widgets](../../Scripting/Custom%20Widgets.html) - Creating custom widgets
- [Script API](../../Script%20API/) - Widget API reference