Skip to main content

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:

MethodPurpose
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
isInitializedCheck if bridge is connected

Parameter Types:

MethodParameter 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:

  1. Validate that code runs inside an iframe
  2. Signal readiness to Host via DELFINO_CLIENT_READY message
  3. Wait for DELFINO_CHANNEL_INIT message with MessagePort
  4. Set endpoint to received MessagePort
  5. Initialize Comlink connection
  6. Verify connection with ping/pong

Static Methods:

MethodPurpose
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:

  1. Start listening for DELFINO_CLIENT_READY signal
  2. Wait for iframe to load (if not already loaded)
  3. Wait for client ready signal
  4. Create MessageChannel
  5. Transfer port2 to iframe via DELFINO_CHANNEL_INIT message
  6. Set endpoint to port1
  7. Initialize Comlink connection
  8. 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):

  1. Request logging (debug level)
  2. Encrypt payload (if enabled)
  3. Wrap in Message<T>
  4. Send via Comlink
  5. Decrypt result
  6. Response logging (debug level)
  7. Error logging (if failure)

Incoming (when receiving):

  1. Request logging (debug level)
  2. Decrypt payload (if enabled)
  3. Rate limiting check
  4. Call handler with decrypted payload
  5. Encrypt result (if enabled)
  6. Response logging (debug level)
  7. 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;
}
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

  1. Define payload type in src/types/[module].ts
  2. Add method to interface in src/[module]/[Module]Handlers.ts
  3. Export type from src/index.ts (if new)
  4. Implement handler in Farfalla
  5. Call from Volpe

See CONTRIBUTING.md for detailed instructions.

X

Graph View