{"record":{"id":"ee2e68f2221afe59","repo":"toeverything/AFFiNE","slug":"errorcode-selectionerror","errorCode":"ErrorCode.SelectionError","errorMessage":"Unknown selection type: ${json.type}","messagePattern":"Unknown selection type: (.+?)","errorType":"exception","errorClass":"BlockSuiteError","httpStatus":null,"severity":"error","filePath":"blocksuite/framework/store/src/extension/selection/selection-extension.ts","lineNumber":36,"sourceCode":"  private readonly _remoteSelections = signal<Map<number, BaseSelection[]>>(\n    new Map()\n  );\n\n  private readonly _itemAdded = (event: { stackItem: StackItem }) => {\n    event.stackItem.meta.set('selection-state', this._selections.value);\n  };\n\n  private readonly _itemPopped = (event: { stackItem: StackItem }) => {\n    const selection = event.stackItem.meta.get('selection-state');\n    if (selection) {\n      this.set(selection as BaseSelection[]);\n    }\n  };\n\n  private readonly _jsonToSelection = (json: Record<string, unknown>) => {\n    const ctor = this._selectionConstructors[json.type as string];\n    if (!ctor) {\n      throw new BlockSuiteError(\n        ErrorCode.SelectionError,\n        `Unknown selection type: ${json.type}`\n      );\n    }\n    return ctor.fromJSON(json);\n  };\n\n  slots = {\n    changed: new Subject<BaseSelection[]>(),\n    remoteChanged: new Subject<Map<number, BaseSelection[]>>(),\n  };\n\n  override loaded() {\n    this.store.provider.getAll(SelectionIdentifier).forEach(ctor => {\n      [ctor].flat().forEach(ctor => {\n        this._selectionConstructors[ctor.type] = ctor;\n      });\n    });","sourceCodeStart":18,"sourceCodeEnd":54,"githubUrl":"https://github.com/toeverything/AFFiNE/blob/26c515e050211269e911f7d9cfe162a26c83ed98/blocksuite/framework/store/src/extension/selection/selection-extension.ts#L18-L54","documentation":"Thrown by `StoreSelectionExtension._jsonToSelection` when a selection JSON's `type` has no entry in `_selectionConstructors`. The constructors are populated in `loaded()` from everything registered under the `SelectionIdentifier` DI token; an unknown type means no selection class was registered for it.","triggerScenarios":"A remote peer (via awareness) or a persisted selection snapshot sends a selection of a type the local doc has no constructor for. Also triggered when calling `selection.fromJSON({ type: 'foo', ... })` for a `foo` selection class that was never registered as a `SelectionIdentifier` provider.","commonSituations":"Custom selection subclass defined but its extension not passed to the store's extensions list; version skew where one client knows a new selection type and another does not; renaming a selection `type` without a migration; selection snapshot from a third-party plugin.","solutions":["Register the selection class via its extension so it appears in `SelectionIdentifier` (add the extension to the store's extensions array).","On the receiving side, the existing `try/catch` in `_jsonToSelection` callers already logs and skips unknown remote types — ensure your code routes remote JSON through that path rather than calling `_jsonToSelection` directly.","If you renamed a type, ship a migration that rewrites old `type` values in stored selections."],"exampleFix":"// before: custom selection type used but never registered\nclass MySelection extends BaseSelection { static type = 'my:selection'; /* ... */ }\nselection.set([instance]); // remote peers throw Unknown selection type\n\n// after: register the selection extension so the constructor is known\nconst store = createStore({ extensions: [MySelectionExtension] });\n// where MySelectionExtension provides SelectionIdentifier -> MySelection","handlingStrategy":"validation","validationCode":"// check that the selection type is registered before instantiating\nfunction isKnownSelectionType(\n  knownTypes: string[],\n  type: string\n): boolean {\n  return knownTypes.includes(type);\n}\n\n// route remote JSON through the existing try/catch in selection-extension;\n// for local fromJSON, validate first:\nif (!isKnownSelectionType(Object.keys(registeredCtors), json.type)) {\n  console.warn('Skipping unknown selection type:', json.type);\n  return null;\n}","typeGuard":"function isRegisteredSelectionType(type: string, ctors: Record<string, unknown>): type is keyof typeof ctors {\n  return type in ctors;\n}","tryCatchPattern":"import { BlockSuiteError, ErrorCode } from '@blocksuite/global/exceptions';\n\ntry {\n  return ctor.fromJSON(json);\n} catch (e) {\n  if (e instanceof BlockSuiteError && e.code === ErrorCode.SelectionError) {\n    console.warn('Skipping unknown remote selection:', json.type);\n    return null;\n  }\n  throw e;\n}","preventionTips":["Register every custom selection type via its extension in the store's extensions list.","Route remote selection JSON through the framework's try/catch path, not _jsonToSelection directly.","Ship a migration when renaming a selection type."],"tags":["selection","di","awareness","registration","remote-sync"],"backgroundTag":null,"analyzedSha":"26c515e050211269e911f7d9cfe162a26c83ed98","analyzedAt":"2026-08-12T13:15:16.447Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}