# Collab

Import these from `@grafloria/engine`.

## On their own pages

- [`ReferentialIntegrity`](https://atloria.dev/p/grafloria-h7YM7amryF/developer/grafloria-engine-collab-referentialintegrity): Holds the diagram to its one hard invariant, and buffers the ops that cannot be applied
- [`Replica`](https://atloria.dev/p/grafloria-h7YM7amryF/developer/grafloria-engine-collab-replica): One peer.

## Functions

### `applyEntitySet`

Write one property register of an entity that is ALREADY RESOLVED.

Split out of applySet so that referential integrity can drive the identical write into
an entity it is holding OUTSIDE the diagram (a link quarantined because its endpoint
node is gone, see integrity.ts). Same mutators, same idempotence guard, same everything —
a quarantined entity that took a DIFFERENT write path would drift from a live one, and
that drift would only surface on resurrection, long after the cause.

```ts
function applyEntitySet(
  entity: Entity,
  path: string,
  value: OpValue | undefined
): boolean
```

### `applyOp`

Apply ONE op. Returns true if it changed anything.

A NO-OP IS NOT AN ERROR, and this is load-bearing rather than lenient. In a
distributed log you will legitimately receive:
  • a `set` on an entity a concurrent `remove` already deleted,
  • a `remove` for something already removed (duplicate delivery, or two peers
    deleting the same node),
  • an `add` for an id that already exists (the same op delivered twice). Every one of those is normal traffic, not corruption. Throwing on them would mean a
single duplicated packet takes down the session. So we IGNORE them and say so by
returning false — while still throwing loudly on an op that is genuinely malformed
(unknown kind, missing id), because that IS a bug and hiding it helps nobody.

```ts
function applyOp(diagram: DiagramModel, op: Op): boolean
```

### `compareOps`

TOTAL ORDER over ops. Every peer must sort the same log into the same sequence, or
"replay converges" is meaningless.

(clock, actor) is a total order because clocks are integers and actor ids are
distinct strings. The actor tiebreak is arbitrary — it does NOT mean the
lexicographically-larger actor is "righter" — but it is the same arbitrary answer
on every peer, which is the only thing required.

```ts
function compareOps(a: Op, b: Op): number
```

### `opId`

Identity of an op, for idempotent delivery: the same op received twice is one op.

```ts
function opId(op: Op): string
```

### `replay`

Replay an ordered log into a diagram.

Sorts defensively rather than trusting the caller: replay determinism is the property
every later card stands on, and it would be silly to lose it because someone handed us
an array in arrival order.

And it ENFORCES THE INVARIANT at the end, because a peer that joins by replaying
a log must arrive exactly where a peer that was in the room the whole time already is. If
integrity only ran in `Replica.receive()`, a log containing "delete a node that had links"
would replay into a document with a dangling link, and the newcomer would be the only one
holding it. One sweep, once, over the final state — the invariant is a function of that
state and of nothing on the way to it.

NOTE this is the DOCUMENT, not a live peer: the quarantine it builds is discarded with the
temporary registry. A peer that intends to go on editing should be seeded through
`Replica.receive(history)`, which keeps it — and can therefore still bring an orphaned
link back if someone undoes the delete.

```ts
function replay(diagram: DiagramModel, ops: readonly Op[]): number
```

## Classes

### `LamportClock`

A Lamport clock.

`tick()` for a local event. `observe(remoteClock)` on receipt, which jumps this
clock past anything the remote had seen — that is what makes the resulting order
respect causality across peers rather than merely within one.

```ts
class LamportClock
```

**Methods**

- `constructor(private readonly actor: ActorId, start = 0)`
- `tick(): number` — The next clock value for a local event.
- `observe(seen: number): void` — Fold in a clock we have just seen. After this, our next tick() is guaranteed to
be greater than `seen` — so any op we issue AFTER observing theirs sorts AFTER it,
which is exactly the causality guarantee.
- `peek(): number`
- `get actorId(): ActorId`

### `LwwRegistry`

Which write currently owns each register.

The key is (target, id, path) for a property, and (target, id, presence) for whether
the entity exists at all — because an add/remove race is the same kind of race as a
concurrent property write, and deserves the same machinery rather than a special case.

```ts
class LwwRegistry
```

**Methods**

- `admit(op: Op): boolean` — Should this op be applied?

False when a NEWER write already owns the register — the op is not late, it is
SUPERSEDED, and applying it would move the register backwards. This is the single
check that makes arrival-order application equal to total-order replay.

Records the stamp as a side effect when the answer is yes: an op that wins now owns
its register.
- `owner(op: Op): Stamp | undefined` — Who currently owns a register — for debugging, and for a UI that shows attribution.
- `presenceOf(target: Op['target'], id: string): Stamp | undefined` — The stamp that decided whether an entity exists. Undefined if never seen.
- `get size(): number`

### `OpApplyError`

Thrown only for a MALFORMED op — never for a merely-losing one.

```ts
class OpApplyError extends Error
```

**Properties**

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `name` | `string` |  |  |
| `message` | `string` |  |  |
| `stack?` | `string` |  |  |

**Methods**

- `constructor(message: string, readonly op: Op)`

### `OpCapture`

Watches a live diagram and emits an Op for every real edit.

Re-entrancy: while `applyRemote()` is running, capture is SUPPRESSED. Without that,
applying a peer's op would capture it as a local op and re-broadcast it, and the two
peers would ping-pong the same edit forever, each amplifying the other. This is the
single most important line in the file and it is three characters long.

```ts
class OpCapture
```

**Methods**

- `constructor( private readonly diagram: DiagramModel, options: OpCaptureOptions )`
- `get lamport(): LamportClock` — The Lamport clock this capture issues from — shared with the sync layer.
- `applyRemote(ops: readonly Op[], apply: (op: Op) => void): void` — Apply remote ops WITHOUT capturing them as local edits.

The clock observes each remote op first, so any op we issue after this sorts after
theirs. That is what makes the resulting order respect causality between peers and
not merely within one.
- `silently(fn: () => void): void` — Run `fn` with capture SUPPRESSED — the model mutations it makes emit no ops.

For work that is DERIVED rather than authored: referential integrity moving an
orphaned link out of the document and back again is a function of state that every
peer computes for itself, so broadcasting it would put a redundant op on the wire that
races with the ops it was derived from. Deriving it locally is not just cheaper, it is
the only thing that converges.

Re-entrant: nesting must not clear the flag early (integrity reconciles INSIDE
applyRemote), or a remote op's mutations would start echoing back mid-batch.
- `stop(): void`

### `OpLog`

An append-only, totally-ordered, de-duplicating op log.

Kept SORTED rather than merely appended, because "the order ops arrived in" is a
property of the network and "the order ops apply in" must not be.

```ts
class OpLog
```

**Methods**

- `append(op: Op): boolean` — Add an op. Returns false if it was already present.

Idempotence is not a nicety here: every real transport redelivers (a WebSocket
reconnect replays, a peer re-sends on timeout, two peers relay the same op to each
other). An op log that applied a duplicate `add` twice, or double-counted a move,
would diverge on nothing more exotic than a flaky wifi connection.
- `appendAll(ops: Iterable<Op>): Op[]`
- `has(op: Op): boolean`
- `toArray(): readonly Op[]` — The ops, in total order.
- `get size(): number`
- `since(clock: number): Op[]` — Ops strictly after `clock` — the tail a peer needs to catch up.
- `maxClock(): number` — The highest clock in the log — what a peer should observe on catch-up.

### `UndoStack`

A per-actor, supersession-aware undo/redo stack.

Owns no model state. It reads the LOG to decide what an undo should do, and applies the
result through the diagram so that capture mints a normal op for it.

```ts
class UndoStack
```

**Methods**

- `constructor( private readonly log: OpLog, private readonly actor: ActorId, private readonly apply: (op: Op) => void )`
- `get canUndo(): boolean`
- `get canRedo(): boolean`
- `get depth(): number` — Depth of the undo stack — one entry per user-visible step.
- `record(op: Op, before: OpBefore): void` — Record a local edit.

Ignored while replaying: the ops an undo emits are the undo, not new work to undo.
- `transact<T>(fn: () => T): T` — Group everything `fn` does into ONE undo step.

Without this, a gesture that touches four registers costs four Ctrl-Zs — and deleting
a node with three links (which the editor does as four ops) would take four. The
grouping is a LOCAL, UI-level concern: it changes what one keypress undoes, never what
is on the wire, and two peers may group differently without diverging.
- `undo(): Op[]` — Undo my last step. Returns the ops it emitted — empty if every part of it was already
superseded, which is a legitimate and silent outcome.

`apply` runs the inverse through the model with capture LIVE, so the resulting ops are
minted, logged, gated and broadcast exactly like any other local edit.
- `redo(): Op[]` — Redo the step undo last took back.

## Interfaces

### `AddOp`

Create an entity. Carries the full serialized form — an add has no prior state to diff against.

```ts
interface AddOp extends OpBase
```

**Properties**

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `op` | `'add'` |  |  |
| `target` | `Exclude<OpTarget, 'diagram'>` |  |  |
| `id` | `string` |  |  |
| `data` | `SerializedNode \| SerializedLink \| SerializedGroup \| SerializedStroke` |  |  |
| `clock` | `number` |  | Lamport clock. Causality, NOT wall time. Advances past every clock this actor has observed, so `caused-by` always implies a strictly greater clock. |
| `actor` | `ActorId` |  | Who. Also the deterministic tiebreak when two ops share a clock. |

### `OpCaptureOptions`

```ts
interface OpCaptureOptions
```

**Properties**

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `actor` | `ActorId` |  | Who this peer is. Must be unique across peers; used for the total-order tiebreak. |
| `onOp` | `(op: Op, before: OpBefore) => void` |  | Called for each captured op — hand it to a sync adapter, an autosave, a test. |
| `startClock?` | `number` |  | Resume a clock across sessions (e.g. from a persisted op-log tail). |

### `RemoveOp`

Remove an entity.

```ts
interface RemoveOp extends OpBase
```

**Properties**

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `op` | `'remove'` |  |  |
| `target` | `Exclude<OpTarget, 'diagram'>` |  |  |
| `id` | `string` |  |  |
| `clock` | `number` |  | Lamport clock. Causality, NOT wall time. Advances past every clock this actor has observed, so `caused-by` always implies a strictly greater clock. |
| `actor` | `ActorId` |  | Who. Also the deterministic tiebreak when two ops share a clock. |

### `ReplicaOptions`

```ts
interface ReplicaOptions
```

**Properties**

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `actor` | `ActorId` |  | This peer's identity. MUST be unique across peers — the total order depends on it. |
| `onLocalOp?` | `(op: Op) => void` |  | Called with each op this peer produces locally. Hand it to a transport. |
| `startClock?` | `number` |  | Resume the Lamport clock from a persisted tail. |

### `SetOp`

Write one property register. The unit of concurrency.

```ts
interface SetOp extends OpBase
```

**Properties**

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `op` | `'set'` |  |  |
| `target` | `OpTarget` |  |  |
| `id` | `string` |  | '' for the diagram itself, which is a singleton. |
| `path` | `OpPath` |  |  |
| `value?` | `OpValue` |  | The value the register now holds. ABSENT when the op is a clear. |
| `clear?` | `true` |  | Explicitly empty the register: the key is deleted, not set to anything. NOT null — null is a legitimate STORED value (a peer that stores null and a peer that cleared must not converge on the same document), so it cannot double as the clear sentinel. |
| `clock` | `number` |  | Lamport clock. Causality, NOT wall time. Advances past every clock this actor has observed, so `caused-by` always implies a strictly greater clock. |
| `actor` | `ActorId` |  | Who. Also the deterministic tiebreak when two ops share a clock. |

### `Stamp`

The stamp of the write that currently owns a register.

```ts
interface Stamp
```

**Properties**

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `clock` | `number` |  |  |
| `actor` | `ActorId` |  |  |

## Types

### `ActorId`

Also has every member of `String`, listed on its own entry.

Who made the edit. Stable for the lifetime of a session; distinct per peer.

```ts
type ActorId = string;
```

### `Op`

Also has every member of `AddOp`, listed on its own entry.

```ts
type Op = AddOp | RemoveOp | SetOp;
```

**Properties**

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `clock` | `number` |  | Lamport clock. Causality, NOT wall time. Advances past every clock this actor has observed, so `caused-by` always implies a strictly greater clock. |
| `actor` | `ActorId` |  | Who. Also the deterministic tiebreak when two ops share a clock. |

### `OpBefore`

What the register held BEFORE the op overwrote it.

Needs it and nothing else does, so it is handed to the capture callback
ALONGSIDE the op rather than being put INSIDE it. An op is a wire format: it crosses a
network and a disk, it is broadcast to every peer, and a peer does not need — and must
not be trusted with — the sender's idea of the previous value. Undo is a LOCAL concern. Widening the op to carry it would have doubled the traffic to serve one machine.

```ts
type OpBefore =

  | { kind: 'value'; value: OpValue | undefined }

  | { kind: 'entity'; data: SerializedNode | SerializedLink | SerializedGroup }

  | { kind: 'none' };
```

**Properties**

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `kind` | `'value'` |  |  |

### `OpPath`

Also has every member of `String`, listed on its own entry.

A property path. Dot-separated for nested registers: 'position', 'size',
'metadata.label', 'state.locked'.

THE PATH IS THE REGISTER KEY. Two concurrent writes to the SAME path conflict and
are resolved by last-writer-wins; two writes to DIFFERENT paths of the same entity
do not conflict at all and both survive. This is the whole point (see the header).

```ts
type OpPath = string;
```

### `OpTarget`

Also has every member of `String`, listed on its own entry.

The entity kinds a diagram is made of, plus the diagram itself.

`stroke` joins the list. Ink is DOCUMENT CONTENT — two people drawing
on the same board must converge — so a stroke is a first-class op target, not annotation
smuggled through a diagram-level `set`. (It very nearly WAS: `trackChange('strokes', …)`
is a diagram-level change event, and without `stroke` in this union the capture layer's
fall-through emitted `set(diagram, strokes, <live StrokeModel>)` — a class instance on the
wire, claiming the whole collection. See capture.ts / apply-op.ts.)

```ts
type OpTarget = 'node' | 'link' | 'group' | 'stroke' | 'diagram';
```

### `OpValue`

JSON-safe value. Ops must survive a network hop and a disk round-trip.

```ts
type OpValue = null | boolean | number | string | OpValue[] | { [k: string]: OpValue };
```

**Members**

- `toString(): string` — Returns a string representation of a string.
- `valueOf(): string` — Returns the primitive value of the specified object.
- `toLocaleString(): string` — Returns a date converted to a string using the current locale.
