# DiagramEntity

Import it from `@grafloria/engine`.

```ts
abstract class DiagramEntity
```

**Properties**

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `id` | `string` |  |  |
| `uuid` | `string` |  |  |
| `version` | `number` | `1` |  |
| `metadata` | `Map<string, any>` |  |  |

**Methods**

- `constructor(id?: string, uuid?: string)`
- `get isDirty(): boolean` — Check if entity has been modified since last render
- `markDirty(reason?: string): void` — Mark entity as dirty (needs re-render)
- `markClean(): void` — Mark entity as clean (rendered)
- `getDirtyReasons(): string[]` — Get reasons why entity is dirty
- `getDirtyTimestamp(): number | null` — Get timestamp when entity was marked dirty
- `beginBatch(): void` — Begin batch update
Delays dirty marking until batch ends
- `endBatch(): void` — End batch update
Marks dirty once if any changes occurred
- `isBatching(): boolean` — Check if currently in batch mode
- `reportRegisterWrite(property: string, oldValue: unknown, newValue: unknown): void` — REPORT A REGISTER WRITE THE COLLAB REDUCER JUST PERFORMED.

`trackChange()` is `protected`, and `applyOp` lives outside the class — so every
register the reducer wrote by plain field assignment (anything with no dedicated
mutator: a link's `style`, `flexConfig`, a group's `isCollapsed`, `layoutConfig`,
`bounds`, `parentId`, …) mutated the model WITHOUT passing the funnel.

That is not a cosmetic gap. `UndoStack.undo()` applies the inverse THROUGH THE MODEL
WITH CAPTURE LIVE and then reads back whatever capture minted. No trackChange means
no op, which means THE UNDO REACHES NOBODY. Measured before this existed:

link style   execute → 1 op   undo → 0 ops
    flexConfig   execute → 1 op   undo → 0 ops
    isCollapsed  execute → 1 op   undo → 0 ops
    position     execute → 1 op   undo → 1 op   ← a real mutator: always worked

The author's own document was correct every time — which is exactly why this
survived. It is the `UpdateLinkStyleCommand` defect (see style-undo.spec.ts) living
one layer down, in the reducer, and hitting every mutator-less register at once.

So the reducer gets a seam onto the funnel rather than a copy of it. There is still
ONE funnel; this is a door onto it, not a second source of truth.
- `on(event: string, handler: Function): () => void` — Subscribe to changes
- `onPropertyChange(property: string, handler: (data: any) => void): () => void` — Subscribe to property changes
- `getChangeLog(): ReadonlyArray<ChangeEntry>` — Get change history
- `clearChangeLog(): void` — Clear change history
- `getLabel(): string | undefined` — CANONICAL label read. `metadata.label` wins; legacy `data['label']` (on
subclasses that carry a data bag) is honoured as a fallback so pre-canon
documents and third-party writers still surface their labels everywhere —
rendering, a11y names, and Mermaid export alike.
- `setLabel(label: string): void` — CANONICAL label write. Writes `metadata.label` (tracked → dirty → repaint)
and mirrors into the legacy `data['label']` slot when the entity has a
data bag, so any reader still on the old field sees the same value. The
mirror is a shadow: metadata is authoritative on every read path.
- `setMetadata(key: string, value: any): void` — Set metadata value
- `getMetadata(key: string): any` — Get metadata value
- `deleteMetadata(key: string): void` — Remove metadata
- `abstract serialize(): SerializedEntity` — Serialize entity to JSON
- `isDisposed(): boolean` — Check if entity has been disposed
- `dispose(): void` — Dispose entity and clean up resources
Prevents memory leaks by:
- Removing all event listeners
- Clearing change log
- Clearing metadata
- Marking as disposed
