Integration Guide
This document describes how to integrate Delfino into Host applications (Farfalla/Fenice) and Client applications (Volpe).
Installation
From GitLab Package Registry
# Configure npm to use GitLab registry for @publicala scope
echo "@publicala:registry=https://gitlab.com/api/v4/packages/npm/" >> .npmrc
# Install
npm install @publicala/delfino
Host Integration (Farfalla/Fenice)
The Host creates a ClientBridge to communicate with Volpe (the iframe).
Basic Setup
import { createClientBridge } from '@publicala/delfino';
import type { ClientBridge, SessionInitData, Note } from '@publicala/delfino';
// 1. Get the iframe element
const iframe = document.getElementById('reader-iframe') as HTMLIFrameElement;
// 2. Create the bridge
const clientBridge = createClientBridge(iframe, {
security: 'production', // or 'development' for debugging
});
// 3. Register handlers (methods that Volpe calls)
clientBridge.registerSessionHandlers({
initialize: async (): Promise<SessionInitData> => {
return {
user: { id: 'user-1', isGuest: false, isAnonymous: false },
tenant: {
id: 'tenant-1',
lang: 'es',
exitUrl: '/library',
hostLocation: window.location.href,
},
issue: {
id: 'issue-1',
name: 'My Book',
fileType: 'pdf',
description: '',
descriptionRawText: '',
lang: 'es',
shareUrl: 'https://example.com/share',
issueFullUrl: 'https://example.com/issue/1',
publicationDate: '2026-01-01',
tags: [],
isBuyable: false,
},
features: {
tts: { enabled: false, driver: '' },
chineseVoice: false,
search: true,
notes: true,
aiIntegrations: false,
concurrencyLimit: false,
copyEnabled: true,
},
settings: {
reader: { layout: 'single' },
ui: { hiddenElements: [], promoteApp: null },
user: {},
},
lastLocation: null,
modes: { preview: false, embedded: true, exhibition: null },
manifestVersion: '1.0',
};
},
});
clientBridge.registerNotes({
store: async (payload): Promise<Note> => {
const response = await api.post('/notes', payload);
return response.data;
},
list: async (payload): Promise<Note[]> => {
const response = await api.get(`/notes?issueId=${payload.issueId}`);
return response.data;
},
update: async (payload): Promise<Note> => {
const response = await api.put(`/notes/${payload.noteId}`, payload.data);
return response.data;
},
delete: async (payload): Promise<void> => {
await api.delete(`/notes/${payload.noteId}`);
},
});
clientBridge.registerContent({
getContentStructure: async () => {
// Return PDF, EPUB, or Audio structure based on issue type
return fetchContentStructure();
},
});
// 4. Initialize and wait for connection
await clientBridge.ready();
console.log('Connected to Volpe');
Calling Commands on Volpe
After connection, the Host can call commands that Volpe implements:
// Navigate to a specific position
await clientBridge.navigation.goToPosition({
position: { type: 'pdf', startPage: 42 },
});
// Notify session expiration
await clientBridge.session.sessionExpired({
reason: 'Token expired',
});
// Control playback (for audiobooks)
await clientBridge.playback.externalPlayTrack({ trackIndex: 0 });
await clientBridge.playback.externalPauseTrack();
Handler Class Pattern (Farfalla)
Farfalla uses a Handler Class pattern for better organization:
// resources/js/Reader/communication/notes/NotesHandlers.ts
import type {
NotesHandlers as INotesHandlers,
NoteCreatePayload,
Note,
} from '@publicala/delfino';
export class NotesHandlers {
private apiClient: ApiRequestClient;
constructor(config: ApiRequestConfig) {
this.apiClient = new ApiRequestClient(config);
}
getHandlers(): INotesHandlers {
return {
store: (payload) => this.store(payload),
update: (payload) => this.update(payload),
delete: (payload) => this.delete(payload),
list: (payload) => this.list(payload),
};
}
private async store(payload: NoteCreatePayload): Promise<Note> {
const response = await this.apiClient.post('/notes', {
uuid: payload.uuid,
color: payload.color,
total_text: payload.totalText,
total_length: payload.totalLength,
annotation_text: payload.annotationText,
// ... flatten position fields
});
return this.transformResponse(response.data);
}
// ... other methods
}
// resources/js/Reader/communication/clientCommunication.ts
export async function initializeClientCommunication(
iframe: HTMLIFrameElement,
options: BridgeInitOptions = {}
): Promise<ClientBridge> {
const clientBridge = createClientBridge(iframe, {
security: options.security || 'development',
});
// Instantiate handlers
const notesHandlers = new NotesHandlers(config);
const sessionHandlers = new SessionHandlers();
const contentHandlers = new ContentHandlers(config);
// Register all handlers
clientBridge.registerSessionHandlers(sessionHandlers.getHandlers());
clientBridge.registerContent(contentHandlers.getHandlers());
clientBridge.registerNotes(notesHandlers.getHandlers());
// ... more registrations
await clientBridge.ready();
return clientBridge;
}
Client Integration (Volpe)
Volpe uses the hostBridge singleton to communicate with the Host.
Basic Setup
import { hostBridge, HostBridge } from '@publicala/delfino';
import type { SessionInitData, ContentStructure } from '@publicala/delfino';
// 1. Check if running standalone (for development)
if (HostBridge.isStandalone()) {
console.log('Running without host - using mock data');
initializeStandalone();
return;
}
// 2. Register commands (methods that Host calls)
hostBridge.registerNavigationCommands({
goBack: async (): Promise<void> => {
closeReader();
},
goToPosition: async (payload): Promise<void> => {
navigateToPosition(payload.position);
},
});
hostBridge.registerSessionCommands({
sessionExpired: async (payload): Promise<void> => {
showExpiredModal(payload?.reason);
},
});
hostBridge.registerPlaybackCommands({
externalPlayTrack: async (payload): Promise<void> => {
audioPlayer.play(payload?.trackIndex);
},
externalPauseTrack: async (): Promise<void> => {
audioPlayer.pause();
},
externalSeekToPosition: async (payload): Promise<void> => {
audioPlayer.seek(payload.newSeekPositionInSeconds);
},
});
// 3. Initialize and wait for connection
await hostBridge.ready();
console.log('Connected to Host');
// 4. Call handlers (methods that Host implements)
const sessionData: SessionInitData = await hostBridge.session.initialize();
console.log('Session initialized', sessionData);
const content: ContentStructure =
await hostBridge.content.getContentStructure();
console.log('Content structure received', content);
const notes = await hostBridge.notes.list({ issueId: sessionData.issue.id });
console.log('Notes loaded', notes);
// 5. Notify Host that reader is ready
await hostBridge.reader.readerInitialized({
version: '1.0.0',
capabilities: ['pdf', 'epub', 'audio'],
});
API Class Pattern (Volpe)
Volpe uses an API Class pattern for organized handler calls:
// src/js/communication/notes/NotesAPI.ts
import { hostBridge } from '@publicala/delfino';
import type {
Note,
NoteCreatePayload,
NoteUpdatePayload,
} from '@publicala/delfino';
export class NotesAPI {
async store(payload: NoteCreatePayload): Promise<Note> {
return hostBridge.notes.store(payload);
}
async update(noteId: string, data: Partial<Note>): Promise<Note> {
return hostBridge.notes.update({ noteId, data });
}
async delete(noteId: string): Promise<void> {
return hostBridge.notes.delete({ noteId });
}
async list(issueId?: string): Promise<Note[]> {
return hostBridge.notes.list({ issueId });
}
}
export const notesAPI = new NotesAPI();
// Usage in components
import { notesAPI } from '@/communication/notes/NotesAPI';
async function saveNote() {
const note = await notesAPI.store({
uuid: crypto.randomUUID(),
color: 'yellow',
totalText: selectedText,
totalLength: selectedText.length,
position: currentPosition,
});
// Update local state
}
Command Registration Pattern (Volpe)
// src/js/communication/session/SessionCommands.ts
import { hostBridge } from '@publicala/delfino';
import type { SessionExpiredPayload } from '@publicala/delfino';
let _store: VuexStore;
export function configureSessionCommands(store: VuexStore): void {
_store = store;
}
export function registerSessionCommands(): void {
if (!_store) {
console.warn('[SessionCommands] Store not configured');
return;
}
hostBridge.registerSessionCommands({
sessionExpired: async (payload: SessionExpiredPayload): Promise<void> => {
console.log('[Volpe] Session expired:', payload?.reason);
_store.dispatch('session/showExpiredModal');
},
});
}
// src/js/communication/hostCommunication.ts
import { hostBridge } from '@publicala/delfino';
import {
configureSessionCommands,
registerSessionCommands,
} from './session/SessionCommands';
import {
configureNavigationCommands,
registerNavigationCommands,
} from './navigation/NavigationCommands';
import {
configurePlaybackCommands,
registerPlaybackCommands,
} from './playback/PlaybackCommands';
export function configureHostCommunication(
eventBus: Vue,
store: VuexStore
): void {
configureSessionCommands(store);
configureNavigationCommands(store);
configurePlaybackCommands(eventBus, store);
}
export async function initializeHostCommunication(): Promise<boolean> {
registerSessionCommands();
registerNavigationCommands();
registerPlaybackCommands();
await hostBridge.ready();
return true;
}
export { hostBridge };
Fenice Integration (UMD)
Fenice (React Native) uses the UMD build via CDN:
<script src="https://cdn.publica.la/delfino/1.1.1/delfino.min.js"></script>
<script>
const { createClientBridge } = window.Delfino;
const iframe = document.getElementById('reader-iframe');
const bridge = createClientBridge(iframe, { security: 'production' });
bridge.registerSessionHandlers({
initialize: async () => ({
/* ... */
}),
});
bridge.ready().then(() => {
console.log('Connected to Volpe');
});
</script>
Content Type Handling
PDF Content
clientBridge.registerContent({
getContentStructure: async () => ({
fileType: 'pdf',
url: 'https://cdn.example.com/content/book.pdf',
files_urls: [
{
large: '/page1-large.jpg',
thumb: '/page1-thumb.jpg',
text_layer: '/page1.json',
annotation_layer: null,
},
// ... more pages
],
files_info: [
/* ... */
],
articles: [{ id: 'art-1', title: 'Chapter 1', startPage: 1, endPage: 10 }],
cover: '/cover.jpg',
tableOfContents: [{ chapterHref: '1', label: 'Chapter 1' }],
}),
});
EPUB Content
clientBridge.registerContent({
getContentStructure: async () => ({
fileType: 'epub',
filePath: 'https://cdn.example.com/content/book/',
spine: [
{ href: 'chapter1.xhtml', previous_words: 0, words: 1500 },
{ href: 'chapter2.xhtml', previous_words: 1500, words: 2000 },
],
totalWords: 3500,
customPreview: false,
cover: '/cover.jpg',
tableOfContents: [{ chapterHref: 'chapter1.xhtml', label: 'Chapter 1' }],
}),
});
Audio Content
clientBridge.registerContent({
getContentStructure: async () => ({
fileType: 'audio',
tracks: [
{
index: 0,
title: 'Chapter 1',
fileUrl: 'https://cdn.example.com/audio/track1.mp3',
duration: 3600,
durationPreview: 60,
chunks: [{ start: 0, end: 30, text: 'Opening paragraph...' }],
},
],
totalDuration: 7200,
totalChapters: 2,
cover: '/cover.jpg',
tableOfContents: [{ chapterHref: '0', label: 'Chapter 1' }],
}),
});
Error Handling
Host Side
try {
await clientBridge.navigation.goToPosition({
position: { type: 'pdf', startPage: 999 },
});
} catch (error) {
if (error instanceof BridgeError) {
if (error.isMethodNotImplemented()) {
console.error('Volpe does not support this command');
} else if (error.isTimeout()) {
console.error('Volpe took too long to respond');
}
}
}
Client Side
try {
const notes = await hostBridge.notes.list({ issueId: 'invalid' });
} catch (error) {
if (error instanceof BridgeError) {
if (error.isRateLimited()) {
await delay(1000);
return hostBridge.notes.list({ issueId });
}
}
throw error;
}
TypeScript Configuration
Both Host and Client projects should configure TypeScript to recognize Delfino types:
{
"compilerOptions": {
"moduleResolution": "bundler",
"esModuleInterop": true,
"strict": true
}
}
Version Compatibility
| Volpe Delfino | Farfalla Delfino | Compatible |
|---|---|---|
| 1.0.0 | 1.1.0 | Yes |
| 1.1.0 | 1.1.0 | Yes |
| 1.1.0 | 1.0.0 | No (Volpe may call methods Farfalla does not have) |
Rule: Always update Farfalla first, then Volpe.