toeverything/AFFiNE · error · BlockSuiteError

ReactiveProxyError

ReactiveProxyError

Error message

flatY2Native does not support Y.Map as value of Y.Map

What it means

In the flat reactive layer (ReactiveFlatYMap), props are stored as flattened dot-notation keys (prop:a.b) and initializeData only knows how to materialize Boxed values, Y.Array, Y.Text and plain scalars. A Y.Map nested as a prop value has no flat representation, so hydration throws ReactiveProxyError instead of producing a broken proxy.

Solutions

  1. Model nested data as flattened dot-notation keys (as addBlock does for isFlatData blocks via the recursive run() at crud.ts:100-110), not nested Y.Maps
  2. Use supported containers for structured values: Y.Array (arrays), Y.Text (rich text), Boxed (opaque values)
  3. Do not point the same flavour at both data models — pick tree or flat and keep all writers consistent
  4. Migrate existing props: walk the Y.Map and flatten each entry into prop:parent.child keys

Example fix

// before — nested Y.Map under a flat-model prop
yBlock.set('prop:style', new Y.Map([['color', 'red']]));
// hydration -> flatY2Native does not support Y.Map as value of Y.Map

// after — flatten to dot-notation keys
yBlock.set('prop:style.color', 'red');
// or model it as a supported type
yBlock.set('prop:tags', new Y.Array(['a', 'b']));
Defensive patterns

Strategy: type-guard

Validate before calling

import * as Y from 'yjs';
function isFlatSafeValue(value: unknown): boolean {
  return (
    value === null ||
    ['string', 'number', 'boolean'].includes(typeof value) ||
    value instanceof Y.Array ||
    value instanceof Y.Text
  );
}

// before writing a prop on a flat-model block:
if (!isFlatSafeValue(value)) {
  throw new Error('flatten the value into dot-notation keys instead');
}

Type guard

const isFlatSafeValue = (v: unknown): boolean =>
  v === null || ['string', 'number', 'boolean'].includes(typeof v) ||
  v instanceof Y.Array || v instanceof Y.Text;

Try / catch

try {
  doc.load();
} catch (e) {
  if (e instanceof BlockSuiteError && e.code === ErrorCode.ReactiveProxyError) {
    // a prop holds a Y.Map: flatten it into prop:a.b keys, then reload
  }
}

Prevention

When it happens

Trigger: A flat-data-model block whose prop:* entry holds a Y.Map — typically data written by the tree-model path (native2Y turns nested objects into Y.Map, crud.ts:115) and then read through the flat layer, or raw Yjs writes putting a Y.Map under a prop key.

Common situations: Switching a block family from the tree data model to isFlatData while old docs still hold nested Y.Map props; import scripts reusing tree-model serialization for flat blocks; two features writing the same flavour through different data models.

Related errors


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

Appendix: source

Thrown at blocksuite/framework/store/src/reactive/flat-native-y/initialize.ts:37

}: InitializeDataOptions): UnRecord => {
  const root: UnRecord = {};
  Array.from(yMap.entries()).forEach(([key, value]) => {
    if (key.startsWith('sys')) {
      return;
    }
    const keys = keyWithoutPrefix(key).split('.');
    const firstKey = keys[0];

    let finalData = value;
    if (Boxed.is(value)) {
      finalData = transform(firstKey, new Boxed(value), value);
    } else if (value instanceof YArray) {
      finalData = transform(firstKey, value.toArray(), value);
    } else if (value instanceof YText) {
      const next = new Text(value);
      finalData = transform(firstKey, next, value);
    } else if (value instanceof YMap) {
      throw new BlockSuiteError(
        BlockSuiteError.ErrorCode.ReactiveProxyError,
        'flatY2Native does not support Y.Map as value of Y.Map'
      );
    } else {
      finalData = transform(firstKey, value, value);
    }
    const allLength = keys.length;
    void keys.reduce((acc: UnRecord, key, index) => {
      if (!acc[key] && index !== allLength - 1) {
        const path = keys.slice(0, index + 1).join('.');
        const data = getProxy({} as UnRecord, root, path);
        acc[key] = data;
      }
      if (index === allLength - 1) {
        acc[key] = finalData;
      }
      return acc[key] as UnRecord;
    }, root);

View on GitHub (pinned to b4c8548c09)