@mmstack/primitives 21.7.0 → 21.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mmstack/primitives",
3
- "version": "21.7.0",
3
+ "version": "21.8.1",
4
4
  "keywords": [
5
5
  "angular",
6
6
  "signals",
@@ -2806,6 +2806,11 @@ type Key$2 = string | number;
2806
2806
  * One structural operation. `set` on a key that did not previously exist carries NO `prev`
2807
2807
  * property (an absent key is not the same as a key holding `undefined` — the merge3 lesson),
2808
2808
  * which is what lets {@link invertBatch} invert an add into a delete.
2809
+ *
2810
+ * `clear` is a sync-layer intent, not a value change: it retires a per-path register (the
2811
+ * observed-remove half of a subtree replace) and contributes NOTHING to a value: {@link applyOps}
2812
+ * treats it as a no-op and the structural diff ({@link diffOps}) never emits one. Only the sync
2813
+ * emission layer produces clears; they ride batches so undo/rebase plumbing can pass them through.
2809
2814
  */
2810
2815
  type StoreOp = {
2811
2816
  kind: 'set';
@@ -2816,6 +2821,9 @@ type StoreOp = {
2816
2821
  kind: 'delete';
2817
2822
  path: readonly Key$2[];
2818
2823
  prev: unknown;
2824
+ } | {
2825
+ kind: 'clear';
2826
+ path: readonly Key$2[];
2819
2827
  };
2820
2828
  /** One emission: every op derived from one commit window (a tick), in path order. */
2821
2829
  type OpBatch = {
@@ -2888,14 +2896,17 @@ declare function applyOps<T>(root: T, ops: OpBatch | readonly StoreOp[]): T;
2888
2896
  * Pure reference-pruned structural diff of two roots into minimal ops (the emission core of
2889
2897
  * {@link opLog}, exported so code outside a log can produce a batch — e.g. diffing a scratch
2890
2898
  * draft against a replica's current value to route a write to its owner). Trusts the
2891
- * copy-on-write contract: an untouched subtree that kept its reference is skipped.
2899
+ * copy-on-write contract: an untouched subtree that kept its reference is skipped. Emits only
2900
+ * `set` and `delete`; `clear` is an emission-layer intent, never a diff product.
2892
2901
  */
2893
2902
  declare function diffOps(prev: unknown, next: unknown): StoreOp[];
2894
2903
  /**
2895
2904
  * Inverts a batch for undo: reversed order, `set`↔its own inverse (an add — a `set` with no
2896
2905
  * `prev` — inverts to a `delete`; a `delete` inverts to a `set` restoring `prev`). Feed the
2897
2906
  * result to {@link OpLog.apply}. Requires the ops' `prev`s, which in-memory batches always
2898
- * carry a wire-serialized batch that stripped them is not invertible.
2907
+ * carry (a wire-serialized batch that stripped them is not invertible). A `clear` is skipped:
2908
+ * it never changed a value, so it has no independent inverse (the accompanying subtree `set`'s
2909
+ * `prev` subsumes restoration).
2899
2910
  */
2900
2911
  declare function invertBatch(batch: OpBatch | readonly StoreOp[]): StoreOp[];
2901
2912
  /**
@@ -3303,11 +3314,51 @@ type HlcClock = {
3303
3314
  */
3304
3315
  declare function createHlcClock(now?: () => number): HlcClock;
3305
3316
 
3306
- declare const OP_PROTO_VERSION = 1;
3317
+ /**
3318
+ * Wire protocol version. Version 2 ops carry `cites` + `epoch` (the dot-citation register);
3319
+ * envelopes from other versions are dropped loudly: an op without citations cannot be merged
3320
+ * soundly (it would supersede nothing and its siblings would accumulate forever), so versions
3321
+ * are never silently mixed.
3322
+ */
3323
+ declare const OP_PROTO_VERSION = 2;
3307
3324
  type Key = string | number;
3308
3325
  /**
3309
- * The wire/journal recor. `writer` is an opaque principal pseudonym
3310
- * natural identity never enters the envelope; `origin` identifies the emitting log instance.
3326
+ * A dot: the globally-unique identity of one op at one path, the emitting replica (`origin`)
3327
+ * plus its clock stamp. Origins are unique per replica and their clocks are monotone, so a dot
3328
+ * never collides. Citing a dot means "I observed this write and am replacing it".
3329
+ */
3330
+ type Dot = {
3331
+ readonly origin: string;
3332
+ readonly hlc: Hlc;
3333
+ };
3334
+ /**
3335
+ * A frozen observation point: the set of writes a replica had seen at a moment in time, captured
3336
+ * cheaply as a monotone sequence marker. Stamping emission against a frontier makes an op cite the
3337
+ * siblings that were live THEN, not the ones live now, so an edit made against stale knowledge (a
3338
+ * fork committed after the base moved on) stays a concurrent sibling instead of superseding writes
3339
+ * it never observed.
3340
+ */
3341
+ type DotFrontier = {
3342
+ readonly seq: number;
3343
+ };
3344
+ /**
3345
+ * A wire op: a structural {@link StoreOp} plus the causal metadata the register needs.
3346
+ * `cites` lists the sibling dot(s) the writer observed at the op's path when it wrote;
3347
+ * exactly those get superseded; a write nobody cited stays live as a concurrent sibling.
3348
+ * `epoch` is the op's precedence term: stamped at emission as the max of the cited dots'
3349
+ * epochs and the writer's own prior epoch at this path (monotone per writer per path), +1
3350
+ * for an authority-bumped write. `prev` (on the underlying op) stays purely an inversion
3351
+ * hint for undo/rebase and plays no role in convergence.
3352
+ */
3353
+ type SyncOp = StoreOp & {
3354
+ readonly cites: readonly Dot[];
3355
+ readonly epoch: number;
3356
+ };
3357
+ /**
3358
+ * The wire/journal record. `writer` is an opaque principal pseudonym (natural identity never
3359
+ * enters the envelope); `origin` identifies the emitting replica. All ops in one envelope share
3360
+ * the envelope stamp, and an envelope carries at most one op per path, so `(origin, hlc)` is a
3361
+ * unique dot per path register.
3311
3362
  */
3312
3363
  type OpEnvelope = {
3313
3364
  readonly proto: number;
@@ -3316,20 +3367,35 @@ type OpEnvelope = {
3316
3367
  readonly version: number;
3317
3368
  readonly hlc: Hlc;
3318
3369
  readonly policyVersion: number;
3319
- readonly ops: readonly StoreOp[];
3370
+ readonly ops: readonly SyncOp[];
3320
3371
  };
3321
3372
  declare const CONFLICT_BRAND = "~mmstackConflict";
3322
3373
  /**
3323
- * A preserved (jj-style) conflict: both sides survive as data, sync never blocks, and
3324
- * resolution is just a later write. String-branded so it survives structured clone.
3374
+ * A preserved (jj-style) conflict: every concurrent write survives as data, sync never blocks,
3375
+ * and resolution is just a later write (which cites all surviving dots and collapses the set).
3376
+ * `siblings` holds every live top-precedence value, winner first; a concurrent delete surfaces
3377
+ * as `undefined`. `mine`/`theirs` alias the first two entries, the shape two-sided reconcile
3378
+ * seams (fork rebase) produce and consume. String-branded so it survives structured clone.
3325
3379
  */
3326
3380
  type Conflicted<T = unknown> = {
3327
3381
  readonly [CONFLICT_BRAND]: true;
3382
+ readonly siblings: readonly T[];
3328
3383
  readonly mine: T;
3329
3384
  readonly theirs: T;
3330
3385
  readonly ancestor?: T;
3331
3386
  };
3332
3387
  declare function isConflicted<T = unknown>(value: unknown): value is Conflicted<T>;
3388
+ /**
3389
+ * Deterministic, total well-formedness check for a received envelope. Returns a short reason
3390
+ * string when the envelope must be rejected WHOLE, or `null` when it is well-formed. It reads only
3391
+ * the envelope (no clock, no local state), so every replica accepts or rejects a given envelope
3392
+ * identically. This validates SHAPE, not authority: it closes malformed input (control characters
3393
+ * in an id or path segment that could forge a path-key separator, a non-integer version, an unknown
3394
+ * op kind, a negative epoch, forged cites, a root delete, two ops racing on one path). Authority and
3395
+ * access control stay at the relay; direct peer-to-peer rooms are trust-full for authority, so this
3396
+ * shape check is a peer's only line against a malformed neighbor.
3397
+ */
3398
+ declare function validateEnvelope(env: OpEnvelope): string | null;
3333
3399
  type MergeContext = {
3334
3400
  readonly path: readonly Key[];
3335
3401
  };
@@ -3352,32 +3418,146 @@ declare const preserve: MergeFn;
3352
3418
  * array. Items are matched by key; per-item fields merge via `merge3` against the ancestor
3353
3419
  * item; items added on either side survive; an item removed on either side and unedited on
3354
3420
  * the other stays removed. Item ORDER follows `mine` (the total-order winner), with `theirs`-
3355
- * only additions appended positional merging is out of scope (fractional indexing is the
3356
- * known upgrade if dogfooding demands it). Arrays still TRAVEL as whole-value sets; identity
3357
- * only shapes conflict resolution, so the wire format is untouched.
3421
+ * only additions appended, and arrays still TRAVEL as whole-value sets. For a list whose elements
3422
+ * move and edit concurrently, model it as a keyed container (a record of elements ordered by
3423
+ * `posBetween`) instead: `insertElement`/`moveElement`/`removeElement` write per element, so a
3424
+ * reorder and a concurrent edit both survive and elements travel one at a time.
3358
3425
  */
3359
3426
  declare function keyedArray(identity: (item: unknown) => unknown, opt?: {
3360
3427
  item?: MergeFn;
3361
3428
  }): MergeFn;
3429
+ /**
3430
+ * One retained concurrent write at a path. The register keeps at most one sibling per origin
3431
+ * (a replica's newer op replaces its own older one) plus a per-origin supersession watermark,
3432
+ * so state stays bounded by the concurrent-writer count, not the op count.
3433
+ */
3434
+ type SyncSibling = {
3435
+ readonly kind: 'set' | 'delete' | 'clear';
3436
+ /** The written value for a `set`; absent for `delete`/`clear`. */
3437
+ readonly value?: unknown;
3438
+ /** The emitter's inversion hint, kept for value-merging folds; irrelevant to convergence. */
3439
+ readonly prev?: unknown;
3440
+ readonly writer: string;
3441
+ readonly origin: string;
3442
+ readonly hlc: Hlc;
3443
+ readonly epoch: number;
3444
+ };
3445
+ /**
3446
+ * What a fold decided for a path: a value to graft, a key removal, or `clear` (the register
3447
+ * abstains and the value at that path comes from the nearest ancestor write instead).
3448
+ */
3449
+ type FoldResult = {
3450
+ readonly kind: 'set';
3451
+ readonly value: unknown;
3452
+ } | {
3453
+ readonly kind: 'delete';
3454
+ } | {
3455
+ readonly kind: 'clear';
3456
+ };
3457
+ /**
3458
+ * A conflict-resolution fold over the live sibling set of one path register. Called with the
3459
+ * full set of causally-maximal concurrent writes; must be a pure function of that SET (never
3460
+ * of arrival order); then the materialized value converges on every peer by construction.
3461
+ *
3462
+ * A custom fold MUST keep `epoch` as its outermost comparison (prefer max-epoch siblings
3463
+ * unconditionally, as {@link compareSiblings} does). The epoch exists to retire stale values: a
3464
+ * long-partitioned replica can resurface an old write that nothing ever cited, and without the
3465
+ * epoch gate a fold that ranks it high would snap the value backwards. Folding a lower-epoch
3466
+ * sibling's content into the result reopens exactly that hazard.
3467
+ */
3468
+ type FoldFn = (siblings: readonly SyncSibling[], ctx: MergeContext) => FoldResult;
3469
+ type FoldPolicyEntry = {
3470
+ /** `'todos.*.title'` or a segment array; `'*'` matches exactly one segment. */
3471
+ readonly path: string | readonly Key[];
3472
+ readonly fold: FoldFn;
3473
+ };
3474
+ /**
3475
+ * The register's total order: max by `(epoch, kind-class, hlc, writer, origin)`, where `set`
3476
+ * and `delete` outrank `clear` at equal epoch. Epoch first makes an authority bump decisive
3477
+ * regardless of clocks (and closes stale-value resurrection); the kind-class tier makes a
3478
+ * concurrent edit's survival of a subtree replace categorical rather than a clock race; origin
3479
+ * last keeps the order strict when two replicas share a writer and a stamp.
3480
+ */
3481
+ declare function compareSiblings(a: SyncSibling, b: SyncSibling): number;
3482
+ /** Last-writer-wins over the live sibling set: the {@link compareSiblings} maximum, as-is. */
3483
+ declare const defaultFold: FoldFn;
3484
+ /**
3485
+ * Per-path register state, serializable: the live + superseded siblings and the per-origin
3486
+ * supersession watermarks. This is what a snapshot ships, never a folded value: a
3487
+ * joiner seeded with only a value cannot supersede or be superseded correctly afterwards.
3488
+ */
3489
+ type RegisterCheckpoint = {
3490
+ readonly path: readonly Key[];
3491
+ readonly siblings: readonly SyncSibling[];
3492
+ readonly water: Readonly<Record<string, Hlc>>;
3493
+ };
3362
3494
  type ConvergingApply = {
3363
3495
  /**
3364
- * Fold an envelope into the register map and return the ops the local store must apply
3365
- * (post-dominance, post-policy, including replays of newer descendant winners). Pass
3366
- * `local: true` for envelopes this peer emitted itself: registered, nothing returned.
3496
+ * Fold an envelope into the per-path registers and return the materialization deltas the
3497
+ * local store must apply: plain `set`/`delete` ops (never `clear`), `[]` when no fold
3498
+ * winner changed. Pass `local: true` for envelopes this peer emitted itself: registered,
3499
+ * nothing returned, unless `reconcile: true` is also set, which registers the op locally AND
3500
+ * returns the fold delta (a fork commit lands as a concurrent sibling, so the store must move to
3501
+ * the fold winner rather than the raw committed value). Pass `frontier` to reject ops at or below
3502
+ * a pruned horizon, so a straggler older than compacted state can never resurrect.
3367
3503
  */
3368
3504
  ingest(env: OpEnvelope, opt?: {
3369
3505
  local?: boolean;
3506
+ reconcile?: boolean;
3507
+ frontier?: Hlc;
3370
3508
  }): StoreOp[];
3371
- /** Drop all registers (snapshot compaction / rehydration boundary). */
3509
+ /**
3510
+ * Stamp locally-diffed ops for emission: each op cites the live dots at its path and adopts
3511
+ * `max(observed epoch, own prior epoch at the path)`, plus 1 per path when `bump` is set (an
3512
+ * authority override). A `set`/`delete` at a path additionally expands into one `clear` per
3513
+ * live descendant register (the observed-remove half of a subtree replace), so a concurrent
3514
+ * descendant edit this replica never saw stays live and survives the replace. Pass `frontier` to
3515
+ * cite only the siblings observed as of that point (a fork committing what it saw when it forked),
3516
+ * so mid-flight writes stay concurrent instead of being superseded.
3517
+ */
3518
+ stamp(ops: readonly StoreOp[], opt?: {
3519
+ bump?: boolean;
3520
+ frontier?: DotFrontier;
3521
+ }): SyncOp[];
3522
+ /** Capture the current observation frontier, for later scoped emission; O(1). */
3523
+ captureFrontier(): DotFrontier;
3524
+ /** The live (causally-maximal) siblings at a path: the emission-frontier read. */
3525
+ liveAt(path: readonly Key[]): readonly SyncSibling[];
3526
+ /**
3527
+ * Deepest-live-wins materialization of the whole tree from the current register state: the
3528
+ * root register's fold value with every live descendant fold grafted on. This is what a
3529
+ * replica's root reads after loading a checkpoint, so a joiner given only register state
3530
+ * (never a folded value) can derive its root through its OWN fold configuration.
3531
+ */
3532
+ materialize(): unknown;
3533
+ /** Serializable register state for a snapshot/seed checkpoint. */
3534
+ checkpoint(): RegisterCheckpoint[];
3535
+ /** Merge checkpointed register state in (idempotent; call after `reset()` on hydrate). */
3536
+ load(registers: readonly RegisterCheckpoint[]): void;
3537
+ /**
3538
+ * Drop settled state at or below the stability frontier: superseded siblings and their
3539
+ * watermarks, plus a register whose only live winner is a below-frontier tombstone once nothing
3540
+ * else still materializes its key (no live descendant register, no live ancestor `set` value
3541
+ * holding it), so state stays bounded under key churn. Never changes a fold above the frontier;
3542
+ * pair with `ingest`'s `frontier` so pruned ops are rejected on re-delivery.
3543
+ */
3544
+ prune(frontier: Hlc): void;
3545
+ /** Drop all registers (snapshot compaction / rehydration boundary). Emission epoch floors survive. */
3372
3546
  reset(): void;
3373
3547
  };
3374
3548
  /**
3375
- * The unsequenced-topology convergence core: a per-path last-writer-wins
3376
- * register map over the total order (hlc, writer), with subtree dominance. Order-independent:
3377
- * any arrival order of the same envelope set yields the same state.
3549
+ * The unsequenced-topology convergence core: a dot-citation multi-value register per path.
3550
+ * An op supersedes exactly the sibling dots it cites; uncited concurrent writes stay live; a
3551
+ * pluggable fold resolves the live set at read. Both the live set and any pure fold over it
3552
+ * are functions of the delivered op SET, so any arrival order of the same envelopes (split,
3553
+ * duplicated, cites-before-ops) yields the same state.
3378
3554
  */
3379
3555
  declare function createConvergingApply(opt?: {
3380
3556
  policies?: readonly MergePolicyEntry[];
3557
+ /** Per-path custom folds; take precedence over the `policies` mapping. */
3558
+ folds?: readonly FoldPolicyEntry[];
3559
+ /** The local replica id: lets loaded checkpoints restore this replica's emission epoch floors. */
3560
+ origin?: string;
3381
3561
  }): ConvergingApply;
3382
3562
  type RebaseResult<T = unknown> = {
3383
3563
  root: T;
@@ -3404,11 +3584,41 @@ type OpSyncOptions = {
3404
3584
  readonly origin?: string;
3405
3585
  readonly policyVersion?: number;
3406
3586
  readonly policies?: readonly MergePolicyEntry[];
3587
+ /** Per-path custom register folds; take precedence over the `policies` mapping. */
3588
+ readonly folds?: readonly FoldPolicyEntry[];
3407
3589
  readonly clock?: HlcClock;
3408
3590
  readonly injector?: Injector;
3409
3591
  readonly driver?: OpLogDriver;
3410
3592
  /** A version gap from a known origin (missed envelopes) — the resync hook. */
3411
3593
  readonly onGap?: (origin: string, expected: number, got: number) => void;
3594
+ /** A received envelope rejected as malformed by {@link validateEnvelope}, with the reason. */
3595
+ readonly onReject?: (env: OpEnvelope, reason: string) => void;
3596
+ };
3597
+ /**
3598
+ * A peer-state checkpoint: the current root as the materialization base, the per-path register
3599
+ * state (siblings + watermarks), and the per-origin version watermark. Register state rides
3600
+ * along because a value alone is not enough to join a room: a peer hydrated from a bare value
3601
+ * cannot tell a late straggler from a live concurrent write, so an already-superseded op would
3602
+ * resurrect on it while every established peer keeps ignoring it.
3603
+ */
3604
+ type OpSyncCheckpoint<T = unknown> = {
3605
+ readonly root: T;
3606
+ readonly registers: readonly RegisterCheckpoint[];
3607
+ readonly wm: Readonly<Record<string, number>>;
3608
+ };
3609
+ /**
3610
+ * A {@link Fork} of a synced store. Everything a plain fork does, plus `rebase()`. Committing it
3611
+ * cites only the dots the fork observed when it was created (or last rebased), so an edit that
3612
+ * landed on the base mid-flight stays a concurrent sibling and the configured fold decides between
3613
+ * them; an approval click never silently discards a concurrent write.
3614
+ */
3615
+ type SyncedFork<T> = Fork<T> & {
3616
+ /**
3617
+ * Re-observe the base: a following `commit()` cites the dots visible NOW, so the fork's edits
3618
+ * supersede everything currently on the base (the reviewed-and-apply step). Keeps the staged
3619
+ * edits; only advances what the next commit claims to have seen.
3620
+ */
3621
+ rebase(): void;
3412
3622
  };
3413
3623
  type OpSync<T = unknown> = {
3414
3624
  readonly origin: string;
@@ -3418,37 +3628,137 @@ type OpSync<T = unknown> = {
3418
3628
  receive(env: OpEnvelope): void;
3419
3629
  /** Synchronously emit any pending local delta now. */
3420
3630
  flush(): void;
3631
+ /**
3632
+ * Run `fn` and emit the writes it makes as authority-bumped ops: each written path gets
3633
+ * epoch `max(observed at that path) + 1`, and a bumped subtree replace bumps every path it
3634
+ * clears, so concurrent edits under it lose the fold instead of surviving. Scoped and
3635
+ * synchronous: writes before/after emit normally. WHO may bump is admission policy at the
3636
+ * transport/relay, not merge semantics; the register accepts any well-formed epoch.
3637
+ */
3638
+ override(fn: () => void): void;
3639
+ /**
3640
+ * Capture the current observation frontier: the set of writes this peer has seen right now, as a
3641
+ * cheap marker. Pair with {@link commitScope} to emit later against what was observed then (the
3642
+ * fork-commit seam); {@link syncedFork} wires both together.
3643
+ */
3644
+ captureFrontier(): DotFrontier;
3645
+ /**
3646
+ * Run `fn` and stamp the writes it makes against `frontier` rather than the current register
3647
+ * state, so they cite only the siblings observed as of that frontier. A write against a stale
3648
+ * frontier lands as a concurrent sibling (the fold decides) instead of superseding writes it
3649
+ * never saw. Scoped and synchronous, like {@link override}.
3650
+ */
3651
+ commitScope(frontier: DotFrontier, fn: () => void): void;
3421
3652
  /** Per-origin latest versions — the handshake watermark. */
3422
3653
  watermark(): Record<string, number>;
3423
- /** The current root + watermark, for answering a peer's hello. */
3424
- snapshot(): {
3425
- root: T;
3426
- wm: Record<string, number>;
3427
- };
3654
+ /** The full checkpoint (root + register state + watermark), for answering a peer's hello. */
3655
+ snapshot(): OpSyncCheckpoint<T>;
3428
3656
  /**
3429
- * Emit the CURRENT root as a root-set envelope the fresh-room seed of the relay
3430
- * contract (a room's snapshot root becomes complete once seeded).
3657
+ * Emit the CURRENT root as a root-set envelope: the fresh-room seed of the relay contract.
3658
+ * In a fresh room nothing was observed, so the op carries no cites and no clears; on a
3659
+ * non-fresh instance it behaves as a whole-root replace (cites the root register's live
3660
+ * dots and clears observed descendant registers).
3431
3661
  */
3432
3662
  seed(): void;
3433
3663
  /**
3434
- * Replace local state with a peer's snapshot, atomically (one notification wave).
3435
- * Local envelopes the snapshot doesn't cover (per its watermark) are re-applied on
3436
- * top, so writes made before hydration are never silently lost.
3664
+ * Replace local state with a peer's checkpoint, atomically (one notification wave): adopt
3665
+ * its root, load its register state, and fold its watermark. Local envelopes the checkpoint
3666
+ * doesn't cover (per its watermark) are re-applied on top, so writes made before hydration
3667
+ * are never silently lost; the next emitted version continues past the folded watermark.
3668
+ * Pass `pending` (a durable outbox) to rebase from it instead of the bounded in-memory recent
3669
+ * ring, so an offline burst larger than that ring is not dropped from the rebase.
3437
3670
  */
3438
- hydrate(root: T, wm?: Record<string, number>): void;
3671
+ hydrate(state: OpSyncCheckpoint<T>, pending?: readonly OpEnvelope[]): void;
3439
3672
  /**
3440
- * Re-inject this origin's persisted local envelopes on boot (a durable outbox), WITHOUT minting
3441
- * new versions: each is applied to the store (echo-free), registered as a local winner, and handed
3442
- * to subscribers so a transport can resend the unacknowledged tail. `highWater` is the highest
3443
- * version this origin ever emitted (>= every `env.version`); the next mint continues past it, so a
3444
- * version acked before the reboot but dropped from a debounced outbox never collides. Call on a
3445
- * FRESH instance, before any `receive`/`hydrate` restoring onto already-ingested remote winners
3446
- * would wrongly let a stale local op override them.
3673
+ * Re-inject a persisted local outbox on boot, WITHOUT minting new versions: each envelope is
3674
+ * applied to the store (echo-free), registered as a local winner, and handed to subscribers so a
3675
+ * transport can resend the unacknowledged tail VERBATIM under its recorded origin (idempotent at
3676
+ * receivers). Envelopes are accepted regardless of origin; their versions track per recorded
3677
+ * origin. `highWater` is that origin's emit high-water (>= every `env.version`, including any acked
3678
+ * then dropped from a debounced outbox), so a resent tail never re-mints below what the room may
3679
+ * have sequenced. This instance mints on its OWN origin: pass a fresh per-boot origin and new
3680
+ * writes start clean, so two clones of one outbox resend an identical tail (a duplicate) yet their
3681
+ * new writes land on distinct origins and never collide. Call on a FRESH instance, before any
3682
+ * `receive`/`hydrate` — restoring onto already-ingested remote winners would wrongly let a stale
3683
+ * local op override them.
3447
3684
  */
3448
3685
  restore(envs: readonly OpEnvelope[], highWater?: number): void;
3686
+ /**
3687
+ * Reclaim settled register state at or below a stability frontier: superseded siblings, their
3688
+ * watermarks, and lone tombstones nothing still materializes. Never changes the current value, so
3689
+ * it is safe to call whenever a transport learns the frontier has advanced (a straggler below it
3690
+ * is rejected at ingest, so nothing can resurrect). Without it, per-path register state grows with
3691
+ * every path ever written; with it, state stays bounded by what is live above the frontier.
3692
+ */
3693
+ prune(frontier: Hlc): void;
3449
3694
  destroy(): void;
3450
3695
  };
3451
3696
  declare function opSync<T extends object>(source: WritableSignal<T>, opt: OpSyncOptions): OpSync<T>;
3697
+ /**
3698
+ * Fork a synced store for isolated edits (an agent branch, a staged review), keeping the correct
3699
+ * emission semantics on commit. The fork observes the base as it was when this call ran; committing
3700
+ * emits its diff citing only those observed dots, so an edit that landed on the base mid-flight
3701
+ * stays a concurrent sibling and the configured fold decides between them, rather than the commit
3702
+ * overwriting a write it never saw. `rebase()` re-observes the base (a following commit then
3703
+ * supersedes what is visible now, the reviewed-and-apply step). Pass the same `store` and `sync`
3704
+ * that are wired together; the fork is a plain {@link Fork} otherwise, so `forkStore` itself stays
3705
+ * sync-agnostic.
3706
+ */
3707
+ declare function syncedFork<T extends Record<string, any>>(sync: OpSync, store: WritableSignalStore<T>, opt?: ForkStoreOptions<T>): SyncedFork<T>;
3708
+
3709
+ /**
3710
+ * Reserved key holding an element's fractional position inside a keyed container. It lives INSIDE
3711
+ * the element (at `[container, elementKey, '~pos']`), so a reorder is a one-field write that never
3712
+ * collides with a concurrent edit to the element's data. It stays visible on the materialized
3713
+ * element value; do not read, write, or strip it by hand, use the helpers in this file.
3714
+ */
3715
+ declare const POS_SEGMENT = "~pos";
3716
+ /**
3717
+ * A compact position string strictly between `before` and `after`, ordered by plain string
3718
+ * comparison. Pass `undefined` for an open end: `posBetween()` seeds the first element,
3719
+ * `posBetween(last)` appends, `posBetween(undefined, first)` prepends. Repeated inserts into the
3720
+ * same gap grow the string one digit at a time rather than colliding, and the result is never equal
3721
+ * to either neighbor. `before` must sort before `after`.
3722
+ */
3723
+ declare function posBetween(before?: string, after?: string): string;
3724
+ /** An element of a keyed container in reading order: its key, its position, and its value. */
3725
+ type OrderedEntry<T> = {
3726
+ readonly key: string;
3727
+ readonly pos: string;
3728
+ readonly value: T;
3729
+ };
3730
+ /**
3731
+ * A keyed container's elements in reading order. Order is a pure function of the materialized
3732
+ * value: elements sort by their `~pos` string, ties broken by key. An element whose `~pos` is
3733
+ * missing or not a string is ordered as if its position were the empty string (it sorts first,
3734
+ * key breaking the tie), so a peer that dropped the position field still lands somewhere
3735
+ * deterministic on every replica.
3736
+ */
3737
+ declare function orderedEntries<T>(container: Record<string, T>): OrderedEntry<T>[];
3738
+ /** A store node (or plain writable signal) holding a keyed container: a RECORD keyed by element id. */
3739
+ type ContainerNode<T> = WritableSignal<Record<string, T>>;
3740
+ /**
3741
+ * Insert `value` under `key` at `index` in reading order (default: append). The position is
3742
+ * computed from the neighbors at that index, so the element lands where asked without renumbering
3743
+ * any sibling. A keyed container is a RECORD, never an array, so this is a per-key write the sync
3744
+ * layer diffs on its own. Returns the assigned position. Re-inserting an existing key overwrites it.
3745
+ */
3746
+ declare function insertElement<T extends object>(container: ContainerNode<T>, key: string, value: T, index?: number): string;
3747
+ /**
3748
+ * Move the element at `key` to `index` in reading order. This writes ONLY the element's `~pos`
3749
+ * field, so it never conflicts with a concurrent edit to the same element's data (they land on
3750
+ * different paths and both survive). Returns the new position, or `undefined` if `key` is absent.
3751
+ */
3752
+ declare function moveElement<T extends object>(container: ContainerNode<T>, key: string, index: number): string | undefined;
3753
+ /** Remove the element at `key`. Deletes the whole element (a per-key delete the sync layer folds). */
3754
+ declare function removeElement<T>(container: ContainerNode<T>, key: string): void;
3755
+ /**
3756
+ * Reassign every element's position to a fresh, evenly spaced sequence, as an authority write:
3757
+ * each `~pos` set is epoch-bumped so it wins the merge against any concurrent move, while leaving
3758
+ * concurrent edits to element DATA untouched (only the `~pos` fields are written). Use this to
3759
+ * reclaim precision after many same-gap inserts. Existing reading order is preserved.
3760
+ */
3761
+ declare function rebalanceContainer<T extends object>(sync: Pick<OpSync, 'override'>, container: ContainerNode<T>): void;
3452
3762
 
3453
3763
  type StoreHistory = {
3454
3764
  readonly canUndo: Signal<boolean>;
@@ -3457,6 +3767,12 @@ type StoreHistory = {
3457
3767
  undo(): void;
3458
3768
  /** Re-apply the most recently undone change. */
3459
3769
  redo(): void;
3770
+ /**
3771
+ * Close the current coalescing run: the next tracked change starts a NEW undo entry even if
3772
+ * it lands inside the `coalesce` window. Call it on the boundaries your UX considers an
3773
+ * action — a field blur, a selection change, a drag drop. A no-op without `coalesce`.
3774
+ */
3775
+ checkpoint(): void;
3460
3776
  /** Forget all tracked history (e.g. after a save boundary). */
3461
3777
  clear(): void;
3462
3778
  destroy(): void;
@@ -3468,11 +3784,29 @@ type StoreHistoryOptions = CreateOpLogOptions & {
3468
3784
  * The change stream to track. Defaults to self-diffing `source` (every change to the store
3469
3785
  * becomes undoable). For collaborative-safe undo, pass a sync client's LOCAL envelope stream
3470
3786
  * (e.g. an `opSync`'s `subscribe`, which fires only for this peer's own writes) — remote
3471
- * peers' changes then never land on your undo stack.
3787
+ * peers' changes then never land on your undo stack. When the stream exposes `flush` (an
3788
+ * `opSync` does), undo/redo drain it synchronously so their own emissions never echo back
3789
+ * onto the stack as fresh entries.
3472
3790
  */
3473
3791
  readonly track?: {
3474
3792
  subscribe(cb: (batch: OpBatch) => void): () => void;
3793
+ flush?(): void;
3794
+ };
3795
+ /**
3796
+ * Merge rapid consecutive edits into ONE undo entry, so a typing run undoes as a unit
3797
+ * instead of per keystroke. A tracked change arriving within `ms` of the previous one AND
3798
+ * touching the same paths with the same op kinds extends the previous entry (set
3799
+ * `samePath: false` to merge on time alone); anything else — a different field, a kind
3800
+ * change, a pause longer than `ms`, a `checkpoint()`, an undo/redo — starts a new entry.
3801
+ * The window is measured between consecutive changes, so an unbroken run keeps merging.
3802
+ * Undoing a merged entry is exactly equivalent to undoing its changes one by one.
3803
+ */
3804
+ readonly coalesce?: {
3805
+ readonly ms: number;
3806
+ readonly samePath?: boolean;
3475
3807
  };
3808
+ /** Clock for the coalescing window (injectable for tests; default `Date.now`). */
3809
+ readonly now?: () => number;
3476
3810
  };
3477
3811
  /**
3478
3812
  * Undo/redo for a copy-on-write store, built on the op-log: each tracked change is stored as
@@ -3482,7 +3816,8 @@ type StoreHistoryOptions = CreateOpLogOptions & {
3482
3816
  *
3483
3817
  * Composes with sync for collaborative undo: pass `track: syncClient` so only YOUR writes are
3484
3818
  * undoable, while `undo()` emits a normal op that propagates to peers (it writes through the
3485
- * store, which the sync client picks up).
3819
+ * store, which the sync client picks up). Coalescing groups only this stack's entries — what
3820
+ * goes over the wire is untouched.
3486
3821
  */
3487
3822
  declare function storeHistory<T extends object>(source: WritableSignal<T>, opt?: StoreHistoryOptions): StoreHistory;
3488
3823
 
@@ -4146,5 +4481,5 @@ type CreateHistoryOptions<T> = Omit<CreateSignalOptions<T[]>, 'equal'> & {
4146
4481
  */
4147
4482
  declare function withHistory<T>(sourceOrValue: WritableSignal<T> | T, opt?: CreateHistoryOptions<T>): SignalWithHistory<T>;
4148
4483
 
4149
- export { CONCURRENCY_INSTRUMENTATION, MmActivity, MmTransition, MmViewTransitionName, OP_PROTO_VERSION, PAUSABLE_OPTIONS, PERSISTED_STORE_OPTIONS, SuspenseBoundary, SuspenseBoundaryBase, UnscopedSuspenseBoundary, activeTransaction, applyOps, batteryStatus, bridgeScopeToPendingTasks, chunked, clipboard, combineWith, compareHlc, compareTotal, createAttributedPending, createConvergingApply, createForwardingScope, createHlcClock, createStoreContext, createTransaction, createTransitionScope, debounce, debounced, deferredValue, derived, diffOps, distinct, elementSize, elementVisibility, extendStore, filter, filterWith, focusWithin, forkStore, geolocation, getTransitionScope, holdUntilReady, idle, indexArray, injectPaused, injectRegisterResource, injectStartTransaction, injectStartTransition, injectTransitionScope, invertBatch, isConflicted, isDerivation, isLeaf, isMutable, isOpaque, isStore, keepPrevious, keyArray, keyedArray, latest, lww, map, mapArray, mapObject, mediaQuery, merge3, mergeThree, mousePosition, mutable, mutableStore, nestedEffect, networkStatus, opLog, opSync, opaque, orientation, pageVisibility, pairwise, pausableComputed, pausableEffect, pausableSignal, perfCustomTracks, persist, persistedStore, pipeable, piped, pointerDrag, policyStrategy, pooled, pooledArray, pooledMap, pooledSet, prefersDarkMode, prefersReducedMotion, preserve, projection, provideConcurrencyInstrumentation, provideForwardingTransitionScope, providePausableOptions, providePaused, providePersistedStoreOptions, provideTransitionScope, rebaseOps, reconcile, registerResource, resolvePause, scan, scrollPosition, select, sensor, sensors, signalFromEvent, startWith, store, storeHistory, stored, tabSync, tap, throttle, throttled, toFakeDerivation, toFakeSignalDerivation, toStore, toWritable, until, use, windowSize, withHistory };
4150
- export type { AsyncStore, BatteryStatus, ClipboardSignal, Computation, ConcurrencyInstrumentation, Conflicted, ConvergingApply, CreateChunkedOptions, CreateDebouncedOptions, CreateHistoryOptions, CreateLatestOptions, CreateOpLogOptions, CreatePooledOptions, CreateProvidedPooledOptions, CreateStoredOptions, CreateThrottledOptions, CreateTransitionScopeOptions, DebouncedSignal, DeferStrategy, DeferredSignal, DeferredValueOptions, DerivedSignal, ElementSize, ElementSizeOptions, ElementSizeSignal, ElementVisibilityOptions, ElementVisibilitySignal, ExtendStoreOptions, Fork, ForkStoreOptions, ForkStrategy, ForwardingTransitionScope, Frame, GeolocationOptions, GeolocationSignal, Hlc, HlcClock, IdleOptions, IdleSignal, LatestSignal, MergeContext, MergeFn, MergePolicyEntry, MmTransitionContext, MousePositionOptions, MousePositionSignal, MutableSignal, MutableSignalStore, NetworkStatusSignal, OpBatch, OpEnvelope, OpLog, OpLogDriver, OpSync, OpSyncOptions, Opaque, PausableOptions, PauseOption, PersistHandle, PersistOptions, PersistedStore, PersistedStoreDefaults, PersistedStoreOptions, PipeableSignal, PointerDragOptions, PointerDragSignal, PointerDragState, PointerModifiers, PointerPoint, ProjectionOptions, RebaseResult, ReconcileFn, ReconcileKey, RegisterOptions, ResourceLike, ScreenOrientation, ScreenOrientationState, ScrollPosition, ScrollPositionOptions, ScrollPositionSignal, SensorRunOptions, SignalFromEventOptions, SignalStore, SignalWithHistory, StoreHistory, StoreHistoryOptions, StoreOp, StoreOptions, StoreTabSyncOptions, StoredSignal, SuspendType, SyncSignalOptions, TabSyncBus, ThrottledSignal, Transaction, TransactionRef, TransitionRef, TransitionScope, UntilOptions, UseSource, Vivify, WindowSize, WindowSizeOptions, WindowSizeSignal, WithVivify, WritableSignalStore, toStoreOptions };
4484
+ export { CONCURRENCY_INSTRUMENTATION, MmActivity, MmTransition, MmViewTransitionName, OP_PROTO_VERSION, PAUSABLE_OPTIONS, PERSISTED_STORE_OPTIONS, POS_SEGMENT, SuspenseBoundary, SuspenseBoundaryBase, UnscopedSuspenseBoundary, activeTransaction, applyOps, batteryStatus, bridgeScopeToPendingTasks, chunked, clipboard, combineWith, compareHlc, compareSiblings, compareTotal, createAttributedPending, createConvergingApply, createForwardingScope, createHlcClock, createStoreContext, createTransaction, createTransitionScope, debounce, debounced, defaultFold, deferredValue, derived, diffOps, distinct, elementSize, elementVisibility, extendStore, filter, filterWith, focusWithin, forkStore, geolocation, getTransitionScope, holdUntilReady, idle, indexArray, injectPaused, injectRegisterResource, injectStartTransaction, injectStartTransition, injectTransitionScope, insertElement, invertBatch, isConflicted, isDerivation, isLeaf, isMutable, isOpaque, isStore, keepPrevious, keyArray, keyedArray, latest, lww, map, mapArray, mapObject, mediaQuery, merge3, mergeThree, mousePosition, moveElement, mutable, mutableStore, nestedEffect, networkStatus, opLog, opSync, opaque, orderedEntries, orientation, pageVisibility, pairwise, pausableComputed, pausableEffect, pausableSignal, perfCustomTracks, persist, persistedStore, pipeable, piped, pointerDrag, policyStrategy, pooled, pooledArray, pooledMap, pooledSet, posBetween, prefersDarkMode, prefersReducedMotion, preserve, projection, provideConcurrencyInstrumentation, provideForwardingTransitionScope, providePausableOptions, providePaused, providePersistedStoreOptions, provideTransitionScope, rebalanceContainer, rebaseOps, reconcile, registerResource, removeElement, resolvePause, scan, scrollPosition, select, sensor, sensors, signalFromEvent, startWith, store, storeHistory, stored, syncedFork, tabSync, tap, throttle, throttled, toFakeDerivation, toFakeSignalDerivation, toStore, toWritable, until, use, validateEnvelope, windowSize, withHistory };
4485
+ export type { AsyncStore, BatteryStatus, ClipboardSignal, Computation, ConcurrencyInstrumentation, Conflicted, ContainerNode, ConvergingApply, CreateChunkedOptions, CreateDebouncedOptions, CreateHistoryOptions, CreateLatestOptions, CreateOpLogOptions, CreatePooledOptions, CreateProvidedPooledOptions, CreateStoredOptions, CreateThrottledOptions, CreateTransitionScopeOptions, DebouncedSignal, DeferStrategy, DeferredSignal, DeferredValueOptions, DerivedSignal, Dot, DotFrontier, ElementSize, ElementSizeOptions, ElementSizeSignal, ElementVisibilityOptions, ElementVisibilitySignal, ExtendStoreOptions, FoldFn, FoldPolicyEntry, FoldResult, Fork, ForkStoreOptions, ForkStrategy, ForwardingTransitionScope, Frame, GeolocationOptions, GeolocationSignal, Hlc, HlcClock, IdleOptions, IdleSignal, LatestSignal, MergeContext, MergeFn, MergePolicyEntry, MmTransitionContext, MousePositionOptions, MousePositionSignal, MutableSignal, MutableSignalStore, NetworkStatusSignal, OpBatch, OpEnvelope, OpLog, OpLogDriver, OpSync, OpSyncCheckpoint, OpSyncOptions, Opaque, OrderedEntry, PausableOptions, PauseOption, PersistHandle, PersistOptions, PersistedStore, PersistedStoreDefaults, PersistedStoreOptions, PipeableSignal, PointerDragOptions, PointerDragSignal, PointerDragState, PointerModifiers, PointerPoint, ProjectionOptions, RebaseResult, ReconcileFn, ReconcileKey, RegisterCheckpoint, RegisterOptions, ResourceLike, ScreenOrientation, ScreenOrientationState, ScrollPosition, ScrollPositionOptions, ScrollPositionSignal, SensorRunOptions, SignalFromEventOptions, SignalStore, SignalWithHistory, StoreHistory, StoreHistoryOptions, StoreOp, StoreOptions, StoreTabSyncOptions, StoredSignal, SuspendType, SyncOp, SyncSibling, SyncSignalOptions, SyncedFork, TabSyncBus, ThrottledSignal, Transaction, TransactionRef, TransitionRef, TransitionScope, UntilOptions, UseSource, Vivify, WindowSize, WindowSizeOptions, WindowSizeSignal, WithVivify, WritableSignalStore, toStoreOptions };