Something went wrong. Try again.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181import type { SetOperation } from "./lww.js";import type { IncrOperation } from "./increment.js";import type { AssignOperation } from "./assign.js";import { applyCollection, collectionSet, collectionSeq, isCollectionOp, type CollectionOperation, type CollectionState, type SeqItem,} from "./collection.js";import { isOrSetOp } from "./orset.js";import { sortOperations } from "./sorter.js";
/** Any operation that contributes to a register value. */export type CounterOperation = SetOperation | IncrOperation | AssignOperation;
/** Any operation that contributes to a value object — register, OR-Set, or RGA. */export type AnyOperation = CounterOperation | CollectionOperation;
export interface CounterResult { value: unknown; lastOp: CounterOperation;}
/** Reducer state: the result so far, or `null` before any op is applied. */export type CounterState = CounterResult | null;
/** * A value object as consumed by `materialize` — one register, read back, with * references left in place (resolved later, at materialization). The reducer's * document body (below) is the serialized form; this is the value-space form the * materializer walks. */export interface ValueObject { /** Register identity — the op `target`. */ id: string; /** The folded value, possibly containing references. */ value: unknown; /** opId of the last op folded into this register. */ lastOpId: string;}
/** A register value object in the document body. */export interface DocValue { $type: "local.crdt.document#value"; id: string; value: unknown; lastOpId: string;}
/** An OR-Set value object in the document body, keyed by element identity. */export interface DocSet { $type: "local.crdt.document#set"; id: string; set: Record<string, unknown>; lastOpId: string;}
/** An RGA value object in the document body, a run-length encoded sequence. */export interface DocSequence { $type: "local.crdt.document#sequence"; id: string; seq: SeqItem[]; lastOpId: string;}
/** One entry in a document body. */export type DocBodyItem = DocValue | DocSet | DocSequence;
/** The synthetic root every document body carries, even when empty. */function rootItem(): DocValue { return { $type: "local.crdt.document#value", id: "ROOT", value: {}, lastOpId: "ROOT", };}
/** * Reduce an op log into a document body — the leading `ROOT`, then one value * object per `target` in the order each first appears. * * The log is first sorted deterministically (`sortOperations`, which also drops * ops whose dependencies can never be satisfied), then grouped by `target` and * folded. A register target folds with `apply` and projects to a `#value`. A * collection target (OR-Set and/or RGA) folds over the shared node substrate * (see `collection.ts`); its projection — `#set` or `#sequence` — is chosen by * the family of its last op. References inside values are left untouched; they * are resolved later, at materialization. */export function reduceOperations(operations: AnyOperation[]): DocBodyItem[] { const byTarget = new Map<string, AnyOperation[]>(); for (const op of sortOperations(operations)) { const group = byTarget.get(op.target); if (group) group.push(op); else byTarget.set(op.target, [op]); }
const byId = new Map<string, DocBodyItem>(); for (const [target, ops] of byTarget) { // A target is a collection (OR-Set and/or RGA) or a register. Collection ops // share one node substrate, so the two families may mix on a single target // and transform across the op stream (see `collection.ts`); a register target // uses the register fold. (Mixing register and collection ops on one target // is undefined.) if (isCollectionOp(ops[0])) { const folded = (ops as CollectionOperation[]).reduce<CollectionState>( applyCollection, null, ); if (!folded) continue; const lastOpId = folded.lastOp.opId; if (isOrSetOp(folded.lastOp)) { byId.set(target, { $type: "local.crdt.document#set", id: target, set: collectionSet(folded), lastOpId, }); } else { byId.set(target, { $type: "local.crdt.document#sequence", id: target, seq: collectionSeq(folded), lastOpId, }); } continue; } const folded = (ops as CounterOperation[]).reduce<CounterState>(apply, null); if (!folded) continue; byId.set(target, { $type: "local.crdt.document#value", id: target, value: folded.value, lastOpId: folded.lastOp.opId, }); }
// ROOT always leads the body; if no op targeted it, fall back to the synthetic // empty root. Every other target follows in first-appearance order. const root = byId.get("ROOT") ?? rootItem(); byId.delete("ROOT"); return [root, ...byId.values()];}
/** * Apply one register operation to the running state: `(state, op) => state`. * * A register is read by folding a sorted op stream (see `sortOperations`) with * the standard reducer idiom — `operations.reduce(apply, null)`. An empty * stream stays `null`. * * - `set` overwrites the value (last write wins). * - `incr` adds its delta to the value when it is a number, otherwise builds * up from zero (no prior set, or a set to a non-number). * - `assign` shallow-merges its keys onto the value when it is a plain object, * otherwise builds up from an empty object (no prior set, or a set to a * non-object such as a number, string, null, or array). */export function apply(state: CounterState, op: CounterOperation): CounterResult { if (op.$type === "local.crdt.ops#set") { return { value: op.value, lastOp: op }; } const prev = state?.value; if (op.$type === "local.crdt.ops#assign") { const base = isPlainObject(prev) ? prev : {}; return { value: { ...base, ...op.value }, lastOp: op }; } const base = typeof prev === "number" ? prev : 0; return { value: base + op.delta, lastOp: op };}
/** A non-null, non-array object — the only kind `assign` merges onto. */function isPlainObject(value: unknown): value is Record<string, unknown> { return typeof value === "object" && value !== null && !Array.isArray(value);}