Event bus
Strongly typed pub/sub on TruPDFSDK.events. Every payload is typed
by SDKEventMap, so listeners get autocomplete and compile-time
payload checks. The SDK forwards events from the internal engines onto
this single bus — hosts never subscribe to internals directly.
Subscribe
import { TruPDFSDK } from '@trupdf/viewer';
const sdk = new TruPDFSDK();
sdk.events.on('document.loaded', ({ numPages }) => {
console.log('Document loaded with', numPages, 'pages');
});
const onAdded = (ev: { id: string; kind: string; page: number }) => { /* … */ };
sdk.events.on('annotation.added', onAdded);
sdk.events.off('annotation.added', onAdded); // unsubscribe with the same reference
sdk.events.once('page.changed', (ev) => { /* fires once */ });
sdk.events.emit('plugin.toolbar.action', { id: 'mark-reviewed' });on / once / off take the handler reference; removeAllListeners()
clears the whole bus (the SDK calls it for you in destroy()).
Event map
The complete SDKEventMap — these are the only keys the bus accepts:
interface SDKEventMap {
// Document
'document.loaded': { numPages: number };
'document.error': { error: Error };
'document.destroyed': void;
// Navigation / view
'page.changed': { page: number };
'scale.changed': { scale: number };
'rotation.changed': { rotation: number };
'secureMode.changed': { enabled: boolean };
// Annotations
'annotation.added': { id: string; kind: string; page: number };
'annotation.updated': { id: string; kind: string; page: number };
'annotation.removed': { id: string };
'annotations.cleared': void;
// Forms
'form.field.added': { id: string; kind: string };
'form.field.updated': { id: string; kind: string };
'form.value.changed': { id: string; value: unknown };
'form.validation.changed': {
issues: ReadonlyArray<{ fieldId: string; code: string; message: string }>;
};
// Tools
'tool.changed': { tool: string };
// Plugins
'plugin.toolbar.action': { id: string; payload?: unknown };
}Wiring host analytics
sdk.events.on('document.loaded', ({ numPages }) => analytics.track('opened', { numPages }));
sdk.events.on('annotation.added', ({ kind, page }) =>
analytics.track('annot.added', { kind, page }),
);
sdk.events.on('tool.changed', ({ tool }) => analytics.track('tool.changed', { tool }));Sentry / log sink
sdk.events.on('document.error', ({ error }) => Sentry.captureException(error));License lifecycle events live on the LicenseManager, not the SDK
bus:
license.on('LicenseExpired', () => Sentry.captureMessage('TruPDF license expired'));
license.on('LicenseRevoked', ({ jti }) =>
Sentry.captureMessage('TruPDF revoked', { extra: { jti } }),
);
license.on('HeartbeatStale', ({ ageMs }) =>
Sentry.captureMessage('TruPDF heartbeat stale', { extra: { ageMs } }),
);Custom events
The bus is strictly typed to SDKEventMap — arbitrary string keys do
not compile. For plugin-defined signals, use
'plugin.toolbar.action' with a vendor-prefixed id and put your
data in payload:
sdk.events.emit('plugin.toolbar.action', {
id: 'acme:invoice.recognized',
payload: { invoiceId, fields },
});
sdk.events.on('plugin.toolbar.action', ({ id, payload }) => {
if (id === 'acme:invoice.recognized') { /* … */ }
});ViewerEngine events vs the SDK bus
There are two event surfaces:
-
SDK bus (
sdk.events) — the public, typed surface above. Use it for integrations. When you callsdk.attach(engine), the SDK wires the engine's document/navigation events, the tool controller'stool.changed, and the annotation / form manager events onto the bus (re-wired automatically on everydocument.loadedso per-document managers stay tracked). -
Engine events — the engine handed to you via the
engineRefprop on<DocViewer>has its ownon/offwith a wider, lower-level catalog:document.loaded·document.error·document.destroyed·page.changed·scale.changed·rotation.changed·loading.progress·state.changed·secureMode.changed·redaction.applied/redaction.failed·content-edits.changed/content-edits.applied/content-edits.failed·properties.requested·reply.requested·comment.focusRequested·calibration.requested·richtext.requested·layers.visibilityChanged·render.refreshRequested
Engine-only events (e.g. loading.progress, redaction.applied) are
not forwarded to the SDK bus. Subscribe on the engine when you need
them:
<DocViewer
source="…"
engineRef={(engine) => {
engine?.on('loading.progress', ({ progress }) => setProgress(progress));
}}
/>Prefer the SDK bus wherever an event exists on both — it is the stable public contract.