Architecture
This document describes the internal architecture of Delfino, including the communication protocol, message format, initialization flow, and module organization.
Project Structure
delfino/
├── src/
│ ├── index.ts # Public exports
│ ├── BaseBridge.ts # Core bridge with domain registration
│ ├── HostBridge.ts # For Volpe (iframe) → singleton
│ ├── ClientBridge.ts # For Host (Farfalla/Fenice) → factory
│ │
│ ├── types/
│ │ ├── index.ts # Re-exports all types
│ │ ├── common.ts # Message<T>, Position, ContentType
│ │ ├── domain.ts # DomainName union type
│ │ ├── errors.ts # BridgeError, ErrorCode
│ │ ├── session.ts # SessionInitData, UserData, TenantData
│ │ ├── notes.ts # Note, NoteCreatePayload
│ │ ├── content.ts # PdfContentStructure, EpubContentStructure
│ │ ├── navigation.ts # LocationChanged, GoToPosition
│ │ ├── playback.ts # PlaybackPlayerState, SeekToPosition
│ │ ├── reader.ts # ReaderInitialized
│ │ ├── analytics.ts # TrackParams
│ │ ├── settings.ts # ReaderSettings, UISettings
│ │ ├── ui.ts # SharePayload, ExportText
│ │ ├── integrations.ts # AI, Dictionary, Listen, Search, Translate
│ │ └── channel.ts # MessageChannel types
│ │
│ ├── security/
│ │ └── SecurityManager.ts # Rate limiting, logging, encryption (disabled)
│ │
│ ├── session/
│ │ ├── SessionHandlers.ts # interface SessionHandlers
│ │ └── SessionCommands.ts # interface SessionCommands
│ │
│ ├── notes/
│ │ └── NotesHandlers.ts # interface NotesHandlers
│ │
│ ├── content/
│ │ └── ContentHandlers.ts # interface ContentHandlers
│ │
│ ├── navigation/
│ │ ├── NavigationHandlers.ts # interface NavigationHandlers
│ │ └── NavigationCommands.ts # interface NavigationCommands
│ │
│ ├── playback/
│ │ ├── PlaybackHandlers.ts # interface PlaybackHandlers
│ │ └── PlaybackCommands.ts # interface PlaybackCommands
│ │
│ ├── reader/
│ │ ├── ReaderHandlers.ts # interface ReaderHandlers
│ │ └── ReaderCommands.ts # interface ReaderCommands
│ │
│ ├── analytics/
│ │ └── AnalyticsHandlers.ts # interface AnalyticsHandlers
│ │
│ ├── settings/
│ │ ├── SettingsHandlers.ts # interface SettingsHandlers
│ │ └── SettingsCommands.ts # interface SettingsCommands
│ │
│ ├── ui/
│ │ ├── UIHandlers.ts # interface UIHandlers
│ │ └── UICommands.ts # interface UICommands
│ │
│ └── integrations/
│ ├── ai/AIHandlers.ts
│ ├── dictionary/DictionaryHandlers.ts
│ ├── listen/ListenHandlers.ts
│ ├── search/SearchHandlers.ts
│ └── translate/TranslateHandlers.ts
│
├── dist/
│ ├── esm/index.js # ES Modules build
│ ├── umd/delfino.min.js # UMD build (bundled + minified)
│ └── types/index.d.ts # TypeScript declarations
│
├── package.json
├── tsconfig.json
├── rollup.config.js
├── vitest.config.ts
├── eslint.config.mjs
└── README.md
Core Components
BaseBridge
The BaseBridge class is the shared base for both ClientBridge and HostBridge. It manages:
- Bidirectional Comlink connection
- Domain registration with typed accessors
- Automatic
Message<T>wrapping/unwrapping - Handler exposure via Comlink
- Proxy creation for remote calls
- Security features (rate limiting, logging)
Key Methods:
| Method | Purpose |
|---|---|
ready(timeout) | Initialize bridge and wait for connection |
register<Domain>Handlers(handlers) | Register handlers for a domain |
register<Domain>Commands(commands) | Register commands for a domain |
bridge.<domain> | Typed accessor for domain |
destroy() | Release resources and close connection |
isInitialized | Check if bridge is connected |
Parameter Types:
| Method | Parameter Type |
|---|---|
register<Domain>Handlers(handlers) | handlers: { [methodName]: (payload) => Promise<Result> } |
register<Domain>Commands(commands) | commands: { [methodName]: (payload) => Promise<void> } |
Usage Examples:
// Handlers: Object with async methods that Host implements
clientBridge.registerNotesHandlers({
store: (payload: NoteCreatePayload) => Promise<Note>,
update: (payload: NoteUpdatePayload) => Promise<Note>,
delete: (payload: NoteDeletePayload) => Promise<void>,
list: (payload: NoteListPayload) => Promise<Note[]>,
});
// Commands: Object with async methods that Volpe implements
hostBridge.registerNavigationCommands({
goBack: () => Promise<void>,
goToPosition: (payload: GoToPositionPayload) => Promise<void>,
});
HostBridge
The HostBridge class is used by Volpe (iframe) to communicate with the Host. It is exported as a singleton (hostBridge).
Initialization Process:
- Validate that code runs inside an iframe
- Signal readiness to Host via
DELFINO_CLIENT_READYmessage - Wait for
DELFINO_CHANNEL_INITmessage with MessagePort - Set endpoint to received MessagePort
- Initialize Comlink connection
- Verify connection with ping/pong
Static Methods:
| Method | Purpose |
|---|---|
isInIframe() | Returns true if running in an iframe |
isStandalone() | Returns true if running independently |
ClientBridge
The ClientBridge class is used by Host applications (Farfalla/Fenice) to communicate with Volpe. It is created via the createClientBridge(iframe, config) factory function.
Initialization Process:
- Start listening for
DELFINO_CLIENT_READYsignal - Wait for iframe to load (if not already loaded)
- Wait for client ready signal
- Create MessageChannel
- Transfer port2 to iframe via
DELFINO_CHANNEL_INITmessage - Set endpoint to port1
- Initialize Comlink connection
- Verify connection with ping/pong
Message Format
All payloads are wrapped in the Message<T> structure:
interface Message<T> {
id: string; // UUID for tracking
timestamp: number; // Date.now() when sent
payload: T; // Actual payload (optionally encrypted)
}
Benefits:
- Tracking: Each message has a unique UUID
- Temporal correlation: Automatic timestamp
- Consistency: Automatic wrapping, not optional
Communication Protocol
Handshake Sequence
┌─────────────────────────────────────────────────────────────────────────┐
│ HANDSHAKE SEQUENCE │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ HOST VOLPE │
│ ──── ───── │
│ │
│ 1. Create iframe │
│ <iframe src="volpe.html"> │
│ │ │
│ 2. createClientBridge(iframe) │
│ │ │
│ 3. Register handlers 4. Register commands │
│ • session.initialize() • navigation.goBack() │
│ • notes.store/list/etc • playback.externalPlay() │
│ • content.getStructure() • session.sessionExpired() │
│ │ │ │
│ 5. clientBridge.ready() hostBridge.ready() │
│ │ │ │
│ │ Start listening for │ │
│ │ DELFINO_CLIENT_READY ◄────────────────┤ │
│ │ DELFINO_CLIENT_READY (repeated) │
│ │ │ │
│ 6. Receive signal, create │ │
│ MessageChannel │ │
│ │ │ │
│ 7. Transfer port2 ─────────────────────────────► │
│ DELFINO_CHANNEL_INIT │ │
│ │ │
│ 8. Receive port │
│ Set endpoint │
│ │ │
│ 9. ◄──────────── ping() ───────────────────────┤ │
│ ├──────────── pong() ──────────────────────►│ │
│ │ │
│ 10. Both sides connected │ │
│ bridge.isInitialized = true │ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
Reader Initialization Sequence
After the handshake, Volpe initializes the reader:
┌─────────────────────────────────────────────────────────────────────────┐
│ READER INITIALIZATION SEQUENCE │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ HOST VOLPE │
│ ──── ───── │
│ │
│ │ │ │
│ │◄────────────── ───────────────┤ │
│ │ session.initialize() │ │
│ │ │ │
│ 1. Return SessionInitData ─────────────►│ │
│ (user, tenant, issue, │ │
│ features, settings) │ │
│ │ │ │
│ │◄─────────────────────────────┤ │
│ │ content.getContentStructure() │
│ │ │ │
│ 2. Return ContentStructure ────────────►│ │
│ (PDF pages, EPUB spine, │ │
│ audio tracks) │ │
│ │ │ │
│ │◄─────────────────────────────┤ │
│ │ notes.list() │ │
│ │ │ │
│ 3. Return Note[] ──────────────────────►│ │
│ │ │ │
│ │◄─────────────────────────────┤ │
│ │ reader.readerInitialized() │ │
│ │ │ │
│ 4. Hide loading spinner │ │
│ Reader is ready │ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
Control Points
Each message transaction passes through 7 control points on each side:
Outgoing (when sending):
- Request logging (debug level)
- Encrypt payload (if enabled)
- Wrap in Message<T>
- Send via Comlink
- Decrypt result
- Response logging (debug level)
- Error logging (if failure)
Incoming (when receiving):
- Request logging (debug level)
- Decrypt payload (if enabled)
- Rate limiting check
- Call handler with decrypted payload
- Encrypt result (if enabled)
- Response logging (debug level)
- Error logging (if failure)
Security Configuration
Available Presets
type SecurityPreset = 'development' | 'production';
Development Preset
{
rateLimiting: { enabled: false, windowMs: 60000, maxRequests: 1000 },
encryption: { enabled: false, algorithm: 'AES-GCM', keySize: 256 },
logging: { enabled: true, level: 'debug' }
}
Production Preset
{
rateLimiting: { enabled: true, windowMs: 30000, maxRequests: 50 },
encryption: { enabled: false, algorithm: 'AES-GCM', keySize: 256 },
logging: { enabled: true, level: 'warn' }
}
Encryption Status
Encryption infrastructure (AES-GCM) is implemented but disabled by design:
- Key exchange mechanism between Host and Client is not yet implemented
- MessageChannel already provides isolation (messages are only accessible to the two endpoints)
- Encryption is redundant for same-origin iframe communication
Error Handling
ErrorCode Enum
enum ErrorCode {
CONNECTION_ERROR = 'CONNECTION_ERROR',
TIMEOUT = 'TIMEOUT',
VALIDATION_ERROR = 'VALIDATION_ERROR',
RATE_LIMIT_EXCEEDED = 'RATE_LIMIT_EXCEEDED',
ENCRYPTION_ERROR = 'ENCRYPTION_ERROR',
DOMAIN_NOT_REGISTERED = 'DOMAIN_NOT_REGISTERED',
METHOD_NOT_IMPLEMENTED = 'METHOD_NOT_IMPLEMENTED',
INVALID_ORIGIN = 'INVALID_ORIGIN',
UNKNOWN_ERROR = 'UNKNOWN_ERROR',
}
BridgeError Class
class BridgeError extends Error {
code: ErrorCode;
domain?: string;
method?: string;
originalError?: Error;
// Fluent helper methods
isConnectionError(): boolean;
isTimeout(): boolean;
isRateLimited(): boolean;
isMethodNotImplemented(): boolean;
}
Error Handling Example
try {
const notes = await hostBridge.notes.list({ issueId: '42' });
} catch (error) {
if (error instanceof BridgeError) {
if (error.isTimeout()) {
console.error('Connection timeout');
} else if (error.isRateLimited()) {
console.error('Too many requests');
} else if (error.isMethodNotImplemented()) {
console.error(`Method not available: ${error.method}`);
}
}
}
Key Data Types
SessionInitData
Contains all data needed to initialize the reader:
interface SessionInitData {
user: UserData; // id, isGuest, isAnonymous
tenant: TenantData; // id, lang, exitUrl, hostLocation
issue: IssueData; // id, name, fileType, shareUrl, etc.
features: FeaturesData; // tts, search, notes, aiIntegrations
settings: SettingsData; // reader, ui, user settings
lastLocation: LocationData | null;
modes: ModesData; // preview, embedded, exhibition
manifestVersion: string;
}
ContentStructure
Discriminated union by fileType:
type ContentStructure =
| PdfContentStructure
| EpubContentStructure
| AudioContentStructure;
// PDF
interface PdfContentStructure {
fileType: 'pdf';
url: string;
files_urls: PdfFileUrls[];
files_info: PdfFileInfo[];
articles: ArticleData[];
cover: string;
tableOfContents: TOCEntry[];
}
// EPUB
interface EpubContentStructure {
fileType: 'epub';
filePath: string;
spine: SpineEntry[];
totalWords: number;
customPreview: boolean;
cover: string;
tableOfContents: TOCEntry[];
}
// Audio
interface AudioContentStructure {
fileType: 'audio';
tracks: AudioTrackData[];
totalDuration: number;
totalChapters: number;
cover: string;
tableOfContents: TOCEntry[];
}
Position
Universal position type for PDF, EPUB, and Audio:
interface Position {
type: 'pdf' | 'epub' | 'audiobook';
// PDF fields
startPage?: number;
startPageStartOffset?: number;
startPageLength?: number;
endPage?: number;
endPageLength?: number;
// EPUB fields
epubLocationStartCfi?: string;
epubLocationEndCfi?: string;
epubChapter?: string;
epubLocationStartChar?: number;
// Audio fields
currentTrack?: number;
currentSeek?: number;
}
Common Types
// Base message wrapper (automatic)
interface Message<T> {
id: string; // UUID
timestamp: number; // Date.now()
payload: T; // Business payload
}
type ContentType = 'pdf' | 'epub' | 'audiobook';
type LayoutMode = 'scroll' | 'paged';
Note
type NoteColor = 'yellow' | 'green' | 'blue' | 'pink' | 'purple';
interface Note {
id: string;
uuid: string;
color: NoteColor;
totalText: string;
totalLength: number;
annotationText?: string;
position: Position;
createdAt: string;
updatedAt: string;
}
interface NoteCreatePayload {
uuid: string;
color: NoteColor;
totalText: string;
totalLength: number;
annotationText?: string;
position: Position;
}
interface NoteUpdatePayload {
noteId: string;
data: Partial<Omit<Note, 'id' | 'uuid' | 'createdAt' | 'updatedAt'>>;
}
interface NoteDeletePayload {
noteId: string;
}
interface NoteListPayload {
issueId?: string;
}
Navigation Types
interface LocationInfo {
page?: number; // PDF
cfi?: string; // EPUB
chapterHref?: string; // EPUB
currentTrack?: number; // Audiobook
currentSeek?: number; // Audiobook
position?: Position;
}
interface LocationChangedPayload {
location: LocationInfo;
fileType: string;
timestamp?: number;
}
interface GoToPositionPayload {
position: Position;
}
interface OpenLinkPayload {
url: string;
target?: string;
}
Playback Types
interface PlaybackPlayerState {
isPlaying: boolean;
currentTrack: number;
currentSeek: number;
duration: number;
rate: number;
volume: number; // 0-1
}
interface PlayTrackPayload {
trackIndex?: number;
}
interface SeekToPositionPayload {
newSeekPositionInSeconds: number;
duration?: number;
rate?: number;
}
interface PlayerStateChangedPayload {
state: PlaybackPlayerState;
}
Analytics Types
type TrackingEventName =
| 'session-start'
| 'session-resume'
| 'session-end'
| 'page-change'
| 'heartbeat'
| 'tab-hidden'
| 'tab-visible';
interface TrackingPayload {
sessionUuid?: string;
uuid?: string;
token?: string;
timestamp?: number;
index?: number;
schemaId?: number;
reader?: string;
volpeHost?: string;
environment?: string;
userAgent?: string;
currentPage?: number | null;
lastPage?: number | null;
secondsReading?: number;
extraPayload?: TrackingExtraPayload;
}
UI Types
interface SharePayload {
url: string;
text?: string;
title?: string;
target?: string;
}
interface ExportTextPayload {
text: string;
fileName: string;
}
interface CopyToClipboardPayload {
text: string;
}
interface ShowToastPayload {
message: string;
type?: 'success' | 'error' | 'warning' | 'info';
duration?: number;
}
Settings Types
interface SettingsUpdatePayload {
settings: Partial<UserSettings>;
}
interface SettingsResponse {
settings: UserSettings;
}
Reader Types
interface ReaderInitializedPayload {
contentType: ContentType;
totalPages?: number;
totalTracks?: number;
initialPosition?: Position;
}
interface ReloadContentPayload {
forceRefresh?: boolean;
}
interface ContentInfoResponse {
contentType: ContentType;
totalPages?: number;
totalTracks?: number;
toc?: TocItem[];
metadata?: ContentMetadata;
}
interface TocItem {
title: string;
href?: string;
page?: number;
children?: TocItem[];
}
interface ContentMetadata {
title?: string;
author?: string;
publisher?: string;
language?: string;
publishDate?: string;
}
Integration Types
AI:
type AIActions = Record<string, string>;
interface AIGeneratePayload {
action: string;
text: string;
lang?: string;
}
interface AIResponse {
data: string[];
uuid: string;
}
interface AIFeedbackPayload {
uuid: string;
score: number;
}
Dictionary:
interface DictionaryLookupPayload {
text: string;
lang?: string;
}
interface DictionaryEntry {
category: string;
definition: string;
example?: string;
}
interface DictionaryResponse {
word: string;
entries: DictionaryEntry[];
}
Listen (TTS):
interface ListenSynthesizePayload {
text: string;
voiceId?: string;
lang?: string;
}
interface ListenResponse {
audio: string;
contentType: string;
}
Translate:
interface TranslatePayload {
text: string;
targetLang: string;
sourceLang?: string;
}
interface Translation {
translatedText: string;
detectedSourceLanguage?: string;
}
interface TranslateResponse {
translations: Translation[];
}
Search:
interface SearchPayload {
search: string;
issueId: string;
}
interface SearchResultLocation {
value: number | string;
type?: 'pdf' | 'epub';
}
interface SearchResultItem {
location: SearchResultLocation;
snippet: string;
queryWords: string[];
}
interface SearchResponse {
results: SearchResultItem[];
}
Module Pattern
Each module follows this structure:
src/[module]/
├── [Module]Handlers.ts # interface [Module]Handlers { ... }
├── [Module]Commands.ts # interface [Module]Commands { ... } (if bidirectional)
src/types/
├── [module].ts # Payload and response types
Adding a New Method
- Define payload type in
src/types/[module].ts - Add method to interface in
src/[module]/[Module]Handlers.ts - Export type from
src/index.ts(if new) - Implement handler in Farfalla
- Call from Volpe
See CONTRIBUTING.md for detailed instructions.