Rasterex APINPM Package/docs/components/viewers-sync

Viewers Sync

Synchronize pan and zoom across two Canvas viewers with the NPM SDK.

Overview

Use one @rasterex/viewer instance per Canvas iframe and coordinate them through viewer.viewSync. The host chooses the group, configures each viewer, aligns the initial snapshot, and relays later changes to the sibling viewer.

This page documents the NPM SDK integration. It does not use the PostMessage commands directly.

Synchronization workflow

  • 1Create both viewers and mount them.
  • 2Wait for both ready() calls to resolve.
  • 3Configure both viewers with the same groupId, distinct instanceId values, and enabled: false.
  • 4Open the documents and wait for both documents.open() calls to resolve.
  • 5Request a snapshot from the source with getSnapshot() and apply it to the target with applySnapshot().
  • 6Enable both viewers and set up changed, applied, and failed subscriptions.
  • 7Relay each accepted change to the sibling with apply(change, { pan, zoom }).

The host keeps relay disabled until both viewers are ready and the target has applied the source snapshot. It stops subscriptions and destroys both viewers during cleanup. The example uses React lifecycle hooks; the same SDK sequence applies to other frameworks or a plain TypeScript host.

React host implementation

tsx
import { useEffect, useRef } from "react";
import { createViewer, type ViewSyncMode } from "@rasterex/viewer";

type Props = {
  canvasUrl: string;
  leftDocumentUrl: string;
  rightDocumentUrl: string;
  mode?: ViewSyncMode;
};

export function SynchronizedViewers({
  canvasUrl,
  leftDocumentUrl,
  rightDocumentUrl,
  mode = "panAndZoom"
}: Props) {
  const leftContainer = useRef<HTMLDivElement>(null);
  const rightContainer = useRef<HTMLDivElement>(null);

  useEffect(() => {
    if (!leftContainer.current || !rightContainer.current) return;

    const options = { viewerUrl: canvasUrl };
    const left = createViewer({ ...options, container: leftContainer.current });
    const right = createViewer({ ...options, container: rightContainer.current });
    const groupId = "review-pair";
    const leftId = left.getInfo().sdkInstanceId;
    const rightId = right.getInfo().sdkInstanceId;
    let relayEnabled = false;
    let lastLeftSequence = -1;
    let lastRightSequence = -1;

    const stopLeft = left.viewSync.on("changed", (change) => {
      if (!relayEnabled || change.groupId !== groupId ||
          change.sourceInstanceId !== leftId || change.sequence <= lastLeftSequence) return;
      lastLeftSequence = change.sequence;
      right.viewSync.apply(change, {
        pan: mode !== "zoom" && mode !== "off" && change.state.pan !== undefined,
        zoom: mode !== "pan" && mode !== "off" && change.state.zoom !== undefined
      });
    });

    const stopRight = right.viewSync.on("changed", (change) => {
      if (!relayEnabled || change.groupId !== groupId ||
          change.sourceInstanceId !== rightId || change.sequence <= lastRightSequence) return;
      lastRightSequence = change.sequence;
      left.viewSync.apply(change, {
        pan: mode !== "zoom" && mode !== "off" && change.state.pan !== undefined,
        zoom: mode !== "pan" && mode !== "off" && change.state.zoom !== undefined
      });
    });

    const start = async () => {
      await Promise.all([left.mount(), right.mount()]);
      await Promise.all([left.ready(), right.ready()]);
      await Promise.all([
        left.viewSync.configure({ groupId, instanceId: leftId, mode, enabled: false }),
        right.viewSync.configure({ groupId, instanceId: rightId, mode, enabled: false })
      ]);
      await Promise.all([
        left.documents.open({ url: leftDocumentUrl }),
        right.documents.open({ url: rightDocumentUrl })
      ]);

      if (mode !== "off") {
        const snapshot = await left.viewSync.getSnapshot({ groupId });
        await right.viewSync.applySnapshot(snapshot);
      }

      await Promise.all([
        left.viewSync.configure({ groupId, instanceId: leftId, mode, enabled: mode !== "off" }),
        right.viewSync.configure({ groupId, instanceId: rightId, mode, enabled: mode !== "off" })
      ]);
      relayEnabled = mode !== "off";
    };

    void start().catch(() => { relayEnabled = false; });
    return () => {
      relayEnabled = false;
      stopLeft();
      stopRight();
      left.destroy();
      right.destroy();
    };
  }, [canvasUrl, leftDocumentUrl, rightDocumentUrl, mode]);

  return <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 12 }}>
    <div ref={leftContainer} style={{ height: 600 }} aria-label="Left drawing" />
    <div ref={rightContainer} style={{ height: 600 }} aria-label="Right drawing" />
  </div>;
}

Modes and document changes

Set one mode for the group. panAndZoom synchronizes both operations, pan synchronizes pan, zoom synchronizes zoom, and off disables local view-sync events.

Use compatible documents and viewport geometry for snapshot alignment. When a viewer changes documents, stop relaying, configure synchronization off, reset sequence tracking, open the new document, align again, and only then enable relay.

Results and errors

configure(), getSnapshot(), and applySnapshot() wait for their results and reject on Canvas failure, invalid results, or timeout. apply() returns after sending the operation; listen for applied or failed to observe the Canvas result.

Canvas may ignore stale sequences without reporting an error. Do not treat a fire-and-forget apply() call as confirmation that the target finished applying the change.

  • Configuration or snapshot failure: keep relay disabled, correct readiness or document alignment, and retry.
  • Apply failure: stop relaying, disable both viewers, and establish a new initial snapshot.
  • Feedback loop: verify that the deployed Canvas build does not emit a new local change for a remotely applied operation.

Events and cleanup

Subscribe to changed for source operations and applied or failed for remote application results. Remove every subscription and call destroy() for both viewers when the host unmounts.

typescript
const stopChanged = viewer.viewSync.on("changed", (change) => {
  sibling.viewSync.apply(change, { pan: true, zoom: true });
});
const stopApplied = viewer.viewSync.on("applied", (result) => {
  console.log("View applied", result.sequence);
});
const stopFailed = viewer.viewSync.on("failed", (result) => {
  console.error("View sync failed", result.reason);
});

stopChanged();
stopApplied();
stopFailed();
viewer.destroy();
Events and cleanup