Skip to main content

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 DelfinoFarfalla DelfinoCompatible
1.0.01.1.0Yes
1.1.01.1.0Yes
1.1.01.0.0No (Volpe may call methods Farfalla does not have)

Rule: Always update Farfalla first, then Volpe.

X

Graph View