TPTruPDF

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 call sdk.attach(engine), the SDK wires the engine's document/navigation events, the tool controller's tool.changed, and the annotation / form manager events onto the bus (re-wired automatically on every document.loaded so per-document managers stay tracked).

  • Engine events — the engine handed to you via the engineRef prop on <DocViewer> has its own on / off with 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.