Something went wrong. Try again.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139/** * Observed-Remove Set (OR-Set). * * An OR-Set is a value object (one `target`) that folds to a set of live * elements, projected as a `#set` keyed by element identity (see `collectionSet` * in `collection.ts`). An element is added either *by reference* — a pointer to * another value object — or as *inline data* — frozen JSON copied in place. The * unified `add` op carries both shapes, and the two carry different identities, * which is what a `remove` names: * * - **by ref** (`add` with `valueId`): the element's identity is `valueId` (the * referenced value object). A `remove` of that `valueId` cancels every live * ref element pointing at it. Adding the same `valueId` twice is one element * (refs dedup by target). * - **inline** (`add` without `valueId`): the element has no stable id, so its * identity is the add's own `opId`. A `remove` of that `opId` cancels it. Two * inline adds of identical data are two distinct, independently removable * elements. * * Like the registers (see `apply` in `reducer.ts`), the fold is a plain linear * pass over the causally-sorted op stream (`sortOperations`); convergence comes * from the deterministic sort, not a merge lattice. A well-formed remove * causally follows the adds it cancels (via `previousOpIds`), so those adds sort * earlier and are live when the remove runs. A later add of the same element * (a re-add) sorts after the remove and re-appears — observed-remove. * * An OR-Set folds over the shared collection node list (see `collection.ts`), so * a target may also receive RGA ops; the two families transform into one another * across the op stream. */import type { CollectionResult, CollectionState, Node } from "./collection.js";
/** * Add an element. With `valueId` it is a reference whose identity is that id; * without, it is inline data whose identity is this op's `opId`. */export interface AddOperation { $type: "local.crdt.ops#add"; opId: string; previousOpIds: string[]; /** The OR-Set value-object id this op writes to. */ target: string; /** Referenced value-object id; also the element's identity for removal. */ valueId?: string; /** Inline data added as an element (value-space JSON; refs are not resolved). */ value: unknown;}
/** Remove the element(s) with the given identity (a ref's `valueId` or an inline add's `opId`). */export interface RemoveOperation { $type: "local.crdt.ops#remove"; opId: string; previousOpIds: string[]; /** The OR-Set value-object id this op writes to. */ target: string; /** Identity to remove: a ref `valueId`, or an inline element's add `opId`. */ valueId: string;}
/** Any operation that contributes to an OR-Set value object. */export type OrSetOperation = AddOperation | RemoveOperation;
/** OR-Set folds share the collection state (see `collection.ts`). */export type OrSetState = CollectionState;
const OR_SET_TYPES = new Set(["local.crdt.ops#add", "local.crdt.ops#remove"]);
/** True if `op` is an OR-Set operation (vs. an RGA or register op). */export function isOrSetOp(op: { $type: string }): op is OrSetOperation { return OR_SET_TYPES.has(op.$type);}
/** * Apply one OR-Set operation to the running state: `(state, op) => state`. * * - `add` with `valueId` appends a ref element, unless that target is already * live (refs dedup by target). A tombstoned ref does not count as live, so it * does not block a re-add. * - `add` without `valueId` appends an inline element, always distinct (keyed by * `opId`). * - `remove` drops every live element under the named identity — ref elements * pointing at it, or the inline element added under it. */export function applyOrSet( state: CollectionState, op: OrSetOperation,): CollectionResult { const nodes: Node[] = state ? state.nodes : []; if (op.$type === "local.crdt.ops#add") { if (op.valueId !== undefined) { const target = op.valueId; const live = nodes.some( (n) => n.kind === "ref" && !n.deleted && n.target === target, ); const next: Node[] = live ? nodes : [ ...nodes, { kind: "ref", id: op.opId, srcId: op.opId, idx: 0, origin: "array", deleted: false, target, }, ]; return { nodes: next, lastOp: op }; } return { nodes: [ ...nodes, { kind: "data", id: op.opId, srcId: op.opId, idx: 0, origin: "array", deleted: false, value: op.value, }, ], lastOp: op, }; } const identity = op.valueId; return { nodes: nodes.filter( (n) => !( (n.kind === "ref" && n.target === identity) || (n.kind === "data" && n.id === identity) ), ), lastOp: op, };}