Caduceus is a TypeScript library that simplifies real-time data synchronization between your client and server using the Mercure protocol. It provides an elegant way to subscribe to updates on API resources and keep your client-side data in sync with server changes.
Caduceus consists of two main components:
- Mercure - A client for the Mercure protocol that handles the low-level communication with a Mercure hub.
- HydraSynchronizer - A higher-level abstraction designed to synchronize Hydra/JSON-LD resources with real-time updates from a Mercure hub.
- π Real-time data synchronization
- π§ Automatic resource management
- π― Topic-based subscriptions
- π Flexible event handling
- π οΈ Customizable configuration options
import { Mercure, type MercureMessageEvent } from 'bentools-caduceus';
// Create a Mercure client
const mercure = new Mercure('https://example.com/.well-known/mercure');
// Subscribe to specific topics
mercure.subscribe(['/api/books/1', '/api/books/2']);
// Add an event listener
mercure.on('message', async (event: MercureMessageEvent) => {
const data = await event.json();
console.log('Received update:', data);
});
// Connect to the Mercure hub
mercure.connect();
// Later, you can unsubscribe from topics
mercure.unsubscribe('/api/books/2');If your back-end is built using Hydra (for example with API-Platform),
you can use the HydraSynchronizer class to simplify resource synchronization:
import { HydraSynchronizer } from 'bentools-caduceus';
// Create a synchronizer connected to your Mercure hub
const synchronizer = new HydraSynchronizer('https://example.com/.well-known/mercure');
// A resource with an @id property (in Hydra/JSON-LD format)
const resource = {
'@id': '/api/books/1',
title: 'The Great Gatsby',
author: 'F. Scott Fitzgerald'
};
// Start synchronizing the resource
synchronizer.sync(resource);
// The resource object will now be automatically updated
// whenever changes are published to the Mercure hubImportant
By default, Caduceus uses the @id property of the resource to determine the topic for Mercure subscriptions.
Synchronizing too many resources at once may lead to performance issues.
Consider using URI templates or a wildcard topic to reduce the number of subscriptions.
synchronizer.sync(resource, '/api/books/{id}');
// or
synchronizer.sync(resource, '*');import { HydraSynchronizer } from 'bentools-caduceus';
const synchronizer = new HydraSynchronizer('https://example.com/.well-known/mercure');
const book = {
'@id': '/api/books/1',
title: 'The Great Gatsby',
author: 'F. Scott Fitzgerald'
};
// Start synchronizing
synchronizer.sync(book);
// Add custom event handlers
synchronizer.onUpdate(book, (updatedData, event) => {
console.log('That book was updated:', updatedData);
// You could trigger UI updates or other side effects here
})
// Listen to deletions (see "Deletion events" below)
synchronizer.onDelete(book, (deletedData, event) => {
console.log('That book was deleted (or replaced by a minimal @-only payload):', deletedData)
})Both Mercure and HydraSynchronizer accept configuration options:
import { HydraSynchronizer, DefaultEventSourceFactory } from 'bentools-caduceus';
const synchronizer = new HydraSynchronizer('https://example.com/.well-known/mercure', {
// Custom event source factory
eventSourceFactory: new DefaultEventSourceFactory(),
// Custom last event ID (for resuming connections)
lastEventId: 'event-id-123',
// Custom resource listener (signature: (resource, isDeletion) => (data, event) => void)
resourceListener: (resource, isDeletion) => (data) => {
if (isDeletion) {
// Example: mark resource as deleted in UI state
;(resource as any).deleted = true
return
}
console.log(`Resource ${resource['@id']} updated`)
Object.assign(resource, data)
},
// Custom subscribe options
subscribeOptions: {
append: false, // Replace rather than append topics
},
// Custom event dispatcher (signature: (mercure, updateListeners) => void)
handler: (mercure, listeners) => {
mercure.on('message', async (event) => {
// Custom message handling logic
const data = await event.json();
// ...
});
},
});Hydra resources deletions are detected when an event payload contains only JSON-LD metadata keys (those starting with @).
When such a payload is received, HydraSynchronizer:
- routes the event to
onDeletelisteners for the matching@id, and - calls your
resourceListener(resource, true)to let you update local state accordingly.
For regular updates (payload includes non-@ keys), the event is routed to onUpdate listeners and resourceListener(resource, false).
The Mercure class provides a low-level client for a Mercure hub:
constructor(hub: string | URL, options?: Partial<MercureOptions>)hub: URL of the Mercure huboptions: Configuration optionseventSourceFactory: Factory for creating EventSource instanceslastEventId: ID of the last event received (for resuming)
subscribe(topic: Topic | Topic[], options?: Partial<SubscribeOptions>): void- Subscribe to one or more topicsunsubscribe(topic: Topic | Topic[]): void- Unsubscribe from one or more topicson(type: string, listener: Listener): void- Add an event listenerconnect(): EventSourceInterface- Connect to the Mercure hub
An EventSourceFactory implementation that uses the mercureAuthorization cookie for authorization when connecting to a Mercure hub.
import { CookieBasedAuthorization, Mercure } from 'bentools-caduceus'
const mercure = new Mercure('https://example.com/.well-known/mercure', {
eventSourceFactory: new CookieBasedAuthorization(),
});An EventSourceFactory implementation that adds an authorization token as a query parameter when connecting to a Mercure hub.
import { QueryParamAuthorization, Mercure } from 'bentools-caduceus'
const token = 'your-jwt-token';
const mercure = new Mercure('https://example.com/.well-known/mercure', {
eventSourceFactory: new QueryParamAuthorization(token),
});The HydraSynchronizer class provides a higher-level abstraction for synchronizing resources:
constructor(hub: string | URL, options?: Partial<HydraSynchronizerOptions>)hub: URL of the Mercure huboptions: Configuration optionsresourceListener(resource: ApiResource, isDeletion: boolean): Listenerβ factory creating a per-resource listener used bysync()subscribeOptions: Options for subscribing to topics (defaultappend: true)handler(mercure: Mercure, listeners: Map<string, Listener[]>): voidβ installs routing of Mercure events (default handler provided)- plus all options from
MercureOptions
sync(resource: ApiResource, topic?: string, subscribeOptions?: Partial<SubscribeOptions>)- Start synchronizing a resource (topic defaults to the resource@id)onUpdate(resource: ApiResource, callback: Listener)- Add an update listener for a specific resourceonDelete(resource: ApiResource, callback: Listener)- Add a delete listener for a specific resourceunsync(resource: ApiResource)- Stop synchronizing a resource