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
- Register handlers before wiring the producer: consumer.register('opName', handler) or consumer.registerAll({...}) prior to any call/subscribe
- Derive op names from a single shared Ops interface on both sides so producer and consumer cannot drift
- 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
- Register all handlers before connecting the producer to the port/channel
- Derive op names from one shared Ops type so both sides stay in sync
- Track registered op names in a Set and gate outgoing calls/queued sends on it
- Treat op handler registration as part of startup; delay producer wiring until it completes
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)