toeverything/AFFiNE · error · BlockSuiteError

MissingViewModelError

MissingViewModelError

Error message

can not find element(id:${referenceElement})

What it means

The op consumer dispatches incoming call/subscribe messages by looking up op.name in registeredOpHandlers, which is populated only via consumer.register()/registerAll(). A message arriving for an unregistered name throws inside a defer()-wrapped observable, so the error travels back to the producer through the channel's serialized error path instead of crashing the consumer loop.

Solutions

  1. Register handlers before wiring the producer: consumer.register('opName', handler) or consumer.registerAll({...}) prior to any call/subscribe
  2. Derive op names from a single shared Ops interface on both sides so producer and consumer cannot drift
  3. If registration is async, buffer outgoing messages until a ready signal instead of firing immediately

Example fix

// before
producer.call('sync', payload); // consumer never registered 'sync'

// after
consumer.register('sync', async (payload, { signal }) => {
  /* ... */
});
consumer.registerAll({ load, save }); // register everything before the producer connects
Defensive patterns

Strategy: validation

Validate before calling

const registeredOps = new Set<string>();
function safeRegister<T>(name: string, handler: T) {
  consumer.register(name as any, handler as any);
  registeredOps.add(name);
}

// producer side: check before sending
function canCall(op: string) {
  return registeredOps.has(op);
}
if (!canCall('sync')) throw new Error(`op 'sync' not registered yet`);

Type guard

null

Prevention

When it happens

Trigger: Producer calls an op the consumer never register()ed; handler registration is asynchronous and the producer sends before it completes; op name typo or rename on one side only; version mismatch between host and plugin op sets; registerAll object missing a key the protocol requires.

Common situations: Worker/port-based RPC where the host races ahead of worker setup; plugin upgrades adding ops the old consumer does not handle; op name strings drifting between packages instead of being derived from a shared Ops type.


AI-assisted analysis of toeverything/AFFiNE@b4c8548c09 (2026-08-18). Data as JSON: /api/errors/7b9f0aeae1bcc671. Report an issue: GitHub.

Appendix: source

Thrown at blocksuite/affine/blocks/surface-ref/src/surface-ref-block.ts:308

    };
    this.disposables.add(effect(refreshViewport));

    const referenceId = this.model.props.reference;
    const referenceXYWH$ = this._referenceXYWH$;
    class SurfaceRefViewportWatcher extends LifeCycleWatcher {
      static override readonly key = 'surface-ref-viewport-watcher';

      private readonly _disposable = new DisposableGroup();

      override mounted() {
        const crud = this.std.get(EdgelessCRUDIdentifier);
        const gfx = this.std.get(GfxControllerIdentifier);
        const { surface, viewport } = gfx;
        if (!surface) return;

        const referenceElement = crud.getElementById(referenceId);
        if (!referenceElement) {
          throw new BlockSuiteError(
            ErrorCode.MissingViewModelError,
            `can not find element(id:${referenceElement})`
          );
        }
        referenceXYWH$.value = referenceElement.xywh;

        const { _disposable } = this;
        refreshViewport();
        _disposable.add(viewport.sizeUpdated.subscribe(refreshViewport));

        if (referenceElement instanceof GfxBlockElementModel) {
          _disposable.add(
            referenceElement.xywh$.subscribe(xywh => {
              referenceXYWH$.value = xywh;
            })
          );
        } else if (referenceElement instanceof GfxPrimitiveElementModel) {
          _disposable.add(

View on GitHub (pinned to b4c8548c09)